numerical-method-lab 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.
- numerical_method_lab-0.1.0/LICENSE +21 -0
- numerical_method_lab-0.1.0/PKG-INFO +35 -0
- numerical_method_lab-0.1.0/README.md +2 -0
- numerical_method_lab-0.1.0/pyproject.toml +18 -0
- numerical_method_lab-0.1.0/setup.cfg +4 -0
- numerical_method_lab-0.1.0/src/numerical_method_lab/__inti__.py +1 -0
- numerical_method_lab-0.1.0/src/numerical_method_lab/integration.py +59 -0
- numerical_method_lab-0.1.0/src/numerical_method_lab/linear_algebra.py +100 -0
- numerical_method_lab-0.1.0/src/numerical_method_lab/root_finding.py +170 -0
- numerical_method_lab-0.1.0/src/numerical_method_lab/visualization.py +0 -0
- numerical_method_lab-0.1.0/src/numerical_method_lab.egg-info/PKG-INFO +35 -0
- numerical_method_lab-0.1.0/src/numerical_method_lab.egg-info/SOURCES.txt +16 -0
- numerical_method_lab-0.1.0/src/numerical_method_lab.egg-info/dependency_links.txt +1 -0
- numerical_method_lab-0.1.0/src/numerical_method_lab.egg-info/requires.txt +2 -0
- numerical_method_lab-0.1.0/src/numerical_method_lab.egg-info/top_level.txt +1 -0
- numerical_method_lab-0.1.0/tests/test_integration.py +177 -0
- numerical_method_lab-0.1.0/tests/test_linear_algebra.py +113 -0
- numerical_method_lab-0.1.0/tests/test_root_finding.py +108 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Abhishek thakur
|
|
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,35 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: numerical-method-lab
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A collection of numerical methods for root finding, integration, and linear algebra
|
|
5
|
+
License: MIT License
|
|
6
|
+
|
|
7
|
+
Copyright (c) 2026 Abhishek thakur
|
|
8
|
+
|
|
9
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
10
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
11
|
+
in the Software without restriction, including without limitation the rights
|
|
12
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
13
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
14
|
+
furnished to do so, subject to the following conditions:
|
|
15
|
+
|
|
16
|
+
The above copyright notice and this permission notice shall be included in all
|
|
17
|
+
copies or substantial portions of the Software.
|
|
18
|
+
|
|
19
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
20
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
21
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
22
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
23
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
24
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
25
|
+
SOFTWARE.
|
|
26
|
+
|
|
27
|
+
Requires-Python: >=3.8
|
|
28
|
+
Description-Content-Type: text/markdown
|
|
29
|
+
License-File: LICENSE
|
|
30
|
+
Requires-Dist: numpy
|
|
31
|
+
Requires-Dist: matplotlib
|
|
32
|
+
Dynamic: license-file
|
|
33
|
+
|
|
34
|
+
# Numerical_Method_Lab
|
|
35
|
+
A from-scratch numerical analysis laboratory implementing classical numerical methods for root finding, numerical integration, and linear algebra, with automated tests, visualization, and experimental notebooks to study accuracy, convergence, and numerical stability.
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
# Numerical_Method_Lab
|
|
2
|
+
A from-scratch numerical analysis laboratory implementing classical numerical methods for root finding, numerical integration, and linear algebra, with automated tests, visualization, and experimental notebooks to study accuracy, convergence, and numerical stability.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "numerical-method-lab"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "A collection of numerical methods for root finding, integration, and linear algebra"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.8"
|
|
11
|
+
license = {file = "LICENSE"} # <-- This line adds your MIT license
|
|
12
|
+
dependencies = [
|
|
13
|
+
"numpy",
|
|
14
|
+
"matplotlib",
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
[tool.setuptools.packages.find]
|
|
18
|
+
where = ["src"]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# src/__init__.py
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
def trapezoidal(f,a,b,n=10000):
|
|
3
|
+
"""
|
|
4
|
+
This function implements the trapezoidal rule for numerical integration.
|
|
5
|
+
:param f: The function to integrate
|
|
6
|
+
:param a: The lower limit of integration
|
|
7
|
+
:param b: The upper limit of integration
|
|
8
|
+
:param n: The number of trapezoids (default is 10000)
|
|
9
|
+
:return: The approximate value of the integral
|
|
10
|
+
"""
|
|
11
|
+
x = np.linspace(a, b, n+1) # x values for the trapezoids
|
|
12
|
+
dx = x[1]-x[0] # width of each trapezoid
|
|
13
|
+
total = np.sum(f(x[:-1])*dx) + np.sum(f(x[1:])*dx) # sum of areas of trapezoids
|
|
14
|
+
return total/2 # return the average of the two sums
|
|
15
|
+
|
|
16
|
+
def rectanglel(f,a,b,n=10000):
|
|
17
|
+
"""
|
|
18
|
+
This function implements the left rectangle rule for numerical integration.
|
|
19
|
+
:param f: The function to integrate
|
|
20
|
+
:param a: The lower limit of integration
|
|
21
|
+
:param b: The upper limit of integration
|
|
22
|
+
:param n: The number of rectangles (default is 10000)
|
|
23
|
+
:return: The approximate value of the integral
|
|
24
|
+
"""
|
|
25
|
+
x = np.linspace(a, b, n+1) # x values for the rectangles
|
|
26
|
+
dx = x[1]-x[0] # width of each rectangle
|
|
27
|
+
total = np.sum(f(x[:-1])*dx) # sum of areas of rectangles
|
|
28
|
+
return total # return the total area
|
|
29
|
+
|
|
30
|
+
def rectangler(f,a,b,n=10000):
|
|
31
|
+
"""
|
|
32
|
+
This function implements the right rectangle rule for numerical integration.
|
|
33
|
+
:param f: The function to integrate
|
|
34
|
+
:param a: The lower limit of integration
|
|
35
|
+
:param b: The upper limit of integration
|
|
36
|
+
:param n: The number of rectangles (default is 10000)
|
|
37
|
+
:return: The approximate value of the integral
|
|
38
|
+
"""
|
|
39
|
+
x = np.linspace(a, b, n+1) # x values for the rectangles
|
|
40
|
+
dx = x[1]-x[0] # width of each rectangle
|
|
41
|
+
total = np.sum(f(x[1:])*dx) # sum of areas of rectangles
|
|
42
|
+
return total # return the total area
|
|
43
|
+
|
|
44
|
+
def simpson(f,a,b,n=10000):
|
|
45
|
+
"""
|
|
46
|
+
This function implements Simpson's rule for numerical integration.
|
|
47
|
+
:param f: The function to integrate
|
|
48
|
+
:param a: The lower limit of integration
|
|
49
|
+
:param b: The upper limit of integration
|
|
50
|
+
:param n: The number of intervals (default is 10000, must be even)
|
|
51
|
+
:return: The approximate value of the integral
|
|
52
|
+
"""
|
|
53
|
+
if n % 2 == 1:
|
|
54
|
+
n += 1 # make n even if it is odd
|
|
55
|
+
x = np.linspace(a, b, n+1) # x values for the intervals
|
|
56
|
+
dx = x[1]-x[0] # width of each interval
|
|
57
|
+
total = f(x[0]) + f(x[-1]) + 4*np.sum(f(x[1:-1:2])) + 2*np.sum(f(x[2:-1:2])) # Simpson's rule formula
|
|
58
|
+
return total * dx / 3 # return the approximate integral value
|
|
59
|
+
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def back_substitution(A, b):
|
|
5
|
+
"""
|
|
6
|
+
Solve an upper-triangular system Ax = b
|
|
7
|
+
using back substitution.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
n = len(b)
|
|
11
|
+
|
|
12
|
+
# Create an array to store the solution
|
|
13
|
+
x = np.zeros(n)
|
|
14
|
+
|
|
15
|
+
# Start from the last equation
|
|
16
|
+
for i in range(n - 1, -1, -1):
|
|
17
|
+
|
|
18
|
+
# Start with the right-hand side
|
|
19
|
+
total = b[i]
|
|
20
|
+
|
|
21
|
+
# Subtract the terms we already know
|
|
22
|
+
for j in range(i + 1, n):
|
|
23
|
+
total -= A[i, j] * x[j]
|
|
24
|
+
|
|
25
|
+
# Check for zero diagonal element
|
|
26
|
+
if abs(A[i, i]) < 1e-12:
|
|
27
|
+
raise ValueError("Zero pivot encountered during back substitution.")
|
|
28
|
+
|
|
29
|
+
# Solve for x[i]
|
|
30
|
+
x[i] = total / A[i, i]
|
|
31
|
+
|
|
32
|
+
return x
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def gaussian_elimination(A, b):
|
|
36
|
+
"""
|
|
37
|
+
Solve Ax = b using Gaussian elimination
|
|
38
|
+
with partial pivoting and back substitution.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
# Convert inputs to NumPy arrays
|
|
42
|
+
A = np.array(A, dtype=float)
|
|
43
|
+
b = np.array(b, dtype=float)
|
|
44
|
+
|
|
45
|
+
# Make sure A is a square matrix
|
|
46
|
+
if A.ndim != 2 or A.shape[0] != A.shape[1]:
|
|
47
|
+
raise ValueError("A must be a square matrix.")
|
|
48
|
+
|
|
49
|
+
# Make sure b has the correct size
|
|
50
|
+
if len(b) != A.shape[0]:
|
|
51
|
+
raise ValueError("The dimensions of A and b do not match.")
|
|
52
|
+
|
|
53
|
+
n = len(b)
|
|
54
|
+
|
|
55
|
+
# -----------------------------------------
|
|
56
|
+
# Gaussian Elimination
|
|
57
|
+
# -----------------------------------------
|
|
58
|
+
|
|
59
|
+
for i in range(n):
|
|
60
|
+
|
|
61
|
+
# -------------------------------------
|
|
62
|
+
# PARTIAL PIVOTING
|
|
63
|
+
# -------------------------------------
|
|
64
|
+
|
|
65
|
+
# Look at the current column from row i downward
|
|
66
|
+
pivot_row = i + np.argmax(np.abs(A[i:, i]))
|
|
67
|
+
|
|
68
|
+
# Check whether the largest value is effectively zero
|
|
69
|
+
if abs(A[pivot_row, i]) < 1e-12:
|
|
70
|
+
raise ValueError("Matrix is singular or nearly singular.")
|
|
71
|
+
|
|
72
|
+
# Swap the current row with the pivot row
|
|
73
|
+
if pivot_row != i:
|
|
74
|
+
A[[i, pivot_row]] = A[[pivot_row, i]]
|
|
75
|
+
b[[i, pivot_row]] = b[[pivot_row, i]]
|
|
76
|
+
|
|
77
|
+
# -------------------------------------
|
|
78
|
+
# ELIMINATION
|
|
79
|
+
# -------------------------------------
|
|
80
|
+
|
|
81
|
+
# Eliminate values below the pivot
|
|
82
|
+
for j in range(i + 1, n):
|
|
83
|
+
|
|
84
|
+
# Calculate elimination factor
|
|
85
|
+
factor = A[j, i] / A[i, i]
|
|
86
|
+
|
|
87
|
+
# Eliminate the value
|
|
88
|
+
for k in range(i, n):
|
|
89
|
+
A[j, k] -= factor * A[i, k]
|
|
90
|
+
|
|
91
|
+
# Apply the same operation to b
|
|
92
|
+
b[j] -= factor * b[i]
|
|
93
|
+
|
|
94
|
+
# -----------------------------------------
|
|
95
|
+
# BACK SUBSTITUTION
|
|
96
|
+
# -----------------------------------------
|
|
97
|
+
|
|
98
|
+
solution = back_substitution(A, b)
|
|
99
|
+
|
|
100
|
+
return solution
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def bisection_method(f, a, b, tol=1e-5, max_iter=100):
|
|
5
|
+
"""
|
|
6
|
+
Bisection method for finding roots of a function.
|
|
7
|
+
|
|
8
|
+
Parameters:
|
|
9
|
+
f : function
|
|
10
|
+
The function for which we are trying to find a root.
|
|
11
|
+
a : float
|
|
12
|
+
The start of the interval.
|
|
13
|
+
b : float
|
|
14
|
+
The end of the interval.
|
|
15
|
+
tol : float
|
|
16
|
+
The tolerance for stopping the algorithm.
|
|
17
|
+
max_iter : int
|
|
18
|
+
The maximum number of iterations.
|
|
19
|
+
|
|
20
|
+
Returns:
|
|
21
|
+
float
|
|
22
|
+
The approximate root of the function.
|
|
23
|
+
"""
|
|
24
|
+
fa = f(a)
|
|
25
|
+
fb = f(b)
|
|
26
|
+
|
|
27
|
+
if fa * fb >= 0:
|
|
28
|
+
raise ValueError(
|
|
29
|
+
"The function must have different signs at the endpoints."
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
for i in range(max_iter):
|
|
33
|
+
|
|
34
|
+
c = (a + b) / 2
|
|
35
|
+
fc = f(c)
|
|
36
|
+
|
|
37
|
+
if abs(fc) < tol or (b - a) / 2 < tol:
|
|
38
|
+
return c
|
|
39
|
+
|
|
40
|
+
if fa * fc < 0:
|
|
41
|
+
b = c
|
|
42
|
+
fb = fc
|
|
43
|
+
else:
|
|
44
|
+
a = c
|
|
45
|
+
fa = fc
|
|
46
|
+
|
|
47
|
+
return (a + b) / 2
|
|
48
|
+
|
|
49
|
+
def derivative(f, x, h=1e-5):
|
|
50
|
+
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
Numerical derivative of a function using central difference.
|
|
54
|
+
|
|
55
|
+
Parameters:
|
|
56
|
+
|
|
57
|
+
f : function
|
|
58
|
+
|
|
59
|
+
The function for which we are calculating the derivative.
|
|
60
|
+
|
|
61
|
+
x : float
|
|
62
|
+
|
|
63
|
+
The point at which to evaluate the derivative.
|
|
64
|
+
|
|
65
|
+
h : float
|
|
66
|
+
|
|
67
|
+
The step size for the finite difference.
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
|
|
71
|
+
float
|
|
72
|
+
|
|
73
|
+
The approximate derivative of the function at point x.
|
|
74
|
+
|
|
75
|
+
"""
|
|
76
|
+
|
|
77
|
+
return (f(x + h) - f(x - h)) / (2 * h)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def Newton_raphson_method(f, x0, df=derivative, tol=1e-5, max_iter=100):
|
|
81
|
+
|
|
82
|
+
"""
|
|
83
|
+
|
|
84
|
+
Newton-Raphson Method
|
|
85
|
+
|
|
86
|
+
=====================
|
|
87
|
+
|
|
88
|
+
What it is:
|
|
89
|
+
|
|
90
|
+
The Newton-Raphson method is an iterative numerical technique used to find
|
|
91
|
+
|
|
92
|
+
roots of a real-valued function f(x) = 0.
|
|
93
|
+
|
|
94
|
+
If x_n is the current estimate, then the next estimate is:
|
|
95
|
+
|
|
96
|
+
x_{n+1} = x_n - f(x_n) / f'(x_n)
|
|
97
|
+
|
|
98
|
+
where f'(x) is the derivative of f(x).
|
|
99
|
+
|
|
100
|
+
How it works:
|
|
101
|
+
|
|
102
|
+
- Start with an initial guess x0.
|
|
103
|
+
|
|
104
|
+
- Compute the tangent line to the curve at x0.
|
|
105
|
+
|
|
106
|
+
- Find where that tangent line crosses the x-axis.
|
|
107
|
+
|
|
108
|
+
- Repeat until the change is very small or the function value is close to zero.
|
|
109
|
+
|
|
110
|
+
Why use it:
|
|
111
|
+
|
|
112
|
+
- Very fast when the initial guess is close to the actual root.
|
|
113
|
+
|
|
114
|
+
- It usually converges quadratically near simple roots.
|
|
115
|
+
|
|
116
|
+
- Works well for smooth functions with known derivatives.
|
|
117
|
+
|
|
118
|
+
Using NumPy:
|
|
119
|
+
|
|
120
|
+
NumPy helps with array operations, mathematical calculations, and plotting
|
|
121
|
+
|
|
122
|
+
when needed. This example uses only standard NumPy math functions.
|
|
123
|
+
|
|
124
|
+
Parameters:
|
|
125
|
+
|
|
126
|
+
f : function
|
|
127
|
+
|
|
128
|
+
The function for which we are trying to find a root.
|
|
129
|
+
|
|
130
|
+
x0 : float
|
|
131
|
+
|
|
132
|
+
The initial guess for the root.
|
|
133
|
+
|
|
134
|
+
df : function, optional
|
|
135
|
+
|
|
136
|
+
The derivative of the function f. If not provided, it will be calculated numerically.
|
|
137
|
+
|
|
138
|
+
tol : float, optional
|
|
139
|
+
|
|
140
|
+
The tolerance for stopping the algorithm.
|
|
141
|
+
|
|
142
|
+
max_iter : int, optional
|
|
143
|
+
|
|
144
|
+
The maximum number of iterations.
|
|
145
|
+
|
|
146
|
+
"""
|
|
147
|
+
|
|
148
|
+
x_n = x0
|
|
149
|
+
|
|
150
|
+
for i in range(max_iter):
|
|
151
|
+
|
|
152
|
+
f_xn = f(x_n)
|
|
153
|
+
|
|
154
|
+
df_xn = df(f, x_n)
|
|
155
|
+
|
|
156
|
+
if abs(df_xn) < 1e-12:
|
|
157
|
+
|
|
158
|
+
raise ValueError("Derivative is too small; no convergence.")
|
|
159
|
+
|
|
160
|
+
x_n1 = x_n - f_xn / df_xn
|
|
161
|
+
|
|
162
|
+
if abs(x_n1 - x_n) < tol:
|
|
163
|
+
return x_n1
|
|
164
|
+
|
|
165
|
+
if abs(f(x_n1)) < tol:
|
|
166
|
+
return x_n1
|
|
167
|
+
|
|
168
|
+
x_n = x_n1
|
|
169
|
+
|
|
170
|
+
raise ValueError("Maximum iterations reached without convergence.")
|
|
File without changes
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: numerical-method-lab
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A collection of numerical methods for root finding, integration, and linear algebra
|
|
5
|
+
License: MIT License
|
|
6
|
+
|
|
7
|
+
Copyright (c) 2026 Abhishek thakur
|
|
8
|
+
|
|
9
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
10
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
11
|
+
in the Software without restriction, including without limitation the rights
|
|
12
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
13
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
14
|
+
furnished to do so, subject to the following conditions:
|
|
15
|
+
|
|
16
|
+
The above copyright notice and this permission notice shall be included in all
|
|
17
|
+
copies or substantial portions of the Software.
|
|
18
|
+
|
|
19
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
20
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
21
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
22
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
23
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
24
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
25
|
+
SOFTWARE.
|
|
26
|
+
|
|
27
|
+
Requires-Python: >=3.8
|
|
28
|
+
Description-Content-Type: text/markdown
|
|
29
|
+
License-File: LICENSE
|
|
30
|
+
Requires-Dist: numpy
|
|
31
|
+
Requires-Dist: matplotlib
|
|
32
|
+
Dynamic: license-file
|
|
33
|
+
|
|
34
|
+
# Numerical_Method_Lab
|
|
35
|
+
A from-scratch numerical analysis laboratory implementing classical numerical methods for root finding, numerical integration, and linear algebra, with automated tests, visualization, and experimental notebooks to study accuracy, convergence, and numerical stability.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
src/numerical_method_lab/__inti__.py
|
|
5
|
+
src/numerical_method_lab/integration.py
|
|
6
|
+
src/numerical_method_lab/linear_algebra.py
|
|
7
|
+
src/numerical_method_lab/root_finding.py
|
|
8
|
+
src/numerical_method_lab/visualization.py
|
|
9
|
+
src/numerical_method_lab.egg-info/PKG-INFO
|
|
10
|
+
src/numerical_method_lab.egg-info/SOURCES.txt
|
|
11
|
+
src/numerical_method_lab.egg-info/dependency_links.txt
|
|
12
|
+
src/numerical_method_lab.egg-info/requires.txt
|
|
13
|
+
src/numerical_method_lab.egg-info/top_level.txt
|
|
14
|
+
tests/test_integration.py
|
|
15
|
+
tests/test_linear_algebra.py
|
|
16
|
+
tests/test_root_finding.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
numerical_method_lab
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import math
|
|
2
|
+
import numpy as np
|
|
3
|
+
import pytest
|
|
4
|
+
from src.numerical_method_lab.integration import trapzoidal, rectanglel, rectangler, simpson
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
# ----------------------------------------------------------------------
|
|
8
|
+
# Helper: exact integrals of test functions
|
|
9
|
+
# ----------------------------------------------------------------------
|
|
10
|
+
def f_linear(x):
|
|
11
|
+
return 2 * x + 3 # integral from a to b: x^2 + 3x
|
|
12
|
+
|
|
13
|
+
def f_quadratic(x):
|
|
14
|
+
return x**2 - 4*x + 5 # integral: x^3/3 - 2x^2 + 5x
|
|
15
|
+
|
|
16
|
+
def f_exp(x):
|
|
17
|
+
return np.exp(x) # integral: e^x
|
|
18
|
+
|
|
19
|
+
def f_sin(x):
|
|
20
|
+
return np.sin(x) # integral: -cos(x)
|
|
21
|
+
|
|
22
|
+
# Exact values for specific intervals
|
|
23
|
+
def exact_integral(f_name, a, b):
|
|
24
|
+
if f_name == 'linear':
|
|
25
|
+
return (b**2 - a**2) + 3*(b - a)
|
|
26
|
+
elif f_name == 'quadratic':
|
|
27
|
+
return (b**3 - a**3)/3 - 2*(b**2 - a**2) + 5*(b - a)
|
|
28
|
+
elif f_name == 'exp':
|
|
29
|
+
return np.exp(b) - np.exp(a)
|
|
30
|
+
elif f_name == 'sin':
|
|
31
|
+
return -np.cos(b) + np.cos(a)
|
|
32
|
+
else:
|
|
33
|
+
raise ValueError("Unknown function name")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
# ----------------------------------------------------------------------
|
|
37
|
+
# Tests for trapezoidal rule
|
|
38
|
+
# ----------------------------------------------------------------------
|
|
39
|
+
def test_trapezoidal_linear_exact():
|
|
40
|
+
"""Trapezoidal rule should be exact for linear functions."""
|
|
41
|
+
a, b = 0.0, 3.0
|
|
42
|
+
result = trapzoidal(f_linear, a, b, n=10) # any n works
|
|
43
|
+
expected = exact_integral('linear', a, b)
|
|
44
|
+
assert math.isclose(result, expected, rel_tol=1e-10)
|
|
45
|
+
|
|
46
|
+
def test_trapezoidal_quadratic_accuracy():
|
|
47
|
+
"""Trapezoidal rule should approximate quadratic with small error for large n."""
|
|
48
|
+
a, b = 0.0, 2.0
|
|
49
|
+
result = trapzoidal(f_quadratic, a, b, n=10000)
|
|
50
|
+
expected = exact_integral('quadratic', a, b)
|
|
51
|
+
assert math.isclose(result, expected, rel_tol=1e-8)
|
|
52
|
+
|
|
53
|
+
def test_trapezoidal_exp_accuracy():
|
|
54
|
+
"""Trapezoidal rule for e^x with n=10000 should be very close."""
|
|
55
|
+
a, b = 0.0, 1.0
|
|
56
|
+
result = trapzoidal(f_exp, a, b, n=10000)
|
|
57
|
+
expected = exact_integral('exp', a, b)
|
|
58
|
+
assert math.isclose(result, expected, rel_tol=1e-8)
|
|
59
|
+
|
|
60
|
+
def test_trapezoidal_convergence():
|
|
61
|
+
"""Error should decrease as n increases."""
|
|
62
|
+
a, b = 0.0, 1.0
|
|
63
|
+
expected = exact_integral('sin', a, b)
|
|
64
|
+
err1 = abs(trapzoidal(f_sin, a, b, n=100) - expected)
|
|
65
|
+
err2 = abs(trapzoidal(f_sin, a, b, n=1000) - expected)
|
|
66
|
+
assert err2 < err1 # larger n should give smaller error
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
# ----------------------------------------------------------------------
|
|
70
|
+
# Tests for left rectangle rule
|
|
71
|
+
# ----------------------------------------------------------------------
|
|
72
|
+
def test_rectanglel_linear_overestimates_or_underestimates():
|
|
73
|
+
"""For increasing function, left rectangle underestimates; for decreasing, overestimates."""
|
|
74
|
+
a, b = 0.0, 1.0
|
|
75
|
+
# f_linear = 2x+3 is increasing, so left rectangles underestimate
|
|
76
|
+
result = rectanglel(f_linear, a, b, n=1000)
|
|
77
|
+
expected = exact_integral('linear', a, b)
|
|
78
|
+
assert result < expected
|
|
79
|
+
|
|
80
|
+
def test_rectanglel_accuracy():
|
|
81
|
+
"""With large n, left rectangle should approximate integral."""
|
|
82
|
+
a, b = 0.0, 2.0
|
|
83
|
+
result = rectanglel(f_quadratic, a, b, n=10000)
|
|
84
|
+
expected = exact_integral('quadratic', a, b)
|
|
85
|
+
assert math.isclose(result, expected, rel_tol=1e-4)
|
|
86
|
+
|
|
87
|
+
def test_rectanglel_convergence():
|
|
88
|
+
"""Error decreases with increasing n."""
|
|
89
|
+
a, b = 0.0, 1.0
|
|
90
|
+
expected = exact_integral('exp', a, b)
|
|
91
|
+
err1 = abs(rectanglel(f_exp, a, b, n=100) - expected)
|
|
92
|
+
err2 = abs(rectanglel(f_exp, a, b, n=1000) - expected)
|
|
93
|
+
assert err2 < err1
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
# ----------------------------------------------------------------------
|
|
97
|
+
# Tests for right rectangle rule
|
|
98
|
+
# ----------------------------------------------------------------------
|
|
99
|
+
def test_rectangler_linear_overestimates_or_underestimates():
|
|
100
|
+
"""For increasing function, right rectangle overestimates."""
|
|
101
|
+
a, b = 0.0, 1.0
|
|
102
|
+
result = rectangler(f_linear, a, b, n=1000)
|
|
103
|
+
expected = exact_integral('linear', a, b)
|
|
104
|
+
assert result > expected
|
|
105
|
+
|
|
106
|
+
def test_rectangler_accuracy():
|
|
107
|
+
"""With large n, right rectangle should approximate integral."""
|
|
108
|
+
a, b = 0.0, 2.0
|
|
109
|
+
result = rectangler(f_quadratic, a, b, n=10000)
|
|
110
|
+
expected = exact_integral('quadratic', a, b)
|
|
111
|
+
assert math.isclose(result, expected, rel_tol=1e-4)
|
|
112
|
+
|
|
113
|
+
def test_rectangler_convergence():
|
|
114
|
+
"""Error decreases with increasing n."""
|
|
115
|
+
a, b = 0.0, 1.0
|
|
116
|
+
expected = exact_integral('exp', a, b)
|
|
117
|
+
err1 = abs(rectangler(f_exp, a, b, n=100) - expected)
|
|
118
|
+
err2 = abs(rectangler(f_exp, a, b, n=1000) - expected)
|
|
119
|
+
assert err2 < err1
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
# ----------------------------------------------------------------------
|
|
123
|
+
# Tests for Simpson's rule
|
|
124
|
+
# ----------------------------------------------------------------------
|
|
125
|
+
def test_simpson_quadratic_exact():
|
|
126
|
+
"""Simpson's rule should be exact for quadratic functions (degree <= 3)."""
|
|
127
|
+
a, b = 0.0, 3.0
|
|
128
|
+
result = simpson(f_quadratic, a, b, n=2) # minimal even n
|
|
129
|
+
expected = exact_integral('quadratic', a, b)
|
|
130
|
+
assert math.isclose(result, expected, rel_tol=1e-10)
|
|
131
|
+
|
|
132
|
+
def test_simpson_cubic_exact():
|
|
133
|
+
"""Simpson's rule is also exact for cubic polynomials."""
|
|
134
|
+
f_cubic = lambda x: x**3 - 2*x + 1
|
|
135
|
+
a, b = 0.0, 2.0
|
|
136
|
+
# Exact integral: x^4/4 - x^2 + x evaluated from 0 to 2
|
|
137
|
+
expected = (2**4)/4 - 2**2 + 2
|
|
138
|
+
result = simpson(f_cubic, a, b, n=2)
|
|
139
|
+
assert math.isclose(result, expected, rel_tol=1e-10)
|
|
140
|
+
|
|
141
|
+
def test_simpson_exp_accuracy():
|
|
142
|
+
"""Simpson's rule for e^x with modest n is very accurate."""
|
|
143
|
+
a, b = 0.0, 1.0
|
|
144
|
+
result = simpson(f_exp, a, b, n=100)
|
|
145
|
+
expected = exact_integral('exp', a, b)
|
|
146
|
+
assert math.isclose(result, expected, rel_tol=1e-10)
|
|
147
|
+
|
|
148
|
+
def test_simpson_odd_n_adjusts_to_even():
|
|
149
|
+
"""If n is odd, the function should internally make it even and still work."""
|
|
150
|
+
a, b = 0.0, 1.0
|
|
151
|
+
result_odd = simpson(f_sin, a, b, n=101) # odd n
|
|
152
|
+
result_even = simpson(f_sin, a, b, n=102) # even n
|
|
153
|
+
expected = exact_integral('sin', a, b)
|
|
154
|
+
assert math.isclose(result_odd, expected, rel_tol=1e-8)
|
|
155
|
+
assert math.isclose(result_even, expected, rel_tol=1e-8)
|
|
156
|
+
|
|
157
|
+
def test_simpson_convergence():
|
|
158
|
+
"""Error decreases with n."""
|
|
159
|
+
a, b = 0.0, 2.0
|
|
160
|
+
expected = exact_integral('sin', a, b)
|
|
161
|
+
err1 = abs(simpson(f_sin, a, b, n=20) - expected)
|
|
162
|
+
err2 = abs(simpson(f_sin, a, b, n=40) - expected)
|
|
163
|
+
assert err2 < err1
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
# ----------------------------------------------------------------------
|
|
167
|
+
# Test that all methods handle n=1 without crashing
|
|
168
|
+
# ----------------------------------------------------------------------
|
|
169
|
+
def test_n1_no_crash():
|
|
170
|
+
"""Each method should at least return a finite number for n=1."""
|
|
171
|
+
a, b = 0.0, 1.0
|
|
172
|
+
f = lambda x: x**2
|
|
173
|
+
assert np.isfinite(trapzoidal(f, a, b, n=1))
|
|
174
|
+
assert np.isfinite(rectanglel(f, a, b, n=1))
|
|
175
|
+
assert np.isfinite(rectangler(f, a, b, n=1))
|
|
176
|
+
# Simpson will adjust n to 2 internally, so should be finite
|
|
177
|
+
assert np.isfinite(simpson(f, a, b, n=1))
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import pytest
|
|
3
|
+
from src.numerical_method_lab.linear_algebra import back_substitution, gaussian_elimination
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
# ----------------------------------------------------------------------
|
|
7
|
+
# Tests for back_substitution
|
|
8
|
+
# ----------------------------------------------------------------------
|
|
9
|
+
def test_back_substitution_2x2():
|
|
10
|
+
A = np.array([[2.0, 1.0],
|
|
11
|
+
[0.0, 3.0]])
|
|
12
|
+
b = np.array([5.0, 6.0]) # 2x + y = 5, 3y = 6 → y=2, x=1.5
|
|
13
|
+
x = back_substitution(A, b)
|
|
14
|
+
expected = np.array([1.5, 2.0])
|
|
15
|
+
assert np.allclose(x, expected, rtol=1e-12)
|
|
16
|
+
|
|
17
|
+
def test_back_substitution_3x3():
|
|
18
|
+
A = np.array([[1.0, 2.0, 3.0],
|
|
19
|
+
[0.0, 4.0, 5.0],
|
|
20
|
+
[0.0, 0.0, 6.0]])
|
|
21
|
+
b = np.array([14.0, 23.0, 18.0])
|
|
22
|
+
# 6z = 18 → z=3; 4y + 5*3 = 23 → y=2; x + 2*2 + 3*3 = 14 → x=1
|
|
23
|
+
x = back_substitution(A, b)
|
|
24
|
+
expected = np.array([1.0, 2.0, 3.0])
|
|
25
|
+
assert np.allclose(x, expected, rtol=1e-12)
|
|
26
|
+
|
|
27
|
+
def test_back_substitution_zero_pivot_raises():
|
|
28
|
+
A = np.array([[1.0, 2.0],
|
|
29
|
+
[0.0, 0.0]]) # zero on diagonal
|
|
30
|
+
b = np.array([1.0, 1.0])
|
|
31
|
+
with pytest.raises(ValueError):
|
|
32
|
+
back_substitution(A, b)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
# ----------------------------------------------------------------------
|
|
36
|
+
# Tests for gaussian_elimination
|
|
37
|
+
# ----------------------------------------------------------------------
|
|
38
|
+
def test_gaussian_elimination_2x2():
|
|
39
|
+
A = np.array([[2.0, 1.0],
|
|
40
|
+
[1.0, 3.0]])
|
|
41
|
+
b = np.array([5.0, 6.0])
|
|
42
|
+
x = gaussian_elimination(A, b)
|
|
43
|
+
expected = np.linalg.solve(A, b)
|
|
44
|
+
assert np.allclose(x, expected, rtol=1e-12)
|
|
45
|
+
|
|
46
|
+
def test_gaussian_elimination_3x3():
|
|
47
|
+
A = np.array([[3.0, 1.0, -1.0],
|
|
48
|
+
[2.0, -2.0, 4.0],
|
|
49
|
+
[-1.0, 0.5, -1.0]])
|
|
50
|
+
b = np.array([1.0, -2.0, 0.0])
|
|
51
|
+
x = gaussian_elimination(A, b)
|
|
52
|
+
expected = np.linalg.solve(A, b)
|
|
53
|
+
assert np.allclose(x, expected, rtol=1e-10)
|
|
54
|
+
|
|
55
|
+
def test_gaussian_elimination_4x4():
|
|
56
|
+
A = np.array([[4.0, 1.0, 2.0, 0.5],
|
|
57
|
+
[3.0, 4.0, 0.0, 1.0],
|
|
58
|
+
[1.0, 0.0, 5.0, 2.0],
|
|
59
|
+
[0.0, 2.0, 1.0, 3.0]])
|
|
60
|
+
b = np.array([8.5, 10.0, 13.0, 9.0])
|
|
61
|
+
x = gaussian_elimination(A, b)
|
|
62
|
+
expected = np.linalg.solve(A, b)
|
|
63
|
+
assert np.allclose(x, expected, rtol=1e-10)
|
|
64
|
+
|
|
65
|
+
def test_gaussian_elimination_requires_pivoting():
|
|
66
|
+
# Matrix where naive Gaussian elimination (without pivoting) would fail
|
|
67
|
+
A = np.array([[0.0, 1.0],
|
|
68
|
+
[1.0, 0.0]])
|
|
69
|
+
b = np.array([2.0, 1.0]) # solution: x=1, y=2
|
|
70
|
+
x = gaussian_elimination(A, b)
|
|
71
|
+
expected = np.array([1.0, 2.0])
|
|
72
|
+
assert np.allclose(x, expected, rtol=1e-12)
|
|
73
|
+
|
|
74
|
+
def test_gaussian_elimination_ill_conditioned_pivoting():
|
|
75
|
+
# A small pivot without pivoting would cause large errors, but with pivoting it's fine
|
|
76
|
+
A = np.array([[1e-12, 1.0],
|
|
77
|
+
[1.0, 1.0]])
|
|
78
|
+
b = np.array([1.0, 2.0])
|
|
79
|
+
x = gaussian_elimination(A, b)
|
|
80
|
+
expected = np.linalg.solve(A, b)
|
|
81
|
+
assert np.allclose(x, expected, rtol=1e-8)
|
|
82
|
+
|
|
83
|
+
def test_gaussian_elimination_singular_raises():
|
|
84
|
+
A = np.array([[1.0, 2.0],
|
|
85
|
+
[2.0, 4.0]]) # linearly dependent rows
|
|
86
|
+
b = np.array([1.0, 2.0])
|
|
87
|
+
with pytest.raises(ValueError):
|
|
88
|
+
gaussian_elimination(A, b)
|
|
89
|
+
|
|
90
|
+
def test_gaussian_elimination_mismatched_dimensions_raises():
|
|
91
|
+
A = np.array([[1.0, 2.0],
|
|
92
|
+
[3.0, 4.0]])
|
|
93
|
+
b = np.array([1.0]) # too short
|
|
94
|
+
with pytest.raises(ValueError):
|
|
95
|
+
gaussian_elimination(A, b)
|
|
96
|
+
|
|
97
|
+
def test_gaussian_elimination_non_square_raises():
|
|
98
|
+
A = np.array([[1.0, 2.0, 3.0],
|
|
99
|
+
[4.0, 5.0, 6.0]]) # 2x3
|
|
100
|
+
b = np.array([1.0, 2.0])
|
|
101
|
+
with pytest.raises(ValueError):
|
|
102
|
+
gaussian_elimination(A, b)
|
|
103
|
+
|
|
104
|
+
def test_gaussian_elimination_does_not_modify_original_input():
|
|
105
|
+
A = np.array([[2.0, 1.0],
|
|
106
|
+
[1.0, 3.0]])
|
|
107
|
+
b = np.array([5.0, 6.0])
|
|
108
|
+
A_copy = A.copy()
|
|
109
|
+
b_copy = b.copy()
|
|
110
|
+
gaussian_elimination(A, b)
|
|
111
|
+
# The function converts to arrays and modifies them internally, but should not affect originals
|
|
112
|
+
assert np.array_equal(A, A_copy)
|
|
113
|
+
assert np.array_equal(b, b_copy)
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import math
|
|
2
|
+
import pytest
|
|
3
|
+
from src.numerical_method_lab.root_finding import (
|
|
4
|
+
bisection_method,
|
|
5
|
+
Newton_raphson_method,
|
|
6
|
+
derivative
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
# ----------------------------------------------------------------------
|
|
11
|
+
# Tests for the bisection method
|
|
12
|
+
# ----------------------------------------------------------------------
|
|
13
|
+
def test_bisection_finds_sqrt2():
|
|
14
|
+
"""Bisection should find sqrt(2) in [1, 2]."""
|
|
15
|
+
f = lambda x: x**2 - 2
|
|
16
|
+
root = bisection_method(f, 1, 2, tol=1e-8)
|
|
17
|
+
assert math.isclose(root, math.sqrt(2), rel_tol=1e-8)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def test_bisection_finds_root_of_linear():
|
|
21
|
+
"""Bisection should find the root of 2x - 6 = 0."""
|
|
22
|
+
f = lambda x: 2 * x - 6
|
|
23
|
+
root = bisection_method(f, 0, 5)
|
|
24
|
+
assert math.isclose(root, 3.0, rel_tol=1e-5)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def test_bisection_handles_tight_tolerance():
|
|
28
|
+
"""Bisection should get very close to the true root with small tol."""
|
|
29
|
+
f = lambda x: x**3 - x - 2
|
|
30
|
+
root = bisection_method(f, 1, 2, tol=1e-12, max_iter=200)
|
|
31
|
+
# true root is approximately 1.5213797068045676
|
|
32
|
+
assert math.isclose(root, 1.5213797068, rel_tol=1e-9)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def test_bisection_invalid_interval_raises():
|
|
36
|
+
"""If f(a) and f(b) have the same sign, a ValueError must be raised."""
|
|
37
|
+
f = lambda x: x**2 + 1 # always positive
|
|
38
|
+
with pytest.raises(ValueError):
|
|
39
|
+
bisection_method(f, -1, 1)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def test_bisection_returns_number_even_if_max_iter_reached():
|
|
43
|
+
"""If max_iter is too small, the method should still return a number."""
|
|
44
|
+
f = lambda x: x**2 - 2
|
|
45
|
+
root = bisection_method(f, 1, 2, tol=1e-12, max_iter=2)
|
|
46
|
+
assert isinstance(root, float)
|
|
47
|
+
assert 1.0 <= root <= 2.0
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
# ----------------------------------------------------------------------
|
|
51
|
+
# Tests for the numerical derivative function
|
|
52
|
+
# ----------------------------------------------------------------------
|
|
53
|
+
def test_derivative_of_linear():
|
|
54
|
+
"""The derivative of f(x) = 3x + 2 should be 3."""
|
|
55
|
+
f = lambda x: 3 * x + 2
|
|
56
|
+
d = derivative(f, 5.0)
|
|
57
|
+
assert math.isclose(d, 3.0, rel_tol=1e-5)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def test_derivative_of_quadratic():
|
|
61
|
+
"""The derivative of f(x) = x^2 at x=2 should be 4."""
|
|
62
|
+
f = lambda x: x**2
|
|
63
|
+
d = derivative(f, 2.0)
|
|
64
|
+
assert math.isclose(d, 4.0, rel_tol=1e-4)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
# ----------------------------------------------------------------------
|
|
68
|
+
# Tests for Newton Raphson method
|
|
69
|
+
# ----------------------------------------------------------------------
|
|
70
|
+
def test_newton_finds_sqrt2():
|
|
71
|
+
"""Newton Raphson should find sqrt(2) starting from 1.5."""
|
|
72
|
+
f = lambda x: x**2 - 2
|
|
73
|
+
root = Newton_raphson_method(f, 1.5, tol=1e-8)
|
|
74
|
+
assert math.isclose(root, math.sqrt(2), rel_tol=1e-8)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def test_newton_with_analytic_derivative():
|
|
78
|
+
"""
|
|
79
|
+
Newton Raphson should work when an analytic derivative is supplied.
|
|
80
|
+
The derivative function passed must accept two arguments (f, x) even if
|
|
81
|
+
the first is unused.
|
|
82
|
+
"""
|
|
83
|
+
f = lambda x: x**3 - x - 2
|
|
84
|
+
df = lambda f, x: 3 * x**2 - 1 # ignore f
|
|
85
|
+
root = Newton_raphson_method(f, 1.5, df=df, tol=1e-8)
|
|
86
|
+
assert math.isclose(root, 1.5213797068, rel_tol=1e-6)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def test_newton_uses_numerical_derivative_by_default():
|
|
90
|
+
"""Without a custom derivative, Newton Raphson should still converge."""
|
|
91
|
+
f = lambda x: x**2 - 2
|
|
92
|
+
root = Newton_raphson_method(f, 1.4, tol=1e-8)
|
|
93
|
+
assert math.isclose(root, math.sqrt(2), rel_tol=1e-6)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def test_newton_raises_when_derivative_zero():
|
|
97
|
+
"""If the derivative is (nearly) zero, a ValueError should be raised."""
|
|
98
|
+
f = lambda x: x**2 - 2
|
|
99
|
+
# Starting at x=0, derivative = 0, so the method should fail early.
|
|
100
|
+
with pytest.raises(ValueError):
|
|
101
|
+
Newton_raphson_method(f, 0.0, max_iter=10)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def test_newton_raises_on_non_convergence():
|
|
105
|
+
"""If the method does not converge within max_iter, it should raise."""
|
|
106
|
+
f = lambda x: x**3 - 2 * x + 2 # chosen to cause oscillation from 0
|
|
107
|
+
with pytest.raises(ValueError):
|
|
108
|
+
Newton_raphson_method(f, 0.0, max_iter=5)
|