import numpy as np import matplotlib.pyplot as plt from iterative_solvers import * # (a): Exact Solution def exact_solution(t: np.ndarray) -> np.ndarray: """Computes the exact solution u(t) = t/6 * (1 - t^2).""" return t / 6.0 * (1 - t**2) def plot_exact_solution(): """Plots the exact solution of the BVP.""" t_fine = np.linspace(0, 1, 500) u_exact_fine = exact_solution(t_fine) plt.figure(figsize=(10, 6)) plt.plot(t_fine, u_exact_fine, 'k-', label='Exact Solution $u(t) = t/6 \cdot (1 - t^2)$') plt.title('Exact Solution of $-u\'\'=t$ with $u(0)=u(1)=0$') plt.xlabel('$t$') plt.ylabel('$u(t)$') plt.grid(True) plt.legend() plt.savefig('problem1_exact_solution.png') # plt.show() print("Exact solution plot saved to 'problem1_exact_solution.png'") # (b): Helper functions for iterative methods def mat_vec_prod_p1(x: np.ndarray, n: int) -> np.ndarray: """Computes the matrix-vector product Ax for Problem 1.""" y = np.zeros(n) # A is tridiagonal with [ -1, 2, -1 ] # y_i = -x_{i-1} + 2x_i - x_{i+1} for i in range(n): term_prev = -x[i-1] if i > 0 else 0 term_next = -x[i+1] if i < n - 1 else 0 y[i] = term_prev + 2 * x[i] + term_next return y def get_diag_inv_p1(i: int, n: int) -> float: """Returns 1/A_ii for Problem 1. Diagonal elements are all 2, so inverse is 1/2.""" return 1.0 / 2.0 def get_off_diag_sum_p1(x: np.ndarray, i: int, n: int) -> float: """Computes sum_{j!=i} A_ij * x_j. For Problem 1, A_ij = -1 for j=i-1, i+1, else 0.""" sum_val = 0.0 if i > 0: sum_val += -x[i-1] if i < n - 1: sum_val += -x[i+1] return sum_val def solve_and_report(n: int): """ Solves the linear system for a given n and reports results. """ print(f"\n--- Solving for n = {n} ---") h = 1.0 / (n + 1) # Setup the right-hand side vector b t = np.linspace(h, 1.0 - h, n) b = (h**2) * t # Calculate SOR parameter omega omega = 2.0 / (1.0 + np.sin(np.pi * h)) # Store results iteration_counts = [] all_residuals = {} all_solutions = {} # Define methods and run solvers methods_to_run = { 'Jacobi': ('jacobi', {}), 'Gauss-Seidel': ('gauss_seidel', {}), 'SOR': ('sor', {'omega': omega}), 'Steepest Descent': (steepest_descent, {}), 'Conjugate Gradient': (conjugate_gradient, {}) } print("\nStarting iterative solvers...") for name, (method_func, params) in methods_to_run.items(): print(f"Running {name}...") common_args = {'b': b, 'n': n} if isinstance(method_func, str): solver_args = { 'method': method_func, 'get_diag_inv': get_diag_inv_p1, 'get_off_diag_sum': get_off_diag_sum_p1, 'mat_vec_prod': mat_vec_prod_p1, **common_args, **params } solution, iters, res_hist = stationary_method(**solver_args) else: solver_args = {'mat_vec_prod': mat_vec_prod_p1, **common_args} solution, iters, res_hist = method_func(**solver_args) iteration_counts.append((name, iters)) all_residuals[name] = res_hist all_solutions[name] = solution if iters < MAX_ITER else None # (c): Report iteration counts print("\n--- Iteration Counts ---") for name, iters in iteration_counts: status = "converged" if iters < MAX_ITER else "did NOT converge" print(f"{name:<20}: {iters} iterations ({status}, last residual = {all_residuals[name][-1]:.2e})") # (d): Plot residue history plt.figure(figsize=(12, 8)) for name, res_hist in all_residuals.items(): res_log = np.log10(np.array(res_hist) + 1e-20) plt.plot(res_log, label=name, linestyle='-') plt.title(f'Convergence History for n = {n}') plt.xlabel('Iteration Step (m)') plt.ylabel('log10(||r^(m)||_2)') plt.legend() plt.grid(True) # Set the y-axis limit to make the plot readable current_ylim = plt.ylim() plt.ylim(bottom=max(current_ylim[0], -13), top=current_ylim[1]) plt.savefig(f'problem1_convergence_n{n}.png') # plt.show() print(f"\nConvergence plot saved to 'problem1_convergence_n{n}.png'") if all_solutions: plt.figure(figsize=(12, 8)) # Plot exact solution on a fine grid t_fine = np.linspace(0, 1, 500) u_exact_fine = exact_solution(t_fine) plt.plot(t_fine, u_exact_fine, 'k-', label='Exact Solution', linewidth=1, zorder=10) # Black, thick, on top # Define some styles for the different numerical methods styles = { 'Jacobi': {'color': 'red', 'linestyle': '--'}, 'Gauss-Seidel': {'color': 'blue', 'linestyle': '-.'}, 'SOR': {'color': 'green', 'linestyle': ':'}, 'Steepest Descent':{'color': 'purple','linestyle': '--'}, 'Conjugate Gradient':{'color': 'orange','linestyle': '-.'}, } # Plot each numerical solution for name, solution in all_solutions.items(): # Add boundary points for a complete plot t_numerical_with_bounds = np.concatenate(([0], t, [1])) u_numerical_with_bounds = np.concatenate(([0], solution, [0])) style = styles.get(name, {'color': 'gray', 'linestyle': '-'}) plt.plot(t_numerical_with_bounds, u_numerical_with_bounds, label=f'Numerical ({name})', **style, linewidth=2) plt.title(f'Comparison of Exact and Numerical Solutions for n = {n}') plt.xlabel('$t$') plt.ylabel('$u(t)$') plt.legend() plt.grid(True) plt.savefig(f'problem1_all_solutions_comparison_n{n}.png') # plt.show() print(f"All solutions comparison plot saved to 'problem1_all_solutions_comparison_n{n}.png'") if __name__ == "__main__": # Part (a) plot_exact_solution() # Part (b), (c), (d) solve_and_report(n=20) solve_and_report(n=40)