pysolverkit 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.
- pysolverkit/__init__.py +70 -0
- pysolverkit/enums.py +91 -0
- pysolverkit/functions/__init__.py +17 -0
- pysolverkit/functions/base.py +445 -0
- pysolverkit/functions/elementary.py +192 -0
- pysolverkit/functions/fem2d.py +128 -0
- pysolverkit/functions/multivariate.py +57 -0
- pysolverkit/linalg/__init__.py +5 -0
- pysolverkit/linalg/linear_system.py +134 -0
- pysolverkit/linalg/matrix.py +42 -0
- pysolverkit/linalg/vector.py +46 -0
- pysolverkit/ode/__init__.py +12 -0
- pysolverkit/ode/base.py +6 -0
- pysolverkit/ode/first_order.py +297 -0
- pysolverkit/ode/second_order.py +323 -0
- pysolverkit/util.py +62 -0
- pysolverkit-0.1.0.dist-info/METADATA +58 -0
- pysolverkit-0.1.0.dist-info/RECORD +20 -0
- pysolverkit-0.1.0.dist-info/WHEEL +5 -0
- pysolverkit-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from fractions import Fraction
|
|
4
|
+
|
|
5
|
+
from ..enums import ODEMethod
|
|
6
|
+
from ..functions.base import Function
|
|
7
|
+
from ..functions.multivariate import BivariateFunction, MultiVariableFunction
|
|
8
|
+
from ..functions.elementary import Polynomial
|
|
9
|
+
from ..linalg.vector import Vector
|
|
10
|
+
from .base import LinearODE
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class FirstOrderLinearODE(LinearODE):
|
|
14
|
+
"""Numerical solver for a first-order linear IVP :math:`y'(x) = f(x, y(x))`.
|
|
15
|
+
|
|
16
|
+
Parameters
|
|
17
|
+
----------
|
|
18
|
+
f:
|
|
19
|
+
Right-hand side :math:`f(x, y)`, given as a :class:`BivariateFunction`
|
|
20
|
+
(or a :class:`~pysolverkit.linalg.Vector` of
|
|
21
|
+
:class:`~pysolverkit.functions.multivariate.MultiVariableFunction` objects
|
|
22
|
+
for systems of equations).
|
|
23
|
+
a, b:
|
|
24
|
+
Integration interval.
|
|
25
|
+
y0:
|
|
26
|
+
Initial condition :math:`y(a)`.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
def __init__(
|
|
30
|
+
self,
|
|
31
|
+
f: BivariateFunction | Vector,
|
|
32
|
+
a: float,
|
|
33
|
+
b: float,
|
|
34
|
+
y0: float | Vector,
|
|
35
|
+
) -> None:
|
|
36
|
+
self.f = f
|
|
37
|
+
self.a = a
|
|
38
|
+
self.b = b
|
|
39
|
+
self.y0 = y0
|
|
40
|
+
|
|
41
|
+
def solve(
|
|
42
|
+
self,
|
|
43
|
+
h: float = 0.1,
|
|
44
|
+
method: ODEMethod = ODEMethod.EULER,
|
|
45
|
+
n: int = 1,
|
|
46
|
+
step: int = 2,
|
|
47
|
+
points: list[float] | None = None,
|
|
48
|
+
) -> Polynomial:
|
|
49
|
+
"""Solve the IVP and return an interpolating :class:`Polynomial`.
|
|
50
|
+
|
|
51
|
+
Parameters
|
|
52
|
+
----------
|
|
53
|
+
h:
|
|
54
|
+
Step size.
|
|
55
|
+
method:
|
|
56
|
+
Solver algorithm (:class:`ODEMethod`).
|
|
57
|
+
n:
|
|
58
|
+
Order parameter for Taylor / Runge-Kutta methods.
|
|
59
|
+
step:
|
|
60
|
+
Number of steps for Adams methods.
|
|
61
|
+
points:
|
|
62
|
+
Known function values at previous grid points for multistep methods.
|
|
63
|
+
"""
|
|
64
|
+
if points is None:
|
|
65
|
+
points = []
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
if method is ODEMethod.EULER:
|
|
69
|
+
return self._solve_taylor(h, 1)
|
|
70
|
+
if method is ODEMethod.RUNGE_KUTTA:
|
|
71
|
+
return self._solve_runge_kutta(h, n)
|
|
72
|
+
if method is ODEMethod.TAYLOR:
|
|
73
|
+
return self._solve_taylor(h, n)
|
|
74
|
+
if method is ODEMethod.TRAPEZOIDAL:
|
|
75
|
+
return self._solve_trapezoidal(h)
|
|
76
|
+
if method is ODEMethod.ADAMS_BASHFORTH:
|
|
77
|
+
return self._solve_adams_bashforth(h, step, points)
|
|
78
|
+
if method is ODEMethod.ADAMS_MOULTON:
|
|
79
|
+
return self._solve_adams_moulton(h, step, points)
|
|
80
|
+
if method is ODEMethod.PREDICTOR_CORRECTOR:
|
|
81
|
+
return self._solve_predictor_corrector(h)
|
|
82
|
+
|
|
83
|
+
raise ValueError(f"Unknown ODE method: {method!r}")
|
|
84
|
+
|
|
85
|
+
# ------------------------------------------------------------------
|
|
86
|
+
# Private solvers
|
|
87
|
+
# ------------------------------------------------------------------
|
|
88
|
+
|
|
89
|
+
def _solve_runge_kutta(self, h: float, n: int) -> Polynomial:
|
|
90
|
+
w = [self.y0]
|
|
91
|
+
N = int((self.b - self.a) / h)
|
|
92
|
+
|
|
93
|
+
if n == 1:
|
|
94
|
+
return self.solve(h, method=ODEMethod.EULER)
|
|
95
|
+
elif n == 2:
|
|
96
|
+
for i in range(N):
|
|
97
|
+
xi = self.a + i * h
|
|
98
|
+
w.append(
|
|
99
|
+
w[i]
|
|
100
|
+
+ (h / 2) * self.f(xi, w[i])
|
|
101
|
+
+ (h / 2) * self.f(xi + h, w[i] + h * self.f(xi, w[i]))
|
|
102
|
+
)
|
|
103
|
+
elif n == 3:
|
|
104
|
+
for i in range(N):
|
|
105
|
+
xi = self.a + i * h
|
|
106
|
+
k1 = self.f(xi, w[i])
|
|
107
|
+
k2 = self.f(xi + (h / 3), w[i] + (h / 3) * k1)
|
|
108
|
+
k3 = self.f(xi + (2 / 3) * h, w[i] + (2 / 3) * h * k2)
|
|
109
|
+
w.append(w[i] + (h / 4) * (k1 + 3 * k3))
|
|
110
|
+
elif n == 4:
|
|
111
|
+
for i in range(N):
|
|
112
|
+
xi = self.a + i * h
|
|
113
|
+
k1 = h * self.f(xi, w[i])
|
|
114
|
+
k2 = h * self.f(xi + h / 2, w[i] + 0.5 * k1)
|
|
115
|
+
k3 = h * self.f(xi + h / 2, w[i] + 0.5 * k2)
|
|
116
|
+
k4 = h * self.f(xi + h, w[i] + k3)
|
|
117
|
+
w.append(w[i] + (1 / 6) * (k1 + 2 * k2 + 2 * k3 + k4))
|
|
118
|
+
else:
|
|
119
|
+
raise NotImplementedError("Runge-Kutta is only implemented for orders 1–4.")
|
|
120
|
+
|
|
121
|
+
try:
|
|
122
|
+
return Polynomial.interpolate([(self.a + i * h, w[i]) for i in range(N + 1)])
|
|
123
|
+
except Exception:
|
|
124
|
+
return w # fall back to list for vector-valued problems
|
|
125
|
+
|
|
126
|
+
def _solve_taylor(self, h: float, n: int) -> Polynomial:
|
|
127
|
+
w = [self.y0]
|
|
128
|
+
N = int((self.b - self.a) / h)
|
|
129
|
+
|
|
130
|
+
if n == 1:
|
|
131
|
+
for i in range(N):
|
|
132
|
+
xi = self.a + i * h
|
|
133
|
+
w.append(w[i] + h * self.f(xi, w[i]))
|
|
134
|
+
elif n == 2:
|
|
135
|
+
for i in range(N):
|
|
136
|
+
xi = self.a + i * h
|
|
137
|
+
g = (
|
|
138
|
+
self.f(None, w[i]).differentiate()(xi)
|
|
139
|
+
+ self.f(xi, w[i]) * self.f(xi, None).differentiate()(w[i])
|
|
140
|
+
)
|
|
141
|
+
w.append(w[i] + h * self.f(xi, w[i]) + (h**2) * g / 2)
|
|
142
|
+
else:
|
|
143
|
+
raise NotImplementedError("Taylor method is only implemented for orders 1 and 2.")
|
|
144
|
+
|
|
145
|
+
try:
|
|
146
|
+
return Polynomial.interpolate([(self.a + i * h, w[i]) for i in range(N + 1)])
|
|
147
|
+
except Exception:
|
|
148
|
+
return w
|
|
149
|
+
|
|
150
|
+
def _solve_trapezoidal(self, h: float) -> Polynomial:
|
|
151
|
+
w = [self.y0]
|
|
152
|
+
N = int((self.b - self.a) / h)
|
|
153
|
+
|
|
154
|
+
for i in range(N):
|
|
155
|
+
xi = self.a + i * h
|
|
156
|
+
g = Function(
|
|
157
|
+
lambda x, _h=h, _xi=xi, _wi=w[i]: _wi
|
|
158
|
+
+ (_h / 2) * (self.f(_xi, _wi) + self.f(_xi + _h, x))
|
|
159
|
+
)
|
|
160
|
+
w.append(g.fixed_point(w[i]))
|
|
161
|
+
|
|
162
|
+
try:
|
|
163
|
+
return Polynomial.interpolate([(self.a + i * h, w[i]) for i in range(N + 1)])
|
|
164
|
+
except Exception:
|
|
165
|
+
return w
|
|
166
|
+
|
|
167
|
+
@staticmethod
|
|
168
|
+
def _adams_bashforth_coefficients(k: int) -> list[Fraction]:
|
|
169
|
+
"""Compute Adams-Bashforth *k*-step coefficients via Lagrange interpolation.
|
|
170
|
+
|
|
171
|
+
Returns ``[b_0, ..., b_{k-1}]`` (as :class:`~fractions.Fraction` objects) such
|
|
172
|
+
that :math:`w_{n+1} = w_n + h \\sum_{j=0}^{k-1} b_j f(t_{n-j}, w_{n-j})`.
|
|
173
|
+
"""
|
|
174
|
+
nodes = [Fraction(-j) for j in range(k)]
|
|
175
|
+
coefficients = []
|
|
176
|
+
for m in range(k):
|
|
177
|
+
poly = [Fraction(1)]
|
|
178
|
+
for j in range(k):
|
|
179
|
+
if j != m:
|
|
180
|
+
new_poly = [Fraction(0)] * (len(poly) + 1)
|
|
181
|
+
for idx, c in enumerate(poly):
|
|
182
|
+
new_poly[idx + 1] += c
|
|
183
|
+
new_poly[idx] -= nodes[j] * c
|
|
184
|
+
poly = new_poly
|
|
185
|
+
denom = Fraction(1)
|
|
186
|
+
for j in range(k):
|
|
187
|
+
if j != m:
|
|
188
|
+
denom *= nodes[m] - nodes[j]
|
|
189
|
+
integral = sum(c / (i + 1) for i, c in enumerate(poly))
|
|
190
|
+
coefficients.append(integral / denom)
|
|
191
|
+
return coefficients
|
|
192
|
+
|
|
193
|
+
@staticmethod
|
|
194
|
+
def _adams_moulton_coefficients(k: int) -> list[Fraction]:
|
|
195
|
+
"""Compute Adams-Moulton *k*-step coefficients via Lagrange interpolation.
|
|
196
|
+
|
|
197
|
+
Returns ``[b_0, ..., b_k]`` (as :class:`~fractions.Fraction` objects) such that
|
|
198
|
+
:math:`w_{n+1} = w_n + h(b_0 f(t_{n+1}, w_{n+1}) + \\sum_{j=1}^{k} b_j f(t_{n+1-j}, w_{n+1-j}))`.
|
|
199
|
+
"""
|
|
200
|
+
nodes = [Fraction(1 - j) for j in range(k + 1)]
|
|
201
|
+
coefficients = []
|
|
202
|
+
for m in range(k + 1):
|
|
203
|
+
poly = [Fraction(1)]
|
|
204
|
+
for j in range(k + 1):
|
|
205
|
+
if j != m:
|
|
206
|
+
new_poly = [Fraction(0)] * (len(poly) + 1)
|
|
207
|
+
for idx, c in enumerate(poly):
|
|
208
|
+
new_poly[idx + 1] += c
|
|
209
|
+
new_poly[idx] -= nodes[j] * c
|
|
210
|
+
poly = new_poly
|
|
211
|
+
denom = Fraction(1)
|
|
212
|
+
for j in range(k + 1):
|
|
213
|
+
if j != m:
|
|
214
|
+
denom *= nodes[m] - nodes[j]
|
|
215
|
+
integral = sum(c / (i + 1) for i, c in enumerate(poly))
|
|
216
|
+
coefficients.append(integral / denom)
|
|
217
|
+
return coefficients
|
|
218
|
+
|
|
219
|
+
def _solve_adams_bashforth(
|
|
220
|
+
self, h: float, step: int, points: list[float]
|
|
221
|
+
) -> Polynomial:
|
|
222
|
+
if step < 2:
|
|
223
|
+
raise ValueError("Adams-Bashforth requires at least 2 steps.")
|
|
224
|
+
w = [self.y0] + list(points)
|
|
225
|
+
N = int((self.b - self.a) / h)
|
|
226
|
+
coeffs = [float(c) for c in FirstOrderLinearODE._adams_bashforth_coefficients(step)]
|
|
227
|
+
|
|
228
|
+
for i in range(step - 1, N):
|
|
229
|
+
xi = self.a + i * h
|
|
230
|
+
w.append(
|
|
231
|
+
w[i]
|
|
232
|
+
+ h * sum(coeffs[j] * self.f(xi - j * h, w[i - j]) for j in range(step))
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
try:
|
|
236
|
+
return Polynomial.interpolate([(self.a + i * h, w[i]) for i in range(N + 1)])
|
|
237
|
+
except Exception:
|
|
238
|
+
return w
|
|
239
|
+
|
|
240
|
+
def _solve_adams_moulton(
|
|
241
|
+
self, h: float, step: int, points: list[float]
|
|
242
|
+
) -> Polynomial:
|
|
243
|
+
if step < 2:
|
|
244
|
+
raise ValueError("Adams-Moulton requires at least 2 steps.")
|
|
245
|
+
w = [self.y0] + list(points)
|
|
246
|
+
N = int((self.b - self.a) / h)
|
|
247
|
+
coeffs = [float(c) for c in FirstOrderLinearODE._adams_moulton_coefficients(step)]
|
|
248
|
+
|
|
249
|
+
for i in range(step - 1, N):
|
|
250
|
+
xi = self.a + i * h
|
|
251
|
+
explicit = sum(
|
|
252
|
+
coeffs[j] * self.f(xi - (j - 1) * h, w[i - j + 1]) for j in range(1, step + 1)
|
|
253
|
+
)
|
|
254
|
+
c0 = coeffs[0]
|
|
255
|
+
g = Function(
|
|
256
|
+
lambda x, _h=h, _xi=xi, _c0=c0, _explicit=explicit, _wi=w[i]: (
|
|
257
|
+
_wi + _h * (_c0 * self.f(_xi + _h, x) + _explicit)
|
|
258
|
+
)
|
|
259
|
+
)
|
|
260
|
+
w.append(g.fixed_point(w[i]))
|
|
261
|
+
|
|
262
|
+
try:
|
|
263
|
+
return Polynomial.interpolate([(self.a + i * h, w[i]) for i in range(N + 1)])
|
|
264
|
+
except Exception:
|
|
265
|
+
return w
|
|
266
|
+
|
|
267
|
+
def _solve_predictor_corrector(self, h: float) -> Polynomial:
|
|
268
|
+
w = [self.y0]
|
|
269
|
+
N = int((self.b - self.a) / h)
|
|
270
|
+
alphas = [self.a + h * i for i in range(1, 4)]
|
|
271
|
+
|
|
272
|
+
# Starting values via RK4
|
|
273
|
+
ivp = FirstOrderLinearODE(self.f, self.a, self.a + 3 * h, self.y0)
|
|
274
|
+
rk_sol = ivp._solve_runge_kutta(h, 4)
|
|
275
|
+
for xi in alphas:
|
|
276
|
+
w.append(rk_sol(xi))
|
|
277
|
+
|
|
278
|
+
for i in range(3, N):
|
|
279
|
+
xi = self.a + i * h
|
|
280
|
+
prediction = w[i] + (h / 24) * (
|
|
281
|
+
55 * self.f(xi, w[i])
|
|
282
|
+
- 59 * self.f(xi - h, w[i - 1])
|
|
283
|
+
+ 37 * self.f(xi - 2 * h, w[i - 2])
|
|
284
|
+
- 9 * self.f(xi - 3 * h, w[i - 3])
|
|
285
|
+
)
|
|
286
|
+
correction = w[i] + (h / 24) * (
|
|
287
|
+
9 * self.f(xi + h, prediction)
|
|
288
|
+
+ 19 * self.f(xi, w[i])
|
|
289
|
+
- 5 * self.f(xi - h, w[i - 1])
|
|
290
|
+
+ self.f(xi - 2 * h, w[i - 2])
|
|
291
|
+
)
|
|
292
|
+
w.append(correction)
|
|
293
|
+
|
|
294
|
+
try:
|
|
295
|
+
return Polynomial.interpolate([(self.a + i * h, w[i]) for i in range(N + 1)])
|
|
296
|
+
except Exception:
|
|
297
|
+
return w
|
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from ..enums import BVPMethod, NonlinearBVPMethod, ODEMethod
|
|
4
|
+
from ..functions.base import Function
|
|
5
|
+
from ..functions.elementary import Polynomial
|
|
6
|
+
from ..functions.multivariate import MultiVariableFunction
|
|
7
|
+
from ..linalg.vector import Vector
|
|
8
|
+
from ..linalg.matrix import Matrix
|
|
9
|
+
from ..linalg.linear_system import LinearSystem
|
|
10
|
+
from .base import LinearODE, OrdinaryDifferentialEquation
|
|
11
|
+
from .first_order import FirstOrderLinearODE
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class SecondOrderLinearODE_BVP(LinearODE):
|
|
15
|
+
"""Solver for the second-order linear BVP
|
|
16
|
+
|
|
17
|
+
.. math::
|
|
18
|
+
|
|
19
|
+
y''(x) = p(x)\\,y'(x) + q(x)\\,y(x) + r(x),
|
|
20
|
+
\\quad y(a) = y_0,\\quad y(b) = y_1.
|
|
21
|
+
|
|
22
|
+
Parameters
|
|
23
|
+
----------
|
|
24
|
+
p, q, r:
|
|
25
|
+
Coefficient functions.
|
|
26
|
+
a, b:
|
|
27
|
+
Boundary points.
|
|
28
|
+
y0, y1:
|
|
29
|
+
Boundary values :math:`y(a)` and :math:`y(b)`.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
def __init__(
|
|
33
|
+
self,
|
|
34
|
+
p: Function,
|
|
35
|
+
q: Function,
|
|
36
|
+
r: Function,
|
|
37
|
+
a: float,
|
|
38
|
+
b: float,
|
|
39
|
+
y0: float,
|
|
40
|
+
y1: float,
|
|
41
|
+
) -> None:
|
|
42
|
+
self.p = p
|
|
43
|
+
self.q = q
|
|
44
|
+
self.r = r
|
|
45
|
+
self.a = a
|
|
46
|
+
self.b = b
|
|
47
|
+
self.y0 = y0
|
|
48
|
+
self.y1 = y1
|
|
49
|
+
|
|
50
|
+
def solve(
|
|
51
|
+
self,
|
|
52
|
+
h: float = 0.1,
|
|
53
|
+
method: BVPMethod = BVPMethod.SHOOTING,
|
|
54
|
+
) -> Polynomial:
|
|
55
|
+
"""Solve the BVP and return an interpolating :class:`Polynomial`.
|
|
56
|
+
|
|
57
|
+
Parameters
|
|
58
|
+
----------
|
|
59
|
+
h:
|
|
60
|
+
Step size / mesh width.
|
|
61
|
+
method:
|
|
62
|
+
Algorithm (:class:`BVPMethod`).
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
if method is BVPMethod.SHOOTING:
|
|
66
|
+
return self._solve_shooting(h)
|
|
67
|
+
if method is BVPMethod.FINITE_DIFFERENCE:
|
|
68
|
+
return self._solve_finite_difference(h)
|
|
69
|
+
|
|
70
|
+
raise ValueError(f"Unknown BVP method: {method!r}")
|
|
71
|
+
|
|
72
|
+
def _solve_shooting(self, h: float) -> Polynomial:
|
|
73
|
+
IVP1 = SecondOrderODE_IVP(
|
|
74
|
+
MultiVariableFunction(
|
|
75
|
+
lambda t, u1, u2: self.p(t) * u2 + self.q(t) * u1 + self.r(t)
|
|
76
|
+
),
|
|
77
|
+
self.a, self.b, self.y0, 0,
|
|
78
|
+
)
|
|
79
|
+
IVP2 = SecondOrderODE_IVP(
|
|
80
|
+
MultiVariableFunction(lambda t, u1, u2: self.p(t) * u2 + self.q(t) * u1),
|
|
81
|
+
self.a, self.b, 0, 1,
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
sol1 = IVP1.solve(h)
|
|
85
|
+
sol2 = IVP2.solve(h)
|
|
86
|
+
c = (self.y1 - sol1(self.b)) / sol2(self.b)
|
|
87
|
+
|
|
88
|
+
return sol1 + c * sol2
|
|
89
|
+
|
|
90
|
+
def _solve_finite_difference(self, h: float) -> Polynomial:
|
|
91
|
+
N = int((self.b - self.a) / h) - 1
|
|
92
|
+
A = Matrix(*[Vector(*[0.0] * N) for _ in range(N)])
|
|
93
|
+
b = Vector(*[0.0] * N)
|
|
94
|
+
|
|
95
|
+
# First row
|
|
96
|
+
A[0][0] = -(2 + h**2 * self.q(self.a + h))
|
|
97
|
+
A[0][1] = 1 - (h / 2) * self.p(self.a + h)
|
|
98
|
+
b[0] = h**2 * self.r(self.a + h) - (1 + (h / 2) * self.p(self.a + h)) * self.y0
|
|
99
|
+
|
|
100
|
+
# Middle rows
|
|
101
|
+
for i in range(1, N - 1):
|
|
102
|
+
xi = self.a + (i + 1) * h
|
|
103
|
+
A[i][i - 1] = 1 + (h / 2) * self.p(xi)
|
|
104
|
+
A[i][i] = -(2 + h**2 * self.q(xi))
|
|
105
|
+
A[i][i + 1] = 1 - (h / 2) * self.p(xi)
|
|
106
|
+
b[i] = h**2 * self.r(xi)
|
|
107
|
+
|
|
108
|
+
# Last row
|
|
109
|
+
A[N - 1][N - 2] = 1 + (h / 2) * self.p(self.b - h)
|
|
110
|
+
A[N - 1][N - 1] = -(2 + h**2 * self.q(self.b - h))
|
|
111
|
+
b[N - 1] = (
|
|
112
|
+
h**2 * self.r(self.b - h)
|
|
113
|
+
- (1 - (h / 2) * self.p(self.b - h)) * self.y1
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
sol = LinearSystem(A, b).solve()
|
|
117
|
+
w = [self.y0] + sol.components + [self.y1]
|
|
118
|
+
|
|
119
|
+
return Polynomial.interpolate([(self.a + i * h, w[i]) for i in range(N + 2)])
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
class SecondOrderODE_IVP(OrdinaryDifferentialEquation):
|
|
123
|
+
"""Solver for the second-order nonlinear IVP
|
|
124
|
+
|
|
125
|
+
.. math::
|
|
126
|
+
|
|
127
|
+
y''(x) = f(x,\\,y(x),\\,y'(x)),
|
|
128
|
+
\\quad y(a) = y_0,\\quad y'(a) = y_1.
|
|
129
|
+
|
|
130
|
+
The equation is reduced to a first-order system before numerical integration.
|
|
131
|
+
|
|
132
|
+
Parameters
|
|
133
|
+
----------
|
|
134
|
+
f:
|
|
135
|
+
Right-hand side :math:`f(x, y, y')`.
|
|
136
|
+
a, b:
|
|
137
|
+
Integration interval.
|
|
138
|
+
y0:
|
|
139
|
+
Initial value :math:`y(a)`.
|
|
140
|
+
y1:
|
|
141
|
+
Initial slope :math:`y'(a)`.
|
|
142
|
+
"""
|
|
143
|
+
|
|
144
|
+
def __init__(
|
|
145
|
+
self,
|
|
146
|
+
f: MultiVariableFunction,
|
|
147
|
+
a: float,
|
|
148
|
+
b: float,
|
|
149
|
+
y0: float,
|
|
150
|
+
y1: float,
|
|
151
|
+
) -> None:
|
|
152
|
+
self.f = f
|
|
153
|
+
self.a = a
|
|
154
|
+
self.b = b
|
|
155
|
+
self.y0 = y0
|
|
156
|
+
self.y1 = y1
|
|
157
|
+
|
|
158
|
+
def solve(
|
|
159
|
+
self,
|
|
160
|
+
h: float = 0.1,
|
|
161
|
+
method: ODEMethod = ODEMethod.EULER,
|
|
162
|
+
n: int = 1,
|
|
163
|
+
step: int = 2,
|
|
164
|
+
points: list[float] | None = None,
|
|
165
|
+
) -> Polynomial:
|
|
166
|
+
"""Solve the IVP via system reduction and return an interpolating :class:`Polynomial`.
|
|
167
|
+
|
|
168
|
+
Accepts the same *method*, *n*, *step*, and *points* parameters as
|
|
169
|
+
:meth:`FirstOrderLinearODE.solve`.
|
|
170
|
+
"""
|
|
171
|
+
if points is None:
|
|
172
|
+
points = []
|
|
173
|
+
|
|
174
|
+
U0 = Vector(self.y0, self.y1)
|
|
175
|
+
F = Vector(
|
|
176
|
+
MultiVariableFunction(lambda t, u1, u2: u2),
|
|
177
|
+
MultiVariableFunction(lambda t, u1, u2: self.f(t, u1, u2)),
|
|
178
|
+
)
|
|
179
|
+
ivp = FirstOrderLinearODE(F, self.a, self.b, U0)
|
|
180
|
+
sol = ivp.solve(h, method, n, step, points)
|
|
181
|
+
|
|
182
|
+
w = [x[0] for x in sol]
|
|
183
|
+
N = int((self.b - self.a) / h)
|
|
184
|
+
return Polynomial.interpolate([(self.a + i * h, w[i]) for i in range(len(w))])
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
class SecondOrderODE_BVP(OrdinaryDifferentialEquation):
|
|
188
|
+
"""Solver for the second-order nonlinear BVP
|
|
189
|
+
|
|
190
|
+
.. math::
|
|
191
|
+
|
|
192
|
+
y''(x) = f(x,\\,y(x),\\,y'(x)),
|
|
193
|
+
\\quad y(a) = y_0,\\quad y(b) = y_1.
|
|
194
|
+
|
|
195
|
+
Parameters
|
|
196
|
+
----------
|
|
197
|
+
f:
|
|
198
|
+
Right-hand side :math:`f(x, y, y')`.
|
|
199
|
+
a, b:
|
|
200
|
+
Boundary points.
|
|
201
|
+
y0, y1:
|
|
202
|
+
Boundary values :math:`y(a)` and :math:`y(b)`.
|
|
203
|
+
"""
|
|
204
|
+
|
|
205
|
+
def __init__(
|
|
206
|
+
self,
|
|
207
|
+
f: MultiVariableFunction,
|
|
208
|
+
a: float,
|
|
209
|
+
b: float,
|
|
210
|
+
y0: float,
|
|
211
|
+
y1: float,
|
|
212
|
+
) -> None:
|
|
213
|
+
self.f = f
|
|
214
|
+
self.a = a
|
|
215
|
+
self.b = b
|
|
216
|
+
self.y0 = y0
|
|
217
|
+
self.y1 = y1
|
|
218
|
+
|
|
219
|
+
def solve(
|
|
220
|
+
self,
|
|
221
|
+
h: float = 0.1,
|
|
222
|
+
method: NonlinearBVPMethod = NonlinearBVPMethod.SHOOTING_NEWTON,
|
|
223
|
+
M: int = 100,
|
|
224
|
+
tol: float = 1e-5,
|
|
225
|
+
initial_approximation=None,
|
|
226
|
+
) -> Polynomial | None:
|
|
227
|
+
"""Solve the nonlinear BVP and return an interpolating :class:`Polynomial`.
|
|
228
|
+
|
|
229
|
+
Parameters
|
|
230
|
+
----------
|
|
231
|
+
h:
|
|
232
|
+
Step size / mesh width.
|
|
233
|
+
method:
|
|
234
|
+
Algorithm (:class:`NonlinearBVPMethod`).
|
|
235
|
+
M:
|
|
236
|
+
Maximum number of outer iterations.
|
|
237
|
+
tol:
|
|
238
|
+
Convergence tolerance.
|
|
239
|
+
initial_approximation:
|
|
240
|
+
Initial guess for :math:`y'(a)` (shooting) or interior node values
|
|
241
|
+
(finite difference).
|
|
242
|
+
"""
|
|
243
|
+
|
|
244
|
+
if method is NonlinearBVPMethod.SHOOTING_NEWTON:
|
|
245
|
+
return self._solve_shooting_newton(h, M, tol, initial_approximation)
|
|
246
|
+
if method is NonlinearBVPMethod.FINITE_DIFFERENCE:
|
|
247
|
+
return self._solve_finite_difference(h, M, tol)
|
|
248
|
+
|
|
249
|
+
raise ValueError(f"Unknown nonlinear BVP method: {method!r}")
|
|
250
|
+
|
|
251
|
+
def _solve_shooting_newton(
|
|
252
|
+
self, h: float, M: int, tol: float, initial_approximation
|
|
253
|
+
) -> Polynomial | None:
|
|
254
|
+
t = 1.0 if initial_approximation is None else initial_approximation
|
|
255
|
+
for _ in range(M):
|
|
256
|
+
IVP1 = SecondOrderODE_IVP(
|
|
257
|
+
MultiVariableFunction(lambda t_var, u1, u2: self.f(t_var, u1, u2)),
|
|
258
|
+
self.a, self.b, self.y0, t,
|
|
259
|
+
)
|
|
260
|
+
y = IVP1.solve(h)
|
|
261
|
+
|
|
262
|
+
p = Function(
|
|
263
|
+
lambda x: self.f(x, None, y.differentiate()(x)).differentiate()(y(x))
|
|
264
|
+
)
|
|
265
|
+
q = Function(
|
|
266
|
+
lambda x: self.f(x, y(x), None).differentiate()(y.differentiate()(x))
|
|
267
|
+
)
|
|
268
|
+
r = Function(lambda x: 0)
|
|
269
|
+
|
|
270
|
+
IVP2 = SecondOrderLinearODE_BVP(p, q, r, self.a, self.b, 0, 1)
|
|
271
|
+
z = IVP2.solve(h)
|
|
272
|
+
|
|
273
|
+
t_new = t - (y(self.b) - self.y1) / z(self.b)
|
|
274
|
+
if abs(t_new - t) < tol:
|
|
275
|
+
return y
|
|
276
|
+
t = t_new
|
|
277
|
+
|
|
278
|
+
return None
|
|
279
|
+
|
|
280
|
+
def _solve_finite_difference(
|
|
281
|
+
self, h: float, M: int, tol: float
|
|
282
|
+
) -> Polynomial | None:
|
|
283
|
+
N = int(round((self.b - self.a) / h)) - 1
|
|
284
|
+
pd_step = 1e-5
|
|
285
|
+
|
|
286
|
+
def df_dy(x: float, y: float, z: float) -> float:
|
|
287
|
+
return (self.f(x, y + pd_step, z) - self.f(x, y - pd_step, z)) / (2 * pd_step)
|
|
288
|
+
|
|
289
|
+
def df_dz(x: float, y: float, z: float) -> float:
|
|
290
|
+
return (self.f(x, y, z + pd_step) - self.f(x, y, z - pd_step)) / (2 * pd_step)
|
|
291
|
+
|
|
292
|
+
# Linear interpolation as initial guess
|
|
293
|
+
u = [
|
|
294
|
+
self.y0 + (j + 1) * h * (self.y1 - self.y0) / (self.b - self.a)
|
|
295
|
+
for j in range(N)
|
|
296
|
+
]
|
|
297
|
+
|
|
298
|
+
for _ in range(M):
|
|
299
|
+
A = Matrix(*[Vector(*[0.0] * N) for _ in range(N)])
|
|
300
|
+
F = [0.0] * N
|
|
301
|
+
|
|
302
|
+
for j in range(N):
|
|
303
|
+
xi = self.a + (j + 1) * h
|
|
304
|
+
wi = u[j]
|
|
305
|
+
wi_prev = u[j - 1] if j > 0 else self.y0
|
|
306
|
+
wi_next = u[j + 1] if j < N - 1 else self.y1
|
|
307
|
+
zprime = (wi_next - wi_prev) / (2 * h)
|
|
308
|
+
|
|
309
|
+
F[j] = wi_next - 2 * wi + wi_prev - h**2 * self.f(xi, wi, zprime)
|
|
310
|
+
A[j][j] = -2 - h**2 * df_dy(xi, wi, zprime)
|
|
311
|
+
if j > 0:
|
|
312
|
+
A[j][j - 1] = 1 + (h / 2) * df_dz(xi, wi, zprime)
|
|
313
|
+
if j < N - 1:
|
|
314
|
+
A[j][j + 1] = 1 - (h / 2) * df_dz(xi, wi, zprime)
|
|
315
|
+
|
|
316
|
+
delta = LinearSystem(A, Vector(*[-fi for fi in F])).solve()
|
|
317
|
+
u = [u[j] + delta[j] for j in range(N)]
|
|
318
|
+
|
|
319
|
+
if max(abs(delta[j]) for j in range(N)) < tol:
|
|
320
|
+
w = [self.y0] + u + [self.y1]
|
|
321
|
+
return Polynomial.interpolate([(self.a + i * h, w[i]) for i in range(N + 2)])
|
|
322
|
+
|
|
323
|
+
return None
|
pysolverkit/util.py
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import math
|
|
4
|
+
|
|
5
|
+
from .functions.base import Function
|
|
6
|
+
from .functions.elementary import Polynomial
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Util:
|
|
10
|
+
"""Utility helpers for interpolation arithmetic."""
|
|
11
|
+
|
|
12
|
+
@staticmethod
|
|
13
|
+
def choose(s: Function, k: int) -> Polynomial:
|
|
14
|
+
"""Return the falling-factorial polynomial :math:`\\binom{s}{k}`.
|
|
15
|
+
|
|
16
|
+
Parameters
|
|
17
|
+
----------
|
|
18
|
+
s:
|
|
19
|
+
A :class:`~pysolverkit.functions.base.Function` (typically a
|
|
20
|
+
linear polynomial in *x*).
|
|
21
|
+
k:
|
|
22
|
+
Non-negative integer.
|
|
23
|
+
"""
|
|
24
|
+
res = Polynomial(1)
|
|
25
|
+
for i in range(k):
|
|
26
|
+
res = res * (-i + s)
|
|
27
|
+
return (1 / math.factorial(k)) * res
|
|
28
|
+
|
|
29
|
+
@staticmethod
|
|
30
|
+
def delta(k: int, i: int, data: list[tuple[float, float]]) -> float:
|
|
31
|
+
"""Compute the *k*-th forward difference :math:`\\Delta^k y_i`.
|
|
32
|
+
|
|
33
|
+
Parameters
|
|
34
|
+
----------
|
|
35
|
+
k:
|
|
36
|
+
Order of the forward difference.
|
|
37
|
+
i:
|
|
38
|
+
Starting index.
|
|
39
|
+
data:
|
|
40
|
+
Sequence of ``(x, y)`` pairs.
|
|
41
|
+
"""
|
|
42
|
+
if k == 1:
|
|
43
|
+
return data[i + 1][1] - data[i][1]
|
|
44
|
+
return Util.delta(k - 1, i + 1, data) - Util.delta(k - 1, i, data)
|
|
45
|
+
|
|
46
|
+
@staticmethod
|
|
47
|
+
def downdelta(k: int, i: int, data: list[tuple[float, float]]) -> float:
|
|
48
|
+
"""Compute the *k*-th backward difference :math:`\\nabla^k y_i`.
|
|
49
|
+
|
|
50
|
+
Parameters
|
|
51
|
+
----------
|
|
52
|
+
k:
|
|
53
|
+
Order of the backward difference.
|
|
54
|
+
i:
|
|
55
|
+
Starting index.
|
|
56
|
+
data:
|
|
57
|
+
Sequence of ``(x, y)`` pairs.
|
|
58
|
+
"""
|
|
59
|
+
if k == 1:
|
|
60
|
+
return data[i][1] - data[i - 1][1]
|
|
61
|
+
return Util.downdelta(k - 1, i, data) - Util.downdelta(k - 1, i - 1, data)
|
|
62
|
+
|