Numerical-Simulation/HW5/Convergence.py

224 lines
8.1 KiB
Python

# convergence.py
import numpy as np
import matplotlib.pyplot as plt
from DiscreteMethod import DiscreteMethod
class Convergence:
def __init__(self, analytical, fdm, fem):
"""
Initialize the Convergence class.
Parameters:
- analytical:
- fdm: Instance of the FDM class.
- fem: Instance of the FEM class.
"""
self.analytical = analytical
self.fdm: 'DiscreteMethod' = fdm
self.fem: 'DiscreteMethod' = fem
def calc_error(self, exact, q_1):
"""Calculate relative error."""
return np.abs((exact - q_1) / exact)
def calc_beta(self, exact, q_1, q_2, dx_1, dx_2):
"""Calculate convergence rate beta."""
return np.log(np.abs((exact - q_1) / (exact - q_2))) / np.log(dx_1 / dx_2)
def calc_extrapolated(self, q1, q2, q3, tolerance=1e-10):
"""Calculate Richardson extrapolation, returns NaN if denominator is too small."""
numerator = q1 * q3 - q2**2
denominator = q1 + q3 - 2 * q2
if abs(denominator) < tolerance:
return float('NaN') # Return NaN if denominator is close to zero
return numerator / denominator
def run_convergence(self, forcing_freq: float, num_sections_range, metric_func):
"""
Run convergence analysis for FDM and FEM.
Parameters:
- forcing_freq: The forcing frequency to test.
- num_sections_range: Array of num_sections to test.
- metric_func: Callable defining the metric to analyze (e.g., U'(L) or U(1 / (2pi))).
Returns:
- results: Dictionary containing errors, betas, extrapolated values, and analytical values for both methods.
"""
results = {
"fdm": {
"num_sections": [],
"errors": [],
"betas": [],
"extrapolated_errors": [],
"extrapolated_values": [],
"analytical_values": [],
},
"fem": {
"num_sections": [],
"errors": [],
"betas": [],
"extrapolated_errors": [],
"extrapolated_values": [],
"analytical_values": [],
},
}
# Analytical value for the metric
analytical_value = metric_func(self.analytical, forcing_freq)
for num_sections in num_sections_range:
dx = 1 / num_sections
# Run FDM
self.fdm.run(forcing_freq, num_sections)
fdm_metric = metric_func(self.fdm)
fdm_error = self.calc_error(analytical_value, fdm_metric)
# Run FEM
self.fem.run(forcing_freq, num_sections)
fem_metric = metric_func(self.fem)
fem_error = self.calc_error(analytical_value, fem_metric)
# Store results
results["fdm"]["num_sections"].append(num_sections)
results["fdm"]["errors"].append(fdm_error)
results["fdm"]["extrapolated_values"].append(fdm_metric)
results["fdm"]["analytical_values"].append(analytical_value)
results["fem"]["num_sections"].append(num_sections)
results["fem"]["errors"].append(fem_error)
results["fem"]["extrapolated_values"].append(fem_metric)
results["fem"]["analytical_values"].append(analytical_value)
# Compute extrapolated errors and betas
for method in ["fdm", "fem"]:
extrapolated_errors = [np.nan] # Padding for the first run
betas = [np.nan] # Padding for the first run
extrapolated_betas = [np.nan] # Padding for the first run
values = results[method]["extrapolated_values"]
num_sections = results[method]["num_sections"]
# Extrapolation and beta calculations
for i in range(len(num_sections) - 1):
q1 = values[i]
q2 = values[i + 1]
dx1 = 1 / num_sections[i]
dx2 = 1 / num_sections[i + 1]
beta = self.calc_beta(analytical_value, q1, q2, dx1, dx2)
if i >= 1:
extrapolated_value = self.calc_extrapolated(
values[i - 1], q1, q2
) # Requires 3 consecutive values for extrapolation
extrapolated_error = self.calc_error(extrapolated_value, q2)
extrapolated_beta = self.calc_beta(extrapolated_value, q1, q2, dx1, dx2)
else:
extrapolated_value = np.nan
extrapolated_error = np.nan
extrapolated_beta = np.nan
if i >= 1:
extrapolated_errors.append(extrapolated_error)
extrapolated_betas.append(extrapolated_beta)
else:
extrapolated_errors.append(np.nan)
extrapolated_betas.append(np.nan)
betas.append(beta)
results[method]["extrapolated_errors"] = extrapolated_errors
results[method]["betas"] = betas
results[method]["extrapolated_betas"] = extrapolated_betas
return results
def plot_convergence(self, results):
"""
Plot convergence results for FDM and FEM, including errors relative to analytical
and extrapolated values.
Parameters:
- results: Dictionary from run_convergence.
"""
plt.figure(figsize=(12, 6))
for method in ["fdm", "fem"]:
num_sections = results[method]["num_sections"]
errors = results[method]["errors"]
extrapolated_errors = results[method]["extrapolated_errors"]
# Pad num_sections to match extrapolated_errors length
padded_num_sections = [np.nan] * 2 + num_sections[:-2]
# Plot true error relative to the analytical solution
plt.loglog(
num_sections, errors, '-o', label=f"{method.upper()} Analytical Error"
)
# Plot error relative to the extrapolated solution
plt.loglog(
num_sections, extrapolated_errors, '--s', label=f"{method.upper()} Extrapolated Error"
)
plt.xlabel("Number of Sections")
plt.ylabel("Error (Relative)")
plt.title("Convergence Analysis: True and Extrapolated Errors")
plt.legend()
plt.grid(True, which="both", linestyle="--")
plt.show()
if __name__ == "__main__":
from Analytical import Analytical
from Bar import Bar
from common import freq_from_alpha
from DiscreteMethod import DiscreteMethod
from FDM import FDM
from FEM import FEM
def metric_u_prime_l(method:'DiscreteMethod', forcing_freq:float = None):
"""Extract U'(L) from the method."""
if forcing_freq is None: # Finite method
return method.get_strain_amp(1.0)
else:
return method.get_strain_amp(forcing_freq, 1.0) # Analytical
def metric_u_half_pi(method:'DiscreteMethod', forcing_freq:float = None):
"""Extract U(1 / (2pi)) from the method."""
x_half_pi = 1 / (2 * np.pi)
if forcing_freq is None: # Finite method
return method.get_disp_amp(x_half_pi)
else:
return method.get_disp_amp(forcing_freq, x_half_pi) # Analytical
# Initialize Bar, FDM, FEM
bar = Bar()
analy = Analytical(bar)
fdm = FDM(bar, desired_order="2")
fem = FEM(bar, desired_order="2")
# Set boundary conditions
fdm.set_boundary_conditions(U_0=0, U_L=100)
fem.set_boundary_conditions(U_0=0, U_L=100)
# Initialize Convergence
convergence = Convergence(analy, fdm, fem)
alpha = 0.25
forcing_freq = freq_from_alpha(bar, alpha)
# Run convergence for U'(L)
num_sections_range = [2**n for n in range(1, 12)]
results = convergence.run_convergence(forcing_freq, num_sections_range, metric_u_half_pi)
# Plot results
convergence.plot_convergence(results)