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,143 @@
|
|
|
1
|
+
"""Certified correspondence between scalar equations and first-order systems."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
|
|
7
|
+
import sympy as sp
|
|
8
|
+
|
|
9
|
+
from .operator import LinearDifferentialOperator
|
|
10
|
+
from .system import FirstOrderSystem, companion_system
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass(frozen=True)
|
|
14
|
+
class CyclicScalarization:
|
|
15
|
+
"""A scalar equation obtained from a certified cyclic output row."""
|
|
16
|
+
|
|
17
|
+
system: FirstOrderSystem
|
|
18
|
+
output_row: sp.ImmutableMatrix
|
|
19
|
+
cyclic_matrix: sp.ImmutableMatrix
|
|
20
|
+
operator: LinearDifferentialOperator | None
|
|
21
|
+
complete: bool
|
|
22
|
+
limitation: str | None = None
|
|
23
|
+
|
|
24
|
+
def verify(self) -> bool:
|
|
25
|
+
if not self.complete or self.operator is None:
|
|
26
|
+
return False
|
|
27
|
+
x = self.system.variable
|
|
28
|
+
rows = [sp.Matrix(self.output_row)]
|
|
29
|
+
for _ in range(self.system.dimension):
|
|
30
|
+
row = rows[-1]
|
|
31
|
+
rows.append(row.diff(x) + row * sp.Matrix(self.system.matrix))
|
|
32
|
+
cyclic = sp.Matrix.vstack(*rows[:-1])
|
|
33
|
+
if sp.simplify(cyclic.det()) == 0:
|
|
34
|
+
return False
|
|
35
|
+
alpha = sp.simplify(rows[-1] * cyclic.inv())
|
|
36
|
+
expected = (
|
|
37
|
+
*tuple(sp.cancel(-alpha[0, j]) for j in range(self.system.dimension)),
|
|
38
|
+
sp.S.One,
|
|
39
|
+
)
|
|
40
|
+
return all(
|
|
41
|
+
sp.simplify(a - b) == 0
|
|
42
|
+
for a, b in zip(expected, self.operator.coefficients, strict=True)
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def scalarize_system(
|
|
47
|
+
system: FirstOrderSystem,
|
|
48
|
+
*,
|
|
49
|
+
output_row: sp.MatrixBase | None = None,
|
|
50
|
+
function: sp.FunctionClass | None = None,
|
|
51
|
+
) -> CyclicScalarization:
|
|
52
|
+
"""Scalarize a homogeneous system when the chosen output is cyclic.
|
|
53
|
+
|
|
54
|
+
The row jets satisfy ``r_(k+1)=r_k' + r_k A``. If the first ``n`` rows
|
|
55
|
+
form an invertible cyclic matrix, the next row gives the monic scalar
|
|
56
|
+
equation for the output. Failure to certify cyclicity is explicit.
|
|
57
|
+
"""
|
|
58
|
+
if not system.is_homogeneous:
|
|
59
|
+
return CyclicScalarization(
|
|
60
|
+
system,
|
|
61
|
+
sp.ImmutableMatrix([]),
|
|
62
|
+
sp.ImmutableMatrix([]),
|
|
63
|
+
None,
|
|
64
|
+
False,
|
|
65
|
+
"inhomogeneous systems are not scalarized",
|
|
66
|
+
)
|
|
67
|
+
n = system.dimension
|
|
68
|
+
if output_row is None:
|
|
69
|
+
row = sp.zeros(1, n)
|
|
70
|
+
row[0, 0] = 1
|
|
71
|
+
else:
|
|
72
|
+
row = sp.Matrix(output_row)
|
|
73
|
+
if row.shape == (n, 1):
|
|
74
|
+
row = row.T
|
|
75
|
+
if row.shape != (1, n):
|
|
76
|
+
raise ValueError(f"output_row must have shape (1, {n})")
|
|
77
|
+
x = system.variable
|
|
78
|
+
rows = [row]
|
|
79
|
+
for _ in range(n):
|
|
80
|
+
rows.append(rows[-1].diff(x) + rows[-1] * sp.Matrix(system.matrix))
|
|
81
|
+
cyclic = sp.Matrix.vstack(*rows[:-1])
|
|
82
|
+
determinant = sp.factor(cyclic.det())
|
|
83
|
+
if determinant == 0 or determinant.is_zero is True:
|
|
84
|
+
return CyclicScalarization(
|
|
85
|
+
system,
|
|
86
|
+
sp.ImmutableMatrix(row),
|
|
87
|
+
sp.ImmutableMatrix(cyclic),
|
|
88
|
+
None,
|
|
89
|
+
False,
|
|
90
|
+
"chosen output row is not cyclic",
|
|
91
|
+
)
|
|
92
|
+
if determinant.is_zero is None and determinant.free_symbols - {x}:
|
|
93
|
+
return CyclicScalarization(
|
|
94
|
+
system,
|
|
95
|
+
sp.ImmutableMatrix(row),
|
|
96
|
+
sp.ImmutableMatrix(cyclic),
|
|
97
|
+
None,
|
|
98
|
+
False,
|
|
99
|
+
"cyclicity is parameter-dependent and was not certified",
|
|
100
|
+
)
|
|
101
|
+
try:
|
|
102
|
+
alpha = (rows[-1] * cyclic.inv()).applyfunc(lambda e: sp.cancel(sp.together(e)))
|
|
103
|
+
except (ValueError, ZeroDivisionError):
|
|
104
|
+
return CyclicScalarization(
|
|
105
|
+
system,
|
|
106
|
+
sp.ImmutableMatrix(row),
|
|
107
|
+
sp.ImmutableMatrix(cyclic),
|
|
108
|
+
None,
|
|
109
|
+
False,
|
|
110
|
+
"cyclic matrix inversion failed",
|
|
111
|
+
)
|
|
112
|
+
coeffs = (*tuple(sp.cancel(-alpha[0, j]) for j in range(n)), sp.S.One)
|
|
113
|
+
if function is None:
|
|
114
|
+
function = sp.Function("u")
|
|
115
|
+
operator = LinearDifferentialOperator(x, function, coeffs)
|
|
116
|
+
return CyclicScalarization(
|
|
117
|
+
system, sp.ImmutableMatrix(row), sp.ImmutableMatrix(cyclic), operator, True
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
@dataclass(frozen=True)
|
|
122
|
+
class ScalarSystemCorrespondence:
|
|
123
|
+
"""Explicit companion/scalarization round-trip evidence."""
|
|
124
|
+
|
|
125
|
+
operator: LinearDifferentialOperator
|
|
126
|
+
system: FirstOrderSystem
|
|
127
|
+
scalarization: CyclicScalarization
|
|
128
|
+
|
|
129
|
+
def verify(self) -> bool:
|
|
130
|
+
if not self.scalarization.verify() or self.scalarization.operator is None:
|
|
131
|
+
return False
|
|
132
|
+
left = self.operator.normalized().coefficients
|
|
133
|
+
right = self.scalarization.operator.normalized().coefficients
|
|
134
|
+
return all(sp.simplify(a - b) == 0 for a, b in zip(left, right, strict=True))
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def scalar_system_correspondence(
|
|
138
|
+
operator: LinearDifferentialOperator,
|
|
139
|
+
) -> ScalarSystemCorrespondence:
|
|
140
|
+
"""Build and verify the standard scalar -> companion -> scalar round trip."""
|
|
141
|
+
system = companion_system(operator)
|
|
142
|
+
scalarization = scalarize_system(system, function=operator.function)
|
|
143
|
+
return ScalarSystemCorrespondence(operator, system, scalarization)
|
odeanalysis/irregular.py
ADDED
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
"""Irregular-singularity invariants and leading WKB/exponential data."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
|
|
7
|
+
import sympy as sp
|
|
8
|
+
|
|
9
|
+
from ._symbolic_errors import SYMBOLIC_FAILURES
|
|
10
|
+
from .newton import (
|
|
11
|
+
DifferentialNewtonEdge,
|
|
12
|
+
DifferentialNewtonPolygon,
|
|
13
|
+
differential_newton_polygon,
|
|
14
|
+
poincare_rank,
|
|
15
|
+
)
|
|
16
|
+
from .operator import LinearDifferentialOperator
|
|
17
|
+
from .singularities import ODESingularityKind, classify_ode_point
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass(frozen=True)
|
|
21
|
+
class IrregularSingularityInvariants:
|
|
22
|
+
"""Newton-theoretic invariants of an irregular scalar equation.
|
|
23
|
+
|
|
24
|
+
``katz_rank`` is the maximum positive Newton slope (the invariant
|
|
25
|
+
Poincare--Katz rank). ``irregularity`` is the sum of each positive
|
|
26
|
+
irregularity slope weighted by
|
|
27
|
+
the horizontal length of its edge. ``ramification_index`` is the least
|
|
28
|
+
common multiple of the denominators of those rational slopes.
|
|
29
|
+
|
|
30
|
+
``euler_system_poincare_rank`` is additionally reported for the natural
|
|
31
|
+
Euler-scaled companion system. Unlike the preceding three quantities it is
|
|
32
|
+
representation/gauge dependent, so it is named as such.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
point: sp.Expr
|
|
36
|
+
polygon: DifferentialNewtonPolygon
|
|
37
|
+
katz_rank: sp.Rational
|
|
38
|
+
irregularity: sp.Rational
|
|
39
|
+
ramification_index: int
|
|
40
|
+
euler_system_poincare_rank: sp.Rational
|
|
41
|
+
|
|
42
|
+
@property
|
|
43
|
+
def poincare_rank(self) -> sp.Rational:
|
|
44
|
+
"""Alias for the natural Euler-scaled companion-system rank."""
|
|
45
|
+
|
|
46
|
+
return self.euler_system_poincare_rank
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@dataclass(frozen=True)
|
|
50
|
+
class FormalExponentialPart:
|
|
51
|
+
"""Leading exponential part contributed by one irregular Newton edge."""
|
|
52
|
+
|
|
53
|
+
point: sp.Expr
|
|
54
|
+
edge: DifferentialNewtonEdge
|
|
55
|
+
characteristic_variable: sp.Symbol
|
|
56
|
+
characteristic_polynomial: sp.Expr
|
|
57
|
+
characteristic_root: sp.Expr
|
|
58
|
+
multiplicity: int
|
|
59
|
+
logarithmic_derivative: sp.Expr
|
|
60
|
+
local_exponent: sp.Expr
|
|
61
|
+
exponent: sp.Expr
|
|
62
|
+
ramification_index: int
|
|
63
|
+
|
|
64
|
+
@property
|
|
65
|
+
def exponential_factor(self) -> sp.Expr:
|
|
66
|
+
return sp.exp(self.exponent)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@dataclass(frozen=True)
|
|
70
|
+
class WKBAnsatz:
|
|
71
|
+
"""A WKB-type ansatz based on a Newton-polygon exponential part."""
|
|
72
|
+
|
|
73
|
+
point: sp.Expr
|
|
74
|
+
exponential_part: FormalExponentialPart
|
|
75
|
+
amplitude_function: sp.FunctionClass
|
|
76
|
+
local_coordinate: sp.Expr
|
|
77
|
+
local_expression: sp.Expr
|
|
78
|
+
expression: sp.Expr
|
|
79
|
+
conjugated_operator: LinearDifferentialOperator
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _known_roots(
|
|
83
|
+
poly: sp.Expr,
|
|
84
|
+
variable: sp.Symbol,
|
|
85
|
+
expected_mult: int,
|
|
86
|
+
) -> tuple[tuple[sp.Expr, int], ...]:
|
|
87
|
+
try:
|
|
88
|
+
roots = sp.roots(poly, variable, cubics=False, quartics=False)
|
|
89
|
+
except SYMBOLIC_FAILURES:
|
|
90
|
+
roots = {}
|
|
91
|
+
result = {
|
|
92
|
+
sp.simplify(root): int(mult)
|
|
93
|
+
for root, mult in roots.items()
|
|
94
|
+
if sp.simplify(root) != 0
|
|
95
|
+
}
|
|
96
|
+
if sum(result.values()) != expected_mult:
|
|
97
|
+
try:
|
|
98
|
+
all_roots = sp.Poly(poly, variable).all_roots(radicals=False)
|
|
99
|
+
except SYMBOLIC_FAILURES:
|
|
100
|
+
all_roots = []
|
|
101
|
+
if all_roots:
|
|
102
|
+
result = {}
|
|
103
|
+
for root in all_roots:
|
|
104
|
+
root = sp.simplify(root)
|
|
105
|
+
if root != 0:
|
|
106
|
+
result[root] = result.get(root, 0) + 1
|
|
107
|
+
if sum(result.values()) != expected_mult:
|
|
108
|
+
raise NotImplementedError(
|
|
109
|
+
"could not resolve all nonzero roots of an irregular-edge characteristic polynomial"
|
|
110
|
+
)
|
|
111
|
+
return tuple(sorted(result.items(), key=lambda item: sp.default_sort_key(item[0])))
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def irregular_singularity_invariants(
|
|
115
|
+
ode: sp.Expr | sp.Equality | LinearDifferentialOperator,
|
|
116
|
+
function: sp.FunctionClass | sp.Expr | None = None,
|
|
117
|
+
variable: sp.Symbol | None = None,
|
|
118
|
+
*,
|
|
119
|
+
point: sp.Expr = 0,
|
|
120
|
+
) -> IrregularSingularityInvariants:
|
|
121
|
+
"""Return Newton/Katz irregular-singularity invariants at ``point``."""
|
|
122
|
+
|
|
123
|
+
polygon = differential_newton_polygon(ode, function, variable, point=point)
|
|
124
|
+
kind = classify_ode_point(
|
|
125
|
+
polygon.localized.operator,
|
|
126
|
+
point=0,
|
|
127
|
+
).kind
|
|
128
|
+
if kind is not ODESingularityKind.IRREGULAR and not polygon.is_irregular:
|
|
129
|
+
raise ValueError(f"point {point!s} is not an irregular singular point")
|
|
130
|
+
return IrregularSingularityInvariants(
|
|
131
|
+
point=sp.sympify(point),
|
|
132
|
+
polygon=polygon,
|
|
133
|
+
katz_rank=polygon.katz_rank,
|
|
134
|
+
irregularity=polygon.irregularity,
|
|
135
|
+
ramification_index=polygon.ramification_index,
|
|
136
|
+
euler_system_poincare_rank=poincare_rank(polygon),
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def formal_exponential_parts(
|
|
141
|
+
ode: sp.Expr | sp.Equality | LinearDifferentialOperator,
|
|
142
|
+
function: sp.FunctionClass | sp.Expr | None = None,
|
|
143
|
+
variable: sp.Symbol | None = None,
|
|
144
|
+
*,
|
|
145
|
+
point: sp.Expr = 0,
|
|
146
|
+
) -> tuple[FormalExponentialPart, ...]:
|
|
147
|
+
"""Return leading formal exponential parts from irregular Newton edges.
|
|
148
|
+
|
|
149
|
+
For an edge of Newton/Katz slope ``rho > 0`` and edge characteristic root
|
|
150
|
+
``c``, the leading logarithmic derivative is ``c*h**(-(rho+1))``.
|
|
151
|
+
Integration gives the leading exponential exponent
|
|
152
|
+
|
|
153
|
+
``Q = -c*h**(-rho)/rho``.
|
|
154
|
+
|
|
155
|
+
This routine returns the Newton-determined *leading* formal
|
|
156
|
+
exponential parts. Lower terms in a full Hukuhara-Turrittin exponential
|
|
157
|
+
polynomial require recursive Riccati/factor analysis and are not guessed.
|
|
158
|
+
"""
|
|
159
|
+
|
|
160
|
+
polygon = differential_newton_polygon(ode, function, variable, point=point)
|
|
161
|
+
h = polygon.localized.local_variable
|
|
162
|
+
lam = sp.Symbol("lambda")
|
|
163
|
+
result: list[FormalExponentialPart] = []
|
|
164
|
+
|
|
165
|
+
for edge in polygon.irregular_edges:
|
|
166
|
+
characteristic = edge.characteristic_polynomial(lam)
|
|
167
|
+
for root, multiplicity in _known_roots(
|
|
168
|
+
characteristic, lam, edge.horizontal_length
|
|
169
|
+
):
|
|
170
|
+
rho = edge.slope
|
|
171
|
+
sigma = rho + 1
|
|
172
|
+
log_derivative = sp.simplify(root * h ** (-sigma))
|
|
173
|
+
local_q = sp.simplify(-root * h ** (-rho) / rho)
|
|
174
|
+
q = polygon.localized.to_original(local_q)
|
|
175
|
+
result.append(
|
|
176
|
+
FormalExponentialPart(
|
|
177
|
+
point=sp.sympify(point),
|
|
178
|
+
edge=edge,
|
|
179
|
+
characteristic_variable=lam,
|
|
180
|
+
characteristic_polynomial=characteristic,
|
|
181
|
+
characteristic_root=root,
|
|
182
|
+
multiplicity=multiplicity,
|
|
183
|
+
logarithmic_derivative=log_derivative,
|
|
184
|
+
local_exponent=local_q,
|
|
185
|
+
exponent=q,
|
|
186
|
+
ramification_index=int(edge.irregularity_slope.q),
|
|
187
|
+
)
|
|
188
|
+
)
|
|
189
|
+
return tuple(result)
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def _exponential_gauge_transform(
|
|
193
|
+
operator: LinearDifferentialOperator,
|
|
194
|
+
exponent: sp.Expr,
|
|
195
|
+
amplitude: sp.FunctionClass,
|
|
196
|
+
) -> LinearDifferentialOperator:
|
|
197
|
+
"""Return ``exp(-Q) L exp(Q)`` as an operator on the amplitude."""
|
|
198
|
+
|
|
199
|
+
x = operator.variable
|
|
200
|
+
v = amplitude(x)
|
|
201
|
+
factor = sp.exp(exponent)
|
|
202
|
+
transformed = sp.S.Zero
|
|
203
|
+
for j, coefficient in enumerate(operator.coefficients):
|
|
204
|
+
transformed += coefficient * sp.diff(factor * v, x, j) / factor
|
|
205
|
+
transformed = sp.expand(transformed)
|
|
206
|
+
if operator.inhomogeneous != 0:
|
|
207
|
+
transformed += operator.inhomogeneous / factor
|
|
208
|
+
return LinearDifferentialOperator.from_ode(transformed, amplitude, x)
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def wkb_ansatze(
|
|
212
|
+
ode: sp.Expr | sp.Equality | LinearDifferentialOperator,
|
|
213
|
+
function: sp.FunctionClass | sp.Expr | None = None,
|
|
214
|
+
variable: sp.Symbol | None = None,
|
|
215
|
+
*,
|
|
216
|
+
point: sp.Expr = 0,
|
|
217
|
+
) -> tuple[WKBAnsatz, ...]:
|
|
218
|
+
"""Construct leading WKB-type ansätze ``y = exp(Q) A``.
|
|
219
|
+
|
|
220
|
+
The returned conjugated operator is exact for the displayed leading Q and
|
|
221
|
+
is intended as the starting point for subsequent amplitude/Frobenius or
|
|
222
|
+
transseries analysis.
|
|
223
|
+
"""
|
|
224
|
+
|
|
225
|
+
polygon = differential_newton_polygon(ode, function, variable, point=point)
|
|
226
|
+
localized = polygon.localized
|
|
227
|
+
h = localized.local_variable
|
|
228
|
+
amplitude = sp.Function("_A")
|
|
229
|
+
parts = formal_exponential_parts(ode, function, variable, point=point)
|
|
230
|
+
ansatze: list[WKBAnsatz] = []
|
|
231
|
+
for part in parts:
|
|
232
|
+
local_expression = sp.exp(part.local_exponent) * amplitude(h)
|
|
233
|
+
conjugated = _exponential_gauge_transform(
|
|
234
|
+
localized.operator,
|
|
235
|
+
part.local_exponent,
|
|
236
|
+
amplitude,
|
|
237
|
+
)
|
|
238
|
+
original_expression = localized.to_original(local_expression)
|
|
239
|
+
ansatze.append(
|
|
240
|
+
WKBAnsatz(
|
|
241
|
+
point=sp.sympify(point),
|
|
242
|
+
exponential_part=part,
|
|
243
|
+
amplitude_function=amplitude,
|
|
244
|
+
local_coordinate=h,
|
|
245
|
+
local_expression=local_expression,
|
|
246
|
+
expression=original_expression,
|
|
247
|
+
conjugated_operator=conjugated,
|
|
248
|
+
)
|
|
249
|
+
)
|
|
250
|
+
return tuple(ansatze)
|