odeanalysis 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 (49) hide show
  1. odeanalysis/__init__.py +296 -0
  2. odeanalysis/_api_policy.py +179 -0
  3. odeanalysis/_assumptions.py +89 -0
  4. odeanalysis/_block_common.py +106 -0
  5. odeanalysis/_formal_gauge.py +115 -0
  6. odeanalysis/_local.py +166 -0
  7. odeanalysis/_moser.py +271 -0
  8. odeanalysis/_power_simplify.py +32 -0
  9. odeanalysis/_spectral.py +215 -0
  10. odeanalysis/_symbolic_compare.py +16 -0
  11. odeanalysis/_symbolic_errors.py +19 -0
  12. odeanalysis/_zero.py +28 -0
  13. odeanalysis/analytic_continuation.py +343 -0
  14. odeanalysis/bell.py +87 -0
  15. odeanalysis/block_decomposition.py +1144 -0
  16. odeanalysis/canonical.py +471 -0
  17. odeanalysis/certified_continuation.py +160 -0
  18. odeanalysis/diagnostics.py +17 -0
  19. odeanalysis/dominance.py +134 -0
  20. odeanalysis/factorization.py +88 -0
  21. odeanalysis/formal.py +1028 -0
  22. odeanalysis/formal_basis.py +1009 -0
  23. odeanalysis/frobenius.py +349 -0
  24. odeanalysis/fuchsian.py +400 -0
  25. odeanalysis/interchange.py +604 -0
  26. odeanalysis/interoperability.py +143 -0
  27. odeanalysis/irregular.py +250 -0
  28. odeanalysis/kovacic.py +478 -0
  29. odeanalysis/levelt.py +679 -0
  30. odeanalysis/local_analysis.py +386 -0
  31. odeanalysis/local_structure.py +290 -0
  32. odeanalysis/matrix_series.py +357 -0
  33. odeanalysis/newton.py +501 -0
  34. odeanalysis/operator.py +193 -0
  35. odeanalysis/parameter_wkb.py +92 -0
  36. odeanalysis/py.typed +0 -0
  37. odeanalysis/series.py +199 -0
  38. odeanalysis/singularities.py +279 -0
  39. odeanalysis/stokes.py +782 -0
  40. odeanalysis/system.py +347 -0
  41. odeanalysis/system_analysis.py +615 -0
  42. odeanalysis/transition_loci.py +302 -0
  43. odeanalysis/turning.py +516 -0
  44. odeanalysis/wronskian.py +110 -0
  45. odeanalysis-0.1.0.dist-info/METADATA +180 -0
  46. odeanalysis-0.1.0.dist-info/RECORD +49 -0
  47. odeanalysis-0.1.0.dist-info/WHEEL +5 -0
  48. odeanalysis-0.1.0.dist-info/licenses/LICENSE +677 -0
  49. odeanalysis-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,302 @@
