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,192 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import math
|
|
4
|
+
from typing import TYPE_CHECKING
|
|
5
|
+
|
|
6
|
+
from ..enums import InterpolationMethod, InterpolationForm
|
|
7
|
+
from .base import Function
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
pass
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Polynomial(Function):
|
|
14
|
+
"""A polynomial function :math:`p(x) = a_0 + a_1 x + \\cdots + a_n x^n`.
|
|
15
|
+
|
|
16
|
+
Parameters
|
|
17
|
+
----------
|
|
18
|
+
*coefficients:
|
|
19
|
+
Coefficients in **ascending** degree order: ``a_0, a_1, ..., a_n``.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
def __init__(self, *coefficients: float) -> None:
|
|
23
|
+
self.coefficients = coefficients
|
|
24
|
+
self.function = lambda x: sum(a * x**i for i, a in enumerate(coefficients))
|
|
25
|
+
|
|
26
|
+
@staticmethod
|
|
27
|
+
def interpolate(
|
|
28
|
+
data: list[tuple[float, float]] | list[float],
|
|
29
|
+
method: InterpolationMethod = InterpolationMethod.NEWTON,
|
|
30
|
+
f: Function | None = None,
|
|
31
|
+
form: InterpolationForm = InterpolationForm.STANDARD,
|
|
32
|
+
) -> Polynomial:
|
|
33
|
+
"""Construct an interpolating polynomial.
|
|
34
|
+
|
|
35
|
+
Parameters
|
|
36
|
+
----------
|
|
37
|
+
data:
|
|
38
|
+
Either a list of ``(x, y)`` tuples, **or** a list of *x* values
|
|
39
|
+
when *f* is provided.
|
|
40
|
+
method:
|
|
41
|
+
Interpolation algorithm (:class:`InterpolationMethod`).
|
|
42
|
+
f:
|
|
43
|
+
If provided, evaluates *f* at each *x* in *data* to build the
|
|
44
|
+
``(x, y)`` pairs.
|
|
45
|
+
form:
|
|
46
|
+
Newton form variant (:class:`InterpolationForm`): ``STANDARD``
|
|
47
|
+
(divided differences), ``FORWARD_DIFF``, or ``BACKWARD_DIFF``.
|
|
48
|
+
"""
|
|
49
|
+
if f is not None:
|
|
50
|
+
data = [(x, f(x)) for x in data]
|
|
51
|
+
|
|
52
|
+
if method is InterpolationMethod.LAGRANGE:
|
|
53
|
+
return Polynomial._interpolate_lagrange(data)
|
|
54
|
+
if method is InterpolationMethod.NEWTON:
|
|
55
|
+
if form is InterpolationForm.STANDARD:
|
|
56
|
+
return Polynomial._interpolate_newton(data)
|
|
57
|
+
if form is InterpolationForm.FORWARD_DIFF:
|
|
58
|
+
return Polynomial._interpolate_newton_forward_diff(data)
|
|
59
|
+
if form is InterpolationForm.BACKWARD_DIFF:
|
|
60
|
+
return Polynomial._interpolate_newton_backward_diff(data)
|
|
61
|
+
|
|
62
|
+
raise ValueError(f"Unknown interpolation method/form combination: {method!r}/{form!r}")
|
|
63
|
+
|
|
64
|
+
@staticmethod
|
|
65
|
+
def _interpolate_lagrange(data: list[tuple[float, float]]) -> Polynomial:
|
|
66
|
+
n = len(data)
|
|
67
|
+
x = [d[0] for d in data]
|
|
68
|
+
y = [d[1] for d in data]
|
|
69
|
+
p = Polynomial(0)
|
|
70
|
+
|
|
71
|
+
for i in range(n):
|
|
72
|
+
L = Polynomial(1)
|
|
73
|
+
for j in range(n):
|
|
74
|
+
if i != j:
|
|
75
|
+
L = L * Polynomial(-x[j], 1) / Polynomial(x[i] - x[j])
|
|
76
|
+
p = p + y[i] * L
|
|
77
|
+
|
|
78
|
+
return p
|
|
79
|
+
|
|
80
|
+
@staticmethod
|
|
81
|
+
def _interpolate_newton(data: list[tuple[float, float]]) -> Polynomial:
|
|
82
|
+
n = len(data)
|
|
83
|
+
x = [d[0] for d in data]
|
|
84
|
+
y = [d[1] for d in data]
|
|
85
|
+
|
|
86
|
+
def divided_difference(i: int, j: int) -> float:
|
|
87
|
+
if i == j:
|
|
88
|
+
return y[i]
|
|
89
|
+
return (divided_difference(i + 1, j) - divided_difference(i, j - 1)) / (x[j] - x[i])
|
|
90
|
+
|
|
91
|
+
def factor_product(roots: list[float]) -> Polynomial:
|
|
92
|
+
if not roots:
|
|
93
|
+
return Polynomial(1)
|
|
94
|
+
return Polynomial(-roots[0], 1) * factor_product(roots[1:])
|
|
95
|
+
|
|
96
|
+
coefficients = [divided_difference(0, i) for i in range(n)]
|
|
97
|
+
p = Polynomial(coefficients[0])
|
|
98
|
+
for i in range(1, n):
|
|
99
|
+
p = p + coefficients[i] * factor_product(x[:i])
|
|
100
|
+
|
|
101
|
+
return p
|
|
102
|
+
|
|
103
|
+
@staticmethod
|
|
104
|
+
def _interpolate_newton_forward_diff(data: list[tuple[float, float]]) -> Polynomial:
|
|
105
|
+
from ..util import Util
|
|
106
|
+
|
|
107
|
+
data = sorted(data, key=lambda d: d[0])
|
|
108
|
+
diffs = sorted([data[i + 1][0] - data[i][0] for i in range(len(data) - 1)])
|
|
109
|
+
for j in range(len(diffs) - 1):
|
|
110
|
+
if abs(diffs[j + 1] - diffs[j]) >= 1e-6:
|
|
111
|
+
raise ValueError("x values must be equally spaced for Newton forward-difference.")
|
|
112
|
+
|
|
113
|
+
h = abs(diffs[0])
|
|
114
|
+
n = len(data) - 1
|
|
115
|
+
|
|
116
|
+
p = Polynomial(data[0][1])
|
|
117
|
+
for k in range(1, n + 1):
|
|
118
|
+
p = p + Util.delta(k, 0, data) * Util.choose(Polynomial(-data[0][0] / h, 1 / h), k)
|
|
119
|
+
|
|
120
|
+
return p
|
|
121
|
+
|
|
122
|
+
@staticmethod
|
|
123
|
+
def _interpolate_newton_backward_diff(data: list[tuple[float, float]]) -> Polynomial:
|
|
124
|
+
from ..util import Util
|
|
125
|
+
|
|
126
|
+
data = sorted(data, key=lambda d: d[0])
|
|
127
|
+
diffs = sorted([data[i + 1][0] - data[i][0] for i in range(len(data) - 1)])
|
|
128
|
+
for j in range(len(diffs) - 1):
|
|
129
|
+
if abs(diffs[j + 1] - diffs[j]) >= 1e-6:
|
|
130
|
+
raise ValueError("x values must be equally spaced for Newton backward-difference.")
|
|
131
|
+
|
|
132
|
+
h = abs(diffs[0])
|
|
133
|
+
n = len(data) - 1
|
|
134
|
+
|
|
135
|
+
p = Polynomial(data[n][1])
|
|
136
|
+
for k in range(1, n + 1):
|
|
137
|
+
p = p + ((-1) ** k * Util.downdelta(k, n, data)) * Util.choose(
|
|
138
|
+
Polynomial(data[n][0] / h, -1 / h), k
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
return p
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
class Exponent(Function):
|
|
145
|
+
"""The exponential function :math:`b^{f(x)}`.
|
|
146
|
+
|
|
147
|
+
Parameters
|
|
148
|
+
----------
|
|
149
|
+
f:
|
|
150
|
+
Exponent function.
|
|
151
|
+
base:
|
|
152
|
+
Base of the exponential (defaults to :math:`e`).
|
|
153
|
+
"""
|
|
154
|
+
|
|
155
|
+
def __init__(self, f: Function, base: float = math.e) -> None:
|
|
156
|
+
self.function = lambda x: base ** f(x)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
class Sin(Function):
|
|
160
|
+
"""The sine function :math:`\\sin(f(x))`."""
|
|
161
|
+
|
|
162
|
+
def __init__(self, f: Function) -> None:
|
|
163
|
+
self.function = lambda x: math.sin(f(x))
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
class Cos(Function):
|
|
167
|
+
"""The cosine function :math:`\\cos(f(x))`."""
|
|
168
|
+
|
|
169
|
+
def __init__(self, f: Function) -> None:
|
|
170
|
+
self.function = lambda x: math.cos(f(x))
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
class Tan(Function):
|
|
174
|
+
"""The tangent function :math:`\\tan(f(x))`."""
|
|
175
|
+
|
|
176
|
+
def __init__(self, f: Function) -> None:
|
|
177
|
+
self.function = lambda x: math.tan(f(x))
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
class Log(Function):
|
|
181
|
+
"""The logarithm :math:`\\log_b(f(x))`.
|
|
182
|
+
|
|
183
|
+
Parameters
|
|
184
|
+
----------
|
|
185
|
+
f:
|
|
186
|
+
Argument function.
|
|
187
|
+
base:
|
|
188
|
+
Logarithm base (defaults to :math:`e`, i.e. natural logarithm).
|
|
189
|
+
"""
|
|
190
|
+
|
|
191
|
+
def __init__(self, f: Function, base: float = math.e) -> None:
|
|
192
|
+
self.function = lambda x: math.log(f(x), base)
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from ..linalg.linear_system import LinearSystem
|
|
4
|
+
from ..linalg.matrix import Matrix
|
|
5
|
+
from ..linalg.vector import Vector
|
|
6
|
+
from ..enums import LinearSolverMethod
|
|
7
|
+
from .multivariate import BivariateFunction
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class FEM2D:
|
|
11
|
+
r"""Solve 2D Poisson problems on rectangles with linear triangular FEM.
|
|
12
|
+
|
|
13
|
+
Solves
|
|
14
|
+
|
|
15
|
+
.. math::
|
|
16
|
+
-\Delta u(x,y) = f(x,y)\ ext{in}\ \Omega=[x_a,x_b] imes[y_a,y_b],
|
|
17
|
+
\quad u=g\ ext{on}\ \partial\Omega.
|
|
18
|
+
|
|
19
|
+
Parameters
|
|
20
|
+
----------
|
|
21
|
+
f:
|
|
22
|
+
Source term :math:`f(x,y)`.
|
|
23
|
+
g:
|
|
24
|
+
Dirichlet boundary condition :math:`g(x,y)`.
|
|
25
|
+
xa, xb, ya, yb:
|
|
26
|
+
Rectangle bounds.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
def __init__(self, f: BivariateFunction, g: BivariateFunction, xa: float, xb: float, ya: float, yb: float) -> None:
|
|
30
|
+
self.f = f
|
|
31
|
+
self.g = g
|
|
32
|
+
self.xa = xa
|
|
33
|
+
self.xb = xb
|
|
34
|
+
self.ya = ya
|
|
35
|
+
self.yb = yb
|
|
36
|
+
|
|
37
|
+
def solve(self, nx: int = 4, ny: int = 4) -> BivariateFunction:
|
|
38
|
+
"""Return a :class:`BivariateFunction` FEM approximation on an ``nx x ny`` mesh."""
|
|
39
|
+
hx = (self.xb - self.xa) / nx
|
|
40
|
+
hy = (self.yb - self.ya) / ny
|
|
41
|
+
n_total = (nx + 1) * (ny + 1)
|
|
42
|
+
|
|
43
|
+
def nidx(i: int, j: int) -> int:
|
|
44
|
+
return i * (ny + 1) + j
|
|
45
|
+
|
|
46
|
+
def ncoords(i: int, j: int) -> tuple[float, float]:
|
|
47
|
+
return self.xa + i * hx, self.ya + j * hy
|
|
48
|
+
|
|
49
|
+
K = [[0.0] * n_total for _ in range(n_total)]
|
|
50
|
+
F = [0.0] * n_total
|
|
51
|
+
|
|
52
|
+
for i in range(nx):
|
|
53
|
+
for j in range(ny):
|
|
54
|
+
x00, y00 = ncoords(i, j)
|
|
55
|
+
x10, y10 = ncoords(i + 1, j)
|
|
56
|
+
x01, y01 = ncoords(i, j + 1)
|
|
57
|
+
x11, y11 = ncoords(i + 1, j + 1)
|
|
58
|
+
|
|
59
|
+
self._add_element(
|
|
60
|
+
K,
|
|
61
|
+
F,
|
|
62
|
+
[(x00, y00), (x10, y10), (x01, y01)],
|
|
63
|
+
[nidx(i, j), nidx(i + 1, j), nidx(i, j + 1)],
|
|
64
|
+
)
|
|
65
|
+
self._add_element(
|
|
66
|
+
K,
|
|
67
|
+
F,
|
|
68
|
+
[(x10, y10), (x11, y11), (x01, y01)],
|
|
69
|
+
[nidx(i + 1, j), nidx(i + 1, j + 1), nidx(i, j + 1)],
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
boundary: dict[int, float] = {}
|
|
73
|
+
for i in range(nx + 1):
|
|
74
|
+
for j in range(ny + 1):
|
|
75
|
+
if i == 0 or i == nx or j == 0 or j == ny:
|
|
76
|
+
k = nidx(i, j)
|
|
77
|
+
xk, yk = ncoords(i, j)
|
|
78
|
+
boundary[k] = self.g(xk, yk)
|
|
79
|
+
|
|
80
|
+
for k, gk in boundary.items():
|
|
81
|
+
for m in range(n_total):
|
|
82
|
+
if m not in boundary:
|
|
83
|
+
F[m] -= K[m][k] * gk
|
|
84
|
+
|
|
85
|
+
for k, gk in boundary.items():
|
|
86
|
+
for m in range(n_total):
|
|
87
|
+
K[k][m] = 0.0
|
|
88
|
+
K[m][k] = 0.0
|
|
89
|
+
K[k][k] = 1.0
|
|
90
|
+
F[k] = gk
|
|
91
|
+
|
|
92
|
+
A_mat = Matrix(*[Vector(*K[r]) for r in range(n_total)])
|
|
93
|
+
b_vec = Vector(*F)
|
|
94
|
+
sol = LinearSystem(A_mat, b_vec).solve(method=LinearSolverMethod.GAUSS_ELIMINATION)
|
|
95
|
+
u = sol.components
|
|
96
|
+
|
|
97
|
+
xa_ = self.xa
|
|
98
|
+
ya_ = self.ya
|
|
99
|
+
|
|
100
|
+
def eval_u(x: float, y: float) -> float:
|
|
101
|
+
ix = max(0, min(nx - 1, int((x - xa_) / hx)))
|
|
102
|
+
jy = max(0, min(ny - 1, int((y - ya_) / hy)))
|
|
103
|
+
lx = max(0.0, min(1.0, (x - (xa_ + ix * hx)) / hx))
|
|
104
|
+
ly = max(0.0, min(1.0, (y - (ya_ + jy * hy)) / hy))
|
|
105
|
+
u00 = u[nidx(ix, jy)]
|
|
106
|
+
u10 = u[nidx(ix + 1, jy)]
|
|
107
|
+
u01 = u[nidx(ix, jy + 1)]
|
|
108
|
+
u11 = u[nidx(ix + 1, jy + 1)]
|
|
109
|
+
return (1 - lx) * (1 - ly) * u00 + lx * (1 - ly) * u10 + (1 - lx) * ly * u01 + lx * ly * u11
|
|
110
|
+
|
|
111
|
+
return BivariateFunction(eval_u)
|
|
112
|
+
|
|
113
|
+
def _add_element(self, K: list[list[float]], F: list[float], verts: list[tuple[float, float]], idxs: list[int]) -> None:
|
|
114
|
+
x1, y1 = verts[0]
|
|
115
|
+
x2, y2 = verts[1]
|
|
116
|
+
x3, y3 = verts[2]
|
|
117
|
+
|
|
118
|
+
area = 0.5 * abs((x2 - x1) * (y3 - y1) - (x3 - x1) * (y2 - y1))
|
|
119
|
+
if area == 0:
|
|
120
|
+
return
|
|
121
|
+
|
|
122
|
+
b = [y2 - y3, y3 - y1, y1 - y2]
|
|
123
|
+
c = [x3 - x2, x1 - x3, x2 - x1]
|
|
124
|
+
|
|
125
|
+
for i in range(3):
|
|
126
|
+
for j in range(3):
|
|
127
|
+
K[idxs[i]][idxs[j]] += (b[i] * b[j] + c[i] * c[j]) / (4 * area)
|
|
128
|
+
F[idxs[i]] += (area / 3) * self.f(verts[i][0], verts[i][1])
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from .base import Function
|
|
4
|
+
from ..linalg.vector import Vector
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class MultiVariableFunction:
|
|
8
|
+
"""A function of two or more scalar (or :class:`~pysolverkit.linalg.Vector`) arguments.
|
|
9
|
+
|
|
10
|
+
Supports **partial application**: pass ``None`` for any argument to defer it,
|
|
11
|
+
returning a new :class:`~pysolverkit.functions.base.Function` (for a single
|
|
12
|
+
free argument) or a new :class:`MultiVariableFunction` (for multiple free arguments).
|
|
13
|
+
|
|
14
|
+
Parameters
|
|
15
|
+
----------
|
|
16
|
+
function:
|
|
17
|
+
A callable that accepts all positional scalar arguments.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
def __init__(self, function) -> None:
|
|
21
|
+
self.function = function
|
|
22
|
+
|
|
23
|
+
def __call__(self, *args):
|
|
24
|
+
# Unpack any Vector arguments into their scalar components
|
|
25
|
+
unwrapped: list = []
|
|
26
|
+
for arg in args:
|
|
27
|
+
if isinstance(arg, Vector):
|
|
28
|
+
unwrapped.extend(arg.components)
|
|
29
|
+
else:
|
|
30
|
+
unwrapped.append(arg)
|
|
31
|
+
args = tuple(unwrapped)
|
|
32
|
+
|
|
33
|
+
if all(arg is not None for arg in args):
|
|
34
|
+
return self.function(*args)
|
|
35
|
+
if all(arg is None for arg in args):
|
|
36
|
+
raise ValueError("At least one argument must be non-None.")
|
|
37
|
+
|
|
38
|
+
# Build a partially-applied function
|
|
39
|
+
original = list(args)
|
|
40
|
+
|
|
41
|
+
def partial(*z):
|
|
42
|
+
filled = list(z)
|
|
43
|
+
arguments = [filled.pop(0) if arg is None else arg for arg in original]
|
|
44
|
+
return self(*arguments)
|
|
45
|
+
|
|
46
|
+
n_free = sum(arg is None for arg in args)
|
|
47
|
+
if n_free == 1:
|
|
48
|
+
return Function(partial)
|
|
49
|
+
return MultiVariableFunction(partial)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class BivariateFunction(MultiVariableFunction):
|
|
53
|
+
"""A :class:`MultiVariableFunction` specialised for exactly two arguments.
|
|
54
|
+
|
|
55
|
+
This is a convenience alias — no additional behaviour beyond
|
|
56
|
+
:class:`MultiVariableFunction`.
|
|
57
|
+
"""
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from ..enums import LinearSolverMethod
|
|
4
|
+
from .vector import Vector
|
|
5
|
+
from .matrix import Matrix
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class LinearSystem:
|
|
9
|
+
"""A system of linear equations :math:`Ax = b`.
|
|
10
|
+
|
|
11
|
+
Parameters
|
|
12
|
+
----------
|
|
13
|
+
A:
|
|
14
|
+
The coefficient :class:`Matrix`.
|
|
15
|
+
b:
|
|
16
|
+
The right-hand-side :class:`Vector`.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
def __init__(self, A: Matrix, b: Vector) -> None:
|
|
20
|
+
self.A = A
|
|
21
|
+
self.b = b
|
|
22
|
+
self.N = len(A)
|
|
23
|
+
|
|
24
|
+
if len(b) != self.N:
|
|
25
|
+
raise ValueError("A and b must have the same number of rows.")
|
|
26
|
+
|
|
27
|
+
def solve(
|
|
28
|
+
self,
|
|
29
|
+
method: LinearSolverMethod = LinearSolverMethod.GAUSS_ELIMINATION,
|
|
30
|
+
tol: float = 1e-5,
|
|
31
|
+
initial_approximation: Vector | None = None,
|
|
32
|
+
max_iterations: int = 100,
|
|
33
|
+
) -> Vector:
|
|
34
|
+
"""Solve the linear system and return the solution :class:`Vector`.
|
|
35
|
+
|
|
36
|
+
Parameters
|
|
37
|
+
----------
|
|
38
|
+
method:
|
|
39
|
+
Solver algorithm to use (:class:`LinearSolverMethod`).
|
|
40
|
+
tol:
|
|
41
|
+
Convergence tolerance for iterative solvers.
|
|
42
|
+
initial_approximation:
|
|
43
|
+
Starting guess for iterative solvers.
|
|
44
|
+
max_iterations:
|
|
45
|
+
Maximum number of iterations for iterative solvers.
|
|
46
|
+
"""
|
|
47
|
+
if method is LinearSolverMethod.GAUSS_ELIMINATION:
|
|
48
|
+
return self._gauss_elimination()
|
|
49
|
+
if method is LinearSolverMethod.GAUSS_JACOBI:
|
|
50
|
+
return self._gauss_jacobi(tol, initial_approximation, max_iterations)
|
|
51
|
+
if method is LinearSolverMethod.GAUSS_SEIDEL:
|
|
52
|
+
return self._gauss_seidel(tol, initial_approximation, max_iterations)
|
|
53
|
+
|
|
54
|
+
raise ValueError(f"Unknown method: {method!r}")
|
|
55
|
+
|
|
56
|
+
# ------------------------------------------------------------------
|
|
57
|
+
# Private solvers
|
|
58
|
+
# ------------------------------------------------------------------
|
|
59
|
+
|
|
60
|
+
def _gauss_elimination(self) -> Vector:
|
|
61
|
+
for i in range(self.N - 1):
|
|
62
|
+
p = next((j for j in range(i, self.N) if abs(self.A[j][i]) != 0), None)
|
|
63
|
+
if p is None:
|
|
64
|
+
raise ValueError("No unique solution exists.")
|
|
65
|
+
|
|
66
|
+
if p != i:
|
|
67
|
+
self.A[i], self.A[p] = self.A[p], self.A[i]
|
|
68
|
+
self.b[i], self.b[p] = self.b[p], self.b[i]
|
|
69
|
+
|
|
70
|
+
for j in range(i + 1, self.N):
|
|
71
|
+
m = self.A[j][i] / self.A[i][i]
|
|
72
|
+
self.A[j] = self.A[j] - m * self.A[i]
|
|
73
|
+
self.b[j] = self.b[j] - m * self.b[i]
|
|
74
|
+
|
|
75
|
+
if abs(self.A[self.N - 1][self.N - 1]) == 0:
|
|
76
|
+
raise ValueError("No unique solution exists.")
|
|
77
|
+
|
|
78
|
+
x: list[float] = [0.0] * self.N
|
|
79
|
+
x[self.N - 1] = self.b[self.N - 1] / self.A[self.N - 1][self.N - 1]
|
|
80
|
+
for i in range(self.N - 2, -1, -1):
|
|
81
|
+
x[i] = (
|
|
82
|
+
self.b[i] - sum(self.A[i][j] * x[j] for j in range(i + 1, self.N))
|
|
83
|
+
) / self.A[i][i]
|
|
84
|
+
|
|
85
|
+
return Vector(*x)
|
|
86
|
+
|
|
87
|
+
def _gauss_jacobi(
|
|
88
|
+
self, tol: float, initial_approximation: Vector | None, max_iterations: int
|
|
89
|
+
) -> Vector | None:
|
|
90
|
+
if initial_approximation is None:
|
|
91
|
+
raise ValueError("initial_approximation must be provided for iterative solvers.")
|
|
92
|
+
|
|
93
|
+
x0 = initial_approximation
|
|
94
|
+
for _ in range(max_iterations):
|
|
95
|
+
x1 = [
|
|
96
|
+
(self.b[i] - sum(self.A[i][j] * x0[j] for j in range(self.N) if j != i))
|
|
97
|
+
/ self.A[i][i]
|
|
98
|
+
for i in range(self.N)
|
|
99
|
+
]
|
|
100
|
+
if (
|
|
101
|
+
max(abs(x1[i] - x0[i]) for i in range(self.N))
|
|
102
|
+
/ max(abs(x1[i]) for i in range(self.N))
|
|
103
|
+
< tol
|
|
104
|
+
):
|
|
105
|
+
return Vector(*x1)
|
|
106
|
+
x0 = x1
|
|
107
|
+
|
|
108
|
+
return None
|
|
109
|
+
|
|
110
|
+
def _gauss_seidel(
|
|
111
|
+
self, tol: float, initial_approximation: Vector | None, max_iterations: int
|
|
112
|
+
) -> Vector | None:
|
|
113
|
+
if initial_approximation is None:
|
|
114
|
+
raise ValueError("initial_approximation must be provided for iterative solvers.")
|
|
115
|
+
|
|
116
|
+
x0 = initial_approximation
|
|
117
|
+
for _ in range(max_iterations):
|
|
118
|
+
x1 = [0.0] * self.N
|
|
119
|
+
for i in range(self.N):
|
|
120
|
+
x1[i] = (
|
|
121
|
+
self.b[i]
|
|
122
|
+
- sum(self.A[i][j] * x1[j] for j in range(i))
|
|
123
|
+
- sum(self.A[i][j] * x0[j] for j in range(i + 1, self.N))
|
|
124
|
+
) / self.A[i][i]
|
|
125
|
+
|
|
126
|
+
if (
|
|
127
|
+
max(abs(x1[i] - x0[i]) for i in range(self.N))
|
|
128
|
+
/ max(abs(x1[i]) for i in range(self.N))
|
|
129
|
+
< tol
|
|
130
|
+
):
|
|
131
|
+
return Vector(*x1)
|
|
132
|
+
x0 = x1
|
|
133
|
+
|
|
134
|
+
return None
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from .vector import Vector
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Matrix:
|
|
7
|
+
"""A rectangular array of scalars stored as a sequence of row :class:`Vector` objects."""
|
|
8
|
+
|
|
9
|
+
def __init__(self, *rows: Vector) -> None:
|
|
10
|
+
self.rows: list[Vector] = list(rows)
|
|
11
|
+
|
|
12
|
+
# ------------------------------------------------------------------
|
|
13
|
+
# Arithmetic
|
|
14
|
+
# ------------------------------------------------------------------
|
|
15
|
+
|
|
16
|
+
def __add__(self, other: Matrix) -> Matrix:
|
|
17
|
+
return Matrix(*[self.rows[i] + other.rows[i] for i in range(len(self))])
|
|
18
|
+
|
|
19
|
+
def __sub__(self, other: Matrix) -> Matrix:
|
|
20
|
+
return Matrix(*[self.rows[i] - other.rows[i] for i in range(len(self))])
|
|
21
|
+
|
|
22
|
+
def __rmul__(self, scalar: float) -> Matrix:
|
|
23
|
+
return Matrix(*[scalar * self.rows[i] for i in range(len(self))])
|
|
24
|
+
|
|
25
|
+
# ------------------------------------------------------------------
|
|
26
|
+
# Container interface
|
|
27
|
+
# ------------------------------------------------------------------
|
|
28
|
+
|
|
29
|
+
def __getitem__(self, i: int) -> Vector:
|
|
30
|
+
return self.rows[i]
|
|
31
|
+
|
|
32
|
+
def __setitem__(self, i: int, value: Vector) -> None:
|
|
33
|
+
self.rows[i] = value
|
|
34
|
+
|
|
35
|
+
def __len__(self) -> int:
|
|
36
|
+
return len(self.rows)
|
|
37
|
+
|
|
38
|
+
def __iter__(self):
|
|
39
|
+
return iter(self.rows)
|
|
40
|
+
|
|
41
|
+
def __repr__(self) -> str:
|
|
42
|
+
return f"Matrix({', '.join(repr(r) for r in self.rows)})"
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class Vector:
|
|
5
|
+
"""An ordered sequence of scalar components with element-wise arithmetic."""
|
|
6
|
+
|
|
7
|
+
def __init__(self, *components: float) -> None:
|
|
8
|
+
self.components: list[float] = list(components)
|
|
9
|
+
|
|
10
|
+
# ------------------------------------------------------------------
|
|
11
|
+
# Arithmetic
|
|
12
|
+
# ------------------------------------------------------------------
|
|
13
|
+
|
|
14
|
+
def __add__(self, other: Vector) -> Vector:
|
|
15
|
+
return Vector(*[self.components[i] + other.components[i] for i in range(len(self))])
|
|
16
|
+
|
|
17
|
+
def __sub__(self, other: Vector) -> Vector:
|
|
18
|
+
return Vector(*[self.components[i] - other.components[i] for i in range(len(self))])
|
|
19
|
+
|
|
20
|
+
def __rmul__(self, scalar: float) -> Vector:
|
|
21
|
+
return Vector(*[scalar * self.components[i] for i in range(len(self))])
|
|
22
|
+
|
|
23
|
+
def __call__(self, *args) -> Vector:
|
|
24
|
+
return Vector(*[self.components[i](*args) for i in range(len(self))])
|
|
25
|
+
|
|
26
|
+
# ------------------------------------------------------------------
|
|
27
|
+
# Container interface
|
|
28
|
+
# ------------------------------------------------------------------
|
|
29
|
+
|
|
30
|
+
def __getitem__(self, i: int) -> float:
|
|
31
|
+
return self.components[i]
|
|
32
|
+
|
|
33
|
+
def __setitem__(self, i: int, value: float) -> None:
|
|
34
|
+
self.components[i] = value
|
|
35
|
+
|
|
36
|
+
def __len__(self) -> int:
|
|
37
|
+
return len(self.components)
|
|
38
|
+
|
|
39
|
+
def __iter__(self):
|
|
40
|
+
return iter(self.components)
|
|
41
|
+
|
|
42
|
+
def __str__(self) -> str:
|
|
43
|
+
return "<" + ", ".join(str(c) for c in self.components) + ">"
|
|
44
|
+
|
|
45
|
+
def __repr__(self) -> str:
|
|
46
|
+
return f"Vector({', '.join(repr(c) for c in self.components)})"
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from .base import OrdinaryDifferentialEquation, LinearODE
|
|
2
|
+
from .first_order import FirstOrderLinearODE
|
|
3
|
+
from .second_order import SecondOrderLinearODE_BVP, SecondOrderODE_IVP, SecondOrderODE_BVP
|
|
4
|
+
|
|
5
|
+
__all__ = [
|
|
6
|
+
"OrdinaryDifferentialEquation",
|
|
7
|
+
"LinearODE",
|
|
8
|
+
"FirstOrderLinearODE",
|
|
9
|
+
"SecondOrderLinearODE_BVP",
|
|
10
|
+
"SecondOrderODE_IVP",
|
|
11
|
+
"SecondOrderODE_BVP",
|
|
12
|
+
]
|