Files
math6601_projects/project1/Project1_Q3.py
2025-10-21 16:49:59 -04:00

194 lines
6.0 KiB
Python

import numpy as np
import matplotlib.pyplot as plt
# householder function from question 2
def householder_qr(A: np.ndarray) -> tuple[np.ndarray, np.ndarray]:
"""
Perform Householder QR factorization on matrix A to obtain orthogonal matrix Q.
Parameters:
A (np.ndarray): The input matrix to be orthogonalized.
Returns:
tuple[np.ndarray, np.ndarray]: The orthogonal matrix Q and upper triangular matrix R such that A = QR.
"""
m, n = A.shape
Q = np.eye(m, dtype=A.dtype)
R = A.copy()
for k in range(n):
# Extract the vector x
# Target: x -> [||x||, 0, 0, ..., 0]^T
x = R[k:, k] # x = [a_k, a_(k+1), ..., a_m]^T
s = np.sign(x[0]) if x[0] != 0 else 1
e = np.zeros_like(x) # e = [||x||, 0, 0, ..., 0]^T
e[0] = s * np.linalg.norm(x)
# Compute the Householder vector
# x - e is the reflection plane normal
u = x - e
if np.linalg.norm(u) < 1e-14:
continue # Skip the transformation if u is too small
v = u / np.linalg.norm(u)
# Apply the Householder transformation to R[k:, k:]
R[k:, k:] -= 2 * np.outer(v, v.conj().T @ R[k:, k:])
# Update Q
# Q_k = I - 2 v v^*
Q_k = np.eye(m, dtype=A.dtype)
Q_k[k:, k:] -= 2 * np.outer(v, v.conj().T)
Q = Q @ Q_k
return Q, R
def householder_lstsq(A: np.ndarray, b: np.ndarray) -> np.ndarray:
"""
Solve the least squares problem Ax = b using Householder reflections.
Parameters:
A (np.ndarray): The input matrix A.
b (np.ndarray): The right-hand side vector b.
Returns:
np.ndarray: The solution vector x.
"""
"""
Ax = b, A = QR
Rx = Q^*b
x = R^{-1}Q^*b
R is upper triangular
Q is orthogonal
"""
m, n = A.shape # m is N + 1, n is n + 1
Q, R = householder_qr(A)
# Compute Q^*b
Qt_b = Q.conj().T @ b
# Solve Rx = Q^*b using back substitution
x = np.zeros(n, dtype=A.dtype)
for i in range(n-1, -1, -1):
x[i] = (Qt_b[i] - R[i, i+1:] @ x[i+1:]) / R[i, i]
return x
def f1(t: float) -> float:
"""
f1(t) = 1 / (1 + t^2)
"""
return 1.0 / (1.0 + t**2)
def build_A(N: int, n: int, f: callable, f_range: tuple) -> tuple[np.ndarray, np.ndarray]:
"""
Build the matrix A for the least squares problem.
Parameters:
n + 1 (int): The number of columns in the matrix A.
f_range (tuple): The range of the function f.
Returns:
np.ndarray: The constructed matrix A.
"""
"""
{t_j}j=0->N, t_0=f_range[0], t_N=f_range[1], t_j divides [f_range[0], f_range[1]] equally
A = [[1, t_0, t_0^2, t_0^3, ..., t_0^n],
[1, t_1, t_1^2, t_1^3, ..., t_1^n],
...
[1, t_N, t_N^2, t_N^3, ..., t_N^n]] (N + 1) x (n + 1)
b = [f(t_0), f(t_1), ..., f(t_N)]^T (N + 1) x 1
x = [a_0, a_1, a_2, ..., a_n]^T (n + 1) x 1
Ax = b, x is the coeffs of the polynomial
"""
A = np.zeros((N + 1, n + 1), dtype=float)
t = np.linspace(f_range[0], f_range[1], N + 1)
for i in range(N + 1):
A[i, :] = [t[i]**j for j in range(n + 1)]
b = np.array([f(t_i) for t_i in t])
return A, b
def question_3c():
Ns = [30, 30, 30]
ns = [5, 15, 30]
# p(t) = a_n t^n + a_(n-1) t^(n-1) + ... + a_1 t + a_0
# plot f(t) and p(t) on the same figure for each (N, n)
t = np.linspace(-5, 5, 1000)
f_t = f1(t)
plt.figure(figsize=(15, 5))
for i in range(len(Ns)):
N = Ns[i]
n = ns[i]
A = np.zeros((N + 1, n + 1), dtype=float)
A, b = build_A(N, n, f1, (-5, 5))
x = householder_lstsq(A, b)
p_t = sum([x[j] * t**j for j in range(n + 1)])
plt.subplot(1, 3, i + 1)
plt.plot(t, f_t, label='f(t)', color='blue')
plt.plot(t, p_t, label='p(t)', color='red', linestyle='--')
plt.title(f'N={N}, n={n}')
plt.xlabel('t')
plt.ylabel('y')
plt.legend()
plt.grid(True, alpha=0.3)
plt.suptitle('Least Squares Polynomial Approximation using Householder QR')
plt.tight_layout(rect=[0, 0.03, 1, 0.95])
plt.savefig('Project1_A3c_least_squares_approximation.png')
#plt.show()
def f2(n:int, t: float) -> float:
"""
f2(t) = t^n + t^(n-1) + ... + t^2 + t + 1
"""
return sum([t**i for i in range(n + 1)])
def question_3d():
Ns = [25, 50, 100]
ns = Ns
t = np.linspace(-5, 5, 1000)
f_t = f1(t)
plt.figure(figsize=(15, 10))
for i in range(len(Ns)):
N = Ns[i]
n = ns[i]
A = np.zeros((N + 1, n + 1), dtype=float)
A, b = build_A(N, n, f1, (-5, 5))
x = householder_lstsq(A, b)
p_t = sum([x[j] * t**j for j in range(n + 1)])
plt.subplot(1, 3, i + 1)
plt.plot(t, f_t, label='f(t)', color='blue')
plt.plot(t, p_t, label='p(t)', color='red', linestyle='--')
plt.title(f'N={N}, n={n}')
plt.xlabel('t')
plt.ylabel('y')
plt.legend()
plt.grid(True, alpha=0.3)
plt.suptitle('Least Squares Polynomial Approximation using Householder QR')
plt.tight_layout(rect=[0, 0.03, 1, 0.95])
plt.savefig('Project1_A3d_least_squares_approximation.png')
#plt.show()
def question_3e():
# n = 4, 6, 8, 10, ..., 20
ns = list(range(4, 21, 2))
# N = 2n
# quantity is log10||(a_0-1,...,a_n-1)||_2)
losses = []
for n in ns:
N = 2 * n
A, b = build_A(N, n, lambda t: f2(n, t), (0, 1))
x = householder_lstsq(A, b)
true_coeffs = np.array([1.0 for _ in range(n + 1)])
loss = np.linalg.norm(x - true_coeffs)
losses.append(np.log10(loss))
plt.figure(figsize=(10, 6))
plt.plot(ns, losses, marker='o')
plt.xlabel('Polynomial Degree n')
plt.ylabel('log10(||a - true_coeffs||_2)')
plt.title('Coefficient Error vs Polynomial Degree using Householder QR')
plt.xticks(ns)
plt.grid(True, alpha=0.3)
plt.savefig('Project1_A3e_coefficient_error.png')
#plt.show()
if __name__ == "__main__":
question_3c()
question_3d()
question_3e()