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,357 @@
|
|
|
1
|
+
"""Sparse matrix Laurent series for formal system calculations.
|
|
2
|
+
|
|
3
|
+
The scalar formal layer uses :class:`~odeanalysis.series.SparseLaurentSeries`.
|
|
4
|
+
This module provides the matrix analogue needed for formal gauge
|
|
5
|
+
transformations and block reduction. Powers are integral in a uniformizing
|
|
6
|
+
parameter ``t``; Puiseux series in a local coordinate ``h`` are represented on
|
|
7
|
+
``h = t**r``.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from collections.abc import Mapping
|
|
13
|
+
from dataclasses import dataclass
|
|
14
|
+
|
|
15
|
+
import sympy as sp
|
|
16
|
+
from sympy.matrices.exceptions import NonInvertibleMatrixError
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass(frozen=True)
|
|
20
|
+
class MatrixLaurentSeries:
|
|
21
|
+
"""Finite sparse Laurent series with matrix coefficients.
|
|
22
|
+
|
|
23
|
+
``terms`` contains pairs ``(power, coefficient_matrix)``. All coefficient
|
|
24
|
+
matrices have the declared shape. Formal inversion is supported for
|
|
25
|
+
square series whose lowest-power coefficient is invertible; because an
|
|
26
|
+
inverse is generally infinite, :meth:`inverse` requires a highest output
|
|
27
|
+
power.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
variable: sp.Symbol
|
|
31
|
+
rows: int
|
|
32
|
+
cols: int
|
|
33
|
+
terms: tuple[tuple[int, sp.ImmutableMatrix], ...]
|
|
34
|
+
|
|
35
|
+
def __post_init__(self) -> None:
|
|
36
|
+
if self.rows < 0 or self.cols < 0:
|
|
37
|
+
raise ValueError("matrix dimensions must be nonnegative")
|
|
38
|
+
combined: dict[int, sp.Matrix] = {}
|
|
39
|
+
for power, coefficient in self.terms:
|
|
40
|
+
if not isinstance(power, int):
|
|
41
|
+
raise TypeError("Laurent powers must be integers")
|
|
42
|
+
matrix = sp.Matrix(coefficient)
|
|
43
|
+
if matrix.shape != (self.rows, self.cols):
|
|
44
|
+
raise ValueError(
|
|
45
|
+
"all Laurent coefficient matrices must have the declared shape"
|
|
46
|
+
)
|
|
47
|
+
if matrix.is_zero_matrix:
|
|
48
|
+
continue
|
|
49
|
+
if power in combined:
|
|
50
|
+
combined[power] = combined[power] + matrix
|
|
51
|
+
else:
|
|
52
|
+
combined[power] = matrix
|
|
53
|
+
cleaned: list[tuple[int, sp.ImmutableMatrix]] = []
|
|
54
|
+
for power, coefficient in sorted(combined.items()):
|
|
55
|
+
coefficient = coefficient.applyfunc(sp.expand)
|
|
56
|
+
if not coefficient.is_zero_matrix:
|
|
57
|
+
cleaned.append((power, sp.ImmutableMatrix(coefficient)))
|
|
58
|
+
object.__setattr__(self, "terms", tuple(cleaned))
|
|
59
|
+
|
|
60
|
+
@classmethod
|
|
61
|
+
def zero(
|
|
62
|
+
cls,
|
|
63
|
+
variable: sp.Symbol,
|
|
64
|
+
rows: int,
|
|
65
|
+
cols: int | None = None,
|
|
66
|
+
) -> MatrixLaurentSeries:
|
|
67
|
+
if cols is None:
|
|
68
|
+
cols = rows
|
|
69
|
+
return cls(variable, rows, cols, ())
|
|
70
|
+
|
|
71
|
+
@classmethod
|
|
72
|
+
def identity(cls, variable: sp.Symbol, size: int) -> MatrixLaurentSeries:
|
|
73
|
+
return cls(variable, size, size, ((0, sp.ImmutableMatrix(sp.eye(size))),))
|
|
74
|
+
|
|
75
|
+
@classmethod
|
|
76
|
+
def from_mapping(
|
|
77
|
+
cls,
|
|
78
|
+
variable: sp.Symbol,
|
|
79
|
+
coefficients: Mapping[int, sp.MatrixBase],
|
|
80
|
+
*,
|
|
81
|
+
shape: tuple[int, int] | None = None,
|
|
82
|
+
) -> MatrixLaurentSeries:
|
|
83
|
+
items = tuple(coefficients.items())
|
|
84
|
+
if shape is None:
|
|
85
|
+
if not items:
|
|
86
|
+
raise ValueError("shape is required for an empty matrix Laurent series")
|
|
87
|
+
first = sp.Matrix(items[0][1])
|
|
88
|
+
shape = first.shape
|
|
89
|
+
return cls(
|
|
90
|
+
variable,
|
|
91
|
+
int(shape[0]),
|
|
92
|
+
int(shape[1]),
|
|
93
|
+
tuple((power, sp.ImmutableMatrix(matrix)) for power, matrix in items),
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
@classmethod
|
|
97
|
+
def from_matrix(
|
|
98
|
+
cls,
|
|
99
|
+
matrix: sp.MatrixBase,
|
|
100
|
+
variable: sp.Symbol,
|
|
101
|
+
) -> MatrixLaurentSeries:
|
|
102
|
+
"""Create a finite Laurent series from a matrix of Laurent polynomials."""
|
|
103
|
+
|
|
104
|
+
matrix = sp.Matrix(matrix)
|
|
105
|
+
by_power: dict[int, sp.MutableDenseMatrix] = {}
|
|
106
|
+
for i in range(matrix.rows):
|
|
107
|
+
for j in range(matrix.cols):
|
|
108
|
+
expression = sp.expand(matrix[i, j])
|
|
109
|
+
if expression == 0:
|
|
110
|
+
continue
|
|
111
|
+
for term in sp.Add.make_args(expression):
|
|
112
|
+
coefficient, power = term.as_coeff_exponent(variable)
|
|
113
|
+
power = sp.sympify(power)
|
|
114
|
+
if not power.is_Integer or coefficient.has(variable):
|
|
115
|
+
raise ValueError(
|
|
116
|
+
"matrix entries must be finite Laurent polynomials with integral powers"
|
|
117
|
+
)
|
|
118
|
+
integer_power = int(power)
|
|
119
|
+
target = by_power.setdefault(
|
|
120
|
+
integer_power,
|
|
121
|
+
sp.zeros(matrix.rows, matrix.cols),
|
|
122
|
+
)
|
|
123
|
+
target[i, j] += coefficient
|
|
124
|
+
return cls.from_mapping(
|
|
125
|
+
variable,
|
|
126
|
+
by_power,
|
|
127
|
+
shape=(matrix.rows, matrix.cols),
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
@property
|
|
131
|
+
def shape(self) -> tuple[int, int]:
|
|
132
|
+
return self.rows, self.cols
|
|
133
|
+
|
|
134
|
+
@property
|
|
135
|
+
def is_zero(self) -> bool:
|
|
136
|
+
return not self.terms
|
|
137
|
+
|
|
138
|
+
@property
|
|
139
|
+
def min_power(self) -> int | None:
|
|
140
|
+
return None if not self.terms else self.terms[0][0]
|
|
141
|
+
|
|
142
|
+
@property
|
|
143
|
+
def max_power(self) -> int | None:
|
|
144
|
+
return None if not self.terms else self.terms[-1][0]
|
|
145
|
+
|
|
146
|
+
def as_dict(self) -> dict[int, sp.ImmutableMatrix]:
|
|
147
|
+
return dict(self.terms)
|
|
148
|
+
|
|
149
|
+
def coefficient(self, power: int) -> sp.ImmutableMatrix:
|
|
150
|
+
if not isinstance(power, int):
|
|
151
|
+
raise TypeError("power must be an integer")
|
|
152
|
+
return self.as_dict().get(
|
|
153
|
+
power,
|
|
154
|
+
sp.ImmutableMatrix(sp.zeros(self.rows, self.cols)),
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
def to_matrix(self) -> sp.ImmutableMatrix:
|
|
158
|
+
result = sp.zeros(self.rows, self.cols)
|
|
159
|
+
for power, coefficient in self.terms:
|
|
160
|
+
result += sp.Matrix(coefficient) * self.variable**power
|
|
161
|
+
return sp.ImmutableMatrix(result.applyfunc(sp.expand))
|
|
162
|
+
|
|
163
|
+
def truncate(
|
|
164
|
+
self,
|
|
165
|
+
*,
|
|
166
|
+
min_power: int | None = None,
|
|
167
|
+
max_power: int | None = None,
|
|
168
|
+
) -> MatrixLaurentSeries:
|
|
169
|
+
if min_power is not None and max_power is not None and min_power > max_power:
|
|
170
|
+
return self.zero(self.variable, self.rows, self.cols)
|
|
171
|
+
return MatrixLaurentSeries(
|
|
172
|
+
self.variable,
|
|
173
|
+
self.rows,
|
|
174
|
+
self.cols,
|
|
175
|
+
tuple(
|
|
176
|
+
(power, coefficient)
|
|
177
|
+
for power, coefficient in self.terms
|
|
178
|
+
if (min_power is None or power >= min_power)
|
|
179
|
+
and (max_power is None or power <= max_power)
|
|
180
|
+
),
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
def scale(self, scalar: sp.Expr) -> MatrixLaurentSeries:
|
|
184
|
+
scalar = sp.sympify(scalar)
|
|
185
|
+
if scalar == 0:
|
|
186
|
+
return self.zero(self.variable, self.rows, self.cols)
|
|
187
|
+
return MatrixLaurentSeries(
|
|
188
|
+
self.variable,
|
|
189
|
+
self.rows,
|
|
190
|
+
self.cols,
|
|
191
|
+
tuple(
|
|
192
|
+
(power, sp.ImmutableMatrix(sp.Matrix(coefficient) * scalar))
|
|
193
|
+
for power, coefficient in self.terms
|
|
194
|
+
),
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
def shift(self, power: int) -> MatrixLaurentSeries:
|
|
198
|
+
"""Multiply the series by ``variable**power`` without expanding."""
|
|
199
|
+
|
|
200
|
+
if not isinstance(power, int):
|
|
201
|
+
raise TypeError("power must be an integer")
|
|
202
|
+
return MatrixLaurentSeries(
|
|
203
|
+
self.variable,
|
|
204
|
+
self.rows,
|
|
205
|
+
self.cols,
|
|
206
|
+
tuple((p + power, coefficient) for p, coefficient in self.terms),
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
def add(
|
|
210
|
+
self,
|
|
211
|
+
other: MatrixLaurentSeries,
|
|
212
|
+
*,
|
|
213
|
+
min_power: int | None = None,
|
|
214
|
+
max_power: int | None = None,
|
|
215
|
+
) -> MatrixLaurentSeries:
|
|
216
|
+
self._check_same_variable(other)
|
|
217
|
+
if self.shape != other.shape:
|
|
218
|
+
raise ValueError(
|
|
219
|
+
"matrix Laurent series have incompatible shapes for addition"
|
|
220
|
+
)
|
|
221
|
+
coefficients: dict[int, sp.Matrix] = {
|
|
222
|
+
power: sp.Matrix(coefficient) for power, coefficient in self.terms
|
|
223
|
+
}
|
|
224
|
+
for power, coefficient in other.terms:
|
|
225
|
+
coefficients[power] = coefficients.get(
|
|
226
|
+
power, sp.zeros(self.rows, self.cols)
|
|
227
|
+
) + sp.Matrix(coefficient)
|
|
228
|
+
return MatrixLaurentSeries.from_mapping(
|
|
229
|
+
self.variable,
|
|
230
|
+
coefficients,
|
|
231
|
+
shape=self.shape,
|
|
232
|
+
).truncate(min_power=min_power, max_power=max_power)
|
|
233
|
+
|
|
234
|
+
def multiply(
|
|
235
|
+
self,
|
|
236
|
+
other: MatrixLaurentSeries,
|
|
237
|
+
*,
|
|
238
|
+
min_power: int | None = None,
|
|
239
|
+
max_power: int | None = None,
|
|
240
|
+
) -> MatrixLaurentSeries:
|
|
241
|
+
self._check_same_variable(other)
|
|
242
|
+
if self.cols != other.rows:
|
|
243
|
+
raise ValueError(
|
|
244
|
+
"matrix Laurent series have incompatible shapes for multiplication"
|
|
245
|
+
)
|
|
246
|
+
coefficients: dict[int, sp.Matrix] = {}
|
|
247
|
+
for left_power, left_coefficient in self.terms:
|
|
248
|
+
for right_power, right_coefficient in other.terms:
|
|
249
|
+
power = left_power + right_power
|
|
250
|
+
if min_power is not None and power < min_power:
|
|
251
|
+
continue
|
|
252
|
+
if max_power is not None and power > max_power:
|
|
253
|
+
continue
|
|
254
|
+
product = sp.Matrix(left_coefficient) * sp.Matrix(right_coefficient)
|
|
255
|
+
coefficients[power] = (
|
|
256
|
+
coefficients.get(power, sp.zeros(self.rows, other.cols)) + product
|
|
257
|
+
)
|
|
258
|
+
return MatrixLaurentSeries.from_mapping(
|
|
259
|
+
self.variable,
|
|
260
|
+
coefficients,
|
|
261
|
+
shape=(self.rows, other.cols),
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
def derivative(
|
|
265
|
+
self,
|
|
266
|
+
*,
|
|
267
|
+
ramification_index: int = 1,
|
|
268
|
+
min_power: int | None = None,
|
|
269
|
+
max_power: int | None = None,
|
|
270
|
+
) -> MatrixLaurentSeries:
|
|
271
|
+
r"""Differentiate with respect to ``h`` when ``h=t**r``.
|
|
272
|
+
|
|
273
|
+
For a matrix coefficient ``A_p``,
|
|
274
|
+
``D_h(A_p*t**p) = (p/r) A_p*t**(p-r)``.
|
|
275
|
+
"""
|
|
276
|
+
|
|
277
|
+
if ramification_index < 1:
|
|
278
|
+
raise ValueError("ramification_index must be positive")
|
|
279
|
+
result = MatrixLaurentSeries(
|
|
280
|
+
self.variable,
|
|
281
|
+
self.rows,
|
|
282
|
+
self.cols,
|
|
283
|
+
tuple(
|
|
284
|
+
(
|
|
285
|
+
power - ramification_index,
|
|
286
|
+
sp.ImmutableMatrix(
|
|
287
|
+
sp.Matrix(coefficient) * sp.Rational(power, ramification_index)
|
|
288
|
+
),
|
|
289
|
+
)
|
|
290
|
+
for power, coefficient in self.terms
|
|
291
|
+
if power != 0
|
|
292
|
+
),
|
|
293
|
+
)
|
|
294
|
+
return result.truncate(min_power=min_power, max_power=max_power)
|
|
295
|
+
|
|
296
|
+
def inverse(
|
|
297
|
+
self,
|
|
298
|
+
*,
|
|
299
|
+
max_power: int,
|
|
300
|
+
min_power: int | None = None,
|
|
301
|
+
) -> MatrixLaurentSeries:
|
|
302
|
+
"""Return a truncated formal inverse through ``max_power``.
|
|
303
|
+
|
|
304
|
+
If ``A(t)=t**p(A0 + A1*t + ...)`` with invertible ``A0``, the inverse
|
|
305
|
+
is generated recursively from ``A(t) B(t)=I``. The leading inverse
|
|
306
|
+
power is ``-p``. A :class:`ValueError` is raised when the leading
|
|
307
|
+
coefficient is singular or the series is zero/non-square.
|
|
308
|
+
"""
|
|
309
|
+
|
|
310
|
+
if self.rows != self.cols:
|
|
311
|
+
raise ValueError("formal inversion requires a square matrix series")
|
|
312
|
+
if self.is_zero:
|
|
313
|
+
raise ValueError("the zero matrix series is not invertible")
|
|
314
|
+
lead_power = self.min_power
|
|
315
|
+
if lead_power is None:
|
|
316
|
+
raise RuntimeError("nonzero Laurent series has no leading power")
|
|
317
|
+
inverse_lead_power = -lead_power
|
|
318
|
+
if max_power < inverse_lead_power:
|
|
319
|
+
return self.zero(self.variable, self.rows, self.cols)
|
|
320
|
+
|
|
321
|
+
coefficients = {
|
|
322
|
+
power - lead_power: sp.Matrix(matrix) for power, matrix in self.terms
|
|
323
|
+
}
|
|
324
|
+
a0 = coefficients[0]
|
|
325
|
+
try:
|
|
326
|
+
b0 = a0.inv()
|
|
327
|
+
except (ValueError, ZeroDivisionError, NonInvertibleMatrixError) as exc:
|
|
328
|
+
raise ValueError("leading matrix coefficient is not invertible") from exc
|
|
329
|
+
if sp.simplify(a0.det()) == 0:
|
|
330
|
+
raise ValueError("leading matrix coefficient is not invertible")
|
|
331
|
+
|
|
332
|
+
relative_limit = max_power - inverse_lead_power
|
|
333
|
+
inverse_coefficients: dict[int, sp.Matrix] = {0: b0}
|
|
334
|
+
zero = sp.zeros(self.rows, self.cols)
|
|
335
|
+
for n in range(1, relative_limit + 1):
|
|
336
|
+
convolution = zero.copy()
|
|
337
|
+
for k in range(1, n + 1):
|
|
338
|
+
ak = coefficients.get(k)
|
|
339
|
+
if ak is not None:
|
|
340
|
+
convolution += ak * inverse_coefficients[n - k]
|
|
341
|
+
inverse_coefficients[n] = -b0 * convolution
|
|
342
|
+
|
|
343
|
+
result = MatrixLaurentSeries.from_mapping(
|
|
344
|
+
self.variable,
|
|
345
|
+
{
|
|
346
|
+
inverse_lead_power + relative_power: coefficient
|
|
347
|
+
for relative_power, coefficient in inverse_coefficients.items()
|
|
348
|
+
},
|
|
349
|
+
shape=self.shape,
|
|
350
|
+
)
|
|
351
|
+
return result.truncate(min_power=min_power, max_power=max_power)
|
|
352
|
+
|
|
353
|
+
def _check_same_variable(self, other: MatrixLaurentSeries) -> None:
|
|
354
|
+
if self.variable != other.variable:
|
|
355
|
+
raise ValueError(
|
|
356
|
+
"matrix Laurent series use different uniformizing variables"
|
|
357
|
+
)
|