import numpy as np from typing import Callable, Any, List, Tuple # Tolerance and maximum iterations TOL = 1e-10 MAX_ITER = 20000 def stationary_method( b: np.ndarray, method: str, get_diag_inv: Callable[[int, Any], float], get_off_diag_sum: Callable[[np.ndarray, int, Any], float], mat_vec_prod: Callable[[np.ndarray, Any], np.ndarray], omega: float = 1.0, **kwargs ) -> Tuple[np.ndarray, int, List[float]]: """ Solves Ax=b using stationary methods (Jacobi, Gauss-Seidel, SOR). """ n = len(b) x = np.zeros(n) r0 = b.copy() r0_norm = np.linalg.norm(r0) if r0_norm == 0: return x, 0, [0.0] residual_history = [] for k in range(MAX_ITER): # 1. Calculate current residual and its norm r = b - mat_vec_prod(x, **kwargs) res_norm = np.linalg.norm(r) # 2. Append to history residual_history.append(res_norm) # 3. Check for convergence if res_norm / r0_norm < TOL: return x, k, residual_history # 4. If not converged, perform the next iteration update if method == 'jacobi': x_old = x.copy() for i in range(n): off_diag_val = get_off_diag_sum(x_old, i, **kwargs) diag_inv_val = get_diag_inv(i, **kwargs) x[i] = diag_inv_val * (b[i] - off_diag_val) else: # Gauss-Seidel and SOR x_k_iter = x.copy() # Needed for SOR for i in range(n): off_diag_val = get_off_diag_sum(x, i, **kwargs) diag_inv_val = get_diag_inv(i, **kwargs) x_gs = diag_inv_val * (b[i] - off_diag_val) if method == 'gauss_seidel': x[i] = x_gs elif method == 'sor': x[i] = (1 - omega) * x_k_iter[i] + omega * x_gs # If loop finishes (MAX_ITER reached), calculate and append final residual r_final = b - mat_vec_prod(x, **kwargs) residual_history.append(np.linalg.norm(r_final)) return x, MAX_ITER, residual_history def steepest_descent( mat_vec_prod: Callable[[np.ndarray, Any], np.ndarray], b: np.ndarray, **kwargs ) -> Tuple[np.ndarray, int, List[float]]: """ Solves Ax=b using Steepest Descent, with corrected residual logging. """ x = np.zeros_like(b) r = b.copy() r0_norm = np.linalg.norm(r) if r0_norm == 0: return x, 0, [0.0] residual_history = [] for k in range(MAX_ITER): # 1. Current residual norm is from previous step (or initial) res_norm = np.linalg.norm(r) # 2. Append to history residual_history.append(res_norm) # 3. Check for convergence BEFORE the update if res_norm / r0_norm < TOL: return x, k, residual_history # 4. If not converged, compute update Ar = mat_vec_prod(r, **kwargs) r_dot_Ar = np.dot(r, Ar) if r_dot_Ar == 0: # Should not happen for SPD if r!=0 break alpha = res_norm**2 / r_dot_Ar x += alpha * r r -= alpha * Ar # If loop finishes, append final residual residual_history.append(np.linalg.norm(r)) return x, MAX_ITER, residual_history def conjugate_gradient( mat_vec_prod: Callable[[np.ndarray, Any], np.ndarray], b: np.ndarray, **kwargs ) -> Tuple[np.ndarray, int, List[float]]: """ Solves Ax=b using Conjugate Gradient, with corrected residual logging. """ x = np.zeros_like(b) r = b.copy() p = r.copy() r0_norm = np.linalg.norm(r) if r0_norm == 0: return x, 0, [0.0] res_norm_old = r0_norm residual_history = [] for k in range(MAX_ITER): # 1. Current residual norm is from previous step # 2. Append to history residual_history.append(res_norm_old) # 3. Check for convergence if res_norm_old / r0_norm < TOL: return x, k, residual_history # 4. If not converged, compute update Ap = mat_vec_prod(p, **kwargs) p_dot_Ap = np.dot(p, Ap) if p_dot_Ap == 0: break alpha = res_norm_old**2 / p_dot_Ap x += alpha * p r -= alpha * Ap res_norm_new = np.linalg.norm(r) beta = res_norm_new**2 / res_norm_old**2 p = r + beta * p res_norm_old = res_norm_new # If loop finishes, append final residual residual_history.append(res_norm_old) return x, MAX_ITER, residual_history