Source code for fol.solvers.fe_nonlinear_residual_based_solver

"""
 Authors: Reza Najian Asl, https://github.com/RezaNajian
 Date: July, 2024
 License: FOL/LICENSE
"""
import jax
import jax.numpy as jnp
from jax import jit
from functools import partial
from  .fe_linear_residual_based_solver import FiniteElementLinearResidualBasedSolver
from fol.tools.decoration_functions import *
from fol.tools.usefull_functions import *
from fol.loss_functions.fe_loss import FiniteElementLoss

[docs]class FiniteElementNonLinearResidualBasedSolver(FiniteElementLinearResidualBasedSolver): """ Nonlinear residual-based finite element solver using incremental Newton–Raphson. This solver extends :class:`~fol.solvers.fe_linear_residual_based_solver.FiniteElementLinearResidualBasedSolver` to support nonlinear problems by repeatedly assembling the tangent (Jacobian) matrix and residual vector and applying Newton–Raphson updates to the global degrees of freedom (DOFs). Loading is applied incrementally according to ``self.nonlinear_solver_settings["load_incr"]``. For each load step, the solver iterates until convergence is detected or the maximum number of Newton iterations is reached. Convergence history (residual norms and update norms) is stored per load step and can optionally be plotted via :meth:`PlotHistoryDict`. Typical applications include problems with geometric nonlinearities, nonlinear material behavior (e.g., hyperelasticity, plasticity, damage), or nonlinear boundary-condition effects. """ @print_with_timestamp_and_execution_time def __init__(self, fe_solver_name: str, fe_loss_function: FiniteElementLoss, fe_solver_settings:dict={}, history_plot_settings:dict={}) -> None: super().__init__(fe_solver_name,fe_loss_function,fe_solver_settings) self.nonlinear_solver_settings = {"rel_tol":1e-8,"abs_tol":1e-8,"maxiter":20,"load_incr":5} self.default_history_plot_settings = {"plot":False,"criteria":["res_norm","delta_dofs_norm"],"save_directory":"."} self.history_plot_settings = history_plot_settings
[docs] @print_with_timestamp_and_execution_time def Initialize(self) -> None: """ Initialize the solver and merge nonlinear and plotting settings. This method calls the base class initialization and updates the nonlinear solver settings from ``fe_solver_settings["nonlinear_solver_settings"]`` if provided. It also merges the user-specified history plotting settings with the default plotting configuration. """ super().Initialize() if "nonlinear_solver_settings" in self.fe_solver_settings.keys(): self.nonlinear_solver_settings = UpdateDefaultDict(self.nonlinear_solver_settings, self.fe_solver_settings["nonlinear_solver_settings"]) self.history_plot_settings = UpdateDefaultDict(self.default_history_plot_settings,self.history_plot_settings)
def PlotHistoryDict(self,history_dict:dict): if self.history_plot_settings["plot"]: plot_dict = {key: [] for key in self.history_plot_settings["criteria"]} for plot_criterion,plot_values in plot_dict.items(): for step,step_dict in history_dict.items(): plot_values.extend(step_dict[plot_criterion]) plt.figure(figsize=(10, 5)) for key,value in plot_dict.items(): plt.semilogy(value, marker='o', markersize=6, linewidth=1.5, label=f"{key}") plt.title("Newton-Raphson History with Load Stepping") plt.xlabel("Cumulative Iteration") plt.ylabel("Log Value") plt.legend() plt.grid(True) plt.savefig(os.path.join(self.history_plot_settings["save_directory"],"newton_raphson_history.png"), bbox_inches='tight') plt.close()
[docs] @print_with_timestamp_and_execution_time def Solve(self,current_control_vars:jnp.array,current_dofs_np:jnp.array): """ Solve the nonlinear FE system using incremental Newton–Raphson iterations. The load is applied over ``load_incr`` steps. For each load step, Dirichlet boundary conditions are applied at the current load factor, the Jacobian and residual are assembled via the loss function, and a Newton update is computed using :meth:`LinearSolve`. The DOFs are updated until convergence is detected or the iteration limit is reached. Convergence metrics are recorded per load step and can be plotted via :meth:`PlotHistoryDict`. Convergence is considered achieved when at least one of the following conditions is satisfied: - Residual norm < ``abs_tol`` - Update norm < ``rel_tol`` - Iteration count reaches ``maxiter`` Args: current_control_vars (jax.numpy.ndarray): Control variables passed to the loss function (e.g., material or design parameters). current_dofs_np (jax.numpy.ndarray): Initial DOF vector. Converted to a JAX array internally. Returns: jax.numpy.ndarray: Converged DOF vector at the end of the final load step. Raises: ValueError: If the residual norm becomes NaN during Newton iterations. """ current_dofs = jnp.asarray(current_dofs_np) current_control_vars = jnp.asarray(current_control_vars) num_load_steps = self.nonlinear_solver_settings["load_incr"] convergence_history = {} for load_step in range(1,num_load_steps+1): load_step_value = (load_step)/num_load_steps # increment load current_dofs = self.fe_loss_function.ApplyDirichletBCOnDofVector(current_dofs,load_step_value) newton_converged = False convergence_history[load_step] = {"res_norm":[],"delta_dofs_norm":[]} for i in range(1,self.nonlinear_solver_settings["maxiter"]+1): BC_applied_jac,BC_applied_r = self.fe_loss_function.ComputeJacobianMatrixAndResidualVector( current_control_vars,current_dofs) # check residuals norm res_norm = jnp.linalg.norm(BC_applied_r,ord=2) if jnp.isnan(res_norm): fol_info( "\n" "──────────────────── NEWTON ERROR ────────────────────\n" " Residual norm has become NaN.\n" " Possible causes:\n" " • Divergent Newton iteration\n" " • Inconsistent or ill-posed boundary conditions\n" " • Invalid / non-physical material parameters or state\n" " • Singular or severely ill-conditioned stiffness matrix\n" " • Severely distorted mesh (element quality breakdown)\n" "───────────────────────────────────────────────────────\n" ) raise ValueError("Residual norm contains NaN values.") # linear solve and calculate nomrs delta_dofs = self.LinearSolve(BC_applied_jac,BC_applied_r,current_dofs) delta_norm = jnp.linalg.norm(delta_dofs,ord=2) newton_converged = ( res_norm < self.nonlinear_solver_settings["abs_tol"] or delta_norm < self.nonlinear_solver_settings["rel_tol"] or i == self.nonlinear_solver_settings["maxiter"] ) indent = " " * 5 fol_info( f"\n" f"{indent} ───────── Load Step {load_step} ─────────\n" f"{indent} Newton Iteration : {i} (max = {self.nonlinear_solver_settings['maxiter']})\n" f"{indent} Residual Norm : {res_norm:.3e} (abs_tol = {self.nonlinear_solver_settings['abs_tol']:.3e})\n" f"{indent} Δ DOFs Norm : {delta_norm:.3e} (rel_tol = {self.nonlinear_solver_settings['rel_tol']:.3e})\n" f"{indent} Converged : {'True' if newton_converged else 'False'}\n" f"{indent}────────────────────────────────────────────" ) convergence_history[load_step]["res_norm"].append(res_norm) convergence_history[load_step]["delta_dofs_norm"].append(delta_norm) if newton_converged: break # if not converged update current_dofs = current_dofs.at[:].add(delta_dofs) self.PlotHistoryDict(convergence_history) return current_dofs