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.
@@ -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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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.")
@@ -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,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)