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.
- numericalanalysisiiser/__init__.py +35 -0
- numericalanalysisiiser/bisection.py +38 -0
- numericalanalysisiiser/euler.py +39 -0
- numericalanalysisiiser/finite_differences.py +33 -0
- numericalanalysisiiser/fixed_point.py +44 -0
- numericalanalysisiiser/gauss_legendre.py +27 -0
- numericalanalysisiiser/gauss_seidel.py +42 -0
- numericalanalysisiiser/hermite.py +39 -0
- numericalanalysisiiser/jacobi.py +42 -0
- numericalanalysisiiser/lagrange.py +28 -0
- numericalanalysisiiser/natural_cubic_spline.py +48 -0
- numericalanalysisiiser/newton.py +36 -0
- numericalanalysisiiser/newton_interpolation.py +51 -0
- numericalanalysisiiser/richardson.py +68 -0
- numericalanalysisiiser/romberg.py +30 -0
- numericalanalysisiiser/secant.py +42 -0
- numericalanalysisiiser/simpson.py +38 -0
- numericalanalysisiiser/sor.py +42 -0
- numericalanalysisiiser/trapezoid.py +33 -0
- numericalanalysisiiser/tridiagonal_matrix.py +13 -0
- numericalanalysisiiser-0.1.0.dist-info/METADATA +622 -0
- numericalanalysisiiser-0.1.0.dist-info/RECORD +25 -0
- numericalanalysisiiser-0.1.0.dist-info/WHEEL +5 -0
- numericalanalysisiiser-0.1.0.dist-info/licenses/LICENSE +21 -0
- numericalanalysisiiser-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -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).
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
numericalanalysisiiser/__init__.py,sha256=Z7zU2fdWML3lzCR5oszQsDc-c0wvfTO9cjWCQQ6aJIA,1322
|
|
2
|
+
numericalanalysisiiser/bisection.py,sha256=_H_h7W2ElobJzn6FdtTv-M0Zevl-Z7ghSZM3ZwXcEEM,1059
|
|
3
|
+
numericalanalysisiiser/euler.py,sha256=wZNig9s5I3hC418ZLL7EEOCevM4ne3AAuLJge4MzSfY,1256
|
|
4
|
+
numericalanalysisiiser/finite_differences.py,sha256=GKpAnL2MnpsYy1830Xqcqsh6Smj5rVForl_UiNTZsms,1277
|
|
5
|
+
numericalanalysisiiser/fixed_point.py,sha256=HFHV1A0BqWxoY5Q80ii5V8auuXvnjGCFFLyS9DktfL0,1368
|
|
6
|
+
numericalanalysisiiser/gauss_legendre.py,sha256=Y_b6BLKZDWSj9aV3U18vkKbXLSYrNKc9tPUBD1Aer00,779
|
|
7
|
+
numericalanalysisiiser/gauss_seidel.py,sha256=GED9wilM0MLwkrt2v3LODIj188KsWlwAmFQyEKonImQ,1429
|
|
8
|
+
numericalanalysisiiser/hermite.py,sha256=InI9cPQmuI1rqIoGzA9_IEbgy2dPPtu15i_xqbYLnG4,1158
|
|
9
|
+
numericalanalysisiiser/jacobi.py,sha256=V_v4hh0h9YL3nO4-lRevYdBi7_JNuiBp9UKVsnDFvbQ,1416
|
|
10
|
+
numericalanalysisiiser/lagrange.py,sha256=9T0YR7xrCFu16GKT4rEBDXLU7HqPLLNCVSEi9QT8ZSI,820
|
|
11
|
+
numericalanalysisiiser/natural_cubic_spline.py,sha256=SRlCQ5dVDdcBoWD_9QBA1TgRB8ExNN0hxQ-C-Vihco4,1482
|
|
12
|
+
numericalanalysisiiser/newton.py,sha256=hkM3XuDCEN0X4ZQnEbX4MmJ2v-SgZArYQsn8pkiSBBo,1302
|
|
13
|
+
numericalanalysisiiser/newton_interpolation.py,sha256=Qjo72sGGT7UdVOnth6m5DQVcwUkX66tmtGU3qUBPsI8,1506
|
|
14
|
+
numericalanalysisiiser/richardson.py,sha256=wV_zE18_kfLLj6NM2jNlwcopSK_o6JSNK0Hnsn8T_8E,2899
|
|
15
|
+
numericalanalysisiiser/romberg.py,sha256=_C9EDPzYcMapewlFGAk44_iqG3BsYlPfov8IIACUJU4,1081
|
|
16
|
+
numericalanalysisiiser/secant.py,sha256=_YcpQeqWe1710p97EMPtaVxu0kfuZwr951keVfcSts0,1586
|
|
17
|
+
numericalanalysisiiser/simpson.py,sha256=UWrhHmjRb9YlQpO_yxKeOeI5yJUM_dHsBXJBvODQQkY,1207
|
|
18
|
+
numericalanalysisiiser/sor.py,sha256=W1l-a0nEsmZFCKyQq8B_stas8_JZG24W3UQrgNb1z9s,1525
|
|
19
|
+
numericalanalysisiiser/trapezoid.py,sha256=EekIw1-IRHv157t3psxwRNaRBj58t105vO0c2sUnSqQ,1056
|
|
20
|
+
numericalanalysisiiser/tridiagonal_matrix.py,sha256=1LnCXJSpyILbaq_P8Pu9CxU_bTra2sEZ8vG62rFV22s,349
|
|
21
|
+
numericalanalysisiiser-0.1.0.dist-info/licenses/LICENSE,sha256=s8SycZyI2seWLEf7rTKXa_Hb0RLnc-AJU_nv7i_4SMk,1070
|
|
22
|
+
numericalanalysisiiser-0.1.0.dist-info/METADATA,sha256=8u5W832ie1VBBx7mYdC2MEPwi1YLzvJgnFTpc_6U_24,23908
|
|
23
|
+
numericalanalysisiiser-0.1.0.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
|
|
24
|
+
numericalanalysisiiser-0.1.0.dist-info/top_level.txt,sha256=_zQR-SFQyAVcOPgMzCt1NQXYy3HsZEoahNQR_WqH7aM,23
|
|
25
|
+
numericalanalysisiiser-0.1.0.dist-info/RECORD,,
|
|
@@ -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 @@
|
|
|
1
|
+
numericalanalysisiiser
|