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,615 @@
|
|
|
1
|
+
"""Public local, formal, Stokes, and parameter analysis for linear systems."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from itertools import combinations
|
|
7
|
+
|
|
8
|
+
import sympy as sp
|
|
9
|
+
from semialg import is_satisfiable, parametric_cad
|
|
10
|
+
|
|
11
|
+
from ._local import rational_valuation
|
|
12
|
+
from ._zero import ZeroStatus, exact_zero_status
|
|
13
|
+
from .block_decomposition import LeveltTurrittinReduction, levelt_turrittin_reduce
|
|
14
|
+
from .matrix_series import MatrixLaurentSeries
|
|
15
|
+
from .system import FirstOrderSystem
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass(frozen=True)
|
|
19
|
+
class SystemResonance:
|
|
20
|
+
source: sp.Expr
|
|
21
|
+
target: sp.Expr
|
|
22
|
+
difference: sp.Expr
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass(frozen=True)
|
|
26
|
+
class SystemSingularityAnalysis:
|
|
27
|
+
point: sp.Expr
|
|
28
|
+
kind: str
|
|
29
|
+
pole_order: int
|
|
30
|
+
poincare_rank: int
|
|
31
|
+
leading_matrix: sp.ImmutableMatrix
|
|
32
|
+
leading_rank: int | None
|
|
33
|
+
residue: sp.ImmutableMatrix | None
|
|
34
|
+
exponents: tuple[sp.Expr, ...]
|
|
35
|
+
resonances: tuple[SystemResonance, ...]
|
|
36
|
+
local_system: FirstOrderSystem
|
|
37
|
+
|
|
38
|
+
@property
|
|
39
|
+
def regular_singular(self) -> bool:
|
|
40
|
+
return self.kind == "regular_singular"
|
|
41
|
+
|
|
42
|
+
@property
|
|
43
|
+
def irregular(self) -> bool:
|
|
44
|
+
return self.kind == "irregular"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _local_system(system: FirstOrderSystem, point: sp.Expr) -> FirstOrderSystem:
|
|
48
|
+
point = sp.sympify(point)
|
|
49
|
+
t = sp.Dummy("t", positive=True)
|
|
50
|
+
if point == sp.oo:
|
|
51
|
+
return system.change_variable(t, 1 / t)
|
|
52
|
+
return system.change_variable(t, point + t)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _matrix_pole_data(system: FirstOrderSystem) -> tuple[int, sp.ImmutableMatrix]:
|
|
56
|
+
t = system.variable
|
|
57
|
+
vals = []
|
|
58
|
+
for entry in system.matrix:
|
|
59
|
+
status = exact_zero_status(entry)
|
|
60
|
+
if status is ZeroStatus.ZERO:
|
|
61
|
+
continue
|
|
62
|
+
if status is ZeroStatus.UNKNOWN and (entry.free_symbols - {t}):
|
|
63
|
+
raise NotImplementedError(
|
|
64
|
+
"system singularity analysis cannot certify a local valuation "
|
|
65
|
+
"for a parameter-dependent entry whose zero status is unresolved; "
|
|
66
|
+
"stratify or specialize the parameters first"
|
|
67
|
+
)
|
|
68
|
+
# An expression depending only on the local coordinate may have unknown
|
|
69
|
+
# SymPy ``is_zero`` status while still being a nonzero meromorphic
|
|
70
|
+
# function (for example (t**2 + 2)/t**3). Its valuation is safe to
|
|
71
|
+
# compute directly.
|
|
72
|
+
valuation = rational_valuation(entry, t, 0)
|
|
73
|
+
if valuation is None:
|
|
74
|
+
raise NotImplementedError(
|
|
75
|
+
"system singularity analysis requires finite rational local valuations"
|
|
76
|
+
)
|
|
77
|
+
vals.append(valuation)
|
|
78
|
+
minimum = min(vals, default=0)
|
|
79
|
+
pole = max(0, -minimum)
|
|
80
|
+
scale = t**pole
|
|
81
|
+
leading = sp.Matrix(system.matrix).applyfunc(
|
|
82
|
+
lambda e: sp.simplify(sp.limit(scale * e, t, 0))
|
|
83
|
+
)
|
|
84
|
+
return pole, sp.ImmutableMatrix(leading)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _resonances(exponents: tuple[sp.Expr, ...]) -> tuple[SystemResonance, ...]:
|
|
88
|
+
result: list[SystemResonance] = []
|
|
89
|
+
for a, b in combinations(exponents, 2):
|
|
90
|
+
d = sp.simplify(a - b)
|
|
91
|
+
# Resonance requires a *nonzero* integral difference. Syntactic
|
|
92
|
+
# ``d != 0`` is unsound for parameterized integer symbols because it
|
|
93
|
+
# is true even when the parameter may specialize to zero.
|
|
94
|
+
if d.is_zero is False and d.is_integer is True:
|
|
95
|
+
result.append(SystemResonance(a, b, d))
|
|
96
|
+
elif d.is_zero is False and (-d).is_integer is True:
|
|
97
|
+
result.append(SystemResonance(b, a, -d))
|
|
98
|
+
return tuple(result)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def analyze_system_singularity(
|
|
102
|
+
system: FirstOrderSystem, point: sp.Expr = 0
|
|
103
|
+
) -> SystemSingularityAnalysis:
|
|
104
|
+
"""Classify a finite point or infinity of ``Y' = A(x)Y+b(x)``.
|
|
105
|
+
|
|
106
|
+
Exponents are residue eigenvalues for regular-singular systems. Resonance
|
|
107
|
+
records nonzero integral differences between residue eigenvalues. For an
|
|
108
|
+
irregular point, ``poincare_rank`` is the raw connection pole order minus
|
|
109
|
+
one; formal gauge reduction may lower it.
|
|
110
|
+
"""
|
|
111
|
+
local = _local_system(system, point)
|
|
112
|
+
pole, leading = _matrix_pole_data(local)
|
|
113
|
+
kind = "ordinary" if pole == 0 else "regular_singular" if pole == 1 else "irregular"
|
|
114
|
+
residue = leading if pole == 1 else None
|
|
115
|
+
exponents: tuple[sp.Expr, ...] = ()
|
|
116
|
+
if residue is not None:
|
|
117
|
+
roots = sp.Matrix(residue).eigenvals()
|
|
118
|
+
exponents = tuple(root for root, mult in roots.items() for _ in range(mult))
|
|
119
|
+
exponents = tuple(sorted(exponents, key=sp.default_sort_key))
|
|
120
|
+
rank = leading.rank() if not leading.free_symbols else None
|
|
121
|
+
return SystemSingularityAnalysis(
|
|
122
|
+
sp.sympify(point),
|
|
123
|
+
kind,
|
|
124
|
+
pole,
|
|
125
|
+
max(0, pole - 1),
|
|
126
|
+
leading,
|
|
127
|
+
rank,
|
|
128
|
+
residue,
|
|
129
|
+
exponents,
|
|
130
|
+
_resonances(exponents),
|
|
131
|
+
local,
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
@dataclass(frozen=True)
|
|
136
|
+
class FormalReductionCertificate:
|
|
137
|
+
"""Evidence describing a bounded or adaptive formal reduction search."""
|
|
138
|
+
|
|
139
|
+
verified: bool
|
|
140
|
+
complete: bool
|
|
141
|
+
adaptive: bool
|
|
142
|
+
attempted_depths: tuple[int, ...]
|
|
143
|
+
attempted_cover_indices: tuple[int | None, ...]
|
|
144
|
+
final_depth: int
|
|
145
|
+
final_cover_index: int | None
|
|
146
|
+
ramification_index: int
|
|
147
|
+
limitation: str | None
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
@dataclass(frozen=True)
|
|
151
|
+
class FormalSystemAnalysis:
|
|
152
|
+
point: sp.Expr
|
|
153
|
+
singularity: SystemSingularityAnalysis
|
|
154
|
+
connection: MatrixLaurentSeries
|
|
155
|
+
reduction: LeveltTurrittinReduction
|
|
156
|
+
exponential_parts: tuple[sp.Expr, ...]
|
|
157
|
+
certificate: FormalReductionCertificate
|
|
158
|
+
|
|
159
|
+
@property
|
|
160
|
+
def complete(self) -> bool:
|
|
161
|
+
return self.reduction.complete
|
|
162
|
+
|
|
163
|
+
@property
|
|
164
|
+
def ramification_index(self) -> int:
|
|
165
|
+
return self.reduction.ramification_index
|
|
166
|
+
|
|
167
|
+
@property
|
|
168
|
+
def limitation(self) -> str | None:
|
|
169
|
+
return self.reduction.limitation
|
|
170
|
+
|
|
171
|
+
def verify(self) -> bool:
|
|
172
|
+
return self.reduction.verify()
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def formal_system_analysis(
|
|
176
|
+
system: FirstOrderSystem,
|
|
177
|
+
point: sp.Expr = 0,
|
|
178
|
+
*,
|
|
179
|
+
max_power: int = 3,
|
|
180
|
+
max_depth: int = 2,
|
|
181
|
+
max_cover_index: int | None = None,
|
|
182
|
+
adaptive: bool = False,
|
|
183
|
+
max_adaptive_depth: int = 6,
|
|
184
|
+
max_adaptive_cover_index: int | None = 12,
|
|
185
|
+
) -> FormalSystemAnalysis:
|
|
186
|
+
"""Run the exact bounded Moser/Levelt--Turrittin pipeline on a system."""
|
|
187
|
+
singularity = analyze_system_singularity(system, point)
|
|
188
|
+
connection = MatrixLaurentSeries.from_matrix(
|
|
189
|
+
singularity.local_system.matrix, singularity.local_system.variable
|
|
190
|
+
)
|
|
191
|
+
if max_adaptive_depth < max_depth:
|
|
192
|
+
raise ValueError("max_adaptive_depth must be at least max_depth")
|
|
193
|
+
attempted_depths: list[int] = []
|
|
194
|
+
attempted_covers: list[int | None] = []
|
|
195
|
+
depth = max_depth
|
|
196
|
+
cover = max_cover_index
|
|
197
|
+
while True:
|
|
198
|
+
attempted_depths.append(depth)
|
|
199
|
+
attempted_covers.append(cover)
|
|
200
|
+
reduction = levelt_turrittin_reduce(
|
|
201
|
+
connection,
|
|
202
|
+
max_power=max_power,
|
|
203
|
+
max_depth=depth,
|
|
204
|
+
max_cover_index=cover,
|
|
205
|
+
)
|
|
206
|
+
if reduction.complete or not adaptive or depth >= max_adaptive_depth:
|
|
207
|
+
break
|
|
208
|
+
depth = min(max_adaptive_depth, max(depth + 1, 2 * max(1, depth)))
|
|
209
|
+
if max_adaptive_cover_index is not None:
|
|
210
|
+
if cover is None:
|
|
211
|
+
cover = min(4, max_adaptive_cover_index)
|
|
212
|
+
elif cover < max_adaptive_cover_index:
|
|
213
|
+
cover = min(max_adaptive_cover_index, max(cover + 1, 2 * cover))
|
|
214
|
+
q_parts: list[sp.Expr] = []
|
|
215
|
+
if reduction.complete:
|
|
216
|
+
for block in reduction.final_diagonalization.blocks:
|
|
217
|
+
q = sp.S.Zero
|
|
218
|
+
dimension = block.rows
|
|
219
|
+
for power, coefficient in block.terms:
|
|
220
|
+
if power >= -1:
|
|
221
|
+
continue
|
|
222
|
+
matrix = sp.Matrix(coefficient)
|
|
223
|
+
scalar = sp.simplify(sp.trace(matrix) / dimension)
|
|
224
|
+
if matrix == scalar * sp.eye(dimension):
|
|
225
|
+
q += scalar * block.variable ** (power + 1) / (power + 1)
|
|
226
|
+
q_parts.append(sp.expand(q))
|
|
227
|
+
certificate = FormalReductionCertificate(
|
|
228
|
+
verified=reduction.verify(),
|
|
229
|
+
complete=reduction.complete,
|
|
230
|
+
adaptive=adaptive,
|
|
231
|
+
attempted_depths=tuple(attempted_depths),
|
|
232
|
+
attempted_cover_indices=tuple(attempted_covers),
|
|
233
|
+
final_depth=depth,
|
|
234
|
+
final_cover_index=cover,
|
|
235
|
+
ramification_index=reduction.ramification_index,
|
|
236
|
+
limitation=reduction.limitation,
|
|
237
|
+
)
|
|
238
|
+
return FormalSystemAnalysis(
|
|
239
|
+
point, singularity, connection, reduction, tuple(q_parts), certificate
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
@dataclass(frozen=True)
|
|
244
|
+
class SystemStokesPair:
|
|
245
|
+
blocks: tuple[int, int]
|
|
246
|
+
difference: sp.Expr
|
|
247
|
+
equal_magnitude_rays: tuple[sp.Expr, ...]
|
|
248
|
+
phase_alignment_rays: tuple[sp.Expr, ...]
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
@dataclass(frozen=True)
|
|
252
|
+
class SystemStokesGeometry:
|
|
253
|
+
point: sp.Expr
|
|
254
|
+
exponential_parts: tuple[sp.Expr, ...]
|
|
255
|
+
pairs: tuple[SystemStokesPair, ...]
|
|
256
|
+
structural_only: bool = True
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def _leading_monomial(q: sp.Expr, t: sp.Symbol) -> tuple[sp.Expr, int] | None:
|
|
260
|
+
q = sp.expand(q)
|
|
261
|
+
terms = []
|
|
262
|
+
for term in sp.Add.make_args(q):
|
|
263
|
+
c, p = term.as_coeff_exponent(t)
|
|
264
|
+
if p.is_Integer and p < 0 and not c.has(t):
|
|
265
|
+
terms.append((int(p), c))
|
|
266
|
+
if not terms:
|
|
267
|
+
return None
|
|
268
|
+
p, c = min(terms, key=lambda z: z[0])
|
|
269
|
+
return c, -p
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def _rays(c: sp.Expr, k: int, phase: bool) -> tuple[sp.Expr, ...]:
|
|
273
|
+
if c.is_real is not True and c.is_number is not True:
|
|
274
|
+
return ()
|
|
275
|
+
arg = sp.arg(c)
|
|
276
|
+
offset = 0 if phase else sp.pi / 2
|
|
277
|
+
return tuple(
|
|
278
|
+
sp.simplify((arg - offset - m * sp.pi) / k % (2 * sp.pi)) for m in range(2 * k)
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def system_stokes_geometry(
|
|
283
|
+
formal: FormalSystemAnalysis,
|
|
284
|
+
*,
|
|
285
|
+
exponential_parts: tuple[sp.Expr, ...] | None = None,
|
|
286
|
+
) -> SystemStokesGeometry:
|
|
287
|
+
"""Return structural Stokes rays between formal exponential blocks.
|
|
288
|
+
|
|
289
|
+
``exponential_parts`` may be supplied when the bounded reducer's block
|
|
290
|
+
objects do not expose scalar exponential polynomials. Analytic Stokes
|
|
291
|
+
multipliers are intentionally outside this result contract.
|
|
292
|
+
"""
|
|
293
|
+
if exponential_parts is None:
|
|
294
|
+
exponential_parts = formal.exponential_parts
|
|
295
|
+
t = formal.reduction.transformed_connection.variable
|
|
296
|
+
pairs = []
|
|
297
|
+
for i, j in combinations(range(len(exponential_parts)), 2):
|
|
298
|
+
d = sp.expand(exponential_parts[i] - exponential_parts[j])
|
|
299
|
+
lead = _leading_monomial(d, t)
|
|
300
|
+
if lead is None:
|
|
301
|
+
pairs.append(SystemStokesPair((i, j), d, (), ()))
|
|
302
|
+
continue
|
|
303
|
+
c, k = lead
|
|
304
|
+
pairs.append(SystemStokesPair((i, j), d, _rays(c, k, False), _rays(c, k, True)))
|
|
305
|
+
return SystemStokesGeometry(formal.point, tuple(exponential_parts), tuple(pairs))
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
@dataclass(frozen=True)
|
|
309
|
+
class SystemParameterStratum:
|
|
310
|
+
condition: sp.Expr
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
@dataclass(frozen=True)
|
|
314
|
+
class ParameterizedSystemAnalysis:
|
|
315
|
+
point: sp.Expr
|
|
316
|
+
parameters: tuple[sp.Symbol, ...]
|
|
317
|
+
transition_polynomials: tuple[sp.Expr, ...]
|
|
318
|
+
rank_loci: tuple[sp.Expr, ...]
|
|
319
|
+
collision_loci: tuple[sp.Expr, ...]
|
|
320
|
+
resonance_loci: tuple[sp.Expr, ...]
|
|
321
|
+
block_loci: tuple[sp.Expr, ...]
|
|
322
|
+
stokes_loci: tuple[sp.Expr, ...]
|
|
323
|
+
strata: tuple[SystemParameterStratum, ...]
|
|
324
|
+
exhaustive: bool
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
def _parameter_factors(
|
|
328
|
+
expr: sp.Expr, parameters: tuple[sp.Symbol, ...]
|
|
329
|
+
) -> list[sp.Expr]:
|
|
330
|
+
expr = sp.factor(expr)
|
|
331
|
+
if expr == 0:
|
|
332
|
+
return []
|
|
333
|
+
try:
|
|
334
|
+
factors = sp.factor_list(expr)[1]
|
|
335
|
+
except (sp.PolynomialError, TypeError, ValueError):
|
|
336
|
+
return []
|
|
337
|
+
return [sp.factor(f) for f, _ in factors if f.free_symbols & set(parameters)]
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
def system_parameter_analysis(
|
|
341
|
+
system: FirstOrderSystem,
|
|
342
|
+
parameters: tuple[sp.Symbol, ...],
|
|
343
|
+
point: sp.Expr = 0,
|
|
344
|
+
*,
|
|
345
|
+
assumptions: sp.Expr | bool = True,
|
|
346
|
+
max_resonance_order: int = 4,
|
|
347
|
+
exponential_parts: tuple[sp.Expr, ...] = (),
|
|
348
|
+
) -> ParameterizedSystemAnalysis:
|
|
349
|
+
"""Stratify exact algebraic system transition loci with ``semialg``.
|
|
350
|
+
|
|
351
|
+
The initial public contract detects leading-rank transitions and residue
|
|
352
|
+
eigenvalue collisions. More refined formal-block and Stokes loci can be
|
|
353
|
+
added without changing the result shape.
|
|
354
|
+
"""
|
|
355
|
+
assumptions = sp.sympify(assumptions)
|
|
356
|
+
local = _local_system(system, point)
|
|
357
|
+
t = local.variable
|
|
358
|
+
structural_vals: list[int] = []
|
|
359
|
+
for entry in local.matrix:
|
|
360
|
+
if entry == 0:
|
|
361
|
+
continue
|
|
362
|
+
num, den = sp.fraction(sp.cancel(entry))
|
|
363
|
+
pn, pd = sp.Poly(num, t), sp.Poly(den, t)
|
|
364
|
+
nv = min(k for k in range(pn.degree() + 1) if pn.nth(k) != 0)
|
|
365
|
+
dv = min(k for k in range(pd.degree() + 1) if pd.nth(k) != 0)
|
|
366
|
+
structural_vals.append(nv - dv)
|
|
367
|
+
minimum = min(structural_vals, default=0)
|
|
368
|
+
pole = max(0, -minimum)
|
|
369
|
+
leading = sp.ImmutableMatrix(
|
|
370
|
+
sp.Matrix(local.matrix).applyfunc(
|
|
371
|
+
lambda e: sp.cancel(sp.limit(t**pole * e, t, 0))
|
|
372
|
+
)
|
|
373
|
+
)
|
|
374
|
+
rank_loci: list[sp.Expr] = []
|
|
375
|
+
collision_loci: list[sp.Expr] = []
|
|
376
|
+
resonance_loci: list[sp.Expr] = []
|
|
377
|
+
block_loci: list[sp.Expr] = []
|
|
378
|
+
stokes_loci: list[sp.Expr] = []
|
|
379
|
+
# Rank transitions: all nonconstant minors can change the leading rank.
|
|
380
|
+
for size in range(1, min(leading.shape) + 1):
|
|
381
|
+
for rows in combinations(range(leading.rows), size):
|
|
382
|
+
for cols in combinations(range(leading.cols), size):
|
|
383
|
+
rank_loci.extend(
|
|
384
|
+
_parameter_factors(
|
|
385
|
+
sp.Matrix(leading).extract(rows, cols).det(), parameters
|
|
386
|
+
)
|
|
387
|
+
)
|
|
388
|
+
# Regular-singular eigenvalue collisions and, for 2x2 residues, exact
|
|
389
|
+
# bounded integer-resonance hypersurfaces from the characteristic discriminant.
|
|
390
|
+
if pole == 1:
|
|
391
|
+
lam = sp.Symbol("_lambda")
|
|
392
|
+
char_poly = sp.Poly(sp.Matrix(leading).charpoly(lam).as_expr(), lam)
|
|
393
|
+
disc = sp.factor(sp.discriminant(char_poly.as_expr(), char_poly.gens[0]))
|
|
394
|
+
collision_loci.extend(_parameter_factors(disc, parameters))
|
|
395
|
+
block_loci.extend(_parameter_factors(disc, parameters))
|
|
396
|
+
if leading.rows == 2:
|
|
397
|
+
for n in range(1, max_resonance_order + 1):
|
|
398
|
+
resonance_loci.extend(_parameter_factors(disc - n * n, parameters))
|
|
399
|
+
# Optional formal exponential blocks expose algebraic degeneracy and phase
|
|
400
|
+
# alignment transitions. Complex coefficients must be represented with
|
|
401
|
+
# explicit real symbols for these expressions to be semialgebraic.
|
|
402
|
+
for qi, qj in combinations(exponential_parts, 2):
|
|
403
|
+
diff = sp.expand(qi - qj)
|
|
404
|
+
if point == sp.oo:
|
|
405
|
+
diff = sp.cancel(diff.subs(system.variable, 1 / local.variable))
|
|
406
|
+
else:
|
|
407
|
+
diff = sp.cancel(
|
|
408
|
+
diff.subs(system.variable, sp.sympify(point) + local.variable)
|
|
409
|
+
)
|
|
410
|
+
lead = _leading_monomial(diff, local.variable)
|
|
411
|
+
if lead is None:
|
|
412
|
+
continue
|
|
413
|
+
coefficient, _ = lead
|
|
414
|
+
re, im = sp.expand_complex(coefficient).as_real_imag()
|
|
415
|
+
stokes_loci.extend(_parameter_factors(sp.expand(re**2 + im**2), parameters))
|
|
416
|
+
# Phase transitions relative to the real axis occur when Im(c)=0 or Re(c)=0.
|
|
417
|
+
stokes_loci.extend(_parameter_factors(re, parameters))
|
|
418
|
+
stokes_loci.extend(_parameter_factors(im, parameters))
|
|
419
|
+
rank_loci = list(dict.fromkeys(rank_loci))
|
|
420
|
+
collision_loci = list(dict.fromkeys(collision_loci))
|
|
421
|
+
resonance_loci = list(dict.fromkeys(resonance_loci))
|
|
422
|
+
block_loci = list(dict.fromkeys(block_loci))
|
|
423
|
+
stokes_loci = list(dict.fromkeys(stokes_loci))
|
|
424
|
+
loci = list(
|
|
425
|
+
dict.fromkeys(
|
|
426
|
+
[*rank_loci, *collision_loci, *resonance_loci, *block_loci, *stokes_loci]
|
|
427
|
+
)
|
|
428
|
+
)
|
|
429
|
+
marker = sp.Ne(sp.prod(loci), 0, evaluate=False) if loci else sp.S.true
|
|
430
|
+
geometry = parametric_cad(
|
|
431
|
+
marker, (), parameters=parameters, assumptions=assumptions, output="result"
|
|
432
|
+
)
|
|
433
|
+
strata = tuple(
|
|
434
|
+
SystemParameterStratum(case.condition)
|
|
435
|
+
for case in geometry.cases
|
|
436
|
+
if is_satisfiable(sp.And(assumptions, case.condition), parameters)
|
|
437
|
+
)
|
|
438
|
+
return ParameterizedSystemAnalysis(
|
|
439
|
+
sp.sympify(point),
|
|
440
|
+
parameters,
|
|
441
|
+
tuple(loci),
|
|
442
|
+
tuple(rank_loci),
|
|
443
|
+
tuple(collision_loci),
|
|
444
|
+
tuple(resonance_loci),
|
|
445
|
+
tuple(block_loci),
|
|
446
|
+
tuple(stokes_loci),
|
|
447
|
+
strata,
|
|
448
|
+
geometry.status == "complete",
|
|
449
|
+
)
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
@dataclass(frozen=True)
|
|
453
|
+
class SystemFormalTypeSignature:
|
|
454
|
+
"""Discrete formal invariants used to compare parameter strata."""
|
|
455
|
+
|
|
456
|
+
singularity_kind: str
|
|
457
|
+
leading_rank: int | None
|
|
458
|
+
spectral_multiplicities: tuple[int, ...]
|
|
459
|
+
resonance_orders: tuple[int, ...]
|
|
460
|
+
ramification_index: int | None
|
|
461
|
+
block_dimensions: tuple[int, ...]
|
|
462
|
+
exponential_parts: tuple[sp.Expr, ...]
|
|
463
|
+
stokes_ray_counts: tuple[int, ...]
|
|
464
|
+
complete: bool
|
|
465
|
+
|
|
466
|
+
|
|
467
|
+
@dataclass(frozen=True)
|
|
468
|
+
class SystemFormalTypeStratum:
|
|
469
|
+
condition: sp.Expr
|
|
470
|
+
sample: tuple[tuple[sp.Symbol, sp.Expr], ...]
|
|
471
|
+
signature: SystemFormalTypeSignature
|
|
472
|
+
certified: bool
|
|
473
|
+
|
|
474
|
+
|
|
475
|
+
@dataclass(frozen=True)
|
|
476
|
+
class ParameterizedSystemFormalTypes:
|
|
477
|
+
"""Semialgebraic parameter cells annotated by verified formal signatures."""
|
|
478
|
+
|
|
479
|
+
base: ParameterizedSystemAnalysis
|
|
480
|
+
strata: tuple[SystemFormalTypeStratum, ...]
|
|
481
|
+
exhaustive: bool
|
|
482
|
+
|
|
483
|
+
|
|
484
|
+
def _formal_type_signature(
|
|
485
|
+
system: FirstOrderSystem,
|
|
486
|
+
point: sp.Expr,
|
|
487
|
+
*,
|
|
488
|
+
max_power: int,
|
|
489
|
+
max_depth: int,
|
|
490
|
+
max_cover_index: int | None,
|
|
491
|
+
max_resonance_order: int,
|
|
492
|
+
) -> SystemFormalTypeSignature:
|
|
493
|
+
singularity = analyze_system_singularity(system, point)
|
|
494
|
+
formal = formal_system_analysis(
|
|
495
|
+
system,
|
|
496
|
+
point,
|
|
497
|
+
max_power=max_power,
|
|
498
|
+
max_depth=max_depth,
|
|
499
|
+
max_cover_index=max_cover_index,
|
|
500
|
+
adaptive=True,
|
|
501
|
+
max_adaptive_depth=max(max_depth, 6),
|
|
502
|
+
max_adaptive_cover_index=max_cover_index if max_cover_index is not None else 12,
|
|
503
|
+
)
|
|
504
|
+
block_dimensions: tuple[int, ...] = ()
|
|
505
|
+
if formal.reduction.formal_stages:
|
|
506
|
+
block_dimensions = tuple(
|
|
507
|
+
block.rows for block in formal.reduction.final_diagonalization.blocks
|
|
508
|
+
)
|
|
509
|
+
stokes = system_stokes_geometry(formal)
|
|
510
|
+
return SystemFormalTypeSignature(
|
|
511
|
+
singularity.kind,
|
|
512
|
+
singularity.leading_rank,
|
|
513
|
+
tuple(sorted(sp.Matrix(singularity.residue).eigenvals().values()))
|
|
514
|
+
if singularity.residue is not None
|
|
515
|
+
else (),
|
|
516
|
+
tuple(
|
|
517
|
+
sorted(
|
|
518
|
+
abs(int(r.difference))
|
|
519
|
+
for r in singularity.resonances
|
|
520
|
+
if r.difference.is_Integer
|
|
521
|
+
and abs(int(r.difference)) <= max_resonance_order
|
|
522
|
+
)
|
|
523
|
+
),
|
|
524
|
+
formal.ramification_index if formal.certificate.verified else None,
|
|
525
|
+
block_dimensions,
|
|
526
|
+
tuple(formal.exponential_parts),
|
|
527
|
+
tuple(len(pair.equal_magnitude_rays) for pair in stokes.pairs),
|
|
528
|
+
formal.complete and formal.certificate.verified,
|
|
529
|
+
)
|
|
530
|
+
|
|
531
|
+
|
|
532
|
+
def system_formal_type_stratification(
|
|
533
|
+
system: FirstOrderSystem,
|
|
534
|
+
parameters: tuple[sp.Symbol, ...],
|
|
535
|
+
point: sp.Expr = 0,
|
|
536
|
+
*,
|
|
537
|
+
assumptions: sp.Expr | bool = True,
|
|
538
|
+
max_resonance_order: int = 4,
|
|
539
|
+
exponential_parts: tuple[sp.Expr, ...] = (),
|
|
540
|
+
max_power: int = 3,
|
|
541
|
+
max_depth: int = 2,
|
|
542
|
+
max_cover_index: int | None = None,
|
|
543
|
+
) -> ParameterizedSystemFormalTypes:
|
|
544
|
+
"""Annotate certified transition cells with exact representative formal types.
|
|
545
|
+
|
|
546
|
+
Certification is conservative: a cell is certified only when the semialgebraic
|
|
547
|
+
transition decomposition is exhaustive and the representative formal reduction
|
|
548
|
+
independently verifies and completes. The transition set covers leading-rank,
|
|
549
|
+
residue collision/resonance and supplied exponential/Stokes degeneracy loci.
|
|
550
|
+
"""
|
|
551
|
+
base = system_parameter_analysis(
|
|
552
|
+
system,
|
|
553
|
+
parameters,
|
|
554
|
+
point,
|
|
555
|
+
assumptions=assumptions,
|
|
556
|
+
max_resonance_order=max_resonance_order,
|
|
557
|
+
exponential_parts=exponential_parts,
|
|
558
|
+
)
|
|
559
|
+
# Recompute the CAD result to retain exact representative samples.
|
|
560
|
+
marker = (
|
|
561
|
+
sp.Ne(sp.prod(base.transition_polynomials), 0, evaluate=False)
|
|
562
|
+
if base.transition_polynomials
|
|
563
|
+
else sp.S.true
|
|
564
|
+
)
|
|
565
|
+
geometry = parametric_cad(
|
|
566
|
+
marker,
|
|
567
|
+
(),
|
|
568
|
+
parameters=parameters,
|
|
569
|
+
assumptions=sp.sympify(assumptions),
|
|
570
|
+
output="result",
|
|
571
|
+
)
|
|
572
|
+
strata: list[SystemFormalTypeStratum] = []
|
|
573
|
+
for case in geometry.cases:
|
|
574
|
+
sample = dict(case.sample)
|
|
575
|
+
specialized = FirstOrderSystem(
|
|
576
|
+
system.variable,
|
|
577
|
+
sp.ImmutableMatrix(sp.Matrix(system.matrix).subs(sample)),
|
|
578
|
+
sp.ImmutableMatrix(sp.Matrix(system.forcing).subs(sample))
|
|
579
|
+
if system.forcing is not None
|
|
580
|
+
else None,
|
|
581
|
+
system.ramification_index,
|
|
582
|
+
)
|
|
583
|
+
signature = _formal_type_signature(
|
|
584
|
+
specialized,
|
|
585
|
+
point,
|
|
586
|
+
max_power=max_power,
|
|
587
|
+
max_depth=max_depth,
|
|
588
|
+
max_cover_index=max_cover_index,
|
|
589
|
+
max_resonance_order=max_resonance_order,
|
|
590
|
+
)
|
|
591
|
+
# The initial transition vocabulary is complete enough to certify
|
|
592
|
+
# bounded discrete formal type for ordinary/regular-singular 2x2
|
|
593
|
+
# families. Irregular formal-block/ramification transitions require
|
|
594
|
+
# additional transition polynomials and remain representative-only.
|
|
595
|
+
cell_certified = (
|
|
596
|
+
base.exhaustive
|
|
597
|
+
and signature.complete
|
|
598
|
+
and system.dimension <= 2
|
|
599
|
+
and signature.singularity_kind in {"ordinary", "regular_singular"}
|
|
600
|
+
)
|
|
601
|
+
strata.append(
|
|
602
|
+
SystemFormalTypeStratum(
|
|
603
|
+
case.condition,
|
|
604
|
+
tuple(
|
|
605
|
+
sorted(
|
|
606
|
+
sample.items(), key=lambda item: sp.default_sort_key(item[0])
|
|
607
|
+
)
|
|
608
|
+
),
|
|
609
|
+
signature,
|
|
610
|
+
cell_certified,
|
|
611
|
+
)
|
|
612
|
+
)
|
|
613
|
+
return ParameterizedSystemFormalTypes(
|
|
614
|
+
base, tuple(strata), base.exhaustive and all(s.certified for s in strata)
|
|
615
|
+
)
|