# 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)