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,1009 @@
|
|
|
1
|
+
"""Logarithmic formal bases and formal monodromy.
|
|
2
|
+
|
|
3
|
+
The regular-singular construction uses a parameterized Frobenius family.
|
|
4
|
+
If ``r`` is a free indicial parameter, the coefficients ``a_m(r)`` are
|
|
5
|
+
chosen so that all positive-order Frobenius equations vanish. At an
|
|
6
|
+
indicial root ``r0`` the coefficient family can have poles when another root
|
|
7
|
+
is resonant. Multiplying by the minimal power ``(r-r0)**p`` regularizes the
|
|
8
|
+
family; derivatives of orders ``p, ..., p+m-1`` at a root of multiplicity
|
|
9
|
+
``m`` give the logarithmic companions. This treats repeated roots and
|
|
10
|
+
integer-difference resonance in one construction.
|
|
11
|
+
|
|
12
|
+
For irregular problems, resolved simple exponential branches are exposed as
|
|
13
|
+
one-dimensional formal blocks. Repeated completed exponential factors are
|
|
14
|
+
isolated through the first-order formal block decomposition, scalarized by a
|
|
15
|
+
cyclic physical output, and then passed through the same logarithmic Frobenius
|
|
16
|
+
construction after their common exponential has been removed.
|
|
17
|
+
|
|
18
|
+
Formal monodromy is computed from the resulting truncated formal basis. On a
|
|
19
|
+
ramified cover ``h=t**r`` the always-defined ``cover_matrix`` corresponds to a
|
|
20
|
+
full turn in ``t`` (hence ``r`` turns in ``h``). ``local_matrix`` is also
|
|
21
|
+
reported when every block is unramified; physical one-turn monodromy of a
|
|
22
|
+
ramified problem additionally permutes cover sheets and is not
|
|
23
|
+
guessed here.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
from dataclasses import dataclass
|
|
29
|
+
from math import factorial, lcm
|
|
30
|
+
|
|
31
|
+
import sympy as sp
|
|
32
|
+
|
|
33
|
+
from ._local import pole_order as _shared_pole_order
|
|
34
|
+
from ._power_simplify import analytic_powsimp
|
|
35
|
+
from ._symbolic_compare import expressions_equal
|
|
36
|
+
from ._symbolic_errors import SYMBOLIC_FAILURES
|
|
37
|
+
from .formal import (
|
|
38
|
+
CompleteFormalExponentialPart,
|
|
39
|
+
complete_formal_exponential_parts,
|
|
40
|
+
formal_amplitude_series,
|
|
41
|
+
)
|
|
42
|
+
from .frobenius import (
|
|
43
|
+
_indicial_polynomial,
|
|
44
|
+
_regularized_coefficients,
|
|
45
|
+
_root_data,
|
|
46
|
+
_taylor_coeff,
|
|
47
|
+
)
|
|
48
|
+
from .newton import (
|
|
49
|
+
LocalizedOperator,
|
|
50
|
+
localize_operator,
|
|
51
|
+
)
|
|
52
|
+
from .operator import LinearDifferentialOperator
|
|
53
|
+
from .singularities import ODESingularityKind, classify_ode_point
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class FormalBasisError(NotImplementedError):
|
|
57
|
+
"""Raised when a complete logarithmic formal basis cannot be certified."""
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@dataclass(frozen=True)
|
|
61
|
+
class LogarithmicBasisVector:
|
|
62
|
+
"""One vector in a truncated logarithmic formal basis.
|
|
63
|
+
|
|
64
|
+
``parameter_expression`` omits the exponential polynomial and is written
|
|
65
|
+
in the block uniformizer. ``local_expression`` includes the exponential
|
|
66
|
+
factor and is written in the local coordinate ``h``.
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
source_exponent: sp.Expr
|
|
70
|
+
exponent: sp.Expr
|
|
71
|
+
ramified_exponent: sp.Expr
|
|
72
|
+
root_multiplicity: int
|
|
73
|
+
pole_order: int
|
|
74
|
+
derivative_order: int
|
|
75
|
+
logarithmic_degree: int
|
|
76
|
+
local_parameter: sp.Symbol
|
|
77
|
+
parameter_expression: sp.Expr
|
|
78
|
+
local_expression: sp.Expr
|
|
79
|
+
expression: sp.Expr
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@dataclass(frozen=True)
|
|
83
|
+
class FormalSolutionBlock:
|
|
84
|
+
"""A formal block sharing one exponential polynomial."""
|
|
85
|
+
|
|
86
|
+
local_exponential_polynomial: sp.Expr
|
|
87
|
+
exponential_polynomial: sp.Expr
|
|
88
|
+
ramification_index: int
|
|
89
|
+
local_parameter: sp.Symbol
|
|
90
|
+
basis_vectors: tuple[LogarithmicBasisVector, ...]
|
|
91
|
+
|
|
92
|
+
@property
|
|
93
|
+
def dimension(self) -> int:
|
|
94
|
+
return len(self.basis_vectors)
|
|
95
|
+
|
|
96
|
+
@property
|
|
97
|
+
def has_logarithms(self) -> bool:
|
|
98
|
+
return any(vector.logarithmic_degree > 0 for vector in self.basis_vectors)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
@dataclass(frozen=True)
|
|
102
|
+
class FormalLogarithmicBasis:
|
|
103
|
+
"""A block-decomposed truncated formal basis at a local point."""
|
|
104
|
+
|
|
105
|
+
point: sp.Expr
|
|
106
|
+
local_coordinate: sp.Symbol
|
|
107
|
+
blocks: tuple[FormalSolutionBlock, ...]
|
|
108
|
+
terms: int
|
|
109
|
+
operator_order: int
|
|
110
|
+
complete: bool = True
|
|
111
|
+
limitation: str | None = None
|
|
112
|
+
|
|
113
|
+
@property
|
|
114
|
+
def vectors(self) -> tuple[LogarithmicBasisVector, ...]:
|
|
115
|
+
return tuple(vector for block in self.blocks for vector in block.basis_vectors)
|
|
116
|
+
|
|
117
|
+
@property
|
|
118
|
+
def dimension(self) -> int:
|
|
119
|
+
return len(self.vectors)
|
|
120
|
+
|
|
121
|
+
@property
|
|
122
|
+
def has_logarithms(self) -> bool:
|
|
123
|
+
return any(block.has_logarithms for block in self.blocks)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
@dataclass(frozen=True)
|
|
127
|
+
class FormalMonodromy:
|
|
128
|
+
"""Formal monodromy in the ordering of ``basis.vectors``.
|
|
129
|
+
|
|
130
|
+
Matrix columns are images of basis vectors under positive analytic
|
|
131
|
+
continuation. ``cover_matrix`` is block diagonal and corresponds to one
|
|
132
|
+
full turn of each block uniformizer. When all blocks are unramified this
|
|
133
|
+
is also the one-turn local monodromy and is returned as ``local_matrix``.
|
|
134
|
+
"""
|
|
135
|
+
|
|
136
|
+
basis: FormalLogarithmicBasis
|
|
137
|
+
cover_matrix: sp.Matrix
|
|
138
|
+
local_matrix: sp.Matrix | None
|
|
139
|
+
|
|
140
|
+
@property
|
|
141
|
+
def matrix(self) -> sp.Matrix:
|
|
142
|
+
"""Prefer physical local monodromy when available, otherwise the cover matrix."""
|
|
143
|
+
|
|
144
|
+
return self.local_matrix if self.local_matrix is not None else self.cover_matrix
|
|
145
|
+
|
|
146
|
+
@property
|
|
147
|
+
def eigenvalues(self) -> tuple[sp.Expr, ...]:
|
|
148
|
+
values: list[sp.Expr] = []
|
|
149
|
+
for value, multiplicity in self.matrix.eigenvals().items():
|
|
150
|
+
values.extend([sp.simplify(value)] * int(multiplicity))
|
|
151
|
+
return tuple(sorted(values, key=sp.default_sort_key))
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
@dataclass(frozen=True)
|
|
155
|
+
class _ParameterizedFrobeniusFamily:
|
|
156
|
+
operator: LinearDifferentialOperator
|
|
157
|
+
exponent_variable: sp.Symbol
|
|
158
|
+
indicial_polynomial: sp.Expr
|
|
159
|
+
roots: tuple[tuple[sp.Expr, int], ...]
|
|
160
|
+
coefficients: tuple[sp.Expr, ...]
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _parameterized_frobenius_family(
|
|
164
|
+
operator: LinearDifferentialOperator,
|
|
165
|
+
*,
|
|
166
|
+
terms: int,
|
|
167
|
+
) -> _ParameterizedFrobeniusFamily:
|
|
168
|
+
if terms < 1:
|
|
169
|
+
raise ValueError("terms must be at least one")
|
|
170
|
+
if not operator.is_homogeneous:
|
|
171
|
+
raise ValueError("formal basis construction requires a homogeneous operator")
|
|
172
|
+
|
|
173
|
+
kind = classify_ode_point(operator, point=0).kind
|
|
174
|
+
if kind not in (ODESingularityKind.ORDINARY, ODESingularityKind.REGULAR):
|
|
175
|
+
raise FormalBasisError(
|
|
176
|
+
"parameterized Frobenius construction requires an ordinary or regular singular operator"
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
x = operator.variable
|
|
180
|
+
r = sp.Dummy("r")
|
|
181
|
+
b = _regularized_coefficients(operator, sp.S.Zero)
|
|
182
|
+
indicial = _indicial_polynomial(b, x, sp.S.Zero, r)
|
|
183
|
+
roots = _root_data(indicial, r)
|
|
184
|
+
if sum(int(mult) for _, mult in roots) != operator.order:
|
|
185
|
+
raise FormalBasisError("could not resolve the complete indicial root multiset")
|
|
186
|
+
|
|
187
|
+
bcoeff = [
|
|
188
|
+
tuple(_taylor_coeff(bj, x, sp.S.Zero, q) for q in range(terms)) for bj in b
|
|
189
|
+
]
|
|
190
|
+
coefficients: list[sp.Expr] = [sp.S.One]
|
|
191
|
+
for m in range(1, terms):
|
|
192
|
+
numerator = sp.S.Zero
|
|
193
|
+
for q in range(1, m + 1):
|
|
194
|
+
inner = sum(bcoeff[j][q] * sp.ff(r + m - q, j) for j in range(len(b)))
|
|
195
|
+
numerator += coefficients[m - q] * inner
|
|
196
|
+
denominator = sp.simplify(indicial.subs(r, r + m))
|
|
197
|
+
coefficients.append(sp.cancel(-numerator / denominator))
|
|
198
|
+
|
|
199
|
+
return _ParameterizedFrobeniusFamily(
|
|
200
|
+
operator=operator,
|
|
201
|
+
exponent_variable=r,
|
|
202
|
+
indicial_polynomial=indicial,
|
|
203
|
+
roots=roots,
|
|
204
|
+
coefficients=tuple(coefficients),
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def _pole_order_at(expr: sp.Expr, variable: sp.Symbol, point: sp.Expr) -> int:
|
|
209
|
+
"""Return a certified meromorphic pole order using shared local valuation logic."""
|
|
210
|
+
order = _shared_pole_order(expr, variable, point)
|
|
211
|
+
if order is None:
|
|
212
|
+
raise ValueError(f"could not determine pole order at {point}")
|
|
213
|
+
return order
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def _logarithmic_degree(expr: sp.Expr, variable: sp.Symbol) -> int:
|
|
217
|
+
logx = sp.log(variable)
|
|
218
|
+
try:
|
|
219
|
+
poly = sp.Poly(sp.expand(expr), logx)
|
|
220
|
+
except sp.PolynomialError:
|
|
221
|
+
return 0
|
|
222
|
+
return max(0, int(poly.degree()))
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def _regularized_family_derivative(
|
|
226
|
+
family: _ParameterizedFrobeniusFamily,
|
|
227
|
+
root: sp.Expr,
|
|
228
|
+
pole_order: int,
|
|
229
|
+
derivative_order: int,
|
|
230
|
+
) -> sp.Expr:
|
|
231
|
+
x = family.operator.variable
|
|
232
|
+
r = family.exponent_variable
|
|
233
|
+
result = sp.S.Zero
|
|
234
|
+
for m, coefficient in enumerate(family.coefficients):
|
|
235
|
+
term = (r - root) ** pole_order * coefficient * x ** (r + m)
|
|
236
|
+
differentiated = sp.diff(term, r, derivative_order) / factorial(
|
|
237
|
+
derivative_order
|
|
238
|
+
)
|
|
239
|
+
try:
|
|
240
|
+
value = sp.limit(differentiated, r, root)
|
|
241
|
+
except SYMBOLIC_FAILURES as exc:
|
|
242
|
+
raise FormalBasisError(
|
|
243
|
+
f"could not regularize Frobenius family at indicial root {root!s}"
|
|
244
|
+
) from exc
|
|
245
|
+
result += value
|
|
246
|
+
return sp.expand(sp.simplify(result))
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def _basis_for_operator_at_zero(
|
|
250
|
+
operator: LinearDifferentialOperator,
|
|
251
|
+
*,
|
|
252
|
+
terms: int,
|
|
253
|
+
) -> tuple[LogarithmicBasisVector, ...]:
|
|
254
|
+
family = _parameterized_frobenius_family(operator, terms=terms)
|
|
255
|
+
x = operator.variable
|
|
256
|
+
vectors: list[LogarithmicBasisVector] = []
|
|
257
|
+
for root, multiplicity in family.roots:
|
|
258
|
+
pole_order = max(
|
|
259
|
+
(
|
|
260
|
+
_pole_order_at(c, family.exponent_variable, root)
|
|
261
|
+
for c in family.coefficients
|
|
262
|
+
),
|
|
263
|
+
default=0,
|
|
264
|
+
)
|
|
265
|
+
for offset in range(int(multiplicity)):
|
|
266
|
+
derivative_order = pole_order + offset
|
|
267
|
+
expression = _regularized_family_derivative(
|
|
268
|
+
family,
|
|
269
|
+
root,
|
|
270
|
+
pole_order,
|
|
271
|
+
derivative_order,
|
|
272
|
+
)
|
|
273
|
+
vectors.append(
|
|
274
|
+
LogarithmicBasisVector(
|
|
275
|
+
source_exponent=sp.simplify(root),
|
|
276
|
+
exponent=sp.simplify(root),
|
|
277
|
+
ramified_exponent=sp.simplify(root),
|
|
278
|
+
root_multiplicity=int(multiplicity),
|
|
279
|
+
pole_order=pole_order,
|
|
280
|
+
derivative_order=derivative_order,
|
|
281
|
+
logarithmic_degree=_logarithmic_degree(expression, x),
|
|
282
|
+
local_parameter=x,
|
|
283
|
+
parameter_expression=expression,
|
|
284
|
+
local_expression=expression,
|
|
285
|
+
expression=expression,
|
|
286
|
+
)
|
|
287
|
+
)
|
|
288
|
+
return tuple(vectors)
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def logarithmic_frobenius_basis(
|
|
292
|
+
ode: sp.Expr | sp.Equality | LinearDifferentialOperator,
|
|
293
|
+
function: sp.FunctionClass | sp.Expr | None = None,
|
|
294
|
+
variable: sp.Symbol | None = None,
|
|
295
|
+
*,
|
|
296
|
+
point: sp.Expr = 0,
|
|
297
|
+
terms: int = 8,
|
|
298
|
+
) -> FormalLogarithmicBasis:
|
|
299
|
+
"""Return a logarithmic Frobenius basis at an ordinary/regular singular point.
|
|
300
|
+
|
|
301
|
+
Repeated indicial roots and integer-difference resonances are handled by
|
|
302
|
+
regularizing a parameterized Frobenius family and differentiating with
|
|
303
|
+
respect to its exponent. This directly produces powers of ``log(h)``
|
|
304
|
+
instead of merely flagging that logarithms may be required.
|
|
305
|
+
"""
|
|
306
|
+
|
|
307
|
+
localized = localize_operator(ode, function, variable, point=point)
|
|
308
|
+
vectors0 = _basis_for_operator_at_zero(localized.operator, terms=terms)
|
|
309
|
+
h = localized.local_variable
|
|
310
|
+
vectors = tuple(
|
|
311
|
+
LogarithmicBasisVector(
|
|
312
|
+
source_exponent=v.source_exponent,
|
|
313
|
+
exponent=v.exponent,
|
|
314
|
+
ramified_exponent=v.ramified_exponent,
|
|
315
|
+
root_multiplicity=v.root_multiplicity,
|
|
316
|
+
pole_order=v.pole_order,
|
|
317
|
+
derivative_order=v.derivative_order,
|
|
318
|
+
logarithmic_degree=v.logarithmic_degree,
|
|
319
|
+
local_parameter=h,
|
|
320
|
+
parameter_expression=v.parameter_expression,
|
|
321
|
+
local_expression=v.local_expression,
|
|
322
|
+
expression=localized.to_original(v.local_expression),
|
|
323
|
+
)
|
|
324
|
+
for v in vectors0
|
|
325
|
+
)
|
|
326
|
+
block = FormalSolutionBlock(
|
|
327
|
+
local_exponential_polynomial=sp.S.Zero,
|
|
328
|
+
exponential_polynomial=sp.S.Zero,
|
|
329
|
+
ramification_index=1,
|
|
330
|
+
local_parameter=h,
|
|
331
|
+
basis_vectors=vectors,
|
|
332
|
+
)
|
|
333
|
+
return FormalLogarithmicBasis(
|
|
334
|
+
point=sp.sympify(point),
|
|
335
|
+
local_coordinate=h,
|
|
336
|
+
blocks=(block,),
|
|
337
|
+
terms=terms,
|
|
338
|
+
operator_order=localized.operator.order,
|
|
339
|
+
complete=len(vectors) == localized.operator.order,
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
def _group_completed_parts(
|
|
344
|
+
parts: tuple[CompleteFormalExponentialPart, ...],
|
|
345
|
+
) -> tuple[tuple[CompleteFormalExponentialPart, ...], ...]:
|
|
346
|
+
groups: list[list[CompleteFormalExponentialPart]] = []
|
|
347
|
+
for part in parts:
|
|
348
|
+
for group in groups:
|
|
349
|
+
if expressions_equal(
|
|
350
|
+
part.local_exponential_polynomial,
|
|
351
|
+
group[0].local_exponential_polynomial,
|
|
352
|
+
):
|
|
353
|
+
group.append(part)
|
|
354
|
+
break
|
|
355
|
+
else:
|
|
356
|
+
groups.append([part])
|
|
357
|
+
return tuple(tuple(group) for group in groups)
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
def _common_ramification(parts: tuple[CompleteFormalExponentialPart, ...]) -> int:
|
|
361
|
+
result = 1
|
|
362
|
+
for part in parts:
|
|
363
|
+
result = lcm(result, int(part.ramification_index))
|
|
364
|
+
return result
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
def _q_conjugated_cover_operator(
|
|
368
|
+
localized: LocalizedOperator,
|
|
369
|
+
local_q: sp.Expr,
|
|
370
|
+
ramification: int,
|
|
371
|
+
) -> tuple[LinearDifferentialOperator, sp.Symbol, sp.Expr]:
|
|
372
|
+
h = localized.local_variable
|
|
373
|
+
t = sp.Dummy("t", positive=True)
|
|
374
|
+
q_t = analytic_powsimp(sp.expand(local_q.subs(h, t**ramification)))
|
|
375
|
+
coefficients = tuple(
|
|
376
|
+
sp.cancel(sp.together(c.subs(h, t**ramification)))
|
|
377
|
+
for c in localized.operator.coefficients
|
|
378
|
+
)
|
|
379
|
+
amplitude = sp.Function("_V")
|
|
380
|
+
v = amplitude(t)
|
|
381
|
+
factor = sp.exp(q_t)
|
|
382
|
+
|
|
383
|
+
def deriv(expr: sp.Expr) -> sp.Expr:
|
|
384
|
+
return sp.diff(expr, t) / (ramification * t ** (ramification - 1))
|
|
385
|
+
|
|
386
|
+
derivatives = [factor * v]
|
|
387
|
+
for _ in range(localized.operator.order):
|
|
388
|
+
derivatives.append(sp.expand(deriv(derivatives[-1])))
|
|
389
|
+
expression = sp.S.Zero
|
|
390
|
+
for j, coefficient in enumerate(coefficients):
|
|
391
|
+
expression += coefficient * derivatives[j] / factor
|
|
392
|
+
expression = sp.cancel(sp.together(sp.expand(expression)))
|
|
393
|
+
try:
|
|
394
|
+
operator = LinearDifferentialOperator.from_ode(expression, amplitude, t)
|
|
395
|
+
except SYMBOLIC_FAILURES as exc:
|
|
396
|
+
raise FormalBasisError(
|
|
397
|
+
"could not construct the exponential-conjugated cover operator"
|
|
398
|
+
) from exc
|
|
399
|
+
return operator, t, q_t
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
def _conjugate_scalar_operator_by_exponential(
|
|
403
|
+
operator: LinearDifferentialOperator,
|
|
404
|
+
exponent: sp.Expr,
|
|
405
|
+
) -> LinearDifferentialOperator:
|
|
406
|
+
"""Return ``exp(-Q) L exp(Q)`` as a scalar differential operator."""
|
|
407
|
+
|
|
408
|
+
variable = operator.variable
|
|
409
|
+
amplitude = sp.Function("_V")
|
|
410
|
+
v = amplitude(variable)
|
|
411
|
+
factor = sp.exp(sp.sympify(exponent))
|
|
412
|
+
expression = sp.S.Zero
|
|
413
|
+
for order, coefficient in enumerate(operator.coefficients):
|
|
414
|
+
expression += coefficient * sp.diff(factor * v, variable, order) / factor
|
|
415
|
+
expression = sp.cancel(sp.together(sp.expand(expression)))
|
|
416
|
+
try:
|
|
417
|
+
return LinearDifferentialOperator.from_ode(expression, amplitude, variable)
|
|
418
|
+
except SYMBOLIC_FAILURES as exc:
|
|
419
|
+
raise FormalBasisError(
|
|
420
|
+
"could not construct the exponential-conjugated isolated block operator"
|
|
421
|
+
) from exc
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
def _cover_to_local(
|
|
425
|
+
expr: sp.Expr, t: sp.Symbol, h: sp.Symbol, ramification: int
|
|
426
|
+
) -> sp.Expr:
|
|
427
|
+
result = sp.expand(expr).subs(sp.log(t), sp.log(h) / ramification)
|
|
428
|
+
result = result.subs(t, h ** sp.Rational(1, ramification))
|
|
429
|
+
return analytic_powsimp(sp.expand(result))
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
def _wrap_cover_vectors(
|
|
433
|
+
vectors: tuple[LogarithmicBasisVector, ...],
|
|
434
|
+
localized: LocalizedOperator,
|
|
435
|
+
local_q: sp.Expr,
|
|
436
|
+
q_t: sp.Expr,
|
|
437
|
+
ramification: int,
|
|
438
|
+
) -> tuple[LogarithmicBasisVector, ...]:
|
|
439
|
+
h = localized.local_variable
|
|
440
|
+
result: list[LogarithmicBasisVector] = []
|
|
441
|
+
for vector in vectors:
|
|
442
|
+
t = vector.local_parameter
|
|
443
|
+
reduced_local = _cover_to_local(vector.parameter_expression, t, h, ramification)
|
|
444
|
+
local_expression = sp.exp(local_q) * reduced_local
|
|
445
|
+
result.append(
|
|
446
|
+
LogarithmicBasisVector(
|
|
447
|
+
source_exponent=sp.simplify(vector.source_exponent / ramification),
|
|
448
|
+
exponent=sp.simplify(vector.exponent / ramification),
|
|
449
|
+
ramified_exponent=vector.ramified_exponent,
|
|
450
|
+
root_multiplicity=vector.root_multiplicity,
|
|
451
|
+
pole_order=vector.pole_order,
|
|
452
|
+
derivative_order=vector.derivative_order,
|
|
453
|
+
logarithmic_degree=vector.logarithmic_degree,
|
|
454
|
+
local_parameter=t,
|
|
455
|
+
parameter_expression=vector.parameter_expression,
|
|
456
|
+
local_expression=analytic_powsimp(sp.expand(local_expression)),
|
|
457
|
+
expression=localized.to_original(
|
|
458
|
+
analytic_powsimp(sp.expand(local_expression))
|
|
459
|
+
),
|
|
460
|
+
)
|
|
461
|
+
)
|
|
462
|
+
return tuple(result)
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
def _simple_irregular_blocks(
|
|
466
|
+
localized: LocalizedOperator,
|
|
467
|
+
parts: tuple[CompleteFormalExponentialPart, ...],
|
|
468
|
+
*,
|
|
469
|
+
terms: int,
|
|
470
|
+
ode,
|
|
471
|
+
function,
|
|
472
|
+
variable,
|
|
473
|
+
point,
|
|
474
|
+
max_branches: int,
|
|
475
|
+
) -> tuple[FormalSolutionBlock, ...]:
|
|
476
|
+
amplitudes = formal_amplitude_series(
|
|
477
|
+
ode,
|
|
478
|
+
function,
|
|
479
|
+
variable,
|
|
480
|
+
point=point,
|
|
481
|
+
terms=terms,
|
|
482
|
+
max_branches=max_branches,
|
|
483
|
+
)
|
|
484
|
+
by_part = {id(amplitude.exponential_part): amplitude for amplitude in amplitudes}
|
|
485
|
+
blocks: list[FormalSolutionBlock] = []
|
|
486
|
+
for part in parts:
|
|
487
|
+
if part.multiplicity != 1:
|
|
488
|
+
continue
|
|
489
|
+
amplitude = by_part.get(id(part))
|
|
490
|
+
if amplitude is None:
|
|
491
|
+
# complete_formal_exponential_parts() was called independently, so
|
|
492
|
+
# object identity need not survive. Match by formal data instead.
|
|
493
|
+
amplitude = next(
|
|
494
|
+
a
|
|
495
|
+
for a in amplitudes
|
|
496
|
+
if expressions_equal(
|
|
497
|
+
a.exponential_part.exponential_polynomial,
|
|
498
|
+
part.exponential_polynomial,
|
|
499
|
+
)
|
|
500
|
+
and expressions_equal(
|
|
501
|
+
a.exponential_part.algebraic_power, part.algebraic_power
|
|
502
|
+
)
|
|
503
|
+
)
|
|
504
|
+
t = amplitude.local_parameter
|
|
505
|
+
beta = sp.simplify(part.ramification_index * part.algebraic_power)
|
|
506
|
+
reduced = sp.expand(
|
|
507
|
+
t**beta
|
|
508
|
+
* sum(
|
|
509
|
+
amplitude.coefficients[k] * t**k
|
|
510
|
+
for k in range(len(amplitude.coefficients))
|
|
511
|
+
)
|
|
512
|
+
)
|
|
513
|
+
h = localized.local_variable
|
|
514
|
+
local_q = part.local_exponential_polynomial.subs(part.local_coordinate, h)
|
|
515
|
+
amplitude_h = amplitude.local_series.subs(
|
|
516
|
+
amplitude.exponential_part.local_coordinate, h
|
|
517
|
+
)
|
|
518
|
+
local_expr = sp.exp(local_q) * h**part.algebraic_power * amplitude_h
|
|
519
|
+
vector = LogarithmicBasisVector(
|
|
520
|
+
source_exponent=part.algebraic_power,
|
|
521
|
+
exponent=part.algebraic_power,
|
|
522
|
+
ramified_exponent=beta,
|
|
523
|
+
root_multiplicity=1,
|
|
524
|
+
pole_order=0,
|
|
525
|
+
derivative_order=0,
|
|
526
|
+
logarithmic_degree=0,
|
|
527
|
+
local_parameter=t,
|
|
528
|
+
parameter_expression=reduced,
|
|
529
|
+
local_expression=analytic_powsimp(sp.expand(local_expr)),
|
|
530
|
+
expression=sp.simplify(
|
|
531
|
+
sp.exp(part.exponential_polynomial)
|
|
532
|
+
* part.algebraic_prefactor
|
|
533
|
+
* amplitude.series
|
|
534
|
+
),
|
|
535
|
+
)
|
|
536
|
+
blocks.append(
|
|
537
|
+
FormalSolutionBlock(
|
|
538
|
+
local_exponential_polynomial=local_q,
|
|
539
|
+
exponential_polynomial=part.exponential_polynomial,
|
|
540
|
+
ramification_index=part.ramification_index,
|
|
541
|
+
local_parameter=t,
|
|
542
|
+
basis_vectors=(vector,),
|
|
543
|
+
)
|
|
544
|
+
)
|
|
545
|
+
merged: list[FormalSolutionBlock] = []
|
|
546
|
+
for block in blocks:
|
|
547
|
+
for index, existing in enumerate(merged):
|
|
548
|
+
if (
|
|
549
|
+
existing.ramification_index == block.ramification_index
|
|
550
|
+
and expressions_equal(
|
|
551
|
+
existing.local_exponential_polynomial,
|
|
552
|
+
block.local_exponential_polynomial,
|
|
553
|
+
)
|
|
554
|
+
):
|
|
555
|
+
merged[index] = FormalSolutionBlock(
|
|
556
|
+
local_exponential_polynomial=existing.local_exponential_polynomial,
|
|
557
|
+
exponential_polynomial=existing.exponential_polynomial,
|
|
558
|
+
ramification_index=existing.ramification_index,
|
|
559
|
+
local_parameter=existing.local_parameter,
|
|
560
|
+
basis_vectors=existing.basis_vectors + block.basis_vectors,
|
|
561
|
+
)
|
|
562
|
+
break
|
|
563
|
+
else:
|
|
564
|
+
merged.append(block)
|
|
565
|
+
return tuple(merged)
|
|
566
|
+
|
|
567
|
+
|
|
568
|
+
def _repeated_irregular_blocks_from_system_decomposition(
|
|
569
|
+
localized: LocalizedOperator,
|
|
570
|
+
parts: tuple[CompleteFormalExponentialPart, ...],
|
|
571
|
+
*,
|
|
572
|
+
ode,
|
|
573
|
+
function,
|
|
574
|
+
variable,
|
|
575
|
+
point,
|
|
576
|
+
terms: int,
|
|
577
|
+
max_branches: int,
|
|
578
|
+
) -> tuple[tuple[FormalSolutionBlock, ...], bool, str | None]:
|
|
579
|
+
"""Isolate repeated exponential blocks with the system decomposition layer."""
|
|
580
|
+
|
|
581
|
+
# Delayed import avoids a module cycle: block_decomposition consumes the
|
|
582
|
+
# completed scalar parts from formal.py, while formal_basis is a client of
|
|
583
|
+
# the resulting system blocks.
|
|
584
|
+
from .block_decomposition import (
|
|
585
|
+
BlockDecompositionError,
|
|
586
|
+
cyclic_scalar_operator,
|
|
587
|
+
exponential_block_decomposition,
|
|
588
|
+
)
|
|
589
|
+
|
|
590
|
+
try:
|
|
591
|
+
decomposition = exponential_block_decomposition(
|
|
592
|
+
ode,
|
|
593
|
+
function,
|
|
594
|
+
variable,
|
|
595
|
+
point=point,
|
|
596
|
+
max_power=max(8, terms + 4),
|
|
597
|
+
max_branches=max_branches,
|
|
598
|
+
)
|
|
599
|
+
except (BlockDecompositionError, ValueError) as exc:
|
|
600
|
+
return (), False, f"formal exponential-block decomposition failed: {exc}"
|
|
601
|
+
if not decomposition.complete:
|
|
602
|
+
return (
|
|
603
|
+
(),
|
|
604
|
+
False,
|
|
605
|
+
decomposition.limitation
|
|
606
|
+
or "formal exponential-block decomposition is incomplete",
|
|
607
|
+
)
|
|
608
|
+
|
|
609
|
+
blocks: list[FormalSolutionBlock] = []
|
|
610
|
+
for system_block in decomposition.blocks:
|
|
611
|
+
if system_block.dimension <= 1:
|
|
612
|
+
continue
|
|
613
|
+
metadata = system_block.metadata
|
|
614
|
+
if metadata is None:
|
|
615
|
+
return (
|
|
616
|
+
tuple(blocks),
|
|
617
|
+
False,
|
|
618
|
+
"an isolated repeated block could not be matched to scalar exponential metadata",
|
|
619
|
+
)
|
|
620
|
+
representative = parts[metadata.part_indices[0]]
|
|
621
|
+
local_q = representative.local_exponential_polynomial.subs(
|
|
622
|
+
representative.local_coordinate, localized.local_variable
|
|
623
|
+
)
|
|
624
|
+
try:
|
|
625
|
+
scalar_operator = cyclic_scalar_operator(
|
|
626
|
+
system_block.connection,
|
|
627
|
+
system_block.output_row,
|
|
628
|
+
function_name=f"_U{system_block.index}",
|
|
629
|
+
)
|
|
630
|
+
reduced_operator = _conjugate_scalar_operator_by_exponential(
|
|
631
|
+
scalar_operator,
|
|
632
|
+
system_block.parameter_exponential_polynomial,
|
|
633
|
+
)
|
|
634
|
+
cover_vectors = _basis_for_operator_at_zero(reduced_operator, terms=terms)
|
|
635
|
+
except (FormalBasisError, BlockDecompositionError, ValueError) as exc:
|
|
636
|
+
return (
|
|
637
|
+
tuple(blocks),
|
|
638
|
+
False,
|
|
639
|
+
(
|
|
640
|
+
"isolated repeated exponential block could not be reduced to a "
|
|
641
|
+
f"regular scalar block: {exc}"
|
|
642
|
+
),
|
|
643
|
+
)
|
|
644
|
+
if len(cover_vectors) != system_block.dimension:
|
|
645
|
+
return (
|
|
646
|
+
tuple(blocks),
|
|
647
|
+
False,
|
|
648
|
+
"isolated repeated block did not produce its full logarithmic basis",
|
|
649
|
+
)
|
|
650
|
+
wrapped = _wrap_cover_vectors(
|
|
651
|
+
cover_vectors,
|
|
652
|
+
localized,
|
|
653
|
+
local_q,
|
|
654
|
+
system_block.parameter_exponential_polynomial,
|
|
655
|
+
decomposition.ramification_index,
|
|
656
|
+
)
|
|
657
|
+
blocks.append(
|
|
658
|
+
FormalSolutionBlock(
|
|
659
|
+
local_exponential_polynomial=local_q,
|
|
660
|
+
exponential_polynomial=localized.to_original(local_q),
|
|
661
|
+
ramification_index=decomposition.ramification_index,
|
|
662
|
+
local_parameter=decomposition.parameter,
|
|
663
|
+
basis_vectors=wrapped,
|
|
664
|
+
)
|
|
665
|
+
)
|
|
666
|
+
return tuple(blocks), True, None
|
|
667
|
+
|
|
668
|
+
|
|
669
|
+
def formal_logarithmic_basis(
|
|
670
|
+
ode: sp.Expr | sp.Equality | LinearDifferentialOperator,
|
|
671
|
+
function: sp.FunctionClass | sp.Expr | None = None,
|
|
672
|
+
variable: sp.Symbol | None = None,
|
|
673
|
+
*,
|
|
674
|
+
point: sp.Expr = 0,
|
|
675
|
+
terms: int = 8,
|
|
676
|
+
max_branches: int = 64,
|
|
677
|
+
) -> FormalLogarithmicBasis:
|
|
678
|
+
"""Return a formal basis with explicit logarithmic companions when possible.
|
|
679
|
+
|
|
680
|
+
Ordinary and regular-singular points use the parameterized Frobenius
|
|
681
|
+
construction directly. At an irregular point, simple completed branches
|
|
682
|
+
are returned as one-dimensional blocks. If every branch shares one
|
|
683
|
+
completed exponential polynomial, the full exponential is removed on a
|
|
684
|
+
common cover and repeated/resonant amplitude roots are resolved there by
|
|
685
|
+
the same logarithmic construction.
|
|
686
|
+
|
|
687
|
+
For a mixed irregular problem, repeated completed exponential factors are
|
|
688
|
+
isolated as formal system blocks, scalarized through the physical output
|
|
689
|
+
row, and reduced independently. This avoids applying Frobenius analysis
|
|
690
|
+
to an exponential-conjugated *full* scalar operator that still contains
|
|
691
|
+
other irregular modes.
|
|
692
|
+
"""
|
|
693
|
+
|
|
694
|
+
localized = localize_operator(ode, function, variable, point=point)
|
|
695
|
+
kind = classify_ode_point(localized.operator, point=0).kind
|
|
696
|
+
if kind in (ODESingularityKind.ORDINARY, ODESingularityKind.REGULAR):
|
|
697
|
+
return logarithmic_frobenius_basis(
|
|
698
|
+
ode,
|
|
699
|
+
function,
|
|
700
|
+
variable,
|
|
701
|
+
point=point,
|
|
702
|
+
terms=terms,
|
|
703
|
+
)
|
|
704
|
+
if kind is not ODESingularityKind.IRREGULAR:
|
|
705
|
+
raise FormalBasisError(f"cannot construct a formal basis at point {point!s}")
|
|
706
|
+
|
|
707
|
+
parts = complete_formal_exponential_parts(
|
|
708
|
+
ode,
|
|
709
|
+
function,
|
|
710
|
+
variable,
|
|
711
|
+
point=point,
|
|
712
|
+
max_branches=max_branches,
|
|
713
|
+
)
|
|
714
|
+
groups = _group_completed_parts(parts)
|
|
715
|
+
order = localized.operator.order
|
|
716
|
+
|
|
717
|
+
if len(groups) == 1 and sum(part.multiplicity for part in groups[0]) == order:
|
|
718
|
+
group = groups[0]
|
|
719
|
+
ramification = _common_ramification(group)
|
|
720
|
+
local_q = group[0].local_exponential_polynomial.subs(
|
|
721
|
+
group[0].local_coordinate, localized.local_variable
|
|
722
|
+
)
|
|
723
|
+
cover_operator, t, q_t = _q_conjugated_cover_operator(
|
|
724
|
+
localized,
|
|
725
|
+
local_q,
|
|
726
|
+
ramification,
|
|
727
|
+
)
|
|
728
|
+
cover_vectors = _basis_for_operator_at_zero(cover_operator, terms=terms)
|
|
729
|
+
wrapped = _wrap_cover_vectors(
|
|
730
|
+
cover_vectors,
|
|
731
|
+
localized,
|
|
732
|
+
local_q,
|
|
733
|
+
q_t,
|
|
734
|
+
ramification,
|
|
735
|
+
)
|
|
736
|
+
block = FormalSolutionBlock(
|
|
737
|
+
local_exponential_polynomial=local_q,
|
|
738
|
+
exponential_polynomial=localized.to_original(local_q),
|
|
739
|
+
ramification_index=ramification,
|
|
740
|
+
local_parameter=t,
|
|
741
|
+
basis_vectors=wrapped,
|
|
742
|
+
)
|
|
743
|
+
return FormalLogarithmicBasis(
|
|
744
|
+
point=sp.sympify(point),
|
|
745
|
+
local_coordinate=localized.local_variable,
|
|
746
|
+
blocks=(block,),
|
|
747
|
+
terms=terms,
|
|
748
|
+
operator_order=order,
|
|
749
|
+
complete=len(wrapped) == order,
|
|
750
|
+
)
|
|
751
|
+
|
|
752
|
+
simple_blocks = _simple_irregular_blocks(
|
|
753
|
+
localized,
|
|
754
|
+
parts,
|
|
755
|
+
terms=terms,
|
|
756
|
+
ode=ode,
|
|
757
|
+
function=function,
|
|
758
|
+
variable=variable,
|
|
759
|
+
point=point,
|
|
760
|
+
max_branches=max_branches,
|
|
761
|
+
)
|
|
762
|
+
repeated_parts_present = any(part.multiplicity > 1 for part in parts)
|
|
763
|
+
repeated_blocks: tuple[FormalSolutionBlock, ...] = ()
|
|
764
|
+
decomposition_complete = True
|
|
765
|
+
limitation = None
|
|
766
|
+
if repeated_parts_present:
|
|
767
|
+
repeated_blocks, decomposition_complete, limitation = (
|
|
768
|
+
_repeated_irregular_blocks_from_system_decomposition(
|
|
769
|
+
localized,
|
|
770
|
+
parts,
|
|
771
|
+
ode=ode,
|
|
772
|
+
function=function,
|
|
773
|
+
variable=variable,
|
|
774
|
+
point=point,
|
|
775
|
+
terms=terms,
|
|
776
|
+
max_branches=max_branches,
|
|
777
|
+
)
|
|
778
|
+
)
|
|
779
|
+
|
|
780
|
+
blocks = simple_blocks + repeated_blocks
|
|
781
|
+
|
|
782
|
+
# Preserve the scalar exponential-part order so branch permutation and
|
|
783
|
+
# monodromy remain deterministic.
|
|
784
|
+
def block_order(block: FormalSolutionBlock) -> int:
|
|
785
|
+
for index, part in enumerate(parts):
|
|
786
|
+
local_q = part.local_exponential_polynomial.subs(
|
|
787
|
+
part.local_coordinate, localized.local_variable
|
|
788
|
+
)
|
|
789
|
+
if expressions_equal(block.local_exponential_polynomial, local_q):
|
|
790
|
+
return index
|
|
791
|
+
return len(parts)
|
|
792
|
+
|
|
793
|
+
blocks = tuple(sorted(blocks, key=block_order))
|
|
794
|
+
dimension = sum(block.dimension for block in blocks)
|
|
795
|
+
return FormalLogarithmicBasis(
|
|
796
|
+
point=sp.sympify(point),
|
|
797
|
+
local_coordinate=localized.local_variable,
|
|
798
|
+
blocks=blocks,
|
|
799
|
+
terms=terms,
|
|
800
|
+
operator_order=order,
|
|
801
|
+
complete=dimension == order and decomposition_complete,
|
|
802
|
+
limitation=limitation
|
|
803
|
+
if dimension != order or not decomposition_complete
|
|
804
|
+
else None,
|
|
805
|
+
)
|
|
806
|
+
|
|
807
|
+
|
|
808
|
+
def _term_signature(
|
|
809
|
+
term: sp.Expr,
|
|
810
|
+
variable: sp.Symbol,
|
|
811
|
+
) -> tuple[sp.Expr, int, sp.Expr]:
|
|
812
|
+
term = sp.expand_power_base(term, force=False)
|
|
813
|
+
logv = sp.log(variable)
|
|
814
|
+
powers = term.as_powers_dict()
|
|
815
|
+
power = sp.simplify(powers.get(variable, sp.S.Zero))
|
|
816
|
+
log_degree_expr = powers.get(logv, sp.S.Zero)
|
|
817
|
+
if log_degree_expr.is_Integer is not True:
|
|
818
|
+
raise FormalBasisError(
|
|
819
|
+
"formal monodromy encountered a non-polynomial logarithm"
|
|
820
|
+
)
|
|
821
|
+
log_degree = int(log_degree_expr)
|
|
822
|
+
coefficient = sp.simplify(term / (variable**power * logv**log_degree))
|
|
823
|
+
if coefficient.has(variable, logv):
|
|
824
|
+
raise FormalBasisError(
|
|
825
|
+
f"could not decompose formal monomial {term!s} into power/log form"
|
|
826
|
+
)
|
|
827
|
+
return power, log_degree, coefficient
|
|
828
|
+
|
|
829
|
+
|
|
830
|
+
def _formal_coefficient_dict(
|
|
831
|
+
expr: sp.Expr, variable: sp.Symbol
|
|
832
|
+
) -> dict[tuple[sp.Expr, int], sp.Expr]:
|
|
833
|
+
result: dict[tuple[sp.Expr, int], sp.Expr] = {}
|
|
834
|
+
for term in sp.Add.make_args(sp.expand(expr)):
|
|
835
|
+
power, log_degree, coefficient = _term_signature(term, variable)
|
|
836
|
+
key = (power, log_degree)
|
|
837
|
+
result[key] = sp.simplify(result.get(key, sp.S.Zero) + coefficient)
|
|
838
|
+
return {key: value for key, value in result.items() if sp.simplify(value) != 0}
|
|
839
|
+
|
|
840
|
+
|
|
841
|
+
def _continued_expression(
|
|
842
|
+
expr: sp.Expr,
|
|
843
|
+
variable: sp.Symbol,
|
|
844
|
+
*,
|
|
845
|
+
turn_fraction: sp.Rational = sp.S.One,
|
|
846
|
+
) -> sp.Expr:
|
|
847
|
+
result = sp.S.Zero
|
|
848
|
+
turn_fraction = sp.Rational(turn_fraction)
|
|
849
|
+
shift = 2 * sp.pi * sp.I * turn_fraction
|
|
850
|
+
logv = sp.log(variable)
|
|
851
|
+
for term in sp.Add.make_args(sp.expand(expr)):
|
|
852
|
+
power, log_degree, coefficient = _term_signature(term, variable)
|
|
853
|
+
result += (
|
|
854
|
+
coefficient
|
|
855
|
+
* sp.exp(2 * sp.pi * sp.I * turn_fraction * power)
|
|
856
|
+
* variable**power
|
|
857
|
+
* (logv + shift) ** log_degree
|
|
858
|
+
)
|
|
859
|
+
return sp.expand(result)
|
|
860
|
+
|
|
861
|
+
|
|
862
|
+
def _coordinates_in_basis(
|
|
863
|
+
target: sp.Expr,
|
|
864
|
+
basis_expressions: tuple[sp.Expr, ...],
|
|
865
|
+
variable: sp.Symbol,
|
|
866
|
+
) -> tuple[sp.Expr, ...]:
|
|
867
|
+
dictionaries = tuple(
|
|
868
|
+
_formal_coefficient_dict(expr, variable) for expr in basis_expressions
|
|
869
|
+
)
|
|
870
|
+
target_dict = _formal_coefficient_dict(target, variable)
|
|
871
|
+
keys = sorted(
|
|
872
|
+
set(target_dict).union(*(set(item) for item in dictionaries)),
|
|
873
|
+
key=lambda item: (sp.default_sort_key(item[0]), item[1]),
|
|
874
|
+
)
|
|
875
|
+
rows: list[list[sp.Expr]] = []
|
|
876
|
+
rhs_values: list[sp.Expr] = []
|
|
877
|
+
rank = 0
|
|
878
|
+
for key in keys:
|
|
879
|
+
candidate = [item.get(key, sp.S.Zero) for item in dictionaries]
|
|
880
|
+
trial = sp.Matrix([*rows, candidate])
|
|
881
|
+
new_rank = int(trial.rank())
|
|
882
|
+
rows.append(candidate)
|
|
883
|
+
rhs_values.append(target_dict.get(key, sp.S.Zero))
|
|
884
|
+
rank = new_rank
|
|
885
|
+
if rank == len(dictionaries):
|
|
886
|
+
break
|
|
887
|
+
if rank != len(dictionaries):
|
|
888
|
+
raise FormalBasisError("formal basis jets do not have full rank")
|
|
889
|
+
matrix = sp.Matrix(rows)
|
|
890
|
+
rhs = sp.Matrix(rhs_values)
|
|
891
|
+
try:
|
|
892
|
+
solution_set = sp.linsolve((matrix, rhs))
|
|
893
|
+
except SYMBOLIC_FAILURES as exc:
|
|
894
|
+
raise FormalBasisError("could not solve formal monodromy coordinates") from exc
|
|
895
|
+
solutions = list(solution_set)
|
|
896
|
+
if len(solutions) != 1:
|
|
897
|
+
raise FormalBasisError(
|
|
898
|
+
"formal monodromy coordinates are not uniquely determined"
|
|
899
|
+
)
|
|
900
|
+
solution = tuple(sp.simplify(value) for value in solutions[0])
|
|
901
|
+
generated = set().union(*(value.free_symbols for value in solution))
|
|
902
|
+
basis_symbols = set().union(
|
|
903
|
+
*(expr.free_symbols for expr in basis_expressions), target.free_symbols
|
|
904
|
+
)
|
|
905
|
+
if generated - basis_symbols:
|
|
906
|
+
raise FormalBasisError("formal monodromy coordinate solve left free parameters")
|
|
907
|
+
return solution
|
|
908
|
+
|
|
909
|
+
|
|
910
|
+
def _block_cover_monodromy(block: FormalSolutionBlock) -> sp.Matrix:
|
|
911
|
+
vectors = block.basis_vectors
|
|
912
|
+
if not vectors:
|
|
913
|
+
return sp.zeros(0, 0)
|
|
914
|
+
variable = block.local_parameter
|
|
915
|
+
expressions = tuple(vector.parameter_expression for vector in vectors)
|
|
916
|
+
columns: list[tuple[sp.Expr, ...]] = []
|
|
917
|
+
for expression in expressions:
|
|
918
|
+
continued = _continued_expression(expression, variable)
|
|
919
|
+
columns.append(_coordinates_in_basis(continued, expressions, variable))
|
|
920
|
+
return sp.Matrix.hstack(*(sp.Matrix(column) for column in columns))
|
|
921
|
+
|
|
922
|
+
|
|
923
|
+
def _continued_local_exponential(expr: sp.Expr, variable: sp.Symbol) -> sp.Expr:
|
|
924
|
+
result = sp.S.Zero
|
|
925
|
+
for term in sp.Add.make_args(sp.expand(expr)):
|
|
926
|
+
term = sp.expand_power_base(term, force=False)
|
|
927
|
+
power = sp.simplify(term.as_powers_dict().get(variable, sp.S.Zero))
|
|
928
|
+
coefficient = sp.simplify(term / variable**power)
|
|
929
|
+
if coefficient.has(variable):
|
|
930
|
+
raise FormalBasisError(f"could not decompose exponential monomial {term!s}")
|
|
931
|
+
result += coefficient * sp.exp(2 * sp.pi * sp.I * power) * variable**power
|
|
932
|
+
return analytic_powsimp(sp.expand(result))
|
|
933
|
+
|
|
934
|
+
|
|
935
|
+
def _local_monodromy_matrix(basis: FormalLogarithmicBasis) -> sp.Matrix | None:
|
|
936
|
+
blocks = basis.blocks
|
|
937
|
+
offsets: list[int] = []
|
|
938
|
+
total = 0
|
|
939
|
+
for block in blocks:
|
|
940
|
+
offsets.append(total)
|
|
941
|
+
total += block.dimension
|
|
942
|
+
matrix = sp.zeros(total, total)
|
|
943
|
+
h = basis.local_coordinate
|
|
944
|
+
|
|
945
|
+
for source_index, source in enumerate(blocks):
|
|
946
|
+
continued_q = _continued_local_exponential(
|
|
947
|
+
source.local_exponential_polynomial, h
|
|
948
|
+
)
|
|
949
|
+
targets = [
|
|
950
|
+
(index, block)
|
|
951
|
+
for index, block in enumerate(blocks)
|
|
952
|
+
if block.ramification_index == source.ramification_index
|
|
953
|
+
and expressions_equal(block.local_exponential_polynomial, continued_q)
|
|
954
|
+
]
|
|
955
|
+
if len(targets) != 1:
|
|
956
|
+
return None
|
|
957
|
+
target_index, target = targets[0]
|
|
958
|
+
target_expressions = tuple(v.parameter_expression for v in target.basis_vectors)
|
|
959
|
+
if not target_expressions:
|
|
960
|
+
return None
|
|
961
|
+
fraction = sp.Rational(1, source.ramification_index)
|
|
962
|
+
for local_column, vector in enumerate(source.basis_vectors):
|
|
963
|
+
continued = _continued_expression(
|
|
964
|
+
vector.parameter_expression,
|
|
965
|
+
source.local_parameter,
|
|
966
|
+
turn_fraction=fraction,
|
|
967
|
+
)
|
|
968
|
+
if source.local_parameter != target.local_parameter:
|
|
969
|
+
continued = continued.subs(
|
|
970
|
+
source.local_parameter, target.local_parameter
|
|
971
|
+
)
|
|
972
|
+
try:
|
|
973
|
+
coordinates = _coordinates_in_basis(
|
|
974
|
+
continued, target_expressions, target.local_parameter
|
|
975
|
+
)
|
|
976
|
+
except FormalBasisError:
|
|
977
|
+
return None
|
|
978
|
+
global_column = offsets[source_index] + local_column
|
|
979
|
+
for row, value in enumerate(coordinates):
|
|
980
|
+
matrix[offsets[target_index] + row, global_column] = sp.simplify(value)
|
|
981
|
+
return matrix
|
|
982
|
+
|
|
983
|
+
|
|
984
|
+
def formal_monodromy(basis: FormalLogarithmicBasis) -> FormalMonodromy:
|
|
985
|
+
"""Return formal monodromy matrices for a completed formal basis.
|
|
986
|
+
|
|
987
|
+
The basis must be complete. Matrix columns give the continued basis
|
|
988
|
+
vectors. For ramified blocks, ``cover_matrix`` is always meaningful; a
|
|
989
|
+
one-turn matrix in the original local coordinate is also reconstructed
|
|
990
|
+
when continuation maps every exponential block to a uniquely represented
|
|
991
|
+
target block; otherwise ``local_matrix`` is ``None``.
|
|
992
|
+
"""
|
|
993
|
+
|
|
994
|
+
if not basis.complete or basis.dimension != basis.operator_order:
|
|
995
|
+
raise FormalBasisError(
|
|
996
|
+
"formal monodromy requires a complete formal basis"
|
|
997
|
+
+ (f": {basis.limitation}" if basis.limitation else "")
|
|
998
|
+
)
|
|
999
|
+
block_matrices = [_block_cover_monodromy(block) for block in basis.blocks]
|
|
1000
|
+
if not block_matrices:
|
|
1001
|
+
matrix = sp.zeros(0, 0)
|
|
1002
|
+
else:
|
|
1003
|
+
matrix = sp.diag(*block_matrices)
|
|
1004
|
+
local_matrix = _local_monodromy_matrix(basis)
|
|
1005
|
+
return FormalMonodromy(
|
|
1006
|
+
basis=basis,
|
|
1007
|
+
cover_matrix=matrix,
|
|
1008
|
+
local_matrix=local_matrix,
|
|
1009
|
+
)
|