import numpy as np
import matplotlib.pyplot as plt
228)
np.random.seed(
# Parameters
= 100 # Dimension of x
n = 10
mu = 100
L
def generate_problem(n=100, mu=mu, L=L, problem_type="clustered"):
228)
np.random.seed(if problem_type == "clustered":
elif problem_type == "random":
= np.random.randn(n, n)
A = max(np.linalg.eigvalsh(A.T@A))
factual_L = A.T.dot(A)/factual_L*L + mu*np.eye(n)
A = np.random.rand(n)
x_opt = A@x_opt
b = 3*np.random.randn(n)
x_0
elif problem_type == "uniform spectrum":
= np.diag(np.linspace(mu, L, n, endpoint=True))
A = np.random.rand(n)
x_opt = A@x_opt
b = 3*np.random.randn(n)
x_0
elif problem_type == "Hilbert":
= np.array([[1.0 / (i+j - 1) for i in range(1, n+1)] for j in range(1, n+1)])
A = np.ones(n)
b = 3*np.random.randn(n)
x_0 = np.linalg.lstsq(A, b)[0]
x_opt
elif problem_type == "worst_cg":
# Parameter t controls the condition number
= 0.6 # Small t leads to worse conditioning
t # Create tridiagonal matrix W
= np.ones(n)
main_diag 0] = t
main_diag[1:] = 1 + t
main_diag[= np.sqrt(t) * np.ones(n-1)
off_diag = np.diag(main_diag) + np.diag(off_diag, k=1) + np.diag(off_diag, k=-1)
A
# Create b vector [1, 0, ..., 0]
= np.zeros(n)
b 0] = 1
b[
# Since this is a specific problem, we compute x_opt explicitly
= np.linalg.solve(A, b)
x_opt = np.zeros(n) # Start from zero vector
x_0 return A, b, x_0, x_opt
return A, b, x_0, x_opt
# Optimization methods
def gradient_descent(f, grad_f, x_0, step_size, iterations, x_opt):
= x_0.copy()
x = f(x_opt)
f_opt = [], []
values, gradients abs(f(x) - f_opt))
values.append(
gradients.append(np.linalg.norm(grad_f(x)))for _ in range(iterations):
-= step_size * grad_f(x)
x abs(f(x) - f_opt))
values.append(
gradients.append(np.linalg.norm(grad_f(x)))return values, gradients
def steepest_descent(A, f, grad_f, x_0, iterations, x_opt):
= x_0.copy()
x = f(x_opt)
f_opt = [], []
values, gradients abs(f(x) - f_opt))
values.append(
gradients.append(np.linalg.norm(grad_f(x)))for _ in range(iterations):
= grad_f(x)
grad = np.dot(grad.T, grad) / np.dot(grad.T, np.dot(A, grad))
step_size -= step_size * grad
x abs(f(x) - f_opt))
values.append(
gradients.append(np.linalg.norm(grad))return values, gradients
def nesterov_accelerated_gradient(A, b, x_0, alpha, beta, iterations, x_opt):
= x_0.copy()
x = x.copy()
y = f(x_opt)
f_opt = x.copy()
prev_x = [f(x)], [np.linalg.norm(np.dot(A, x) - b)]
values, gradients for _ in range(iterations):
= (1-beta)*y + beta * x
y = grad_f(y)
grad = y - alpha * grad, y
x, y
abs(f(x) - f_opt))
values.append(
gradients.append(np.linalg.norm(grad))return values, gradients
def conjugate_gradient(A, b, x_0, iterations, x_opt):
= x_0.copy()
x = lambda x: 0.5 * x.T @ A @ x - b.T @ x
f = f(x_opt)
f_opt = b - np.dot(A, x)
r = r.copy()
p = [f(x)], [np.linalg.norm(r)]
values, gradients for _ in range(iterations-1):
= np.dot(r.T, r) / np.dot(p.T, np.dot(A, p))
alpha += alpha * p
x = r - alpha * np.dot(A, p)
r_next = np.dot(r_next.T, r_next) / np.dot(r.T, r)
beta = r_next + beta * p
p = r_next
r abs(f(x) - f_opt))
values.append(
gradients.append(np.linalg.norm(r))return values, gradients
def run_experiment(params):
= generate_problem(n=params["n"], mu=params["mu"], L=params["L"], problem_type=params["problem_type"])
A, b, x_0, x_opt = np.linalg.eigvalsh(A)
eigs = min(eigs), max(eigs)
mu, L
= lambda x: 0.5 * x.T @ A @ x - b.T @ x
f = lambda x: A@x - b
grad_f
if mu <= 1e-2:
= 1/L
alpha else:
= 2/(mu+L) # Step size
alpha = (np.sqrt(L) - np.sqrt(mu))/(np.sqrt(L) + np.sqrt(mu)) # Momentum parameter
beta
= {
results "methods": {
"Gradient Descent": gradient_descent(f, grad_f, x_0, alpha, params["iterations"], x_opt),
"Steepest Descent": steepest_descent(A, f, grad_f, x_0, params["iterations"], x_opt),
"Conjugate Gradients": conjugate_gradient(A, b, x_0, params["iterations"], x_opt),
# "nag": nesterov_accelerated_gradient(A, b, x_0, alpha, beta, params["iterations"], x_opt),
},"problem":{
"eigs": eigs,
"params": params
}
}return results
def plot_results(results):
= {
linestyles "Gradient Descent": "r-",
"Steepest Descent": "b-.",
"Conjugate Gradients": "g--"
}=(10, 3.5))
plt.figure(figsize= results["problem"]["params"]["mu"]
mu = results["problem"]["params"]["L"]
L = results["problem"]["params"]["n"]
n = results["problem"]["params"]["problem_type"]
problem_type
if mu > 1e-2:
f"Strongly convex quadratics. n={n}, {problem_type} matrix. ")
plt.suptitle(else:
f"Convex quadratics. n={n}, {problem_type} matrix. ")
plt.suptitle(
1, 3, 1)
plt.subplot(= results["problem"]["eigs"]
eigs len(eigs)), eigs)
plt.scatter(np.arange('Dimension')
plt.xlabel('Eigenvalues of A')
plt.ylabel(=":")
plt.grid(linestyle"Eigenvalues")
plt.title(if results["problem"]["params"]["problem_type"] == "Hilbert":
"log")
plt.yscale(
1, 3, 2)
plt.subplot(for method, result_ in results["methods"].items():
0], linestyles[method])
plt.semilogy(result_['Iteration')
plt.xlabel(r'$|f(x) -f^*|$')
plt.ylabel(=":")
plt.grid(linestyle"Function gap")
plt.title(
1, 3, 3)
plt.subplot(for method, result_ in results["methods"].items():
1], linestyles[method], label=method)
plt.semilogy(result_[r'$\|\nabla f(x)\|_2$')
plt.ylabel(=":")
plt.grid(linestyle"Norm of Gradient")
plt.title(
# Place the legend below the plots
='lower center', ncol=4, bbox_to_anchor=(0.5, -0.00))
plt.figlegend(loc# Adjust layout to make space for the legend below
=[0, 0.05, 1, 1])
plt.tight_layout(rectf"cg_{problem_type}_{mu}_{L}_{n}.pdf")
plt.savefig( plt.show()
# Experiment parameters
= {
params "n": 60,
"mu": 1e-3,
"L": 100,
"iterations": 100,
"problem_type": "random", # Change to "clustered", "uniform spectrum", or "Hilbert" as needed
}
= run_experiment(params)
results plot_results(results)
Упражнение 1
В коде выше добавьте генерацию матрицы положительно определенной A, кластер которой состоит из 4 различных значений.
# Experiment parameters
= {
params "n": 60,
"mu": 1e-3,
"L": 100,
"iterations": 100,
"problem_type": "random", # Change to "clustered"
}
= run_experiment(params)
results plot_results(results)
Предобуславливатели
Упражнение 2
В коде ниже добавьте функцию, реализующую предобуславливатель Якоби.
def jacobi_preconditioner(A):
""" Creates a LinearOperator for the inverse of the diagonal of A (Jacobi preconditioner). """
= spla.LinearOperator(
P_inv # shape of the preconditioner
A.shape, =lambda v: A * v, # matrix-vector product
matvec=lambda v: A * v # transpose matrix-vector product
rmatvec
)
return P_inv
# exercise_pcg.py
import numpy as np
import scipy.sparse as sp
import scipy.sparse.linalg as spla
import scipy.io as sio
import matplotlib.pyplot as plt
import requests
import io
import tarfile
import os
def download_and_load_matrix(matrix_name="bcsstk17", data_dir="."):
"""
Downloads and loads a matrix from the SuiteSparse Matrix Collection.
Falls back to a random SPD matrix if download fails.
"""
= os.path.join(data_dir, f"{matrix_name}.mtx")
mm_filename
if not os.path.exists(mm_filename):
print(f"Matrix file {mm_filename} not found. Attempting download...")
= f"https://sparse.tamu.edu/MM/Boeing/{matrix_name}.tar.gz"
url try:
= requests.get(url, stream=True)
response # Raise an exception for bad status codes
response.raise_for_status()
# Use BytesIO to handle the tarfile in memory
= io.BytesIO(response.content)
tar_data
with tarfile.open(fileobj=tar_data, mode="r:gz") as tar:
# Find the .mtx file in the tar archive
= None
mtx_member_name for member in tar.getmembers():
if member.name.endswith(".mtx"):
= member.name
mtx_member_name break
if mtx_member_name:
# Extract directly to the data_dir
= os.path.join(data_dir, os.path.basename(mtx_member_name))
member_path # Ensure extraction doesn't overwrite outside data_dir (security)
if os.path.commonpath([data_dir, os.path.abspath(member_path)]) != os.path.abspath(data_dir):
raise Exception("Attempted path traversal in tar file.")
=True)
os.makedirs(data_dir, exist_ok
# Extract the specific member
= tar.extractfile(mtx_member_name)
extracted_file if extracted_file:
with open(mm_filename, "wb") as f:
f.write(extracted_file.read())print(f"Successfully downloaded and extracted {mtx_member_name} to {mm_filename}")
else:
raise Exception(f"Could not extract {mtx_member_name} from tar archive.")
else:
raise FileNotFoundError("No .mtx file found in the downloaded archive.")
except requests.exceptions.RequestException as e:
print(f"Download failed: {e}")
return None, None, None, False # Indicate failure
except tarfile.TarError as e:
print(f"Tar extraction failed: {e}")
return None, None, None, False # Indicate failure
except Exception as e:
print(f"An error occurred during download/extraction: {e}")
return None, None, None, False # Indicate failure
# Load the matrix
try:
= sio.mmread(mm_filename)
A if sp.isspmatrix_coo(A):
= A.tocsr() # Convert to CSR for efficient matrix-vector products
A print(f"Loaded matrix {matrix_name}: shape={A.shape}, nnz={A.nnz}")
# Check if symmetric (required for eigsh and CG theory)
if not np.allclose((A - A.T).data, 0):
print("Warning: Matrix is not symmetric. Making it symmetric (A+A.T)/2.")
= 0.5 * (A + A.T)
A = A.tocsr()
A
# Check if positive definite by trying Cholesky or checking eigenvalues
try:
# Attempt Cholesky factorization (requires scikit-sparse or similar)
# Alternatively, check smallest eigenvalue (can be slow for large matrices)
= spla.eigsh(A, k=1, which='SM', return_eigenvectors=False)[0]
lambda_min if lambda_min <= 1e-10: # Check if sufficiently positive
print(f"Warning: Matrix may not be positive definite (smallest eigenvalue ~ {lambda_min}). Adding diagonal shift.")
= A + sp.identity(A.shape[0]) * 1e-6
A else:
print(f"Matrix appears positive definite (smallest eigenvalue ~ {lambda_min}).")
except Exception as e:
print(f"Could not verify positive definiteness ({e}). Proceeding with caution.")
# Generate true solution and rhs
= A.shape[0]
n 0)
np.random.seed(= np.random.rand(n)
x_true = A @ x_true
b return A, b, x_true, True # Indicate success
except FileNotFoundError:
print(f"Matrix file {mm_filename} not found after download attempt.")
return None, None, None, False
except Exception as e:
print(f"Error loading matrix {mm_filename}: {e}")
return None, None, None, False
def gradient_descent(A, b, x0, max_iter=1000, tol=1e-6, x_true=None):
""" Solves Ax=b using Gradient Descent. """
= []
history = x0.copy()
x = A.shape[0]
n
# Estimate lambda_max(A) for step size
try:
# Ensure A is symmetric before calling eigsh
if not hasattr(A, 'symmetric') or not A.symmetric:
if np.allclose((A - A.T).data, 0):
= True # Mark as symmetric if it is
A.symmetric else: # If not symmetric, eigsh on non-symmetric matrices is deprecated/problematic
raise ValueError("Matrix A must be symmetric for eigsh.")
= spla.eigsh(A, k=1, which='LM', return_eigenvectors=False)[0]
lambda_max # Optimal step size for quadratic requires lambda_min as well
# lambda_min = spla.eigsh(A, k=1, which='SM', return_eigenvectors=False)[0]
# tau = 2.0 / (lambda_max + lambda_min)
# Simpler choice (may converge slower):
= 1.0 / lambda_max
tau except Exception as e:
print(f"Warning: eigsh failed for A ({e}), using fixed step size 1e-7 for GD")
= 1e-7 # Small fixed fallback
tau
print(f"Using step size tau = {tau:.2e} for GD")
= np.linalg.norm(x_true) if x_true is not None else 1.0
norm_xtrue = np.linalg.norm(b)
norm_b
if x_true is not None:
- x_true) / norm_xtrue)
history.append(np.linalg.norm(x0 else:
- A @ x0) / norm_b)
history.append(np.linalg.norm(b
for k in range(max_iter):
= b - A @ x
r = np.linalg.norm(r)
norm_r
if x_true is not None:
= np.linalg.norm(x - x_true) / norm_xtrue
err
history.append(err)else:
# Use relative residual
/ norm_b)
history.append(norm_r
# Use relative tolerance for residual check
if norm_r / norm_b < tol:
print(f"GD converged in {k} iterations.")
break
= x + tau * r
x else: # Loop finished without break
print(f"GD did not converge within {max_iter} iterations.")
# Pad history if converges early
= len(history)
actual_iters if actual_iters < max_iter + 1:
-1]] * (max_iter + 1 - actual_iters))
history.extend([history[
return x, history[:max_iter+1]
def conjugate_gradient_scipy(A, b, x0, max_iter=1000, tol=1e-6, x_true=None):
""" Solves Ax=b using scipy's Conjugate Gradient. """
= []
history = np.linalg.norm(x_true) if x_true is not None else 1.0
norm_xtrue = np.linalg.norm(b)
norm_b
if x_true is not None:
- x_true) / norm_xtrue)
history.append(np.linalg.norm(x0 else:
- A @ x0) / norm_b)
history.append(np.linalg.norm(b
= 0
iter_count def callback(xk):
nonlocal iter_count
+= 1
iter_count if x_true is not None:
= np.linalg.norm(xk - x_true) / norm_xtrue
err
history.append(err)else:
# Use relative residual
= b - A @ xk
r = np.linalg.norm(r)
norm_r / norm_b)
history.append(norm_r
# Use relative tolerance for spla.cg
= spla.cg(A, b, x0=x0, maxiter=max_iter, tol=tol, callback=callback)
x, info
if info == 0:
print(f"CG converged in {iter_count} iterations.")
elif info > 0:
print(f"CG did not converge within {info} iterations.")
else:
print(f"CG failed with error code {info}.")
# Pad history if converges early
= len(history)
actual_iters if actual_iters < max_iter + 1:
-1]] * (max_iter + 1 - actual_iters))
history.extend([history[
return x, history[:max_iter+1] # Return history up to max_iter
def jacobi_preconditioner(A):
return P_inv
# ============================================================
# === EXERCISE: Implement Preconditioned Methods ===
# ============================================================
# Objective: Implement PGD and PCG using the Jacobi preconditioner.
# The functions below are provided as a solution.
# For the exercise, you might comment out the implementation details
# and try to write them yourself based on the GD/CG implementations
# and the lecture notes on preconditioning.
def preconditioned_gradient_descent(A, b, x0, P_inv, max_iter=1000, tol=1e-6, x_true=None):
"""
Solves Ax=b using Preconditioned Gradient Descent with a given preconditioner P_inv.
P_inv should be a LinearOperator for the inverse of the preconditioner matrix P.
"""
= []
history = x0.copy()
x = A.shape[0]
n
# Estimate lambda_max(P_inv * A) for step size
# Note: P_inv * A is generally not symmetric even if A and P are.
# eigsh requires symmetric operators. Power iteration is safer for lambda_max.
# For simplicity here, we'll try eigsh assuming effective symmetry or use fallback.
= 1e-7 # Small fixed fallback step size
tau
try:
# Define the operator P_inv * A
# Note: For Jacobi (P=diag(A)), if A is SPD, P is SPD, and P^{-1/2} A P^{-1/2} is SPD.
# We apply GD to the transformed system P^{-1}Ax = P^{-1}b, or minimize
# f(x) w.r.t. P-inner product. Update is x = x + tau * P^{-1}r
# Step size depends on eigenvalues of P^{-1}A
= spla.LinearOperator(A.shape, matvec=lambda v: P_inv @ (A @ v))
P_inv_A_op # Use power iteration (or eigsh if confident about symmetry properties)
# spla.eigsh might fail if P_inv_A is not symmetric.
# Power iteration finds the largest eigenvalue in magnitude.
# We need largest positive eigenvalue for step size calculation if P_inv A is SPD.
# If A is SPD and P=diag(A) with positive entries, P_inv*A is similar to P^{-1/2}AP^{-1/2} which is SPD.
= spla.eigsh(P_inv_A_op, k=1, which='LM', return_eigenvectors=False, tol=1e-3)[0]
lambda_max_prec
# Heuristic step size (often too conservative or requires lambda_min too)
if lambda_max_prec > 1e-10:
= 1.0 / lambda_max_prec
tau else:
print("Warning: Estimated lambda_max(P_inv*A) is close to zero.")
# tau remains the fallback
except Exception as e:
print(f"Warning: eigsh failed for P_inv_A ({e}), using fixed step size {tau:.1e} for PGD")
# tau remains the fallback
print(f"Using step size tau = {tau:.2e} for PGD")
= np.linalg.norm(x_true) if x_true is not None else 1.0
norm_xtrue = np.linalg.norm(b)
norm_b
if x_true is not None:
- x_true) / norm_xtrue)
history.append(np.linalg.norm(x0 else:
- A @ x0) / norm_b)
history.append(np.linalg.norm(b
for k in range(max_iter):
= b - A @ x
r = np.linalg.norm(r)
norm_r
if x_true is not None:
= np.linalg.norm(x - x_true) / norm_xtrue
err
history.append(err)else:
# Use relative residual
/ norm_b)
history.append(norm_r
# Use relative tolerance for residual check
if norm_r / norm_b < tol:
print(f"PGD converged in {k} iterations.")
break
= P_inv @ r # Apply preconditioner: z_k = P^{-1} r_k
z = x + tau * z # Update using preconditioned residual: x_{k+1} = x_k + tau * z_k
x
else: # Loop finished without break
print(f"PGD did not converge within {max_iter} iterations.")
# Pad history if converges early
= len(history)
actual_iters if actual_iters < max_iter + 1:
-1]] * (max_iter + 1 - actual_iters))
history.extend([history[
return x, history[:max_iter+1]
def preconditioned_conjugate_gradient_scipy(A, b, x0, P_inv, max_iter=1000, tol=1e-6, x_true=None):
"""
Solves Ax=b using Preconditioned Conjugate Gradient with scipy's cg.
P_inv should be a LinearOperator for the inverse of the preconditioner P.
"""
= []
history = np.linalg.norm(x_true) if x_true is not None else 1.0
norm_xtrue = np.linalg.norm(b)
norm_b
if x_true is not None:
- x_true) / norm_xtrue)
history.append(np.linalg.norm(x0 else:
- A @ x0) / norm_b)
history.append(np.linalg.norm(b
= 0
iter_count def callback(xk):
nonlocal iter_count
+= 1
iter_count if x_true is not None:
= np.linalg.norm(xk - x_true) / norm_xtrue
err
history.append(err)else:
# Use relative residual
= b - A @ xk
r = np.linalg.norm(r)
norm_r / norm_b)
history.append(norm_r
# Use spla.cg with the M argument for the preconditioner (M = P_inv)
# M must represent the action of P_inv.
= spla.cg(A, b, x0=x0, maxiter=max_iter, tol=tol, M=P_inv, callback=callback)
x, info
if info == 0:
print(f"PCG converged in {iter_count} iterations.")
elif info > 0:
print(f"PCG did not converge within {info} iterations.")
else:
print(f"PCG failed with error code {info}.")
# Pad history if converges early
= len(history)
actual_iters if actual_iters < max_iter + 1:
-1]] * (max_iter + 1 - actual_iters))
history.extend([history[
return x, history[:max_iter+1]
# ============================================================
# === END OF EXERCISE SECTION ===
# ============================================================
def plot_convergence(histories, labels, title="Convergence Comparison", use_error=True):
""" Plots the convergence history of multiple methods. """
=(12, 7))
plt.figure(figsizefor history, label in zip(histories, labels):
if history: # Check if history exists (method might have been skipped)
# Ensure history length matches iterations 0 to max_iter
= range(len(history))
iterations =label, linewidth=2)
plt.semilogy(iterations, history, label
"Iteration", fontsize=12)
plt.xlabel(= "Relative Error $\|x_k - x^*\|_2 / \|x^*\|_2$" if use_error else "Relative Residual Norm $\|Ax_k - b\|_2 / \|b\|_2$"
ylabel =12)
plt.ylabel(ylabel, fontsize=14)
plt.title(title, fontsize=10)
plt.legend(fontsizeTrue, which='both', linestyle='--', linewidth=0.5)
plt.grid(# Adjust ylim based on expected convergence, avoid overly small limits if methods don't converge well
= min(h[-1] for h in histories if h)
min_val =max(1e-12, min_val / 10), top=1.1 * max(h[0] for h in histories if h)) # Start slightly above initial error/residual
plt.ylim(bottom
plt.tight_layout()
plt.show()
# Main execution block
if __name__ == "__main__":
= "bcsstk17"
MATRIX_NAME = "./matrix_data" # Directory to store downloaded matrix
DATA_DIR
# Download or generate matrix
= download_and_load_matrix(MATRIX_NAME, DATA_DIR)
A, b, x_true, loaded_real_matrix
if not loaded_real_matrix:
print("Falling back to a random SPD matrix.")
# Generate a small random SPD matrix as fallback
= 200 # Larger random matrix
n 0)
np.random.seed(# Generate sparse matrix directly
= 0.01
density = sp.random(n, n, density=density, format='csr', random_state=0)
A = 0.5 * (A + A.T) # Make symmetric
A # Ensure positive definiteness by adding diagonal dominance
= sp.diags(A.sum(axis=1).A1 + 0.1, 0, format='csr')
diag_shift = A + diag_shift
A
= np.random.rand(n)
x_true = A @ x_true
b = f"Random {n}x{n} SPD"
MATRIX_NAME print(f"Using random matrix: shape={A.shape}, nnz={A.nnz}")
= (x_true is not None)
use_error_metric = A.shape[0]
n_dim = np.zeros(n_dim) # Initial guess
x0
# Parameters
= min(1000, 2 * n_dim) # Max iterations, capped
MAX_ITER = 1e-8 # Relative tolerance
TOL
# --- Run Methods ---
print("\n--- Running Solvers ---")
= gradient_descent(A, b, x0, max_iter=MAX_ITER, tol=TOL, x_true=x_true)
x_gd, hist_gd = conjugate_gradient_scipy(A, b, x0, max_iter=MAX_ITER, tol=TOL, x_true=x_true)
x_cg, hist_cg
# Setup and run preconditioned methods
= None, None
hist_pgd, hist_pcg try:
print("\nSetting up Jacobi Preconditioner...")
= jacobi_preconditioner(A)
P_inv print("Running Preconditioned Gradient Descent (PGD)...")
= preconditioned_gradient_descent(A, b, x0, P_inv, max_iter=MAX_ITER, tol=TOL, x_true=x_true)
x_pgd, hist_pgd print("Running Preconditioned Conjugate Gradient (PCG)...")
= preconditioned_conjugate_gradient_scipy(A, b, x0, P_inv, max_iter=MAX_ITER, tol=TOL, x_true=x_true)
x_pcg, hist_pcg except ValueError as e:
print(f"\nError setting up/using Jacobi preconditioner: {e}")
print("Skipping preconditioned methods.")
= None
P_inv
# --- Plot Results ---
print("\n--- Plotting Convergence ---")
= [hist_gd, hist_cg]
histories_list = ["Gradient Descent (GD)", "Conjugate Gradient (CG)"]
labels_list if hist_pgd:
histories_list.append(hist_pgd)"PGD (Jacobi)")
labels_list.append(if hist_pcg:
histories_list.append(hist_pcg)"PCG (Jacobi)")
labels_list.append(
= f"Convergence Comparison for '{MATRIX_NAME}' (N={n_dim})"
plot_title =plot_title, use_error=use_error_metric)
plot_convergence(histories_list, labels_list, title
print("\n--- Exercise Completed ---")
Matrix file ./matrix_data/bcsstk17.mtx not found. Attempting download...
Download failed: 404 Client Error: Not Found for url: http://sparse-files.engr.tamu.edu/MM/Boeing/bcsstk17.tar.gz
Falling back to a random SPD matrix.
Using random matrix: shape=(200, 200), nnz=992
--- Running Solvers ---
Using step size tau = 2.59e-01 for GD
GD did not converge within 400 iterations.
CG converged in 46 iterations.
Setting up Jacobi Preconditioner...
Running Preconditioned Gradient Descent (PGD)...
Using step size tau = 5.20e-01 for PGD
PGD converged in 121 iterations.
Running Preconditioned Conjugate Gradient (PCG)...
PCG converged in 25 iterations.
--- Plotting Convergence ---
--- Exercise Completed ---