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,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)
@@ -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)