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.
Files changed (53) hide show
  1. alggeom/__init__.py +75 -0
  2. alggeom/_algebra.py +57 -0
  3. alggeom/_monomials.py +60 -0
  4. alggeom/absolute_factorization.py +377 -0
  5. alggeom/chevalley.py +166 -0
  6. alggeom/components.py +316 -0
  7. alggeom/comprehensive_strata.py +301 -0
  8. alggeom/constructible.py +359 -0
  9. alggeom/cover_proofs.py +495 -0
  10. alggeom/curves.py +164 -0
  11. alggeom/decomposition/__init__.py +23 -0
  12. alggeom/decomposition/_api.py +97 -0
  13. alggeom/decomposition/_validation.py +88 -0
  14. alggeom/decomposition/algebraic.py +315 -0
  15. alggeom/decomposition/chains.py +767 -0
  16. alggeom/decomposition/common.py +84 -0
  17. alggeom/decomposition/discovery.py +183 -0
  18. alggeom/decomposition/factors.py +442 -0
  19. alggeom/decomposition/parameters.py +556 -0
  20. alggeom/decomposition/rational.py +358 -0
  21. alggeom/decomposition/real.py +207 -0
  22. alggeom/families.py +168 -0
  23. alggeom/fields.py +23 -0
  24. alggeom/function_field_constants.py +508 -0
  25. alggeom/geometric.py +1061 -0
  26. alggeom/graded.py +167 -0
  27. alggeom/groebner_certificates.py +212 -0
  28. alggeom/groebner_cover.py +445 -0
  29. alggeom/ideals.py +602 -0
  30. alggeom/linear_strata.py +134 -0
  31. alggeom/local.py +234 -0
  32. alggeom/localized_linear_cover.py +258 -0
  33. alggeom/maps.py +323 -0
  34. alggeom/modules.py +629 -0
  35. alggeom/morphism_invariants.py +149 -0
  36. alggeom/noether.py +224 -0
  37. alggeom/nonlinear_strata.py +262 -0
  38. alggeom/normalization.py +350 -0
  39. alggeom/parameter_cover.py +186 -0
  40. alggeom/parameter_fields.py +428 -0
  41. alggeom/parameter_localization.py +222 -0
  42. alggeom/points.py +114 -0
  43. alggeom/projective_normalization.py +192 -0
  44. alggeom/properties.py +80 -0
  45. alggeom/rational_functions.py +391 -0
  46. alggeom/resolutions.py +252 -0
  47. alggeom/rings.py +296 -0
  48. alggeom/schemes.py +971 -0
  49. alggeom-0.1.0.dist-info/METADATA +164 -0
  50. alggeom-0.1.0.dist-info/RECORD +53 -0
  51. alggeom-0.1.0.dist-info/WHEEL +5 -0
  52. alggeom-0.1.0.dist-info/licenses/LICENSE +674 -0
  53. 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
+ ]