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.
Files changed (49) hide show
  1. odeanalysis/__init__.py +296 -0
  2. odeanalysis/_api_policy.py +179 -0
  3. odeanalysis/_assumptions.py +89 -0
  4. odeanalysis/_block_common.py +106 -0
  5. odeanalysis/_formal_gauge.py +115 -0
  6. odeanalysis/_local.py +166 -0
  7. odeanalysis/_moser.py +271 -0
  8. odeanalysis/_power_simplify.py +32 -0
  9. odeanalysis/_spectral.py +215 -0
  10. odeanalysis/_symbolic_compare.py +16 -0
  11. odeanalysis/_symbolic_errors.py +19 -0
  12. odeanalysis/_zero.py +28 -0
  13. odeanalysis/analytic_continuation.py +343 -0
  14. odeanalysis/bell.py +87 -0
  15. odeanalysis/block_decomposition.py +1144 -0
  16. odeanalysis/canonical.py +471 -0
  17. odeanalysis/certified_continuation.py +160 -0
  18. odeanalysis/diagnostics.py +17 -0
  19. odeanalysis/dominance.py +134 -0
  20. odeanalysis/factorization.py +88 -0
  21. odeanalysis/formal.py +1028 -0
  22. odeanalysis/formal_basis.py +1009 -0
  23. odeanalysis/frobenius.py +349 -0
  24. odeanalysis/fuchsian.py +400 -0
  25. odeanalysis/interchange.py +604 -0
  26. odeanalysis/interoperability.py +143 -0
  27. odeanalysis/irregular.py +250 -0
  28. odeanalysis/kovacic.py +478 -0
  29. odeanalysis/levelt.py +679 -0
  30. odeanalysis/local_analysis.py +386 -0
  31. odeanalysis/local_structure.py +290 -0
  32. odeanalysis/matrix_series.py +357 -0
  33. odeanalysis/newton.py +501 -0
  34. odeanalysis/operator.py +193 -0
  35. odeanalysis/parameter_wkb.py +92 -0
  36. odeanalysis/py.typed +0 -0
  37. odeanalysis/series.py +199 -0
  38. odeanalysis/singularities.py +279 -0
  39. odeanalysis/stokes.py +782 -0
  40. odeanalysis/system.py +347 -0
  41. odeanalysis/system_analysis.py +615 -0
  42. odeanalysis/transition_loci.py +302 -0
  43. odeanalysis/turning.py +516 -0
  44. odeanalysis/wronskian.py +110 -0
  45. odeanalysis-0.1.0.dist-info/METADATA +180 -0
  46. odeanalysis-0.1.0.dist-info/RECORD +49 -0
  47. odeanalysis-0.1.0.dist-info/WHEEL +5 -0
  48. odeanalysis-0.1.0.dist-info/licenses/LICENSE +677 -0
  49. odeanalysis-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,92 @@
