numericalanalysisiiser 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,35 @@
1
+ """Numerical analysis methods that return pandas DataFrames.
2
+
3
+ from numericalanalysis import bisection, richardson, jacobi, lagrange
4
+ """
5
+
6
+ __version__ = "0.1.0"
7
+
8
+ from .lagrange import lagrange
9
+ from .newton_interpolation import newton_interpolation, divided_differences
10
+ from .hermite import hermite
11
+ from .natural_cubic_spline import natural_cubic_spline
12
+ from .finite_differences import finite_difference
13
+ from .richardson import richardson, richardson_triangle
14
+ from .trapezoid import trapezoid, composite_trapezoid
15
+ from .simpson import simpson, composite_simpson
16
+ from .romberg import romberg
17
+ from .gauss_legendre import gauss_legendre
18
+ from .bisection import bisection
19
+ from .fixed_point import fixed_point
20
+ from .newton import newton
21
+ from .secant import secant
22
+ from .jacobi import jacobi
23
+ from .gauss_seidel import gauss_seidel
24
+ from .sor import sor
25
+ from .tridiagonal_matrix import tridiagonal_matrix
26
+ from .euler import euler
27
+
28
+ __all__ = [
29
+ "lagrange", "newton_interpolation", "divided_differences", "hermite", "natural_cubic_spline",
30
+ "finite_difference", "richardson", "richardson_triangle",
31
+ "trapezoid", "composite_trapezoid", "simpson", "composite_simpson", "romberg", "gauss_legendre",
32
+ "bisection", "fixed_point", "newton", "secant",
33
+ "jacobi", "gauss_seidel", "sor", "tridiagonal_matrix",
34
+ "euler",
35
+ ]
@@ -0,0 +1,38 @@
1
+ import numpy as np
2
+ import pandas as pd
3
+
4
+
5
+ def bisection(f, a, b, tol=1e-10, max_iter=1000, p_star=None):
6
+ """Bisection method for f(x) = 0 on [a, b]; stops when (b - a) / 2 < tol.
7
+
8
+ p_star : optional exact root, adds error
9
+
10
+ Returns a DataFrame, one row per iteration: n, a, b, p, f(p), b-a, bound, f_evals [, error]
11
+ bound = (b0 - a0) / 2^(n+1). The root is the last p.
12
+ """
13
+ fa = f(a)
14
+ if fa * f(b) >= 0:
15
+ raise ValueError("f(a) and f(b) must have opposite signs")
16
+
17
+ a0, b0 = a, b
18
+ evals = 2
19
+ rows = []
20
+
21
+ for n in range(max_iter):
22
+ p = (a + b) / 2
23
+ fp = f(p)
24
+ evals += 1
25
+ rows.append([n, a, b, p, fp, b - a, (b0 - a0) / 2 ** (n + 1), evals])
26
+
27
+ if fp == 0 or (b - a) / 2 < tol:
28
+ break
29
+ if fa * fp < 0:
30
+ b = p
31
+ else:
32
+ a = p
33
+ fa = fp
34
+
35
+ table = pd.DataFrame(rows, columns=["n", "a", "b", "p", "f(p)", "b-a", "bound", "f_evals"])
36
+ if p_star is not None:
37
+ table["error"] = np.abs(table["p"] - p_star)
38
+ return table
@@ -0,0 +1,39 @@
1
+ import numpy as np
2
+ import pandas as pd
3
+
4
+
5
+ def euler(f, t0, T, y0, h, exact=None):
6
+ """Explicit Euler method for y' = f(t, y), y(t0) = y0 on [t0, T].
7
+
8
+ h : step size, or a list of step sizes
9
+ exact : optional exact solution y(t), adds exact and error
10
+
11
+ Returns a DataFrame (the step sizes stacked): h, t, y, stable [, exact, error]
12
+ stable is False when |y| exceeds 1e10; y is then nan from that step on.
13
+ """
14
+ tables = []
15
+
16
+ for step in np.atleast_1d(h):
17
+ N = int(round((T - t0) / step))
18
+ if abs(N * step - (T - t0)) > 1e-9:
19
+ raise ValueError("T - t0 must be a multiple of h")
20
+
21
+ t = t0 + step * np.arange(N + 1)
22
+ y = np.full(N + 1, np.nan)
23
+ y[0] = y0
24
+ stable = True
25
+
26
+ for i in range(N):
27
+ y[i + 1] = y[i] + step * f(t[i], y[i])
28
+ if not abs(y[i + 1]) < 1e10:
29
+ stable = False
30
+ y[i + 1:] = np.nan
31
+ break
32
+
33
+ table = pd.DataFrame({"h": step, "t": t, "y": y, "stable": stable})
34
+ if exact is not None:
35
+ table["exact"] = [exact(ti) for ti in t]
36
+ table["error"] = (table["exact"] - table["y"]).abs()
37
+ tables.append(table)
38
+
39
+ return pd.concat(tables, ignore_index=True)
@@ -0,0 +1,33 @@
1
+ import numpy as np
2
+ import pandas as pd
3
+
4
+ FORMULAS = {
5
+ "forward": lambda f, x, h: (f(x + h) - f(x)) / h,
6
+ "backward": lambda f, x, h: (f(x) - f(x - h)) / h,
7
+ "central": lambda f, x, h: (f(x + h) - f(x - h)) / (2 * h),
8
+ "second": lambda f, x, h: (f(x + h) - 2 * f(x) + f(x - h)) / h**2,
9
+ }
10
+
11
+
12
+ def finite_difference(f, x, h, exact=None, methods=("forward", "backward", "central")):
13
+ """Finite difference approximation of f'(x) ("second": f''(x)).
14
+
15
+ h : a step size or a list of step sizes
16
+ exact : optional true derivative, adds <method>_error and <method>_order
17
+ methods : any of "forward", "backward", "central", "second"
18
+
19
+ Returns a DataFrame: h, <method> [, <method>_error, <method>_order]
20
+ """
21
+ h = np.atleast_1d(np.asarray(h, dtype=float))
22
+ table = pd.DataFrame({"h": h})
23
+
24
+ for method in methods:
25
+ table[method] = [FORMULAS[method](f, x, step) for step in h]
26
+ if exact is not None:
27
+ error = np.abs(table[method].to_numpy() - exact)
28
+ with np.errstate(divide="ignore", invalid="ignore"):
29
+ order = np.log(error[:-1] / error[1:]) / np.log(h[:-1] / h[1:])
30
+ table[method + "_error"] = error
31
+ table[method + "_order"] = np.append(order, np.nan)
32
+
33
+ return table
@@ -0,0 +1,44 @@
1
+ import numpy as np
2
+ import pandas as pd
3
+
4
+
5
+ def fixed_point(g, p0, tol=1e-10, max_iter=1000, f=None, p_star=None):
6
+ """Fixed point iteration p_(n+1) = g(p_n); stops when |p_(n+1) - p_n| < tol.
7
+
8
+ f : optional function f(x) = 0, adds the residual f(p)
9
+ p_star : optional exact fixed point, adds error
10
+
11
+ Returns a DataFrame, one row per iterate: n, p, g(p), step, [f(p)], f_evals [, error]
12
+ The last row is the final iterate. The table stops early if g leaves the real numbers or
13
+ the values exceed 1e12.
14
+ """
15
+ p = p0
16
+ rows = []
17
+
18
+ for n in range(max_iter):
19
+ try:
20
+ p_next = g(p)
21
+ if isinstance(p_next, complex) or abs(p_next) > 1e12:
22
+ break
23
+ except (ValueError, OverflowError, ZeroDivisionError):
24
+ break
25
+
26
+ row = [n, p, p_next, abs(p_next - p)]
27
+ if f is not None:
28
+ row.append(f(p))
29
+ rows.append(row + [n + 1])
30
+
31
+ p = p_next
32
+ if rows[-1][3] < tol:
33
+ break
34
+
35
+ final = [len(rows), p, np.nan, np.nan]
36
+ if f is not None:
37
+ final.append(f(p))
38
+ rows.append(final + [len(rows)])
39
+
40
+ columns = ["n", "p", "g(p)", "step"] + (["f(p)"] if f is not None else []) + ["f_evals"]
41
+ table = pd.DataFrame(rows, columns=columns)
42
+ if p_star is not None:
43
+ table["error"] = np.abs(table["p"] - p_star)
44
+ return table
@@ -0,0 +1,27 @@
1
+ import numpy as np
2
+ import pandas as pd
3
+
4
+
5
+ def gauss_legendre(f, a, b, n=2, exact=None):
6
+ """Gauss-Legendre quadrature for the integral of f over [a, b].
7
+
8
+ n : number of Gauss points, or a list of them
9
+ exact : optional true value, adds error
10
+
11
+ Returns a DataFrame: n, value [, error]
12
+ """
13
+ n = np.atleast_1d(n)
14
+ values = []
15
+
16
+ for k in n:
17
+ points, weights = np.polynomial.legendre.leggauss(int(k))
18
+ total = 0
19
+ for i in range(k):
20
+ x = (a + b) / 2 + ((b - a) / 2) * points[i]
21
+ total += weights[i] * f(x)
22
+ values.append(((b - a) / 2) * total)
23
+
24
+ table = pd.DataFrame({"n": n, "value": values})
25
+ if exact is not None:
26
+ table["error"] = np.abs(table["value"].to_numpy() - exact)
27
+ return table
@@ -0,0 +1,42 @@
1
+ import numpy as np
2
+ import pandas as pd
3
+
4
+
5
+ def gauss_seidel(A, b, x0=None, tol=1e-6, max_iter=100):
6
+ """Gauss-Seidel iteration for A x = b; stops when max|x_k - x_(k-1)| < tol.
7
+
8
+ x0 : starting vector (zeros by default)
9
+
10
+ Returns a DataFrame, one row per iteration (k = 0 is the starting vector):
11
+ k, step, residual, q, x1, x2, ...
12
+ step = max|x_k - x_(k-1)|, residual = max|b - A x_k|, q = residual_k / residual_(k-1).
13
+ The solution is the last row of x1, x2, ...
14
+ """
15
+ A = np.array(A, dtype=float)
16
+ b = np.array(b, dtype=float)
17
+ n = len(b)
18
+ x = np.zeros(n) if x0 is None else np.array(x0, dtype=float)
19
+
20
+ xs = [x]
21
+ step = [np.nan]
22
+ residual = [np.max(np.abs(b - A @ x))]
23
+
24
+ for k in range(1, max_iter + 1):
25
+ x_new = x.copy()
26
+ for i in range(n):
27
+ sum_off_diag = 0.0
28
+ for j in range(n):
29
+ if i != j:
30
+ sum_off_diag += A[i, j] * x_new[j]
31
+ x_new[i] = (b[i] - sum_off_diag) / A[i, i]
32
+
33
+ step.append(np.max(np.abs(x_new - x)))
34
+ x = x_new
35
+ xs.append(x)
36
+ residual.append(np.max(np.abs(b - A @ x)))
37
+ if step[-1] < tol:
38
+ break
39
+
40
+ table = pd.DataFrame({"k": range(len(xs)), "step": step, "residual": residual})
41
+ table["q"] = table["residual"] / table["residual"].shift(1)
42
+ return pd.concat([table, pd.DataFrame(xs, columns=[f"x{i + 1}" for i in range(n)])], axis=1)
@@ -0,0 +1,39 @@
1
+ import numpy as np
2
+ import pandas as pd
3
+
4
+
5
+ def hermite(x, y, dy, alpha, exact=None):
6
+ """Hermite polynomial matching the values y and slopes dy at the nodes x, evaluated at alpha.
7
+
8
+ alpha : a number or a list of points
9
+ exact : optional function f(x), adds the columns exact and error
10
+
11
+ Returns a DataFrame: alpha, p [, exact, error]
12
+ """
13
+ alpha = np.atleast_1d(np.asarray(alpha, dtype=float))
14
+ n = len(x)
15
+
16
+ a = np.repeat(x, 2)
17
+ M = np.zeros((2 * n, 2 * n))
18
+ M[:, 0] = np.repeat(y, 2)
19
+
20
+ for i in range(1, 2 * n):
21
+ if a[i] == a[i - 1]:
22
+ M[i, 1] = dy[i // 2]
23
+ else:
24
+ M[i, 1] = (M[i, 0] - M[i - 1, 0]) / (a[i] - a[i - 1])
25
+
26
+ for j in range(2, 2 * n):
27
+ for i in range(j, 2 * n):
28
+ M[i, j] = (M[i, j - 1] - M[i - 1, j - 1]) / (a[i] - a[i - j])
29
+
30
+ c = np.diag(M)
31
+ p = np.zeros(len(alpha))
32
+ for k in range(2 * n - 1, -1, -1):
33
+ p = c[k] + (alpha - a[k]) * p
34
+
35
+ table = pd.DataFrame({"alpha": alpha, "p": p})
36
+ if exact is not None:
37
+ table["exact"] = [exact(a) for a in alpha]
38
+ table["error"] = (table["exact"] - table["p"]).abs()
39
+ return table
@@ -0,0 +1,42 @@
1
+ import numpy as np
2
+ import pandas as pd
3
+
4
+
5
+ def jacobi(A, b, x0=None, tol=1e-6, max_iter=100):
6
+ """Jacobi iteration for A x = b; stops when max|x_k - x_(k-1)| < tol.
7
+
8
+ x0 : starting vector (zeros by default)
9
+
10
+ Returns a DataFrame, one row per iteration (k = 0 is the starting vector):
11
+ k, step, residual, q, x1, x2, ...
12
+ step = max|x_k - x_(k-1)|, residual = max|b - A x_k|, q = residual_k / residual_(k-1).
13
+ The solution is the last row of x1, x2, ...
14
+ """
15
+ A = np.array(A, dtype=float)
16
+ b = np.array(b, dtype=float)
17
+ n = len(b)
18
+ x = np.zeros(n) if x0 is None else np.array(x0, dtype=float)
19
+
20
+ xs = [x]
21
+ step = [np.nan]
22
+ residual = [np.max(np.abs(b - A @ x))]
23
+
24
+ for k in range(1, max_iter + 1):
25
+ x_new = np.zeros(n)
26
+ for i in range(n):
27
+ sum_off_diag = 0.0
28
+ for j in range(n):
29
+ if i != j:
30
+ sum_off_diag += A[i, j] * x[j]
31
+ x_new[i] = (b[i] - sum_off_diag) / A[i, i]
32
+
33
+ step.append(np.max(np.abs(x_new - x)))
34
+ x = x_new
35
+ xs.append(x)
36
+ residual.append(np.max(np.abs(b - A @ x)))
37
+ if step[-1] < tol:
38
+ break
39
+
40
+ table = pd.DataFrame({"k": range(len(xs)), "step": step, "residual": residual})
41
+ table["q"] = table["residual"] / table["residual"].shift(1)
42
+ return pd.concat([table, pd.DataFrame(xs, columns=[f"x{i + 1}" for i in range(n)])], axis=1)
@@ -0,0 +1,28 @@
1
+ import numpy as np
2
+ import pandas as pd
3
+
4
+
5
+ def lagrange(x, y, alpha, exact=None):
6
+ """Lagrange polynomial through the points (x, y), evaluated at alpha.
7
+
8
+ alpha : a number or a list of points
9
+ exact : optional function f(x), adds the columns exact and error
10
+
11
+ Returns a DataFrame: alpha, p [, exact, error]
12
+ """
13
+ alpha = np.atleast_1d(np.asarray(alpha, dtype=float))
14
+ n = len(x)
15
+ p = np.zeros(len(alpha))
16
+
17
+ for i in range(n):
18
+ l_i = np.ones(len(alpha))
19
+ for j in range(n):
20
+ if j != i:
21
+ l_i *= (alpha - x[j]) / (x[i] - x[j])
22
+ p += y[i] * l_i
23
+
24
+ table = pd.DataFrame({"alpha": alpha, "p": p})
25
+ if exact is not None:
26
+ table["exact"] = [exact(a) for a in alpha]
27
+ table["error"] = (table["exact"] - table["p"]).abs()
28
+ return table
@@ -0,0 +1,48 @@
1
+ import numpy as np
2
+ import pandas as pd
3
+
4
+
5
+ def natural_cubic_spline(x, y, alpha, exact=None):
6
+ """Natural cubic spline through the points (x, y), evaluated at alpha.
7
+
8
+ alpha : a number or a list of points
9
+ exact : optional function f(x), adds the columns exact and error
10
+
11
+ Returns a DataFrame: alpha, p [, exact, error]
12
+ """
13
+ x = np.array(x, dtype=float)
14
+ y = np.array(y, dtype=float)
15
+ alpha = np.atleast_1d(np.asarray(alpha, dtype=float))
16
+ n = len(x) - 1
17
+ h = np.diff(x)
18
+
19
+ A = np.zeros((n + 1, n + 1))
20
+ b = np.zeros(n + 1)
21
+ A[0, 0] = 1.0
22
+ A[n, n] = 1.0
23
+
24
+ for i in range(1, n):
25
+ A[i, i - 1] = h[i - 1]
26
+ A[i, i] = 2.0 * (h[i - 1] + h[i])
27
+ A[i, i + 1] = h[i]
28
+ b[i] = 6.0 * ((y[i + 1] - y[i]) / h[i] - (y[i] - y[i - 1]) / h[i - 1])
29
+
30
+ M = np.linalg.solve(A, b)
31
+
32
+ p = np.zeros(len(alpha))
33
+ for idx, t in enumerate(alpha):
34
+ i = int(np.clip(np.searchsorted(x, t) - 1, 0, n - 1))
35
+ dx_right = x[i + 1] - t
36
+ dx_left = t - x[i]
37
+ p[idx] = (
38
+ M[i] * dx_right**3 / (6.0 * h[i])
39
+ + M[i + 1] * dx_left**3 / (6.0 * h[i])
40
+ + (y[i] - M[i] * h[i]**2 / 6.0) * dx_right / h[i]
41
+ + (y[i + 1] - M[i + 1] * h[i]**2 / 6.0) * dx_left / h[i]
42
+ )
43
+
44
+ table = pd.DataFrame({"alpha": alpha, "p": p})
45
+ if exact is not None:
46
+ table["exact"] = [exact(a) for a in alpha]
47
+ table["error"] = (table["exact"] - table["p"]).abs()
48
+ return table
@@ -0,0 +1,36 @@
1
+ import numpy as np
2
+ import pandas as pd
3
+
4
+
5
+ def newton(f, df, p0, tol=1e-12, max_iter=100, p_star=None):
6
+ """Newton's method; stops when |p_(n+1) - p_n| < tol.
7
+
8
+ df : derivative f'(x)
9
+ p_star : optional exact root, adds error and q
10
+
11
+ Returns a DataFrame, one row per iterate: n, p, f(p), f'(p), step, f_evals, df_evals [, error, q]
12
+ The last row is the final iterate. q = log(e_(n+1)/e_n) / log(e_n/e_(n-1)) is the estimated order.
13
+ """
14
+ p = p0
15
+ rows = []
16
+
17
+ for n in range(max_iter):
18
+ fp, dfp = f(p), df(p)
19
+ p_next = p - fp / dfp
20
+ rows.append([n, p, fp, dfp, abs(p_next - p), n + 1, n + 1])
21
+ p = p_next
22
+ if rows[-1][4] < tol:
23
+ break
24
+
25
+ rows.append([len(rows), p, f(p), np.nan, np.nan, len(rows), len(rows)])
26
+ table = pd.DataFrame(rows, columns=["n", "p", "f(p)", "f'(p)", "step", "f_evals", "df_evals"])
27
+
28
+ if p_star is not None:
29
+ error = np.abs(table["p"].to_numpy() - p_star)
30
+ e = np.where(error < 1e-15, np.nan, error) # round-off sized errors are ignored
31
+ q = np.full(len(e), np.nan)
32
+ with np.errstate(divide="ignore", invalid="ignore"):
33
+ q[1:-1] = np.log(e[2:] / e[1:-1]) / np.log(e[1:-1] / e[:-2])
34
+ table["error"] = error
35
+ table["q"] = q
36
+ return table
@@ -0,0 +1,51 @@
1
+ import numpy as np
2
+ import pandas as pd
3
+
4
+
5
+ def divided_differences(x, y):
6
+ """Divided difference table of the points (x, y).
7
+
8
+ Returns a DataFrame: x, f[0], f[1], ... (the first row holds the Newton coefficients)
9
+ """
10
+ n = len(x)
11
+ columns = [list(y)]
12
+
13
+ for j in range(1, n):
14
+ col = []
15
+ for i in range(n - j):
16
+ col.append((columns[j - 1][i + 1] - columns[j - 1][i]) / (x[i + j] - x[i]))
17
+ columns.append(col + [np.nan] * j)
18
+
19
+ table = pd.DataFrame({"x": list(x)})
20
+ for j in range(n):
21
+ table[f"f[{j}]"] = columns[j]
22
+ return table
23
+
24
+
25
+ def newton_interpolation(x, y, alpha, exact=None):
26
+ """Newton form of the interpolation polynomial through the points (x, y), evaluated at alpha.
27
+
28
+ alpha : a number or a list of points
29
+ exact : optional function f(x), adds the columns exact and error
30
+
31
+ Returns a DataFrame: alpha, p [, exact, error]
32
+ """
33
+ alpha = np.atleast_1d(np.asarray(alpha, dtype=float))
34
+ n = len(x)
35
+
36
+ a = list(y)
37
+ for j in range(1, n):
38
+ for i in range(n - 1, j - 1, -1):
39
+ a[i] = (a[i] - a[i - 1]) / (x[i] - x[i - j])
40
+
41
+ p = a[0] * np.ones(len(alpha))
42
+ product = np.ones(len(alpha))
43
+ for i in range(1, n):
44
+ product = product * (alpha - x[i - 1])
45
+ p += a[i] * product
46
+
47
+ table = pd.DataFrame({"alpha": alpha, "p": p})
48
+ if exact is not None:
49
+ table["exact"] = [exact(a) for a in alpha]
50
+ table["error"] = (table["exact"] - table["p"]).abs()
51
+ return table
@@ -0,0 +1,68 @@
1
+ import numpy as np
2
+ import pandas as pd
3
+
4
+ from .finite_differences import FORMULAS
5
+
6
+
7
+ def richardson_triangle(first_column, p=2):
8
+ """Richardson table as a list of rows: R[k][j] = R[k][j-1] + (R[k][j-1] - R[k-1][j-1]) / (2^(p*j) - 1).
9
+
10
+ first_column[k] is computed with step h / 2^k.
11
+ p = 2 for central differences and the trapezoidal rule, p = 1 for forward / backward differences.
12
+ """
13
+ R = []
14
+ for k in range(len(first_column)):
15
+ row = [first_column[k]]
16
+ for j in range(1, k + 1):
17
+ row.append(row[j - 1] + (row[j - 1] - R[k - 1][j - 1]) / (2 ** (p * j) - 1))
18
+ R.append(row)
19
+ return R
20
+
21
+
22
+ def richardson(f, x, h, levels=1, method="central", exact=None, layout="by_h"):
23
+ """Richardson extrapolation of a finite difference formula at the point x.
24
+
25
+ method : "forward", "backward", "central" or "second"
26
+ exact : optional true derivative, adds R<j>_error and R<j>_order
27
+ layout : "by_h" h is a list of steps; R0 = D(h), R1 = (4 D(h/2) - D(h)) / 3, ...
28
+ "triangle" h is one step h0; row k uses h0 / 2^k and R[k][j] = R[k][j-1] + (R[k][j-1] - R[k-1][j-1]) / (4^j - 1)
29
+
30
+ Returns a DataFrame: h, R0, R1, ... (triangle: k, h, R0, R1, ...)
31
+ """
32
+ D = lambda step: FORMULAS[method](f, x, step)
33
+ p = 1 if method in ("forward", "backward") else 2
34
+
35
+ if layout == "by_h":
36
+ h = np.atleast_1d(np.asarray(h, dtype=float))
37
+ rows = []
38
+ for step in h:
39
+ values = [D(step / 2**m) for m in range(levels + 1)]
40
+ row = [values[0]]
41
+ for j in range(1, levels + 1):
42
+ factor = 2 ** (p * j)
43
+ values = [(factor * values[m + 1] - values[m]) / (factor - 1) for m in range(len(values) - 1)]
44
+ row.append(values[0])
45
+ rows.append(row)
46
+ table = pd.DataFrame(rows, columns=[f"R{j}" for j in range(levels + 1)])
47
+ table.insert(0, "h", h)
48
+ elif layout == "triangle":
49
+ steps = [h / 2**k for k in range(levels + 1)]
50
+ table = pd.DataFrame(richardson_triangle([D(step) for step in steps], p))
51
+ table.columns = [f"R{j}" for j in range(levels + 1)]
52
+ table.insert(0, "h", steps)
53
+ table.insert(0, "k", range(levels + 1))
54
+ else:
55
+ raise ValueError("layout must be 'by_h' or 'triangle'")
56
+
57
+ if exact is not None:
58
+ for j in range(levels + 1):
59
+ error = np.abs(table[f"R{j}"].to_numpy() - exact)
60
+ with np.errstate(divide="ignore", invalid="ignore"):
61
+ if layout == "by_h":
62
+ order = np.append(np.log(error[:-1] / error[1:]) / np.log(h[:-1] / h[1:]), np.nan)
63
+ else:
64
+ order = np.append(np.nan, np.log2(error[:-1] / error[1:]))
65
+ table[f"R{j}_error"] = error
66
+ table[f"R{j}_order"] = order
67
+
68
+ return table
@@ -0,0 +1,30 @@
1
+ import pandas as pd
2
+
3
+ from .richardson import richardson_triangle
4
+ from .trapezoid import composite_trapezoid
5
+
6
+
7
+ def romberg(f, a, b, levels=6, tol=None, exact=None):
8
+ """Romberg integration of f over [a, b].
9
+
10
+ levels : number of rows, row k uses n = 2^k sub-intervals
11
+ tol : if given, rows are added until |R[k][k] - R[k-1][k-1]| < tol
12
+ exact : optional true value, adds the error of the diagonal entries
13
+
14
+ Returns a DataFrame: n, R0, R1, ... [, error]. The last entry of the last row is the answer.
15
+ """
16
+ T = []
17
+ while True:
18
+ T.append(composite_trapezoid(f, a, b, 2 ** len(T)))
19
+ R = richardson_triangle(T)
20
+ if tol is None and len(T) >= levels:
21
+ break
22
+ if tol is not None and (len(T) >= 25 or (len(T) >= 2 and abs(R[-1][-1] - R[-2][-1]) < tol)):
23
+ break
24
+
25
+ table = pd.DataFrame(R)
26
+ table.columns = [f"R{j}" for j in range(len(R))]
27
+ table.insert(0, "n", [2**k for k in range(len(R))])
28
+ if exact is not None:
29
+ table["error"] = [abs(R[k][k] - exact) for k in range(len(R))]
30
+ return table
@@ -0,0 +1,42 @@
1
+ import numpy as np
2
+ import pandas as pd
3
+
4
+
5
+ def secant(f, p0, p1, tol=1e-12, max_iter=100, p_star=None):
6
+ """Secant method; stops when |p_(n+1) - p_n| < tol.
7
+
8
+ p_star : optional exact root, adds error and q
9
+
10
+ Returns a DataFrame, one row per iterate: n, p_prev, p, f(p), p_next, step, f_evals [, error, q]
11
+ The last row is the final iterate. q = log(e_(n+1)/e_n) / log(e_n/e_(n-1)) is the estimated order.
12
+ """
13
+ p_prev, p_curr = p0, p1
14
+ f_prev, f_curr = f(p0), f(p1)
15
+ evals = 2
16
+ rows = []
17
+
18
+ for n in range(1, max_iter + 1):
19
+ if f_curr == f_prev:
20
+ break
21
+ p_next = p_curr - f_curr * (p_curr - p_prev) / (f_curr - f_prev)
22
+ f_next = f(p_next)
23
+ evals += 1
24
+ rows.append([n, p_prev, p_curr, f_curr, p_next, abs(p_next - p_curr), evals])
25
+
26
+ p_prev, f_prev = p_curr, f_curr
27
+ p_curr, f_curr = p_next, f_next
28
+ if rows[-1][5] < tol:
29
+ break
30
+
31
+ rows.append([len(rows) + 1, p_prev, p_curr, f_curr, np.nan, np.nan, evals])
32
+ table = pd.DataFrame(rows, columns=["n", "p_prev", "p", "f(p)", "p_next", "step", "f_evals"])
33
+
34
+ if p_star is not None:
35
+ e_prev, e_curr, e_next = (np.abs(table[c].to_numpy() - p_star) for c in ("p_prev", "p", "p_next"))
36
+ mask = np.minimum(np.minimum(e_prev, e_curr), e_next) < 1e-15 # round-off sized errors are ignored
37
+ with np.errstate(divide="ignore", invalid="ignore"):
38
+ q = np.log(e_next / e_curr) / np.log(e_curr / e_prev)
39
+ q[mask] = np.nan
40
+ table["error"] = e_curr
41
+ table["q"] = q
42
+ return table