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/kovacic.py
ADDED
|
@@ -0,0 +1,478 @@
|
|
|
1
|
+
"""Kovacic Liouvillian analysis for rational second-order equations."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from enum import Enum
|
|
7
|
+
from itertools import product
|
|
8
|
+
from math import factorial
|
|
9
|
+
|
|
10
|
+
import sympy as sp
|
|
11
|
+
|
|
12
|
+
from .factorization import FirstOrderFactorization, factor_differential_operator
|
|
13
|
+
from .operator import LinearDifferentialOperator, _coerce_linear_operator
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class KovacicOutcome(Enum):
|
|
17
|
+
"""Outcome of the Kovacic decision procedure."""
|
|
18
|
+
|
|
19
|
+
CASE_1 = "case_1"
|
|
20
|
+
CASE_2 = "case_2"
|
|
21
|
+
CASE_3 = "case_3"
|
|
22
|
+
NO_LIOUVILLIAN_SOLUTION = "no_liouvillian_solution"
|
|
23
|
+
UNDECIDED = "undecided"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass(frozen=True)
|
|
27
|
+
class KovacicCase2Certificate:
|
|
28
|
+
"""Polynomial and Riccati evidence for Kovacic Case 2."""
|
|
29
|
+
|
|
30
|
+
degree: int
|
|
31
|
+
theta: sp.Expr
|
|
32
|
+
polynomial: sp.Expr
|
|
33
|
+
logarithmic_derivatives: tuple[sp.Expr, ...]
|
|
34
|
+
|
|
35
|
+
def verify(self, r: sp.Expr, x: sp.Symbol) -> bool:
|
|
36
|
+
"""Replay the Case-2 auxiliary equation and Riccati identities."""
|
|
37
|
+
|
|
38
|
+
p = self.polynomial
|
|
39
|
+
theta = self.theta
|
|
40
|
+
auxiliary = (
|
|
41
|
+
sp.diff(p, x, 3)
|
|
42
|
+
+ 3 * theta * sp.diff(p, x, 2)
|
|
43
|
+
+ (3 * theta**2 + 3 * sp.diff(theta, x) - 4 * r) * sp.diff(p, x)
|
|
44
|
+
+ (
|
|
45
|
+
sp.diff(theta, x, 2)
|
|
46
|
+
+ 3 * theta * sp.diff(theta, x)
|
|
47
|
+
+ theta**3
|
|
48
|
+
- 4 * r * theta
|
|
49
|
+
- 2 * sp.diff(r, x)
|
|
50
|
+
)
|
|
51
|
+
* p
|
|
52
|
+
)
|
|
53
|
+
if sp.simplify(auxiliary) != 0:
|
|
54
|
+
return False
|
|
55
|
+
return bool(self.logarithmic_derivatives) and all(
|
|
56
|
+
sp.simplify(sp.diff(w, x) + w**2 - r) == 0
|
|
57
|
+
for w in self.logarithmic_derivatives
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@dataclass(frozen=True)
|
|
62
|
+
class KovacicCase3Certificate:
|
|
63
|
+
"""Polynomial recurrence evidence for Kovacic Case 3."""
|
|
64
|
+
|
|
65
|
+
n: int
|
|
66
|
+
degree: int
|
|
67
|
+
theta: sp.Expr
|
|
68
|
+
pole_polynomial: sp.Expr
|
|
69
|
+
polynomial: sp.Expr
|
|
70
|
+
recurrence: tuple[sp.Expr, ...]
|
|
71
|
+
algebraic_log_derivative: sp.Expr
|
|
72
|
+
|
|
73
|
+
def verify(self, r: sp.Expr, x: sp.Symbol) -> bool:
|
|
74
|
+
"""Replay the Case-3 recurrence and terminal polynomial identity."""
|
|
75
|
+
|
|
76
|
+
n = self.n
|
|
77
|
+
s = self.pole_polynomial
|
|
78
|
+
theta = self.theta
|
|
79
|
+
# Stored order is P_n, P_{n-1}, ..., P_-1.
|
|
80
|
+
values = self.recurrence
|
|
81
|
+
if len(values) != n + 2 or sp.simplify(values[0] + self.polynomial) != 0:
|
|
82
|
+
return False
|
|
83
|
+
p_by_index = {n - offset: value for offset, value in enumerate(values)}
|
|
84
|
+
for i in range(n, -1, -1):
|
|
85
|
+
pi = p_by_index[i]
|
|
86
|
+
pnext = p_by_index.get(i + 1, sp.S.Zero)
|
|
87
|
+
expected = (
|
|
88
|
+
-s * sp.diff(pi, x)
|
|
89
|
+
+ ((n - i) * sp.diff(s, x) - s * theta) * pi
|
|
90
|
+
- (n - i) * (i + 1) * s**2 * r * pnext
|
|
91
|
+
)
|
|
92
|
+
if sp.simplify(p_by_index[i - 1] - expected) != 0:
|
|
93
|
+
return False
|
|
94
|
+
if sp.simplify(p_by_index[-1]) != 0:
|
|
95
|
+
return False
|
|
96
|
+
omega = sp.Symbol("omega")
|
|
97
|
+
expected_poly = sp.expand(
|
|
98
|
+
sum(
|
|
99
|
+
s**i * p_by_index[i] * omega**i / factorial(n - i) for i in range(n + 1)
|
|
100
|
+
)
|
|
101
|
+
)
|
|
102
|
+
return sp.simplify(expected_poly - self.algebraic_log_derivative) == 0
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
@dataclass(frozen=True)
|
|
106
|
+
class KovacicAnalysis:
|
|
107
|
+
"""Certificate-oriented result of Kovacic's algorithm."""
|
|
108
|
+
|
|
109
|
+
operator: LinearDifferentialOperator
|
|
110
|
+
normal_form_potential: sp.Expr
|
|
111
|
+
outcome: KovacicOutcome
|
|
112
|
+
case: int | None
|
|
113
|
+
factorizations: tuple[FirstOrderFactorization, ...]
|
|
114
|
+
finite_pole_orders: tuple[tuple[sp.Expr, int], ...]
|
|
115
|
+
infinity_order: int
|
|
116
|
+
reason: str
|
|
117
|
+
case2_certificates: tuple[KovacicCase2Certificate, ...] = ()
|
|
118
|
+
case3_certificates: tuple[KovacicCase3Certificate, ...] = ()
|
|
119
|
+
|
|
120
|
+
@property
|
|
121
|
+
def is_liouvillian(self) -> bool | None:
|
|
122
|
+
"""Return the exact three-valued Liouvillian decision."""
|
|
123
|
+
|
|
124
|
+
if self.outcome in {
|
|
125
|
+
KovacicOutcome.CASE_1,
|
|
126
|
+
KovacicOutcome.CASE_2,
|
|
127
|
+
KovacicOutcome.CASE_3,
|
|
128
|
+
}:
|
|
129
|
+
return True
|
|
130
|
+
if self.outcome is KovacicOutcome.NO_LIOUVILLIAN_SOLUTION:
|
|
131
|
+
return False
|
|
132
|
+
return None
|
|
133
|
+
|
|
134
|
+
def verify(self) -> bool:
|
|
135
|
+
"""Replay the stored Kovacic certificate."""
|
|
136
|
+
|
|
137
|
+
r = _normal_form_potential(self.operator)
|
|
138
|
+
if sp.simplify(self.normal_form_potential - r) != 0:
|
|
139
|
+
return False
|
|
140
|
+
if self.outcome is KovacicOutcome.CASE_1:
|
|
141
|
+
return bool(self.factorizations) and all(
|
|
142
|
+
f.verify() for f in self.factorizations
|
|
143
|
+
)
|
|
144
|
+
if self.outcome is KovacicOutcome.CASE_2:
|
|
145
|
+
return bool(self.case2_certificates) and all(
|
|
146
|
+
certificate.verify(r, self.operator.variable)
|
|
147
|
+
for certificate in self.case2_certificates
|
|
148
|
+
)
|
|
149
|
+
if self.outcome is KovacicOutcome.CASE_3:
|
|
150
|
+
return bool(self.case3_certificates) and all(
|
|
151
|
+
certificate.verify(r, self.operator.variable)
|
|
152
|
+
for certificate in self.case3_certificates
|
|
153
|
+
)
|
|
154
|
+
return True
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def _normal_form_potential(op: LinearDifferentialOperator) -> sp.Expr:
|
|
158
|
+
normalized = op.normalized()
|
|
159
|
+
x = op.variable
|
|
160
|
+
p = normalized.coefficients[1]
|
|
161
|
+
q = normalized.coefficients[0]
|
|
162
|
+
return sp.cancel(p**2 / 4 + sp.diff(p, x) / 2 - q)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _pole_data(
|
|
166
|
+
r: sp.Expr, x: sp.Symbol
|
|
167
|
+
) -> tuple[tuple[tuple[sp.Expr, int], ...], int, sp.Expr]:
|
|
168
|
+
numerator, denominator = sp.cancel(r).as_numer_denom()
|
|
169
|
+
try:
|
|
170
|
+
den_poly = sp.Poly(denominator, x, extension=True)
|
|
171
|
+
num_poly = sp.Poly(numerator, x, extension=True)
|
|
172
|
+
except sp.PolynomialError:
|
|
173
|
+
return (), 0, sp.S.Zero
|
|
174
|
+
roots = sp.roots(den_poly.as_expr(), x)
|
|
175
|
+
finite = tuple(
|
|
176
|
+
sorted(
|
|
177
|
+
((root, int(mult)) for root, mult in roots.items()),
|
|
178
|
+
key=lambda item: sp.default_sort_key(item[0]),
|
|
179
|
+
)
|
|
180
|
+
)
|
|
181
|
+
infinity_order = int(den_poly.degree() - num_poly.degree())
|
|
182
|
+
infinity_b = (
|
|
183
|
+
sp.cancel(num_poly.LC() / den_poly.LC()) if infinity_order == 2 else sp.S.Zero
|
|
184
|
+
)
|
|
185
|
+
return finite, infinity_order, infinity_b
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def _integer_value(value: sp.Expr) -> int | None:
|
|
189
|
+
simplified = sp.simplify(value)
|
|
190
|
+
if simplified.is_integer is True and simplified.is_nonnegative is True:
|
|
191
|
+
return int(simplified)
|
|
192
|
+
return None
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def _integer_set(values: list[sp.Expr]) -> tuple[sp.Expr, ...]:
|
|
196
|
+
result = []
|
|
197
|
+
for value in values:
|
|
198
|
+
simplified = sp.simplify(value)
|
|
199
|
+
if simplified.is_integer is True and simplified not in result:
|
|
200
|
+
result.append(simplified)
|
|
201
|
+
return tuple(result)
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def _pole_b(r: sp.Expr, x: sp.Symbol, pole: sp.Expr) -> sp.Expr:
|
|
205
|
+
return sp.simplify(sp.limit((x - pole) ** 2 * r, x, pole))
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def _monic_auxiliary_solution(
|
|
209
|
+
expr_builder, degree: int, x: sp.Symbol
|
|
210
|
+
) -> sp.Expr | None:
|
|
211
|
+
if degree == 0:
|
|
212
|
+
return sp.S.One if sp.simplify(expr_builder(sp.S.One)) == 0 else None
|
|
213
|
+
coeffs = sp.symbols(f"_k0:{degree}")
|
|
214
|
+
polynomial = x**degree + sum(coeffs[i] * x**i for i in range(degree))
|
|
215
|
+
expression = sp.cancel(expr_builder(polynomial))
|
|
216
|
+
numerator = sp.together(expression).as_numer_denom()[0]
|
|
217
|
+
try:
|
|
218
|
+
equations = sp.Poly(sp.expand(numerator), x).all_coeffs()
|
|
219
|
+
except sp.PolynomialError:
|
|
220
|
+
return None
|
|
221
|
+
solution_set = sp.linsolve(equations, coeffs)
|
|
222
|
+
if solution_set is sp.EmptySet:
|
|
223
|
+
return None
|
|
224
|
+
for solution in solution_set:
|
|
225
|
+
if any(value.free_symbols & set(coeffs) for value in solution):
|
|
226
|
+
continue
|
|
227
|
+
candidate = sp.expand(polynomial.subs(dict(zip(coeffs, solution, strict=True))))
|
|
228
|
+
if sp.simplify(expr_builder(candidate)) == 0:
|
|
229
|
+
return candidate
|
|
230
|
+
return None
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def _case2(
|
|
234
|
+
r: sp.Expr,
|
|
235
|
+
x: sp.Symbol,
|
|
236
|
+
finite: tuple[tuple[sp.Expr, int], ...],
|
|
237
|
+
infinity_order: int,
|
|
238
|
+
infinity_b: sp.Expr,
|
|
239
|
+
) -> tuple[KovacicCase2Certificate, ...]:
|
|
240
|
+
choices: list[tuple[sp.Expr, ...]] = []
|
|
241
|
+
poles: list[sp.Expr] = []
|
|
242
|
+
for pole, order in finite:
|
|
243
|
+
poles.append(pole)
|
|
244
|
+
if order == 1:
|
|
245
|
+
choices.append((sp.Integer(4),))
|
|
246
|
+
elif order == 2:
|
|
247
|
+
root = sp.sqrt(1 + 4 * _pole_b(r, x, pole))
|
|
248
|
+
choices.append(_integer_set([2, 2 + 2 * root, 2 - 2 * root]))
|
|
249
|
+
else:
|
|
250
|
+
choices.append((sp.Integer(order),))
|
|
251
|
+
if any(not values for values in choices):
|
|
252
|
+
return ()
|
|
253
|
+
if infinity_order > 2:
|
|
254
|
+
infinity_choices = (sp.Integer(0), sp.Integer(2), sp.Integer(4))
|
|
255
|
+
elif infinity_order == 2:
|
|
256
|
+
root = sp.sqrt(1 + 4 * infinity_b)
|
|
257
|
+
infinity_choices = _integer_set([2, 2 + 2 * root, 2 - 2 * root])
|
|
258
|
+
else:
|
|
259
|
+
infinity_choices = (sp.Integer(infinity_order),)
|
|
260
|
+
certificates = []
|
|
261
|
+
for finite_values in product(*choices):
|
|
262
|
+
for e_inf in infinity_choices:
|
|
263
|
+
degree = _integer_value((e_inf - sum(finite_values)) / 2)
|
|
264
|
+
if degree is None:
|
|
265
|
+
continue
|
|
266
|
+
theta = sp.cancel(
|
|
267
|
+
sp.Rational(1, 2)
|
|
268
|
+
* sum(
|
|
269
|
+
e / (x - pole) for e, pole in zip(finite_values, poles, strict=True)
|
|
270
|
+
)
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
def auxiliary(p, theta=theta):
|
|
274
|
+
return (
|
|
275
|
+
sp.diff(p, x, 3)
|
|
276
|
+
+ 3 * theta * sp.diff(p, x, 2)
|
|
277
|
+
+ (3 * theta**2 + 3 * sp.diff(theta, x) - 4 * r) * sp.diff(p, x)
|
|
278
|
+
+ (
|
|
279
|
+
sp.diff(theta, x, 2)
|
|
280
|
+
+ 3 * theta * sp.diff(theta, x)
|
|
281
|
+
+ theta**3
|
|
282
|
+
- 4 * r * theta
|
|
283
|
+
- 2 * sp.diff(r, x)
|
|
284
|
+
)
|
|
285
|
+
* p
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
p = _monic_auxiliary_solution(auxiliary, degree, x)
|
|
289
|
+
if p is None:
|
|
290
|
+
continue
|
|
291
|
+
phi = sp.cancel(theta + sp.diff(p, x) / p)
|
|
292
|
+
constant = sp.cancel(sp.diff(phi, x) / 2 + phi**2 / 2 - r)
|
|
293
|
+
discriminant = sp.cancel(phi**2 - 4 * constant)
|
|
294
|
+
roots = tuple(
|
|
295
|
+
sp.simplify((phi + sign * sp.sqrt(discriminant)) / 2)
|
|
296
|
+
for sign in (1, -1)
|
|
297
|
+
)
|
|
298
|
+
valid = tuple(
|
|
299
|
+
w for w in roots if sp.simplify(sp.diff(w, x) + w**2 - r) == 0
|
|
300
|
+
)
|
|
301
|
+
if valid:
|
|
302
|
+
certificates.append(KovacicCase2Certificate(degree, theta, p, valid))
|
|
303
|
+
return tuple(certificates)
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def _case3_sets(
|
|
307
|
+
n: int,
|
|
308
|
+
r: sp.Expr,
|
|
309
|
+
x: sp.Symbol,
|
|
310
|
+
finite: tuple[tuple[sp.Expr, int], ...],
|
|
311
|
+
infinity_order: int,
|
|
312
|
+
infinity_b: sp.Expr,
|
|
313
|
+
):
|
|
314
|
+
if any(order not in {1, 2} for _, order in finite) or infinity_order < 2:
|
|
315
|
+
return None
|
|
316
|
+
choices = []
|
|
317
|
+
poles = []
|
|
318
|
+
for pole, order in finite:
|
|
319
|
+
poles.append(pole)
|
|
320
|
+
if order == 1:
|
|
321
|
+
choices.append((sp.Integer(12),))
|
|
322
|
+
else:
|
|
323
|
+
root = sp.sqrt(1 + 4 * _pole_b(r, x, pole))
|
|
324
|
+
values = [
|
|
325
|
+
6 + sp.Rational(12 * k, n) * root for k in range(-n // 2, n // 2 + 1)
|
|
326
|
+
]
|
|
327
|
+
choices.append(_integer_set(values))
|
|
328
|
+
root_inf = sp.sqrt(1 + 4 * infinity_b)
|
|
329
|
+
infinity_choices = _integer_set(
|
|
330
|
+
[6 + sp.Rational(12 * k, n) * root_inf for k in range(-n // 2, n // 2 + 1)]
|
|
331
|
+
)
|
|
332
|
+
if any(not values for values in choices) or not infinity_choices:
|
|
333
|
+
return None
|
|
334
|
+
return poles, choices, infinity_choices
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
def _case3(
|
|
338
|
+
r: sp.Expr,
|
|
339
|
+
x: sp.Symbol,
|
|
340
|
+
finite: tuple[tuple[sp.Expr, int], ...],
|
|
341
|
+
infinity_order: int,
|
|
342
|
+
infinity_b: sp.Expr,
|
|
343
|
+
) -> tuple[KovacicCase3Certificate, ...]:
|
|
344
|
+
certificates = []
|
|
345
|
+
for n in (4, 6, 12):
|
|
346
|
+
sets = _case3_sets(n, r, x, finite, infinity_order, infinity_b)
|
|
347
|
+
if sets is None:
|
|
348
|
+
continue
|
|
349
|
+
poles, choices, infinity_choices = sets
|
|
350
|
+
s = sp.prod(x - pole for pole in poles)
|
|
351
|
+
for finite_values in product(*choices):
|
|
352
|
+
for e_inf in infinity_choices:
|
|
353
|
+
degree = _integer_value(
|
|
354
|
+
sp.Rational(n, 12) * (e_inf - sum(finite_values))
|
|
355
|
+
)
|
|
356
|
+
if degree is None:
|
|
357
|
+
continue
|
|
358
|
+
theta = sp.cancel(
|
|
359
|
+
sp.Rational(n, 12)
|
|
360
|
+
* sum(
|
|
361
|
+
e / (x - pole)
|
|
362
|
+
for e, pole in zip(finite_values, poles, strict=True)
|
|
363
|
+
)
|
|
364
|
+
)
|
|
365
|
+
|
|
366
|
+
def terminal(polynomial, n=n, s=s, theta=theta):
|
|
367
|
+
by_index = {n + 1: sp.S.Zero, n: -polynomial}
|
|
368
|
+
for i in range(n, -1, -1):
|
|
369
|
+
by_index[i - 1] = sp.cancel(
|
|
370
|
+
-s * sp.diff(by_index[i], x)
|
|
371
|
+
+ ((n - i) * sp.diff(s, x) - s * theta) * by_index[i]
|
|
372
|
+
- (n - i) * (i + 1) * s**2 * r * by_index[i + 1]
|
|
373
|
+
)
|
|
374
|
+
return by_index[-1]
|
|
375
|
+
|
|
376
|
+
polynomial = _monic_auxiliary_solution(terminal, degree, x)
|
|
377
|
+
if polynomial is None:
|
|
378
|
+
continue
|
|
379
|
+
by_index = {n + 1: sp.S.Zero, n: -polynomial}
|
|
380
|
+
for i in range(n, -1, -1):
|
|
381
|
+
by_index[i - 1] = sp.cancel(
|
|
382
|
+
-s * sp.diff(by_index[i], x)
|
|
383
|
+
+ ((n - i) * sp.diff(s, x) - s * theta) * by_index[i]
|
|
384
|
+
- (n - i) * (i + 1) * s**2 * r * by_index[i + 1]
|
|
385
|
+
)
|
|
386
|
+
if sp.simplify(by_index[-1]) != 0:
|
|
387
|
+
continue
|
|
388
|
+
omega = sp.Symbol("omega")
|
|
389
|
+
equation = sp.expand(
|
|
390
|
+
sum(
|
|
391
|
+
s**i * by_index[i] * omega**i / factorial(n - i)
|
|
392
|
+
for i in range(n + 1)
|
|
393
|
+
)
|
|
394
|
+
)
|
|
395
|
+
certificate = KovacicCase3Certificate(
|
|
396
|
+
n=n,
|
|
397
|
+
degree=degree,
|
|
398
|
+
theta=theta,
|
|
399
|
+
pole_polynomial=sp.expand(s),
|
|
400
|
+
polynomial=polynomial,
|
|
401
|
+
recurrence=tuple(by_index[i] for i in range(n, -2, -1)),
|
|
402
|
+
algebraic_log_derivative=equation,
|
|
403
|
+
)
|
|
404
|
+
if certificate.verify(r, x):
|
|
405
|
+
certificates.append(certificate)
|
|
406
|
+
if certificates:
|
|
407
|
+
return tuple(certificates)
|
|
408
|
+
return ()
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
def kovacic_analysis(
|
|
412
|
+
ode: sp.Expr | sp.Equality | LinearDifferentialOperator,
|
|
413
|
+
function: sp.FunctionClass | sp.Expr | None = None,
|
|
414
|
+
variable: sp.Symbol | None = None,
|
|
415
|
+
) -> KovacicAnalysis:
|
|
416
|
+
"""Run Kovacic's Cases 1--3 for a rational homogeneous second-order ODE."""
|
|
417
|
+
|
|
418
|
+
op = _coerce_linear_operator(ode, function, variable)
|
|
419
|
+
if not op.is_homogeneous or op.order != 2:
|
|
420
|
+
raise ValueError(
|
|
421
|
+
"Kovacic analysis requires a homogeneous second-order operator"
|
|
422
|
+
)
|
|
423
|
+
normalized = op.normalized()
|
|
424
|
+
x = normalized.variable
|
|
425
|
+
if not all(
|
|
426
|
+
sp.cancel(c).is_rational_function(x) for c in normalized.coefficients[:-1]
|
|
427
|
+
):
|
|
428
|
+
raise ValueError("Kovacic analysis requires rational-function coefficients")
|
|
429
|
+
potential = _normal_form_potential(op)
|
|
430
|
+
finite, infinity_order, infinity_b = _pole_data(potential, x)
|
|
431
|
+
factors = factor_differential_operator(op)
|
|
432
|
+
if factors:
|
|
433
|
+
return KovacicAnalysis(
|
|
434
|
+
op,
|
|
435
|
+
potential,
|
|
436
|
+
KovacicOutcome.CASE_1,
|
|
437
|
+
1,
|
|
438
|
+
factors,
|
|
439
|
+
finite,
|
|
440
|
+
infinity_order,
|
|
441
|
+
"a rational Riccati solution certifies Kovacic Case 1",
|
|
442
|
+
)
|
|
443
|
+
case2 = _case2(potential, x, finite, infinity_order, infinity_b)
|
|
444
|
+
if case2:
|
|
445
|
+
return KovacicAnalysis(
|
|
446
|
+
op,
|
|
447
|
+
potential,
|
|
448
|
+
KovacicOutcome.CASE_2,
|
|
449
|
+
2,
|
|
450
|
+
(),
|
|
451
|
+
finite,
|
|
452
|
+
infinity_order,
|
|
453
|
+
"a quadratic algebraic Riccati certificate proves Kovacic Case 2",
|
|
454
|
+
case2_certificates=case2,
|
|
455
|
+
)
|
|
456
|
+
case3 = _case3(potential, x, finite, infinity_order, infinity_b)
|
|
457
|
+
if case3:
|
|
458
|
+
return KovacicAnalysis(
|
|
459
|
+
op,
|
|
460
|
+
potential,
|
|
461
|
+
KovacicOutcome.CASE_3,
|
|
462
|
+
3,
|
|
463
|
+
(),
|
|
464
|
+
finite,
|
|
465
|
+
infinity_order,
|
|
466
|
+
"the finite-group polynomial recurrence certifies Kovacic Case 3",
|
|
467
|
+
case3_certificates=case3,
|
|
468
|
+
)
|
|
469
|
+
return KovacicAnalysis(
|
|
470
|
+
op,
|
|
471
|
+
potential,
|
|
472
|
+
KovacicOutcome.NO_LIOUVILLIAN_SOLUTION,
|
|
473
|
+
None,
|
|
474
|
+
(),
|
|
475
|
+
finite,
|
|
476
|
+
infinity_order,
|
|
477
|
+
"Kovacic Cases 1, 2, and 3 all failed",
|
|
478
|
+
)
|