alggeom 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- alggeom/__init__.py +75 -0
- alggeom/_algebra.py +57 -0
- alggeom/_monomials.py +60 -0
- alggeom/absolute_factorization.py +377 -0
- alggeom/chevalley.py +166 -0
- alggeom/components.py +316 -0
- alggeom/comprehensive_strata.py +301 -0
- alggeom/constructible.py +359 -0
- alggeom/cover_proofs.py +495 -0
- alggeom/curves.py +164 -0
- alggeom/decomposition/__init__.py +23 -0
- alggeom/decomposition/_api.py +97 -0
- alggeom/decomposition/_validation.py +88 -0
- alggeom/decomposition/algebraic.py +315 -0
- alggeom/decomposition/chains.py +767 -0
- alggeom/decomposition/common.py +84 -0
- alggeom/decomposition/discovery.py +183 -0
- alggeom/decomposition/factors.py +442 -0
- alggeom/decomposition/parameters.py +556 -0
- alggeom/decomposition/rational.py +358 -0
- alggeom/decomposition/real.py +207 -0
- alggeom/families.py +168 -0
- alggeom/fields.py +23 -0
- alggeom/function_field_constants.py +508 -0
- alggeom/geometric.py +1061 -0
- alggeom/graded.py +167 -0
- alggeom/groebner_certificates.py +212 -0
- alggeom/groebner_cover.py +445 -0
- alggeom/ideals.py +602 -0
- alggeom/linear_strata.py +134 -0
- alggeom/local.py +234 -0
- alggeom/localized_linear_cover.py +258 -0
- alggeom/maps.py +323 -0
- alggeom/modules.py +629 -0
- alggeom/morphism_invariants.py +149 -0
- alggeom/noether.py +224 -0
- alggeom/nonlinear_strata.py +262 -0
- alggeom/normalization.py +350 -0
- alggeom/parameter_cover.py +186 -0
- alggeom/parameter_fields.py +428 -0
- alggeom/parameter_localization.py +222 -0
- alggeom/points.py +114 -0
- alggeom/projective_normalization.py +192 -0
- alggeom/properties.py +80 -0
- alggeom/rational_functions.py +391 -0
- alggeom/resolutions.py +252 -0
- alggeom/rings.py +296 -0
- alggeom/schemes.py +971 -0
- alggeom-0.1.0.dist-info/METADATA +164 -0
- alggeom-0.1.0.dist-info/RECORD +53 -0
- alggeom-0.1.0.dist-info/WHEEL +5 -0
- alggeom-0.1.0.dist-info/licenses/LICENSE +674 -0
- alggeom-0.1.0.dist-info/top_level.txt +1 -0
alggeom/__init__.py
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""Exact computational algebraic geometry over characteristic-zero and finite fields."""
|
|
2
|
+
|
|
3
|
+
from . import (
|
|
4
|
+
absolute_factorization,
|
|
5
|
+
chevalley,
|
|
6
|
+
components,
|
|
7
|
+
comprehensive_strata,
|
|
8
|
+
constructible,
|
|
9
|
+
curves,
|
|
10
|
+
decomposition,
|
|
11
|
+
families,
|
|
12
|
+
fields,
|
|
13
|
+
function_field_constants,
|
|
14
|
+
geometric,
|
|
15
|
+
graded,
|
|
16
|
+
groebner_certificates,
|
|
17
|
+
groebner_cover,
|
|
18
|
+
ideals,
|
|
19
|
+
linear_strata,
|
|
20
|
+
local,
|
|
21
|
+
localized_linear_cover,
|
|
22
|
+
maps,
|
|
23
|
+
modules,
|
|
24
|
+
morphism_invariants,
|
|
25
|
+
noether,
|
|
26
|
+
nonlinear_strata,
|
|
27
|
+
normalization,
|
|
28
|
+
parameter_cover,
|
|
29
|
+
parameter_fields,
|
|
30
|
+
parameter_localization,
|
|
31
|
+
points,
|
|
32
|
+
projective_normalization,
|
|
33
|
+
properties,
|
|
34
|
+
rational_functions,
|
|
35
|
+
resolutions,
|
|
36
|
+
rings,
|
|
37
|
+
schemes,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
__all__ = [
|
|
41
|
+
"absolute_factorization",
|
|
42
|
+
"chevalley",
|
|
43
|
+
"components",
|
|
44
|
+
"comprehensive_strata",
|
|
45
|
+
"constructible",
|
|
46
|
+
"curves",
|
|
47
|
+
"decomposition",
|
|
48
|
+
"families",
|
|
49
|
+
"fields",
|
|
50
|
+
"function_field_constants",
|
|
51
|
+
"geometric",
|
|
52
|
+
"graded",
|
|
53
|
+
"groebner_certificates",
|
|
54
|
+
"groebner_cover",
|
|
55
|
+
"ideals",
|
|
56
|
+
"linear_strata",
|
|
57
|
+
"local",
|
|
58
|
+
"localized_linear_cover",
|
|
59
|
+
"maps",
|
|
60
|
+
"modules",
|
|
61
|
+
"morphism_invariants",
|
|
62
|
+
"noether",
|
|
63
|
+
"nonlinear_strata",
|
|
64
|
+
"normalization",
|
|
65
|
+
"parameter_cover",
|
|
66
|
+
"parameter_fields",
|
|
67
|
+
"parameter_localization",
|
|
68
|
+
"points",
|
|
69
|
+
"projective_normalization",
|
|
70
|
+
"properties",
|
|
71
|
+
"rational_functions",
|
|
72
|
+
"resolutions",
|
|
73
|
+
"rings",
|
|
74
|
+
"schemes",
|
|
75
|
+
]
|
alggeom/_algebra.py
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""Shared exact polynomial arithmetic and coordinate validation."""
|
|
2
|
+
|
|
3
|
+
import sympy as sp
|
|
4
|
+
from algroots.quotient import _require_quotient_domain
|
|
5
|
+
|
|
6
|
+
exact_field = _require_quotient_domain
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def coordinates(values, *, allow_empty=True):
|
|
10
|
+
values = (values,) if isinstance(values, sp.Symbol) else tuple(values)
|
|
11
|
+
if (
|
|
12
|
+
(not values and not allow_empty)
|
|
13
|
+
or len(set(values)) != len(values)
|
|
14
|
+
or any(not isinstance(v, sp.Symbol) or v.is_commutative is not True for v in values)
|
|
15
|
+
):
|
|
16
|
+
raise ValueError("distinct commutative Symbol coordinates are required")
|
|
17
|
+
return values
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def integer(value, name, minimum=0):
|
|
21
|
+
if (
|
|
22
|
+
isinstance(value, bool)
|
|
23
|
+
or not isinstance(value, (int, sp.Integer))
|
|
24
|
+
or (minimum is not None and value < minimum)
|
|
25
|
+
):
|
|
26
|
+
constraint = "" if minimum is None else f" >= {minimum}"
|
|
27
|
+
raise ValueError(f"{name} must be an integer{constraint}")
|
|
28
|
+
return int(value)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def visible_denominators(expression):
|
|
32
|
+
"""Find affine denominator exclusions before rational cancellation."""
|
|
33
|
+
expression = sp.sympify(expression)
|
|
34
|
+
denominators = [sp.together(expression).as_numer_denom()[1]]
|
|
35
|
+
for term in sp.preorder_traversal(expression):
|
|
36
|
+
if term.is_Pow and term.exp.is_Integer and term.exp < 0:
|
|
37
|
+
denominators.extend(sp.together(term.base).as_numer_denom())
|
|
38
|
+
return tuple(dict.fromkeys(d for d in denominators if d != 1))
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def rational_parts(expression, ring):
|
|
42
|
+
"""Reduce a rational expression over the declared coordinate and coefficient fields."""
|
|
43
|
+
expression = expression.as_expr() if isinstance(expression, sp.Poly) else sp.sympify(expression)
|
|
44
|
+
if expression.has(sp.Float):
|
|
45
|
+
raise ValueError("exact rational functions are required")
|
|
46
|
+
numerator, denominator = sp.together(expression).as_numer_denom()
|
|
47
|
+
left, right = ring._poly(numerator), ring._poly(denominator)
|
|
48
|
+
if not right:
|
|
49
|
+
raise ZeroDivisionError("a rational function has a zero denominator")
|
|
50
|
+
common = left.gcd(right)
|
|
51
|
+
left, right = left.exquo(common), right.exquo(common)
|
|
52
|
+
scale = right.LC
|
|
53
|
+
domain = right.ring.domain
|
|
54
|
+
return left.mul_ground(domain.exquo(domain.one, scale)), right.monic()
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
__all__ = []
|
alggeom/_monomials.py
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""Exact combinatorics of monomial ideals."""
|
|
2
|
+
|
|
3
|
+
from functools import cache
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def minimal_exponents(exponents):
|
|
7
|
+
"""Return the minimal monomial generators under componentwise divisibility."""
|
|
8
|
+
values = tuple(sorted(set(tuple(e) for e in exponents)))
|
|
9
|
+
return tuple(
|
|
10
|
+
candidate
|
|
11
|
+
for candidate in values
|
|
12
|
+
if not any(
|
|
13
|
+
divisor != candidate and all(a <= b for a, b in zip(divisor, candidate, strict=True))
|
|
14
|
+
for divisor in values
|
|
15
|
+
)
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def dimension(exponents, variable_count):
|
|
20
|
+
"""Compute dimension as n minus the minimum vertex cover of monomial supports.
|
|
21
|
+
|
|
22
|
+
A coordinate prime contains a monomial precisely when it contains one of
|
|
23
|
+
its variables. Branch on one support: every covering prime must choose at
|
|
24
|
+
least one of those variables. Memoization shares the residual subproblems.
|
|
25
|
+
"""
|
|
26
|
+
exponents = minimal_exponents(exponents)
|
|
27
|
+
supports = frozenset(frozenset(i for i, power in enumerate(e) if power) for e in exponents)
|
|
28
|
+
if frozenset() in supports:
|
|
29
|
+
return -1
|
|
30
|
+
|
|
31
|
+
@cache
|
|
32
|
+
def height(edges):
|
|
33
|
+
if not edges:
|
|
34
|
+
return 0
|
|
35
|
+
edge = min(edges, key=lambda e: (len(e), tuple(sorted(e))))
|
|
36
|
+
return min(1 + height(frozenset(e for e in edges if v not in e)) for v in edge)
|
|
37
|
+
|
|
38
|
+
return variable_count - height(supports)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def hilbert_numerator(exponents, variable_count):
|
|
42
|
+
"""Return coefficients of the monomial quotient's Hilbert numerator by lcm degree.
|
|
43
|
+
|
|
44
|
+
Inclusion-exclusion over divisibility cones is combined on the lcm lattice,
|
|
45
|
+
avoiding a separate term for every subset when many lcms coincide.
|
|
46
|
+
"""
|
|
47
|
+
lattice = {(0,) * variable_count: 1}
|
|
48
|
+
for generator in minimal_exponents(exponents):
|
|
49
|
+
updates = {}
|
|
50
|
+
for monomial, coefficient in lattice.items():
|
|
51
|
+
lcm = tuple(max(a, b) for a, b in zip(monomial, generator, strict=True))
|
|
52
|
+
updates[lcm] = updates.get(lcm, 0) - coefficient
|
|
53
|
+
for monomial, coefficient in updates.items():
|
|
54
|
+
lattice[monomial] = lattice.get(monomial, 0) + coefficient
|
|
55
|
+
lattice = {m: c for m, c in lattice.items() if c}
|
|
56
|
+
coefficients = {}
|
|
57
|
+
for monomial, coefficient in lattice.items():
|
|
58
|
+
degree = sum(monomial)
|
|
59
|
+
coefficients[degree] = coefficients.get(degree, 0) + coefficient
|
|
60
|
+
return {d: c for d, c in coefficients.items() if c}
|
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
"""Absolute factorization of bivariate polynomials using logarithmic derivatives.
|
|
2
|
+
|
|
3
|
+
For a squarefree polynomial f in QQ[x, y], the space of closed rational
|
|
4
|
+
one-forms with poles along f and bounded bidegrees has one dimension per
|
|
5
|
+
geometric irreducible factor. Its multiplication algebra separates factors
|
|
6
|
+
using exact rational linear algebra and algebraic eigenvalues. Every factor
|
|
7
|
+
is recovered by polynomial gcd and checked by exact reconstruction.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
|
|
12
|
+
import sympy as sp
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class FactorizationLimitError(NotImplementedError):
|
|
16
|
+
"""Exact factor reconstruction exceeds a declared algebraic complexity limit."""
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass(frozen=True)
|
|
20
|
+
class AbsoluteFactors:
|
|
21
|
+
"""Geometrically irreducible factors and their multiplicities over a number field."""
|
|
22
|
+
|
|
23
|
+
factors: tuple[tuple[sp.Expr, int], ...]
|
|
24
|
+
field: object
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _closed_forms(polynomial, variables, max_unknowns):
|
|
28
|
+
"""Solve the logarithmic-derivative equations with bounded numerators."""
|
|
29
|
+
x, y = variables
|
|
30
|
+
degree_x = sp.degree(polynomial, x)
|
|
31
|
+
degree_y = sp.degree(polynomial, y)
|
|
32
|
+
if min(degree_x, degree_y) < 1:
|
|
33
|
+
raise ValueError("the polynomial must involve both variables")
|
|
34
|
+
exponents_x = [(i, j) for i in range(degree_x) for j in range(degree_y + 1)]
|
|
35
|
+
exponents_y = [(i, j) for i in range(degree_x + 1) for j in range(degree_y)]
|
|
36
|
+
total = len(exponents_x) + len(exponents_y)
|
|
37
|
+
if total > max_unknowns:
|
|
38
|
+
raise FactorizationLimitError("logarithmic form system exceeds max_unknowns")
|
|
39
|
+
coefficients = sp.symbols(f"form_0:{total}")
|
|
40
|
+
numerator_x = sum(
|
|
41
|
+
coefficient * x**i * y**j
|
|
42
|
+
for coefficient, (i, j) in zip(coefficients, exponents_x, strict=False)
|
|
43
|
+
)
|
|
44
|
+
numerator_y = sum(
|
|
45
|
+
coefficient * x**i * y**j
|
|
46
|
+
for coefficient, (i, j) in zip(coefficients[len(exponents_x) :], exponents_y, strict=False)
|
|
47
|
+
)
|
|
48
|
+
closed = sp.Poly(
|
|
49
|
+
sp.expand(
|
|
50
|
+
polynomial * (sp.diff(numerator_x, y) - sp.diff(numerator_y, x))
|
|
51
|
+
- numerator_x * sp.diff(polynomial, y)
|
|
52
|
+
+ numerator_y * sp.diff(polynomial, x)
|
|
53
|
+
),
|
|
54
|
+
x,
|
|
55
|
+
y,
|
|
56
|
+
)
|
|
57
|
+
matrix, _ = sp.linear_eq_to_matrix(closed.coeffs(), coefficients)
|
|
58
|
+
return tuple(
|
|
59
|
+
(
|
|
60
|
+
sp.expand(numerator_x.subs(dict(zip(coefficients, vector, strict=True)))),
|
|
61
|
+
sp.expand(numerator_y.subs(dict(zip(coefficients, vector, strict=True)))),
|
|
62
|
+
)
|
|
63
|
+
for vector in matrix.nullspace()
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _multiplication_matrices(polynomial, variables, forms):
|
|
68
|
+
"""Construct the étale residue algebra of the logarithmic-derivative space."""
|
|
69
|
+
x, y = variables
|
|
70
|
+
for shift in range(len(forms) + 2):
|
|
71
|
+
derivative = sp.diff(polynomial, x) + shift * sp.diff(polynomial, y)
|
|
72
|
+
if sp.gcd(polynomial, derivative) == 1:
|
|
73
|
+
break
|
|
74
|
+
else:
|
|
75
|
+
raise ArithmeticError("no derivative separates all geometric branches")
|
|
76
|
+
numerators = tuple(first + shift * second for first, second in forms)
|
|
77
|
+
groebner = sp.groebner((polynomial,), x, y, order="lex", domain=sp.QQ)
|
|
78
|
+
remainder_cache = {}
|
|
79
|
+
|
|
80
|
+
def reduced(expression):
|
|
81
|
+
terms = sp.Poly(expression, x, y).terms()
|
|
82
|
+
result = 0
|
|
83
|
+
for exponents, coefficient in terms:
|
|
84
|
+
if exponents not in remainder_cache:
|
|
85
|
+
monomial = x ** exponents[0] * y ** exponents[1]
|
|
86
|
+
remainder_cache[exponents] = groebner.reduce(monomial)[1]
|
|
87
|
+
result += coefficient * remainder_cache[exponents]
|
|
88
|
+
return sp.Poly(result, x, y)
|
|
89
|
+
|
|
90
|
+
dimension = len(forms)
|
|
91
|
+
unknowns = sp.symbols(f"residue_0:{dimension}")
|
|
92
|
+
candidate = derivative * sum(
|
|
93
|
+
unknown * numerator for unknown, numerator in zip(unknowns, numerators, strict=True)
|
|
94
|
+
)
|
|
95
|
+
matrices = []
|
|
96
|
+
for left in numerators:
|
|
97
|
+
columns = []
|
|
98
|
+
for right in numerators:
|
|
99
|
+
equations = reduced(left * right - candidate).coeffs()
|
|
100
|
+
solutions = sp.linsolve(equations, unknowns)
|
|
101
|
+
if solutions is sp.EmptySet:
|
|
102
|
+
raise ArithmeticError("logarithmic residue multiplication is not closed")
|
|
103
|
+
answer = next(iter(solutions))
|
|
104
|
+
if any(value.free_symbols & set(unknowns) for value in answer):
|
|
105
|
+
raise ArithmeticError("logarithmic residue multiplication is not unique")
|
|
106
|
+
columns.append(answer)
|
|
107
|
+
matrices.append(sp.Matrix.hstack(*(sp.Matrix(column) for column in columns)))
|
|
108
|
+
return tuple(matrices), numerators, derivative
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _squarefree_factors(polynomial, variables, max_unknowns, max_roots):
|
|
112
|
+
forms = _closed_forms(polynomial, variables, max_unknowns)
|
|
113
|
+
dimension = len(forms)
|
|
114
|
+
if dimension == 1:
|
|
115
|
+
return (polynomial,), sp.QQ
|
|
116
|
+
if dimension < 1 or dimension > max_roots:
|
|
117
|
+
raise FactorizationLimitError("absolute factor count exceeds max_roots")
|
|
118
|
+
matrices, numerators, derivative = _multiplication_matrices(polynomial, variables, forms)
|
|
119
|
+
candidates = (
|
|
120
|
+
tuple(range(1, dimension + 1)),
|
|
121
|
+
tuple(2**i for i in range(dimension)),
|
|
122
|
+
tuple(i**2 + 1 for i in range(dimension)),
|
|
123
|
+
)
|
|
124
|
+
for weights in candidates:
|
|
125
|
+
operator = sp.zeros(dimension)
|
|
126
|
+
for weight, matrix in zip(weights, matrices, strict=True):
|
|
127
|
+
operator += weight * matrix
|
|
128
|
+
characteristic = sp.Poly(operator.charpoly().as_expr(), domain=sp.QQ)
|
|
129
|
+
if sp.gcd(characteristic, characteristic.diff()).degree() == 0:
|
|
130
|
+
break
|
|
131
|
+
else:
|
|
132
|
+
raise FactorizationLimitError("no separating element found in the residue algebra")
|
|
133
|
+
roots = sp.roots(characteristic.as_expr(), characteristic.gen)
|
|
134
|
+
if len(roots) != dimension or any(multiplicity != 1 for multiplicity in roots.values()):
|
|
135
|
+
raise FactorizationLimitError("algebraic residue eigenvalues cannot be represented exactly")
|
|
136
|
+
field = (
|
|
137
|
+
sp.QQ.algebraic_field(*roots)
|
|
138
|
+
if any(value.is_Rational is not True for value in roots)
|
|
139
|
+
else sp.QQ
|
|
140
|
+
)
|
|
141
|
+
numerator = sum(weight * value for weight, value in zip(weights, numerators, strict=True))
|
|
142
|
+
x, y = variables
|
|
143
|
+
exact = sp.Poly(polynomial, x, y, domain=field)
|
|
144
|
+
factors = []
|
|
145
|
+
for eigenvalue in roots:
|
|
146
|
+
polynomial_value = sp.Poly(numerator - eigenvalue * derivative, x, y, domain=field)
|
|
147
|
+
factor = sp.gcd(exact, polynomial_value).monic()
|
|
148
|
+
if factor.total_degree() < 1:
|
|
149
|
+
raise ArithmeticError("a residue eigenvalue did not recover an algebraic component")
|
|
150
|
+
factors.append(factor.as_expr())
|
|
151
|
+
reconstructed = sp.Poly(sp.prod(factors), x, y, domain=field)
|
|
152
|
+
quotient = exact.exquo(reconstructed)
|
|
153
|
+
if quotient.total_degree() != 0 or quotient.is_zero:
|
|
154
|
+
raise ArithmeticError("absolute factors did not reconstruct the original polynomial")
|
|
155
|
+
return tuple(factors), field
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def absolute_factor_bivariate(expression, variables, *, max_unknowns=100, max_roots=12):
|
|
159
|
+
"""Factor a bivariate QQ polynomial into certified geometric factors.
|
|
160
|
+
|
|
161
|
+
Multiplicities are preserved. Factors are represented over a common exact
|
|
162
|
+
number field. The method uses logarithmic derivatives and characteristic-
|
|
163
|
+
zero residue-algebra splitting; it does not rely on factorization over a
|
|
164
|
+
generic rational-function field, which could split a geometrically prime
|
|
165
|
+
polynomial incorrectly.
|
|
166
|
+
"""
|
|
167
|
+
variables = tuple(variables)
|
|
168
|
+
if len(variables) != 2 or any(not isinstance(var, sp.Symbol) for var in variables):
|
|
169
|
+
raise ValueError("two polynomial variables are required")
|
|
170
|
+
polynomial = sp.Poly(expression, *variables, domain=sp.QQ)
|
|
171
|
+
if polynomial.total_degree() <= 0:
|
|
172
|
+
raise ValueError("a nonconstant polynomial is required")
|
|
173
|
+
factors = []
|
|
174
|
+
extensions = []
|
|
175
|
+
for rational_factor, multiplicity in polynomial.factor_list()[1]:
|
|
176
|
+
factor = rational_factor.as_expr()
|
|
177
|
+
if any(sp.degree(factor, coordinate) == 0 for coordinate in variables):
|
|
178
|
+
# A univariate polynomial may require algebraic roots beyond radicals.
|
|
179
|
+
coordinate = next(var for var in variables if sp.degree(factor, var) > 0)
|
|
180
|
+
roots = sp.roots(factor, coordinate)
|
|
181
|
+
if sum(roots.values()) != sp.degree(factor, coordinate):
|
|
182
|
+
raise FactorizationLimitError("a univariate factor has unresolved algebraic roots")
|
|
183
|
+
factors.extend(
|
|
184
|
+
(coordinate - root, multiplicity * power) for root, power in roots.items()
|
|
185
|
+
)
|
|
186
|
+
extensions.extend(root for root in roots if root.is_Rational is not True)
|
|
187
|
+
continue
|
|
188
|
+
geometric_factors, field = _squarefree_factors(factor, variables, max_unknowns, max_roots)
|
|
189
|
+
factors.extend((part, multiplicity) for part in geometric_factors)
|
|
190
|
+
extensions.extend(getattr(field, "orig_ext", ()))
|
|
191
|
+
domain = sp.QQ.algebraic_field(*extensions) if extensions else sp.QQ
|
|
192
|
+
product = sp.Poly(
|
|
193
|
+
sp.prod(part**multiplicity for part, multiplicity in factors),
|
|
194
|
+
*variables,
|
|
195
|
+
domain=domain,
|
|
196
|
+
)
|
|
197
|
+
residual = sp.Poly(expression, *variables, domain=domain).exquo(product)
|
|
198
|
+
if residual.total_degree() != 0 or residual.is_zero:
|
|
199
|
+
raise ArithmeticError("the geometric factorization failed reconstruction")
|
|
200
|
+
return AbsoluteFactors(tuple(factors), domain)
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def absolute_factor_hypersurface(expression, variables, *, max_unknowns=90, max_roots=12):
|
|
204
|
+
"""Factor a rational-coefficient multivariate hypersurface geometrically.
|
|
205
|
+
|
|
206
|
+
For two variables the optimized bivariate backend is used. In higher
|
|
207
|
+
dimensions, solve the closed-form equations for logarithmic differentials
|
|
208
|
+
in all coordinates, then reconstruct geometric factors from residue
|
|
209
|
+
eigenvalues and exact multivariate gcds. Only characteristic zero and
|
|
210
|
+
rational coefficients are supported; expensive systems raise explicitly.
|
|
211
|
+
"""
|
|
212
|
+
from itertools import product
|
|
213
|
+
|
|
214
|
+
variables = tuple(variables)
|
|
215
|
+
if len(variables) == 2:
|
|
216
|
+
return absolute_factor_bivariate(
|
|
217
|
+
expression, variables, max_unknowns=max_unknowns, max_roots=max_roots
|
|
218
|
+
)
|
|
219
|
+
if len(variables) < 2:
|
|
220
|
+
raise ValueError("at least two variables are required")
|
|
221
|
+
polynomial = sp.Poly(expression, *variables, domain=sp.QQ)
|
|
222
|
+
if polynomial.total_degree() <= 0:
|
|
223
|
+
raise ValueError("a nonconstant polynomial is required")
|
|
224
|
+
|
|
225
|
+
def split_squarefree(factor):
|
|
226
|
+
degrees = tuple(sp.degree(factor, variable) for variable in variables)
|
|
227
|
+
supports = []
|
|
228
|
+
for coordinate in range(len(variables)):
|
|
229
|
+
if not degrees[coordinate]:
|
|
230
|
+
supports.append(())
|
|
231
|
+
continue
|
|
232
|
+
ranges = (
|
|
233
|
+
range(degree + 1 - int(index == coordinate)) for index, degree in enumerate(degrees)
|
|
234
|
+
)
|
|
235
|
+
supports.append(tuple(product(*ranges)))
|
|
236
|
+
unknown_count = sum(len(support) for support in supports)
|
|
237
|
+
if unknown_count > max_unknowns:
|
|
238
|
+
raise FactorizationLimitError("multivariate residue system exceeds max_unknowns")
|
|
239
|
+
unknowns = sp.symbols(f"form_0:{unknown_count}")
|
|
240
|
+
numerators = []
|
|
241
|
+
offset = 0
|
|
242
|
+
for support in supports:
|
|
243
|
+
numerator = sum(
|
|
244
|
+
unknowns[offset + position]
|
|
245
|
+
* sp.prod(
|
|
246
|
+
variable**power for variable, power in zip(variables, exponents, strict=True)
|
|
247
|
+
)
|
|
248
|
+
for position, exponents in enumerate(support)
|
|
249
|
+
)
|
|
250
|
+
numerators.append(sp.sympify(numerator))
|
|
251
|
+
offset += len(support)
|
|
252
|
+
equations = []
|
|
253
|
+
for first in range(len(variables)):
|
|
254
|
+
for second in range(first + 1, len(variables)):
|
|
255
|
+
closed = sp.expand(
|
|
256
|
+
factor
|
|
257
|
+
* (
|
|
258
|
+
sp.diff(numerators[first], variables[second])
|
|
259
|
+
- sp.diff(numerators[second], variables[first])
|
|
260
|
+
)
|
|
261
|
+
- numerators[first] * sp.diff(factor, variables[second])
|
|
262
|
+
+ numerators[second] * sp.diff(factor, variables[first])
|
|
263
|
+
)
|
|
264
|
+
equations.extend(sp.Poly(closed, *variables).coeffs())
|
|
265
|
+
matrix, _ = sp.linear_eq_to_matrix(equations, unknowns)
|
|
266
|
+
forms = tuple(
|
|
267
|
+
tuple(
|
|
268
|
+
sp.expand(poly.subs(dict(zip(unknowns, vector, strict=True))))
|
|
269
|
+
for poly in numerators
|
|
270
|
+
)
|
|
271
|
+
for vector in matrix.nullspace()
|
|
272
|
+
)
|
|
273
|
+
count = len(forms)
|
|
274
|
+
if count == 1:
|
|
275
|
+
return (factor,), sp.QQ
|
|
276
|
+
if count < 1 or count > max_roots:
|
|
277
|
+
raise FactorizationLimitError("absolute factor count exceeds max_roots")
|
|
278
|
+
for shift in range(1, count + 4):
|
|
279
|
+
derivative = sum(
|
|
280
|
+
shift**index * sp.diff(factor, variable) for index, variable in enumerate(variables)
|
|
281
|
+
)
|
|
282
|
+
if sp.gcd(factor, derivative) == 1:
|
|
283
|
+
break
|
|
284
|
+
else:
|
|
285
|
+
raise ArithmeticError("no derivative is nonzero on every factor")
|
|
286
|
+
values = tuple(
|
|
287
|
+
sum(shift**index * component for index, component in enumerate(form)) for form in forms
|
|
288
|
+
)
|
|
289
|
+
basis = sp.groebner((factor,), *variables, order="lex", domain=sp.QQ)
|
|
290
|
+
remainder_cache = {}
|
|
291
|
+
|
|
292
|
+
def reduced(expression):
|
|
293
|
+
result = 0
|
|
294
|
+
for exponents, coefficient in sp.Poly(expression, *variables).terms():
|
|
295
|
+
if exponents not in remainder_cache:
|
|
296
|
+
monomial = sp.prod(
|
|
297
|
+
symbol**power for symbol, power in zip(variables, exponents, strict=True)
|
|
298
|
+
)
|
|
299
|
+
remainder_cache[exponents] = basis.reduce(monomial)[1]
|
|
300
|
+
result += coefficient * remainder_cache[exponents]
|
|
301
|
+
return sp.Poly(result, *variables)
|
|
302
|
+
|
|
303
|
+
coefficients = sp.symbols(f"residue_0:{count}")
|
|
304
|
+
candidate = derivative * sum(
|
|
305
|
+
coefficient * value for coefficient, value in zip(coefficients, values, strict=True)
|
|
306
|
+
)
|
|
307
|
+
matrices = []
|
|
308
|
+
for left in values:
|
|
309
|
+
columns = []
|
|
310
|
+
for right in values:
|
|
311
|
+
solutions = sp.linsolve(reduced(left * right - candidate).coeffs(), coefficients)
|
|
312
|
+
if solutions is sp.EmptySet:
|
|
313
|
+
raise ArithmeticError("residue algebra multiplication failed")
|
|
314
|
+
answer = next(iter(solutions))
|
|
315
|
+
if any(value.free_symbols & set(coefficients) for value in answer):
|
|
316
|
+
raise ArithmeticError("residue algebra product is not unique")
|
|
317
|
+
columns.append(answer)
|
|
318
|
+
matrices.append(sp.Matrix.hstack(*(sp.Matrix(column) for column in columns)))
|
|
319
|
+
for weights in (
|
|
320
|
+
tuple(range(1, count + 1)),
|
|
321
|
+
tuple(2**index for index in range(count)),
|
|
322
|
+
tuple(index**2 + 1 for index in range(count)),
|
|
323
|
+
):
|
|
324
|
+
operator = sum(
|
|
325
|
+
(weight * matrix for weight, matrix in zip(weights, matrices, strict=True)),
|
|
326
|
+
sp.zeros(count),
|
|
327
|
+
)
|
|
328
|
+
characteristic = sp.Poly(operator.charpoly().as_expr(), domain=sp.QQ)
|
|
329
|
+
if sp.gcd(characteristic, characteristic.diff()).degree() == 0:
|
|
330
|
+
break
|
|
331
|
+
else:
|
|
332
|
+
raise FactorizationLimitError("residue algebra has no separating element")
|
|
333
|
+
roots = sp.roots(characteristic.as_expr(), characteristic.gen)
|
|
334
|
+
if len(roots) != count or any(multiplicity != 1 for multiplicity in roots.values()):
|
|
335
|
+
raise FactorizationLimitError("residue eigenvalues cannot be represented exactly")
|
|
336
|
+
field = (
|
|
337
|
+
sp.QQ.algebraic_field(*roots)
|
|
338
|
+
if any(root.is_Rational is not True for root in roots)
|
|
339
|
+
else sp.QQ
|
|
340
|
+
)
|
|
341
|
+
numerator = sum(weight * value for weight, value in zip(weights, values, strict=True))
|
|
342
|
+
exact = sp.Poly(factor, *variables, domain=field)
|
|
343
|
+
factors = tuple(
|
|
344
|
+
sp.gcd(
|
|
345
|
+
exact,
|
|
346
|
+
sp.Poly(numerator - root * derivative, *variables, domain=field),
|
|
347
|
+
)
|
|
348
|
+
.monic()
|
|
349
|
+
.as_expr()
|
|
350
|
+
for root in roots
|
|
351
|
+
)
|
|
352
|
+
reconstructed = sp.Poly(sp.prod(factors), *variables, domain=field)
|
|
353
|
+
if exact.exquo(reconstructed).total_degree() != 0:
|
|
354
|
+
raise ArithmeticError("geometric hypersurface factors failed reconstruction")
|
|
355
|
+
if any(sp.Poly(part, *variables).total_degree() <= 0 for part in factors):
|
|
356
|
+
raise ArithmeticError("residue eigenvalue failed to identify a geometric factor")
|
|
357
|
+
return factors, field
|
|
358
|
+
|
|
359
|
+
result = []
|
|
360
|
+
extensions = []
|
|
361
|
+
for factor, power in polynomial.factor_list()[1]:
|
|
362
|
+
geometric, field = split_squarefree(factor.as_expr())
|
|
363
|
+
result.extend((part, power) for part in geometric)
|
|
364
|
+
extensions.extend(getattr(field, "orig_ext", ()))
|
|
365
|
+
field = sp.QQ.algebraic_field(*extensions) if extensions else sp.QQ
|
|
366
|
+
combined = sp.Poly(sp.prod(part**power for part, power in result), *variables, domain=field)
|
|
367
|
+
if sp.Poly(expression, *variables, domain=field).exquo(combined).total_degree() != 0:
|
|
368
|
+
raise ArithmeticError("absolute hypersurface reconstruction failed")
|
|
369
|
+
return AbsoluteFactors(tuple(result), field)
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
__all__ = [
|
|
373
|
+
"AbsoluteFactors",
|
|
374
|
+
"FactorizationLimitError",
|
|
375
|
+
"absolute_factor_bivariate",
|
|
376
|
+
"absolute_factor_hypersurface",
|
|
377
|
+
]
|