1
+ """ODE-specific algebraic loci where structural analysis can change."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Sequence
6
+ from dataclasses import dataclass
7
+
8
+ import sympy as sp
9
+ from semialg import is_satisfiable, matrix_rank_stratification
10
+
11
+ from ._zero import ZeroStatus, exact_zero_status
12
+ from .newton import localize_operator
13
+ from .operator import LinearDifferentialOperator, _coerce_linear_operator
14
+ from .system import FirstOrderSystem
15
+ from .turning import liouville_normal_form
16
+
17
+
18
+ def _parameter_factors(
19
+ expression: sp.Expr, excluded: set[sp.Symbol]
20
+ ) -> tuple[sp.Expr, ...]:
21
+ expression = sp.factor(expression)
22
+ factors = (
23
+ sp.factor_list(expression)[1]
24
+ if exact_zero_status(expression) is not ZeroStatus.ZERO
25
+ else ()
26
+ )
27
+ result = []
28
+ for factor, _ in factors:
29
+ if factor.free_symbols - excluded:
30
+ result.append(sp.factor(factor))
31
+ return tuple(dict.fromkeys(result))
32
+
33
+
34
+ def _polynomial_parameter_loci(
35
+ expression: sp.Expr, variable: sp.Symbol
36
+ ) -> tuple[sp.Expr, ...]:
37
+ """Return loci where the effective degree or finite root multiplicities can change."""
38
+ try:
39
+ polynomial = sp.Poly(sp.expand(expression), variable)
40
+ except sp.PolynomialError:
41
+ return ()
42
+ loci: list[sp.Expr] = []
43
+ # Every coefficient can become the leading coefficient on a lower-degree stratum.
44
+ # Its vanishing therefore represents a projective root-at-infinity transition.
45
+ coefficients = [
46
+ sp.factor(polynomial.nth(k)) for k in range(polynomial.degree() + 1)
47
+ ]
48
+ for coefficient in coefficients:
49
+ loci.extend(_parameter_factors(coefficient, {variable}))
50
+ # On each possible effective-degree stratum, the corresponding truncation's
51
+ # discriminant detects finite multiple-root collisions.
52
+ for degree in range(2, polynomial.degree() + 1):
53
+ truncated = sum(polynomial.nth(k) * variable**k for k in range(degree + 1))
54
+ if polynomial.nth(degree) == 0:
55
+ continue
56
+ loci.extend(
57
+ _parameter_factors(sp.discriminant(truncated, variable), {variable})
58
+ )
59
+ return tuple(dict.fromkeys(loci))
60
+
61
+
62
+ def _effective_support_polynomials(
63
+ expression: sp.Expr, variable: sp.Symbol
64
+ ) -> tuple[sp.Poly, ...]:
65
+ """Canonical numerator/denominator polynomials for rational support analysis."""
66
+ numerator, denominator = sp.fraction(sp.cancel(expression))
67
+ polynomials: list[sp.Poly] = []
68
+ for part in (numerator, denominator):
69
+ try:
70
+ polynomials.append(sp.Poly(part, variable))
71
+ except sp.PolynomialError:
72
+ continue
73
+ return tuple(polynomials)
74
+
75
+
76
+ def _support_loci(expression: sp.Expr, variable: sp.Symbol) -> tuple[sp.Expr, ...]:
77
+ loci: list[sp.Expr] = []
78
+ for polynomial in _effective_support_polynomials(expression, variable):
79
+ for coefficient in polynomial.all_coeffs():
80
+ loci.extend(_parameter_factors(coefficient, {variable}))
81
+ return tuple(dict.fromkeys(loci))
82
+
83
+
84
+ def _rational_numerator(expression: sp.Expr) -> sp.Expr:
85
+ return sp.fraction(sp.cancel(expression))[0]
86
+
87
+
88
+ def newton_loci(
89
+ ode: sp.Expr | sp.Equality | LinearDifferentialOperator,
90
+ function: sp.FunctionClass | sp.Expr | None = None,
91
+ variable: sp.Symbol | None = None,
92
+ *,
93
+ point: sp.Expr = 0,
94
+ ) -> tuple[sp.Expr, ...]:
95
+ """Return loci where normalized effective support can change the Newton polygon."""
96
+ localized = localize_operator(ode, function, variable, point=point)
97
+ t = localized.local_variable
98
+ loci: list[sp.Expr] = []
99
+ for coefficient in localized.operator.coefficients:
100
+ if coefficient != 0:
101
+ loci.extend(_support_loci(coefficient, t))
102
+ return tuple(dict.fromkeys(loci))
103
+
104
+
105
+ @dataclass(frozen=True)
106
+ class ProjectivePolynomialStratum:
107
+ """A certified effective-degree stratum of a fixed projective polynomial family."""
108
+
109
+ condition: sp.Expr
110
+ effective_degree: int
111
+ infinity_multiplicity: int
112
+ certified: bool = True
113
+
114
+
115
+ def _projective_strata_from_polynomial(
116
+ polynomial: sp.Poly, parameters: Sequence[sp.Symbol]
117
+ ) -> tuple[ProjectivePolynomialStratum, ...]:
118
+ """Interpret nested leading-coefficient losses as cumulative roots at infinity."""
119
+ reference_degree = polynomial.degree()
120
+ strata: list[ProjectivePolynomialStratum] = []
121
+ higher_zero: list[sp.Expr] = []
122
+ for degree in range(reference_degree, -1, -1):
123
+ coefficient = sp.factor(polynomial.nth(degree))
124
+ if coefficient == 0:
125
+ higher_zero.append(sp.true)
126
+ continue
127
+ condition = sp.And(*higher_zero, sp.Ne(coefficient, 0, evaluate=False))
128
+ if is_satisfiable(condition, parameters) is not False:
129
+ strata.append(
130
+ ProjectivePolynomialStratum(
131
+ condition, degree, reference_degree - degree
132
+ )
133
+ )
134
+ higher_zero.append(sp.Eq(coefficient, 0, evaluate=False))
135
+ zero_condition = sp.And(*higher_zero) if higher_zero else sp.false
136
+ if is_satisfiable(zero_condition, parameters) is not False:
137
+ strata.append(
138
+ ProjectivePolynomialStratum(zero_condition, -1, reference_degree + 1)
139
+ )
140
+ return tuple(strata)
141
+
142
+
143
+ def singularity_loci(
144
+ ode: sp.Expr | sp.Equality | LinearDifferentialOperator,
145
+ function: sp.FunctionClass | sp.Expr | None = None,
146
+ variable: sp.Symbol | None = None,
147
+ ) -> tuple[sp.Expr, ...]:
148
+ """Return finite-collision and projective degree-loss loci of singular points."""
149
+ operator = _coerce_linear_operator(ode, function, variable)
150
+ x = operator.variable
151
+ numerator = _rational_numerator(operator.coefficients[-1])
152
+ return _polynomial_parameter_loci(numerator, x)
153
+
154
+
155
+ def singularity_projective_strata(
156
+ ode: sp.Expr | sp.Equality | LinearDifferentialOperator,
157
+ function: sp.FunctionClass | sp.Expr | None = None,
158
+ variable: sp.Symbol | None = None,
159
+ *,
160
+ parameters: Sequence[sp.Symbol],
161
+ ) -> tuple[ProjectivePolynomialStratum, ...]:
162
+ """Return certified effective-degree/root-at-infinity strata for singular points."""
163
+ operator = _coerce_linear_operator(ode, function, variable)
164
+ x = operator.variable
165
+ polynomial = sp.Poly(sp.expand(_rational_numerator(operator.coefficients[-1])), x)
166
+ return _projective_strata_from_polynomial(polynomial, parameters)
167
+
168
+
169
+ def turning_loci(
170
+ ode: sp.Expr | sp.Equality | LinearDifferentialOperator,
171
+ function: sp.FunctionClass | sp.Expr | None = None,
172
+ variable: sp.Symbol | None = None,
173
+ ) -> tuple[sp.Expr, ...]:
174
+ """Return finite-collision and projective degree-loss loci of turning points."""
175
+ normal = liouville_normal_form(ode, function, variable)
176
+ x = normal.operator.variable
177
+ numerator = _rational_numerator(normal.potential)
178
+ return _polynomial_parameter_loci(numerator, x)
179
+
180
+
181
+ def turning_point_projective_strata(
182
+ ode: sp.Expr | sp.Equality | LinearDifferentialOperator,
183
+ function: sp.FunctionClass | sp.Expr | None = None,
184
+ variable: sp.Symbol | None = None,
185
+ *,
186
+ parameters: Sequence[sp.Symbol],
187
+ ) -> tuple[ProjectivePolynomialStratum, ...]:
188
+ """Return certified effective-degree/root-at-infinity strata for turning points."""
189
+ normal = liouville_normal_form(ode, function, variable)
190
+ x = normal.operator.variable
191
+ polynomial = sp.Poly(sp.expand(_rational_numerator(normal.potential)), x)
192
+ return _projective_strata_from_polynomial(polynomial, parameters)
193
+
194
+
195
+ def stokes_formal_loci(
196
+ exponential_parts: Sequence[sp.Expr], local_variable: sp.Symbol
197
+ ) -> tuple[sp.Expr, ...]:
198
+ """Return loci where leading pairwise exponential differences lose formal degree."""
199
+ loci: list[sp.Expr] = []
200
+ for i, left in enumerate(exponential_parts):
201
+ for right in exponential_parts[i + 1 :]:
202
+ difference = sp.expand(left - right)
203
+ if exact_zero_status(difference) is not ZeroStatus.ZERO:
204
+ loci.extend(_support_loci(difference, local_variable))
205
+ return tuple(dict.fromkeys(loci))
206
+
207
+
208
+ def _require_real_parameters(parameters: Sequence[sp.Symbol]) -> None:
209
+ if any(parameter.is_real is not True for parameter in parameters):
210
+ raise ValueError(
211
+ "certified Stokes-ray geometry requires explicit real parameter coordinates"
212
+ )
213
+
214
+
215
+ def _complex_zero_polynomial(expression: sp.Expr) -> sp.Expr:
216
+ expanded = sp.expand_complex(expression)
217
+ return sp.factor(sp.re(expanded) ** 2 + sp.im(expanded) ** 2)
218
+
219
+
220
+ def _phase_alignment_polynomial(left: sp.Expr, right: sp.Expr) -> sp.Expr:
221
+ left = sp.expand_complex(left)
222
+ right = sp.expand_complex(right)
223
+ return sp.simplify(sp.im(left * sp.conjugate(right)))
224
+
225
+
226
+ def stokes_ray_loci(
227
+ exponential_parts: Sequence[sp.Expr],
228
+ local_variable: sp.Symbol,
229
+ parameters: Sequence[sp.Symbol],
230
+ ) -> tuple[sp.Expr, ...]:
231
+ """Return real-algebraic phase-alignment loci where Stokes rays can coincide/reorder."""
232
+ leading_data: list[tuple[int, sp.Expr]] = []
233
+ for i, left in enumerate(exponential_parts):
234
+ for right in exponential_parts[i + 1 :]:
235
+ difference = sp.cancel(left - right)
236
+ if exact_zero_status(difference) is ZeroStatus.ZERO:
237
+ continue
238
+ try:
239
+ polynomial = sp.Poly(difference, local_variable)
240
+ except sp.PolynomialError:
241
+ continue
242
+ leading_data.append((polynomial.degree(), sp.factor(polynomial.LC())))
243
+ _require_real_parameters(parameters)
244
+ loci: list[sp.Expr] = []
245
+ for formal_locus in stokes_formal_loci(exponential_parts, local_variable):
246
+ real_zero = _complex_zero_polynomial(formal_locus)
247
+ if exact_zero_status(real_zero) is not ZeroStatus.ZERO:
248
+ loci.append(real_zero)
249
+ for i, (degree, left) in enumerate(leading_data):
250
+ for other_degree, right in leading_data[i + 1 :]:
251
+ if degree != other_degree:
252
+ continue
253
+ alignment = _phase_alignment_polynomial(left, right)
254
+ if exact_zero_status(alignment) is not ZeroStatus.ZERO:
255
+ loci.extend(_parameter_factors(alignment, set()))
256
+ return tuple(dict.fromkeys(loci))
257
+
258
+
259
+ @dataclass(frozen=True)
260
+ class LeadingMatrixRankAnalysis:
261
+ """Parameter stratification of the leading Laurent matrix rank."""
262
+
263
+ pole_order: int
264
+ leading_matrix: sp.ImmutableMatrix
265
+ strata: object
266
+
267
+
268
+ def leading_rank_analysis(
269
+ system: FirstOrderSystem,
270
+ point: sp.Expr,
271
+ parameters: Sequence[sp.Symbol],
272
+ *,
273
+ parameter_domain: sp.Expr = sp.true,
274
+ ) -> LeadingMatrixRankAnalysis:
275
+ """Stratify the rank of a system's leading Laurent matrix with semialg."""
276
+ x = system.variable
277
+ h = sp.Symbol("_h")
278
+ entries = [sp.cancel(entry.subs(x, point + h)) for entry in system.matrix]
279
+ valuations = []
280
+ for entry in entries:
281
+ if entry == 0:
282
+ continue
283
+ numerator, denominator = sp.fraction(entry)
284
+ try:
285
+ n = sp.Poly(numerator, h)
286
+ d = sp.Poly(denominator, h)
287
+ except sp.PolynomialError as exc:
288
+ raise NotImplementedError(
289
+ "leading-matrix rank requires rational local entries"
290
+ ) from exc
291
+ nv = min(k for k in range(n.degree() + 1) if n.nth(k) != 0)
292
+ dv = min(k for k in range(d.degree() + 1) if d.nth(k) != 0)
293
+ valuations.append(nv - dv)
294
+ minimum = min(valuations, default=0)
295
+ leading = system.matrix.applyfunc(
296
+ lambda entry: sp.simplify(sp.limit(entry.subs(x, point + h) / h**minimum, h, 0))
297
+ )
298
+ immutable = sp.ImmutableMatrix(leading)
299
+ strata = matrix_rank_stratification(
300
+ immutable, parameters, parameter_domain=parameter_domain
301
+ )
302
+ return LeadingMatrixRankAnalysis(-minimum, immutable, strata)