algroots 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.
algroots/__init__.py ADDED
@@ -0,0 +1,123 @@
1
+ """Numerical and exact roots of zero-dimensional algebraic equation systems."""
2
+
3
+ from importlib.metadata import PackageNotFoundError, version
4
+
5
+ from .algebraic import AlgebraicSystemRoots, algsolve
6
+ from .algebraization import AlgebraizedSystem, algebraize_system
7
+ from .border_basis import (
8
+ BorderBasisDiagnostics,
9
+ BorderBasisError,
10
+ BorderBasisResult,
11
+ compute_border_basis,
12
+ compute_border_basis_linear,
13
+ )
14
+ from .continuation import (
15
+ HomotopySystem,
16
+ PathResult,
17
+ PathStep,
18
+ PathStepError,
19
+ PathTrackerOptions,
20
+ PathTrackingError,
21
+ SympyHomotopy,
22
+ track_path,
23
+ )
24
+ from .errors import (
25
+ ActionMatrixError,
26
+ HomotopySolveError,
27
+ NotZeroDimensionalError,
28
+ NumericalRootError,
29
+ PolynomialSystemError,
30
+ PolynomialSystemInputError,
31
+ ShapePositionError,
32
+ SystemSolveLimitError,
33
+ TriangularSolveError,
34
+ )
35
+ from .monodromy import (
36
+ MonodromyLoop,
37
+ MonodromyOrbitResult,
38
+ MonodromyPermutation,
39
+ MonodromyRootInfo,
40
+ closed_additive_loop,
41
+ discover_monodromy_orbit,
42
+ monodromy_permutation,
43
+ track_loop,
44
+ )
45
+ from .monodromy_stopping import (
46
+ CaptureRecaptureEstimate,
47
+ capture_recapture_estimate,
48
+ second_order_trace_test,
49
+ )
50
+ from .rational_univariate import (
51
+ RationalUnivariateError,
52
+ RationalUnivariatePoint,
53
+ RationalUnivariateRepresentation,
54
+ compute_rational_univariate_representation,
55
+ solve_rur_points,
56
+ solve_rur_representation,
57
+ solve_zero_dimensional_system_with_rur,
58
+ )
59
+ from .recognition import (
60
+ ExactCertificationError,
61
+ RecognizedSystemRoot,
62
+ recognize_system_roots,
63
+ )
64
+ from .solver import PolynomialSystemRoots, RootDiagnostics, polysolve
65
+
66
+ try:
67
+ __version__ = version("algroots")
68
+ except PackageNotFoundError:
69
+ __version__ = "0+unknown"
70
+
71
+ __all__ = [
72
+ "HomotopySystem",
73
+ "SympyHomotopy",
74
+ "PathTrackerOptions",
75
+ "PathStep",
76
+ "PathResult",
77
+ "PathTrackingError",
78
+ "PathStepError",
79
+ "track_path",
80
+ "CaptureRecaptureEstimate",
81
+ "capture_recapture_estimate",
82
+ "second_order_trace_test",
83
+ "MonodromyLoop",
84
+ "MonodromyPermutation",
85
+ "MonodromyOrbitResult",
86
+ "MonodromyRootInfo",
87
+ "closed_additive_loop",
88
+ "track_loop",
89
+ "monodromy_permutation",
90
+ "discover_monodromy_orbit",
91
+ "algsolve",
92
+ "polysolve",
93
+ "algebraize_system",
94
+ "AlgebraicSystemRoots",
95
+ "AlgebraizedSystem",
96
+ "recognize_system_roots",
97
+ "PolynomialSystemRoots",
98
+ "RootDiagnostics",
99
+ "RecognizedSystemRoot",
100
+ "ExactCertificationError",
101
+ "PolynomialSystemError",
102
+ "PolynomialSystemInputError",
103
+ "NotZeroDimensionalError",
104
+ "ActionMatrixError",
105
+ "HomotopySolveError",
106
+ "ShapePositionError",
107
+ "TriangularSolveError",
108
+ "NumericalRootError",
109
+ "SystemSolveLimitError",
110
+ "RationalUnivariateError",
111
+ "RationalUnivariateRepresentation",
112
+ "RationalUnivariatePoint",
113
+ "compute_rational_univariate_representation",
114
+ "solve_zero_dimensional_system_with_rur",
115
+ "solve_rur_representation",
116
+ "solve_rur_points",
117
+ "BorderBasisDiagnostics",
118
+ "BorderBasisError",
119
+ "BorderBasisResult",
120
+ "compute_border_basis",
121
+ "compute_border_basis_linear",
122
+ "__version__",
123
+ ]
@@ -0,0 +1,48 @@
1
+ """Shared lightweight input and option validation."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Sequence
6
+ from typing import Any
7
+
8
+ import sympy as sp
9
+
10
+ from .errors import PolynomialSystemInputError
11
+
12
+
13
+ def validate_variables(variables: Sequence[Any]) -> tuple[Any, ...]:
14
+ """Validate and normalize a nonempty sequence of distinct SymPy symbols."""
15
+ vars_tuple = tuple(variables)
16
+ if not vars_tuple:
17
+ raise PolynomialSystemInputError("at least one variable is required")
18
+ if len(set(vars_tuple)) != len(vars_tuple):
19
+ raise PolynomialSystemInputError("variables must be distinct")
20
+ if any(not isinstance(var, sp.Symbol) for var in vars_tuple):
21
+ raise PolynomialSystemInputError("variables must be SymPy Symbol objects")
22
+ return vars_tuple
23
+
24
+
25
+ def normalize_equation(equation: Any, *, expand: bool = False) -> Any:
26
+ """Convert an equality/expression to a zero-form expression."""
27
+ equation = sp.sympify(equation)
28
+ if equation is sp.true:
29
+ expression = sp.Integer(0)
30
+ elif equation is sp.false:
31
+ expression = sp.Integer(1)
32
+ elif isinstance(equation, sp.Equality):
33
+ expression = equation.lhs - equation.rhs
34
+ elif equation.is_Relational:
35
+ raise PolynomialSystemInputError(
36
+ "only equalities are supported; inequalities are outside algroots' scope"
37
+ )
38
+ else:
39
+ expression = equation
40
+ return sp.expand(expression) if expand else expression
41
+
42
+
43
+ def validate_recognition_options(recognize: bool, max_degree: int) -> None:
44
+ """Validate common automatic-recognition options."""
45
+ if not isinstance(recognize, bool):
46
+ raise TypeError("recognize must be bool")
47
+ if not isinstance(max_degree, int) or max_degree <= 0:
48
+ raise ValueError("recognition_max_degree must be a positive integer")
algroots/algebraic.py ADDED
@@ -0,0 +1,329 @@
1
+ """All-roots solving for exact algebraic equation systems."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Iterable, Sequence
6
+ from dataclasses import dataclass
7
+ from typing import Any
8
+
9
+ import mpmath as mp
10
+ import sympy as sp
11
+
12
+ from ._validation import validate_recognition_options
13
+ from .algebraization import AlgebraizedSystem, algebraize_system
14
+ from .numerical import NumericExpression, number_to_mpc
15
+ from .solver import (
16
+ PolynomialSystemRoots,
17
+ RootDiagnostics,
18
+ SolveMethod,
19
+ _roots_close,
20
+ polysolve,
21
+ )
22
+
23
+
24
+ @dataclass(frozen=True)
25
+ class AlgebraicSystemRoots(PolynomialSystemRoots):
26
+ """Verified roots of an algebraic system and its polynomialization metadata."""
27
+
28
+ polynomial_equations: tuple[Any, ...] = ()
29
+ polynomial_variables: tuple[Any, ...] = ()
30
+ auxiliary_variables: tuple[Any, ...] = ()
31
+ nonzero_constraints: tuple[Any, ...] = ()
32
+ homotopy_projected_candidates: int | None = None
33
+ homotopy_projected_roots_rejected: int | None = None
34
+
35
+
36
+ @dataclass(frozen=True)
37
+ class _ProjectedRoot:
38
+ """Projected algebraic root with aligned diagnostics and residual."""
39
+
40
+ root: tuple[Any, ...]
41
+ diagnostics: RootDiagnostics
42
+ residual: Any
43
+
44
+
45
+ @dataclass(frozen=True)
46
+ class _ProjectedCandidate:
47
+ """Projected algebraic root and its original-system residual."""
48
+
49
+ root: tuple[Any, ...]
50
+ relative_residual: mp.mpf
51
+
52
+
53
+ class _AlgebraicProjector:
54
+ """Lift original seeds and verify projected roots for one algebraization."""
55
+
56
+ def __init__(self, algebraized: AlgebraizedSystem):
57
+ self.algebraized = algebraized
58
+ self.original_count = len(algebraized.original_variables)
59
+ self._equation_evals = tuple(
60
+ _CompiledAlgResidual.build(eq, algebraized.original_variables)
61
+ for eq in algebraized.original_equations
62
+ )
63
+ self._constraint_evals = tuple(
64
+ NumericExpression(constraint, algebraized.augmented_variables)
65
+ for constraint in algebraized.nonzero_constraints
66
+ )
67
+ lift_evals = []
68
+ for index, expression in enumerate(algebraized.auxiliary_lift_expressions):
69
+ variables = algebraized.original_variables + algebraized.auxiliary_variables[:index]
70
+ lift_evals.append(NumericExpression(expression, variables))
71
+ self._lift_evals = tuple(lift_evals)
72
+
73
+ def lift(self, root: Sequence[Any], digits: int) -> tuple[mp.mpc, ...]:
74
+ """Lift an original-variable root to the principal algebraic sheet."""
75
+ if len(root) != self.original_count:
76
+ raise ValueError("every seed root must have one coordinate per variable")
77
+ with mp.workdps(digits + 10):
78
+ values = [number_to_mpc(value, digits + 5) for value in root]
79
+ for evaluator in self._lift_evals:
80
+ values.append(evaluator.evaluate(tuple(values), digits + 5))
81
+ return tuple(values)
82
+
83
+ def project(
84
+ self,
85
+ full_root: Sequence[Any],
86
+ *,
87
+ work_digits: int,
88
+ verification_digits: int,
89
+ ) -> _ProjectedCandidate | None:
90
+ """Project and verify one augmented root against the original system."""
91
+ if len(full_root) != len(self.algebraized.augmented_variables):
92
+ raise ValueError("augmented root dimension does not match polynomial variables")
93
+ with mp.workdps(work_digits + 10):
94
+ values = tuple(number_to_mpc(value, work_digits + 5) for value in full_root)
95
+ threshold = mp.power(10, -verification_digits)
96
+ if any(
97
+ abs(evaluator.evaluate(values, work_digits + 5)) <= threshold
98
+ for evaluator in self._constraint_evals
99
+ ):
100
+ return None
101
+ projected_values = values[: self.original_count]
102
+ residual = max(
103
+ (
104
+ evaluator.evaluate(projected_values, work_digits + 5)
105
+ for evaluator in self._equation_evals
106
+ ),
107
+ default=mp.mpf(0),
108
+ )
109
+ if not mp.isfinite(residual) or residual > threshold:
110
+ return None
111
+ return _ProjectedCandidate(
112
+ tuple(full_root[: self.original_count]),
113
+ mp.mpf(residual),
114
+ )
115
+
116
+
117
+ def _project_candidates(
118
+ projector: _AlgebraicProjector,
119
+ roots: Sequence[Sequence[Any]],
120
+ precisions: Sequence[tuple[int, int]],
121
+ ):
122
+ """Yield projected candidates using aligned work/verification precisions."""
123
+ if len(roots) != len(precisions):
124
+ raise ValueError("projection precision metadata must align with roots")
125
+ for root, (work_digits, verification_digits) in zip(roots, precisions, strict=True):
126
+ yield projector.project(
127
+ root,
128
+ work_digits=work_digits,
129
+ verification_digits=verification_digits,
130
+ )
131
+
132
+
133
+ @dataclass(frozen=True)
134
+ class _CompiledAlgResidual:
135
+ evaluator: NumericExpression
136
+ term_evaluators: tuple[NumericExpression, ...]
137
+
138
+ @classmethod
139
+ def build(cls, expression: Any, variables: tuple[Any, ...]):
140
+ return cls(
141
+ evaluator=NumericExpression(expression, variables),
142
+ term_evaluators=tuple(
143
+ NumericExpression(term, variables) for term in sp.Add.make_args(expression)
144
+ ),
145
+ )
146
+
147
+ def evaluate(self, values: tuple[Any, ...], digits: int) -> mp.mpf:
148
+ with mp.workdps(digits + 8):
149
+ value = self.evaluator.evaluate(values, digits + 5)
150
+ if len(self.term_evaluators) == 1:
151
+ scale = max(mp.mpf(1), abs(value))
152
+ else:
153
+ scale = sum(
154
+ (abs(term.evaluate(values, digits + 5)) for term in self.term_evaluators),
155
+ mp.mpf(0),
156
+ )
157
+ scale = max(mp.mpf(1), scale)
158
+ return abs(value) / scale
159
+
160
+
161
+ def _filter_algebraic_roots(
162
+ polynomial_result: PolynomialSystemRoots,
163
+ algebraized: AlgebraizedSystem,
164
+ *,
165
+ include_projection_counts: bool = False,
166
+ ):
167
+ digits = polynomial_result.precision_digits
168
+ verify_digits = polynomial_result.verification_digits
169
+ work_digits = polynomial_result.working_digits
170
+ projector = _AlgebraicProjector(algebraized)
171
+
172
+ records: list[_ProjectedRoot] = []
173
+ rejected = 0
174
+ precisions = ((work_digits + 5, verify_digits),) * len(polynomial_result.roots)
175
+ for index, candidate in enumerate(
176
+ _project_candidates(projector, polynomial_result.roots, precisions)
177
+ ):
178
+ if candidate is None:
179
+ rejected += 1
180
+ continue
181
+ residual = sp.Float(mp.nstr(candidate.relative_residual, work_digits), work_digits)
182
+ if index < len(polynomial_result.diagnostics):
183
+ diagnostics = polynomial_result.diagnostics[index]
184
+ else:
185
+ diagnostics = RootDiagnostics(
186
+ initial_relative_residual=residual,
187
+ final_relative_residual=residual,
188
+ refinement_attempted=False,
189
+ refinement_succeeded=False,
190
+ refinement_improved=False,
191
+ )
192
+ records.append(_ProjectedRoot(candidate.root, diagnostics, residual))
193
+
194
+ tolerance_sp = sp.Float(10, digits) ** (-max(8, digits - 5))
195
+ comparison_digits = max(30, digits + 10)
196
+ unique: list[_ProjectedRoot] = []
197
+ for record in records:
198
+ if any(
199
+ _roots_close(
200
+ record.root,
201
+ existing.root,
202
+ tolerance_sp,
203
+ comparison_digits=comparison_digits,
204
+ )
205
+ for existing in unique
206
+ ):
207
+ continue
208
+ unique.append(record)
209
+
210
+ roots = tuple(record.root for record in unique)
211
+ diagnostics = tuple(record.diagnostics for record in unique)
212
+ max_relative_residual = max(
213
+ (record.residual for record in unique),
214
+ default=sp.Integer(0),
215
+ )
216
+ if include_projection_counts:
217
+ return (
218
+ roots,
219
+ diagnostics,
220
+ max_relative_residual,
221
+ len(records),
222
+ rejected + (len(records) - len(unique)),
223
+ )
224
+ return roots, diagnostics, max_relative_residual
225
+
226
+
227
+ def algsolve(
228
+ equations: Iterable[Any],
229
+ variables: Sequence[Any],
230
+ *,
231
+ digits: int = 50,
232
+ verification_digits: int | None = None,
233
+ guard_digits: int = 10,
234
+ maxsteps: int = 200,
235
+ max_solutions: int = 10_000,
236
+ max_action_dimension: int = 256,
237
+ max_precision_digits: int | None = None,
238
+ max_auxiliary_variables: int = 32,
239
+ method: SolveMethod = "auto",
240
+ recognize: bool = True,
241
+ recognition_max_degree: int = 8,
242
+ max_homotopy_paths: int = 10_000,
243
+ homotopy_seed: int = 0,
244
+ homotopy_gamma_attempts: int = 4,
245
+ homotopy_parallel: bool = False,
246
+ homotopy_max_workers: int | None = None,
247
+ ) -> AlgebraicSystemRoots:
248
+ """Find all distinct roots of a zero-dimensional exact algebraic system.
249
+
250
+ Exact algebraic recognition is attempted by default for the final projected
251
+ roots. Pass ``recognize=False`` to disable recognition.
252
+
253
+ Supported variable-dependent algebraic operations are rational functions and
254
+ rational powers, including nested radicals. Rational powers use SymPy's
255
+ principal-branch semantics. Algebraization may introduce extra polynomial
256
+ branches, so every projected result is checked against the original equations
257
+ and all retained denominator constraints before it is returned.
258
+ """
259
+ validate_recognition_options(recognize, recognition_max_degree)
260
+ algebraized = algebraize_system(
261
+ equations,
262
+ variables,
263
+ max_auxiliary_variables=max_auxiliary_variables,
264
+ )
265
+ polynomial_result = polysolve(
266
+ algebraized.polynomial_equations,
267
+ algebraized.augmented_variables,
268
+ digits=digits,
269
+ verification_digits=verification_digits,
270
+ guard_digits=guard_digits,
271
+ maxsteps=maxsteps,
272
+ max_solutions=max_solutions,
273
+ max_action_dimension=max_action_dimension,
274
+ max_precision_digits=max_precision_digits,
275
+ method=method,
276
+ recognize=False,
277
+ max_homotopy_paths=max_homotopy_paths,
278
+ homotopy_seed=homotopy_seed,
279
+ homotopy_gamma_attempts=homotopy_gamma_attempts,
280
+ homotopy_parallel=homotopy_parallel,
281
+ homotopy_max_workers=homotopy_max_workers,
282
+ )
283
+ (
284
+ roots,
285
+ diagnostics,
286
+ max_residual,
287
+ projected_candidates,
288
+ projected_rejected,
289
+ ) = _filter_algebraic_roots(
290
+ polynomial_result,
291
+ algebraized,
292
+ include_projection_counts=True,
293
+ )
294
+
295
+ result = AlgebraicSystemRoots(
296
+ roots=roots,
297
+ variables=algebraized.original_variables,
298
+ equations=algebraized.original_equations,
299
+ groebner_basis=polynomial_result.groebner_basis,
300
+ precision_digits=polynomial_result.precision_digits,
301
+ working_digits=polynomial_result.working_digits,
302
+ verification_digits=polynomial_result.verification_digits,
303
+ max_relative_residual=max_residual,
304
+ method=polynomial_result.method,
305
+ diagnostics=diagnostics,
306
+ eliminant=polynomial_result.eliminant,
307
+ parameter_variable=polynomial_result.parameter_variable,
308
+ quotient_dimension=polynomial_result.quotient_dimension,
309
+ standard_monomials=polynomial_result.standard_monomials,
310
+ separator_coeffs=polynomial_result.separator_coeffs,
311
+ rational_univariate_representation=polynomial_result.rational_univariate_representation,
312
+ homotopy_paths_total=polynomial_result.homotopy_paths_total,
313
+ homotopy_paths_succeeded=polynomial_result.homotopy_paths_succeeded,
314
+ homotopy_paths_failed=polynomial_result.homotopy_paths_failed,
315
+ homotopy_paths_divergent=polynomial_result.homotopy_paths_divergent,
316
+ homotopy_gamma=polynomial_result.homotopy_gamma,
317
+ homotopy_gamma_attempts=polynomial_result.homotopy_gamma_attempts,
318
+ homotopy_endpoint_smallest_singular_values=polynomial_result.homotopy_endpoint_smallest_singular_values,
319
+ homotopy_endpoint_condition_estimates=polynomial_result.homotopy_endpoint_condition_estimates,
320
+ polynomial_equations=algebraized.polynomial_equations,
321
+ polynomial_variables=algebraized.augmented_variables,
322
+ auxiliary_variables=algebraized.auxiliary_variables,
323
+ nonzero_constraints=algebraized.nonzero_constraints,
324
+ homotopy_projected_candidates=(projected_candidates if method == "homotopy" else None),
325
+ homotopy_projected_roots_rejected=(projected_rejected if method == "homotopy" else None),
326
+ )
327
+ from .recognition import _auto_recognize
328
+
329
+ return _auto_recognize(result, recognize=recognize, max_degree=recognition_max_degree)