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.
- odeanalysis/__init__.py +296 -0
- odeanalysis/_api_policy.py +179 -0
- odeanalysis/_assumptions.py +89 -0
- odeanalysis/_block_common.py +106 -0
- odeanalysis/_formal_gauge.py +115 -0
- odeanalysis/_local.py +166 -0
- odeanalysis/_moser.py +271 -0
- odeanalysis/_power_simplify.py +32 -0
- odeanalysis/_spectral.py +215 -0
- odeanalysis/_symbolic_compare.py +16 -0
- odeanalysis/_symbolic_errors.py +19 -0
- odeanalysis/_zero.py +28 -0
- odeanalysis/analytic_continuation.py +343 -0
- odeanalysis/bell.py +87 -0
- odeanalysis/block_decomposition.py +1144 -0
- odeanalysis/canonical.py +471 -0
- odeanalysis/certified_continuation.py +160 -0
- odeanalysis/diagnostics.py +17 -0
- odeanalysis/dominance.py +134 -0
- odeanalysis/factorization.py +88 -0
- odeanalysis/formal.py +1028 -0
- odeanalysis/formal_basis.py +1009 -0
- odeanalysis/frobenius.py +349 -0
- odeanalysis/fuchsian.py +400 -0
- odeanalysis/interchange.py +604 -0
- odeanalysis/interoperability.py +143 -0
- odeanalysis/irregular.py +250 -0
- odeanalysis/kovacic.py +478 -0
- odeanalysis/levelt.py +679 -0
- odeanalysis/local_analysis.py +386 -0
- odeanalysis/local_structure.py +290 -0
- odeanalysis/matrix_series.py +357 -0
- odeanalysis/newton.py +501 -0
- odeanalysis/operator.py +193 -0
- odeanalysis/parameter_wkb.py +92 -0
- odeanalysis/py.typed +0 -0
- odeanalysis/series.py +199 -0
- odeanalysis/singularities.py +279 -0
- odeanalysis/stokes.py +782 -0
- odeanalysis/system.py +347 -0
- odeanalysis/system_analysis.py +615 -0
- odeanalysis/transition_loci.py +302 -0
- odeanalysis/turning.py +516 -0
- odeanalysis/wronskian.py +110 -0
- odeanalysis-0.1.0.dist-info/METADATA +180 -0
- odeanalysis-0.1.0.dist-info/RECORD +49 -0
- odeanalysis-0.1.0.dist-info/WHEEL +5 -0
- odeanalysis-0.1.0.dist-info/licenses/LICENSE +677 -0
- 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)
|