Source code for fol.deep_neural_networks.explicit_parametric_operator_learning

"""
 Authors: Reza Najian Asl, https://github.com/RezaNajian
 Date: April, 2024
 License: FOL/LICENSE
"""

from typing import Iterator,Tuple
import jax
import jax.numpy as jnp
from jax import jit,vmap
from functools import partial
from optax import GradientTransformation
from flax import nnx
from .deep_network import DeepNetwork
from fol.tools.decoration_functions import *
from fol.loss_functions.loss import Loss
from fol.controls.control import Control
from fol.tools.usefull_functions import *

[docs]class ExplicitParametricOperatorLearning(DeepNetwork): """ Explicit parametric operator learning on discretized fields. This class implements explicit parametric operator learning where both the input parameter space and the output field space are discretized and have fixed shape. The input typically represents a low-dimensional parametric space, such as control variables of a Fourier parametrization, while the output corresponds to a high-dimensional discretized field, for example a temperature or displacement field. The neural network explicitly represents the mapping from parametric inputs to field unknowns. Learning is unsupervised or physics-informed: no direct target fields are required. Instead, training is driven by physical loss functions evaluated on the predicted fields, such as residual-based or energy-based formulations. The :class:`Control` object maps parametric inputs to controlled variables, and the :class:`Loss` object evaluates the predicted fields after Dirichlet boundary conditions are applied. Consistency between network dimensions, control variables, and loss unknowns is enforced during initialization. Args: name (str): Name identifier for the model instance, used for logging and checkpointing. control (Control): Control object defining the parametric input space and how raw parameters are mapped to controlled variables. loss_function (Loss): Physics-based loss function used to evaluate predicted fields. The loss operates on full discretized fields and does not require supervised target data. flax_neural_network (nnx.Module): Flax/NNX neural network mapping parametric inputs to unknown field degrees of freedom. The network must expose ``in_features`` and ``out_features`` attributes. optax_optimizer (GradientTransformation): Optax optimizer transformation used to construct the optimizer for training. Raises: RuntimeError: If the neural network input or output dimensions are inconsistent with the control variable size or the number of unknowns defined by the loss function. """ def __init__(self, name:str, control:Control, loss_function:Loss, flax_neural_network:nnx.Module, optax_optimizer:GradientTransformation): super().__init__(name,loss_function,flax_neural_network, optax_optimizer) self.control = control
[docs] @print_with_timestamp_and_execution_time def Initialize(self,reinitialize=False) -> None: """ Initialize model, loss, control, optimizer, and consistency checks. This method first runs the base-class initialization, which prepares the loss function, constructs the Orbax checkpointer, and builds the :class:`nnx.Optimizer` around the provided network and Optax transformation. It then ensures that the associated control object is initialized and finally checks that the neural network input and output dimensions are consistent with the control and loss function sizes. Concretely, the following consistency checks are performed: * ``flax_neural_network.in_features`` is compared to ``control.GetNumberOfVariables()`` to ensure that the parametric input dimension matches the control space. * ``flax_neural_network.out_features`` is compared to ``loss_function.GetNumberOfUnknowns()`` to ensure that the network output dimension matches the number of unknown field DOFs. Args: reinitialize (bool, optional): If ``True``, force reinitialization of all components, even if they have already been initialized. Default is ``False``. Returns: None Raises: RuntimeError: If the network does not expose ``in_features`` or ``out_features``, or if these dimensions are inconsistent with the control or loss function sizes. The underlying implementation uses ``fol_error`` to signal these conditions. """ if self.initialized and not reinitialize: return super().Initialize(reinitialize) if not self.control.initialized: self.control.Initialize(reinitialize) self.initialized = True # now check if the input output layers size match with # loss and control sizes, this is explicit parametric learning if not hasattr(self.flax_neural_network, 'in_features'): fol_error(f"the provided flax neural netwrok does not have in_features "\ "which specifies the size of the input layer ") if not hasattr(self.flax_neural_network, 'out_features'): fol_error(f"the provided flax neural netwrok does not have out_features "\ "which specifies the size of the output layer") if self.flax_neural_network.in_features != self.control.GetNumberOfVariables(): fol_error(f"the size of the input layer is {self.flax_neural_network.in_features} "\ f"does not match the size of control variables {self.control.GetNumberOfVariables()}") if self.flax_neural_network.out_features != self.loss_function.GetNumberOfUnknowns(): fol_error(f"the size of the output layer is {self.flax_neural_network.out_features} " \ f" does not match the size of unknowns of the loss function {self.loss_function.GetNumberOfUnknowns()}")
[docs] def ComputeBatchPredictions(self,batch_X:jnp.ndarray,nn_model:nnx.Module): """ Compute network predictions for a batch of parametric inputs. This helper applies the provided neural network to a batch of parametric inputs ``batch_X`` and returns the corresponding batch of predicted unknown DOFs. It does not insert Dirichlet values or build full field vectors; that is handled at the loss level. Args: batch_X (jax.numpy.ndarray): Batch of parametric inputs (for example control variables entering the operator). The leading dimension is the batch size, and the second dimension must match ``nn_model.in_features``. nn_model (nnx.Module): Neural network that maps parametric inputs to unknown DOFs. Returns: jax.numpy.ndarray: Batch of predicted unknown DOF vectors, one per row in ``batch_X``. Raises: None: This method is a thin wrapper around the network call and does not introduce additional failure modes beyond those of ``nn_model`` itself. """ return nn_model(batch_X)
[docs] def ComputeBatchLossValue(self,batch:Tuple[jnp.ndarray, jnp.ndarray],nn_model:nnx.Module): """ Compute the batch loss for explicit parametric operator learning. This method evaluates the loss in an unsupervised or physics-informed setting. The batch is provided as a tuple for consistency with the :class:`DeepNetwork` base class interface, but only the first entry is used. The second entry of the batch tuple is expected to be ``None`` and does not represent supervised targets. The computation proceeds by mapping the batch of parametric inputs to controlled variables using the associated :class:`Control` object, then predicting the unknown degrees of freedom with the neural network. The predicted unknowns are inserted into full discretized field vectors, and the loss function is evaluated directly on these fields using physical constraints or residual-based objectives. The returned metrics dictionary always includes the key ``"total_loss"``, which is required by the training loop for logging, convergence checks, plotting, and checkpointing. Args: batch (Tuple[jax.numpy.ndarray, None]): Batch tuple ``(batch_X, None)`` where ``batch_X`` contains the parametric input samples. The second entry is unused and present only to maintain a consistent interface with the base class. nn_model (nnx.Module): Neural network used to infer the unknown degrees of freedom from the parametric inputs. Returns: Tuple[jax.numpy.ndarray, dict]: A tuple ``(batch_loss, metrics_dict)`` where ``batch_loss`` is the scalar loss aggregated over the batch, and ``metrics_dict`` contains loss statistics including the mandatory key ``"total_loss"``. Raises: None: This method assumes consistency between the control, loss function, and network dimensions, which is enforced during initialization. """ control_outputs = self.control.ComputeBatchControlledVariables(batch[0]) batch_unknowns_predictions = self.ComputeBatchPredictions(batch[0],nn_model) batch_full_pred = jnp.zeros((batch[0].shape[0],self.loss_function.GetTotalNumberOfDOFs())) batch_full_pred = batch_full_pred.at[:,self.loss_function.non_dirichlet_indices].set(batch_unknowns_predictions) batch_loss,(batch_min,batch_max,batch_avg) = self.loss_function.ComputeBatchLoss(control_outputs,batch_full_pred) loss_name = self.loss_function.GetName() return batch_loss, ({loss_name+"_min":batch_min, loss_name+"_max":batch_max, loss_name+"_avg":batch_avg, "total_loss":batch_loss})
[docs] @print_with_timestamp_and_execution_time @partial(nnx.jit, static_argnums=(0,), donate_argnums=1) def Predict(self,batch_X): """ Perform inference for a batch of parametric inputs and apply Dirichlet boundary conditions. This method runs inference by evaluating the trained neural network on a batch of parametric inputs to predict the unknown degrees of freedom. The predicted unknowns are then embedded into full discretized field vectors, and Dirichlet boundary conditions are applied by setting the prescribed boundary values at the corresponding DOF indices for every sample in the batch. The result is a batch of full field predictions that satisfy the imposed boundary conditions and are suitable for post-processing or evaluation. Args: batch_X (jax.numpy.ndarray): Batch of parametric inputs used for inference. The leading dimension corresponds to the batch size, and the feature dimension must match the number of control variables. Returns: jax.numpy.ndarray: Batch of full discretized field vectors obtained by inference, with Dirichlet boundary conditions applied consistently across the batch. Raises: None """ batch_unknowns_predictions = self.ComputeBatchPredictions(batch_X,self.flax_neural_network) batch_full_pred = jnp.zeros((batch_X.shape[0],self.loss_function.GetTotalNumberOfDOFs())) batch_full_pred = batch_full_pred.at[:,self.loss_function.non_dirichlet_indices].set(batch_unknowns_predictions) return self.loss_function.GetFullDofVector(batch_X,batch_full_pred.reshape(batch_full_pred.shape[0], -1))
[docs] def Finalize(self): pass