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/formal.py
ADDED
|
@@ -0,0 +1,1028 @@
|
|
|
1
|
+
"""Riccati/Bell refinement and formal WKB amplitude series.
|
|
2
|
+
|
|
3
|
+
For a scalar operator ``L = sum_j a_j(h) D_h**j`` and a nonzero formal
|
|
4
|
+
solution ``y``, put ``w = D_h(log(y))``. Then
|
|
5
|
+
|
|
6
|
+
``D_h**j(y) / y = B_j(w, w', ..., w**(j-1))``,
|
|
7
|
+
|
|
8
|
+
where the complete differential Bell polynomials satisfy
|
|
9
|
+
``B_0 = 1`` and ``B_{j+1} = D_h(B_j) + w B_j``. This turns the linear ODE
|
|
10
|
+
into its Riccati equation ``sum_j a_j B_j = 0``.
|
|
11
|
+
|
|
12
|
+
A Newton edge supplies the first term of ``w``. In a ramified coordinate
|
|
13
|
+
``h = t**r`` we recursively cancel the Riccati equation through the
|
|
14
|
+
``h**(-1)`` term. Integrating the terms below ``h**(-1)`` gives the complete
|
|
15
|
+
finite exponential polynomial for that branch; the ``h**(-1)`` coefficient
|
|
16
|
+
is the algebraic power prefactor. The remaining conjugated equation then
|
|
17
|
+
produces a formal amplitude series in ``t``.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
from dataclasses import dataclass
|
|
23
|
+
|
|
24
|
+
import sympy as sp
|
|
25
|
+
|
|
26
|
+
from ._power_simplify import analytic_powsimp
|
|
27
|
+
from ._symbolic_errors import SYMBOLIC_FAILURES
|
|
28
|
+
from .irregular import FormalExponentialPart, formal_exponential_parts
|
|
29
|
+
from .newton import (
|
|
30
|
+
LocalizedOperator,
|
|
31
|
+
local_order_and_leading_coefficient,
|
|
32
|
+
localize_operator,
|
|
33
|
+
)
|
|
34
|
+
from .operator import LinearDifferentialOperator
|
|
35
|
+
from .series import SparseLaurentSeries
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class FormalRefinementError(NotImplementedError):
|
|
39
|
+
"""Raised when a formal branch requires a refinement outside the supported representation."""
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dataclass(frozen=True)
|
|
43
|
+
class LogDerivativeCoefficient:
|
|
44
|
+
"""One coefficient ``c*t**power`` in a ramified logarithmic derivative."""
|
|
45
|
+
|
|
46
|
+
power: int
|
|
47
|
+
coefficient: sp.Expr
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@dataclass(frozen=True)
|
|
51
|
+
class RiccatiRefinementStep:
|
|
52
|
+
"""One Newton--Puiseux step in a formal Riccati branch.
|
|
53
|
+
|
|
54
|
+
``local_power`` is the exponent of the correction in the local coordinate
|
|
55
|
+
``h`` (so a correction is ``coefficient*h**local_power``). A zero
|
|
56
|
+
coefficient records a zero characteristic root: that branch survives the
|
|
57
|
+
current secondary Newton edge and must be tested at a higher power.
|
|
58
|
+
``ramification_before`` and ``ramification_after`` expose any new
|
|
59
|
+
uniformizing cover introduced by a nonintegral correction exponent.
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
local_power: sp.Rational
|
|
63
|
+
coefficient: sp.Expr
|
|
64
|
+
characteristic_polynomial: sp.Expr
|
|
65
|
+
root_multiplicity: int
|
|
66
|
+
ramification_before: int
|
|
67
|
+
ramification_after: int
|
|
68
|
+
|
|
69
|
+
@property
|
|
70
|
+
def introduces_ramification(self) -> bool:
|
|
71
|
+
return self.ramification_after > self.ramification_before
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@dataclass(frozen=True)
|
|
75
|
+
class CompleteFormalExponentialPart:
|
|
76
|
+
"""A Newton exponential branch refined through its algebraic prefactor.
|
|
77
|
+
|
|
78
|
+
``local_exponential_polynomial`` contains every negative-power term of the
|
|
79
|
+
integrated logarithmic derivative. ``algebraic_power`` is the coefficient
|
|
80
|
+
of ``log(h)`` and hence gives the factor ``h**algebraic_power``.
|
|
81
|
+
"""
|
|
82
|
+
|
|
83
|
+
leading_part: FormalExponentialPart
|
|
84
|
+
local_coordinate: sp.Symbol
|
|
85
|
+
local_parameter: sp.Symbol
|
|
86
|
+
ramification_index: int
|
|
87
|
+
logarithmic_derivative: sp.Expr
|
|
88
|
+
logarithmic_derivative_parameter: sp.Expr
|
|
89
|
+
coefficients: tuple[LogDerivativeCoefficient, ...]
|
|
90
|
+
local_exponential_polynomial: sp.Expr
|
|
91
|
+
exponential_polynomial: sp.Expr
|
|
92
|
+
algebraic_power: sp.Expr
|
|
93
|
+
local_algebraic_prefactor: sp.Expr
|
|
94
|
+
algebraic_prefactor: sp.Expr
|
|
95
|
+
multiplicity: int = 1
|
|
96
|
+
refinement_steps: tuple[RiccatiRefinementStep, ...] = ()
|
|
97
|
+
|
|
98
|
+
@property
|
|
99
|
+
def point(self) -> sp.Expr:
|
|
100
|
+
return self.leading_part.point
|
|
101
|
+
|
|
102
|
+
@property
|
|
103
|
+
def exponential_factor(self) -> sp.Expr:
|
|
104
|
+
return sp.exp(self.exponential_polynomial)
|
|
105
|
+
|
|
106
|
+
@property
|
|
107
|
+
def local_exponential_factor(self) -> sp.Expr:
|
|
108
|
+
return sp.exp(self.local_exponential_polynomial)
|
|
109
|
+
|
|
110
|
+
@property
|
|
111
|
+
def local_prefactor(self) -> sp.Expr:
|
|
112
|
+
return self.local_exponential_factor * self.local_algebraic_prefactor
|
|
113
|
+
|
|
114
|
+
@property
|
|
115
|
+
def prefactor(self) -> sp.Expr:
|
|
116
|
+
return self.exponential_factor * self.algebraic_prefactor
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
@dataclass(frozen=True)
|
|
120
|
+
class FormalAmplitudeSeries:
|
|
121
|
+
"""Formal amplitude after exponential and algebraic factors are removed.
|
|
122
|
+
|
|
123
|
+
``coefficients[k]`` multiplies ``t**k`` in the uniformizing parameter
|
|
124
|
+
``h=t**ramification_index``. The normalization is ``coefficients[0] = 1``.
|
|
125
|
+
"""
|
|
126
|
+
|
|
127
|
+
exponential_part: CompleteFormalExponentialPart
|
|
128
|
+
coefficients: tuple[sp.Expr, ...]
|
|
129
|
+
local_parameter: sp.Symbol
|
|
130
|
+
local_series: sp.Expr
|
|
131
|
+
series: sp.Expr
|
|
132
|
+
residual: sp.Expr
|
|
133
|
+
residual_valuation: sp.Rational | None
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
@dataclass(frozen=True)
|
|
137
|
+
class FormalAsymptoticSolution:
|
|
138
|
+
"""A complete finite exponential/power prefactor with a formal amplitude."""
|
|
139
|
+
|
|
140
|
+
point: sp.Expr
|
|
141
|
+
exponential_part: CompleteFormalExponentialPart
|
|
142
|
+
amplitude: FormalAmplitudeSeries
|
|
143
|
+
local_expression: sp.Expr
|
|
144
|
+
expression: sp.Expr
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
@dataclass(frozen=True)
|
|
148
|
+
class _LogDerivativeState:
|
|
149
|
+
parameter: sp.Symbol
|
|
150
|
+
ramification: int
|
|
151
|
+
expression: sp.Expr
|
|
152
|
+
coefficients: tuple[LogDerivativeCoefficient, ...]
|
|
153
|
+
multiplicity: int
|
|
154
|
+
search_floor: sp.Rational
|
|
155
|
+
refinement_steps: tuple[RiccatiRefinementStep, ...] = ()
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
@dataclass(frozen=True)
|
|
159
|
+
class _RiccatiSupportTerm:
|
|
160
|
+
monomial: tuple[int, ...]
|
|
161
|
+
valuation: sp.Rational
|
|
162
|
+
leading_coefficient: sp.Expr
|
|
163
|
+
degree: int
|
|
164
|
+
derivative_weight: int
|
|
165
|
+
|
|
166
|
+
@property
|
|
167
|
+
def intercept(self) -> sp.Rational:
|
|
168
|
+
raise AttributeError("intercept depends on the current ramification")
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
@dataclass(frozen=True)
|
|
172
|
+
class _SecondaryBalance:
|
|
173
|
+
parameter_power: sp.Rational
|
|
174
|
+
characteristic_variable: sp.Symbol
|
|
175
|
+
characteristic_polynomial: sp.Expr
|
|
176
|
+
roots: tuple[tuple[sp.Expr, int], ...]
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def differential_bell_polynomials(
|
|
180
|
+
logarithmic_derivative: sp.Expr,
|
|
181
|
+
variable: sp.Symbol,
|
|
182
|
+
order: int,
|
|
183
|
+
*,
|
|
184
|
+
derivative=None,
|
|
185
|
+
) -> tuple[sp.Expr, ...]:
|
|
186
|
+
"""Return ``B_0, ..., B_order`` using the incremental differential recurrence.
|
|
187
|
+
|
|
188
|
+
These are the complete exponential Bell polynomials specialized to
|
|
189
|
+
``(w, w', ..., w**(n-1))``. The recurrence is retained as the production
|
|
190
|
+
symbolic path because a Riccati reduction naturally needs all orders in
|
|
191
|
+
succession. :func:`complete_exponential_bell_polynomial` provides the
|
|
192
|
+
independent partition-based combinatorial construction.
|
|
193
|
+
|
|
194
|
+
``derivative`` may provide a custom derivation, which is used for ramified
|
|
195
|
+
coordinates where ``D_h = (r*t**(r-1))**-1 D_t``.
|
|
196
|
+
"""
|
|
197
|
+
|
|
198
|
+
if order < 0:
|
|
199
|
+
raise ValueError("order must be nonnegative")
|
|
200
|
+
deriv = derivative or (lambda expr: sp.diff(expr, variable))
|
|
201
|
+
bells: list[sp.Expr] = [sp.S.One]
|
|
202
|
+
for _ in range(order):
|
|
203
|
+
bells.append(sp.expand(deriv(bells[-1]) + logarithmic_derivative * bells[-1]))
|
|
204
|
+
return tuple(bells)
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def differential_bell_laurent_series(
|
|
208
|
+
logarithmic_derivative: SparseLaurentSeries,
|
|
209
|
+
order: int,
|
|
210
|
+
*,
|
|
211
|
+
ramification_index: int = 1,
|
|
212
|
+
min_power: int | None = None,
|
|
213
|
+
max_power: int | None = None,
|
|
214
|
+
) -> tuple[SparseLaurentSeries, ...]:
|
|
215
|
+
"""Return differential Bell polynomials as sparse truncated Laurent series.
|
|
216
|
+
|
|
217
|
+
Powers are integral in the uniformizing variable ``t`` with local
|
|
218
|
+
coordinate ``h=t**ramification_index``. Truncation is applied after each
|
|
219
|
+
differentiation and multiplication, so formal Riccati calculations can
|
|
220
|
+
avoid materializing generic expanded Bell expressions outside the
|
|
221
|
+
valuation window that is actually needed.
|
|
222
|
+
"""
|
|
223
|
+
|
|
224
|
+
if order < 0:
|
|
225
|
+
raise ValueError("order must be nonnegative")
|
|
226
|
+
if ramification_index < 1:
|
|
227
|
+
raise ValueError("ramification_index must be positive")
|
|
228
|
+
|
|
229
|
+
bells: list[SparseLaurentSeries] = [
|
|
230
|
+
SparseLaurentSeries.one(logarithmic_derivative.variable)
|
|
231
|
+
]
|
|
232
|
+
for _ in range(order):
|
|
233
|
+
derivative_part = bells[-1].derivative(
|
|
234
|
+
ramification_index=ramification_index,
|
|
235
|
+
min_power=min_power,
|
|
236
|
+
max_power=max_power,
|
|
237
|
+
)
|
|
238
|
+
product_part = logarithmic_derivative.multiply(
|
|
239
|
+
bells[-1],
|
|
240
|
+
min_power=min_power,
|
|
241
|
+
max_power=max_power,
|
|
242
|
+
)
|
|
243
|
+
bells.append(
|
|
244
|
+
derivative_part.add(
|
|
245
|
+
product_part,
|
|
246
|
+
min_power=min_power,
|
|
247
|
+
max_power=max_power,
|
|
248
|
+
)
|
|
249
|
+
)
|
|
250
|
+
return tuple(bells)
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def riccati_expression(
|
|
254
|
+
operator: LinearDifferentialOperator,
|
|
255
|
+
logarithmic_derivative: sp.Expr,
|
|
256
|
+
*,
|
|
257
|
+
derivative=None,
|
|
258
|
+
) -> sp.Expr:
|
|
259
|
+
"""Return the Bell-polynomial Riccati expression ``L[y]/y``."""
|
|
260
|
+
|
|
261
|
+
if not operator.is_homogeneous:
|
|
262
|
+
raise ValueError("Riccati refinement requires a homogeneous operator")
|
|
263
|
+
bells = differential_bell_polynomials(
|
|
264
|
+
logarithmic_derivative,
|
|
265
|
+
operator.variable,
|
|
266
|
+
operator.order,
|
|
267
|
+
derivative=derivative,
|
|
268
|
+
)
|
|
269
|
+
return sp.expand(
|
|
270
|
+
sum(operator.coefficients[j] * bells[j] for j in range(operator.order + 1))
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def _ramified_derivative(parameter: sp.Symbol, ramification: int):
|
|
275
|
+
def derivative(expression: sp.Expr) -> sp.Expr:
|
|
276
|
+
return sp.diff(expression, parameter) / (
|
|
277
|
+
ramification * parameter ** (ramification - 1)
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
derivative.parameter = parameter
|
|
281
|
+
derivative.ramification_index = ramification
|
|
282
|
+
return derivative
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def _ramified_operator_data(
|
|
286
|
+
localized: LocalizedOperator,
|
|
287
|
+
parameter: sp.Symbol,
|
|
288
|
+
ramification: int,
|
|
289
|
+
) -> tuple[tuple[sp.Expr, ...], callable]:
|
|
290
|
+
h = localized.local_variable
|
|
291
|
+
coefficients = tuple(
|
|
292
|
+
sp.cancel(sp.together(c.subs(h, parameter**ramification)))
|
|
293
|
+
for c in localized.operator.coefficients
|
|
294
|
+
)
|
|
295
|
+
return coefficients, _ramified_derivative(parameter, ramification)
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def _solve_leading_correction(
|
|
299
|
+
expression: sp.Expr,
|
|
300
|
+
parameter: sp.Symbol,
|
|
301
|
+
unknown: sp.Symbol,
|
|
302
|
+
) -> tuple[sp.Expr, ...]:
|
|
303
|
+
"""Solve the leading coefficient equation for a prescribed series term.
|
|
304
|
+
|
|
305
|
+
The amplitude recurrence still uses a fixed integral parameter grid; the
|
|
306
|
+
Newton--Puiseux Riccati refinement below uses the more general secondary
|
|
307
|
+
balance machinery.
|
|
308
|
+
"""
|
|
309
|
+
|
|
310
|
+
expression = sp.cancel(sp.together(sp.expand(expression)))
|
|
311
|
+
if expression == 0:
|
|
312
|
+
return (sp.S.Zero,)
|
|
313
|
+
try:
|
|
314
|
+
_, leading = local_order_and_leading_coefficient(expression, parameter)
|
|
315
|
+
except (ValueError, NotImplementedError) as exc:
|
|
316
|
+
raise FormalRefinementError(
|
|
317
|
+
f"could not determine the next formal coefficient from {expression!s}"
|
|
318
|
+
) from exc
|
|
319
|
+
leading = sp.factor(leading)
|
|
320
|
+
if not leading.has(unknown):
|
|
321
|
+
raise FormalRefinementError(
|
|
322
|
+
"the current residual cannot be cancelled at the requested series power"
|
|
323
|
+
)
|
|
324
|
+
try:
|
|
325
|
+
solutions = sp.solve(sp.Eq(leading, 0), unknown)
|
|
326
|
+
except SYMBOLIC_FAILURES as exc:
|
|
327
|
+
raise FormalRefinementError(
|
|
328
|
+
f"could not solve formal coefficient equation {leading!s} = 0"
|
|
329
|
+
) from exc
|
|
330
|
+
cleaned: list[sp.Expr] = []
|
|
331
|
+
for solution in solutions:
|
|
332
|
+
solution = sp.simplify(solution)
|
|
333
|
+
if solution.has(unknown):
|
|
334
|
+
continue
|
|
335
|
+
if not any(sp.simplify(solution - old) == 0 for old in cleaned):
|
|
336
|
+
cleaned.append(solution)
|
|
337
|
+
if not cleaned:
|
|
338
|
+
raise FormalRefinementError(
|
|
339
|
+
f"formal coefficient equation {leading!s} = 0 has no resolved branch"
|
|
340
|
+
)
|
|
341
|
+
return tuple(sorted(cleaned, key=sp.default_sort_key))
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
def _differential_perturbation_polynomial(
|
|
345
|
+
coefficients: tuple[sp.Expr, ...],
|
|
346
|
+
base_log_derivative: sp.Expr,
|
|
347
|
+
parameter: sp.Symbol,
|
|
348
|
+
ramification: int,
|
|
349
|
+
) -> tuple[sp.Poly, tuple[sp.Symbol, ...]]:
|
|
350
|
+
"""Return the Riccati residual as a differential polynomial in a perturbation.
|
|
351
|
+
|
|
352
|
+
If ``w = w0 + z``, symbols ``z0, z1, ...`` stand for
|
|
353
|
+
``z, D_h z, ...``. The derivation is implemented algebraically, avoiding
|
|
354
|
+
an internal undefined SymPy function, keeping the Newton support easy to
|
|
355
|
+
inspect exactly.
|
|
356
|
+
"""
|
|
357
|
+
|
|
358
|
+
order = len(coefficients) - 1
|
|
359
|
+
if order < 1:
|
|
360
|
+
raise FormalRefinementError(
|
|
361
|
+
"secondary Riccati refinement requires positive order"
|
|
362
|
+
)
|
|
363
|
+
z = sp.symbols(f"_z0:{order}")
|
|
364
|
+
|
|
365
|
+
def derivative(expression: sp.Expr) -> sp.Expr:
|
|
366
|
+
result = sp.diff(expression, parameter) / (
|
|
367
|
+
ramification * parameter ** (ramification - 1)
|
|
368
|
+
)
|
|
369
|
+
for k in range(order - 1):
|
|
370
|
+
result += sp.diff(expression, z[k]) * z[k + 1]
|
|
371
|
+
return sp.expand(result)
|
|
372
|
+
|
|
373
|
+
bells: list[sp.Expr] = [sp.S.One]
|
|
374
|
+
w = base_log_derivative + z[0]
|
|
375
|
+
for _ in range(order):
|
|
376
|
+
bells.append(sp.expand(derivative(bells[-1]) + w * bells[-1]))
|
|
377
|
+
residual = sp.expand(sum(coefficients[j] * bells[j] for j in range(order + 1)))
|
|
378
|
+
try:
|
|
379
|
+
return sp.Poly(residual, *z), z
|
|
380
|
+
except sp.PolynomialError as exc:
|
|
381
|
+
raise FormalRefinementError(
|
|
382
|
+
"could not represent the translated Riccati equation as a differential polynomial"
|
|
383
|
+
) from exc
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
def _riccati_newton_support(
|
|
387
|
+
coefficients: tuple[sp.Expr, ...],
|
|
388
|
+
base_log_derivative: sp.Expr,
|
|
389
|
+
parameter: sp.Symbol,
|
|
390
|
+
ramification: int,
|
|
391
|
+
) -> tuple[_RiccatiSupportTerm, ...]:
|
|
392
|
+
polynomial, _ = _differential_perturbation_polynomial(
|
|
393
|
+
coefficients, base_log_derivative, parameter, ramification
|
|
394
|
+
)
|
|
395
|
+
support: list[_RiccatiSupportTerm] = []
|
|
396
|
+
for monomial, coefficient in polynomial.terms():
|
|
397
|
+
coefficient = sp.cancel(sp.together(coefficient))
|
|
398
|
+
if coefficient == 0:
|
|
399
|
+
continue
|
|
400
|
+
try:
|
|
401
|
+
valuation, leading = local_order_and_leading_coefficient(
|
|
402
|
+
coefficient, parameter
|
|
403
|
+
)
|
|
404
|
+
except (ValueError, NotImplementedError) as exc:
|
|
405
|
+
raise FormalRefinementError(
|
|
406
|
+
"could not determine a coefficient valuation in the secondary "
|
|
407
|
+
"Riccati Newton polygon"
|
|
408
|
+
) from exc
|
|
409
|
+
support.append(
|
|
410
|
+
_RiccatiSupportTerm(
|
|
411
|
+
monomial=tuple(int(e) for e in monomial),
|
|
412
|
+
valuation=sp.Rational(valuation),
|
|
413
|
+
leading_coefficient=sp.simplify(leading),
|
|
414
|
+
degree=sum(monomial),
|
|
415
|
+
derivative_weight=sum(k * e for k, e in enumerate(monomial)),
|
|
416
|
+
)
|
|
417
|
+
)
|
|
418
|
+
return tuple(support)
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
def _falling_derivative_factor(
|
|
422
|
+
parameter_power: sp.Rational,
|
|
423
|
+
ramification: int,
|
|
424
|
+
derivative_order: int,
|
|
425
|
+
) -> sp.Expr:
|
|
426
|
+
exponent = sp.Rational(parameter_power, ramification)
|
|
427
|
+
result = sp.S.One
|
|
428
|
+
for j in range(derivative_order):
|
|
429
|
+
result *= exponent - j
|
|
430
|
+
return sp.simplify(result)
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
def _characteristic_roots_with_multiplicity(
|
|
434
|
+
polynomial: sp.Expr,
|
|
435
|
+
variable: sp.Symbol,
|
|
436
|
+
) -> tuple[tuple[sp.Expr, int], ...]:
|
|
437
|
+
"""Resolve every characteristic root, including zero, exactly when possible."""
|
|
438
|
+
|
|
439
|
+
polynomial = sp.factor(polynomial)
|
|
440
|
+
try:
|
|
441
|
+
poly = sp.Poly(polynomial, variable)
|
|
442
|
+
except sp.PolynomialError as exc:
|
|
443
|
+
raise FormalRefinementError(
|
|
444
|
+
f"secondary characteristic expression is not polynomial in {variable!s}"
|
|
445
|
+
) from exc
|
|
446
|
+
degree = int(poly.degree())
|
|
447
|
+
if degree <= 0:
|
|
448
|
+
return ()
|
|
449
|
+
|
|
450
|
+
try:
|
|
451
|
+
root_map = sp.roots(poly, cubics=False, quartics=False)
|
|
452
|
+
except SYMBOLIC_FAILURES:
|
|
453
|
+
root_map = {}
|
|
454
|
+
roots = {sp.simplify(root): int(mult) for root, mult in root_map.items()}
|
|
455
|
+
|
|
456
|
+
if sum(roots.values()) != degree:
|
|
457
|
+
try:
|
|
458
|
+
all_roots = poly.all_roots(radicals=False)
|
|
459
|
+
except SYMBOLIC_FAILURES:
|
|
460
|
+
all_roots = []
|
|
461
|
+
if len(all_roots) == degree:
|
|
462
|
+
roots = {}
|
|
463
|
+
for root in all_roots:
|
|
464
|
+
root = sp.simplify(root)
|
|
465
|
+
roots[root] = roots.get(root, 0) + 1
|
|
466
|
+
|
|
467
|
+
if sum(roots.values()) != degree:
|
|
468
|
+
try:
|
|
469
|
+
solved = sp.solve(sp.Eq(polynomial, 0), variable)
|
|
470
|
+
except SYMBOLIC_FAILURES:
|
|
471
|
+
solved = []
|
|
472
|
+
if solved and len(solved) == degree:
|
|
473
|
+
roots = {}
|
|
474
|
+
for root in solved:
|
|
475
|
+
root = sp.simplify(root)
|
|
476
|
+
roots[root] = roots.get(root, 0) + 1
|
|
477
|
+
|
|
478
|
+
if sum(roots.values()) != degree:
|
|
479
|
+
raise FormalRefinementError(
|
|
480
|
+
f"could not resolve all roots of secondary characteristic polynomial "
|
|
481
|
+
f"{polynomial!s}"
|
|
482
|
+
)
|
|
483
|
+
return tuple(sorted(roots.items(), key=lambda item: sp.default_sort_key(item[0])))
|
|
484
|
+
|
|
485
|
+
|
|
486
|
+
def _secondary_characteristic_at_power(
|
|
487
|
+
support: tuple[_RiccatiSupportTerm, ...],
|
|
488
|
+
parameter_power: sp.Rational,
|
|
489
|
+
ramification: int,
|
|
490
|
+
variable: sp.Symbol,
|
|
491
|
+
) -> sp.Expr:
|
|
492
|
+
active: list[tuple[_RiccatiSupportTerm, sp.Expr, sp.Rational]] = []
|
|
493
|
+
for item in support:
|
|
494
|
+
derivative_factor = sp.S.One
|
|
495
|
+
for order, exponent in enumerate(item.monomial):
|
|
496
|
+
if exponent:
|
|
497
|
+
derivative_factor *= (
|
|
498
|
+
_falling_derivative_factor(parameter_power, ramification, order)
|
|
499
|
+
** exponent
|
|
500
|
+
)
|
|
501
|
+
derivative_factor = sp.simplify(derivative_factor)
|
|
502
|
+
if derivative_factor == 0:
|
|
503
|
+
continue
|
|
504
|
+
intercept = item.valuation - ramification * item.derivative_weight
|
|
505
|
+
value = sp.simplify(intercept + item.degree * parameter_power)
|
|
506
|
+
active.append((item, derivative_factor, sp.Rational(value)))
|
|
507
|
+
|
|
508
|
+
if not active:
|
|
509
|
+
return sp.S.Zero
|
|
510
|
+
minimum = min(value for _, _, value in active)
|
|
511
|
+
characteristic = sp.expand(
|
|
512
|
+
sum(
|
|
513
|
+
item.leading_coefficient * derivative_factor * variable**item.degree
|
|
514
|
+
for item, derivative_factor, value in active
|
|
515
|
+
if value == minimum
|
|
516
|
+
)
|
|
517
|
+
)
|
|
518
|
+
return sp.factor(characteristic)
|
|
519
|
+
|
|
520
|
+
|
|
521
|
+
def _next_secondary_balance(
|
|
522
|
+
coefficients: tuple[sp.Expr, ...],
|
|
523
|
+
state: _LogDerivativeState,
|
|
524
|
+
) -> _SecondaryBalance | None:
|
|
525
|
+
"""Find the next Newton--Puiseux correction of a translated Riccati branch."""
|
|
526
|
+
|
|
527
|
+
support = _riccati_newton_support(
|
|
528
|
+
coefficients,
|
|
529
|
+
state.expression,
|
|
530
|
+
state.parameter,
|
|
531
|
+
state.ramification,
|
|
532
|
+
)
|
|
533
|
+
# If the translated equation has no constant term, the current logarithmic
|
|
534
|
+
# derivative is already an exact formal solution; positive-degree support
|
|
535
|
+
# describes perturbations of it, not further coefficients of this branch.
|
|
536
|
+
if not any(item.degree == 0 for item in support):
|
|
537
|
+
return None
|
|
538
|
+
|
|
539
|
+
candidates: set[sp.Rational] = set()
|
|
540
|
+
for index, left in enumerate(support):
|
|
541
|
+
left_intercept = left.valuation - state.ramification * left.derivative_weight
|
|
542
|
+
for right in support[index + 1 :]:
|
|
543
|
+
if left.degree == right.degree:
|
|
544
|
+
continue
|
|
545
|
+
right_intercept = (
|
|
546
|
+
right.valuation - state.ramification * right.derivative_weight
|
|
547
|
+
)
|
|
548
|
+
power = sp.simplify(
|
|
549
|
+
(right_intercept - left_intercept) / (left.degree - right.degree)
|
|
550
|
+
)
|
|
551
|
+
if power.is_Rational is not True:
|
|
552
|
+
continue
|
|
553
|
+
power = sp.Rational(power)
|
|
554
|
+
if state.search_floor < power <= -state.ramification:
|
|
555
|
+
candidates.add(power)
|
|
556
|
+
|
|
557
|
+
characteristic_variable = sp.Dummy("c")
|
|
558
|
+
for power in sorted(candidates):
|
|
559
|
+
characteristic = _secondary_characteristic_at_power(
|
|
560
|
+
support, power, state.ramification, characteristic_variable
|
|
561
|
+
)
|
|
562
|
+
if not characteristic.has(characteristic_variable):
|
|
563
|
+
continue
|
|
564
|
+
roots = _characteristic_roots_with_multiplicity(
|
|
565
|
+
characteristic, characteristic_variable
|
|
566
|
+
)
|
|
567
|
+
if roots:
|
|
568
|
+
return _SecondaryBalance(
|
|
569
|
+
parameter_power=power,
|
|
570
|
+
characteristic_variable=characteristic_variable,
|
|
571
|
+
characteristic_polynomial=characteristic,
|
|
572
|
+
roots=roots,
|
|
573
|
+
)
|
|
574
|
+
return None
|
|
575
|
+
|
|
576
|
+
|
|
577
|
+
def _reramify_state(
|
|
578
|
+
localized: LocalizedOperator,
|
|
579
|
+
state: _LogDerivativeState,
|
|
580
|
+
multiplier: int,
|
|
581
|
+
) -> tuple[_LogDerivativeState, tuple[sp.Expr, ...]]:
|
|
582
|
+
if multiplier == 1:
|
|
583
|
+
coefficients, _ = _ramified_operator_data(
|
|
584
|
+
localized, state.parameter, state.ramification
|
|
585
|
+
)
|
|
586
|
+
return state, coefficients
|
|
587
|
+
new_parameter = sp.Dummy("t", positive=True)
|
|
588
|
+
new_ramification = state.ramification * multiplier
|
|
589
|
+
new_expression = sp.expand(
|
|
590
|
+
state.expression.subs(state.parameter, new_parameter**multiplier)
|
|
591
|
+
)
|
|
592
|
+
new_coefficients_history = tuple(
|
|
593
|
+
LogDerivativeCoefficient(item.power * multiplier, item.coefficient)
|
|
594
|
+
for item in state.coefficients
|
|
595
|
+
)
|
|
596
|
+
reramified = _LogDerivativeState(
|
|
597
|
+
parameter=new_parameter,
|
|
598
|
+
ramification=new_ramification,
|
|
599
|
+
expression=new_expression,
|
|
600
|
+
coefficients=new_coefficients_history,
|
|
601
|
+
multiplicity=state.multiplicity,
|
|
602
|
+
search_floor=sp.Rational(state.search_floor * multiplier),
|
|
603
|
+
refinement_steps=state.refinement_steps,
|
|
604
|
+
)
|
|
605
|
+
coefficients, _ = _ramified_operator_data(
|
|
606
|
+
localized, new_parameter, new_ramification
|
|
607
|
+
)
|
|
608
|
+
return reramified, coefficients
|
|
609
|
+
|
|
610
|
+
|
|
611
|
+
def _log_derivative_to_local(
|
|
612
|
+
expression: sp.Expr,
|
|
613
|
+
parameter: sp.Symbol,
|
|
614
|
+
local_coordinate: sp.Symbol,
|
|
615
|
+
ramification: int,
|
|
616
|
+
) -> sp.Expr:
|
|
617
|
+
result = sp.S.Zero
|
|
618
|
+
for term in sp.Add.make_args(sp.expand(expression)):
|
|
619
|
+
coefficient, power = term.as_coeff_exponent(parameter)
|
|
620
|
+
power = sp.sympify(power)
|
|
621
|
+
if not power.is_Integer:
|
|
622
|
+
raise FormalRefinementError(
|
|
623
|
+
"ramified logarithmic derivative has a noninteger t-power"
|
|
624
|
+
)
|
|
625
|
+
result += coefficient * local_coordinate ** (
|
|
626
|
+
sp.Rational(int(power), ramification)
|
|
627
|
+
)
|
|
628
|
+
return sp.simplify(result)
|
|
629
|
+
|
|
630
|
+
|
|
631
|
+
def _integrated_exponential_and_power(
|
|
632
|
+
coefficients: tuple[LogDerivativeCoefficient, ...],
|
|
633
|
+
local_coordinate: sp.Symbol,
|
|
634
|
+
ramification: int,
|
|
635
|
+
) -> tuple[sp.Expr, sp.Expr]:
|
|
636
|
+
q = sp.S.Zero
|
|
637
|
+
alpha = sp.S.Zero
|
|
638
|
+
for item in coefficients:
|
|
639
|
+
m = item.power
|
|
640
|
+
c = item.coefficient
|
|
641
|
+
if c == 0:
|
|
642
|
+
continue
|
|
643
|
+
if m < -ramification:
|
|
644
|
+
h_power = sp.Rational(m, ramification) + 1
|
|
645
|
+
q += c * local_coordinate**h_power / h_power
|
|
646
|
+
elif m == -ramification:
|
|
647
|
+
alpha += c
|
|
648
|
+
return sp.simplify(q), sp.simplify(alpha)
|
|
649
|
+
|
|
650
|
+
|
|
651
|
+
def _refine_one_leading_part(
|
|
652
|
+
localized: LocalizedOperator,
|
|
653
|
+
leading: FormalExponentialPart,
|
|
654
|
+
*,
|
|
655
|
+
max_branches: int,
|
|
656
|
+
) -> tuple[CompleteFormalExponentialPart, ...]:
|
|
657
|
+
h = localized.local_variable
|
|
658
|
+
initial_ramification = int(leading.ramification_index)
|
|
659
|
+
parameter = sp.Dummy("t", positive=True)
|
|
660
|
+
|
|
661
|
+
rho = sp.Rational(leading.edge.slope)
|
|
662
|
+
first_power_expr = -initial_ramification * (rho + 1)
|
|
663
|
+
if not first_power_expr.is_Integer:
|
|
664
|
+
raise FormalRefinementError(
|
|
665
|
+
"Newton ramification did not integralize the leading Riccati power"
|
|
666
|
+
)
|
|
667
|
+
first_power = int(first_power_expr)
|
|
668
|
+
initial = sp.simplify(leading.characteristic_root * parameter**first_power)
|
|
669
|
+
pending: list[_LogDerivativeState] = [
|
|
670
|
+
_LogDerivativeState(
|
|
671
|
+
parameter=parameter,
|
|
672
|
+
ramification=initial_ramification,
|
|
673
|
+
expression=initial,
|
|
674
|
+
coefficients=(
|
|
675
|
+
LogDerivativeCoefficient(first_power, leading.characteristic_root),
|
|
676
|
+
),
|
|
677
|
+
multiplicity=int(leading.multiplicity),
|
|
678
|
+
search_floor=sp.Rational(first_power),
|
|
679
|
+
)
|
|
680
|
+
]
|
|
681
|
+
finished: list[_LogDerivativeState] = []
|
|
682
|
+
|
|
683
|
+
while pending:
|
|
684
|
+
state = pending.pop()
|
|
685
|
+
if state.search_floor >= -state.ramification:
|
|
686
|
+
finished.append(state)
|
|
687
|
+
continue
|
|
688
|
+
|
|
689
|
+
coefficients, _ = _ramified_operator_data(
|
|
690
|
+
localized, state.parameter, state.ramification
|
|
691
|
+
)
|
|
692
|
+
balance = _next_secondary_balance(coefficients, state)
|
|
693
|
+
if balance is None:
|
|
694
|
+
finished.append(state)
|
|
695
|
+
continue
|
|
696
|
+
|
|
697
|
+
power = balance.parameter_power
|
|
698
|
+
zero_multiplicity = 0
|
|
699
|
+
nonzero_roots: list[tuple[sp.Expr, int]] = []
|
|
700
|
+
for root, multiplicity in balance.roots:
|
|
701
|
+
if sp.simplify(root) == 0:
|
|
702
|
+
zero_multiplicity += multiplicity
|
|
703
|
+
else:
|
|
704
|
+
nonzero_roots.append((root, multiplicity))
|
|
705
|
+
|
|
706
|
+
# A zero characteristic root represents unresolved branches whose next
|
|
707
|
+
# nonzero correction occurs at a strictly higher power. Keep the same
|
|
708
|
+
# translated Riccati equation but advance its Newton search floor.
|
|
709
|
+
if zero_multiplicity:
|
|
710
|
+
pending.append(
|
|
711
|
+
_LogDerivativeState(
|
|
712
|
+
parameter=state.parameter,
|
|
713
|
+
ramification=state.ramification,
|
|
714
|
+
expression=state.expression,
|
|
715
|
+
coefficients=state.coefficients,
|
|
716
|
+
multiplicity=zero_multiplicity,
|
|
717
|
+
search_floor=power,
|
|
718
|
+
refinement_steps=(
|
|
719
|
+
*state.refinement_steps,
|
|
720
|
+
RiccatiRefinementStep(
|
|
721
|
+
local_power=sp.Rational(power, state.ramification),
|
|
722
|
+
coefficient=sp.S.Zero,
|
|
723
|
+
characteristic_polynomial=balance.characteristic_polynomial,
|
|
724
|
+
root_multiplicity=zero_multiplicity,
|
|
725
|
+
ramification_before=state.ramification,
|
|
726
|
+
ramification_after=state.ramification,
|
|
727
|
+
),
|
|
728
|
+
),
|
|
729
|
+
)
|
|
730
|
+
)
|
|
731
|
+
|
|
732
|
+
if nonzero_roots:
|
|
733
|
+
multiplier = int(power.q)
|
|
734
|
+
base_state, _ = _reramify_state(localized, state, multiplier)
|
|
735
|
+
integral_power_expr = sp.simplify(power * multiplier)
|
|
736
|
+
if integral_power_expr.is_Integer is not True:
|
|
737
|
+
raise FormalRefinementError(
|
|
738
|
+
"secondary Newton ramification failed to integralize a correction power"
|
|
739
|
+
)
|
|
740
|
+
integral_power = int(integral_power_expr)
|
|
741
|
+
for root, multiplicity in nonzero_roots:
|
|
742
|
+
step = RiccatiRefinementStep(
|
|
743
|
+
local_power=sp.Rational(power, state.ramification),
|
|
744
|
+
coefficient=sp.simplify(root),
|
|
745
|
+
characteristic_polynomial=balance.characteristic_polynomial,
|
|
746
|
+
root_multiplicity=multiplicity,
|
|
747
|
+
ramification_before=state.ramification,
|
|
748
|
+
ramification_after=base_state.ramification,
|
|
749
|
+
)
|
|
750
|
+
pending.append(
|
|
751
|
+
_LogDerivativeState(
|
|
752
|
+
parameter=base_state.parameter,
|
|
753
|
+
ramification=base_state.ramification,
|
|
754
|
+
expression=sp.expand(
|
|
755
|
+
base_state.expression
|
|
756
|
+
+ sp.simplify(root) * base_state.parameter**integral_power
|
|
757
|
+
),
|
|
758
|
+
coefficients=(
|
|
759
|
+
*base_state.coefficients,
|
|
760
|
+
LogDerivativeCoefficient(integral_power, sp.simplify(root)),
|
|
761
|
+
),
|
|
762
|
+
multiplicity=multiplicity,
|
|
763
|
+
search_floor=sp.Rational(integral_power),
|
|
764
|
+
refinement_steps=(*state.refinement_steps, step),
|
|
765
|
+
)
|
|
766
|
+
)
|
|
767
|
+
|
|
768
|
+
if len(pending) + len(finished) > max_branches:
|
|
769
|
+
raise FormalRefinementError(
|
|
770
|
+
f"formal Riccati refinement produced more than {max_branches} branches"
|
|
771
|
+
)
|
|
772
|
+
|
|
773
|
+
result: list[CompleteFormalExponentialPart] = []
|
|
774
|
+
for state in finished:
|
|
775
|
+
local_q, alpha = _integrated_exponential_and_power(
|
|
776
|
+
state.coefficients, h, state.ramification
|
|
777
|
+
)
|
|
778
|
+
local_w = _log_derivative_to_local(
|
|
779
|
+
state.expression, state.parameter, h, state.ramification
|
|
780
|
+
)
|
|
781
|
+
result.append(
|
|
782
|
+
CompleteFormalExponentialPart(
|
|
783
|
+
leading_part=leading,
|
|
784
|
+
local_coordinate=h,
|
|
785
|
+
local_parameter=state.parameter,
|
|
786
|
+
ramification_index=state.ramification,
|
|
787
|
+
logarithmic_derivative=local_w,
|
|
788
|
+
logarithmic_derivative_parameter=state.expression,
|
|
789
|
+
coefficients=state.coefficients,
|
|
790
|
+
local_exponential_polynomial=local_q,
|
|
791
|
+
exponential_polynomial=localized.to_original(local_q),
|
|
792
|
+
algebraic_power=alpha,
|
|
793
|
+
local_algebraic_prefactor=sp.simplify(h**alpha),
|
|
794
|
+
algebraic_prefactor=localized.to_original(h**alpha),
|
|
795
|
+
multiplicity=state.multiplicity,
|
|
796
|
+
refinement_steps=state.refinement_steps,
|
|
797
|
+
)
|
|
798
|
+
)
|
|
799
|
+
return tuple(
|
|
800
|
+
sorted(
|
|
801
|
+
result,
|
|
802
|
+
key=lambda item: (
|
|
803
|
+
sp.default_sort_key(item.local_exponential_polynomial),
|
|
804
|
+
sp.default_sort_key(item.algebraic_power),
|
|
805
|
+
),
|
|
806
|
+
)
|
|
807
|
+
)
|
|
808
|
+
|
|
809
|
+
|
|
810
|
+
def complete_formal_exponential_parts(
|
|
811
|
+
ode: sp.Expr | sp.Equality | LinearDifferentialOperator,
|
|
812
|
+
function: sp.FunctionClass | sp.Expr | None = None,
|
|
813
|
+
variable: sp.Symbol | None = None,
|
|
814
|
+
*,
|
|
815
|
+
point: sp.Expr = 0,
|
|
816
|
+
max_branches: int = 64,
|
|
817
|
+
) -> tuple[CompleteFormalExponentialPart, ...]:
|
|
818
|
+
"""Refine Newton leading parts through the complete exponential polynomial.
|
|
819
|
+
|
|
820
|
+
The refinement solves the Bell-polynomial Riccati equation in the Newton
|
|
821
|
+
uniformizer through logarithmic-derivative order ``h**(-1)``. Thus every
|
|
822
|
+
term that integrates to a negative power is included in the exponential
|
|
823
|
+
polynomial and the remaining ``h**(-1)`` coefficient is returned as the
|
|
824
|
+
algebraic power.
|
|
825
|
+
|
|
826
|
+
Degenerate characteristic roots are handled recursively: the translated
|
|
827
|
+
Riccati differential polynomial is given its own Newton--Puiseux support,
|
|
828
|
+
zero characteristic roots are propagated to later edges, and any rational
|
|
829
|
+
correction exponent introduces the additional ramification required to
|
|
830
|
+
integralize it. Refinement stops after the ``h**(-1)`` coefficient, since
|
|
831
|
+
later terms belong to the formal amplitude rather than the finite
|
|
832
|
+
exponential/power prefactor.
|
|
833
|
+
"""
|
|
834
|
+
|
|
835
|
+
localized = localize_operator(ode, function, variable, point=point)
|
|
836
|
+
result: list[CompleteFormalExponentialPart] = []
|
|
837
|
+
for leading in formal_exponential_parts(ode, function, variable, point=point):
|
|
838
|
+
result.extend(
|
|
839
|
+
_refine_one_leading_part(localized, leading, max_branches=max_branches)
|
|
840
|
+
)
|
|
841
|
+
return tuple(result)
|
|
842
|
+
|
|
843
|
+
|
|
844
|
+
def _amplitude_conjugated_coefficients(
|
|
845
|
+
localized: LocalizedOperator,
|
|
846
|
+
completed: CompleteFormalExponentialPart,
|
|
847
|
+
) -> tuple[tuple[sp.Expr, ...], callable]:
|
|
848
|
+
parameter = completed.local_parameter
|
|
849
|
+
ramification = completed.ramification_index
|
|
850
|
+
base_coefficients, derivative = _ramified_operator_data(
|
|
851
|
+
localized, parameter, ramification
|
|
852
|
+
)
|
|
853
|
+
try:
|
|
854
|
+
logarithmic_series = SparseLaurentSeries.from_expr(
|
|
855
|
+
completed.logarithmic_derivative_parameter, parameter
|
|
856
|
+
)
|
|
857
|
+
except ValueError:
|
|
858
|
+
bells = differential_bell_polynomials(
|
|
859
|
+
completed.logarithmic_derivative_parameter,
|
|
860
|
+
parameter,
|
|
861
|
+
localized.operator.order,
|
|
862
|
+
derivative=derivative,
|
|
863
|
+
)
|
|
864
|
+
else:
|
|
865
|
+
bells = tuple(
|
|
866
|
+
item.to_expr()
|
|
867
|
+
for item in differential_bell_laurent_series(
|
|
868
|
+
logarithmic_series,
|
|
869
|
+
localized.operator.order,
|
|
870
|
+
ramification_index=ramification,
|
|
871
|
+
)
|
|
872
|
+
)
|
|
873
|
+
transformed: list[sp.Expr] = []
|
|
874
|
+
n = localized.operator.order
|
|
875
|
+
for k in range(n + 1):
|
|
876
|
+
transformed.append(
|
|
877
|
+
sp.expand(
|
|
878
|
+
sum(
|
|
879
|
+
base_coefficients[j] * sp.binomial(j, k) * bells[j - k]
|
|
880
|
+
for j in range(k, n + 1)
|
|
881
|
+
)
|
|
882
|
+
)
|
|
883
|
+
)
|
|
884
|
+
return tuple(transformed), derivative
|
|
885
|
+
|
|
886
|
+
|
|
887
|
+
def _amplitude_residual(
|
|
888
|
+
transformed_coefficients: tuple[sp.Expr, ...],
|
|
889
|
+
amplitude: sp.Expr,
|
|
890
|
+
derivative,
|
|
891
|
+
) -> sp.Expr:
|
|
892
|
+
derivatives = [amplitude]
|
|
893
|
+
for _ in range(1, len(transformed_coefficients)):
|
|
894
|
+
derivatives.append(sp.expand(derivative(derivatives[-1])))
|
|
895
|
+
return sp.expand(
|
|
896
|
+
sum(c * derivatives[k] for k, c in enumerate(transformed_coefficients))
|
|
897
|
+
)
|
|
898
|
+
|
|
899
|
+
|
|
900
|
+
def formal_amplitude_series(
|
|
901
|
+
ode: sp.Expr | sp.Equality | LinearDifferentialOperator,
|
|
902
|
+
function: sp.FunctionClass | sp.Expr | None = None,
|
|
903
|
+
variable: sp.Symbol | None = None,
|
|
904
|
+
*,
|
|
905
|
+
point: sp.Expr = 0,
|
|
906
|
+
terms: int = 8,
|
|
907
|
+
max_branches: int = 64,
|
|
908
|
+
) -> tuple[FormalAmplitudeSeries, ...]:
|
|
909
|
+
"""Generate normalized formal amplitude series for all completed branches.
|
|
910
|
+
|
|
911
|
+
``terms`` is the number of coefficients in the ramified parameter,
|
|
912
|
+
including the normalized constant coefficient. Zeros are retained, so a
|
|
913
|
+
series whose natural step is ``t**3`` will contain two explicit zero slots
|
|
914
|
+
between successive nonzero coefficients.
|
|
915
|
+
"""
|
|
916
|
+
|
|
917
|
+
if terms < 1:
|
|
918
|
+
raise ValueError("terms must be at least 1")
|
|
919
|
+
localized = localize_operator(ode, function, variable, point=point)
|
|
920
|
+
completed_parts = complete_formal_exponential_parts(
|
|
921
|
+
ode,
|
|
922
|
+
function,
|
|
923
|
+
variable,
|
|
924
|
+
point=point,
|
|
925
|
+
max_branches=max_branches,
|
|
926
|
+
)
|
|
927
|
+
result: list[FormalAmplitudeSeries] = []
|
|
928
|
+
|
|
929
|
+
for branch_index, completed in enumerate(completed_parts):
|
|
930
|
+
parameter = completed.local_parameter
|
|
931
|
+
transformed, derivative = _amplitude_conjugated_coefficients(
|
|
932
|
+
localized, completed
|
|
933
|
+
)
|
|
934
|
+
amplitude = sp.S.One
|
|
935
|
+
values: list[sp.Expr] = [sp.S.One]
|
|
936
|
+
for power in range(1, terms):
|
|
937
|
+
unknown = sp.Dummy(f"a_{branch_index}_{power}")
|
|
938
|
+
trial = amplitude + unknown * parameter**power
|
|
939
|
+
residual = _amplitude_residual(transformed, trial, derivative)
|
|
940
|
+
solutions = _solve_leading_correction(residual, parameter, unknown)
|
|
941
|
+
if len(solutions) != 1:
|
|
942
|
+
raise FormalRefinementError(
|
|
943
|
+
"amplitude recurrence branched; logarithmic or resonant amplitude "
|
|
944
|
+
"solutions require a separate formal basis construction"
|
|
945
|
+
)
|
|
946
|
+
value = solutions[0]
|
|
947
|
+
amplitude = sp.expand(amplitude + value * parameter**power)
|
|
948
|
+
values.append(value)
|
|
949
|
+
|
|
950
|
+
residual = sp.cancel(
|
|
951
|
+
sp.together(_amplitude_residual(transformed, amplitude, derivative))
|
|
952
|
+
)
|
|
953
|
+
if residual == 0:
|
|
954
|
+
residual_valuation = None
|
|
955
|
+
else:
|
|
956
|
+
try:
|
|
957
|
+
residual_valuation, _ = local_order_and_leading_coefficient(
|
|
958
|
+
residual, parameter
|
|
959
|
+
)
|
|
960
|
+
except (ValueError, NotImplementedError):
|
|
961
|
+
residual_valuation = None
|
|
962
|
+
|
|
963
|
+
h = completed.local_coordinate
|
|
964
|
+
local_series = sp.expand(
|
|
965
|
+
sum(
|
|
966
|
+
values[k] * h ** sp.Rational(k, completed.ramification_index)
|
|
967
|
+
for k in range(terms)
|
|
968
|
+
)
|
|
969
|
+
)
|
|
970
|
+
x = localized.original_operator.variable
|
|
971
|
+
if sp.sympify(point) == sp.oo:
|
|
972
|
+
original_series = sp.simplify(local_series.subs(h, 1 / x))
|
|
973
|
+
else:
|
|
974
|
+
original_series = sp.simplify(local_series.subs(h, x - sp.sympify(point)))
|
|
975
|
+
result.append(
|
|
976
|
+
FormalAmplitudeSeries(
|
|
977
|
+
exponential_part=completed,
|
|
978
|
+
coefficients=tuple(values),
|
|
979
|
+
local_parameter=parameter,
|
|
980
|
+
local_series=local_series,
|
|
981
|
+
series=original_series,
|
|
982
|
+
residual=residual,
|
|
983
|
+
residual_valuation=residual_valuation,
|
|
984
|
+
)
|
|
985
|
+
)
|
|
986
|
+
return tuple(result)
|
|
987
|
+
|
|
988
|
+
|
|
989
|
+
def formal_asymptotic_solutions(
|
|
990
|
+
ode: sp.Expr | sp.Equality | LinearDifferentialOperator,
|
|
991
|
+
function: sp.FunctionClass | sp.Expr | None = None,
|
|
992
|
+
variable: sp.Symbol | None = None,
|
|
993
|
+
*,
|
|
994
|
+
point: sp.Expr = 0,
|
|
995
|
+
terms: int = 8,
|
|
996
|
+
max_branches: int = 64,
|
|
997
|
+
) -> tuple[FormalAsymptoticSolution, ...]:
|
|
998
|
+
"""Return formal WKB solutions through the requested amplitude order."""
|
|
999
|
+
|
|
1000
|
+
amplitudes = formal_amplitude_series(
|
|
1001
|
+
ode,
|
|
1002
|
+
function,
|
|
1003
|
+
variable,
|
|
1004
|
+
point=point,
|
|
1005
|
+
terms=terms,
|
|
1006
|
+
max_branches=max_branches,
|
|
1007
|
+
)
|
|
1008
|
+
result: list[FormalAsymptoticSolution] = []
|
|
1009
|
+
for amplitude in amplitudes:
|
|
1010
|
+
completed = amplitude.exponential_part
|
|
1011
|
+
local_expression = sp.exp(completed.local_exponential_polynomial)
|
|
1012
|
+
local_expression *= completed.local_algebraic_prefactor * amplitude.local_series
|
|
1013
|
+
result.append(
|
|
1014
|
+
FormalAsymptoticSolution(
|
|
1015
|
+
point=sp.sympify(point),
|
|
1016
|
+
exponential_part=completed,
|
|
1017
|
+
amplitude=amplitude,
|
|
1018
|
+
local_expression=analytic_powsimp(
|
|
1019
|
+
sp.expand_power_base(local_expression, force=False)
|
|
1020
|
+
),
|
|
1021
|
+
expression=sp.simplify(
|
|
1022
|
+
sp.exp(completed.exponential_polynomial)
|
|
1023
|
+
* completed.algebraic_prefactor
|
|
1024
|
+
* amplitude.series
|
|
1025
|
+
),
|
|
1026
|
+
)
|
|
1027
|
+
)
|
|
1028
|
+
return tuple(result)
|