Compare commits

...

36 Commits

Author SHA1 Message Date
Deniz
ea901c03ab update eigen.py
fixed -> matrix and vector imported twice from linalg
2025-09-17 14:09:49 +03:00
e0d0eb1fc9 Merge pull request 'broken links corrected' (#5) from ougur/numethods:main into main
Reviewed-on: #5
2025-09-17 13:31:14 +03:00
9fe1855fc6 typo 2025-09-17 13:30:00 +03:00
094778f67e more typo 2025-09-17 13:28:33 +03:00
61b567ec19 broken links corrected 2025-09-17 13:11:55 +03:00
0cfbdbe02d Update tutorials/README.md 2025-09-17 13:08:57 +03:00
94759a9d71 Merge pull request 'main: some modification and typos' (#4) from ougur/numethods:main into main
Reviewed-on: #4
2025-09-17 13:07:02 +03:00
cf0b91f94a some typos for math in tutorial1 2025-09-17 12:30:58 +03:00
f73789bca6 created README in tutorials (by modifying .gitignore - beginning ignored files are removed.) 2025-09-17 12:26:42 +03:00
f56e48cebd README is modified 2025-09-17 12:20:41 +03:00
Deniz
08622ae3cf Merge branch 'main' of https://ougur.iam.metu.edu.tr/gitea/denizdonmez/numethods 2025-09-17 11:44:30 +03:00
Deniz
5d36dbabb9 new methods; eigenvalue. condition number implemetation
eigenvalue module, readme and demo update. condition number implemented in linalg.py
2025-09-17 11:44:12 +03:00
629f1f25ff Update README.md 2025-09-17 11:29:23 +03:00
Deniz
baf47bb3a6 Update README.md 2025-09-17 11:28:53 +03:00
Deniz
fcd63fa1b2 Update README.md 2025-09-17 11:25:56 +03:00
Deniz
2d4071453d Update tutorial3_orthogonalization.ipynb 2025-09-17 11:20:18 +03:00
Deniz
cc5d292584 update tutorial4_root_finding.ipynb to add trace printing for all methods and handle exceptions in FixedPoint method 2025-09-17 11:16:34 +03:00
Deniz
44f7559aa1 update examples/demo.py and tutorials/tutorial4_root_finding.ipynb# On branch main 2025-09-17 09:24:25 +03:00
17b7fd4657 Upload files to "tutorials" 2025-09-16 17:11:46 +03:00
5a3cbe277f tutorial 3 import, tutorial 1 minor fix 2025-09-16 15:47:04 +03:00
22bc76dcb3 Update numethods/orthogonal.py 2025-09-16 11:19:13 +03:00
d32044f8f2 Update numethods/orthogonal.py 2025-09-16 11:18:51 +03:00
fd7cce037e Update numethods/solvers.py 2025-09-16 11:17:49 +03:00
0c5af0d46e trace and history added 2025-09-16 10:47:29 +03:00
57dec5dc6d Tutorial 2 - Linear Systems 2025-09-16 10:45:20 +03:00
f3f86659e1 Update README.md 2025-09-15 16:47:16 +03:00
21ed32673b Update tutorials/tutorial1_vectors_matrices.ipynb 2025-09-15 14:22:00 +03:00
a561f2c46c Tutorial 1 - Vectors & Matrices in numethods 2025-09-15 14:14:31 +03:00
b55ee766a7 fix norm, fix matrix matrix sub-sum 2025-09-15 14:13:16 +03:00
1f2b143f84 Update DISCLAIMER.md 2025-09-15 10:55:54 +03:00
8507a5ca01 add DISCLAIMER.md 2025-09-15 10:54:34 +03:00
ceb8008db2 Update numethods/__init__.py 2025-09-15 10:41:30 +03:00
591495159e new examples 2025-09-15 10:40:52 +03:00
6addbce56a init differentiation 2025-09-15 10:36:17 +03:00
435dfb8ac5 new module differentiation, orthogonal small fix, roots pretty table (print trace method)
introduced differentiation module
built-in l2 norm implementation
root finding problems now print #of iteration, new value, error, etc.
2025-09-15 10:35:45 +03:00
05ba5da272 new module 'differentiation' 2025-09-15 10:32:07 +03:00
16 changed files with 2320 additions and 124 deletions

35
.gitignore vendored
View File

@@ -1,38 +1,3 @@
Logo
Issues
Pull Requests
Milestones
Explore
PhiloMath
/
data-fitting-models
Private
generated from PhiloMath/default-project-template
Code
Issues
Pull Requests
Actions
Packages
Projects
Releases
Wiki
Activity
Settings
Files
.gitignore
LICENSE
README.md
data-fitting-models
/
.gitignore
Ömür Uğur
f9860d7f48
Initial commit
2 days ago
357 lines
6.4 KiB
Plaintext
# ---> JupyterNotebooks # ---> JupyterNotebooks
# gitignore template for Jupyter Notebooks # gitignore template for Jupyter Notebooks
# website: http://jupyter.org/ # website: http://jupyter.org/

16
DISCLAIMER.md Normal file
View File

@@ -0,0 +1,16 @@
Disclaimer
This package is developed for learning and research purposes only.
Most of the methods here are inspired by or extended from homework assignments I worked on during my PhD coursework over the past two years. What you see are more object-oriented and "fancier" versions of those exercises.
The algorithms are based primarily on course notes and Scientific Computing: An Introductory Survey by Michael T. Heath. Since I will be taking my PhD qualifying exam in a couple of months (wish me luck!), I've been preparing these codes and notes as part of my study, and I wanted to share them.
I'm sure many of you reading this already have a deeper understanding of these topics than I do. If you find some implementations or explanations overly simple, that's intentional: my goal here is to understand and learn, step by step.
There are mistakes, and there will be mistakes. As our dear Ömür Hoca always says:
“Do not trust me. Do not trust someone else. Only trust yourself.”
Please use these materials as a study companion, not as a definitive reference.
Good luck, and have fun exploring numerical methods!

View File

@@ -1,8 +1,19 @@
# numethods # numethods
A small, from-scratch, object-oriented Python package implementing classic numerical methods. A lightweight, from-scratch, object-oriented Python package implementing classic numerical methods.
**No NumPy / SciPy solvers used**, algorithms are implemented transparently for learning and research. **No NumPy / SciPy solvers used**, algorithms are implemented transparently for learning and research.
## Why this might be useful
- Great for teaching/learning numerical methods step by step.
- Good reference for people writing their own solvers in C/Fortran/Julia.
- Lightweight, no dependencies.
- Consistent object-oriented API (.solve() etc).
## Tutorial Series
This package comes with a set of Jupyter notebooks designed as a structured tutorial series in **numerical methods**, both mathematically rigorous and hands-on with code. See [Tutorials](./tutorials/README.md).
## Features ## Features
### Linear system solvers ### Linear system solvers
@@ -25,7 +36,7 @@ A small, from-scratch, object-oriented Python package implementing classic numer
- **Newton** (divided differences): `NewtonInterpolation` - **Newton** (divided differences): `NewtonInterpolation`
- **Lagrange** polynomials: `LagrangeInterpolation` - **Lagrange** polynomials: `LagrangeInterpolation`
### Orthogonalization, QR, and Least Squares (NEW) ### Orthogonalization, QR, and Least Squares
- **Classical Gram-Schmidt**: `QRGramSchmidt` - **Classical Gram-Schmidt**: `QRGramSchmidt`
- **Modified Gram-Schmidt**: `QRModifiedGramSchmidt` - **Modified Gram-Schmidt**: `QRModifiedGramSchmidt`
@@ -33,13 +44,30 @@ A small, from-scratch, object-oriented Python package implementing classic numer
- **QR-based linear solver** (square systems): `QRSolver` - **QR-based linear solver** (square systems): `QRSolver`
- **Least Squares** for overdetermined systems (via QR): `LeastSquaresSolver` - **Least Squares** for overdetermined systems (via QR): `LeastSquaresSolver`
### Numerical Differentiation
- **Forward difference**: `ForwardDiff`
- **Backward difference**: `BackwardDiff`
- **Central difference (2nd order)**: `CentralDiff`
- **Central difference (4th order)**: `CentralDiff4th`
- **Second derivative**: `SecondDerivative`
- **Richardson extrapolation**: `RichardsonExtrap`
### Eigenvalue methods
- **Power Iteration** (dominant eigenvalue/vector): `PowerIteration`
- **Inverse Power Iteration** (optionally shifted): `InversePowerIteration`
- **Rayleigh Quotient Iteration**: `RayleighQuotientIteration`
- **QR eigenvalue iteration** (unshifted, educational): `QREigenvalues`
### Matrix & Vector utilities ### Matrix & Vector utilities
- Minimal `Matrix` / `Vector` classes - Minimal `Matrix` / `Vector` classes
- `@` operator for **matrix multiplication** (NEW) - `@` operator for **matrix multiplication**
- `*` for **scalar**-matrix multiplication - `*` for **scalar**-matrix multiplication
- `.T` for transpose - `.T` for transpose
- Forward / backward substitution helpers - Forward / backward substitution helpers
- Norms, dot products, row/column access
--- ---

View File

@@ -1,28 +1,8 @@
import sys import sys
sys.path.append('../')
# sys.path.append("../")
from numethods import * from numethods import *
A = Matrix([[2, -1], [1, 2], [1, 1]])
b = Vector([1, 2, 3])
# Factorization
qr = QRHouseholder(A)
Q, R = qr.Q, qr.R
print("Q =", Q)
print("R =", R)
qrm = QRModifiedGramSchmidt(A)
Qm, Rm = qrm.Q, qrm.R
print("Qm =", Qm)
print("Rm =", Rm)
print("Q^T Q =", Q.T @ Q)
print("Qm^T Qm =", Qm.T @ Qm)
print("A=Qm Rm =", Qm @ Rm)
# Solve Ax = b (least squares, since A is tall)
x_ls = LeastSquaresSolver(A, b).solve()
print("Least squares solution:", x_ls)
def demo_linear_solvers(): def demo_linear_solvers():
A = Matrix([[4, -1, 0], [-1, 4, -1], [0, -1, 3]]) A = Matrix([[4, -1, 0], [-1, 4, -1], [0, -1, 3]])
@@ -35,15 +15,55 @@ def demo_linear_solvers():
print("Gauss-Seidel:", GaussSeidel(A, b, tol=1e-12).solve()) print("Gauss-Seidel:", GaussSeidel(A, b, tol=1e-12).solve())
def demo_qr():
A = Matrix([[2, -1], [1, 2], [1, 1]])
b = Vector([1, 2, 3])
# Factorization
qr = QRHouseholder(A)
Q, R = qr.Q, qr.R
print("Q =", Q)
print("R =", R)
qrm = QRModifiedGramSchmidt(A)
Qm, Rm = qrm.Q, qrm.R
print("Qm =", Qm)
print("Rm =", Rm)
print("Q^T Q =", Q.T @ Q)
print("Qm^T Qm =", Qm.T @ Qm)
print("A=Qm Rm =", Qm @ Rm)
print("A=Q R =", Q @ R)
# Solve Ax = b (least squares, since A is tall)
x_ls = LeastSquaresSolver(A, b).solve()
print("Least squares solution:", x_ls)
def demo_roots(): def demo_roots():
f = lambda x: x**3 - x - 2 f = lambda x: x**2 - 2
df = lambda x: 3 * x**2 - 1 df = lambda x: 2 * x
print("Bisection:", Bisection(f, 1, 2).solve())
print("Secant:", Secant(f, 1.0, 2.0).solve()) # Newton
print("Newton root:", NewtonRoot(f, df, 1.5).solve()) steps = NewtonRoot(f, df, x0=1.0).trace()
# A simple contraction for demonstration; not general-purpose print("Newton Method Trace (x^2 - 2):")
g = lambda x: (x + 2 / x**2) / 2 print_trace(steps)
print("Fixed point (demo):", FixedPoint(g, 1.5).solve())
# Secant
steps = Secant(f, 0, 2).trace()
print("\nSecant Method Trace (x^2 - 2):")
print_trace(steps)
# Bisection
steps = Bisection(f, 0, 2).trace()
print("\nBisection Method Trace (x^2 - 2):")
print_trace(steps)
# Fixed-point: solve
g = lambda x: 0.5 * (x + 2 / x)
steps = FixedPoint(g, 1.0).trace()
print("\nFixed-Point Iteration Trace (x^2 - 2):")
print_trace(steps)
def demo_interpolation(): def demo_interpolation():
@@ -56,7 +76,49 @@ def demo_interpolation():
print("Lagrange interpolation at", t, "=", lagr.evaluate(t)) print("Lagrange interpolation at", t, "=", lagr.evaluate(t))
def demo_differentiation():
f = lambda x: x**3 # f'(x) = 3x^2, f''(x) = 6x
x0 = 2.0
print("Forward :", ForwardDiff(f, x0))
print("Backward :", BackwardDiff(f, x0))
print("Central :", CentralDiff(f, x0))
print("4th order:", CentralDiff4th(f, x0))
print("Richardson:", RichardsonExtrap(f, x0))
print("Second derivative:", SecondDerivative(f, x0))
def demo_eigen():
# Not sure about eigenvalue methods but let's include some demos
A = Matrix([[4, 1, 1], [1, 3, 0], [1, 0, 2]])
print("\n=== Power Iteration ===")
solver_pi = PowerIteration(A, tol=1e-12, max_iter=100)
lam, x = solver_pi.solve()
solver_pi.trace()
print(f"Dominant eigenvalue ≈ {lam:.6f}, eigenvector ≈ {x}\n")
print("\n=== Inverse Power Iteration (shift=0) ===")
solver_ip = InversePowerIteration(A, shift=0.0, tol=1e-12, max_iter=100)
mu, x = solver_ip.solve()
solver_ip.trace()
print(f"Smallest eigenvalue ≈ {mu:.6f}, eigenvector ≈ {x}\n")
print("\n=== Rayleigh Quotient Iteration ===")
solver_rqi = RayleighQuotientIteration(A, tol=1e-12, max_iter=20)
mu, x = solver_rqi.solve()
solver_rqi.trace()
print(f"Eigenvalue ≈ {mu:.6f}, eigenvector ≈ {x}\n")
M = Matrix([[3, 1, 1], [-1, 3, 1], [1, 1, 3], [0, 2, 1]])
U, S, V = SVD(M).solve()
print("Singular values:", S)
if __name__ == "__main__": if __name__ == "__main__":
demo_linear_solvers() demo_linear_solvers()
demo_roots() demo_roots()
demo_interpolation() demo_interpolation()
demo_qr()
demo_differentiation()
demo_eigen()

View File

@@ -6,7 +6,22 @@ from .orthogonal import (
QRSolver, QRSolver,
LeastSquaresSolver, LeastSquaresSolver,
) )
from .differentiation import (
ForwardDiff,
BackwardDiff,
CentralDiff,
CentralDiff4th,
SecondDerivative,
RichardsonExtrap,
)
from .eigen import (
PowerIteration,
InversePowerIteration,
RayleighQuotientIteration,
QREigenvalues,
SVD,
)
from .solvers import LUDecomposition, GaussJordan, Jacobi, GaussSeidel, Cholesky from .solvers import LUDecomposition, GaussJordan, Jacobi, GaussSeidel, Cholesky
from .roots import Bisection, FixedPoint, Secant, NewtonRoot from .roots import Bisection, FixedPoint, Secant, NewtonRoot, print_trace
from .interpolation import NewtonInterpolation, LagrangeInterpolation from .interpolation import NewtonInterpolation, LagrangeInterpolation
from .exceptions import * from .exceptions import *

View File

@@ -0,0 +1,51 @@
from __future__ import annotations
from typing import Callable
# ----------------------------
# First derivative approximations
# ----------------------------
def ForwardDiff(f: Callable[[float], float], x: float, h: float = 1e-5) -> float:
"""Forward finite difference approximation of f'(x)."""
return (f(x + h) - f(x)) / h
def BackwardDiff(f: Callable[[float], float], x: float, h: float = 1e-5) -> float:
"""Backward finite difference approximation of f'(x)."""
return (f(x) - f(x - h)) / h
def CentralDiff(f: Callable[[float], float], x: float, h: float = 1e-5) -> float:
"""Central finite difference approximation of f'(x) (2nd-order accurate)."""
return (f(x + h) - f(x - h)) / (2 * h)
def CentralDiff4th(f: Callable[[float], float], x: float, h: float = 1e-5) -> float:
"""Fourth-order accurate central difference approximation of f'(x)."""
return (-f(x + 2 * h) + 8 * f(x + h) - 8 * f(x - h) + f(x - 2 * h)) / (12 * h)
# ----------------------------
# Second derivative
# ----------------------------
def SecondDerivative(f: Callable[[float], float], x: float, h: float = 1e-5) -> float:
"""Central difference approximation of second derivative f''(x)."""
return (f(x + h) - 2 * f(x) + f(x - h)) / (h**2)
# ----------------------------
# Richardson Extrapolation
# ----------------------------
def RichardsonExtrap(f: Callable[[float], float], x: float, h: float = 1e-2) -> float:
"""Richardson extrapolation to improve derivative accuracy.
Combines estimates with step h and h/2.
"""
D_h = CentralDiff(f, x, h)
D_h2 = CentralDiff(f, x, h / 2)
return (4 * D_h2 - D_h) / 3

218
numethods/eigen.py Normal file
View File

@@ -0,0 +1,218 @@
from __future__ import annotations
from .linalg import Matrix, Vector
from .orthogonal import QRHouseholder
from .solvers import LUDecomposition
from .exceptions import NonSquareMatrixError, ConvergenceError
import math
def solve_linear(M: Matrix, b: Vector) -> Vector:
"""Solve Mx = b using LU decomposition."""
solver = LUDecomposition(M)
return solver.solve(b)
class PowerIteration:
def __init__(self, A: Matrix, tol: float = 1e-10, max_iter: int = 5000):
if not A.is_square():
raise NonSquareMatrixError("A must be square")
self.A, self.tol, self.max_iter = A, tol, max_iter
self.history = []
def solve(self, x0: Vector | None = None) -> tuple[float, Vector]:
n = self.A.n
x = Vector([1.0] * n) if x0 is None else x0.copy()
lam_old = 0.0
self.history.clear()
for k in range(self.max_iter):
y = self.A @ x
nrm = y.norm2()
if nrm == 0.0:
raise ConvergenceError("Zero vector encountered")
x = (1.0 / nrm) * y
lam = (x.dot(self.A @ x)) / (x.dot(x))
err = abs(lam - lam_old)
self.history.append({"iter": k, "lambda": lam, "error": err})
if err <= self.tol * (1.0 + abs(lam)):
return lam, x
lam_old = lam
raise ConvergenceError("Power iteration did not converge")
def trace(self):
if not self.history:
print("No iterations stored. Run .solve() first.")
return
print("Power Iteration Trace")
print(f"{'iter':>6} | {'lambda':>12} | {'error':>12}")
print("-" * 40)
for row in self.history:
print(f"{row['iter']:6d} | {row['lambda']:12.6e} | {row['error']:12.6e}")
class InversePowerIteration:
def __init__(
self, A: Matrix, shift: float = 0.0, tol: float = 1e-10, max_iter: int = 5000
):
if not A.is_square():
raise NonSquareMatrixError("A must be square")
self.A, self.shift, self.tol, self.max_iter = A, shift, tol, max_iter
self.history = []
def solve(self, x0: Vector | None = None) -> tuple[float, Vector]:
n = self.A.n
x = Vector([1.0] * n) if x0 is None else x0.copy()
mu_old = None
self.history.clear()
for k in range(self.max_iter):
M = Matrix(
[
[
self.A.data[i][j] - (self.shift if i == j else 0.0)
for j in range(n)
]
for i in range(n)
]
)
y = solve_linear(M, x)
nrm = y.norm2()
if nrm == 0.0:
raise ConvergenceError("Zero vector")
x = (1.0 / nrm) * y
mu = (x.dot(self.A @ x)) / (x.dot(x))
err = abs(mu - mu_old) if mu_old is not None else float("inf")
self.history.append({"iter": k, "mu": mu, "error": err})
if (mu_old is not None) and err <= self.tol * (1.0 + abs(mu)):
return mu, x
mu_old = mu
raise ConvergenceError("Inverse/shifted power iteration did not converge")
def trace(self):
if not self.history:
print("No iterations stored. Run .solve() first.")
return
print("Inverse/Shifted Power Iteration Trace")
print(f"{'iter':>6} | {'mu':>12} | {'error':>12}")
print("-" * 40)
for row in self.history:
print(f"{row['iter']:6d} | {row['mu']:12.6e} | {row['error']:12.6e}")
class RayleighQuotientIteration:
def __init__(self, A: Matrix, tol: float = 1e-12, max_iter: int = 1000):
if not A.is_square():
raise NonSquareMatrixError("A must be square")
self.A, self.tol, self.max_iter = A, tol, max_iter
self.history = []
def solve(self, x0: Vector | None = None) -> tuple[float, Vector]:
n = self.A.n
x = Vector([1.0] * n) if x0 is None else x0.copy()
x = (1.0 / x.norm2()) * x
mu = (x.dot(self.A @ x)) / (x.dot(x))
self.history.clear()
for k in range(self.max_iter):
M = Matrix(
[
[self.A.data[i][j] - (mu if i == j else 0.0) for j in range(n)]
for i in range(n)
]
)
y = solve_linear(M, x)
x = (1.0 / y.norm2()) * y
mu_new = (x.dot(self.A @ x)) / (x.dot(x))
err = abs(mu_new - mu)
self.history.append({"iter": k, "mu": mu_new, "error": err})
if err <= self.tol * (1.0 + abs(mu_new)):
return mu_new, x
mu = mu_new
raise ConvergenceError("Rayleigh quotient iteration did not converge")
def trace(self):
if not self.history:
print("No iterations stored. Run .solve() first.")
return
print("Rayleigh Quotient Iteration Trace")
print(f"{'iter':>6} | {'mu':>12} | {'error':>12}")
print("-" * 40)
for row in self.history:
print(f"{row['iter']:6d} | {row['mu']:12.6e} | {row['error']:12.6e}")
class QREigenvalues:
def __init__(self, A: Matrix, tol: float = 1e-10, max_iter: int = 10000):
if not A.is_square():
raise NonSquareMatrixError("A must be square")
self.A0, self.tol, self.max_iter = A.copy(), tol, max_iter
def solve(self) -> Matrix:
A = self.A0.copy()
n = A.n
for _ in range(self.max_iter):
qr = QRHouseholder(A)
Q, R = qr.Q, qr.R
A = R @ Q
off = 0.0
for i in range(1, n):
off += sum(abs(A.data[i][j]) for j in range(0, i))
if off <= self.tol:
return A
raise ConvergenceError("QR did not converge")
class SVD:
def __init__(self, A: Matrix, tol: float = 1e-10, max_iter: int = 10000):
self.A, A = A, A
self.tol, self.max_iter = tol, max_iter
def _eig_sym(self, S: Matrix):
n = S.n
V = Matrix.identity(n)
A = S.copy()
for _ in range(self.max_iter):
qr = QRHouseholder(A)
Q, R = qr.Q, qr.R
A = R @ Q
V = V @ Q
off = 0.0
for i in range(1, n):
off += sum(abs(A.data[i][j]) for j in range(0, i))
if off <= self.tol:
break
return [A.data[i][i] for i in range(n)], V
def solve(self) -> tuple[Matrix, Vector, Matrix]:
At = self.A.transpose()
S = At @ self.A
eigvals, V = self._eig_sym(S)
idx = sorted(range(len(eigvals)), key=lambda i: eigvals[i], reverse=True)
eigvals = [eigvals[i] for i in idx]
V = Matrix([[V.data[r][i] for i in idx] for r in range(V.m)])
sing = [math.sqrt(ev) if ev > 0 else 0.0 for ev in eigvals]
Ucols = []
for j, sv in enumerate(sing):
vj = V.col(j)
Av = self.A @ vj
if sv > 1e-14:
uj = (1.0 / sv) * Av
else:
nrm = Av.norm2()
uj = (1.0 / nrm) * Av if nrm > 0 else Vector([0.0] * self.A.m)
nrm = uj.norm2()
uj = (1.0 / nrm) * uj if nrm > 0 else uj
Ucols.append(uj.data)
U = Matrix([[Ucols[j][i] for j in range(len(Ucols))] for i in range(self.A.m)])
Sigma = Vector(sing)
return U, Sigma, V

View File

@@ -1,6 +1,7 @@
from __future__ import annotations from __future__ import annotations
from typing import Iterable, Tuple, List from typing import Iterable, Tuple, List, Union
from .exceptions import NonSquareMatrixError, SingularMatrixError from .exceptions import NonSquareMatrixError, SingularMatrixError
import math
Number = float # We'll use float throughout Number = float # We'll use float throughout
@@ -21,16 +22,24 @@ class Vector:
def copy(self) -> "Vector": def copy(self) -> "Vector":
return Vector(self.data[:]) return Vector(self.data[:])
def norm(self) -> Number:
return sum(abs(x) for x in self.data)
def norm_inf(self) -> Number: def norm_inf(self) -> Number:
return max(abs(x) for x in self.data) if self.data else 0.0 return max(abs(x) for x in self.data) if self.data else 0.0
def norm2(self) -> Number:
return sum(x * x for x in self.data) ** 0.5
def __add__(self, other: "Vector") -> "Vector": def __add__(self, other: "Vector") -> "Vector":
assert len(self) == len(other) if len(self) != len(other):
return Vector(a + b for a, b in zip(self.data, other.data)) raise ValueError("Vector dimensions must match for addition")
return Vector([a + b for a, b in zip(self.data, other.data)])
def __sub__(self, other: "Vector") -> "Vector": def __sub__(self, other: "Vector") -> "Vector":
assert len(self) == len(other) if len(self) != len(other):
return Vector(a - b for a, b in zip(self.data, other.data)) raise ValueError("Vector dimensions must match for subtraction")
return Vector([a - b for a, b in zip(self.data, other.data)])
def __mul__(self, scalar: Number) -> "Vector": def __mul__(self, scalar: Number) -> "Vector":
return Vector(scalar * x for x in self.data) return Vector(scalar * x for x in self.data)
@@ -89,30 +98,129 @@ class Matrix:
def col(self, j: int) -> Vector: def col(self, j: int) -> Vector:
return Vector(self.data[i][j] for i in range(self.m)) return Vector(self.data[i][j] for i in range(self.m))
def norm(self) -> float:
"""Matrix 1-norm: max column sum."""
return max(
sum(abs(self.data[i][j]) for i in range(self.m)) for j in range(self.n)
)
def norm_inf(self) -> float:
"""Matrix infinity norm: max row sum."""
return max(sum(abs(v) for v in row) for row in self.data)
def norm2(self, tol: float = 1e-10, max_iter: int = 5000) -> float:
"""Spectral norm via power iteration on A^T A."""
# lazy import, avoids circular import
from .eigen import PowerIteration
if not self.is_square():
raise NonSquareMatrixError("Spectral norm requires square matrix")
AtA = self.T @ self
lam, _ = PowerIteration(AtA, tol=tol, max_iter=max_iter).solve()
return math.sqrt(lam)
def norm_fro(self) -> Number:
return (
sum(self.data[i][j] ** 2 for i in range(self.m) for j in range(self.n))
** 0.5
)
def inverse(self) -> "Matrix":
# lazy import, avoids circular import
from .solvers import LUDecomposition
"""Compute A^{-1} using LU decomposition."""
if not self.is_square():
raise NonSquareMatrixError("Inverse requires square matrix")
n = self.n
solver = LUDecomposition(self)
cols = []
for j in range(n):
e = Vector([1.0 if i == j else 0.0 for i in range(n)])
x = solver.solve(e)
cols.append([x[i] for i in range(n)])
return Matrix([[cols[j][i] for j in range(n)] for i in range(n)])
def condition_number(self, norm: str = "2") -> float:
"""
Compute condition number κ(A) = ||A|| * ||A^{-1}||.
norm: "1", "inf", or "2"
"""
if not self.is_square():
raise NonSquareMatrixError("Condition number requires square matrix")
if norm == "1":
normA = self.norm()
normAinv = self.inverse().norm()
elif norm == "inf":
normA = self.norm_inf()
normAinv = self.inverse().norm_inf()
elif norm == "2":
normA = self.norm2()
normAinv = self.inverse().norm2()
else:
raise ValueError("norm must be '1', 'inf', or '2'")
return normA * normAinv
def __add__(self, other: "Matrix") -> "Matrix":
if not isinstance(other, Matrix):
raise TypeError("Can only add Matrix with Matrix")
if self.m != other.m or self.n != other.n:
raise ValueError("Matrix dimensions must match for addition")
return Matrix(
[
[self.data[i][j] + other.data[i][j] for j in range(self.n)]
for i in range(self.m)
]
)
def __sub__(self, other: "Matrix") -> "Matrix":
if not isinstance(other, Matrix):
raise TypeError("Can only subtract Matrix with Matrix")
if self.m != other.m or self.n != other.n:
raise ValueError("Matrix dimensions must match for subtraction")
return Matrix(
[
[self.data[i][j] - other.data[i][j] for j in range(self.n)]
for i in range(self.m)
]
)
def transpose(self) -> "Matrix": def transpose(self) -> "Matrix":
return Matrix([[self.data[i][j] for i in range(self.m)] for j in range(self.n)]) return Matrix([[self.data[i][j] for i in range(self.m)] for j in range(self.n)])
T = property(transpose) T = property(transpose)
def __matmul__(self, other: "Matrix") -> "Matrix": def __matmul__(self, other: Union["Matrix", "Vector"]):
"""Matrix multiplication with @ operator.""" if isinstance(other, Matrix):
if self.n != other.m: if self.n != other.m:
raise ValueError("Matrix dimensions do not align for multiplication") raise ValueError("dims")
return Matrix( return Matrix(
[
[ [
sum(self.data[i][k] * other.data[k][j] for k in range(self.n)) [
for j in range(other.n) sum(self.data[i][k] * other.data[k][j] for k in range(self.n))
for j in range(other.n)
]
for i in range(self.m)
] ]
for i in range(self.m) )
] elif isinstance(other, Vector):
) if self.n != len(other):
raise ValueError("dims")
return Vector(
[
sum(self.data[i][k] * other[k] for k in range(self.n))
for i in range(self.m)
]
)
else:
raise TypeError("Unsupported @")
def __mul__(self, other): def __mul__(self, s):
"""Overload * for scalar multiplication.""" if isinstance(s, (int, float)):
if isinstance(other, (int, float)): return Matrix([[v * s for v in row] for row in self.data])
return Matrix([[val * other for val in row] for row in self.data]) raise TypeError("Use @ for matrix multiply; * is scalar")
raise TypeError("Use @ for matrix multiplication, * only supports scalars")
__rmul__ = __mul__ __rmul__ = __mul__
@@ -142,6 +250,7 @@ class Matrix:
def forward_substitution(L: Matrix, b: Vector) -> Vector: def forward_substitution(L: Matrix, b: Vector) -> Vector:
"""Solve Lx = b for x using forward substitution"""
if not L.is_square(): if not L.is_square():
raise NonSquareMatrixError("L must be square") raise NonSquareMatrixError("L must be square")
n = L.n n = L.n
@@ -155,6 +264,7 @@ def forward_substitution(L: Matrix, b: Vector) -> Vector:
def backward_substitution(U: Matrix, b: Vector) -> Vector: def backward_substitution(U: Matrix, b: Vector) -> Vector:
"""Solve Ux = b for x using backward substitution"""
if not U.is_square(): if not U.is_square():
raise NonSquareMatrixError("U must be square") raise NonSquareMatrixError("U must be square")
n = U.n n = U.n

View File

@@ -79,7 +79,7 @@ class QRHouseholder:
sign = 1.0 if x[0] >= 0 else -1.0 sign = 1.0 if x[0] >= 0 else -1.0
u1 = x[0] + sign * normx u1 = x[0] + sign * normx
v = [xi / u1 if i > 0 else 1.0 for i, xi in enumerate(x)] v = [xi / u1 if i > 0 else 1.0 for i, xi in enumerate(x)]
normv = sum(vi * vi for vi in v) ** 0.5 normv = Vector(v).norm2()
v = [vi / normv for vi in v] v = [vi / normv for vi in v]
for j in range(k, n): for j in range(k, n):
s = sum(v[i] * self.R.data[k + i][j] for i in range(len(v))) s = sum(v[i] * self.R.data[k + i][j] for i in range(len(v)))
@@ -89,7 +89,7 @@ class QRHouseholder:
s = sum(v[i] * self.Q.data[j][k + i] for i in range(len(v))) s = sum(v[i] * self.Q.data[j][k + i] for i in range(len(v)))
for i in range(len(v)): for i in range(len(v)):
self.Q.data[j][k + i] -= 2 * s * v[i] self.Q.data[j][k + i] -= 2 * s * v[i]
# self.Q = self.Q.transpose() # self.Q = self.Q.transpose()
class QRSolver: class QRSolver:

View File

@@ -1,9 +1,17 @@
from __future__ import annotations from __future__ import annotations
from typing import Callable from typing import Callable, List, Dict, Any
from .exceptions import ConvergenceError, DomainError from .exceptions import ConvergenceError, DomainError
class Bisection: class Bisection:
def __init__(self, f: Callable[[float], float], a: float, b: float, tol: float = 1e-10, max_iter: int = 10_000): def __init__(
self,
f: Callable[[float], float],
a: float,
b: float,
tol: float = 1e-10,
max_iter: int = 10_000,
):
if a >= b: if a >= b:
raise ValueError("Require a < b") raise ValueError("Require a < b")
fa, fb = f(a), f(b) fa, fb = f(a), f(b)
@@ -16,11 +24,38 @@ class Bisection:
a, b, f = self.a, self.b, self.f a, b, f = self.a, self.b, self.f
fa, fb = f(a), f(b) fa, fb = f(a), f(b)
for _ in range(self.max_iter): for _ in range(self.max_iter):
c = 0.5*(a+b) c = 0.5 * (a + b)
fc = f(c) fc = f(c)
if abs(fc) <= self.tol or 0.5*(b-a) <= self.tol: if abs(fc) <= self.tol or 0.5 * (b - a) <= self.tol:
return c return c
if fa*fc < 0: if fa * fc < 0:
b, fb = c, fc
else:
a, fa = c, fc
raise ConvergenceError("Bisection did not converge")
def trace(self) -> List[Dict[str, Any]]:
steps = []
a, b, f = self.a, self.b, self.f
fa, fb = f(a), f(b)
for k in range(self.max_iter):
c = 0.5 * (a + b)
fc = f(c)
steps.append(
{
"iter": k,
"a": a,
"b": b,
"c": c,
"f(a)": fa,
"f(b)": fb,
"f(c)": fc,
"interval": b - a,
}
)
if abs(fc) <= self.tol or 0.5 * (b - a) <= self.tol:
return steps
if fa * fc < 0:
b, fb = c, fc b, fb = c, fc
else: else:
a, fa = c, fc a, fa = c, fc
@@ -28,7 +63,13 @@ class Bisection:
class FixedPoint: class FixedPoint:
def __init__(self, g: Callable[[float], float], x0: float, tol: float = 1e-10, max_iter: int = 10_000): def __init__(
self,
g: Callable[[float], float],
x0: float,
tol: float = 1e-10,
max_iter: int = 10_000,
):
self.g, self.x0, self.tol, self.max_iter = g, x0, tol, max_iter self.g, self.x0, self.tol, self.max_iter = g, x0, tol, max_iter
def solve(self) -> float: def solve(self) -> float:
@@ -40,28 +81,79 @@ class FixedPoint:
x = x_new x = x_new
raise ConvergenceError("Fixed-point iteration did not converge") raise ConvergenceError("Fixed-point iteration did not converge")
def trace(self) -> List[Dict[str, Any]]:
steps = []
x = self.x0
for k in range(self.max_iter):
x_new = self.g(x)
steps.append({"iter": k, "x": x, "x_new": x_new, "error": abs(x_new - x)})
if abs(x_new - x) <= self.tol * (1.0 + abs(x_new)):
return steps
x = x_new
raise ConvergenceError("Fixed-point iteration did not converge")
class Secant: class Secant:
def __init__(self, f: Callable[[float], float], x0: float, x1: float, tol: float = 1e-10, max_iter: int = 10_000): def __init__(
self,
f: Callable[[float], float],
x0: float,
x1: float,
tol: float = 1e-10,
max_iter: int = 10_000,
):
self.f, self.x0, self.x1, self.tol, self.max_iter = f, x0, x1, tol, max_iter self.f, self.x0, self.x1, self.tol, self.max_iter = f, x0, x1, tol, max_iter
def solve(self) -> float: def solve(self) -> float:
x0, x1, f = self.x0, self.x1, self.f x0, x1, f = self.x0, self.x1, self.f
f0, f1 = f(x0), f(x1) f0, f1 = f(x0), f(x1)
for _ in range(self.max_iter): for _ in range(self.max_iter):
denom = (f1 - f0) denom = f1 - f0
if abs(denom) < 1e-20: if abs(denom) < 1e-20:
raise ConvergenceError("Secant encountered nearly zero denominator") raise ConvergenceError("Secant encountered nearly zero denominator")
x2 = x1 - f1*(x1 - x0)/denom x2 = x1 - f1 * (x1 - x0) / denom
if abs(x2 - x1) <= self.tol * (1.0 + abs(x2)): if abs(x2 - x1) <= self.tol * (1.0 + abs(x2)):
return x2 return x2
x0, x1 = x1, x2 x0, x1 = x1, x2
f0, f1 = f1, f(x1) f0, f1 = f1, f(x1)
raise ConvergenceError("Secant did not converge") raise ConvergenceError("Secant did not converge")
def trace(self) -> List[Dict[str, Any]]:
steps = []
x0, x1, f = self.x0, self.x1, self.f
f0, f1 = f(x0), f(x1)
for k in range(self.max_iter):
denom = f1 - f0
if abs(denom) < 1e-20:
raise ConvergenceError("Secant encountered nearly zero denominator")
x2 = x1 - f1 * (x1 - x0) / denom
steps.append(
{
"iter": k,
"x0": x0,
"x1": x1,
"x2": x2,
"f(x0)": f0,
"f(x1)": f1,
"error": abs(x2 - x1),
}
)
if abs(x2 - x1) <= self.tol * (1.0 + abs(x2)):
return steps
x0, x1 = x1, x2
f0, f1 = f1, f(x1)
raise ConvergenceError("Secant did not converge")
class NewtonRoot: class NewtonRoot:
def __init__(self, f: Callable[[float], float], df: Callable[[float], float], x0: float, tol: float = 1e-10, max_iter: int = 10_000): def __init__(
self,
f: Callable[[float], float],
df: Callable[[float], float],
x0: float,
tol: float = 1e-10,
max_iter: int = 10_000,
):
self.f, self.df, self.x0, self.tol, self.max_iter = f, df, x0, tol, max_iter self.f, self.df, self.x0, self.tol, self.max_iter = f, df, x0, tol, max_iter
def solve(self) -> float: def solve(self) -> float:
@@ -70,8 +162,50 @@ class NewtonRoot:
dfx = self.df(x) dfx = self.df(x)
if abs(dfx) < 1e-20: if abs(dfx) < 1e-20:
raise ConvergenceError("Derivative near zero in Newton method") raise ConvergenceError("Derivative near zero in Newton method")
x_new = x - self.f(x)/dfx x_new = x - self.f(x) / dfx
if abs(x_new - x) <= self.tol * (1.0 + abs(x_new)): if abs(x_new - x) <= self.tol * (1.0 + abs(x_new)):
return x_new return x_new
x = x_new x = x_new
raise ConvergenceError("Newton method did not converge") raise ConvergenceError("Newton method did not converge")
def trace(self) -> List[Dict[str, Any]]:
steps = []
x = self.x0
for k in range(self.max_iter):
dfx = self.df(x)
if abs(dfx) < 1e-20:
raise ConvergenceError("Derivative near zero in Newton method")
x_new = x - self.f(x) / dfx
steps.append(
{
"iter": k,
"x": x,
"f(x)": self.f(x),
"df(x)": dfx,
"x_new": x_new,
"error": abs(x_new - x),
}
)
if abs(x_new - x) <= self.tol * (1.0 + abs(x_new)):
return steps
x = x_new
raise ConvergenceError("Newton method did not converge")
def print_trace(steps: List[Dict[str, Any]]):
if not steps:
print("No steps recorded.")
return
# Get headers from dict keys
headers = list(steps[0].keys())
# Print header
print(" | ".join(f"{h:>10}" for h in headers))
print("-" * (13 * len(headers)))
# Print rows
for row in steps:
print(
" | ".join(
f"{row[h]:>10.6g}" if isinstance(row[h], (int, float)) else str(row[h])
for h in headers
)
)

View File

@@ -1,9 +1,17 @@
from __future__ import annotations from __future__ import annotations
from .linalg import Matrix, Vector, forward_substitution, backward_substitution from .linalg import Matrix, Vector, forward_substitution, backward_substitution
from .exceptions import NonSquareMatrixError, SingularMatrixError, NotSymmetricError, NotPositiveDefiniteError, ConvergenceError from .exceptions import (
NonSquareMatrixError,
SingularMatrixError,
NotSymmetricError,
NotPositiveDefiniteError,
ConvergenceError,
)
class LUDecomposition: class LUDecomposition:
"""LU with partial pivoting: PA = LU""" """LU decomposition with partial pivoting: PA = LU"""
def __init__(self, A: Matrix): def __init__(self, A: Matrix):
if not A.is_square(): if not A.is_square():
raise NonSquareMatrixError("A must be square") raise NonSquareMatrixError("A must be square")
@@ -11,6 +19,7 @@ class LUDecomposition:
self.L = Matrix.identity(self.n) self.L = Matrix.identity(self.n)
self.U = A.copy() self.U = A.copy()
self.P = Matrix.identity(self.n) self.P = Matrix.identity(self.n)
self.steps: list[tuple[int, Matrix, Matrix, Matrix]] = [] # store pivot steps
self._decompose() self._decompose()
def _decompose(self) -> None: def _decompose(self) -> None:
@@ -22,26 +31,47 @@ class LUDecomposition:
self.U.swap_rows(k, pivot_row) self.U.swap_rows(k, pivot_row)
self.P.swap_rows(k, pivot_row) self.P.swap_rows(k, pivot_row)
if k > 0: if k > 0:
self.L.data[k][:k], self.L.data[pivot_row][:k] = self.L.data[pivot_row][:k], self.L.data[k][:k] self.L.data[k][:k], self.L.data[pivot_row][:k] = (
for i in range(k+1, n): self.L.data[pivot_row][:k],
self.L.data[k][:k],
)
for i in range(k + 1, n):
m = self.U.data[i][k] / self.U.data[k][k] m = self.U.data[i][k] / self.U.data[k][k]
self.L.data[i][k] = m self.L.data[i][k] = m
for j in range(k, n): for j in range(k, n):
self.U.data[i][j] -= m * self.U.data[k][j] self.U.data[i][j] -= m * self.U.data[k][j]
# record step
self.steps.append((k, self.L.copy(), self.U.copy(), self.P.copy()))
def solve(self, b: Vector) -> Vector: def solve(self, b: Vector) -> Vector:
Pb = Vector([sum(self.P.data[i][j]*b[j] for j in range(self.n)) for i in range(self.n)]) Pb = Vector(
[
sum(self.P.data[i][j] * b[j] for j in range(self.n))
for i in range(self.n)
]
)
y = forward_substitution(self.L, Pb) y = forward_substitution(self.L, Pb)
x = backward_substitution(self.U, y) x = backward_substitution(self.U, y)
return x return x
def trace(self):
print("LU Decomposition Trace (steps of elimination)")
for k, L, U, P in self.steps:
print(f"\nStep {k}:")
print(f"L = {L}")
print(f"U = {U}")
print(f"P = {P}")
class GaussJordan: class GaussJordan:
"""Gauss-Jordan elimination."""
def __init__(self, A: Matrix): def __init__(self, A: Matrix):
if not A.is_square(): if not A.is_square():
raise NonSquareMatrixError("A must be square") raise NonSquareMatrixError("A must be square")
self.n = A.n self.n = A.n
self.A = A.copy() self.A = A.copy()
self.steps: list[tuple[int, Matrix]] = []
def solve(self, b: Vector) -> Vector: def solve(self, b: Vector) -> Vector:
n = self.n n = self.n
@@ -57,12 +87,26 @@ class GaussJordan:
if r == col: if r == col:
continue continue
factor = Ab.data[r][col] factor = Ab.data[r][col]
Ab.data[r] = [rv - factor*cv for rv, cv in zip(Ab.data[r], Ab.data[col])] Ab.data[r] = [
rv - factor * cv for rv, cv in zip(Ab.data[r], Ab.data[col])
]
# record step
self.steps.append((col, Ab.copy()))
return Vector(row[-1] for row in Ab.data) return Vector(row[-1] for row in Ab.data)
def trace(self):
print("Gauss-Jordan Trace (row reduction steps)")
for step, Ab in self.steps:
print(f"\nColumn {step}:")
print(f"Augmented matrix = {Ab}")
class Jacobi: class Jacobi:
def __init__(self, A: Matrix, b: Vector, tol: float = 1e-10, max_iter: int = 10_000): """Jacobi iterative method for Ax = b."""
def __init__(
self, A: Matrix, b: Vector, tol: float = 1e-10, max_iter: int = 10_000
):
if not A.is_square(): if not A.is_square():
raise NonSquareMatrixError("A must be square") raise NonSquareMatrixError("A must be square")
if A.n != len(b): if A.n != len(b):
@@ -71,27 +115,42 @@ class Jacobi:
self.b = b.copy() self.b = b.copy()
self.tol = tol self.tol = tol
self.max_iter = max_iter self.max_iter = max_iter
self.history: list[float] = []
def solve(self, x0: Vector | None = None) -> Vector: def solve(self, x0: Vector | None = None) -> Vector:
n = self.A.n n = self.A.n
x = Vector([0.0]*n) if x0 is None else x0.copy() x = Vector([0.0] * n) if x0 is None else x0.copy()
for _ in range(self.max_iter): for _ in range(self.max_iter):
x_new = [0.0]*n x_new = [0.0] * n
for i in range(n): for i in range(n):
diag = self.A.data[i][i] diag = self.A.data[i][i]
if abs(diag) < 1e-15: if abs(diag) < 1e-15:
raise SingularMatrixError("Zero diagonal entry in Jacobi") raise SingularMatrixError("Zero diagonal entry in Jacobi")
s = sum(self.A.data[i][j]*x[j] for j in range(n) if j != i) s = sum(self.A.data[i][j] * x[j] for j in range(n) if j != i)
x_new[i] = (self.b[i] - s) / diag x_new[i] = (self.b[i] - s) / diag
x_new = Vector(x_new) x_new = Vector(x_new)
if (x_new - x).norm_inf() <= self.tol * (1.0 + x_new.norm_inf()): r = (self.A @ x_new) - self.b
res_norm = r.norm2()
self.history.append(res_norm)
if res_norm <= self.tol * (1.0 + x_new.norm2()):
return x_new return x_new
x = x_new x = x_new
raise ConvergenceError("Jacobi did not converge within max_iter") raise ConvergenceError("Jacobi did not converge within max_iter")
def trace(self):
print("Jacobi Iteration Trace")
print(f"{'iter':>6} | {'residual norm':>14}")
print("-" * 26)
for k, res in enumerate(self.history):
print(f"{k:6d} | {res:14.6e}")
class GaussSeidel: class GaussSeidel:
def __init__(self, A: Matrix, b: Vector, tol: float = 1e-10, max_iter: int = 10_000): """Gauss-Seidel iterative method for Ax = b."""
def __init__(
self, A: Matrix, b: Vector, tol: float = 1e-10, max_iter: int = 10_000
):
if not A.is_square(): if not A.is_square():
raise NonSquareMatrixError("A must be square") raise NonSquareMatrixError("A must be square")
if A.n != len(b): if A.n != len(b):
@@ -100,52 +159,75 @@ class GaussSeidel:
self.b = b.copy() self.b = b.copy()
self.tol = tol self.tol = tol
self.max_iter = max_iter self.max_iter = max_iter
self.history: list[float] = []
def solve(self, x0: Vector | None = None) -> Vector: def solve(self, x0: Vector | None = None) -> Vector:
n = self.A.n n = self.A.n
x = Vector([0.0]*n) if x0 is None else x0.copy() x = Vector([0.0] * n) if x0 is None else x0.copy()
for _ in range(self.max_iter): for _ in range(self.max_iter):
x_old = x.copy() x_old = x.copy()
for i in range(n): for i in range(n):
diag = self.A.data[i][i] diag = self.A.data[i][i]
if abs(diag) < 1e-15: if abs(diag) < 1e-15:
raise SingularMatrixError("Zero diagonal entry in Gauss-Seidel") raise SingularMatrixError("Zero diagonal entry in Gauss-Seidel")
s1 = sum(self.A.data[i][j]*x[j] for j in range(i)) s1 = sum(self.A.data[i][j] * x[j] for j in range(i))
s2 = sum(self.A.data[i][j]*x_old[j] for j in range(i+1, n)) s2 = sum(self.A.data[i][j] * x_old[j] for j in range(i + 1, n))
x[i] = (self.b[i] - s1 - s2) / diag x[i] = (self.b[i] - s1 - s2) / diag
if (x - x_old).norm_inf() <= self.tol * (1.0 + x.norm_inf()): r = (self.A @ x) - self.b
res_norm = r.norm2()
self.history.append(res_norm)
if res_norm <= self.tol * (1.0 + x.norm2()):
return x return x
raise ConvergenceError("Gauss-Seidel did not converge within max_iter") raise ConvergenceError("Gauss-Seidel did not converge within max_iter")
def trace(self):
print("Gauss-Seidel Iteration Trace")
print(f"{'iter':>6} | {'residual norm':>14}")
print("-" * 26)
for k, res in enumerate(self.history):
print(f"{k:6d} | {res:14.6e}")
class Cholesky: class Cholesky:
"""A = L L^T for SPD matrices.""" """Cholesky factorization A = L L^T for SPD matrices."""
def __init__(self, A: Matrix): def __init__(self, A: Matrix):
if not A.is_square(): if not A.is_square():
raise NonSquareMatrixError("A must be square") raise NonSquareMatrixError("A must be square")
n = A.n n = A.n
for i in range(n): for i in range(n):
for j in range(i+1, n): for j in range(i + 1, n):
if abs(A.data[i][j] - A.data[j][i]) > 1e-12: if abs(A.data[i][j] - A.data[j][i]) > 1e-12:
raise NotSymmetricError("Matrix is not symmetric") raise NotSymmetricError("Matrix is not symmetric")
self.n = n self.n = n
self.L = Matrix.zeros(n, n) self.L = Matrix.zeros(n, n)
self.steps: list[tuple[int, Matrix]] = []
self._decompose(A.copy()) self._decompose(A.copy())
def _decompose(self, A: Matrix) -> None: def _decompose(self, A: Matrix) -> None:
n = self.n n = self.n
for i in range(n): for i in range(n):
for j in range(i+1): for j in range(i + 1):
s = sum(self.L.data[i][k]*self.L.data[j][k] for k in range(j)) s = sum(self.L.data[i][k] * self.L.data[j][k] for k in range(j))
if i == j: if i == j:
val = A.data[i][i] - s val = A.data[i][i] - s
if val <= 0.0: if val <= 0.0:
raise NotPositiveDefiniteError("Matrix is not positive definite") raise NotPositiveDefiniteError(
"Matrix is not positive definite"
)
self.L.data[i][j] = val**0.5 self.L.data[i][j] = val**0.5
else: else:
self.L.data[i][j] = (A.data[i][j] - s) / self.L.data[j][j] self.L.data[i][j] = (A.data[i][j] - s) / self.L.data[j][j]
# record after each row i
self.steps.append((i, self.L.copy()))
def solve(self, b: Vector) -> Vector: def solve(self, b: Vector) -> Vector:
y = forward_substitution(self.L, b) y = forward_substitution(self.L, b)
x = backward_substitution(self.L.transpose(), y) x = backward_substitution(self.L.transpose(), y)
return x return x
def trace(self):
print("Cholesky Decomposition Trace")
for i, L in self.steps:
print(f"\nRow {i}:")
print(f"L = {L}")

47
tutorials/README.md Normal file
View File

@@ -0,0 +1,47 @@
# Tutorial Series
This package comes with a set of Jupyter notebooks designed as a structured tutorial series in **numerical methods**, both mathematically rigorous and hands-on with code.
## Core Tutorials
1. [Tutorial 1: Vectors and Matrices](./tutorial1_vectors_matrices.ipynb)
- Definitions of vectors and matrices.
- Vector operations: addition, scalar multiplication, dot product, norms.
- Matrix operations: addition, multiplication, transpose, inverse.
- Matrix and vector norms.
- Examples with `numethods.linalg`.
2. [Tutorial 2: Linear Systems of Equations](./tutorial2_linear_systems.ipynb)
- Gaussian elimination and GaussJordan.
- LU decomposition.
- Cholesky decomposition.
- Iterative methods: Jacobi and Gauss-Seidel.
- Examples with `numethods.solvers`.
3. [Tutorial 3: Orthogonalization and QR Factorization](./tutorial3_orthogonalization.ipynb)
- Inner products and orthogonality.
- GramSchmidt process (classical and modified).
- Householder reflections.
- QR decomposition and applications.
- Examples with `numethods.orthogonal`.
4. [Tutorial 4: Root-Finding Methods](./tutorial4_root_finding.ipynb)
- Bisection method.
- Fixed-point iteration.
- Newtons method.
- Secant method.
- Convergence analysis and error behavior.
- Trace outputs for iteration history.
- Examples with `numethods.roots`.
- [Polynomial Regression Demo](./polynomial_regression.ipynb)
- Step-by-step example of polynomial regression.
- Shows how to fit polynomials of different degrees to data.
- Visualizes fitted curves against the original data.
---

View File

@@ -0,0 +1,476 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "a9d0ab82",
"metadata": {},
"source": [
"# Tutorial 1 - Vectors & Matrices in `numethods`\n",
"\n",
"---\n",
"\n",
"## 1. Introduction\n",
"\n",
"In numerical computing, **vectors** and **matrices** are the basic objects. \n",
"Almost every algorithm (linear solvers, eigenvalue problems, optimization, curve fitting, etc.) is built upon them. \n",
"\n",
"In this tutorial, we will:\n",
"- Define what vectors and matrices are.\n",
"- Review their basic operations (addition, scalar multiplication, multiplication).\n",
"- Define and compute vector and matrix **norms**.\n",
"- Show how these operations work in the `numethods` package."
]
},
{
"cell_type": "markdown",
"id": "b91edc5c",
"metadata": {},
"source": [
"## 2. Importing our package"
]
},
{
"cell_type": "code",
"execution_count": 1,
"id": "2107103a",
"metadata": {},
"outputs": [],
"source": [
"from numethods.linalg import Vector, Matrix"
]
},
{
"cell_type": "markdown",
"id": "f64a4d9a",
"metadata": {},
"source": [
"## 3. What is a vector?\n",
"\n",
"A **vector** is an ordered collection of numbers (scalars). \n",
"We can think of a vector as a column:\n",
"\n",
"$$\n",
"v =\n",
"\\begin{bmatrix}\n",
"v_1 \\\\\n",
"v_2 \\\\\n",
"\\vdots \\\\\n",
"v_n\n",
"\\end{bmatrix}, \\quad v \\in \\mathbb{R}^n\n",
"$$\n",
"\n",
"or as a row:\n",
"\n",
"$$\n",
"v^T = \\begin{bmatrix} v_1 & v_2 & \\cdots & v_n \\end{bmatrix}.\n",
"$$\n",
"\n",
"### Example\n",
"A vector in $\\mathbb{R}^3$:\n",
"\n",
"$$\n",
"v = \\begin{bmatrix} 1 \\\\ 2 \\\\ 3 \\end{bmatrix}.\n",
"$$"
]
},
{
"cell_type": "code",
"execution_count": 2,
"id": "46c96e42",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"v = Vector([1.0, 2.0, 3.0])\n"
]
}
],
"source": [
"v = Vector([1, 2, 3])\n",
"print(\"v =\", v)"
]
},
{
"cell_type": "markdown",
"id": "5e951a1b",
"metadata": {},
"source": [
"## 4. What is a matrix?\n",
"\n",
"A **matrix** is a rectangular array of numbers with rows and columns:\n",
"\n",
"$$\n",
"A =\n",
"\\begin{bmatrix}\n",
"a_{11} & a_{12} & \\cdots & a_{1n} \\\\\n",
"a_{21} & a_{22} & \\cdots & a_{2n} \\\\\n",
"\\vdots & \\vdots & \\ddots & \\vdots \\\\\n",
"a_{m1} & a_{m2} & \\cdots & a_{mn}\n",
"\\end{bmatrix}, \\quad A \\in \\mathbb{R}^{m \\times n}\n",
"$$"
]
},
{
"cell_type": "code",
"execution_count": 3,
"id": "a092da2f",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"A =\n",
" Matrix([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0], [7.0, 8.0, 9.0]])\n"
]
}
],
"source": [
"A = Matrix([[1, 2, 3],\n",
" [4, 5, 6],\n",
" [7, 8, 9]])\n",
"print(\"A =\\n\", A)"
]
},
{
"cell_type": "markdown",
"id": "4f0d9ec4",
"metadata": {},
"source": [
"## 5. Basic operations\n",
"\n",
"### 5.1 Vector addition and subtraction\n",
"For $u, v \\in \\mathbb{R}^n$:\n",
"\n",
"$$\n",
"u + v = \\begin{bmatrix} u_1 + v_1 \\\\ u_2 + v_2 \\\\ \\vdots \\\\ u_n + v_n \\end{bmatrix},\n",
"\\quad\n",
"u - v = \\begin{bmatrix} u_1 - v_1 \\\\ u_2 - v_2 \\\\ \\vdots \\\\ u_n - v_n \\end{bmatrix}.\n",
"$$"
]
},
{
"cell_type": "code",
"execution_count": 4,
"id": "b7571e71",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"u + v = Vector([4.0, 4.0, 4.0])\n",
"u - v = Vector([2.0, 0.0, -2.0])\n"
]
}
],
"source": [
"u = Vector([3, 2, 1])\n",
"v = Vector([1, 2, 3])\n",
"\n",
"print(\"u + v =\", u + v)\n",
"print(\"u - v =\", u - v)"
]
},
{
"cell_type": "markdown",
"id": "1aa8f396",
"metadata": {},
"source": [
"### 5.2 Scalar multiplication\n",
"For $\\alpha \\in \\mathbb{R}, v \\in \\mathbb{R}^n$:\n",
"\n",
"$$\n",
"\\alpha v = \\begin{bmatrix} \\alpha v_1 \\\\ \\alpha v_2 \\\\ \\vdots \\\\ \\alpha v_n \\end{bmatrix}.\n",
"$$"
]
},
{
"cell_type": "code",
"execution_count": 5,
"id": "b4168dfe",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"2 * v = Vector([2.0, 4.0, 6.0])\n",
"v * 2 = Vector([2.0, 4.0, 6.0])\n"
]
}
],
"source": [
"v = Vector([1, 2, 3])\n",
"\n",
"print(\"2 * v =\", 2 * v)\n",
"print(\"v * 2 =\", v * 2)"
]
},
{
"cell_type": "markdown",
"id": "e4919b59",
"metadata": {},
"source": [
"### 5.3 Matrix addition and subtraction\n",
"For $A, B \\in \\mathbb{R}^{m \\times n}$:\n",
"\n",
"$$\n",
"A + B = [ a_{ij} + b_{ij} ], \\quad\n",
"A - B = [ a_{ij} - b_{ij} ].\n",
"$$"
]
},
{
"cell_type": "code",
"execution_count": 6,
"id": "bd544880",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"A + B =\n",
" Matrix([[6.0, 8.0], [10.0, 12.0]])\n",
"A - B =\n",
" Matrix([[-4.0, -4.0], [-4.0, -4.0]])\n"
]
}
],
"source": [
"A = Matrix([[1, 2], [3, 4]])\n",
"B = Matrix([[5, 6], [7, 8]])\n",
"\n",
"print(\"A + B =\\n\", A + B)\n",
"print(\"A - B =\\n\", A - B)"
]
},
{
"cell_type": "markdown",
"id": "0a1fb7f1",
"metadata": {},
"source": [
"### 5.4 Matrix-Vector multiplication\n",
"For $A \\in \\mathbb{R}^{m \\times n}, v \\in \\mathbb{R}^n$:\n",
"\n",
"$$\n",
"(Av)_i = \\sum_{j=1}^n a_{ij} v_j.\n",
"$$"
]
},
{
"cell_type": "code",
"execution_count": 7,
"id": "d65fd20e",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"A @ v = Vector([-2.0, -2.0, -2.0])\n"
]
}
],
"source": [
"A = Matrix([[1, 2, 3],\n",
" [4, 5, 6],\n",
" [7, 8, 9]])\n",
"v = Vector([1, 0, -1])\n",
"\n",
"print(\"A @ v =\", A @ v)"
]
},
{
"cell_type": "markdown",
"id": "8c2d30ec",
"metadata": {},
"source": [
"### 5.5 Matrix-Matrix multiplication\n",
"For $A \\in \\mathbb{R}^{m \\times n}, B \\in \\mathbb{R}^{n \\times p}$:\n",
"\n",
"$$\n",
"(AB)_{ij} = \\sum_{k=1}^n a_{ik} b_{kj}.\n",
"$$"
]
},
{
"cell_type": "code",
"execution_count": 8,
"id": "f46a96ec",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"A @ B =\n",
" Matrix([[4.0, 4.0], [10.0, 8.0]])\n"
]
}
],
"source": [
"A = Matrix([[1, 2],\n",
" [3, 4]])\n",
"B = Matrix([[2, 0],\n",
" [1, 2]])\n",
"\n",
"print(\"A @ B =\\n\", A @ B)"
]
},
{
"cell_type": "markdown",
"id": "470e38cb",
"metadata": {},
"source": [
"### 5.6 Transpose\n",
"For $A \\in \\mathbb{R}^{m \\times n}$:\n",
"\n",
"$$\n",
"A^T_{ij} = A_{ji}.\n",
"$$"
]
},
{
"cell_type": "code",
"execution_count": 9,
"id": "42168bd4",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"A^T =\n",
" Matrix([[1.0, 4.0], [2.0, 5.0], [3.0, 6.0]])\n"
]
}
],
"source": [
"A = Matrix([[1, 2, 3],\n",
" [4, 5, 6]])\n",
"\n",
"print(\"A^T =\\n\", A.T)"
]
},
{
"cell_type": "markdown",
"id": "226efb8f",
"metadata": {},
"source": [
"## 6. Norms\n",
"\n",
"Norms measure the **size** or **length** of vectors and matrices."
]
},
{
"cell_type": "markdown",
"id": "120cf212",
"metadata": {},
"source": [
"### 6.1 Vector norms\n",
"- **1-norm**: $\\|v\\|_1 = \\sum |v_i|$ \n",
"- **2-norm (Euclidean)**: $\\|v\\|_2 = \\sqrt{\\sum v_i^2}$ \n",
"- **∞-norm**: $\\|v\\|_\\infty = \\max |v_i|$"
]
},
{
"cell_type": "code",
"execution_count": 10,
"id": "3b187412",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"‖v‖₁ = 12.0\n",
"‖v‖₂ = 7.0710678118654755\n",
"‖v‖∞ = 5.0\n"
]
}
],
"source": [
"v = Vector([3, -4, 5])\n",
"\n",
"print(\"‖v‖₁ =\", v.norm())\n",
"print(\"‖v‖₂ =\", v.norm2())\n",
"print(\"‖v‖∞ =\", v.norm_inf())"
]
},
{
"cell_type": "markdown",
"id": "3ad8369e",
"metadata": {},
"source": [
"### 6.2 Matrix norms\n",
"- **Frobenius norm**: $\\|A\\|_F = \\sqrt{\\sum_{i,j} a_{ij}^2}$ \n",
"- **1-norm** (maximum column sum): $\\|A\\|_1 = \\max_j \\sum_i |a_{ij}|$ \n",
"- **∞-norm** (maximum row sum): $\\|A\\|_\\infty = \\max_i \\sum_j |a_{ij}|$"
]
},
{
"cell_type": "code",
"execution_count": 11,
"id": "72a8ae7c",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"‖A‖_F = 5.477225575051661\n",
"‖A‖₁ = 6.0\n",
"‖A‖∞ = 7.0\n"
]
}
],
"source": [
"A = Matrix([[1, -2],\n",
" [3, 4]])\n",
"\n",
"print(\"‖A‖_F =\", A.norm_fro())\n",
"print(\"‖A‖₁ =\", A.norm())\n",
"print(\"‖A‖∞ =\", A.norm_inf())"
]
},
{
"cell_type": "markdown",
"id": "eab94be3",
"metadata": {},
"source": [
"## 7. Summary\n",
"- A **vector** is an element of $\\mathbb{R}^n$, a list of numbers. \n",
"- A **matrix** is a rectangular array of numbers $\\mathbb{R}^{m \\times n}$. \n",
"- We can add, subtract, and scale vectors/matrices. \n",
"- Multiplication extends naturally: matrix-vector and matrix-matrix. \n",
"- **Transpose** flips rows and columns. \n",
"- **Norms** measure the size of vectors and matrices."
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.10.11"
}
},
"nbformat": 4,
"nbformat_minor": 5
}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,309 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "b2277afb",
"metadata": {},
"source": [
"# Tutorial 3 - Orthogonalization & QR Decomposition"
]
},
{
"cell_type": "markdown",
"id": "1e4a68da",
"metadata": {},
"source": [
"\n",
"In this tutorial, we study **orthogonalization** methods and the **QR decomposition**, which are central to numerical linear algebra.\n",
"\n",
"We will cover:\n",
"\n",
"- Orthogonal and orthonormal vectors\n",
"- QR decomposition\n",
"- Classical Gram-Schmidt\n",
"- Modified Gram-Schmidt\n",
"- Householder transformations\n",
"- Applications: least squares\n",
"- Examples with the `numethods` package\n"
]
},
{
"cell_type": "markdown",
"id": "515dc61f",
"metadata": {},
"source": [
"\n",
"## 1. Why Orthogonalization?\n",
"\n",
"- Orthogonal vectors are easier to work with numerically.\n",
"- Many algorithms are more **stable** when using orthogonal bases.\n",
"- Key applications:\n",
" - Solving **least squares problems**\n",
" - Computing **eigenvalues**\n",
" - Ensuring numerical stability in projections\n"
]
},
{
"cell_type": "markdown",
"id": "af61a18f",
"metadata": {},
"source": [
"\n",
"## 2. Definitions\n",
"\n",
"### Orthogonal and Orthonormal vectors\n",
"\n",
"Two vectors $u, v \\in \\mathbb{R}^n$ are **orthogonal** if\n",
"\n",
"$$ u \\cdot v = 0. $$\n",
"\n",
"A set of vectors $\\{q_1, \\dots, q_m\\}$ is **orthonormal** if\n",
"\n",
"$$ q_i \\cdot q_j = \\begin{cases} 1 & i = j, \\\\ 0 & i \\neq j. \\end{cases} $$\n"
]
},
{
"cell_type": "markdown",
"id": "4f2330f1",
"metadata": {},
"source": [
"\n",
"### QR Decomposition\n",
"\n",
"For any $A \\in \\mathbb{R}^{m \\times n}$ with linearly independent columns, we can write\n",
"\n",
"$$ A = QR, $$\n",
"\n",
"- $Q \\in \\mathbb{R}^{m \\times n}$ has orthonormal columns ($Q^T Q = I$)\n",
"- $R \\in \\mathbb{R}^{n \\times n}$ is upper triangular\n"
]
},
{
"cell_type": "markdown",
"id": "66567b83",
"metadata": {},
"source": [
"\n",
"## 3. Gram-Schmidt Orthogonalization\n",
"\n",
"### Classical Gram-Schmidt (CGS)\n",
"\n",
"Given linearly independent vectors $a_1, \\dots, a_n$:\n",
"\n",
"$$\n",
"q_1 = \\frac{a_1}{\\|a_1\\|}\n",
"$$\n",
"$$\n",
"q_k = \\frac{a_k - \\sum_{j=1}^{k-1} (q_j \\cdot a_k) q_j}{\\left\\|a_k - \\sum_{j=1}^{k-1} (q_j \\cdot a_k) q_j\\right\\|}, \\qquad k = 2, \\ldots, n\n",
"$$\n",
"\n",
"Matrix form:\n",
"\n",
"$$ A = QR, \\quad R_{jk} = q_j^T a_k. $$\n",
"\n",
"⚠️ CGS can lose orthogonality in finite precision arithmetic.\n"
]
},
{
"cell_type": "code",
"execution_count": 1,
"id": "3fe97ce3",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Q (CGS): Matrix([[0.8164965809277261, -0.5520524474738834], [0.4082482904638631, 0.7590721152765896], [0.4082482904638631, 0.34503277967117707]])\n",
"R (CGS): Matrix([[2.449489742783178, 0.4082482904638631], [0.0, 2.41522945769824]])\n"
]
}
],
"source": [
"from numethods import Matrix, Vector\n",
"from numethods import QRGramSchmidt, QRModifiedGramSchmidt, QRHouseholder, LeastSquaresSolver\n",
"\n",
"# Example matrix\n",
"A = Matrix([[2, -1], [1, 2], [1, 1]])\n",
"\n",
"# Classical Gram-Schmidt\n",
"qrg = QRGramSchmidt(A)\n",
"print(\"Q (CGS):\", qrg.Q)\n",
"print(\"R (CGS):\", qrg.R)"
]
},
{
"cell_type": "markdown",
"id": "ba84b59a",
"metadata": {},
"source": [
"\n",
"### Modified Gram-Schmidt (MGS)\n",
"\n",
"Same idea, but orthogonalization is done step by step:\n",
"\n",
"```\n",
"for k = 1..n:\n",
" q_k = a_k\n",
" for j = 1..k-1:\n",
" r_jk = q_j^T q_k\n",
" q_k = q_k - r_jk q_j\n",
" r_kk = ||q_k||\n",
" q_k = q_k / r_kk\n",
"```\n",
"MGS is more stable than CGS.\n"
]
},
{
"cell_type": "code",
"execution_count": 2,
"id": "e01e25ff",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Q (MGS): Matrix([[0.8164965809277261, -0.5520524474738834], [0.4082482904638631, 0.7590721152765896], [0.4082482904638631, 0.34503277967117707]])\n",
"R (MGS): Matrix([[2.449489742783178, 0.4082482904638631], [0.0, 2.41522945769824]])\n"
]
}
],
"source": [
"# Modified Gram-Schmidt\n",
"qrm = QRModifiedGramSchmidt(A)\n",
"print(\"Q (MGS):\", qrm.Q)\n",
"print(\"R (MGS):\", qrm.R)\n"
]
},
{
"cell_type": "markdown",
"id": "d893d189",
"metadata": {},
"source": [
"\n",
"## 4. Householder Reflections\n",
"\n",
"A more stable method uses **Householder matrices**.\n",
"\n",
"For a vector $x \\in \\mathbb{R}^m$:\n",
"\n",
"$$\n",
"v = x \\pm \\|x\\| e_1, \\quad H = I - 2 \\frac{vv^T}{v^T v}.\n",
"$$\n",
"\n",
"- $H$ is orthogonal ($H^T H = I$).\n",
"- Applying $H$ zeros out all but the first component of $x$.\n",
"\n",
"QR via Householder:\n",
"\n",
"$$\n",
"R = H_n H_{n-1} \\cdots H_1 A, \\quad Q = H_1^T H_2^T \\cdots H_n^T.\n",
"$$"
]
},
{
"cell_type": "code",
"execution_count": 3,
"id": "15dfc35c",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Q (Householder): Matrix([[-0.8164965809277258, 0.552052447473883, -0.16903085094570333], [-0.40824829046386296, -0.7590721152765892, -0.5070925528371099], [-0.40824829046386296, -0.34503277967117707, 0.8451542547285166]])\n",
"R (Householder): Matrix([[-2.449489742783177, -0.408248290463863], [2.220446049250313e-16, -2.415229457698238], [2.220446049250313e-16, 2.220446049250313e-16]])\n"
]
}
],
"source": [
"# Householder QR\n",
"qrh = QRHouseholder(A)\n",
"print(\"Q (Householder):\", qrh.Q)\n",
"print(\"R (Householder):\", qrh.R)\n"
]
},
{
"cell_type": "markdown",
"id": "2b6c612f",
"metadata": {},
"source": [
"\n",
"## 5. Applications of QR\n",
"\n",
"### Least Squares\n",
"\n",
"We want to solve\n",
"\n",
"$$ \\min_x \\Vert Ax - b \\Vert_2^2. $$\n",
"\n",
"If $A = QR$, then\n",
"\n",
"$$ \\min_x \\Vert Ax - b \\Vert_2^2 = \\min_x \\Vert QRx - b \\Vert_2^2. $$\n",
"\n",
"Since $Q$ has orthonormal columns, and the normal equations boils down to\n",
"\n",
"$$ R x = Q^T b, $$\n",
"\n",
"we can therefore solve for $x$ by using back-substitution.\n"
]
},
{
"cell_type": "code",
"execution_count": 4,
"id": "25b399b7",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Least squares solution: Vector([1.0285714285714287, 0.828571428571429])\n"
]
}
],
"source": [
"# Least squares example\n",
"b = Vector([1, 2, 3])\n",
"x_ls = LeastSquaresSolver(A, b).solve()\n",
"print(\"Least squares solution:\", x_ls)"
]
},
{
"cell_type": "markdown",
"id": "a94c88c8",
"metadata": {},
"source": [
"\n",
"## 6. Key Takeaways\n",
"\n",
"- CGS is simple but numerically unstable.\n",
"- MGS is more stable and preferred if using Gram-Schmidt.\n",
"- Householder QR is the standard in practice (stable and efficient).\n",
"- QR decomposition underlies least squares, eigenvalue methods, and more.\n"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.13.7"
}
},
"nbformat": 4,
"nbformat_minor": 5
}

