numanlib 0.1.0__tar.gz
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.
- numanlib-0.1.0/PKG-INFO +12 -0
- numanlib-0.1.0/README.md +0 -0
- numanlib-0.1.0/pyproject.toml +30 -0
- numanlib-0.1.0/src/numanlib/__init__.py +8 -0
- numanlib-0.1.0/src/numanlib/core/__init__.py +2 -0
- numanlib-0.1.0/src/numanlib/core/formatting.py +18 -0
- numanlib-0.1.0/src/numanlib/core/newton_polynomial.py +56 -0
- numanlib-0.1.0/src/numanlib/core/polynomial.py +248 -0
- numanlib-0.1.0/src/numanlib/differentiation/__init__.py +23 -0
- numanlib-0.1.0/src/numanlib/differentiation/finite_differences.py +190 -0
- numanlib-0.1.0/src/numanlib/integration/__init__.py +33 -0
- numanlib-0.1.0/src/numanlib/integration/composite.py +150 -0
- numanlib-0.1.0/src/numanlib/integration/newton_cotes.py +229 -0
- numanlib-0.1.0/src/numanlib/interpolation/__init__.py +13 -0
- numanlib-0.1.0/src/numanlib/interpolation/cubic_spline.py +145 -0
- numanlib-0.1.0/src/numanlib/interpolation/divided_differences.py +113 -0
- numanlib-0.1.0/src/numanlib/interpolation/hermite.py +161 -0
- numanlib-0.1.0/src/numanlib/interpolation/lagrange.py +43 -0
- numanlib-0.1.0/src/numanlib/interpolation/neville.py +111 -0
- numanlib-0.1.0/src/numanlib/linear_systems/__init__.py +30 -0
- numanlib-0.1.0/src/numanlib/linear_systems/base.py +207 -0
- numanlib-0.1.0/src/numanlib/linear_systems/cholesky_factorization.py +57 -0
- numanlib-0.1.0/src/numanlib/linear_systems/cholesky_solver.py +49 -0
- numanlib-0.1.0/src/numanlib/linear_systems/crout_tridiagonal.py +74 -0
- numanlib-0.1.0/src/numanlib/linear_systems/gauss_seidel.py +38 -0
- numanlib-0.1.0/src/numanlib/linear_systems/gaussian_elimination.py +124 -0
- numanlib-0.1.0/src/numanlib/linear_systems/jacobi.py +38 -0
- numanlib-0.1.0/src/numanlib/linear_systems/ldlt_factorization.py +33 -0
- numanlib-0.1.0/src/numanlib/linear_systems/ldlt_solver.py +52 -0
- numanlib-0.1.0/src/numanlib/linear_systems/lu_factorization.py +69 -0
- numanlib-0.1.0/src/numanlib/linear_systems/lu_solver.py +64 -0
- numanlib-0.1.0/src/numanlib/linear_systems/operations.py +46 -0
- numanlib-0.1.0/src/numanlib/linear_systems/partial_pivoting.py +53 -0
- numanlib-0.1.0/src/numanlib/linear_systems/scaled_partial_pivoting.py +56 -0
- numanlib-0.1.0/src/numanlib/linear_systems/sor.py +65 -0
- numanlib-0.1.0/src/numanlib/linear_systems/stationary.py +68 -0
- numanlib-0.1.0/src/numanlib/linear_systems/substitution.py +22 -0
- numanlib-0.1.0/src/numanlib/py.typed +0 -0
- numanlib-0.1.0/src/numanlib/root_finding/__init__.py +8 -0
- numanlib-0.1.0/src/numanlib/root_finding/base.py +62 -0
- numanlib-0.1.0/src/numanlib/root_finding/bisection.py +105 -0
- numanlib-0.1.0/src/numanlib/root_finding/fixed_point.py +59 -0
- numanlib-0.1.0/src/numanlib/root_finding/muller.py +106 -0
- numanlib-0.1.0/src/numanlib/root_finding/newton.py +115 -0
- numanlib-0.1.0/src/numanlib/root_finding/results.py +74 -0
- numanlib-0.1.0/src/numanlib/root_finding/secant.py +74 -0
- numanlib-0.1.0/src/numanlib/visualization/__init__.py +13 -0
- numanlib-0.1.0/src/numanlib/visualization/base.py +24 -0
- numanlib-0.1.0/src/numanlib/visualization/differentiation.py +75 -0
- numanlib-0.1.0/src/numanlib/visualization/integration.py +131 -0
- numanlib-0.1.0/src/numanlib/visualization/interpolant.py +60 -0
- numanlib-0.1.0/src/numanlib/visualization/solver.py +129 -0
- numanlib-0.1.0/src/numanlib/visualization/style.py +16 -0
numanlib-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: numanlib
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Add your description here
|
|
5
|
+
Author: Fabio Nicotra
|
|
6
|
+
Author-email: Fabio Nicotra <nicotra.fabio@icloud.com>
|
|
7
|
+
Requires-Dist: matplotlib>=3.5
|
|
8
|
+
Requires-Dist: numpy>=1.26
|
|
9
|
+
Requires-Dist: pandas>=1.5
|
|
10
|
+
Requires-Python: >=3.14
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
|
numanlib-0.1.0/README.md
ADDED
|
File without changes
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "numanlib"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Add your description here"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
authors = [
|
|
7
|
+
{ name = "Fabio Nicotra", email = "nicotra.fabio@icloud.com" }
|
|
8
|
+
]
|
|
9
|
+
requires-python = ">=3.14"
|
|
10
|
+
dependencies = [
|
|
11
|
+
"matplotlib>=3.5",
|
|
12
|
+
"numpy>=1.26",
|
|
13
|
+
"pandas>=1.5",
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
[build-system]
|
|
17
|
+
requires = ["uv_build>=0.10.2,<0.11.0"]
|
|
18
|
+
build-backend = "uv_build"
|
|
19
|
+
|
|
20
|
+
[dependency-groups]
|
|
21
|
+
dev = [
|
|
22
|
+
"pytest>=9.0.2",
|
|
23
|
+
"pytest-cov>=7.0.0",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
[tool.pytest.ini_options]
|
|
27
|
+
testpaths = ["tests"]
|
|
28
|
+
|
|
29
|
+
[tool.hatch.build.targets.wheel]
|
|
30
|
+
packages = ["src/numanlib"]
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""Text helpers shared by the stencil-style result objects
|
|
2
|
+
(FiniteDifferenceResult, NewtonCotesResult, CompositeNewtonCotesResult)
|
|
3
|
+
for rendering formula() and error_term() strings."""
|
|
4
|
+
from fractions import Fraction
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def derivative_notation(derivative_order: int) -> str:
|
|
8
|
+
if derivative_order <= 3:
|
|
9
|
+
return "'" * derivative_order
|
|
10
|
+
return f"^({derivative_order})"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def signed_term(coeff: Fraction, expr: str) -> str:
|
|
14
|
+
if coeff == 1:
|
|
15
|
+
return expr
|
|
16
|
+
if coeff == -1:
|
|
17
|
+
return f"-{expr}"
|
|
18
|
+
return f"{coeff}*{expr}"
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from typing import SupportsFloat
|
|
4
|
+
from numpy.typing import NDArray
|
|
5
|
+
from .polynomial import Polynomial
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass(frozen=True)
|
|
9
|
+
class NewtonPolynomial:
|
|
10
|
+
nodes: NDArray[np.float64] # x_0, ..., x_{n-1} (n nodes, one per non-constant term)
|
|
11
|
+
coeffs: NDArray[np.float64] # a_0, ..., a_n (n+1 divided-difference coefficients)
|
|
12
|
+
|
|
13
|
+
def __post_init__(self):
|
|
14
|
+
object.__setattr__(self, 'nodes', np.asarray(self.nodes, dtype=float))
|
|
15
|
+
object.__setattr__(self, 'coeffs', np.asarray(self.coeffs, dtype=float))
|
|
16
|
+
if self.nodes.ndim != 1 or self.coeffs.ndim != 1:
|
|
17
|
+
raise ValueError("nodes and coeffs must be 1-D arrays.")
|
|
18
|
+
if len(self.coeffs) != len(self.nodes) + 1:
|
|
19
|
+
raise ValueError(
|
|
20
|
+
f"len(coeffs) must equal len(nodes) + 1, "
|
|
21
|
+
f"got {len(self.coeffs)} and {len(self.nodes)}."
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
@property
|
|
25
|
+
def degree(self) -> int:
|
|
26
|
+
return len(self.coeffs) - 1
|
|
27
|
+
|
|
28
|
+
def _evaluate(self, x: float) -> float:
|
|
29
|
+
result = self.coeffs[self.degree]
|
|
30
|
+
for i in range(self.degree - 1, -1, -1):
|
|
31
|
+
result = self.coeffs[i] + (x - self.nodes[i]) * result
|
|
32
|
+
return result
|
|
33
|
+
|
|
34
|
+
def __call__(self, x):
|
|
35
|
+
if isinstance(x, (list, tuple, np.ndarray)):
|
|
36
|
+
return np.array([self._evaluate(float(v)) for v in x], dtype=float)
|
|
37
|
+
return self._evaluate(float(x))
|
|
38
|
+
|
|
39
|
+
def to_polynomial(self) -> 'Polynomial':
|
|
40
|
+
result = Polynomial([self.coeffs[0]])
|
|
41
|
+
node_poly = Polynomial([1.0])
|
|
42
|
+
for k in range(1, self.degree + 1):
|
|
43
|
+
node_poly = node_poly * Polynomial([1.0, -self.nodes[k - 1]])
|
|
44
|
+
result = result + self.coeffs[k] * node_poly
|
|
45
|
+
return result
|
|
46
|
+
|
|
47
|
+
def __str__(self) -> str:
|
|
48
|
+
if self.degree == 0:
|
|
49
|
+
return f"{self.coeffs[0]:.6g}"
|
|
50
|
+
parts = [f"{self.coeffs[0]:.6g}"]
|
|
51
|
+
for k in range(1, self.degree + 1):
|
|
52
|
+
node_str = "·".join(f"(x - {self.nodes[j]:.6g})" for j in range(k))
|
|
53
|
+
coeff = self.coeffs[k]
|
|
54
|
+
sign = "+" if coeff >= 0 else "-"
|
|
55
|
+
parts.append(f"{sign} {abs(coeff):.6g}·{node_str}")
|
|
56
|
+
return " ".join(parts)
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
import warnings
|
|
2
|
+
import numpy as np
|
|
3
|
+
from typing import SupportsFloat
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
|
|
6
|
+
@dataclass(frozen=True)
|
|
7
|
+
class Polynomial:
|
|
8
|
+
coeffs: np.ndarray # [a_n, a_{n-1}, ..., a_0]
|
|
9
|
+
degree: int = 0 # Default value
|
|
10
|
+
|
|
11
|
+
def __post_init__(self):
|
|
12
|
+
object.__setattr__(self, 'coeffs', np.array(self.coeffs, dtype=float))
|
|
13
|
+
# Calculate degree based on the length of the coefficients
|
|
14
|
+
object.__setattr__(self, 'degree', len(self.coeffs) - 1)
|
|
15
|
+
|
|
16
|
+
def __str__(self):
|
|
17
|
+
monomials = []
|
|
18
|
+
for degree, coeff in enumerate(reversed(self.coeffs)):
|
|
19
|
+
if coeff == 0:
|
|
20
|
+
continue
|
|
21
|
+
if degree == 0:
|
|
22
|
+
monomials.append(f"{coeff:.2g}")
|
|
23
|
+
elif degree == 1 and coeff == 1:
|
|
24
|
+
monomials.append("x")
|
|
25
|
+
elif degree == 1 and coeff == -1:
|
|
26
|
+
monomials.append("-x")
|
|
27
|
+
elif degree == 1:
|
|
28
|
+
monomials.append(f"{coeff:.2g}·x")
|
|
29
|
+
elif coeff == 1:
|
|
30
|
+
monomials.append(f"x^{degree}")
|
|
31
|
+
elif coeff == -1:
|
|
32
|
+
monomials.append(f"-x^{degree}")
|
|
33
|
+
else:
|
|
34
|
+
monomials.append(f"{coeff:.2g}·x^{degree}")
|
|
35
|
+
|
|
36
|
+
return " + ".join(reversed(monomials)).replace("+ -", "- ")
|
|
37
|
+
|
|
38
|
+
def __add__(self, other):
|
|
39
|
+
if isinstance(other, (int, float, complex)):
|
|
40
|
+
other = Polynomial([other])
|
|
41
|
+
if not isinstance(other, Polynomial):
|
|
42
|
+
return NotImplemented
|
|
43
|
+
max_degree = max(self.degree, other.degree)
|
|
44
|
+
new_coeffs = np.zeros(max_degree + 1)
|
|
45
|
+
new_coeffs[-len(self.coeffs):] += self.coeffs
|
|
46
|
+
new_coeffs[-len(other.coeffs):] += other.coeffs
|
|
47
|
+
return Polynomial(new_coeffs)
|
|
48
|
+
|
|
49
|
+
def __radd__(self, other):
|
|
50
|
+
return self.__add__(other)
|
|
51
|
+
|
|
52
|
+
def __mul__(self, other):
|
|
53
|
+
if isinstance(other, Polynomial):
|
|
54
|
+
return Polynomial(np.convolve(self.coeffs, other.coeffs))
|
|
55
|
+
if isinstance(other, (int, float, complex)):
|
|
56
|
+
return Polynomial(self.coeffs * other)
|
|
57
|
+
return NotImplemented
|
|
58
|
+
|
|
59
|
+
def __rmul__(self, other):
|
|
60
|
+
return self.__mul__(other)
|
|
61
|
+
|
|
62
|
+
def __pow__(self, n: int) -> 'Polynomial':
|
|
63
|
+
if not isinstance(n, int) or n < 0:
|
|
64
|
+
return NotImplemented
|
|
65
|
+
result = Polynomial([1.0])
|
|
66
|
+
base = self
|
|
67
|
+
while n > 0:
|
|
68
|
+
if n % 2 == 1:
|
|
69
|
+
result = result * base
|
|
70
|
+
base = base * base
|
|
71
|
+
n //= 2
|
|
72
|
+
return result
|
|
73
|
+
|
|
74
|
+
def __sub__(self, other):
|
|
75
|
+
if not isinstance(other, Polynomial):
|
|
76
|
+
return NotImplemented
|
|
77
|
+
return self + (-other)
|
|
78
|
+
|
|
79
|
+
def __neg__(self):
|
|
80
|
+
return Polynomial(-self.coeffs)
|
|
81
|
+
|
|
82
|
+
def __truediv__(self, other):
|
|
83
|
+
if isinstance(other, (int, float, complex)):
|
|
84
|
+
if other == 0:
|
|
85
|
+
raise ZeroDivisionError("Cannot divide by zero.")
|
|
86
|
+
return 1/other * self
|
|
87
|
+
if isinstance(other, Polynomial):
|
|
88
|
+
return NotImplemented
|
|
89
|
+
return NotImplemented
|
|
90
|
+
|
|
91
|
+
def __eq__(self, other):
|
|
92
|
+
if not isinstance(other, Polynomial):
|
|
93
|
+
return NotImplemented
|
|
94
|
+
return np.array_equal(self.coeffs, other.coeffs)
|
|
95
|
+
|
|
96
|
+
def __hash__(self):
|
|
97
|
+
return hash(tuple(self.coeffs))
|
|
98
|
+
|
|
99
|
+
def from_roots(roots, multiplicities: np.ndarray = None) -> 'Polynomial':
|
|
100
|
+
roots = np.asarray(list(roots) if not hasattr(roots, '__len__') else roots)
|
|
101
|
+
if multiplicities is None:
|
|
102
|
+
multiplicities = np.ones(len(roots), dtype=int)
|
|
103
|
+
elif len(roots) != len(multiplicities):
|
|
104
|
+
raise ValueError("Length of multiplicities must match length of roots.")
|
|
105
|
+
res = Polynomial([1])
|
|
106
|
+
for r, m in zip(roots, multiplicities):
|
|
107
|
+
res *= Polynomial([1, -r])**m
|
|
108
|
+
return res
|
|
109
|
+
|
|
110
|
+
def horner(self, x0: float) -> 'HornerResult':
|
|
111
|
+
n = self.degree
|
|
112
|
+
q_coeffs = np.zeros(n) # Coefficients of Q(x)
|
|
113
|
+
|
|
114
|
+
q_coeffs[0] = self.coeffs[0]
|
|
115
|
+
p_deriv = q_coeffs[0]
|
|
116
|
+
for k in range(1, n):
|
|
117
|
+
q_coeffs[k] = self.coeffs[k] + x0 * q_coeffs[k-1]
|
|
118
|
+
p_deriv = q_coeffs[k] + x0 * p_deriv
|
|
119
|
+
|
|
120
|
+
p_val = self.coeffs[n] + x0 * q_coeffs[n-1]
|
|
121
|
+
|
|
122
|
+
return HornerResult(
|
|
123
|
+
value=p_val,
|
|
124
|
+
derivative=p_deriv,
|
|
125
|
+
quotient=Polynomial(q_coeffs)
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
def _evaluate(self, x: SupportsFloat):
|
|
129
|
+
p_val = self.coeffs[0]
|
|
130
|
+
for i in range(1, self.degree + 1):
|
|
131
|
+
p_val = self.coeffs[i] + x * p_val
|
|
132
|
+
return p_val
|
|
133
|
+
|
|
134
|
+
def __call__(self, x):
|
|
135
|
+
if isinstance(x, (list, tuple, np.ndarray)):
|
|
136
|
+
# Create a numpy array from the evaluated points
|
|
137
|
+
return np.array([self._evaluate(val) for val in x], dtype=float)
|
|
138
|
+
# Otherwise, treat as scalar
|
|
139
|
+
return self._evaluate(x)
|
|
140
|
+
|
|
141
|
+
def deflate(self, root: SupportsFloat):
|
|
142
|
+
return self.horner(root).quotient
|
|
143
|
+
|
|
144
|
+
def deflate_quadratic(self, r: complex) -> 'Polynomial':
|
|
145
|
+
"""Deflate by (x - r)(x - r̄) = x² - 2·Re(r)·x + |r|², keeping real coefficients."""
|
|
146
|
+
p = -2 * r.real
|
|
147
|
+
q = r.real**2 + r.imag**2
|
|
148
|
+
a = self.coeffs
|
|
149
|
+
n = self.degree
|
|
150
|
+
b = np.zeros(n - 1)
|
|
151
|
+
b[0] = a[0]
|
|
152
|
+
b[1] = a[1] - p * b[0]
|
|
153
|
+
for k in range(2, n - 1):
|
|
154
|
+
b[k] = a[k] - p * b[k-1] - q * b[k-2]
|
|
155
|
+
return Polynomial(b)
|
|
156
|
+
|
|
157
|
+
def solve(self, x_hat: SupportsFloat, polish: bool = True, use_muller: bool = False) -> np.ndarray:
|
|
158
|
+
from ..root_finding import Newton, Muller
|
|
159
|
+
newton_solver = Newton()
|
|
160
|
+
muller_solver = Muller() if use_muller else None
|
|
161
|
+
roots = []
|
|
162
|
+
current_poly = self
|
|
163
|
+
|
|
164
|
+
while current_poly.degree > 2:
|
|
165
|
+
if use_muller:
|
|
166
|
+
x = float(x_hat)
|
|
167
|
+
h = max(0.5, abs(x) * 0.1)
|
|
168
|
+
result = muller_solver.solve(current_poly, p0=x - h, p1=x, p2=x + h)
|
|
169
|
+
if not result.converged:
|
|
170
|
+
warnings.warn(
|
|
171
|
+
f"Müller's method failed during deflation; returning {len(roots)} of {self.degree} roots.",
|
|
172
|
+
RuntimeWarning
|
|
173
|
+
)
|
|
174
|
+
return np.array(roots)
|
|
175
|
+
if result.complex_root is not None:
|
|
176
|
+
r = result.complex_root
|
|
177
|
+
roots.extend([r, r.conjugate()])
|
|
178
|
+
current_poly = current_poly.deflate_quadratic(r)
|
|
179
|
+
x_hat = r.real
|
|
180
|
+
else:
|
|
181
|
+
roots.append(result.root)
|
|
182
|
+
current_poly = current_poly.deflate(result.root)
|
|
183
|
+
x_hat = result.root
|
|
184
|
+
else:
|
|
185
|
+
root = newton_solver.solve(current_poly, x_hat).root
|
|
186
|
+
if root is None:
|
|
187
|
+
warnings.warn(
|
|
188
|
+
f"Newton's method failed during deflation; returning {len(roots)} of {self.degree} roots.",
|
|
189
|
+
RuntimeWarning
|
|
190
|
+
)
|
|
191
|
+
return np.array(roots, dtype=float)
|
|
192
|
+
roots.append(root)
|
|
193
|
+
current_poly = current_poly.deflate(root)
|
|
194
|
+
x_hat = root
|
|
195
|
+
|
|
196
|
+
if current_poly.degree == 2:
|
|
197
|
+
roots.extend(current_poly.solve_quadratic())
|
|
198
|
+
elif current_poly.degree == 1:
|
|
199
|
+
roots.append(-current_poly.coeffs[1] / current_poly.coeffs[0])
|
|
200
|
+
|
|
201
|
+
if polish:
|
|
202
|
+
polished = []
|
|
203
|
+
for r in roots:
|
|
204
|
+
if np.iscomplex(r):
|
|
205
|
+
polished.append(r)
|
|
206
|
+
continue
|
|
207
|
+
result = newton_solver.solve(self, r)
|
|
208
|
+
if result.root is None:
|
|
209
|
+
warnings.warn(
|
|
210
|
+
f"Newton's method failed while polishing root {r}; keeping unpolished value.",
|
|
211
|
+
RuntimeWarning
|
|
212
|
+
)
|
|
213
|
+
polished.append(r)
|
|
214
|
+
else:
|
|
215
|
+
polished.append(result.root)
|
|
216
|
+
roots = polished
|
|
217
|
+
|
|
218
|
+
return np.array(sorted(roots, key=lambda r: (complex(r).real, complex(r).imag)))
|
|
219
|
+
|
|
220
|
+
def solve_quadratic(self) -> np.ndarray:
|
|
221
|
+
if self.degree != 2:
|
|
222
|
+
raise ValueError("The polynomial must be of degree 2 to use the quadratic formula.")
|
|
223
|
+
a, b, c = self.coeffs
|
|
224
|
+
discriminant = b**2 - 4*a*c
|
|
225
|
+
sqrt_d = np.sqrt(complex(discriminant)) if discriminant < 0 else np.sqrt(discriminant)
|
|
226
|
+
return np.array([(-b + sqrt_d) / (2*a), (-b - sqrt_d) / (2*a)])
|
|
227
|
+
|
|
228
|
+
def derivative(self) -> 'Polynomial':
|
|
229
|
+
n = self.degree
|
|
230
|
+
coeffs = [self.coeffs[i] * (n - i) for i in range(n)]
|
|
231
|
+
return Polynomial(coeffs)
|
|
232
|
+
|
|
233
|
+
def integral(self, constant: float = 0.0) -> 'Polynomial':
|
|
234
|
+
n = self.degree
|
|
235
|
+
coeffs = [self.coeffs[i] / (n - i + 1) for i in range(n + 1)]
|
|
236
|
+
coeffs.append(constant)
|
|
237
|
+
return Polynomial(coeffs)
|
|
238
|
+
|
|
239
|
+
def integrate(self, a: SupportsFloat, b: SupportsFloat) -> float:
|
|
240
|
+
antiderivative = self.integral()
|
|
241
|
+
return antiderivative(b) - antiderivative(a)
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
@dataclass(frozen=True)
|
|
245
|
+
class HornerResult:
|
|
246
|
+
value: float
|
|
247
|
+
derivative: float
|
|
248
|
+
quotient: Polynomial
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from .finite_differences import (
|
|
2
|
+
FiniteDifferenceResult,
|
|
3
|
+
finite_difference,
|
|
4
|
+
FORWARD_DIFFERENCE,
|
|
5
|
+
BACKWARD_DIFFERENCE,
|
|
6
|
+
THREE_POINT_MIDPOINT,
|
|
7
|
+
THREE_POINT_ENDPOINT,
|
|
8
|
+
FIVE_POINT_MIDPOINT,
|
|
9
|
+
FIVE_POINT_ENDPOINT,
|
|
10
|
+
SECOND_DERIVATIVE_MIDPOINT,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"FiniteDifferenceResult",
|
|
15
|
+
"finite_difference",
|
|
16
|
+
"FORWARD_DIFFERENCE",
|
|
17
|
+
"BACKWARD_DIFFERENCE",
|
|
18
|
+
"THREE_POINT_MIDPOINT",
|
|
19
|
+
"THREE_POINT_ENDPOINT",
|
|
20
|
+
"FIVE_POINT_MIDPOINT",
|
|
21
|
+
"FIVE_POINT_ENDPOINT",
|
|
22
|
+
"SECOND_DERIVATIVE_MIDPOINT",
|
|
23
|
+
]
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
from dataclasses import dataclass, field
|
|
2
|
+
from fractions import Fraction
|
|
3
|
+
from math import factorial
|
|
4
|
+
from typing import Any, Callable, Dict, List, SupportsFloat, Tuple
|
|
5
|
+
import numpy as np
|
|
6
|
+
import pandas as pd
|
|
7
|
+
from numpy.typing import NDArray
|
|
8
|
+
|
|
9
|
+
from ..core.formatting import derivative_notation, signed_term
|
|
10
|
+
from ..interpolation import lagrange
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _node_label(offset: int) -> str:
|
|
14
|
+
if offset == 0:
|
|
15
|
+
return "x0"
|
|
16
|
+
if abs(offset) == 1:
|
|
17
|
+
return "x0+h" if offset > 0 else "x0-h"
|
|
18
|
+
return f"x0{offset:+d}h"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _rational_coefficients(
|
|
22
|
+
weights: NDArray[np.float64], h: float, derivative_order: int
|
|
23
|
+
) -> List[Fraction]:
|
|
24
|
+
return [Fraction(w * h**derivative_order).limit_denominator(10_000) for w in weights]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _error_term(
|
|
28
|
+
coeffs: List[Fraction], offsets: NDArray, derivative_order: int
|
|
29
|
+
) -> Tuple[int, Fraction]:
|
|
30
|
+
n = len(offsets) - 1
|
|
31
|
+
j = n + 1
|
|
32
|
+
while j <= n + 6:
|
|
33
|
+
leak = sum(c * Fraction(int(o)) ** j for c, o in zip(coeffs, offsets))
|
|
34
|
+
if leak != 0:
|
|
35
|
+
return j - derivative_order, leak / factorial(j)
|
|
36
|
+
j += 1
|
|
37
|
+
raise RuntimeError("Could not determine the truncation-error term for this stencil.")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
FORWARD_DIFFERENCE = np.array([0, 1])
|
|
41
|
+
BACKWARD_DIFFERENCE = np.array([-1, 0])
|
|
42
|
+
THREE_POINT_MIDPOINT = np.array([-1, 0, 1])
|
|
43
|
+
THREE_POINT_ENDPOINT = np.array([0, 1, 2])
|
|
44
|
+
FIVE_POINT_MIDPOINT = np.array([-2, -1, 0, 1, 2])
|
|
45
|
+
FIVE_POINT_ENDPOINT = np.array([0, 1, 2, 3, 4])
|
|
46
|
+
SECOND_DERIVATIVE_MIDPOINT = np.array([-1, 0, 1])
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@dataclass(frozen=True)
|
|
50
|
+
class FiniteDifferenceResult:
|
|
51
|
+
value: float
|
|
52
|
+
x0: float
|
|
53
|
+
h: float
|
|
54
|
+
derivative_order: int
|
|
55
|
+
nodes: NDArray[np.float64]
|
|
56
|
+
f_values: NDArray[np.float64]
|
|
57
|
+
weights: NDArray[np.float64]
|
|
58
|
+
error_order: int
|
|
59
|
+
error_coefficient: Fraction
|
|
60
|
+
method_name: str = "Finite Difference"
|
|
61
|
+
metadata: Dict[str, Any] = field(default_factory=dict)
|
|
62
|
+
|
|
63
|
+
def __repr__(self) -> str:
|
|
64
|
+
deriv = derivative_notation(self.derivative_order)
|
|
65
|
+
order_tag = "O(h)" if self.error_order == 1 else f"O(h^{self.error_order})"
|
|
66
|
+
return (
|
|
67
|
+
f"{self.method_name}: f{deriv}({self.x0}) ≈ {self.value} "
|
|
68
|
+
f"({len(self.nodes)} points, h={self.h}, {order_tag})\n"
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
def to_df(self) -> pd.DataFrame:
|
|
72
|
+
table = pd.DataFrame({
|
|
73
|
+
"node": self.nodes,
|
|
74
|
+
"f(node)": self.f_values,
|
|
75
|
+
"weight": self.weights,
|
|
76
|
+
})
|
|
77
|
+
table.index.name = "i"
|
|
78
|
+
return table
|
|
79
|
+
|
|
80
|
+
def _error_expr(self, coeff: Fraction) -> str:
|
|
81
|
+
k = self.derivative_order + self.error_order
|
|
82
|
+
deriv = derivative_notation(k)
|
|
83
|
+
h_term = "h" if self.error_order == 1 else f"h^{self.error_order}"
|
|
84
|
+
return signed_term(coeff, f"{h_term}*f{deriv}(ξ)")
|
|
85
|
+
|
|
86
|
+
def formula(self) -> str:
|
|
87
|
+
offsets = np.round((self.nodes - self.x0) / self.h).astype(int)
|
|
88
|
+
coeffs = _rational_coefficients(self.weights, self.h, self.derivative_order)
|
|
89
|
+
|
|
90
|
+
terms = [
|
|
91
|
+
signed_term(frac, f"f({_node_label(offset)})")
|
|
92
|
+
for frac, offset in zip(coeffs, offsets)
|
|
93
|
+
if frac != 0
|
|
94
|
+
]
|
|
95
|
+
rhs = " + ".join(terms).replace("+ -", "- ")
|
|
96
|
+
|
|
97
|
+
deriv = derivative_notation(self.derivative_order)
|
|
98
|
+
denom = "h" if self.derivative_order == 1 else f"h^{self.derivative_order}"
|
|
99
|
+
|
|
100
|
+
# exact = formula_value - error_coefficient * h^error_order * f^(k)(ξ)
|
|
101
|
+
error_part = self._error_expr(-self.error_coefficient)
|
|
102
|
+
return f"f{deriv}(x0) = (1/{denom}) * ({rhs}) + {error_part}".replace("+ -", "- ")
|
|
103
|
+
|
|
104
|
+
def error_term(self) -> str:
|
|
105
|
+
return f"error ≈ {self._error_expr(self.error_coefficient)}"
|
|
106
|
+
|
|
107
|
+
def error_bound(self, M: SupportsFloat) -> float:
|
|
108
|
+
return abs(float(self.error_coefficient)) * abs(self.h) ** self.error_order * float(M)
|
|
109
|
+
|
|
110
|
+
def _abs_coefficient_sum(self) -> Fraction:
|
|
111
|
+
coeffs = _rational_coefficients(self.weights, self.h, self.derivative_order)
|
|
112
|
+
return sum(abs(c) for c in coeffs)
|
|
113
|
+
|
|
114
|
+
def total_error_bound(self, M: SupportsFloat, epsilon: SupportsFloat) -> float:
|
|
115
|
+
S = float(self._abs_coefficient_sum())
|
|
116
|
+
round_off = float(epsilon) * S / abs(self.h) ** self.derivative_order
|
|
117
|
+
return round_off + self.error_bound(M)
|
|
118
|
+
|
|
119
|
+
def optimal_h(self, M: SupportsFloat, epsilon: SupportsFloat) -> float:
|
|
120
|
+
S = float(self._abs_coefficient_sum())
|
|
121
|
+
M, epsilon = float(M), float(epsilon)
|
|
122
|
+
numerator = self.derivative_order * epsilon * S
|
|
123
|
+
denominator = self.error_order * abs(float(self.error_coefficient)) * M
|
|
124
|
+
return (numerator / denominator) ** (1 / (self.derivative_order + self.error_order))
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def finite_difference(
|
|
128
|
+
f: Callable[[SupportsFloat], SupportsFloat],
|
|
129
|
+
x0: SupportsFloat,
|
|
130
|
+
h: SupportsFloat,
|
|
131
|
+
offsets: NDArray,
|
|
132
|
+
derivative_order: int = 1,
|
|
133
|
+
) -> 'FiniteDifferenceResult':
|
|
134
|
+
if not callable(f):
|
|
135
|
+
raise TypeError("f must be a callable function.")
|
|
136
|
+
try:
|
|
137
|
+
x0 = float(x0)
|
|
138
|
+
h = float(h)
|
|
139
|
+
except (TypeError, ValueError):
|
|
140
|
+
raise TypeError("x0 and h must be numeric values.")
|
|
141
|
+
if h == 0:
|
|
142
|
+
raise ValueError("h must be nonzero.")
|
|
143
|
+
if not isinstance(derivative_order, int) or derivative_order <= 0:
|
|
144
|
+
raise ValueError("derivative_order must be a positive integer.")
|
|
145
|
+
|
|
146
|
+
offsets = np.array(offsets, dtype=float)
|
|
147
|
+
if offsets.ndim != 1:
|
|
148
|
+
raise ValueError("offsets must be a 1-D array.")
|
|
149
|
+
if len(np.unique(offsets)) != len(offsets):
|
|
150
|
+
raise ValueError("offsets must be distinct.")
|
|
151
|
+
if len(offsets) <= derivative_order:
|
|
152
|
+
raise ValueError(
|
|
153
|
+
"offsets must contain more points than derivative_order to "
|
|
154
|
+
"produce a nonzero derivative."
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
nodes = x0 + h * offsets
|
|
158
|
+
f_values = np.array([f(node) for node in nodes], dtype=float)
|
|
159
|
+
|
|
160
|
+
# Interpolate in the offset variable s = (x - x0) / h rather than in x
|
|
161
|
+
# directly: the offsets are always small integers near the origin, so
|
|
162
|
+
# the monomial expansion inside lagrange() stays well-conditioned
|
|
163
|
+
# regardless of how large x0 or how small h is. Recover the x-derivative
|
|
164
|
+
# via the chain rule: d^n f/dx^n(x0) = (1/h^n) * d^n g/ds^n(0).
|
|
165
|
+
lag = lagrange(offsets, f_values)
|
|
166
|
+
polynomial = lag.polynomial
|
|
167
|
+
basis_polynomials = lag.metadata["basis_polynomials"]
|
|
168
|
+
for _ in range(derivative_order):
|
|
169
|
+
polynomial = polynomial.derivative()
|
|
170
|
+
basis_polynomials = [basis.derivative() for basis in basis_polynomials]
|
|
171
|
+
|
|
172
|
+
scale = 1.0 / h**derivative_order
|
|
173
|
+
weights = np.array([basis(0.0) for basis in basis_polynomials]) * scale
|
|
174
|
+
|
|
175
|
+
coeffs = _rational_coefficients(weights, h, derivative_order)
|
|
176
|
+
offsets_int = np.round(offsets).astype(int)
|
|
177
|
+
error_order, error_coefficient = _error_term(coeffs, offsets_int, derivative_order)
|
|
178
|
+
|
|
179
|
+
return FiniteDifferenceResult(
|
|
180
|
+
value=polynomial(0.0) * scale,
|
|
181
|
+
x0=x0,
|
|
182
|
+
h=h,
|
|
183
|
+
derivative_order=derivative_order,
|
|
184
|
+
nodes=nodes,
|
|
185
|
+
f_values=f_values,
|
|
186
|
+
weights=weights,
|
|
187
|
+
error_order=error_order,
|
|
188
|
+
error_coefficient=error_coefficient,
|
|
189
|
+
metadata={"function": f},
|
|
190
|
+
)
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
from .composite import (
|
|
2
|
+
CompositeNewtonCotesResult,
|
|
3
|
+
composite_newton_cotes,
|
|
4
|
+
)
|
|
5
|
+
from .newton_cotes import (
|
|
6
|
+
NewtonCotesRule,
|
|
7
|
+
NewtonCotesResult,
|
|
8
|
+
newton_cotes,
|
|
9
|
+
TRAPEZOIDAL,
|
|
10
|
+
SIMPSONS,
|
|
11
|
+
SIMPSONS_THREE_EIGHTHS,
|
|
12
|
+
BOOLES,
|
|
13
|
+
MIDPOINT,
|
|
14
|
+
OPEN_NEWTON_COTES_1,
|
|
15
|
+
OPEN_NEWTON_COTES_2,
|
|
16
|
+
OPEN_NEWTON_COTES_3,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
__all__ = [
|
|
20
|
+
"CompositeNewtonCotesResult",
|
|
21
|
+
"composite_newton_cotes",
|
|
22
|
+
"NewtonCotesRule",
|
|
23
|
+
"NewtonCotesResult",
|
|
24
|
+
"newton_cotes",
|
|
25
|
+
"TRAPEZOIDAL",
|
|
26
|
+
"SIMPSONS",
|
|
27
|
+
"SIMPSONS_THREE_EIGHTHS",
|
|
28
|
+
"BOOLES",
|
|
29
|
+
"MIDPOINT",
|
|
30
|
+
"OPEN_NEWTON_COTES_1",
|
|
31
|
+
"OPEN_NEWTON_COTES_2",
|
|
32
|
+
"OPEN_NEWTON_COTES_3",
|
|
33
|
+
]
|