224 lines
8.1 KiB
Python
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)
|