1
+ """Certified parameter strata for turning-point and uniform-WKB structure."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+
7
+ import sympy as sp
8
+ from semialg import parametric_cad
9
+
10
+ from .operator import LinearDifferentialOperator, _coerce_linear_operator
11
+ from .transition_loci import turning_loci
12
+ from .turning import analyze_turning_points
13
+
14
+
15
+ @dataclass(frozen=True)
16
+ class ParameterizedTurningStratum:
17
+ condition: sp.Expr
18
+ sample: tuple[tuple[sp.Symbol, sp.Expr], ...]
19
+ finite_multiplicities: tuple[int, ...]
20
+ turning_kinds: tuple[str, ...]
21
+ uniform_families: tuple[str | None, ...]
22
+ certified: bool
23
+
24
+
25
+ @dataclass(frozen=True)
26
+ class ParameterizedTurningAnalysis:
27
+ parameters: tuple[sp.Symbol, ...]
28
+ transition_polynomials: tuple[sp.Expr, ...]
29
+ strata: tuple[ParameterizedTurningStratum, ...]
30
+ exhaustive: bool
31
+
32
+
33
+ def parameterized_turning_analysis(
34
+ ode: sp.Expr | sp.Equality | LinearDifferentialOperator,
35
+ function: sp.FunctionClass | sp.Expr | None = None,
36
+ variable: sp.Symbol | None = None,
37
+ *,
38
+ parameters: tuple[sp.Symbol, ...],
39
+ assumptions: sp.Expr | bool = True,
40
+ ) -> ParameterizedTurningAnalysis:
41
+ """Stratify parameter space where finite turning multiplicities are constant.
42
+
43
+ The discriminant/degree-loss loci are computed symbolically; each CAD cell
44
+ is checked by exact specialization and turning-point analysis. This is the
45
+ parameter geometry needed to choose Airy versus degenerate Weber local
46
+ uniformization without crossing a confluence locus.
47
+ """
48
+ op = _coerce_linear_operator(ode, function, variable)
49
+ loci = tuple(dict.fromkeys(turning_loci(op)))
50
+ marker = sp.Ne(sp.prod(loci), 0, evaluate=False) if loci else sp.S.true
51
+ geometry = parametric_cad(
52
+ marker,
53
+ (),
54
+ parameters=parameters,
55
+ assumptions=sp.sympify(assumptions),
56
+ output="result",
57
+ )
58
+ strata: list[ParameterizedTurningStratum] = []
59
+ for case in geometry.cases:
60
+ sample = dict(case.sample)
61
+ specialized = LinearDifferentialOperator(
62
+ op.variable,
63
+ op.function,
64
+ tuple(sp.cancel(sp.sympify(c).subs(sample)) for c in op.coefficients),
65
+ sp.cancel(sp.sympify(op.inhomogeneous).subs(sample)),
66
+ )
67
+ analysis = analyze_turning_points(specialized)
68
+ finite = tuple(tp for tp in analysis.points if tp.point != sp.oo)
69
+ strata.append(
70
+ ParameterizedTurningStratum(
71
+ case.condition,
72
+ tuple(
73
+ sorted(
74
+ sample.items(), key=lambda item: sp.default_sort_key(item[0])
75
+ )
76
+ ),
77
+ tuple(tp.multiplicity for tp in finite),
78
+ tuple(tp.kind.value for tp in finite),
79
+ tuple(
80
+ "airy"
81
+ if tp.multiplicity == 1
82
+ else "weber"
83
+ if tp.multiplicity == 2
84
+ else None
85
+ for tp in finite
86
+ ),
87
+ geometry.status == "complete",
88
+ )
89
+ )
90
+ return ParameterizedTurningAnalysis(
91
+ parameters, loci, tuple(strata), geometry.status == "complete"
92
+ )
odeanalysis/py.typed ADDED
File without changes
odeanalysis/series.py ADDED
@@ -0,0 +1,199 @@
1
+ """Small sparse Laurent-series representation for formal local calculations.
2
+
3
+ Powers are integral in a chosen uniformizing parameter ``t``. A Puiseux
4
+ series in a local coordinate ``h`` is therefore represented after choosing
5
+ ``h = t**r``. The class provides only the operations needed by
6
+ formal ODE recurrences: addition, multiplication, truncation, and the ramified
7
+ local derivative ``D_h``.
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
+
17
+
18
+ @dataclass(frozen=True)
19
+ class SparseLaurentSeries:
20
+ """Finite sparse Laurent series in an integral-power uniformizer."""
21
+
22
+ variable: sp.Symbol
23
+ terms: tuple[tuple[int, sp.Expr], ...]
24
+
25
+ def __post_init__(self) -> None:
26
+ combined: dict[int, sp.Expr] = {}
27
+ for power, coefficient in self.terms:
28
+ if not isinstance(power, int):
29
+ raise TypeError("Laurent powers must be integers")
30
+ coefficient = sp.sympify(coefficient)
31
+ if coefficient == 0:
32
+ continue
33
+ combined[power] = sp.expand(combined.get(power, sp.S.Zero) + coefficient)
34
+ cleaned = tuple(
35
+ (power, coefficient)
36
+ for power, coefficient in sorted(combined.items())
37
+ if coefficient != 0
38
+ )
39
+ object.__setattr__(self, "terms", cleaned)
40
+
41
+ @classmethod
42
+ def zero(cls, variable: sp.Symbol) -> SparseLaurentSeries:
43
+ return cls(variable, ())
44
+
45
+ @classmethod
46
+ def one(cls, variable: sp.Symbol) -> SparseLaurentSeries:
47
+ return cls(variable, ((0, sp.S.One),))
48
+
49
+ @classmethod
50
+ def from_mapping(
51
+ cls,
52
+ variable: sp.Symbol,
53
+ coefficients: Mapping[int, sp.Expr],
54
+ ) -> SparseLaurentSeries:
55
+ return cls(variable, tuple(coefficients.items()))
56
+
57
+ @classmethod
58
+ def from_expr(
59
+ cls,
60
+ expression: sp.Expr,
61
+ variable: sp.Symbol,
62
+ ) -> SparseLaurentSeries:
63
+ """Create a finite Laurent series from an expanded Laurent polynomial."""
64
+
65
+ expression = sp.expand(sp.sympify(expression))
66
+ if expression == 0:
67
+ return cls.zero(variable)
68
+
69
+ terms: list[tuple[int, sp.Expr]] = []
70
+ for term in sp.Add.make_args(expression):
71
+ coefficient, power = term.as_coeff_exponent(variable)
72
+ power = sp.sympify(power)
73
+ if not power.is_Integer:
74
+ raise ValueError(
75
+ "expression is not a finite Laurent polynomial with integral powers"
76
+ )
77
+ # as_coeff_exponent leaves non-monomial dependence in the coefficient.
78
+ if coefficient.has(variable):
79
+ raise ValueError(
80
+ "expression is not a finite Laurent polynomial in the requested variable"
81
+ )
82
+ terms.append((int(power), coefficient))
83
+ return cls(variable, tuple(terms))
84
+
85
+ @property
86
+ def is_zero(self) -> bool:
87
+ return not self.terms
88
+
89
+ @property
90
+ def min_power(self) -> int | None:
91
+ return None if not self.terms else self.terms[0][0]
92
+
93
+ @property
94
+ def max_power(self) -> int | None:
95
+ return None if not self.terms else self.terms[-1][0]
96
+
97
+ def as_dict(self) -> dict[int, sp.Expr]:
98
+ return dict(self.terms)
99
+
100
+ def to_expr(self) -> sp.Expr:
101
+ return sp.Add(
102
+ *(coefficient * self.variable**power for power, coefficient in self.terms)
103
+ )
104
+
105
+ def truncate(
106
+ self,
107
+ *,
108
+ min_power: int | None = None,
109
+ max_power: int | None = None,
110
+ ) -> SparseLaurentSeries:
111
+ if min_power is not None and max_power is not None and min_power > max_power:
112
+ return self.zero(self.variable)
113
+ return SparseLaurentSeries(
114
+ self.variable,
115
+ tuple(
116
+ (power, coefficient)
117
+ for power, coefficient in self.terms
118
+ if (min_power is None or power >= min_power)
119
+ and (max_power is None or power <= max_power)
120
+ ),
121
+ )
122
+
123
+ def scale(self, scalar: sp.Expr) -> SparseLaurentSeries:
124
+ scalar = sp.sympify(scalar)
125
+ if scalar == 0:
126
+ return self.zero(self.variable)
127
+ return SparseLaurentSeries(
128
+ self.variable,
129
+ tuple((power, scalar * coefficient) for power, coefficient in self.terms),
130
+ )
131
+
132
+ def add(
133
+ self,
134
+ other: SparseLaurentSeries,
135
+ *,
136
+ min_power: int | None = None,
137
+ max_power: int | None = None,
138
+ ) -> SparseLaurentSeries:
139
+ self._check_variable(other)
140
+ coefficients = self.as_dict()
141
+ for power, coefficient in other.terms:
142
+ coefficients[power] = coefficients.get(power, sp.S.Zero) + coefficient
143
+ return SparseLaurentSeries.from_mapping(self.variable, coefficients).truncate(
144
+ min_power=min_power,
145
+ max_power=max_power,
146
+ )
147
+
148
+ def multiply(
149
+ self,
150
+ other: SparseLaurentSeries,
151
+ *,
152
+ min_power: int | None = None,
153
+ max_power: int | None = None,
154
+ ) -> SparseLaurentSeries:
155
+ self._check_variable(other)
156
+ coefficients: dict[int, sp.Expr] = {}
157
+ for left_power, left_coefficient in self.terms:
158
+ for right_power, right_coefficient in other.terms:
159
+ power = left_power + right_power
160
+ if min_power is not None and power < min_power:
161
+ continue
162
+ if max_power is not None and power > max_power:
163
+ continue
164
+ coefficients[power] = (
165
+ coefficients.get(power, sp.S.Zero)
166
+ + left_coefficient * right_coefficient
167
+ )
168
+ return SparseLaurentSeries.from_mapping(self.variable, coefficients)
169
+
170
+ def derivative(
171
+ self,
172
+ *,
173
+ ramification_index: int = 1,
174
+ min_power: int | None = None,
175
+ max_power: int | None = None,
176
+ ) -> SparseLaurentSeries:
177
+ r"""Differentiate with respect to ``h`` when ``h=t**r``.
178
+
179
+ ``D_h(c*t**p) = (p/r) c*t**(p-r)``.
180
+ """
181
+
182
+ if ramification_index < 1:
183
+ raise ValueError("ramification_index must be positive")
184
+ result = SparseLaurentSeries(
185
+ self.variable,
186
+ tuple(
187
+ (
188
+ power - ramification_index,
189
+ sp.Rational(power, ramification_index) * coefficient,
190
+ )
191
+ for power, coefficient in self.terms
192
+ if power != 0
193
+ ),
194
+ )
195
+ return result.truncate(min_power=min_power, max_power=max_power)
196
+
197
+ def _check_variable(self, other: SparseLaurentSeries) -> None:
198
+ if self.variable != other.variable:
199
+ raise ValueError("Laurent series use different uniformizing variables")
@@ -0,0 +1,279 @@
1
+ """Singularity analysis for scalar linear ordinary differential equations."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from enum import StrEnum
7
+
8
+ import sympy as sp
9
+ from funcprops import normalize_assumptions
10
+
11
+ from ._assumptions import assumption_substitutions
12
+ from ._local import linear_ode_data
13
+ from ._local import pole_order as _pole_order
14
+ from ._local import pole_order_upper_bound as _pole_order_upper_bound
15
+ from ._symbolic_errors import SYMBOLIC_FAILURES
16
+ from .operator import (
17
+ LinearDifferentialOperator,
18
+ _coerce_linear_operator,
19
+ as_ode_expression,
20
+ function_class,
21
+ )
22
+
23
+
24
+ class ODESingularityKind(StrEnum):
25
+ """Local classification of a scalar linear ODE."""
26
+
27
+ ORDINARY = "ordinary"
28
+ REGULAR = "regular_singular"
29
+ IRREGULAR = "irregular_singular"
30
+ UNKNOWN = "unknown"
31
+
32
+
33
+ @dataclass(frozen=True)
34
+ class ODESingularity:
35
+ """Local singularity data for a scalar linear ODE."""
36
+
37
+ point: sp.Expr
38
+ kind: ODESingularityKind
39
+ order: int
40
+ normalized_coefficients: tuple[sp.Expr, ...]
41
+ pole_orders: tuple[int | None, ...]
42
+ indicial_polynomial: sp.Expr | None = None
43
+ indicial_roots: tuple[sp.Expr, ...] = ()
44
+ transformed_equation: sp.Expr | None = None
45
+
46
+ @property
47
+ def regular(self) -> bool:
48
+ return self.kind in {ODESingularityKind.ORDINARY, ODESingularityKind.REGULAR}
49
+
50
+
51
+ @dataclass(frozen=True)
52
+ class ODESingularityAnalysis:
53
+ """Singularity analysis of a scalar linear ODE."""
54
+
55
+ equation: sp.Expr
56
+ function: sp.FunctionClass
57
+ variable: sp.Symbol
58
+ order: int
59
+ finite: tuple[ODESingularity, ...]
60
+ infinity: ODESingularity | None = None
61
+ operator: LinearDifferentialOperator | None = None
62
+
63
+ @property
64
+ def singular_points(self) -> tuple[sp.Expr, ...]:
65
+ points = tuple(
66
+ item.point
67
+ for item in self.finite
68
+ if item.kind is not ODESingularityKind.ORDINARY
69
+ )
70
+ if (
71
+ self.infinity is not None
72
+ and self.infinity.kind is not ODESingularityKind.ORDINARY
73
+ ):
74
+ points += (sp.oo,)
75
+ return points
76
+
77
+
78
+ def _finite_singular_candidates(
79
+ normalized: tuple[sp.Expr, ...], variable: sp.Symbol
80
+ ) -> tuple[sp.Expr, ...]:
81
+ points: set[sp.Expr] = set()
82
+ for coeff in normalized[:-1]:
83
+ try:
84
+ singular = sp.singularities(coeff, variable)
85
+ except (NotImplementedError, ValueError):
86
+ continue
87
+ if isinstance(singular, sp.FiniteSet):
88
+ points.update(singular)
89
+ return tuple(sorted(points, key=sp.default_sort_key))
90
+
91
+
92
+ def _indicial_data(
93
+ normalized: tuple[sp.Expr, ...],
94
+ order: int,
95
+ variable: sp.Symbol,
96
+ point: sp.Expr,
97
+ ) -> tuple[sp.Expr | None, tuple[sp.Expr, ...]]:
98
+ r = sp.Symbol("r")
99
+ poly = sp.ff(r, order)
100
+ for j in range(order):
101
+ scale = (variable - point) ** (order - j)
102
+ try:
103
+ b = sp.simplify(sp.limit(scale * normalized[j], variable, point))
104
+ except SYMBOLIC_FAILURES:
105
+ return None, ()
106
+ if b.has(sp.oo, -sp.oo, sp.zoo, sp.nan):
107
+ return None, ()
108
+ poly += b * sp.ff(r, j)
109
+ poly = sp.factor(sp.expand(poly))
110
+ roots: tuple[sp.Expr, ...] = ()
111
+ try:
112
+ root_dict = sp.roots(poly, r)
113
+ if root_dict:
114
+ roots = tuple(
115
+ root
116
+ for root, multiplicity in root_dict.items()
117
+ for _ in range(multiplicity)
118
+ )
119
+ except SYMBOLIC_FAILURES:
120
+ pass
121
+ return poly, roots
122
+
123
+
124
+ def classify_ode_point(
125
+ ode: sp.Expr | sp.Equality | LinearDifferentialOperator,
126
+ function: sp.FunctionClass | sp.Expr | None = None,
127
+ variable: sp.Symbol | None = None,
128
+ point: sp.Expr = 0,
129
+ *,
130
+ assumptions: sp.Expr | bool = True,
131
+ ) -> ODESingularity:
132
+ """Classify one finite point of a scalar linear ODE or operator."""
133
+
134
+ data = linear_ode_data(ode, function, variable)
135
+ operator = data.operator
136
+ normalized = data.normalized_coefficients
137
+ x = operator.variable
138
+ order = operator.order
139
+ point = sp.sympify(point)
140
+ assumptions = normalize_assumptions(assumptions)
141
+ substitutions = assumption_substitutions(assumptions)
142
+ if substitutions:
143
+ normalized = tuple(sp.cancel(c.subs(substitutions)) for c in normalized)
144
+ pole_orders = (
145
+ *tuple(
146
+ _pole_order(
147
+ normalized[j],
148
+ x,
149
+ point,
150
+ assumptions=sp.S.true
151
+ if (_pole_order_upper_bound(normalized[j], x, point) or 0) <= order - j
152
+ else assumptions,
153
+ )
154
+ for j in range(order)
155
+ ),
156
+ 0,
157
+ )
158
+ known = pole_orders[:-1]
159
+ if all(value == 0 for value in known):
160
+ kind = ODESingularityKind.ORDINARY
161
+ elif all(
162
+ (value is not None and value <= order - j)
163
+ or (
164
+ value is None
165
+ and (_pole_order_upper_bound(normalized[j], x, point) or 0) <= order - j
166
+ )
167
+ for j, value in enumerate(known)
168
+ ):
169
+ kind = ODESingularityKind.REGULAR
170
+ elif any(value is None for value in known):
171
+ kind = ODESingularityKind.UNKNOWN
172
+ else:
173
+ kind = ODESingularityKind.IRREGULAR
174
+
175
+ indicial = None
176
+ roots: tuple[sp.Expr, ...] = ()
177
+ if kind is ODESingularityKind.REGULAR:
178
+ indicial, roots = _indicial_data(normalized, order, x, point)
179
+ return ODESingularity(
180
+ point=point,
181
+ kind=kind,
182
+ order=order,
183
+ normalized_coefficients=normalized,
184
+ pole_orders=pole_orders,
185
+ indicial_polynomial=indicial,
186
+ indicial_roots=roots,
187
+ )
188
+
189
+
190
+ def change_ode_variable_reciprocal(
191
+ ode: sp.Expr | sp.Equality | LinearDifferentialOperator,
192
+ function: sp.FunctionClass | sp.Expr | None = None,
193
+ variable: sp.Symbol | None = None,
194
+ new_function: sp.FunctionClass | sp.Expr | None = None,
195
+ new_variable: sp.Symbol | None = None,
196
+ ) -> sp.Expr:
197
+ """Transform an ODE under ``variable = 1/new_variable``."""
198
+
199
+ operator = _coerce_linear_operator(ode, function, variable)
200
+ if new_function is None or new_variable is None:
201
+ raise TypeError("new_function and new_variable are required")
202
+ transformed = operator.reciprocal_transform(new_function, new_variable)
203
+ return sp.factor(sp.together(transformed.expression))
204
+
205
+
206
+ def analyze_ode_singularities(
207
+ ode: sp.Expr | sp.Equality | LinearDifferentialOperator,
208
+ function: sp.FunctionClass | sp.Expr | None = None,
209
+ variable: sp.Symbol | None = None,
210
+ *,
211
+ include_infinity: bool = True,
212
+ assumptions: sp.Expr | bool = True,
213
+ ) -> ODESingularityAnalysis:
214
+ """Find and classify singular points of a scalar linear ODE."""
215
+
216
+ operator = _coerce_linear_operator(ode, function, variable)
217
+ assumptions = normalize_assumptions(assumptions)
218
+ normalized = operator.normalized().coefficients
219
+ finite = tuple(
220
+ classify_ode_point(operator, point=p, assumptions=assumptions)
221
+ for p in _finite_singular_candidates(normalized, operator.variable)
222
+ )
223
+
224
+ infinity: ODESingularity | None = None
225
+ if include_infinity:
226
+ t = sp.Dummy("t", positive=True)
227
+ u = sp.Function("_u")
228
+ transformed_operator = operator.reciprocal_transform(u, t)
229
+ local = classify_ode_point(
230
+ transformed_operator, point=sp.S.Zero, assumptions=assumptions
231
+ )
232
+ infinity = ODESingularity(
233
+ point=sp.oo,
234
+ kind=local.kind,
235
+ order=local.order,
236
+ normalized_coefficients=local.normalized_coefficients,
237
+ pole_orders=local.pole_orders,
238
+ indicial_polynomial=local.indicial_polynomial,
239
+ indicial_roots=local.indicial_roots,
240
+ transformed_equation=transformed_operator.expression,
241
+ )
242
+
243
+ return ODESingularityAnalysis(
244
+ equation=operator.expression,
245
+ function=operator.function,
246
+ variable=operator.variable,
247
+ order=operator.order,
248
+ finite=finite,
249
+ infinity=infinity,
250
+ operator=operator,
251
+ )
252
+
253
+
254
+ def ode_singular_points(
255
+ ode: sp.Expr | sp.Equality | LinearDifferentialOperator,
256
+ function: sp.FunctionClass | sp.Expr | None = None,
257
+ variable: sp.Symbol | None = None,
258
+ *,
259
+ include_infinity: bool = True,
260
+ assumptions: sp.Expr | bool = True,
261
+ ) -> tuple[tuple[sp.Expr, ODESingularityKind], ...]:
262
+ """Compact ``(point, classification)`` view of singularity analysis."""
263
+
264
+ analysis = analyze_ode_singularities(
265
+ ode,
266
+ function,
267
+ variable,
268
+ include_infinity=include_infinity,
269
+ assumptions=assumptions,
270
+ )
271
+ result = tuple((item.point, item.kind) for item in analysis.finite)
272
+ if analysis.infinity is not None:
273
+ result += ((sp.oo, analysis.infinity.kind),)
274
+ return result
275
+
276
+
277
+ # Backward-compatible aliases for helpers that were private in 0.1.0.
278
+ _as_expression = as_ode_expression
279
+ _function_class = function_class