numericalanalysisiiser 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.
Files changed (30) hide show
  1. numericalanalysisiiser-0.1.0/LICENSE +21 -0
  2. numericalanalysisiiser-0.1.0/PKG-INFO +622 -0
  3. numericalanalysisiiser-0.1.0/README.md +601 -0
  4. numericalanalysisiiser-0.1.0/numericalanalysisiiser/__init__.py +35 -0
  5. numericalanalysisiiser-0.1.0/numericalanalysisiiser/bisection.py +38 -0
  6. numericalanalysisiiser-0.1.0/numericalanalysisiiser/euler.py +39 -0
  7. numericalanalysisiiser-0.1.0/numericalanalysisiiser/finite_differences.py +33 -0
  8. numericalanalysisiiser-0.1.0/numericalanalysisiiser/fixed_point.py +44 -0
  9. numericalanalysisiiser-0.1.0/numericalanalysisiiser/gauss_legendre.py +27 -0
  10. numericalanalysisiiser-0.1.0/numericalanalysisiiser/gauss_seidel.py +42 -0
  11. numericalanalysisiiser-0.1.0/numericalanalysisiiser/hermite.py +39 -0
  12. numericalanalysisiiser-0.1.0/numericalanalysisiiser/jacobi.py +42 -0
  13. numericalanalysisiiser-0.1.0/numericalanalysisiiser/lagrange.py +28 -0
  14. numericalanalysisiiser-0.1.0/numericalanalysisiiser/natural_cubic_spline.py +48 -0
  15. numericalanalysisiiser-0.1.0/numericalanalysisiiser/newton.py +36 -0
  16. numericalanalysisiiser-0.1.0/numericalanalysisiiser/newton_interpolation.py +51 -0
  17. numericalanalysisiiser-0.1.0/numericalanalysisiiser/richardson.py +68 -0
  18. numericalanalysisiiser-0.1.0/numericalanalysisiiser/romberg.py +30 -0
  19. numericalanalysisiiser-0.1.0/numericalanalysisiiser/secant.py +42 -0
  20. numericalanalysisiiser-0.1.0/numericalanalysisiiser/simpson.py +38 -0
  21. numericalanalysisiiser-0.1.0/numericalanalysisiiser/sor.py +42 -0
  22. numericalanalysisiiser-0.1.0/numericalanalysisiiser/trapezoid.py +33 -0
  23. numericalanalysisiiser-0.1.0/numericalanalysisiiser/tridiagonal_matrix.py +13 -0
  24. numericalanalysisiiser-0.1.0/numericalanalysisiiser.egg-info/PKG-INFO +622 -0
  25. numericalanalysisiiser-0.1.0/numericalanalysisiiser.egg-info/SOURCES.txt +28 -0
  26. numericalanalysisiiser-0.1.0/numericalanalysisiiser.egg-info/dependency_links.txt +1 -0
  27. numericalanalysisiiser-0.1.0/numericalanalysisiiser.egg-info/requires.txt +2 -0
  28. numericalanalysisiiser-0.1.0/numericalanalysisiiser.egg-info/top_level.txt +1 -0
  29. numericalanalysisiiser-0.1.0/pyproject.toml +32 -0
  30. numericalanalysisiiser-0.1.0/setup.cfg +4 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 jampanirohith
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,622 @@
1
+ Metadata-Version: 2.4
2
+ Name: numericalanalysisiiser
3
+ Version: 0.1.0
4
+ Summary: Numerical analysis methods (interpolation, differentiation, integration, root finding, linear systems, ODEs) that return pandas DataFrames
5
+ Author: jampanirohith
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/jampanirohith/numericalanalysis
8
+ Project-URL: Issues, https://github.com/jampanirohith/numericalanalysis/issues
9
+ Keywords: numerical analysis,interpolation,numerical integration,numerical differentiation,root finding,iterative methods,ode,pandas
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Intended Audience :: Education
12
+ Classifier: Intended Audience :: Science/Research
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Topic :: Scientific/Engineering :: Mathematics
15
+ Requires-Python: >=3.9
16
+ Description-Content-Type: text/markdown
17
+ License-File: LICENSE
18
+ Requires-Dist: numpy
19
+ Requires-Dist: pandas
20
+ Dynamic: license-file
21
+
22
+ # numericalanalysisiiser
23
+
24
+ Classical numerical analysis methods in plain Python: interpolation, numerical differentiation and
25
+ integration, root finding, iterative linear solvers and an ODE solver.
26
+
27
+ **Every method returns a pandas DataFrame.** The table has one row per iteration, step size or
28
+ evaluation point, so you can read the result, filter it, and plot any columns you like.
29
+
30
+ ```python
31
+ from numericalanalysisiiser import bisection
32
+ import matplotlib.pyplot as plt
33
+
34
+ f = lambda x: x**3 - x - 2
35
+ t = bisection(f, 1, 2, tol=1e-10, p_star=1.5213797068045676)
36
+
37
+ print(t["p"].iloc[-1]) # the root (last row)
38
+ plt.semilogy(t["n"], t["error"]) # any columns can be plotted
39
+ ```
40
+
41
+ ## Contents
42
+
43
+ * [Installation](#installation)
44
+ * [How the functions behave](#how-the-functions-behave)
45
+ * [Interpolation](#interpolation) - `lagrange`, `newton_interpolation`, `divided_differences`, `hermite`, `natural_cubic_spline`
46
+ * [Numerical differentiation](#numerical-differentiation) - `finite_difference`, `richardson`, `richardson_triangle`
47
+ * [Numerical integration](#numerical-integration) - `trapezoid`, `simpson`, `romberg`, `gauss_legendre`
48
+ * [Root finding](#root-finding) - `bisection`, `fixed_point`, `newton`, `secant`
49
+ * [Linear systems](#linear-systems) - `jacobi`, `gauss_seidel`, `sor`, `tridiagonal_matrix`
50
+ * [Ordinary differential equations](#ordinary-differential-equations) - `euler`
51
+ * [Recipes](#recipes)
52
+ * [Notes and limitations](#notes-and-limitations)
53
+ * [License](#license)
54
+
55
+ ## Installation
56
+
57
+ ```bash
58
+ pip install numericalanalysisiiser
59
+ ```
60
+
61
+ Requires Python 3.9 or newer, numpy and pandas (installed automatically). matplotlib is not
62
+ required; install it only if you want to plot the tables.
63
+
64
+ From a clone of the repository:
65
+
66
+ ```bash
67
+ cd pythonlibrary
68
+ pip install -e .
69
+ ```
70
+
71
+ ## How the functions behave
72
+
73
+ **The functions.** `f`, `g`, `df` and `exact` are ordinary Python functions of one number
74
+ (`f(x)`; ODE right-hand sides take two: `f(t, y)`). Functions from `math` and from `numpy` both work.
75
+
76
+ **Lists in, rows out.** Wherever a quantity can vary (`alpha`, `h`, `n`, `omega`), you may pass one
77
+ value or a list. You get one row per value.
78
+
79
+ **Optional exact values.** `exact=` (or `p_star=` for root finders) is the true value. When given,
80
+ the table gets `error` columns, and `order` (observed order of convergence) or `q` (estimated order
81
+ of an iteration) where they make sense.
82
+
83
+ **Iterative methods.** Root finders and linear solvers return one row per iteration. **The last
84
+ row is the final answer.**
85
+
86
+ **`order` columns.** `order` in row `k` is `log(E_k / E_(k+1)) / log(h_k / h_(k+1))`, computed from a
87
+ row and the next one, so the last row is `NaN`. It needs the step sizes to be listed in
88
+ decreasing order.
89
+
90
+ **`NaN` values** mean "not defined for this row", never an error.
91
+
92
+ ## Interpolation
93
+
94
+ All five functions return the columns `alpha, p` (and `exact, error` when `exact` is given), where
95
+ `p` is the interpolant evaluated at `alpha`.
96
+
97
+ ### `lagrange(x, y, alpha, exact=None)`
98
+
99
+ The Lagrange interpolating polynomial through the points `(x_i, y_i)`:
100
+ `p(a) = sum_i y_i * prod_(j != i) (a - x_j) / (x_i - x_j)`.
101
+
102
+ | Parameter | |
103
+ |---|---|
104
+ | `x`, `y` | nodes and values (lists or arrays); the `x` values must be distinct |
105
+ | `alpha` | a number or a list of points where the polynomial is evaluated |
106
+ | `exact` | optional function `f(x)`; adds `exact` and `error = abs(exact - p)` |
107
+
108
+ ```python
109
+ from numericalanalysisiiser import lagrange
110
+
111
+ lagrange([-2, 0, -1, 1, 2], [4, 1, -1, 1, 9], [0.7, 1.5], exact=lambda t: t**2)
112
+ ```
113
+ ```
114
+ alpha p exact error
115
+ 0.7 0.982413 0.49 0.492413
116
+ 1.5 2.632812 2.25 0.382812
117
+ ```
118
+
119
+ ### `newton_interpolation(x, y, alpha, exact=None)`
120
+
121
+ The same polynomial in Newton form, `c0 + c1 (a - x0) + c2 (a - x0)(a - x1) + ...`, with divided
122
+ difference coefficients. Same parameters and columns as `lagrange`. The nodes may be in any order.
123
+
124
+ ### `divided_differences(x, y)`
125
+
126
+ The divided difference table. Columns `x, f[0], f[1], ...`; the **first row holds the Newton
127
+ coefficients** `c0, c1, ...`.
128
+
129
+ ```python
130
+ from numericalanalysisiiser import divided_differences
131
+
132
+ divided_differences([0, 1, 2, 3], [1, 2, 5, 10])
133
+ ```
134
+ ```
135
+ x f[0] f[1] f[2] f[3]
136
+ 0 1 1.0 1.0 0.0
137
+ 1 2 3.0 1.0 NaN
138
+ 2 5 5.0 NaN NaN
139
+ 3 10 NaN NaN NaN
140
+ ```
141
+
142
+ ### `hermite(x, y, dy, alpha, exact=None)`
143
+
144
+ Hermite interpolation: the polynomial of degree `2n - 1` that matches both the values `y_i` and the
145
+ slopes `dy_i` at the `n` nodes. Same columns as `lagrange`.
146
+
147
+ ```python
148
+ import numpy as np
149
+ from numericalanalysisiiser import hermite
150
+
151
+ f = lambda x: 1 / (1 + 25 * x**2)
152
+ df = lambda x: -50 * x / (1 + 25 * x**2) ** 2
153
+ x = np.linspace(-1, 1, 10)
154
+ t = hermite(x, f(x), df(x), np.linspace(-1, 1, 2001), exact=f)
155
+ t["error"].max()
156
+ ```
157
+
158
+ ### `natural_cubic_spline(x, y, alpha, exact=None)`
159
+
160
+ The natural cubic spline (second derivative zero at both ends). `x` must be in increasing order.
161
+ Outside `[x[0], x[-1]]` the first / last cubic piece is extended. Same columns as `lagrange`.
162
+
163
+ ```python
164
+ from numericalanalysisiiser import natural_cubic_spline
165
+
166
+ f = lambda t: np.exp(-t) * np.sin(2 * t)
167
+ for h in [0.5, 0.25, 0.125]:
168
+ nodes = np.arange(0, 4 + h / 2, h)
169
+ err = natural_cubic_spline(nodes, f(nodes), np.linspace(0, 4, 2001), exact=f)["error"].max()
170
+ print(h, err)
171
+ ```
172
+
173
+ ## Numerical differentiation
174
+
175
+ ### `finite_difference(f, x, h, exact=None, methods=("forward", "backward", "central"))`
176
+
177
+ Finite difference approximations at the point `x`.
178
+
179
+ | Method | Formula | Error |
180
+ |---|---|---|
181
+ | `"forward"` | `(f(x+h) - f(x)) / h` | O(h) |
182
+ | `"backward"` | `(f(x) - f(x-h)) / h` | O(h) |
183
+ | `"central"` | `(f(x+h) - f(x-h)) / (2h)` | O(h^2) |
184
+ | `"second"` | `(f(x+h) - 2 f(x) + f(x-h)) / h^2`, approximates `f''(x)` | O(h^2) |
185
+
186
+ | Parameter | |
187
+ |---|---|
188
+ | `h` | a step size or a list of step sizes |
189
+ | `exact` | the true derivative at `x`; adds `<method>_error` and `<method>_order` |
190
+ | `methods` | tuple of method names |
191
+
192
+ Returns `h`, then one column per method (and its `_error`, `_order`).
193
+
194
+ ```python
195
+ import math
196
+ from numericalanalysisiiser import finite_difference
197
+
198
+ f = lambda x: math.exp(x) * math.cos(x)
199
+ exact = math.exp(1) * (math.cos(1) - math.sin(1))
200
+ finite_difference(f, 1.0, [0.1, 0.05, 0.025], exact)
201
+ ```
202
+ ```
203
+ h forward forward_error forward_order backward backward_error backward_order central central_error central_order
204
+ 0.100 -1.060159 0.241497 1.039001 -0.602199 0.216463 0.960073 -0.831179 0.012517 1.999763
205
+ 0.050 -0.936190 0.117528 1.019624 -0.707393 0.111268 0.980145 -0.821791 0.003130 1.999941
206
+ 0.025 -0.876632 0.057970 NaN -0.762256 0.056405 NaN -0.819444 0.000782 NaN
207
+ ```
208
+
209
+ ### `richardson(f, x, h, levels=1, method="central", exact=None, layout="by_h")`
210
+
211
+ Richardson extrapolation of a finite difference formula `D(h)`, which removes the leading error terms.
212
+ For `"central"` and `"second"` the error series is `h^2, h^4, ...` (factor `4^j`); for `"forward"` and
213
+ `"backward"` it is `h, h^2, ...` (factor `2^j`).
214
+
215
+ `layout="by_h"` (default): `h` is a list of step sizes, one row per `h`. Each row uses `D` at
216
+ `h, h/2, ..., h/2^levels`:
217
+
218
+ ```
219
+ R0 = D(h)
220
+ R1 = (4 D(h/2) - D(h)) / 3 (central)
221
+ R2 = (16 R1(h/2) - R1(h)) / 15
222
+ ```
223
+
224
+ Returns `h, R0, R1, ..., R<levels>`.
225
+
226
+ ```python
227
+ from numericalanalysisiiser import richardson
228
+
229
+ richardson(f, 1.0, [0.4, 0.2, 0.1], levels=1, exact=exact)
230
+ ```
231
+ ```
232
+ h R0 R1 R0_error R0_order R1_error R1_order
233
+ 0.4 -1.018261 -0.818844 0.199600 1.996053 1.822700e-04 4.045968
234
+ 0.2 -0.868698 -0.818672 0.050037 1.999046 1.103000e-05 4.011736
235
+ 0.1 -0.831179 -0.818662 0.012517 NaN 6.800000e-07 NaN
236
+ ```
237
+
238
+ `layout="triangle"`: `h` is a **single** starting step `h0`; row `k` uses `h0 / 2^k` and the table is
239
+ the classical triangle `R[k][j] = R[k][j-1] + (R[k][j-1] - R[k-1][j-1]) / (4^j - 1)`.
240
+ `levels + 1` rows. Returns `k, h, R0, R1, ...`; the entries above the diagonal are `NaN`. With
241
+ `exact`, `R<j>_order` is the observed order of column `j` from row to row.
242
+
243
+ ```python
244
+ richardson(f, 1.0, 1.0, levels=3, layout="triangle")
245
+ ```
246
+ ```
247
+ k h R0 R1 R2 R3
248
+ 0 1.000 -2.037466 NaN NaN NaN
249
+ 1 0.500 -1.129867 -0.827334 NaN NaN
250
+ 2 0.250 -0.896804 -0.819117 -0.818569 NaN
251
+ 3 0.125 -0.838217 -0.818688 -0.818660 -0.818661
252
+ ```
253
+
254
+ ### `richardson_triangle(first_column, p=2)`
255
+
256
+ The triangle on its own, for values you computed yourself. `first_column[k]` must be computed with
257
+ step `h / 2^k`. Returns a list of rows (`R[k][j]`). Use `p=2` for central differences and the
258
+ trapezoidal rule, `p=1` for forward / backward differences.
259
+
260
+ ## Numerical integration
261
+
262
+ ### `trapezoid(f, a, b, n=1, exact=None)` and `simpson(f, a, b, n=2, exact=None)`
263
+
264
+ Composite trapezoidal rule and composite Simpson 1/3 rule for the integral of `f` over `[a, b]`.
265
+
266
+ | Parameter | |
267
+ |---|---|
268
+ | `n` | number of sub-intervals, or a list of them. `n=1` (trapezoid) and `n=2` (Simpson) are the basic single-application rules. For Simpson `n` must be even, otherwise `ValueError` |
269
+ | `exact` | the true integral; adds `error` and `order` |
270
+
271
+ Returns `n, h, value` (and `error, order`), where `h = (b - a) / n`.
272
+
273
+ ```python
274
+ import math
275
+ from numericalanalysisiiser import trapezoid, simpson
276
+
277
+ g = lambda x: math.exp(x)
278
+ trapezoid(g, 0, 1, [2, 4, 8], exact=math.e - 1)
279
+ ```
280
+ ```
281
+ n h value error order
282
+ 2 0.500 1.753931 0.035649 1.995513
283
+ 4 0.250 1.727222 0.008940 1.998874
284
+ 8 0.125 1.720519 0.002237 NaN
285
+ ```
286
+ ```python
287
+ simpson(g, 0, 1, [2, 4, 8], exact=math.e - 1)
288
+ ```
289
+ ```
290
+ n h value error order
291
+ 2 0.500 1.718861 0.000579 3.968247
292
+ 4 0.250 1.718319 0.000037 3.991978
293
+ 8 0.125 1.718284 0.000002 NaN
294
+ ```
295
+
296
+ The observed orders are about 2 for the trapezoidal rule and about 4 for Simpson's rule.
297
+
298
+ `composite_trapezoid(f, a, b, n)` and `composite_simpson(f, a, b, n)` return the single number.
299
+
300
+ ### `romberg(f, a, b, levels=6, tol=None, exact=None)`
301
+
302
+ Romberg integration. Column `R0` is the trapezoidal rule with `n = 1, 2, 4, 8, ...` sub-intervals; the other
303
+ columns are Richardson extrapolations of it (`R[k][j] = R[k][j-1] + (R[k][j-1] - R[k-1][j-1]) / (4^j - 1)`).
304
+
305
+ | Parameter | |
306
+ |---|---|
307
+ | `levels` | number of rows (row `k` uses `n = 2^k`) |
308
+ | `tol` | if given, rows are added until `abs(R[k][k] - R[k-1][k-1]) < tol` (at most 25 rows); `levels` is then ignored |
309
+ | `exact` | the true integral; adds `error`, the error of the diagonal entry `R[k][k]` of each row |
310
+
311
+ Returns `n, R0, R1, ...` (and `error`). **The answer is the last entry of the last row.**
312
+
313
+ ```python
314
+ from numericalanalysisiiser import romberg
315
+
316
+ romberg(g, 0, 1, levels=4, exact=math.e - 1)
317
+ ```
318
+ ```
319
+ n R0 R1 R2 R3 error
320
+ 1 1.859141 NaN NaN NaN 1.408591e-01
321
+ 2 1.753931 1.718861 NaN NaN 5.793234e-04
322
+ 4 1.727222 1.718319 1.718283 NaN 8.595000e-07
323
+ 8 1.720519 1.718284 1.718282 1.718282 3.000000e-10
324
+ ```
325
+
326
+ ### `gauss_legendre(f, a, b, n=2, exact=None)`
327
+
328
+ Gauss-Legendre quadrature with `n` points (nodes and weights from numpy, mapped from `[-1, 1]` to
329
+ `[a, b]`). An `n`-point rule is exact for polynomials up to degree `2n - 1`.
330
+
331
+ `n` may be a list. Returns `n, value` (and `error`).
332
+
333
+ ```python
334
+ from numericalanalysisiiser import gauss_legendre
335
+
336
+ gauss_legendre(g, 0, 1, [1, 2, 3], exact=math.e - 1)
337
+ ```
338
+ ```
339
+ n value error
340
+ 1 1.648721 6.956056e-02
341
+ 2 1.717896 3.854505e-04
342
+ 3 1.718281 8.240870e-07
343
+ ```
344
+
345
+ ## Root finding
346
+
347
+ All four methods share these conventions:
348
+
349
+ * One row per iteration; the last row is the final iterate, so **the root is `t["p"].iloc[-1]`**.
350
+ * `p_star` is the exact root. Only when you pass it do the tables get `error` (and `q`).
351
+ * `f_evals` / `df_evals` are the cumulative numbers of evaluations of `f` / `f'` the method made.
352
+ * `q = log(e_(n+1) / e_n) / log(e_n / e_(n-1))` is the estimated order of convergence (about 2 for Newton, 1.618 for secant). It is only meaningful once the errors are small, and is `NaN` where the error is at round-off level.
353
+ * Number of iterations: `len(t)` for `bisection`; `len(t) - 1` for `fixed_point`, `newton` and `secant` (their last row only shows the final iterate).
354
+
355
+ ### `bisection(f, a, b, tol=1e-10, max_iter=1000, p_star=None)`
356
+
357
+ Bisection on `[a, b]`. `f(a)` and `f(b)` must have opposite signs, otherwise `ValueError`. Stops when
358
+ `(b - a) / 2 < tol`, which guarantees an error below `tol`.
359
+
360
+ Columns: `n, a, b, p, f(p), b-a, bound, f_evals` (+ `error`). `bound = (b0 - a0) / 2^(n+1)` is the
361
+ theoretical error bound after iteration `n`. The number of iterations needed for the error to be below `tol` is
362
+ `math.ceil(math.log2((b - a) / tol))`.
363
+
364
+ ```python
365
+ from numericalanalysisiiser import bisection
366
+
367
+ f = lambda x: x**3 - x - 2
368
+ t = bisection(f, 1, 2, tol=1e-3, p_star=1.5213797068045676)
369
+ t.head(4)
370
+ ```
371
+ ```
372
+ n a b p f(p) b-a bound f_evals error
373
+ 0 1.0 2.000 1.5000 -0.125000 1.000 0.5000 3 0.02138
374
+ 1 1.5 2.000 1.7500 1.609375 0.500 0.2500 4 0.22862
375
+ 2 1.5 1.750 1.6250 0.666016 0.250 0.1250 5 0.10362
376
+ 3 1.5 1.625 1.5625 0.252197 0.125 0.0625 6 0.04112
377
+ ```
378
+
379
+ ### `fixed_point(g, p0, tol=1e-10, max_iter=1000, f=None, p_star=None)`
380
+
381
+ Fixed point iteration `p_(n+1) = g(p_n)`; stops when `abs(p_(n+1) - p_n) < tol`.
382
+ `g` converges near a fixed point `p*` when `abs(g'(p*)) < 1`.
383
+
384
+ | Parameter | |
385
+ |---|---|
386
+ | `f` | optional original function; adds the residual column `f(p)` |
387
+ | `p_star` | the exact **fixed point** of `g` (not necessarily a root of `f`); adds `error` |
388
+
389
+ Columns: `n, p, g(p), step, [f(p)], f_evals` (+ `error`). If `g` raises an error, returns a complex
390
+ number, or the values exceed `1e12`, the iteration stops there. If it never converges you get
391
+ `max_iter + 1` rows.
392
+
393
+ ```python
394
+ from numericalanalysisiiser import fixed_point
395
+
396
+ F = lambda x: x**3 - 2 * x - 5
397
+ t = fixed_point(lambda x: (2 * x + 5) ** (1 / 3), 2.5, tol=1e-6, f=F, p_star=2.0945514815423265)
398
+ ```
399
+ ```
400
+ n p g(p) step f(p) f_evals error
401
+ 0 2.500000 2.154435 3.455653e-01 5.625000 1 4.054485e-01
402
+ 1 2.154435 2.103612 5.082266e-02 0.691131 2 5.988321e-02
403
+ ...
404
+ 7 2.094552 2.094552 6.200000e-07 0.000008 8 7.300000e-07
405
+ 8 2.094552 NaN NaN 0.000001 8 1.100000e-07
406
+ ```
407
+
408
+ ### `newton(f, df, p0, tol=1e-12, max_iter=100, p_star=None)`
409
+
410
+ Newton's method `p_(n+1) = p_n - f(p_n) / f'(p_n)`; stops when `abs(p_(n+1) - p_n) < tol`.
411
+ `df` is the derivative function and must not be zero at an iterate.
412
+
413
+ Columns: `n, p, f(p), f'(p), step, f_evals, df_evals` (+ `error, q`).
414
+
415
+ ```python
416
+ from numericalanalysisiiser import newton
417
+
418
+ t = newton(lambda x: math.exp(-x) - x, lambda x: -math.exp(-x) - 1, 0.5, p_star=0.5671432904097838)
419
+ ```
420
+ ```
421
+ n p f(p) f'(p) step f_evals df_evals error q
422
+ 0 0.500000 1.065307e-01 -1.606531 6.631100e-02 1 1 6.714329e-02 NaN
423
+ 1 0.566311 1.304510e-03 -1.567616 8.321600e-04 2 2 8.322900e-04 2.004512
424
+ 2 0.567143 2.000000e-07 -1.567143 1.300000e-07 3 3 1.300000e-07 2.002809
425
+ 3 0.567143 0.000000e+00 -1.567143 0.000000e+00 4 4 0.000000e+00 NaN
426
+ 4 0.567143 -0.000000e+00 NaN NaN 4 4 0.000000e+00 NaN
427
+ ```
428
+
429
+ ### `secant(f, p0, p1, tol=1e-12, max_iter=100, p_star=None)`
430
+
431
+ Secant method (no derivative): `p_(n+1) = p_n - f(p_n) (p_n - p_(n-1)) / (f(p_n) - f(p_(n-1)))`.
432
+ Stops when `abs(p_(n+1) - p_n) < tol`, or early if `f(p_n) == f(p_(n-1))`.
433
+
434
+ Columns: `n, p_prev, p, f(p), p_next, step, f_evals` (+ `error, q`). Here `error` is `abs(p - p_star)`.
435
+
436
+ ```python
437
+ from numericalanalysisiiser import secant
438
+
439
+ g = lambda x: x**3 - 2 * x - 5
440
+ secant(g, 2, 3, tol=1e-10, p_star=2.0945514815423265)
441
+ ```
442
+ ```
443
+ n p_prev p f(p) p_next step f_evals error q
444
+ 1 2.000000 3.000000 16.000000 2.058824 0.941176 3 0.905449 -1.430760
445
+ 2 3.000000 2.058824 -0.390800 2.081264 0.022440 4 0.035728 0.305982
446
+ 3 2.058824 2.081264 -0.147204 2.094824 0.013560 5 0.013288 3.929247
447
+ 4 2.081264 2.094824 0.003044 2.094549 0.000275 6 0.000273 1.258286
448
+ 5 2.094824 2.094549 -0.000023 2.094551 0.000002 7 0.000002 1.795831
449
+ ...
450
+ ```
451
+
452
+ ## Linear systems
453
+
454
+ Solve `A x = b` with stationary iterations. `A` is a square matrix (nested lists or a numpy array) and
455
+ `b` a vector. `A` needs a non-zero diagonal; the methods are guaranteed to converge for example when
456
+ `A` is strictly diagonally dominant. The solvers loop in pure Python, which is fine for small and
457
+ medium systems.
458
+
459
+ All three return one row per iteration and stop when `max|x_k - x_(k-1)| < tol` or after `max_iter` iterations.
460
+ Columns:
461
+
462
+ | Column | |
463
+ |---|---|
464
+ | `k` | iteration number; row `0` is the starting vector |
465
+ | `omega` | the relaxation parameter (SOR only) |
466
+ | `step` | `max|x_k - x_(k-1)|` (`NaN` in row 0) |
467
+ | `residual` | `max|b - A x_k|` |
468
+ | `q` | `residual_k / residual_(k-1)`; its average over the last rows estimates the asymptotic convergence factor |
469
+ | `x1, x2, ...` | the iterate `x_k`; **the solution is the last row** |
470
+
471
+ ### `jacobi(A, b, x0=None, tol=1e-6, max_iter=100)`
472
+
473
+ `x_i^(k+1) = (b_i - sum_(j != i) a_ij x_j^(k)) / a_ii`. `x0` is the starting vector (zeros by default).
474
+
475
+ ```python
476
+ from numericalanalysisiiser import jacobi
477
+
478
+ A = [[10, -1, 2, 1], [-1, 11, -1, 3], [2, -1, 10, -1], [1, 3, -1, 8]]
479
+ b = [6, 25, -11, 15]
480
+ t = jacobi(A, b, tol=1e-4)
481
+ t.head(4)
482
+ ```
483
+ ```
484
+ k step residual q x1 x2 x3 x4
485
+ 0 NaN 25.000000 NaN 0.000000 0.000000 0.000000 0.000000
486
+ 1 2.272727 8.518182 0.340727 0.600000 2.272727 -1.100000 1.875000
487
+ 2 1.064773 3.748864 0.440101 0.859773 1.715909 -0.805227 0.810227
488
+ 3 0.340806 1.228372 0.327665 0.851614 2.056715 -1.019341 1.023409
489
+ ```
490
+
491
+ The solution vector and the number of iterations:
492
+
493
+ ```python
494
+ solution = t.filter(regex=r"^x\d+$").iloc[-1].to_numpy()
495
+ iterations = t["k"].iloc[-1]
496
+ ```
497
+
498
+ ### `gauss_seidel(A, b, x0=None, tol=1e-6, max_iter=100)`
499
+
500
+ Like Jacobi, but each component uses the already updated components: `x_i^(k+1) = (b_i - sum_(j<i) a_ij x_j^(k+1) - sum_(j>i) a_ij x_j^(k)) / a_ii`. Same columns as `jacobi`.
501
+
502
+ ### `sor(A, b, omega, x0=None, tol=1e-6, max_iter=1000)`
503
+
504
+ Successive over-relaxation: `x_i^(k+1) = (1 - omega) x_i^(k) + omega * (Gauss-Seidel value)`.
505
+ `omega = 1` is Gauss-Seidel; `0 < omega < 2` is required for convergence.
506
+ `omega` is one number per call; to compare several values, stack the tables:
507
+
508
+ ```python
509
+ import pandas as pd
510
+ from numericalanalysisiiser import sor
511
+
512
+ A = [[4, 1, -1, 1], [1, 4, -1, -1], [-1, -1, 5, 1], [1, -1, 1, 3]]
513
+ b = [-2, -1, 0, 1]
514
+ t = pd.concat([sor(A, b, w, tol=1e-3) for w in [0.9, 1.0, 1.1, 1.5]])
515
+ t.groupby("omega").last()[["k", "step", "residual"]]
516
+ ```
517
+ ```
518
+ k step residual
519
+ omega
520
+ 0.9 9 0.000971 0.002034
521
+ 1.0 8 0.000451 0.000641
522
+ 1.1 6 0.000105 0.000157
523
+ 1.5 11 0.000825 0.001213
524
+ ```
525
+
526
+ ### `tridiagonal_matrix(n, diag=4.0, off=-1.0)`
527
+
528
+ The `n x n` numpy matrix with `diag` on the main diagonal and `off` on the first sub- and
529
+ super-diagonal.
530
+
531
+ ```python
532
+ from numericalanalysisiiser import tridiagonal_matrix
533
+
534
+ tridiagonal_matrix(4)
535
+ ```
536
+ ```
537
+ [[ 4. -1. 0. 0.]
538
+ [-1. 4. -1. 0.]
539
+ [ 0. -1. 4. -1.]
540
+ [ 0. 0. -1. 4.]]
541
+ ```
542
+
543
+ ## Ordinary differential equations
544
+
545
+ ### `euler(f, t0, T, y0, h, exact=None)`
546
+
547
+ The explicit Euler method `y_(i+1) = y_i + h f(t_i, y_i)` for the scalar problem `y' = f(t, y)`,
548
+ `y(t0) = y0` on `[t0, T]`.
549
+
550
+ | Parameter | |
551
+ |---|---|
552
+ | `f` | `f(t, y)`, takes two numbers |
553
+ | `h` | step size or a list of step sizes; `T - t0` must be a multiple of every `h`, otherwise `ValueError` |
554
+ | `exact` | the exact solution `y(t)`; adds `exact` and `error` |
555
+
556
+ Returns one table with the runs for all step sizes stacked: `h, t, y, stable` (+ `exact, error`).
557
+ `stable` is `False` for a run whose solution blew up (`abs(y) > 1e10`); `y` is then `NaN` from that step on.
558
+
559
+ ```python
560
+ import math
561
+ from numericalanalysisiiser import euler
562
+
563
+ f = lambda t, y: -1000 * (y - math.cos(t)) - math.sin(t) # stiff problem, exact solution cos(t)
564
+ t = euler(f, 0, 1, 1.0, [0.01, 0.002, 0.001], exact=math.cos)
565
+
566
+ t.groupby("h", sort=False).agg(N=("t", "count"), max_error=("error", "max"), stable=("stable", "all"))
567
+ ```
568
+ ```
569
+ N max_error stable
570
+ h
571
+ 0.010 101 9.265092e+09 False
572
+ 0.002 501 1.999999e-06 True
573
+ 0.001 1001 5.000000e-07 True
574
+ ```
575
+
576
+ (`N` here counts the rows, i.e. the number of steps plus one.) Plot the runs with
577
+ `for h, part in t.groupby("h"): plt.plot(part["t"], part["y"], label=h)`.
578
+
579
+ ## Recipes
580
+
581
+ **Compare root finders (final row of each table):**
582
+
583
+ ```python
584
+ import pandas as pd
585
+ runs = {"Bisection": bisection(f, 2, 3, p_star=root),
586
+ "Fixed point": fixed_point(g, 2.5, p_star=root),
587
+ "Newton": newton(f, df, 2.5, p_star=root),
588
+ "Secant": secant(f, 2, 3, p_star=root)}
589
+ last = pd.DataFrame([t.iloc[-1] for t in runs.values()], index=runs.keys())
590
+ last[["n", "p", "f_evals", "df_evals", "error"]]
591
+ ```
592
+
593
+ **Error against the number of points or the mesh size.** Evaluate on a fine grid with `exact=` and take
594
+ the maximum error:
595
+
596
+ ```python
597
+ errors = [natural_cubic_spline(nodes, f(nodes), fine_grid, exact=f)["error"].max() for nodes in node_sets]
598
+ ```
599
+
600
+ **Log-log convergence plot of a differencing table:**
601
+
602
+ ```python
603
+ t = finite_difference(f, 1.0, [2.0**-k for k in range(1, 11)], exact)
604
+ plt.loglog(t["h"], t["forward_error"], label="forward")
605
+ plt.loglog(t["h"], t["central_error"], label="central")
606
+ ```
607
+
608
+ **Exact values.** For root finders get `p_star` from `scipy.optimize.brentq` or `mpmath.findroot`; for
609
+ integrals use `scipy.integrate.quad`.
610
+
611
+ ## Notes and limitations
612
+
613
+ * Hermite and Lagrange interpolation at many equally spaced nodes is numerically unstable (Runge phenomenon). Use fewer nodes or a spline.
614
+ * `natural_cubic_spline` needs increasing `x`.
615
+ * `euler` handles one scalar equation, not systems.
616
+ * The linear solvers use plain Python loops (`O(n^2)` per iteration) and a dense matrix.
617
+ * `newton` raises `ZeroDivisionError` if `f'` is zero at an iterate; `secant` stops early if two consecutive function values are equal.
618
+ * Developed and checked with Python 3.12, numpy 2 and pandas 3.
619
+
620
+ ## License
621
+
622
+ MIT, see [LICENSE](LICENSE).