View File

@@ -0,0 +1,366 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "3c51341a",
"metadata": {},
"source": [
"# Tutorial 4: Root-Finding Methods\n",
"---\n",
"In this tutorial, we study classical **root-finding algorithms** for nonlinear equations. We will:\n",
"\n",
"- Define the root-finding problem mathematically\n",
"- Derive several algorithms (bisection, fixed-point, Newton, secant)\n",
"- Discuss convergence conditions and error behavior\n",
"- Compare methods with worked examples using the `numethods` package.\n"
]
},
{
"cell_type": "markdown",
"id": "2999aa4f",
"metadata": {},
"source": [
"## 1. Problem Setup and Notation\n",
"\n",
"We seek to solve a nonlinear scalar equation:\n",
"$$\n",
"f(x) = 0, \\quad f: \\mathbb{R} \\to \\mathbb{R}.\n",
"$$\n",
"with a continuously differentiable function.\n",
"\n",
"\n",
"### Root, residual, and error\n",
"- A **root** $r$ satisfies $f(r)=0$.\n",
"- **Absolute error:** $(e_k = |x_k - x^\\star|)$.\n",
"- **Residual:** $(r_k = |f(x_k)|)$. \n",
"Note that small residual does not always imply small error.\n",
"\n",
"### Multiplicity\n",
"A root $r$ has **multiplicity** $m$ if\n",
"$$\n",
"f(r) = f'(r) = \\dots = f^{(m-1)}(r) = 0, \\quad f^{(m)}(r) \\neq 0.\n",
"$$\n",
"If $x^\\star$ satisfies $f(x^\\star)=0$ and $f'(x^\\star)\\ne 0$, we say the root is **simple** (multiplicity 1).\n",
"\n",
"If $f'(x^\\star)=\\cdots=f^{(m-1)}(x^\\star)=0$ and $f^{(m)}(x^\\star)\\ne 0$, we say the root has **multiplicity** (m).\n",
"\n",
"- **Simple roots** ($m=1$): most methods converge rapidly.\n",
"- **Multiple roots** ($m>1$): convergence often slows.\n"
]
},
{
"cell_type": "markdown",
"id": "b610cea2",
"metadata": {},
"source": [
"## 2. Bisection Method\n",
"\n",
"**Assumption (Intermediate Value Theorem):** If f is continuous on $[a,b]$ and $f(a)f(b) < 0$,\n",
"then there exists $x^\\star$ in (a,b) with $f(x^\\star)=0$.\n",
"\n",
"- Assumes $f$ is continuous on $[a,b]$ with $f(a)f(b)<0$.\n",
"- Repeatedly bisect interval and select subinterval containing the root.\n",
"\n",
"**Iteration:**\n",
"$$\n",
"c_k = \\frac{a_k+b_k}{2}, \\quad f(c_k).\n",
"$$\n",
"\n",
"**Error bound:** interval length halves each step:\n",
"$$\n",
"|c_k-r| \\le \\frac{b-a}{2^k}.\n",
"$$\n",
"- Convergence: **linear**, guaranteed.\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "d26f75fb",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Bisection root: 1.4142135605216026\n",
"\n",
"Bisection Method Trace (x^2 - 2):\n",
" iter | a | b | c | f(a) | f(b) | f(c) | interval\n",
"--------------------------------------------------------------------------------------------------------\n",
" 0 | 0 | 2 | 1 | -2 | 2 | -1 | 2\n",
" 1 | 1 | 2 | 1.5 | -1 | 2 | 0.25 | 1\n",
" 2 | 1 | 1.5 | 1.25 | -1 | 0.25 | -0.4375 | 0.5\n",
" 3 | 1.25 | 1.5 | 1.375 | -0.4375 | 0.25 | -0.109375 | 0.25\n",
" 4 | 1.375 | 1.5 | 1.4375 | -0.109375 | 0.25 | 0.0664062 | 0.125\n",
" 5 | 1.375 | 1.4375 | 1.40625 | -0.109375 | 0.0664062 | -0.0224609 | 0.0625\n",
" 6 | 1.40625 | 1.4375 | 1.42188 | -0.0224609 | 0.0664062 | 0.0217285 | 0.03125\n",
" 7 | 1.40625 | 1.42188 | 1.41406 | -0.0224609 | 0.0217285 | -0.000427246 | 0.015625\n",
" 8 | 1.41406 | 1.42188 | 1.41797 | -0.000427246 | 0.0217285 | 0.0106354 | 0.0078125\n",
" 9 | 1.41406 | 1.41797 | 1.41602 | -0.000427246 | 0.0106354 | 0.00510025 | 0.00390625\n",
" 10 | 1.41406 | 1.41602 | 1.41504 | -0.000427246 | 0.00510025 | 0.00233555 | 0.00195312\n",
" 11 | 1.41406 | 1.41504 | 1.41455 | -0.000427246 | 0.00233555 | 0.000953913 | 0.000976562\n",
" 12 | 1.41406 | 1.41455 | 1.41431 | -0.000427246 | 0.000953913 | 0.000263274 | 0.000488281\n",
" 13 | 1.41406 | 1.41431 | 1.41418 | -0.000427246 | 0.000263274 | -8.20011e-05 | 0.000244141\n",
" 14 | 1.41418 | 1.41431 | 1.41425 | -8.20011e-05 | 0.000263274 | 9.06326e-05 | 0.00012207\n",
" 15 | 1.41418 | 1.41425 | 1.41422 | -8.20011e-05 | 9.06326e-05 | 4.31482e-06 | 6.10352e-05\n",
" 16 | 1.41418 | 1.41422 | 1.4142 | -8.20011e-05 | 4.31482e-06 | -3.88434e-05 | 3.05176e-05\n",
" 17 | 1.4142 | 1.41422 | 1.41421 | -3.88434e-05 | 4.31482e-06 | -1.72643e-05 | 1.52588e-05\n",
" 18 | 1.41421 | 1.41422 | 1.41421 | -1.72643e-05 | 4.31482e-06 | -6.47477e-06 | 7.62939e-06\n",
" 19 | 1.41421 | 1.41422 | 1.41421 | -6.47477e-06 | 4.31482e-06 | -1.07998e-06 | 3.8147e-06\n",
" 20 | 1.41421 | 1.41422 | 1.41421 | -1.07998e-06 | 4.31482e-06 | 1.61742e-06 | 1.90735e-06\n",
" 21 | 1.41421 | 1.41421 | 1.41421 | -1.07998e-06 | 1.61742e-06 | 2.68718e-07 | 9.53674e-07\n",
" 22 | 1.41421 | 1.41421 | 1.41421 | -1.07998e-06 | 2.68718e-07 | -4.05632e-07 | 4.76837e-07\n",
" 23 | 1.41421 | 1.41421 | 1.41421 | -4.05632e-07 | 2.68718e-07 | -6.84571e-08 | 2.38419e-07\n",
" 24 | 1.41421 | 1.41421 | 1.41421 | -6.84571e-08 | 2.68718e-07 | 1.0013e-07 | 1.19209e-07\n",
" 25 | 1.41421 | 1.41421 | 1.41421 | -6.84571e-08 | 1.0013e-07 | 1.58366e-08 | 5.96046e-08\n",
" 26 | 1.41421 | 1.41421 | 1.41421 | -6.84571e-08 | 1.58366e-08 | -2.63102e-08 | 2.98023e-08\n",
" 27 | 1.41421 | 1.41421 | 1.41421 | -2.63102e-08 | 1.58366e-08 | -5.23681e-09 | 1.49012e-08\n"
]
}
],
"source": [
"from numethods import Bisection, FixedPoint, NewtonRoot, Secant, print_trace\n",
"import math\n",
"\n",
"# Example function: f(x) = x^2 - 2\n",
"f = lambda x: x**2 - 2\n",
"df = lambda x: 2*x\n",
"\n",
"# Bisection\n",
"bisect = Bisection(f, 0, 2, tol=1e-8)\n",
"root_b = bisect.solve()\n",
"print('Bisection root:', root_b)\n",
"\n",
"steps = bisect.trace()\n",
"print(\"\\nBisection Method Trace (x^2 - 2):\")\n",
"print_trace(steps)"
]
},
{
"cell_type": "markdown",
"id": "3be4a510",
"metadata": {},
"source": [
"## 3. Fixed-Point Iteration\n",
"- Rewrite equation as $x=g(x)$.\n",
"- Iterate:\n",
"$$\n",
"x_{k+1} = g(x_k).\n",
"$$\n",
"\n",
"**Convergence theorem (Banach fixed-point):** If g is continuously differentiable near $(x^\\star)$ and\n",
"$$\n",
"|g'(x_\\star)| < 1,\n",
"$$\n",
"then for initial guesses $x_0$ sufficiently close to $x^\\star$, the iterates converge **linearly** to $x^\\star$ with asymptotic rate $|g'(x^\\star)|$.\n",
"\n",
"**Choice of g.** Different rearrangements yield different g's with different convergence properties.\n",
"A poor choice (with $(|g'|\\ge 1))$ can diverge.\n",
"\n",
"- Rate: linear with factor $|g'(r)|$.\n"
]
},
{
"cell_type": "code",
"execution_count": 2,
"id": "436ce6f6",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Fixed-point failed: Fixed-point iteration did not converge\n",
"\n",
"Fixed-Point Iteration Trace (x^2 - 2):\n",
" iter | x | x_new | error\n",
"----------------------------------------------------\n",
" 0 | 1 | 1.5 | 0.5\n",
" 1 | 1.5 | 1.41667 | 0.0833333\n",
" 2 | 1.41667 | 1.41422 | 0.00245098\n",
" 3 | 1.41422 | 1.41421 | 2.1239e-06\n",
" 4 | 1.41421 | 1.41421 | 1.59495e-12\n"
]
}
],
"source": [
"# Fixed point: g(x)=sqrt(2) ~ rewriting\n",
"g = lambda x: (2/x) # not always convergent, but demonstrates\n",
"try:\n",
" fp = FixedPoint(g, 1.0, tol=1e-8)\n",
" root_fp = fp.solve()\n",
" print('Fixed-point root:', root_fp)\n",
"except Exception as e:\n",
" print('Fixed-point failed:', e)\n",
"\n",
"g = lambda x: 0.5 * (x + 2 / x)\n",
"steps = FixedPoint(g, 1.0).trace()\n",
"print(\"\\nFixed-Point Iteration Trace (x^2 - 2):\")\n",
"print_trace(steps)"
]
},
{
"cell_type": "markdown",
"id": "40e66a30",
"metadata": {},
"source": [
"## 4. Newtons Method\n",
"From Taylor expansion:\n",
"$$\n",
"f(x) \\approx f(x_k) + f'(x_k)(x-x_k).\n",
"$$\n",
"Set $f(x)=0$ to solve for next iterate:\n",
"$$\n",
"x_{k+1} = x_k - \\frac{f(x_k)}{f'(x_k)}.\n",
"$$\n",
"\n",
"- **Quadratic convergence** for simple roots.\n",
"- For multiple roots: drops to linear.\n",
"- Requires derivative, sensitive to initial guess.\n"
]
},
{
"cell_type": "code",
"execution_count": 4,
"id": "7ebf9068",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Newton root: 1.4142135623730951\n",
"Newton Method Trace (x^2 - 2):\n",
" iter | x | f(x) | df(x) | x_new | error\n",
"------------------------------------------------------------------------------\n",
" 0 | 1 | -1 | 2 | 1.5 | 0.5\n",
" 1 | 1.5 | 0.25 | 3 | 1.41667 | 0.0833333\n",
" 2 | 1.41667 | 0.00694444 | 2.83333 | 1.41422 | 0.00245098\n",
" 3 | 1.41422 | 6.0073e-06 | 2.82843 | 1.41421 | 2.1239e-06\n",
" 4 | 1.41421 | 4.51061e-12 | 2.82843 | 1.41421 | 1.59472e-12\n"
]
}
],
"source": [
"# Newton\n",
"newton = NewtonRoot(f, df, 1.0, tol=1e-12)\n",
"root_n = newton.solve()\n",
"print('Newton root:', root_n)\n",
"\n",
"steps = newton.trace()\n",
"print(\"Newton Method Trace (x^2 - 2):\")\n",
"print_trace(steps)"
]
},
{
"cell_type": "markdown",
"id": "84888305",
"metadata": {},
"source": [
"## 5. Secant Method\n",
"- Avoids derivative by approximating slope with finite difference:\n",
"$$\n",
"x_{k+1} = x_k - f(x_k) \\frac{x_k - x_{k-1}}{f(x_k)-f(x_{k-1})}.\n",
"$$\n",
"\n",
"- Convergence order: $\\approx 1.618$ (superlinear).\n",
"- More efficient than Newton if derivative expensive.\n"
]
},
{
"cell_type": "code",
"execution_count": 5,
"id": "f2318bf3",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Secant root: 1.414213562373095\n",
"\n",
"Secant Method Trace (x^2 - 2):\n",
" iter | x0 | x1 | x2 | f(x0) | f(x1) | error\n",
"-------------------------------------------------------------------------------------------\n",
" 0 | 0 | 2 | 1 | -2 | 2 | 1\n",
" 1 | 2 | 1 | 1.33333 | 2 | -1 | 0.333333\n",
" 2 | 1 | 1.33333 | 1.42857 | -1 | -0.222222 | 0.0952381\n",
" 3 | 1.33333 | 1.42857 | 1.41379 | -0.222222 | 0.0408163 | 0.0147783\n",
" 4 | 1.42857 | 1.41379 | 1.41421 | 0.0408163 | -0.00118906 | 0.000418335\n",
" 5 | 1.41379 | 1.41421 | 1.41421 | -0.00118906 | -6.00729e-06 | 2.12421e-06\n",
" 6 | 1.41421 | 1.41421 | 1.41421 | -6.00729e-06 | 8.93146e-10 | 3.15775e-10\n",
" 7 | 1.41421 | 1.41421 | 1.41421 | 8.93146e-10 | -8.88178e-16 | 2.22045e-16\n"
]
}
],
"source": [
"# Secant\n",
"sec = Secant(f, 0, 2, tol=1e-12)\n",
"root_s = sec.solve()\n",
"print('Secant root:', root_s)\n",
"\n",
"steps = sec.trace()\n",
"print(\"\\nSecant Method Trace (x^2 - 2):\")\n",
"print_trace(steps)\n"
]
},
{
"cell_type": "markdown",
"id": "7ed8a0b5",
"metadata": {},
"source": [
"## 6. Stopping Criteria\n",
"We stop iteration when:\n",
"- $|f(x_k)| \\le \\varepsilon$ (residual small), or\n",
"- $|x_{k+1}-x_k| \\le \\varepsilon(1+|x_{k+1}|)$ (relative step small).\n"
]
},
{
"cell_type": "markdown",
"id": "833b538c",
"metadata": {},
"source": [
"## 7. Comparison of Methods\n",
"| Method | Requires derivative | Convergence rate | Guarantee? |\n",
"|--------|---------------------|------------------|------------|\n",
"| Bisection | No | Linear | Yes (if sign change) |\n",
"| Fixed-Point | No | Linear | Not always |\n",
"| Newton | Yes | Quadratic | Locally (good guess) |\n",
"| Secant | No | ~1.618 (superlinear) | Locally (good guess) |\n"
]
},
{
"cell_type": "markdown",
"id": "0d372aa0",
"metadata": {},
"source": [
"## 8. Exercises\n",
"1. Apply all four methods to $f(x)=\\cos x - x$.\n",
"2. Try Newtons method on $f(x)=(x-1)^2$ and compare convergence rate with $f(x)=x^2-2$.\n",
"3. Modify Secant to stop if denominator too small.\n"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.13.7"
}
},
"nbformat": 4,
"nbformat_minor": 5
}