Source code for fol.loss_functions.kratos_small_displacement

"""
 Authors: Reza Najian Asl, https://github.com/RezaNajian
 Date: Sep, 2025
 License: FOL/LICENSE
"""
try:
    import fol_ffi_functions
    _HAS_FOL_FFI_LIB = True
except ImportError:
    _HAS_FOL_FFI_LIB = False

from  fol.loss_functions.fe_loss import FiniteElementLoss
import jax
import jax.numpy as jnp
from jax import jit
from functools import partial
from fol.tools.decoration_functions import *
from fol.tools.fem_utilities import *
from fol.mesh_input_output.mesh import Mesh
from fol.tools.usefull_functions import *
from jax.experimental import sparse

if _HAS_FOL_FFI_LIB:
    from fol_ffi_functions import kr_small_displacement_element
    jax.ffi.register_ffi_target("compute_nodal_residuals", kr_small_displacement_element.compute_nodal_residuals(), platform="CUDA")
    jax.ffi.register_ffi_type_id("compute_nodal_residuals", kr_small_displacement_element.type_id(), platform="CUDA")
    jax.ffi.register_ffi_target("compute_elements", kr_small_displacement_element.compute_elements(), platform="CUDA")

[docs]class KratosSmallDisplacement3DTetra(FiniteElementLoss): """ Kratos-based 3D small-displacement tetrahedral loss accelerated via JAX FFI. This class provides a finite element loss for linear small-displacement elasticity in 3D using tetrahedral elements. It derives from :class:`fol.loss_functions.fe_loss.FiniteElementLoss` and reuses the base class infrastructure for DOF bookkeeping, Dirichlet boundary conditions, and sparse assembly. In contrast to standard Python/JAX element implementations, this class delegates element-level computations to custom CUDA kernels exposed through ``fol_ffi_functions`` and registered with ``jax.ffi``. The kernels compute element residuals and element stiffness matrices, which are then processed to enforce Dirichlet boundary conditions and assembled into a global sparse Jacobian matrix and residual vector. The total loss value is computed in an energy-consistent manner using the dot product of the global displacement vector with the global nodal residual vector produced by the FFI kernel. Args: name (str): Name identifier for the loss instance. loss_settings (dict): Configuration dictionary. User-provided settings may include ``"material_dict"`` with keys ``"poisson_ratio"`` and ``"young_modulus"`` and the standard FE settings expected by the base class. The element discretization is fixed to 3D tetrahedra with displacement DOFs. fe_mesh (Mesh): Finite element mesh containing node coordinates and tetrahedral connectivity. Raises: RuntimeError: If ``fol_ffi_functions`` is not available. Notes: This class relies on CUDA FFI targets registered at import time. It is expected to run on platforms where the registered targets are available. Element-level routines are not implemented in Python for this class. """ @print_with_timestamp_and_execution_time def __init__(self, name: str, loss_settings: dict, fe_mesh: Mesh): if not _HAS_FOL_FFI_LIB: fol_error(" fol_ffi_functions is not available, install by running install script under ffi_functions folder!") super().__init__(name,{**loss_settings,"compute_dims":3, "ordered_dofs": ["Ux","Uy","Uz"], "element_type":"tetra"},fe_mesh)
[docs] @print_with_timestamp_and_execution_time def Initialize(self,reinitialize=False) -> None: """ Initialize FE bookkeeping and material parameters. This method calls the base class initialization and then prepares the material parameters used by the CUDA kernels. If ``material_dict`` is provided in ``loss_settings``, it is used directly; otherwise default values are applied. Args: reinitialize (bool, optional): If ``True``, re-run initialization even if already initialized. Default is ``False``. Returns: None """ if self.initialized and not reinitialize: return super().Initialize() self.material_settings = {"poisson_ratio":0.3,"young_modulus":1.0} if "material_dict" in self.loss_settings.keys(): self.material_settings = self.loss_settings["material_dict"]
def ComputeElement(self,xyze,de,te,body_force=0): """ Element-level computation is not available for this FFI-based loss. The element residual and stiffness contributions are computed in CUDA through JAX FFI. The Python element routine is intentionally not implemented for this class. Args: xyze: Element nodal coordinates. de: Element control values. te: Element DOF vector. body_force: Body force term (unused). Returns: None Raises: RuntimeError: Always raised because the method is not implemented. """ fol_error(" is not implemented for KratosSmallDisplacement3DTetra!")
[docs] def ComputeTotalEnergy(self,total_control_vars:jnp.array,total_primal_vars:jnp.array): """ Compute the total energy-consistent scalar loss value for the system. This method calls the FFI kernel ``compute_nodal_residuals`` to evaluate the global nodal residual vector for the provided displacement field. The returned scalar value is computed as the dot product of the global displacement vector with the global residual vector. Args: total_control_vars (jax.numpy.ndarray): Control variables. This argument is accepted for API compatibility and is not used by the current FFI kernels. total_primal_vars (jax.numpy.ndarray): Global displacement (DOF) vector with shape ``(total_number_of_dofs,)`` or compatible. The vector is reshaped internally to a batch of size 1. Returns: jax.numpy.ndarray: Scalar loss value computed from the displacement field and the nodal residuals. """ total_primal_vars = total_primal_vars.reshape(1,-1) batch_size = 1 nodal_res_type = jax.ShapeDtypeStruct((batch_size,self.number_dofs_per_node*self.fe_mesh.GetNumberOfNodes()), total_primal_vars.dtype) nodal_res = jax.ffi.ffi_call("compute_nodal_residuals", nodal_res_type, vmap_method="legacy_vectorized")(self.fe_mesh.GetNodesCoordinates(), self.fe_mesh.GetElementsNodes(self.element_type), jnp.array([self.material_settings["poisson_ratio"], self.material_settings["young_modulus"]]),total_primal_vars) return (total_primal_vars @ nodal_res.T)[0,0]
[docs] @print_with_timestamp_and_execution_time @partial(jit, static_argnums=(0,)) def ComputeJacobianMatrixAndResidualVector(self,total_control_vars: jnp.array,total_primal_vars: jnp.array,transpose_jacobian:bool=False): """ Assemble the global sparse Jacobian matrix and residual vector using FFI. This method calls the FFI kernel ``compute_elements`` to compute element stiffness matrices and element residual vectors for all tetrahedral elements. Dirichlet boundary conditions are applied at element level using the base class masking strategy, and the processed contributions are assembled into: - a global residual vector, and - a global sparse Jacobian matrix in BCOO format. Args: total_control_vars (jax.numpy.ndarray): Control variables. This argument is accepted for API compatibility and is not used by the current FFI kernels. total_primal_vars (jax.numpy.ndarray): Global displacement (DOF) vector. transpose_jacobian (bool, optional): If ``True``, element stiffness matrices are transposed before applying boundary conditions and assembly. Default is ``False``. Returns: Tuple[jax.experimental.sparse.BCOO, jax.numpy.ndarray]: A tuple containing the global sparse Jacobian matrix and the global residual vector. """ BC_vector = jnp.ones((self.total_number_of_dofs)) BC_vector = BC_vector.at[self.dirichlet_indices.astype(jnp.int32)].set(0) mask_BC_vector = jnp.zeros((self.total_number_of_dofs)) mask_BC_vector = mask_BC_vector.at[self.dirichlet_indices.astype(jnp.int32)].set(1) total_num_elements = self.fe_mesh.GetNumberOfElements(self.element_type) num_nodes_per_elem = len(self.fe_mesh.GetElementsNodes(self.element_type)[0]) element_matrix_size = self.number_dofs_per_node * num_nodes_per_elem lhs_type = jax.ShapeDtypeStruct((total_num_elements,element_matrix_size,element_matrix_size), total_primal_vars.dtype) rhs_type = jax.ShapeDtypeStruct((total_num_elements,element_matrix_size), total_primal_vars.dtype) lhs,res = jax.ffi.ffi_call("compute_elements", (lhs_type, rhs_type))(self.fe_mesh.GetNodesCoordinates(), self.fe_mesh.GetElementsNodes(self.element_type), jnp.array([self.material_settings["poisson_ratio"], self.material_settings["young_modulus"]]), total_primal_vars) def Proccess(ke:jnp.array, re:jnp.array, elem_BC:jnp.array, elem_mask_BC:jnp.array): index = jnp.asarray(transpose_jacobian, dtype=jnp.int32) # Define the two branches for switch branches = [ lambda _: ke, # Case 0: No transpose lambda _: jnp.transpose(ke) # Case 1: Transpose ke ] # Apply the switch operation ke = jax.lax.switch(index, branches, None) return self.ApplyDirichletBCOnElementResidualAndJacobian(re,ke,elem_BC,elem_mask_BC) def ProccessVmapCompatible(element_id:jnp.integer, elements_nodes:jnp.array, elements_stiffness:jnp.array, elements_residuals:jnp.array, full_dirichlet_BC_vec:jnp.array, full_mask_dirichlet_BC_vec:jnp.array): return Proccess(elements_stiffness[element_id], elements_residuals[element_id], full_dirichlet_BC_vec[((self.number_dofs_per_node*elements_nodes[element_id])[:, jnp.newaxis] + jnp.arange(self.number_dofs_per_node))].reshape(-1,1), full_mask_dirichlet_BC_vec[((self.number_dofs_per_node*elements_nodes[element_id])[:, jnp.newaxis] + jnp.arange(self.number_dofs_per_node))].reshape(-1,1)) elements_residuals, elements_stiffness = jax.vmap(ProccessVmapCompatible, (0,None,None,None,None,None))(self.fe_mesh.GetElementsIds(self.element_type), self.fe_mesh.GetElementsNodes(self.element_type), lhs, res, BC_vector, mask_BC_vector) # first compute the global residual vector residuals_vector = jnp.zeros((self.total_number_of_dofs)) for dof_idx in range(self.number_dofs_per_node): residuals_vector = residuals_vector.at[self.number_dofs_per_node*self.fe_mesh.GetElementsNodes(self.element_type)+dof_idx].add(jnp.squeeze(elements_residuals[:,dof_idx::self.number_dofs_per_node])) # second compute the global jacobian matrix jacobian_data = jnp.ravel(elements_stiffness) jacobian_indices = jax.vmap(self.ComputeElementJacobianIndices)(self.fe_mesh.GetElementsNodes(self.element_type)) # Get the indices jacobian_indices = jacobian_indices.reshape(-1,2) sparse_jacobian = sparse.BCOO((jacobian_data,jacobian_indices),shape=(self.total_number_of_dofs,self.total_number_of_dofs)) return sparse_jacobian, residuals_vector