Source code for fol.loss_functions.transient_thermal

"""
Authors: Yusuke Yamazaki; Reza Najian Asl (https://github.com/RezaNajian)
Date: May, 2025
License: FOL/LICENSE
"""
from  .thermal import ThermalLoss
import jax
import jax.numpy as jnp
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 *

[docs]class TransientThermalLoss(ThermalLoss): """ Transient thermal energy functional with implicit time integration. This class defines an energy-based loss functional for transient heat conduction problems. The total loss value represents the total discrete thermal energy of the system at a time step and is assembled by summing element-level energy contributions over all finite elements in the mesh. For each element, a scalar energy contribution is computed using Gaussian quadrature based on the current and next nodal temperatures. The global energy is obtained by accumulating these element energies across the computational domain. In addition to the scalar energy contribution, the class provides the element residual vector and Jacobian matrix associated with the discrete system used for implicit time stepping. The default time integration method is implicit Euler and is configured through ``loss_settings["time_integration_dict"]``. The thermal conductivity may depend on temperature through the parameters ``beta`` and ``c`` and through a nodal heterogeneity field ``k0``. Args: name (str): Name identifier for the loss instance. loss_settings (dict): Dictionary containing material parameters and time integration settings. Expected entries include a ``"material_dict"`` with material properties and a ``"time_integration_dict"`` specifying the time step. fe_mesh (Mesh): Finite element mesh over which the energy functional is defined. """
[docs] @print_with_timestamp_and_execution_time def Initialize(self,reinitialize=False) -> None: """ Initialize material and time-integration settings. This method initializes the base thermal loss and then configures material parameters and time integration settings. If the instance is already initialized and ``reinitialize`` is ``False``, the method returns without modifying the current configuration. Args: reinitialize (bool, optional): If ``True``, forces reinitialization even if the object is already initialized. Default is ``False``. Raises: ValueError: If the provided nodal conductivity field ``k0`` does not match the number of mesh nodes. ValueError: If ``time_integration_dict["time_step"]`` is not provided. """ if self.initialized and not reinitialize: return super().Initialize() self.default_material_settings = {"rho":1.0,"cp":1.0,"k0":np.ones((self.fe_mesh.GetNumberOfNodes())),"beta":0.0,"c":1.0} self.default_time_integration_settings = {"method":"implicit-euler","time_step":None} if "material_dict" in self.loss_settings.keys(): self.material_settings = UpdateDefaultDict(self.default_material_settings,self.loss_settings["material_dict"]) if "k0" in self.loss_settings["material_dict"].keys(): input_k0_shape = self.loss_settings["material_dict"]["k0"].shape if input_k0_shape != self.default_material_settings["k0"].shape: fol_error(f"provided k0({input_k0_shape}) in the material_dict does not match the mesh with {self.fe_mesh.GetNumberOfNodes()} nodes !") else: self.material_settings["k0"] = jnp.array(self.material_settings["k0"]) else: self.material_settings = self.default_material_settings self.time_integration_settings = UpdateDefaultDict(self.default_time_integration_settings,self.loss_settings["time_integration_dict"]) if self.time_integration_settings["time_step"] == None: fol_error("time step should be provided in the time_integration_dict ")
[docs] def ComputeElement(self,xyze,Te_c,Te_n,Ke): """ Compute element-level energy, residual, and Jacobian contributions. This method evaluates the discrete transient thermal energy contribution of a single finite element using Gaussian quadrature. The returned scalar value represents the energy contribution of this element to the total system energy. The total energy is obtained by summing element energies over all elements in the mesh. The element residual vector and Jacobian matrix correspond to the discrete system used for implicit time integration. The residual has the form of a backward-Euler update using the element mass and conductivity operators constructed from the current and next temperatures. Args: xyze: Element nodal coordinates. Te_c: Element nodal temperatures at the current (previous) time step. Expected shape is compatible with the element node count. Te_n: Element nodal temperatures at the next (unknown) time step. Expected shape is compatible with the element node count. Ke: Element nodal conductivity/heterogeneity field (e.g., nodal values of ``k0``) used to interpolate conductivity to Gauss points. Returns: Tuple[jax.numpy.ndarray, jax.numpy.ndarray, jax.numpy.ndarray]: - Scalar element thermal energy contribution. - Element residual vector for the time-discrete system. - Element Jacobian matrix associated with the residual. """ Te_c = Te_c.reshape(-1,1) Te_n = Te_n.reshape(-1,1) Ke = Ke.reshape(-1,1) def compute_at_gauss_point(gp_point,gp_weight): N_vec = self.fe_element.ShapeFunctionsValues(gp_point) T_at_gauss_n = jnp.dot(N_vec.reshape(1,-1), Te_n) T_at_gauss_c = jnp.dot(N_vec.reshape(1,-1), Te_c) K_at_gauss = jnp.dot(N_vec, Ke.squeeze()) * (1 + self.material_settings["beta"]*(T_at_gauss_n)**self.thermal_loss_settings["c"]) DN_DX = self.fe_element.ShapeFunctionsLocalGradients(gp_point) J = self.fe_element.Jacobian(xyze,gp_point) detJ = jnp.linalg.det(J) invJ = jnp.linalg.inv(J) B_mat = jnp.dot(invJ,DN_DX.T) gp_stiffness = B_mat.T @ B_mat*K_at_gauss * detJ * gp_weight gp_mass = self.material_settings["rho"] * self.material_settings["cp"]* jnp.outer(N_vec, N_vec) * detJ * gp_weight gp_t = self.material_settings["rho"] * self.material_settings["cp"] * 0.5/(self.time_integration_settings["time_step"])*gp_weight * detJ *(T_at_gauss_n-T_at_gauss_c)**2 dk_dT = jnp.dot(N_vec, Ke.squeeze()) * self.material_settings["beta"] * self.thermal_loss_settings["c"] * T_at_gauss_n ** (self.thermal_loss_settings["c"] - 1) gp_dR = (dk_dT * jnp.outer(N_vec, (B_mat@Te_n).T@B_mat) + K_at_gauss *B_mat.T @ B_mat)* detJ * gp_weight return gp_stiffness,gp_mass, gp_t, gp_dR gp_points,gp_weights = self.fe_element.GetIntegrationData() k_gps,m_gps,t_gps, dR_gps = jax.vmap(compute_at_gauss_point,in_axes=(0,0))(gp_points,gp_weights) Se = jnp.sum(k_gps, axis=0) Me = jnp.sum(m_gps, axis=0) Se_dR = jnp.sum(dR_gps, axis=0) Te = jnp.sum(t_gps) element_residuals = jax.lax.stop_gradient((Me+self.time_integration_settings["time_step"]*Se)@Te_n - Me@Te_c) # element_weighted_residual_loss = ((Te_n.T @ element_residuals)[0,0]) return 0.5*Te_n.T@Se@Te_n + Te, (Me+self.time_integration_settings["time_step"]*Se)@Te_n - Me@Te_c, (Me+self.time_integration_settings["time_step"]*Se_dR)
[docs] def ComputeElementEnergy(self, elem_xyz:jnp.array, elem_current_temps:jnp.array, elem_next_temps:jnp.array, elem_heterogeneity:jnp.array) -> float: return self.ComputeElement(elem_xyz,elem_current_temps,elem_next_temps,elem_heterogeneity)[0]
[docs] def ComputeElementEnergyVmapCompatible(self, element_id:jnp.integer, elements_nodes:jnp.array, xyz:jnp.array, nodal_current_temps:jnp.array, nodal_next_temps:jnp.array, nodal_heterogeneity:jnp.array): return self.ComputeElementEnergy(xyz[elements_nodes[element_id],:], nodal_current_temps[elements_nodes[element_id]], nodal_next_temps[elements_nodes[element_id]], nodal_heterogeneity[elements_nodes[element_id]])
[docs] def ComputeElementsEnergies(self,nodal_current_temps:jnp.array,nodal_next_temps:jnp.array): """ Compute element energy contributions for all elements in parallel. Args: nodal_current_temps (jax.numpy.ndarray): Current-step nodal temperatures. nodal_next_temps (jax.numpy.ndarray): Next-step nodal temperatures. Returns: jax.numpy.ndarray: Array of per-element energy contributions. """ return jax.vmap(self.ComputeElementEnergyVmapCompatible,(0,None,None,None,None,None)) \ (self.fe_mesh.GetElementsIds(self.element_type), self.fe_mesh.GetElementsNodes(self.element_type), self.fe_mesh.GetNodesCoordinates(), nodal_current_temps, nodal_next_temps, self.material_settings["k0"])
[docs] def ComputeElementResidualAndJacobian(self, elem_xyz:jnp.array, elem_current_temps:jnp.array, elem_next_temps:jnp.array, elem_heterogeneity:jnp.array, elem_BC:jnp.array, elem_mask_BC:jnp.array, transpose_jac:bool): """ Compute element residual and Jacobian with Dirichlet boundary conditions. This method computes the element residual and Jacobian from :meth:`ComputeElement`, optionally transposes the Jacobian, and applies element-level Dirichlet boundary conditions using the provided boundary condition vectors and masks. Args: elem_xyz (jax.numpy.ndarray): Element nodal coordinates. elem_current_temps (jax.numpy.ndarray): Element nodal temperatures at the current time step. elem_next_temps (jax.numpy.ndarray): Element nodal temperatures at the next time step. elem_heterogeneity (jax.numpy.ndarray): Element nodal heterogeneity/conductivity field. elem_BC (jax.numpy.ndarray): Element Dirichlet boundary condition vector. elem_mask_BC (jax.numpy.ndarray): Element boundary condition mask. transpose_jac (bool): If ``True``, the element Jacobian is transposed before applying boundary conditions. Returns: Tuple[jax.numpy.ndarray, jax.numpy.ndarray]: Element residual vector and Jacobian matrix after applying Dirichlet boundary conditions. """ _,re,ke = self.ComputeElement(elem_xyz,elem_current_temps,elem_next_temps,elem_heterogeneity) # Convert transpose_jac (bool) to an integer index (0 = False, 1 = True) index = jnp.asarray(transpose_jac, 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)
[docs] def ComputeElementResidualAndJacobianVmapCompatible(self,element_id:jnp.integer, elements_nodes:jnp.array, xyz:jnp.array, nodal_current_temps:jnp.array, nodal_next_temps:jnp.array, full_dirichlet_BC_vec:jnp.array, full_mask_dirichlet_BC_vec:jnp.array, transpose_jac:bool): """ VMAP-compatible wrapper to compute element residual and Jacobian by element id. Args: element_id (jax.numpy.ndarray): Element index. elements_nodes (jax.numpy.ndarray): Element connectivity array. xyz (jax.numpy.ndarray): Nodal coordinate array. nodal_current_temps (jax.numpy.ndarray): Current-step nodal temperatures. nodal_next_temps (jax.numpy.ndarray): Next-step nodal temperatures. full_dirichlet_BC_vec (jax.numpy.ndarray): Global Dirichlet boundary condition vector. full_mask_dirichlet_BC_vec (jax.numpy.ndarray): Global boundary condition mask vector. transpose_jac (bool): If ``True``, the element Jacobian is transposed before applying boundary conditions. Returns: Tuple[jax.numpy.ndarray, jax.numpy.ndarray]: Element residual vector and Jacobian matrix after applying Dirichlet boundary conditions. """ return self.ComputeElementResidualAndJacobian(xyz[elements_nodes[element_id],:], nodal_current_temps[elements_nodes[element_id]], nodal_next_temps[elements_nodes[element_id]], self.material_settings["k0"][elements_nodes[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), transpose_jac)
[docs]class TransientThermalLoss3DTetra(TransientThermalLoss): """ Transient thermal loss for 3D tetrahedral finite elements. This class configures :class:`TransientThermalLoss` for three-dimensional problems discretized with tetrahedral elements. The temperature field has a single DOF per node (``T``), 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 ``time_integration_dict``. fe_mesh (Mesh): Finite element mesh associated with the loss. """ @print_with_timestamp_and_execution_time def __init__(self, name: str, loss_settings: dict, fe_mesh: Mesh): super().__init__(name,{**loss_settings,"compute_dims":3, "ordered_dofs": ["T"], "element_type":"tetra"},fe_mesh)
[docs]class TransientThermalLoss3DHexa(TransientThermalLoss): """ Transient thermal loss for 3D hexahedral finite elements. This class configures :class:`TransientThermalLoss` for three-dimensional problems discretized with hexahedral elements. The temperature field has a single DOF per node (``T``), 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 ``time_integration_dict``. fe_mesh (Mesh): Finite element mesh associated with the loss. """ @print_with_timestamp_and_execution_time 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": ["T"], "element_type":"hexahedron"},fe_mesh)
[docs]class TransientThermalLoss2DQuad(TransientThermalLoss): """ Transient thermal loss for 2D quadrilateral finite elements. This class configures :class:`TransientThermalLoss` for two-dimensional problems discretized with quadrilateral elements. The temperature field has a single DOF per node (``T``), 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 ``time_integration_dict``. 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": ["T"], "element_type":"quad"},fe_mesh)
[docs]class TransientThermalLoss2DTri(TransientThermalLoss): """ Transient thermal loss for 2D triangular finite elements. This class configures :class:`TransientThermalLoss` for two-dimensional problems discretized with triangular elements. The temperature field has a single DOF per node (``T``), 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 ``time_integration_dict``. 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": ["T"], "element_type":"triangle"},fe_mesh)