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
odeanalysis/fuchsian.py
ADDED
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
"""Global Fuchsian invariants, Riemann schemes, and apparent singularities."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
|
+
|
|
8
|
+
import sympy as sp
|
|
9
|
+
from funcprops import normalize_assumptions
|
|
10
|
+
|
|
11
|
+
from .formal_basis import FormalBasisError, logarithmic_frobenius_basis
|
|
12
|
+
from .operator import LinearDifferentialOperator, _coerce_linear_operator
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from .frobenius import FrobeniusAnalysis
|
|
16
|
+
from .local_structure import FrobeniusLocalMonodromy
|
|
17
|
+
|
|
18
|
+
from .singularities import (
|
|
19
|
+
ODESingularity,
|
|
20
|
+
ODESingularityKind,
|
|
21
|
+
analyze_ode_singularities,
|
|
22
|
+
classify_ode_point,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass(frozen=True)
|
|
27
|
+
class ApparentSingularityAnalysis:
|
|
28
|
+
"""Decision data for whether a regular singular point is apparent.
|
|
29
|
+
|
|
30
|
+
``apparent`` is ``True`` only when the complete local exponent multiset is
|
|
31
|
+
nonnegative integral and a complete logarithmic Frobenius basis has no
|
|
32
|
+
logarithms. ``None`` means the symbolic data were insufficient to decide.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
point: sp.Expr
|
|
36
|
+
kind: ODESingularityKind
|
|
37
|
+
apparent: bool | None
|
|
38
|
+
exponents: tuple[sp.Expr, ...]
|
|
39
|
+
has_logarithms: bool | None
|
|
40
|
+
basis_dimension: int | None
|
|
41
|
+
reason: str
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclass(frozen=True)
|
|
45
|
+
class RiemannSchemePoint:
|
|
46
|
+
"""One singular column of a scalar Fuchsian Riemann scheme."""
|
|
47
|
+
|
|
48
|
+
point: sp.Expr
|
|
49
|
+
exponents: tuple[sp.Expr, ...]
|
|
50
|
+
indicial_polynomial: sp.Expr
|
|
51
|
+
apparent: bool | None = None
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@dataclass(frozen=True)
|
|
55
|
+
class FuchsRelation:
|
|
56
|
+
"""Exact exponent-sum relation for a scalar Fuchsian equation."""
|
|
57
|
+
|
|
58
|
+
order: int
|
|
59
|
+
singularity_count: int
|
|
60
|
+
exponent_sum: sp.Expr
|
|
61
|
+
expected_sum: sp.Expr
|
|
62
|
+
residual: sp.Expr
|
|
63
|
+
holds: bool | None
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@dataclass(frozen=True)
|
|
67
|
+
class RiemannScheme:
|
|
68
|
+
"""Riemann scheme of a scalar equation Fuchsian on the Riemann sphere."""
|
|
69
|
+
|
|
70
|
+
order: int
|
|
71
|
+
variable: sp.Symbol
|
|
72
|
+
singularities: tuple[RiemannSchemePoint, ...]
|
|
73
|
+
fuchs_relation: FuchsRelation
|
|
74
|
+
|
|
75
|
+
@property
|
|
76
|
+
def points(self) -> tuple[sp.Expr, ...]:
|
|
77
|
+
"""Return singular points in the order used by the scheme."""
|
|
78
|
+
|
|
79
|
+
return tuple(item.point for item in self.singularities)
|
|
80
|
+
|
|
81
|
+
@property
|
|
82
|
+
def exponent_columns(self) -> tuple[tuple[sp.Expr, ...], ...]:
|
|
83
|
+
"""Return one local-exponent column per singular point."""
|
|
84
|
+
|
|
85
|
+
return tuple(item.exponents for item in self.singularities)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _zero_decision(expression: sp.Expr) -> bool | None:
|
|
89
|
+
value = sp.simplify(expression)
|
|
90
|
+
if value == 0 or value.is_zero is True:
|
|
91
|
+
return True
|
|
92
|
+
if value.is_zero is False:
|
|
93
|
+
return False
|
|
94
|
+
return None
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _complete_exponents(singularity: ODESingularity) -> tuple[sp.Expr, ...] | None:
|
|
98
|
+
if singularity.indicial_polynomial is None:
|
|
99
|
+
return None
|
|
100
|
+
r = sp.Symbol("r")
|
|
101
|
+
try:
|
|
102
|
+
roots = sp.roots(
|
|
103
|
+
singularity.indicial_polynomial, r, cubics=False, quartics=False
|
|
104
|
+
)
|
|
105
|
+
except (NotImplementedError, TypeError, ValueError, sp.PolynomialError):
|
|
106
|
+
roots = {}
|
|
107
|
+
if roots and sum(int(mult) for mult in roots.values()) == singularity.order:
|
|
108
|
+
return tuple(
|
|
109
|
+
root
|
|
110
|
+
for root, mult in sorted(
|
|
111
|
+
roots.items(), key=lambda item: sp.default_sort_key(item[0])
|
|
112
|
+
)
|
|
113
|
+
for _ in range(int(mult))
|
|
114
|
+
)
|
|
115
|
+
if len(singularity.indicial_roots) == singularity.order:
|
|
116
|
+
return tuple(sorted(singularity.indicial_roots, key=sp.default_sort_key))
|
|
117
|
+
return None
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def apparent_singularity_analysis(
|
|
121
|
+
ode: sp.Expr | sp.Equality | LinearDifferentialOperator,
|
|
122
|
+
function: sp.FunctionClass | sp.Expr | None = None,
|
|
123
|
+
variable: sp.Symbol | None = None,
|
|
124
|
+
*,
|
|
125
|
+
point: sp.Expr = 0,
|
|
126
|
+
assumptions: sp.Expr | bool = True,
|
|
127
|
+
frobenius: FrobeniusAnalysis | None = None,
|
|
128
|
+
monodromy: FrobeniusLocalMonodromy | None = None,
|
|
129
|
+
) -> ApparentSingularityAnalysis:
|
|
130
|
+
"""Determine whether ``point`` is an apparent regular singularity.
|
|
131
|
+
|
|
132
|
+
Completed Frobenius and monodromy evidence is reused when supplied, so the
|
|
133
|
+
apparentness decision cannot diverge from the unified local analysis.
|
|
134
|
+
"""
|
|
135
|
+
operator = _coerce_linear_operator(ode, function, variable)
|
|
136
|
+
assumptions = normalize_assumptions(assumptions)
|
|
137
|
+
requested_point = sp.sympify(point)
|
|
138
|
+
if requested_point == sp.oo:
|
|
139
|
+
from .newton import localize_operator
|
|
140
|
+
|
|
141
|
+
localized = localize_operator(operator, point=sp.oo)
|
|
142
|
+
local = classify_ode_point(localized.operator, point=0, assumptions=assumptions)
|
|
143
|
+
else:
|
|
144
|
+
local = classify_ode_point(
|
|
145
|
+
operator, point=requested_point, assumptions=assumptions
|
|
146
|
+
)
|
|
147
|
+
if local.kind is ODESingularityKind.ORDINARY:
|
|
148
|
+
return ApparentSingularityAnalysis(
|
|
149
|
+
requested_point,
|
|
150
|
+
local.kind,
|
|
151
|
+
False,
|
|
152
|
+
(),
|
|
153
|
+
False,
|
|
154
|
+
operator.order,
|
|
155
|
+
"the point is ordinary rather than a singularity",
|
|
156
|
+
)
|
|
157
|
+
if local.kind is ODESingularityKind.IRREGULAR:
|
|
158
|
+
return ApparentSingularityAnalysis(
|
|
159
|
+
requested_point,
|
|
160
|
+
local.kind,
|
|
161
|
+
False,
|
|
162
|
+
(),
|
|
163
|
+
None,
|
|
164
|
+
None,
|
|
165
|
+
"an irregular singularity cannot be apparent",
|
|
166
|
+
)
|
|
167
|
+
if local.kind is ODESingularityKind.UNKNOWN:
|
|
168
|
+
return ApparentSingularityAnalysis(
|
|
169
|
+
requested_point,
|
|
170
|
+
local.kind,
|
|
171
|
+
None,
|
|
172
|
+
(),
|
|
173
|
+
None,
|
|
174
|
+
None,
|
|
175
|
+
"the local singularity type is unresolved",
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
exponents = (
|
|
179
|
+
tuple(frobenius.roots)
|
|
180
|
+
if frobenius is not None and len(frobenius.roots) == operator.order
|
|
181
|
+
else _complete_exponents(local)
|
|
182
|
+
)
|
|
183
|
+
if exponents is None:
|
|
184
|
+
return ApparentSingularityAnalysis(
|
|
185
|
+
requested_point,
|
|
186
|
+
local.kind,
|
|
187
|
+
None,
|
|
188
|
+
(),
|
|
189
|
+
None,
|
|
190
|
+
None,
|
|
191
|
+
"the complete indicial root multiset is unresolved",
|
|
192
|
+
)
|
|
193
|
+
for exponent in exponents:
|
|
194
|
+
if exponent.is_integer is False or exponent.is_nonnegative is False:
|
|
195
|
+
return ApparentSingularityAnalysis(
|
|
196
|
+
requested_point,
|
|
197
|
+
local.kind,
|
|
198
|
+
False,
|
|
199
|
+
exponents,
|
|
200
|
+
None,
|
|
201
|
+
None,
|
|
202
|
+
"at least one local exponent is not a nonnegative integer",
|
|
203
|
+
)
|
|
204
|
+
if exponent.is_integer is not True or exponent.is_nonnegative is not True:
|
|
205
|
+
return ApparentSingularityAnalysis(
|
|
206
|
+
requested_point,
|
|
207
|
+
local.kind,
|
|
208
|
+
None,
|
|
209
|
+
exponents,
|
|
210
|
+
None,
|
|
211
|
+
None,
|
|
212
|
+
"integrality or nonnegativity of a local exponent is unresolved",
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
if monodromy is not None and monodromy.certified and monodromy.matrix is not None:
|
|
216
|
+
identity = sp.eye(operator.order)
|
|
217
|
+
trivial = all(
|
|
218
|
+
_zero_decision(monodromy.matrix[i, j] - identity[i, j]) is True
|
|
219
|
+
for i in range(operator.order)
|
|
220
|
+
for j in range(operator.order)
|
|
221
|
+
)
|
|
222
|
+
if not trivial:
|
|
223
|
+
return ApparentSingularityAnalysis(
|
|
224
|
+
requested_point,
|
|
225
|
+
local.kind,
|
|
226
|
+
False,
|
|
227
|
+
exponents,
|
|
228
|
+
monodromy.logarithmic,
|
|
229
|
+
operator.order,
|
|
230
|
+
"certified local monodromy is nontrivial",
|
|
231
|
+
)
|
|
232
|
+
if monodromy.logarithmic is False:
|
|
233
|
+
return ApparentSingularityAnalysis(
|
|
234
|
+
requested_point,
|
|
235
|
+
local.kind,
|
|
236
|
+
True,
|
|
237
|
+
exponents,
|
|
238
|
+
False,
|
|
239
|
+
operator.order,
|
|
240
|
+
"nonnegative integral exponents and trivial certified local monodromy",
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
if frobenius is not None and frobenius.logarithm_required:
|
|
244
|
+
return ApparentSingularityAnalysis(
|
|
245
|
+
requested_point,
|
|
246
|
+
local.kind,
|
|
247
|
+
False,
|
|
248
|
+
exponents,
|
|
249
|
+
True,
|
|
250
|
+
len(frobenius.branches),
|
|
251
|
+
"the completed Frobenius recurrence has a logarithmic obstruction",
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
integer_exponents = [int(exponent) for exponent in exponents]
|
|
255
|
+
resonance_span = max(integer_exponents) - min(integer_exponents) if exponents else 0
|
|
256
|
+
terms = max(8, operator.order + resonance_span + 3)
|
|
257
|
+
basis_operator = frobenius.operator if frobenius is not None else operator
|
|
258
|
+
basis_point = frobenius.point if frobenius is not None else requested_point
|
|
259
|
+
try:
|
|
260
|
+
basis = logarithmic_frobenius_basis(
|
|
261
|
+
basis_operator, point=basis_point, terms=terms
|
|
262
|
+
)
|
|
263
|
+
except (FormalBasisError, NotImplementedError, ValueError):
|
|
264
|
+
return ApparentSingularityAnalysis(
|
|
265
|
+
requested_point,
|
|
266
|
+
local.kind,
|
|
267
|
+
None,
|
|
268
|
+
exponents,
|
|
269
|
+
None,
|
|
270
|
+
None,
|
|
271
|
+
"a complete logarithmic Frobenius basis could not be certified",
|
|
272
|
+
)
|
|
273
|
+
if not basis.complete or basis.dimension != operator.order:
|
|
274
|
+
return ApparentSingularityAnalysis(
|
|
275
|
+
requested_point,
|
|
276
|
+
local.kind,
|
|
277
|
+
None,
|
|
278
|
+
exponents,
|
|
279
|
+
basis.has_logarithms,
|
|
280
|
+
basis.dimension,
|
|
281
|
+
"the local Frobenius basis is incomplete",
|
|
282
|
+
)
|
|
283
|
+
if basis.has_logarithms:
|
|
284
|
+
return ApparentSingularityAnalysis(
|
|
285
|
+
requested_point,
|
|
286
|
+
local.kind,
|
|
287
|
+
False,
|
|
288
|
+
exponents,
|
|
289
|
+
True,
|
|
290
|
+
basis.dimension,
|
|
291
|
+
"logarithmic local solutions give nontrivial local monodromy",
|
|
292
|
+
)
|
|
293
|
+
return ApparentSingularityAnalysis(
|
|
294
|
+
requested_point,
|
|
295
|
+
local.kind,
|
|
296
|
+
True,
|
|
297
|
+
exponents,
|
|
298
|
+
False,
|
|
299
|
+
basis.dimension,
|
|
300
|
+
"all local solutions are holomorphic and single-valued",
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def is_apparent_singularity(
|
|
305
|
+
ode: sp.Expr | sp.Equality | LinearDifferentialOperator,
|
|
306
|
+
function: sp.FunctionClass | sp.Expr | None = None,
|
|
307
|
+
variable: sp.Symbol | None = None,
|
|
308
|
+
*,
|
|
309
|
+
point: sp.Expr = 0,
|
|
310
|
+
assumptions: sp.Expr | bool = True,
|
|
311
|
+
) -> bool | None:
|
|
312
|
+
"""Return ``True``, ``False``, or ``None`` for apparentness at ``point``."""
|
|
313
|
+
|
|
314
|
+
return apparent_singularity_analysis(
|
|
315
|
+
ode, function, variable, point=point, assumptions=assumptions
|
|
316
|
+
).apparent
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def _relation_from_points(
|
|
320
|
+
order: int,
|
|
321
|
+
points: tuple[RiemannSchemePoint, ...],
|
|
322
|
+
) -> FuchsRelation:
|
|
323
|
+
exponent_sum = sp.simplify(sum((sum(item.exponents) for item in points), sp.S.Zero))
|
|
324
|
+
expected = sp.Rational((len(points) - 2) * order * (order - 1), 2)
|
|
325
|
+
residual = sp.simplify(exponent_sum - expected)
|
|
326
|
+
return FuchsRelation(
|
|
327
|
+
order=order,
|
|
328
|
+
singularity_count=len(points),
|
|
329
|
+
exponent_sum=exponent_sum,
|
|
330
|
+
expected_sum=expected,
|
|
331
|
+
residual=residual,
|
|
332
|
+
holds=_zero_decision(residual),
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def riemann_scheme(
|
|
337
|
+
ode: sp.Expr | sp.Equality | LinearDifferentialOperator,
|
|
338
|
+
function: sp.FunctionClass | sp.Expr | None = None,
|
|
339
|
+
variable: sp.Symbol | None = None,
|
|
340
|
+
) -> RiemannScheme:
|
|
341
|
+
"""Return the Riemann scheme of an equation Fuchsian on the sphere.
|
|
342
|
+
|
|
343
|
+
Every singular point, including infinity when singular, must be regular
|
|
344
|
+
singular with a completely resolved indicial root multiset. Irregular or
|
|
345
|
+
unresolved singularities are rejected rather than being displayed in a
|
|
346
|
+
misleading P-symbol.
|
|
347
|
+
"""
|
|
348
|
+
|
|
349
|
+
operator = _coerce_linear_operator(ode, function, variable)
|
|
350
|
+
if not operator.is_homogeneous:
|
|
351
|
+
raise ValueError("Riemann schemes require a homogeneous linear equation")
|
|
352
|
+
analysis = analyze_ode_singularities(operator, include_infinity=True)
|
|
353
|
+
singularities = list(analysis.finite)
|
|
354
|
+
if (
|
|
355
|
+
analysis.infinity is not None
|
|
356
|
+
and analysis.infinity.kind is not ODESingularityKind.ORDINARY
|
|
357
|
+
):
|
|
358
|
+
singularities.append(analysis.infinity)
|
|
359
|
+
|
|
360
|
+
points: list[RiemannSchemePoint] = []
|
|
361
|
+
for singularity in singularities:
|
|
362
|
+
if singularity.kind is not ODESingularityKind.REGULAR:
|
|
363
|
+
raise ValueError(
|
|
364
|
+
f"Riemann scheme requires a Fuchsian equation; {singularity.point!s} "
|
|
365
|
+
f"is {singularity.kind.value}"
|
|
366
|
+
)
|
|
367
|
+
exponents = _complete_exponents(singularity)
|
|
368
|
+
if exponents is None or singularity.indicial_polynomial is None:
|
|
369
|
+
raise NotImplementedError(
|
|
370
|
+
f"could not resolve the complete indicial data at {singularity.point!s}"
|
|
371
|
+
)
|
|
372
|
+
apparent = apparent_singularity_analysis(
|
|
373
|
+
operator, point=singularity.point
|
|
374
|
+
).apparent
|
|
375
|
+
points.append(
|
|
376
|
+
RiemannSchemePoint(
|
|
377
|
+
point=singularity.point,
|
|
378
|
+
exponents=exponents,
|
|
379
|
+
indicial_polynomial=singularity.indicial_polynomial,
|
|
380
|
+
apparent=apparent,
|
|
381
|
+
)
|
|
382
|
+
)
|
|
383
|
+
|
|
384
|
+
scheme_points = tuple(points)
|
|
385
|
+
return RiemannScheme(
|
|
386
|
+
order=operator.order,
|
|
387
|
+
variable=operator.variable,
|
|
388
|
+
singularities=scheme_points,
|
|
389
|
+
fuchs_relation=_relation_from_points(operator.order, scheme_points),
|
|
390
|
+
)
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
def fuchs_relation(
|
|
394
|
+
ode: sp.Expr | sp.Equality | LinearDifferentialOperator,
|
|
395
|
+
function: sp.FunctionClass | sp.Expr | None = None,
|
|
396
|
+
variable: sp.Symbol | None = None,
|
|
397
|
+
) -> FuchsRelation:
|
|
398
|
+
"""Return the exact Fuchs exponent-sum relation for a Fuchsian equation."""
|
|
399
|
+
|
|
400
|
+
return riemann_scheme(ode, function, variable).fuchs_relation
|