Source code for fol.loss_functions.mechanical_saint_venant

"""
 Authors: Kianoosh Taghikhani, https://github.com/kianoosh1989
 Date: July, 2024
 License: FOL/LICENSE
"""
from  .fe_loss import FiniteElementLoss
import jax
import jax.numpy as jnp
import numpy as np
from jax import jit
from functools import partial
from fol.tools.fem_utilities import *
from fol.tools.decoration_functions import *
from fol.mesh_input_output.mesh import Mesh
from fol.tools.usefull_functions import *
from fol.constitutive_material_models.saint_venant import SaintVenant

[docs]class SaintVenantMechanicalLoss(FiniteElementLoss): """ Saint Venant–Kirchhoff mechanical energy loss for finite deformation elasticity. This class defines an energy-based loss functional for mechanical problems governed by a Saint Venant–Kirchhoff constitutive model. The total loss value represents the total strain energy of the structure and is assembled by summing element-level energy contributions over all finite elements in the mesh. For each element, the strain energy density is evaluated at Gauss points and integrated to obtain an element energy contribution. The global energy is obtained by accumulating these element energies across the computational domain. In addition to the scalar energy contribution, this loss provides the element residual vector (internal minus external forces) and the element tangent matrix (material stiffness plus geometric stiffness) required for Newton-based solution procedures. Args: name (str): Name identifier for the loss instance. loss_settings (dict): Configuration dictionary. Must include ``material_dict`` with keys ``"young_modulus"`` and ``"poisson_ratio"``. Optional entries include ``"body_foce"`` and parametric heterogeneity settings via ``"heterogeneity_field_name"`` and ``"heterogeneity_default_value"``. Element discretization settings (dimension, element type, ordered DOFs) are typically provided by specialized subclasses. fe_mesh (Mesh): Finite element mesh over which the energy functional is defined. Attributes: e (float): Young's modulus used by the constitutive model. v (float): Poisson's ratio used by the constitutive model. material_model (SaintVenant): Constitutive material model used to compute energy density, stress, and consistent tangent. body_force (jax.numpy.ndarray): Body force vector with shape ``(2, 1)`` in 2D or ``(3, 1)`` in 3D. """ default_material_settings = {"young_modulus":1.0, "poisson_ratio":"0.3", "heterogeneity_field_name":"K", "heterogeneity_default_value":1.0} def __init__(self, name: str, loss_settings: dict, fe_mesh: Mesh): loss_settings["material_dict"] = UpdateDefaultDict(self.default_material_settings,loss_settings["material_dict"]) super().__init__(name,loss_settings,fe_mesh) def Initialize(self) -> None: super().Initialize() if "material_dict" not in self.loss_settings.keys(): fol_error("material_dict should provided in the loss settings !") self.e = self.loss_settings["material_dict"]["young_modulus"] self.v = self.loss_settings["material_dict"]["poisson_ratio"] self.material_model = SaintVenant() if self.dim == 2: self.CalculateNMatrix = self.CalculateNMatrix2D self.CalculateKinematics = self.CalculateKinematics2D if self.element_type == "quad": self.CalculateGeometricStiffness = self.CalculateQuadGeometricStiffness2D elif self.element_type == "triangle": self.CalculateGeometricStiffness = self.CalculateTriangleGeometricStiffness2D self.body_force = jnp.zeros((2,1)) if "body_foce" in self.loss_settings: self.body_force = jnp.array(self.loss_settings["body_foce"]) if self.dim == 3: self.CalculateNMatrix = self.CalculateNMatrix3D self.CalculateKinematics = self.CalculateKinematics3D if self.element_type == "tetra": self.CalculateGeometricStiffness = self.CalculateTetraGeometricStiffness3D elif self.element_type == "hexahedron": self.CalculateGeometricStiffness = self.CalculateHexaGeometricStiffness3D self.body_force = jnp.zeros((3,1)) if "body_foce" in self.loss_settings: self.body_force = jnp.array(self.loss_settings["body_foce"]) if self.loss_settings.get("parametric_boundary_learning"): heterogeneity_field = self.loss_settings["material_dict"]["heterogeneity_field_name"] heterogeneity_default_value = self.loss_settings["material_dict"]["heterogeneity_default_value"] if not self.fe_mesh.HasPointData(heterogeneity_field): self.fe_mesh[heterogeneity_field] = heterogeneity_default_value * np.ones(self.fe_mesh.GetNumberOfNodes()) self.heterogeneity_field = jnp.asarray(self.fe_mesh[heterogeneity_field]) self.get_param_function = lambda x: self.heterogeneity_field @partial(jit, static_argnums=(0,)) def CalculateKinematics2D(self,DN_DX_T:jnp.array,uve:jnp.array) -> jnp.array: num_nodes = DN_DX_T.shape[1] uveT = jnp.array([uve[::2].squeeze(),uve[1::2].squeeze()]).T H = jnp.dot(DN_DX_T,uveT).T F = H + jnp.eye(H.shape[0]) indices = np.arange(num_nodes) B = jnp.zeros((3, 2*num_nodes)) B = B.at[0, 2 * indices].set(F[0, 0] * DN_DX_T[0, indices]) B = B.at[0, 2 * indices + 1].set(F[1, 0] * DN_DX_T[0, indices]) B = B.at[1, 2 * indices].set(F[0, 1] * DN_DX_T[1, indices]) B = B.at[1, 2 * indices + 1].set(F[1, 1] * DN_DX_T[1, indices]) B = B.at[2, 2 * indices].set(F[0, 1] * DN_DX_T[0, indices] + F[0, 0] * DN_DX_T[1, indices]) B = B.at[2, 2 * indices + 1].set(F[1, 1] * DN_DX_T[0, indices] + F[1, 0] * DN_DX_T[1, indices]) return H,F,B @partial(jit, static_argnums=(0,)) def CalculateKinematics3D(self,DN_DX_T:jnp.array,uvwe:jnp.array) -> jnp.array: num_nodes = DN_DX_T.shape[1] uvweT = jnp.array([uvwe[::3].squeeze(),uvwe[1::3].squeeze(),uvwe[2::3].squeeze()]).T H = jnp.dot(DN_DX_T,uvweT).T F = H + jnp.eye(H.shape[0]) indices = jnp.arange(num_nodes) B = jnp.zeros((6, 3 * num_nodes)) B = B.at[0, 3 * indices].set(F[0, 0] * DN_DX_T[0, indices]) B = B.at[0, 3 * indices + 1].set(F[1, 0] * DN_DX_T[0, indices]) B = B.at[0, 3 * indices + 2].set(F[2, 0] * DN_DX_T[0, indices]) B = B.at[1, 3 * indices].set(F[0, 1] * DN_DX_T[1, indices]) B = B.at[1, 3 * indices + 1].set(F[1, 1] * DN_DX_T[1, indices]) B = B.at[1, 3 * indices + 2].set(F[2, 1] * DN_DX_T[1, indices]) B = B.at[2, 3 * indices].set(F[0, 2] * DN_DX_T[2, indices]) B = B.at[2, 3 * indices + 1].set(F[1, 2] * DN_DX_T[2, indices]) B = B.at[2, 3 * indices + 2].set(F[2, 2] * DN_DX_T[2, indices]) B = B.at[3, 3 * indices].set(F[0, 1] * DN_DX_T[2, indices] + F[0, 2] * DN_DX_T[1, indices]) B = B.at[3, 3 * indices + 1].set(F[1, 1] * DN_DX_T[2, indices] + F[1, 2] * DN_DX_T[1, indices]) B = B.at[3, 3 * indices + 2].set(F[2, 1] * DN_DX_T[2, indices] + F[2, 2] * DN_DX_T[1, indices]) B = B.at[4, 3 * indices].set(F[0, 0] * DN_DX_T[2, indices] + F[0, 2] * DN_DX_T[0, indices]) B = B.at[4, 3 * indices + 1].set(F[1, 0] * DN_DX_T[2, indices] + F[1, 2] * DN_DX_T[0, indices]) B = B.at[4, 3 * indices + 2].set(F[2, 0] * DN_DX_T[2, indices] + F[2, 2] * DN_DX_T[0, indices]) B = B.at[5, 3 * indices].set(F[0, 0] * DN_DX_T[1, indices] + F[0, 1] * DN_DX_T[0, indices]) B = B.at[5, 3 * indices + 1].set(F[1, 0] * DN_DX_T[1, indices] + F[1, 1] * DN_DX_T[0, indices]) B = B.at[5, 3 * indices + 2].set(F[2, 0] * DN_DX_T[1, indices] + F[2, 1] * DN_DX_T[0, indices]) return H,F,B @partial(jit, static_argnums=(0,)) def CalculateNMatrix2D(self,N_vec:jnp.array) -> jnp.array: N_mat = jnp.zeros((2, 2 * N_vec.size)) indices = jnp.arange(N_vec.size) N_mat = N_mat.at[0, 2 * indices].set(N_vec) N_mat = N_mat.at[1, 2 * indices + 1].set(N_vec) return N_mat @partial(jit, static_argnums=(0,)) def CalculateNMatrix3D(self,N_vec:jnp.array) -> jnp.array: N_mat = jnp.zeros((3,3*N_vec.size)) N_mat = N_mat.at[0,0::3].set(N_vec) N_mat = N_mat.at[1,1::3].set(N_vec) N_mat = N_mat.at[2,2::3].set(N_vec) return N_mat
[docs] @partial(jit, static_argnums=(0,)) def CalculateQuadGeometricStiffness2D(self,DN_DX_T:jnp.array,S:jnp.array) -> jnp.array: """ Compute the geometric stiffness matrix for a quadratic element. Args: DN_DX_T: (2, num_nodes), shape function derivatives w.r.t spatial coordinates at Gauss point S: (3,1), stress vector in Voigt notation at Gauss point Returns: gp_geo_stiffness: (2*num_nodes, 2*num_nodes), geometric stiffness matrix """ S_mat = jnp.zeros((2,2)) S_mat = S_mat.at[0,0].set(S[0,0]) S_mat = S_mat.at[0,1].set(S[2,0]) S_mat = S_mat.at[1,0].set(S[2,0]) S_mat = S_mat.at[1,1].set(S[1,0]) num_nodes = DN_DX_T.shape[1] gp_geo_stiffness = jnp.zeros((2*num_nodes,2*num_nodes)) def geo_stiffness_entry(i, j, DN_DX_T, S_mat): val = DN_DX_T[:, i].T @ (S_mat @ DN_DX_T[:, j]) return jnp.eye(2) * val # Returns a (2, 2) block # Vectorize over i and j vmap_j = jax.vmap(lambda j: jax.vmap(lambda i: geo_stiffness_entry(i, j, DN_DX_T, S_mat))(jnp.arange(num_nodes)), in_axes=0) blocks = vmap_j(jnp.arange(num_nodes)) # Shape: (4, 4, 2, 2) # Rearrange blocks into full (8, 8) matrix # Vectorized reshape instead of jnp.block gp_geo_stiffness = blocks.transpose(0, 2, 1, 3).reshape(2*num_nodes, 2*num_nodes) return gp_geo_stiffness
[docs] @partial(jit, static_argnums=(0,)) def CalculateTriangleGeometricStiffness2D(self,DN_DX_T:jnp.array,S:jnp.array) -> jnp.array: """ Compute the geometric stiffness matrix for a triangle element. Args: DN_DX_T: (2, num_nodes), shape function derivatives w.r.t spatial coordinates at Gauss point S: (3,1), stress vector in Voigt notation at Gauss point Returns: gp_geo_stiffness: (2*num_nodes, 2*num_nodes), geometric stiffness matrix """ S_mat = jnp.zeros((2,2)) S_mat = S_mat.at[0,0].set(S[0,0]) S_mat = S_mat.at[0,1].set(S[2,0]) S_mat = S_mat.at[1,0].set(S[2,0]) S_mat = S_mat.at[1,1].set(S[1,0]) num_nodes = DN_DX_T.shape[1] gp_geo_stiffness = jnp.zeros((2*num_nodes,2*num_nodes)) def geo_stiffness_entry(i, j, DN_DX_T, S_mat): val = DN_DX_T[:, i].T @ (S_mat @ DN_DX_T[:, j]) return jnp.eye(2) * val # Returns a (2, 2) block # Vectorize over i and j vmap_j = jax.vmap(lambda j: jax.vmap(lambda i: geo_stiffness_entry(i, j, DN_DX_T, S_mat))(jnp.arange(num_nodes)), in_axes=0) blocks = vmap_j(jnp.arange(num_nodes)) # Shape: (3, 3, 2, 2) # Rearrange blocks into full (8, 8) matrix # Vectorized reshape instead of jnp.block gp_geo_stiffness = blocks.transpose(0, 2, 1, 3).reshape(2*num_nodes, 2*num_nodes) return gp_geo_stiffness
[docs] @partial(jit, static_argnums=(0,)) def CalculateTetraGeometricStiffness3D(self,DN_DX_T:jnp.array,S:jnp.array) -> jnp.array: """ Compute the geometric stiffness matrix for a tetra element. Args: DN_DX_T: (3, num_nodes), shape function derivatives w.r.t spatial coordinates at Gauss point S: (6,1), stress vector in Voigt notation at Gauss point Returns: gp_geo_stiffness: (3*num_nodes, 3*num_nodes), geometric stiffness matrix """ S_mat = jnp.zeros((3,3)) S_mat = S_mat.at[0,0].set(S[0,0]) S_mat = S_mat.at[0,1].set(S[5,0]) S_mat = S_mat.at[0,2].set(S[4,0]) S_mat = S_mat.at[1,0].set(S[5,0]) S_mat = S_mat.at[1,1].set(S[1,0]) S_mat = S_mat.at[1,2].set(S[3,0]) S_mat = S_mat.at[2,0].set(S[4,0]) S_mat = S_mat.at[2,1].set(S[3,0]) S_mat = S_mat.at[2,2].set(S[2,0]) num_nodes = DN_DX_T.shape[1] gp_geo_stiffness = jnp.zeros((3*num_nodes,3*num_nodes)) def geo_stiffness_entry(i, j, DN_DX_T, S_mat): val = DN_DX_T[:, i].T @ (S_mat @ DN_DX_T[:, j]) return jnp.eye(3) * val # Returns a (3, 3) block # Vectorize over i and j vmap_j = jax.vmap(lambda j: jax.vmap(lambda i: geo_stiffness_entry(i, j, DN_DX_T, S_mat))(jnp.arange(num_nodes)), in_axes=0) blocks = vmap_j(jnp.arange(num_nodes)) # Shape: (4, 4, 3, 3) # Rearrange blocks into full (12, 12) matrix # Vectorized reshape instead of jnp.block gp_geo_stiffness = blocks.transpose(0, 2, 1, 3).reshape(3*num_nodes, 3*num_nodes) return gp_geo_stiffness
[docs] @partial(jit, static_argnums=(0,)) def CalculateHexaGeometricStiffness3D(self,DN_DX_T:jnp.array,S:jnp.array) -> jnp.array: """ Compute the geometric stiffness matrix for a hexahedral element. Args: DN_DX_T: (3, num_nodes), shape function derivatives w.r.t spatial coordinates at Gauss point S: (6,1), stress vector in Voigt notation at Gauss point Returns: gp_geo_stiffness: (3*num_nodes, 3*num_nodes), geometric stiffness matrix """ S_mat = jnp.zeros((3,3)) S_mat = S_mat.at[0,0].set(S[0,0]) S_mat = S_mat.at[0,1].set(S[5,0]) S_mat = S_mat.at[0,2].set(S[4,0]) S_mat = S_mat.at[1,0].set(S[5,0]) S_mat = S_mat.at[1,1].set(S[1,0]) S_mat = S_mat.at[1,2].set(S[3,0]) S_mat = S_mat.at[2,0].set(S[4,0]) S_mat = S_mat.at[2,1].set(S[3,0]) S_mat = S_mat.at[2,2].set(S[2,0]) num_nodes = DN_DX_T.shape[1] gp_geo_stiffness = jnp.zeros((3*num_nodes,3*num_nodes)) def geo_stiffness_entry(i, j, DN_DX_T, S_mat): val = DN_DX_T[:, i].T @ (S_mat @ DN_DX_T[:, j]) return jnp.eye(3) * val # Returns a (3, 3) block # Vectorize over i and j vmap_j = jax.vmap(lambda j: jax.vmap(lambda i: geo_stiffness_entry(i, j, DN_DX_T, S_mat))(jnp.arange(num_nodes)), in_axes=0) blocks = vmap_j(jnp.arange(num_nodes)) # Shape: (8, 8, 3, 3) # Rearrange blocks into full (24, 24) matrix # Vectorized reshape instead of jnp.block gp_geo_stiffness = blocks.transpose(0, 2, 1, 3).reshape(3*num_nodes, 3*num_nodes) return gp_geo_stiffness
[docs] @partial(jit, static_argnums=(0,)) def ComputeElement(self,xyze,de,uvwe): """ Compute element-level energy, residual, and tangent contributions. This method evaluates the element strain energy by Gaussian quadrature using the Saint Venant–Kirchhoff constitutive model. The returned scalar value represents the strain energy contribution of this element to the total energy of the system. The total loss value is obtained by summing these element energies over all elements in the mesh. The method also computes the element residual vector as the difference between internal and external nodal force vectors and returns the element tangent matrix including both material and geometric stiffness contributions. Args: xyze: Element nodal coordinates. de: Element parameter field values at nodes used to interpolate material parameters to Gauss points. uvwe: Element displacement DOF vector arranged consistently with the element type and ordered degrees of freedom. Returns: Tuple[jax.numpy.ndarray, jax.numpy.ndarray, jax.numpy.ndarray]: - Scalar element strain energy contribution. - Element residual vector defined as ``Fint - Fe``. - Element tangent matrix including material and geometric stiffness contributions. """ @jit def compute_at_gauss_point(gp_point,gp_weight): N_vec = self.fe_element.ShapeFunctionsValues(gp_point) N_mat = self.CalculateNMatrix(N_vec) DN_DX_T = self.fe_element.ShapeFunctionsGlobalGradients(xyze,gp_point).T J = self.fe_element.Jacobian(xyze,gp_point) detJ = jnp.linalg.det(J) e_at_gauss = jnp.dot(N_vec, de.squeeze()) k_at_gauss = e_at_gauss / (3 * (1 - 2*self.v)) mu_at_gauss = e_at_gauss / (2 * (1 + self.v)) lambda_ = e_at_gauss * self.v / ((1 + self.v) * (1 - 2*self.v)) H,F,B = self.CalculateKinematics(DN_DX_T,uvwe) xsi,S,C = self.material_model.evaluate(F,lambda_,mu_at_gauss) gp_geo_stiffness = self.CalculateGeometricStiffness(DN_DX_T,S) gp_stiffness = gp_weight * detJ * (B.T @ C @ B) gp_geo_stiffness = gp_weight * detJ * gp_geo_stiffness # will be added to gp_stiffness gp_f = gp_weight * detJ * N_mat.T @ self.body_force gp_fint = gp_weight * detJ * jnp.dot(B.T,S) gp_energy = gp_weight * detJ * xsi return gp_energy,gp_stiffness + gp_geo_stiffness,gp_f,gp_fint gp_points,gp_weights = self.fe_element.GetIntegrationData() E_gps,k_gps,f_gps,fint_gps = jax.vmap(compute_at_gauss_point,in_axes=(0,0))(gp_points,gp_weights) Se = jnp.sum(k_gps, axis=0) Fe = jnp.sum(f_gps, axis=0) Fint = jnp.sum(fint_gps, axis=0) Ee = jnp.sum(E_gps, axis=0) return Ee, Fint - Fe, Se
[docs]class SaintVenantMechanicalLoss2DQuad(SaintVenantMechanicalLoss): """ Saint Venant–Kirchhoff mechanical energy loss for 2D quadrilateral elements. This class configures :class:`SaintVenantMechanicalLoss` for two-dimensional problems discretized with quadrilateral elements. The displacement field consists of two components (``Ux``, ``Uy``) per node, and the total energy is assembled by summing element energy contributions defined in the base class. If the number of Gauss points is not specified in the loss settings, a default value of ``num_gp = 2`` is used. Args: name (str): Name identifier for the loss instance. loss_settings (dict): Dictionary containing ``material_dict`` and optional settings such as integration parameters. fe_mesh (Mesh): Finite element mesh associated with the loss. """ def __init__(self, name: str, loss_settings: dict, fe_mesh: Mesh): if not "num_gp" in loss_settings.keys(): loss_settings["num_gp"] = 2 super().__init__(name,{**loss_settings,"compute_dims":2, "ordered_dofs": ["Ux","Uy"], "element_type":"quad"},fe_mesh)
[docs]class SaintVenantMechanicalLoss2DTri(SaintVenantMechanicalLoss): """ Saint Venant–Kirchhoff mechanical energy loss for 2D triangular elements. This class configures :class:`SaintVenantMechanicalLoss` for two-dimensional problems discretized with triangular elements. The displacement field consists of two components (``Ux``, ``Uy``) per node, and the total energy is assembled by summing element energy contributions defined in the base class. Args: name (str): Name identifier for the loss instance. loss_settings (dict): Dictionary containing ``material_dict`` and optional settings. fe_mesh (Mesh): Finite element mesh associated with the loss. """ def __init__(self, name: str, loss_settings: dict, fe_mesh: Mesh): super().__init__(name,{**loss_settings,"compute_dims":2, "ordered_dofs": ["Ux","Uy"], "element_type":"triangle"},fe_mesh)
[docs]class SaintVenantMechanicalLoss3DTetra(SaintVenantMechanicalLoss): """ Saint Venant–Kirchhoff mechanical energy loss for 3D tetrahedral elements. This class configures :class:`SaintVenantMechanicalLoss` for three-dimensional problems discretized with tetrahedral elements. The displacement field consists of three components (``Ux``, ``Uy``, ``Uz``) per node, and the total energy is assembled by summing element energy contributions defined in the base class. Args: name (str): Name identifier for the loss instance. loss_settings (dict): Dictionary containing ``material_dict`` and optional settings. fe_mesh (Mesh): Finite element mesh associated with the loss. """ def __init__(self, name: str, loss_settings: dict, fe_mesh: Mesh): super().__init__(name,{**loss_settings,"compute_dims":3, "ordered_dofs": ["Ux","Uy","Uz"], "element_type":"tetra"},fe_mesh)
[docs]class SaintVenantMechanicalLoss3DHexa(SaintVenantMechanicalLoss): """ Saint Venant–Kirchhoff mechanical energy loss for 3D hexahedral elements. This class configures :class:`SaintVenantMechanicalLoss` for three-dimensional problems discretized with hexahedral elements. The displacement field consists of three components (``Ux``, ``Uy``, ``Uz``) per node, and the total energy is assembled by summing element energy contributions defined in the base class. If the number of Gauss points is not specified in the loss settings, a default value of ``num_gp = 2`` is used. Args: name (str): Name identifier for the loss instance. loss_settings (dict): Dictionary containing ``material_dict`` and optional settings such as integration parameters. fe_mesh (Mesh): Finite element mesh associated with the loss. """ def __init__(self, name: str, loss_settings: dict, fe_mesh: Mesh): if not "num_gp" in loss_settings.keys(): loss_settings["num_gp"] = 2 super().__init__(name,{**loss_settings,"compute_dims":3, "ordered_dofs": ["Ux","Uy","Uz"], "element_type":"hexahedron"},fe_mesh)