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.
@@ -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
+