Source code for neural_lam.models.step_predictors.graph.graph_efm

"""Graph-based Ensemble Forecasting Model (Graph-EFM) single-step
predictors, for hierarchical (GraphEFM) and flat (GraphEFMMultiScale) mesh
graphs."""

# Standard library
from typing import Any, Dict, Optional

# Third-party
import torch
from torch import nn

# Local
from .... import utils
from ....datastore import BaseDatastore
from ...latent import (
    ConstantLatentEncoder,
    GraphLatentDecoder,
    GraphLatentEncoder,
    HiGraphLatentDecoder,
    HiGraphLatentEncoder,
)
from ..base import StepPredictor


[docs] class BaseGraphEFM(StepPredictor): """ Base class for Graph-based Ensemble Forecasting Model single-step predictors. A latent-variable step predictor consisting of a conditional prior, a variational encoder and a latent decoder, each of which carries its own grid-to-mesh, on-mesh and mesh-to-grid GNNs. The encode-process-decode backbone of ``BaseGraphModel`` therefore does not apply -- this extends ``StepPredictor`` directly. ``forward`` samples a single step from the prior. The encoder (variational posterior) and prior are exposed as ``self.encoder``/``self.prior_model`` for a forecaster to condition on the target and assemble a training objective from; the predictor itself does not compute any loss. This base class sets up everything that is independent of the mesh graph type: it loads the graph, calls the subclass's :meth:`check_graph_type` to verify the loaded graph matches what it requires, then builds the prior, delegating to the subclass's :meth:`build_learnable_prior` when learned and to :attr:`latent_spatial_dim` for the constant prior's node count. Concrete subclasses build the mesh embedders and the encoder/decoder latent modules, and implement :meth:`embedd_mesh`, :meth:`check_graph_type`, :meth:`build_learnable_prior` and :attr:`latent_spatial_dim`. See :class:`GraphEFM` (hierarchical graph) and :class:`GraphEFMMultiScale` (flat graph). """ g2m_edge_index: torch.Tensor m2g_edge_index: torch.Tensor def __init__( self, datastore: BaseDatastore, graph_name: str, hidden_dim: int = 64, hidden_layers: int = 1, latent_dim: Optional[int] = None, learn_prior: bool = True, prior_dist: str = "isotropic", prior_layers: int = 2, g2m_gnn_type: str = "InteractionNet", num_past_forcing_steps: int = 1, num_future_forcing_steps: int = 1, output_std: bool = False, output_clamping_lower: Optional[Dict[str, float]] = None, output_clamping_upper: Optional[Dict[str, float]] = None, ) -> None: """ Set up the graph-type independent parts of the predictor. Loads the graph, builds the grid embedders, the grid-mesh edge embedders and the prior. Building the mesh embedders and the encoder/decoder latent modules is left to the subclass constructor. Parameters ---------- datastore : BaseDatastore Datastore providing static features, standardization statistics and variable counts. graph_name : str Name of the graph directory (under ``<root>/graph``) to load. Must be of the graph type required by the concrete subclass. hidden_dim : int Dimensionality of internal node and edge representations. hidden_layers : int Number of hidden layers in internal MLPs. latent_dim : int, optional Dimensionality of the latent variable at each latent-carrying mesh node (top level for hierarchical graphs, all mesh nodes for flat graphs); defaults to ``hidden_dim`` when None. The resolved value is stored as ``self.latent_dim`` for the subclass to reuse when building its encoder/decoder. learn_prior : bool If True, the prior is the graph-type specific learnable encoder built by :meth:`build_learnable_prior`, conditioned on the previous state; if False, a constant ``Normal(0, 1)`` prior is used. prior_dist : str Output distribution of the prior: ``"isotropic"`` or ``"diagonal"``. prior_layers : int Number of on-mesh GNN layers in the learnable prior. g2m_gnn_type : str GNN type for the grid-to-mesh step of the prior (key in ``gnn_layers.GNN_TYPES``). num_past_forcing_steps : int Number of past forcing steps included in the input window. num_future_forcing_steps : int Number of future forcing steps included in the input window. output_std : bool If True, the decoder outputs a per-variable std alongside the mean; if False, ``forward`` returns ``None`` for the std. output_clamping_lower : dict of str to float, optional Lower clamping limits per output variable. output_clamping_upper : dict of str to float, optional Upper clamping limits per output variable. """ super().__init__( datastore=datastore, output_std=output_std, output_clamping_lower=output_clamping_lower, output_clamping_upper=output_clamping_upper, ) # Retrieve difference statistics for rescaling in forward pass, as # for the deterministic models: this keeps the decoder's output # (like the deterministic models' network output) at a similar # scale regardless of a variable's own timescale. da_state_stats = datastore.get_standardization_dataarray("state") self.register_buffer( "diff_mean", torch.tensor( da_state_stats.state_diff_mean_standardized.values, dtype=torch.float32, ), persistent=False, ) self.register_buffer( "diff_std", torch.tensor( da_state_stats.state_diff_std_standardized.values, dtype=torch.float32, ), persistent=False, ) # Load graph with static features. grid_xy_extent = datastore.get_xy_extent(category="state") grid_xy_max_span = max( grid_xy_extent[1] - grid_xy_extent[0], grid_xy_extent[3] - grid_xy_extent[2], ) self.hierarchical = utils.load_and_register_graph( self, datastore, graph_name, mesh_node_features_scaling=grid_xy_max_span, ) # Delegated to the subclass, which knows whether it requires a # hierarchical or flat mesh graph. Must run before anything below # that assumes a specific graph shape (build_learnable_prior, # latent_spatial_dim), so it cannot wait until the subclass # constructor resumes after this call returns. self.check_graph_type(graph_name) # Specify dimensions of data self.num_state_vars = datastore.get_num_data_vars(category="state") num_state_vars = self.num_state_vars # grid_dim: total grid input dim. grid_current_dim additionally # includes the target state, for the encoder input. self.grid_dim = utils.compute_grid_input_dim( datastore, num_past_forcing_steps, num_future_forcing_steps, ) grid_current_dim = self.grid_dim + num_state_vars assert isinstance(self.g2m_features, torch.Tensor) assert isinstance(self.m2g_features, torch.Tensor) g2m_dim = self.g2m_features.shape[1] m2g_dim = self.m2g_features.shape[1] # Define sub-models # Feature embedders for grid self.mlp_blueprint_end = [hidden_dim] * (hidden_layers + 1) self.grid_prev_embedder = utils.make_mlp( [self.grid_dim] + self.mlp_blueprint_end ) # For states up to t-1 self.grid_current_embedder = utils.make_mlp( [grid_current_dim] + self.mlp_blueprint_end ) # For states including t # Embedders for mesh edges self.g2m_embedder = utils.make_mlp([g2m_dim] + self.mlp_blueprint_end) self.m2g_embedder = utils.make_mlp([m2g_dim] + self.mlp_blueprint_end) # Compute indices and define the clamping functions applied to the # predicted next-state mean in forward. self.prepare_clamping_params(datastore) # Prior over the latent variable. When learn_prior is True the # (graph-type specific) learnable prior is delegated to # build_learnable_prior; otherwise the constant Normal(0, 1) prior, # identical for every graph type, is built directly here. self.latent_dim = latent_dim if latent_dim is not None else hidden_dim if learn_prior: self.prior_model = self.build_learnable_prior( latent_dim=self.latent_dim, hidden_dim=hidden_dim, hidden_layers=hidden_layers, g2m_gnn_type=g2m_gnn_type, prior_dist=prior_dist, prior_layers=prior_layers, ) else: self.prior_model = ConstantLatentEncoder( latent_dim=self.latent_dim, num_mesh_nodes=self.latent_spatial_dim, output_dist=prior_dist, )
[docs] def check_graph_type(self, graph_name: str) -> None: """ Verify the loaded graph (``self.hierarchical``) is of the type this predictor requires. Implemented by the concrete subclass, which is the only place that knows whether it requires a hierarchical or flat mesh graph. Called by this base class right after loading the graph, before anything that assumes a specific graph shape. Parameters ---------- graph_name : str Name of the graph directory that was loaded, for the error message. Raises ------ ValueError If ``self.hierarchical`` does not match what this predictor requires. """ raise NotImplementedError("check_graph_type not implemented")
@property def latent_spatial_dim(self) -> int: """ Number of mesh nodes the latent variable lives on. Implemented by the concrete subclass, which knows the mesh graph type: the top mesh level for hierarchical graphs, or every mesh node for flat graphs. Returns ------- int Number of latent-carrying mesh nodes. """ raise NotImplementedError("latent_spatial_dim not implemented")
[docs] def build_learnable_prior( self, latent_dim: int, hidden_dim: int, hidden_layers: int, g2m_gnn_type: str, prior_dist: str, prior_layers: int, ) -> nn.Module: """ Build the graph-type specific learnable prior encoder. Implemented by the concrete subclass, which knows the mesh graph type and therefore the appropriate latent encoder class. Parameters ---------- latent_dim : int Dimensionality of the latent variable at each mesh node. hidden_dim : int Dimensionality of internal node and edge representations. hidden_layers : int Number of hidden layers in internal MLPs. g2m_gnn_type : str GNN type for the grid-to-mesh step of the prior. prior_dist : str Output distribution of the prior: ``"isotropic"`` or ``"diagonal"``. prior_layers : int Number of on-mesh GNN layers in the prior. Returns ------- torch.nn.Module The learnable prior latent encoder. """ raise NotImplementedError("build_learnable_prior not implemented")
[docs] def embedd_grid_with_target( self, prev_state: torch.Tensor, prev_prev_state: torch.Tensor, forcing: torch.Tensor, current_state: torch.Tensor, ) -> torch.Tensor: """ Embed the grid representation including the current (target) state. Used as input to the encoder, which is conditioned also on the target. Parameters ---------- prev_state : torch.Tensor Shape ``(B, num_grid_nodes, d_state)``. ``X_t``. prev_prev_state : torch.Tensor Shape ``(B, num_grid_nodes, d_state)``. ``X_{t-1}``. forcing : torch.Tensor Shape ``(B, num_grid_nodes, d_forcing)``. current_state : torch.Tensor Shape ``(B, num_grid_nodes, d_state)``. ``X_{t+1}`` (target). Returns ------- torch.Tensor Shape ``(B, num_grid_nodes, d_h)``. Grid embedding. """ batch_size = prev_state.shape[0] grid_current_features = torch.cat( ( prev_prev_state, prev_state, forcing, self.expand_to_batch(self.grid_static_features, batch_size), current_state, ), dim=-1, ) # (B, num_grid_nodes, grid_current_dim) return self.grid_current_embedder( grid_current_features ) # (B, num_grid_nodes, d_h)
[docs] def embedd_mesh(self, batch_size: int) -> dict[str, Any]: """ Embed static mesh node and intra-mesh edge features. Parameters ---------- batch_size : int Batch size to expand the embeddings to. Returns ------- dict Mesh-related entries of the graph embedding (``mesh``, ``m2m`` and, for hierarchical graphs, ``mesh_up`` and ``mesh_down``). Entries are tensors of shape ``(B, *, d_h)`` for flat graphs and per-level lists of such tensors for hierarchical graphs. """ raise NotImplementedError("embedd_mesh not implemented")
[docs] def embedd_grid_and_graph( self, prev_state: torch.Tensor, prev_prev_state: torch.Tensor, forcing: torch.Tensor, ) -> tuple[torch.Tensor, dict[str, Any]]: """ Embed the grid (states up to t-1) and the full graph. Parameters ---------- prev_state : torch.Tensor Shape ``(B, num_grid_nodes, d_state)``. ``X_t``. prev_prev_state : torch.Tensor Shape ``(B, num_grid_nodes, d_state)``. ``X_{t-1}``. forcing : torch.Tensor Shape ``(B, num_grid_nodes, d_forcing)``. Returns ------- grid_emb : torch.Tensor Shape ``(B, num_grid_nodes, d_h)``. Grid embedding. graph_emb : dict Edge/mesh embeddings, each entry of shape ``(B, *, d_h)``. """ batch_size = prev_state.shape[0] grid_features = torch.cat( ( prev_prev_state, prev_state, forcing, self.expand_to_batch(self.grid_static_features, batch_size), ), dim=-1, ) # (B, num_grid_nodes, grid_dim) grid_emb = self.grid_prev_embedder(grid_features) # (B, num_grid_nodes, d_h) # Graph embedding. NOTE: this block depends only on static graph # features, so it is constant across an autoregressive rollout. It is # kept as a self-contained block so a future embedd_graph()/ # embedd_grid() split (hoisting it out of the AR loop) is mechanical. graph_emb = { "g2m": self.expand_to_batch( self.g2m_embedder(self.g2m_features), batch_size ), # (B, M_g2m, d_h) "m2g": self.expand_to_batch( self.m2g_embedder(self.m2g_features), batch_size ), # (B, M_m2g, d_h) } graph_emb.update(self.embedd_mesh(batch_size)) return grid_emb, graph_emb
[docs] def forward( self, prev_state: torch.Tensor, prev_prev_state: torch.Tensor, forcing: torch.Tensor, ) -> tuple[torch.Tensor, Optional[torch.Tensor]]: """ Sample one time step prediction: embed features, sample the latent from the prior, decode, and return the predicted next state. The prediction is stochastic only through the latent sample; no observation noise is added. Parameters ---------- prev_state : torch.Tensor Shape ``(B, num_grid_nodes, d_state)``. ``X_t``. prev_prev_state : torch.Tensor Shape ``(B, num_grid_nodes, d_state)``. ``X_{t-1}``. forcing : torch.Tensor Shape ``(B, num_grid_nodes, d_forcing)``. Returns ------- new_state : torch.Tensor Shape ``(B, num_grid_nodes, d_state)``. Predicted ``X_{t+1}`` (the decoder mean, given the sampled latent). pred_std : torch.Tensor or None Shape ``(B, num_grid_nodes, d_state)`` when ``output_std`` is True, otherwise None. """ # embed all features grid_prev_emb, graph_emb = self.embedd_grid_and_graph( prev_state, prev_prev_state, forcing ) # Compute prior prior_dist = self.prior_model( grid_prev_emb, graph_emb=graph_emb ) # (B, num_mesh_nodes, d_latent) # Sample from prior latent_samples = prior_dist.rsample() # (B, num_mesh_nodes, d_latent) # Decode the latent into a state increment, rescale it with the # one-step difference statistics, then add it onto prev_state (X_t) # and clamp to the valid range (a no-op when no clamping limits are # configured), as for the deterministic models. assert isinstance(self.decoder, torch.nn.Module) mean_delta, pred_std = self.decoder( grid_prev_emb, latent_samples, graph_emb ) rescaled_mean_delta = mean_delta * self.diff_std + self.diff_mean pred_mean = self.get_clamped_new_state(rescaled_mean_delta, prev_state) # (B, num_grid_nodes, d_state) return pred_mean, pred_std
[docs] class GraphEFM(BaseGraphEFM): """ Graph-based Ensemble Forecasting Model on a hierarchical mesh graph. The latent variable lives on the top level of the mesh hierarchy. The prior and variational encoder are ``HiGraphLatentEncoder``s and the decoder is a ``HiGraphLatentDecoder``. """ mesh_static_features: list[torch.Tensor] mesh_up_features: list[torch.Tensor] mesh_down_features: list[torch.Tensor] m2m_features: list[torch.Tensor] m2m_edge_index: utils.BufferList mesh_up_edge_index: utils.BufferList mesh_down_edge_index: utils.BufferList def __init__( self, datastore: BaseDatastore, graph_name: str = "hierarchical", hidden_dim: int = 64, hidden_layers: int = 1, latent_dim: Optional[int] = None, prior_intra_level_layers: int = 2, encoder_intra_level_layers: int = 2, decoder_intra_level_layers: int = 4, learn_prior: bool = True, prior_dist: str = "isotropic", num_past_forcing_steps: int = 1, num_future_forcing_steps: int = 1, g2m_gnn_type: str = "InteractionNet", m2g_gnn_type: str = "InteractionNet", output_std: bool = False, output_clamping_lower: Optional[Dict[str, float]] = None, output_clamping_upper: Optional[Dict[str, float]] = None, ) -> None: """ Build the mesh embedders and the hierarchical encoder/decoder latent modules. The prior is built by the base class. Parameters ---------- datastore : BaseDatastore Datastore providing static features, standardization statistics and variable counts. graph_name : str Name of the graph directory (under ``<root>/graph``) to load. Must be a hierarchical graph. hidden_dim : int Dimensionality of internal node and edge representations. hidden_layers : int Number of hidden layers in internal MLPs. latent_dim : int, optional Dimensionality of the latent variable at each top-level mesh node; defaults to ``hidden_dim`` when None. Forwarded to the base class, which resolves the default and stores it as ``self.latent_dim``. prior_intra_level_layers : int Number of intra-level GNN layers in the (learned) prior. Forwarded to the base class as ``prior_layers``. encoder_intra_level_layers : int Number of intra-level GNN layers in the variational encoder. decoder_intra_level_layers : int Number of intra-level GNN layers in the latent decoder. learn_prior : bool If True, the prior is a hierarchical graph encoder conditioned on the previous state; if False, a constant ``Normal(0, 1)`` prior is used. Forwarded to the base class. prior_dist : str Output distribution of the prior: ``"isotropic"`` or ``"diagonal"``. Forwarded to the base class. num_past_forcing_steps : int Number of past forcing steps included in the input window. num_future_forcing_steps : int Number of future forcing steps included in the input window. g2m_gnn_type : str GNN type for the grid-to-mesh steps of the prior, encoder and decoder (key in ``gnn_layers.GNN_TYPES``). m2g_gnn_type : str GNN type for the mesh-to-grid step of the decoder (key in ``gnn_layers.GNN_TYPES``). output_std : bool If True, the decoder outputs a per-variable std alongside the mean; if False, ``forward`` returns ``None`` for the std. output_clamping_lower : dict of str to float, optional Lower clamping limits per output variable. output_clamping_upper : dict of str to float, optional Upper clamping limits per output variable. """ super().__init__( datastore=datastore, graph_name=graph_name, hidden_dim=hidden_dim, hidden_layers=hidden_layers, latent_dim=latent_dim, learn_prior=learn_prior, prior_dist=prior_dist, prior_layers=prior_intra_level_layers, g2m_gnn_type=g2m_gnn_type, num_past_forcing_steps=num_past_forcing_steps, num_future_forcing_steps=num_future_forcing_steps, output_std=output_std, output_clamping_lower=output_clamping_lower, output_clamping_upper=output_clamping_upper, ) level_mesh_sizes = [ mesh_feat.shape[0] for mesh_feat in self.mesh_static_features ] num_levels = len(self.mesh_static_features) utils.log_on_rank_zero("Loaded hierarchical graph with structure:") for level_index, level_mesh_size in enumerate(level_mesh_sizes): same_level_edges = self.m2m_features[level_index].shape[0] utils.log_on_rank_zero( f"level {level_index} - {level_mesh_size} nodes, " f"{same_level_edges} same-level edges" ) if level_index < (num_levels - 1): up_edges = self.mesh_up_features[level_index].shape[0] down_edges = self.mesh_down_features[level_index].shape[0] utils.log_on_rank_zero(f" {level_index}<->{level_index + 1}") utils.log_on_rank_zero( f" - {up_edges} up edges, {down_edges} down edges" ) # Embedders. Assume all levels share static feature dimensionality. mesh_dim = self.mesh_static_features[0].shape[1] m2m_dim = self.m2m_features[0].shape[1] mesh_up_dim = self.mesh_up_features[0].shape[1] mesh_down_dim = self.mesh_down_features[0].shape[1] # Separate mesh node embedders for each level self.mesh_embedders = nn.ModuleList( [ utils.make_mlp([mesh_dim] + self.mlp_blueprint_end) for _ in range(num_levels) ] ) self.mesh_up_embedders = nn.ModuleList( [ utils.make_mlp([mesh_up_dim] + self.mlp_blueprint_end) for _ in range(num_levels - 1) ] ) self.mesh_down_embedders = nn.ModuleList( [ utils.make_mlp([mesh_down_dim] + self.mlp_blueprint_end) for _ in range(num_levels - 1) ] ) # If not using any intra-level layers, no need to embed m2m self.embedd_m2m = ( max( prior_intra_level_layers, encoder_intra_level_layers, decoder_intra_level_layers, ) > 0 ) if self.embedd_m2m: self.m2m_embedders = nn.ModuleList( [ utils.make_mlp([m2m_dim] + self.mlp_blueprint_end) for _ in range(num_levels) ] ) # Encoder (variational posterior) + Decoder self.encoder = HiGraphLatentEncoder( latent_dim=self.latent_dim, g2m_edge_index=self.g2m_edge_index, m2m_edge_index=self.m2m_edge_index, mesh_up_edge_index=self.mesh_up_edge_index, hidden_dim=hidden_dim, intra_level_layers=encoder_intra_level_layers, hidden_layers=hidden_layers, g2m_gnn_type=g2m_gnn_type, output_dist="diagonal", ) self.decoder = HiGraphLatentDecoder( g2m_edge_index=self.g2m_edge_index, m2m_edge_index=self.m2m_edge_index, m2g_edge_index=self.m2g_edge_index, mesh_up_edge_index=self.mesh_up_edge_index, mesh_down_edge_index=self.mesh_down_edge_index, hidden_dim=hidden_dim, latent_dim=self.latent_dim, num_state_vars=self.num_state_vars, intra_level_layers=decoder_intra_level_layers, hidden_layers=hidden_layers, g2m_gnn_type=g2m_gnn_type, m2g_gnn_type=m2g_gnn_type, output_std=bool(output_std), )
[docs] def check_graph_type(self, graph_name: str) -> None: """ Verify the loaded graph is hierarchical. Parameters ---------- graph_name : str Name of the graph directory that was loaded, for the error message. Raises ------ ValueError If the loaded graph is flat. """ if not self.hierarchical: raise ValueError( f"{type(self).__name__} requires a hierarchical mesh " f"graph, but graph '{graph_name}' is flat" )
@property def latent_spatial_dim(self) -> int: """ Number of mesh nodes on the top mesh level, where the latent variable lives. Returns ------- int Number of top-level mesh nodes. """ return self.mesh_static_features[-1].shape[0]
[docs] def build_learnable_prior( self, latent_dim: int, hidden_dim: int, hidden_layers: int, g2m_gnn_type: str, prior_dist: str, prior_layers: int, ) -> HiGraphLatentEncoder: """ Build the hierarchical learnable prior encoder. Parameters ---------- latent_dim : int Dimensionality of the latent variable at each top-level mesh node. hidden_dim : int Dimensionality of internal node and edge representations. hidden_layers : int Number of hidden layers in internal MLPs. g2m_gnn_type : str GNN type for the grid-to-mesh step of the prior. prior_dist : str Output distribution of the prior: ``"isotropic"`` or ``"diagonal"``. prior_layers : int Number of intra-level GNN layers in the prior. Returns ------- HiGraphLatentEncoder The learnable prior latent encoder. """ return HiGraphLatentEncoder( latent_dim=latent_dim, g2m_edge_index=self.g2m_edge_index, m2m_edge_index=self.m2m_edge_index, mesh_up_edge_index=self.mesh_up_edge_index, hidden_dim=hidden_dim, intra_level_layers=prior_layers, hidden_layers=hidden_layers, g2m_gnn_type=g2m_gnn_type, output_dist=prior_dist, )
[docs] def embedd_mesh(self, batch_size: int) -> dict[str, Any]: """ Embed static mesh node and intra-mesh edge features per level. Parameters ---------- batch_size : int Batch size to expand the embeddings to. Returns ------- dict Entries ``mesh``, ``m2m``, ``mesh_up`` and ``mesh_down``, each a list with one ``(B, *, d_h)`` tensor per mesh level (or inter-level connection). """ mesh_emb = { "mesh": [ self.expand_to_batch(emb(node_static_features), batch_size) for emb, node_static_features in zip( self.mesh_embedders, self.mesh_static_features, ) ], "mesh_up": [ self.expand_to_batch(emb(edge_feat), batch_size) for emb, edge_feat in zip( self.mesh_up_embedders, self.mesh_up_features ) ], "mesh_down": [ self.expand_to_batch(emb(edge_feat), batch_size) for emb, edge_feat in zip( self.mesh_down_embedders, self.mesh_down_features ) ], } if self.embedd_m2m: mesh_emb["m2m"] = [ self.expand_to_batch(emb(edge_feat), batch_size) for emb, edge_feat in zip(self.m2m_embedders, self.m2m_features) ] else: # No intra-level GNNs consume these, so no embedding is produced mesh_emb["m2m"] = [] return mesh_emb
[docs] class GraphEFMMultiScale(BaseGraphEFM): """ Graph-based Ensemble Forecasting Model on a flat mesh graph (e.g. a multi-scale graph). The latent variable lives on the (single) level of the mesh graph. The prior and variational encoder are ``GraphLatentEncoder``s and the decoder is a ``GraphLatentDecoder``. """ mesh_static_features: torch.Tensor m2m_features: torch.Tensor m2m_edge_index: torch.Tensor def __init__( self, datastore: BaseDatastore, graph_name: str = "multiscale", hidden_dim: int = 64, hidden_layers: int = 1, latent_dim: Optional[int] = None, prior_m2m_layers: int = 2, encoder_m2m_layers: int = 2, decoder_m2m_layers: int = 4, learn_prior: bool = True, prior_dist: str = "isotropic", num_past_forcing_steps: int = 1, num_future_forcing_steps: int = 1, g2m_gnn_type: str = "InteractionNet", m2g_gnn_type: str = "InteractionNet", output_std: bool = False, output_clamping_lower: Optional[Dict[str, float]] = None, output_clamping_upper: Optional[Dict[str, float]] = None, ) -> None: """ Build the mesh embedders and the flat-graph encoder/decoder latent modules. The prior is built by the base class. Parameters ---------- datastore : BaseDatastore Datastore providing static features, standardization statistics and variable counts. graph_name : str Name of the graph directory (under ``<root>/graph``) to load. Must be a flat graph. hidden_dim : int Dimensionality of internal node and edge representations. hidden_layers : int Number of hidden layers in internal MLPs. latent_dim : int, optional Dimensionality of the latent variable at each mesh node; defaults to ``hidden_dim`` when None. Forwarded to the base class, which resolves the default and stores it as ``self.latent_dim``. prior_m2m_layers : int Number of on-mesh (m2m) GNN layers in the (learned) prior. Forwarded to the base class as ``prior_layers``. encoder_m2m_layers : int Number of on-mesh (m2m) GNN layers in the variational encoder. decoder_m2m_layers : int Number of on-mesh (m2m) GNN layers in the latent decoder. learn_prior : bool If True, the prior is a graph encoder conditioned on the previous state; if False, a constant ``Normal(0, 1)`` prior is used. Forwarded to the base class. prior_dist : str Output distribution of the prior: ``"isotropic"`` or ``"diagonal"``. Forwarded to the base class. num_past_forcing_steps : int Number of past forcing steps included in the input window. num_future_forcing_steps : int Number of future forcing steps included in the input window. g2m_gnn_type : str GNN type for the grid-to-mesh steps of the prior, encoder and decoder (key in ``gnn_layers.GNN_TYPES``). m2g_gnn_type : str GNN type for the mesh-to-grid step of the decoder (key in ``gnn_layers.GNN_TYPES``). output_std : bool If True, the decoder outputs a per-variable std alongside the mean; if False, ``forward`` returns ``None`` for the std. output_clamping_lower : dict of str to float, optional Lower clamping limits per output variable. output_clamping_upper : dict of str to float, optional Upper clamping limits per output variable. """ super().__init__( datastore=datastore, graph_name=graph_name, hidden_dim=hidden_dim, hidden_layers=hidden_layers, latent_dim=latent_dim, learn_prior=learn_prior, prior_dist=prior_dist, prior_layers=prior_m2m_layers, g2m_gnn_type=g2m_gnn_type, num_past_forcing_steps=num_past_forcing_steps, num_future_forcing_steps=num_future_forcing_steps, output_std=output_std, output_clamping_lower=output_clamping_lower, output_clamping_upper=output_clamping_upper, ) utils.log_on_rank_zero( f"Loaded graph with " f"{self.num_grid_nodes + self.latent_spatial_dim} nodes " f"({self.num_grid_nodes} grid, {self.latent_spatial_dim} mesh)" ) # Embedders mesh_static_dim = self.mesh_static_features.shape[1] self.mesh_embedder = utils.make_mlp( [mesh_static_dim] + self.mlp_blueprint_end ) m2m_dim = self.m2m_features.shape[1] self.m2m_embedder = utils.make_mlp([m2m_dim] + self.mlp_blueprint_end) # Encoder (variational posterior) + Decoder self.encoder = GraphLatentEncoder( latent_dim=self.latent_dim, g2m_edge_index=self.g2m_edge_index, m2m_edge_index=self.m2m_edge_index, hidden_dim=hidden_dim, m2m_layers=encoder_m2m_layers, hidden_layers=hidden_layers, g2m_gnn_type=g2m_gnn_type, output_dist="diagonal", ) self.decoder = GraphLatentDecoder( g2m_edge_index=self.g2m_edge_index, m2m_edge_index=self.m2m_edge_index, m2g_edge_index=self.m2g_edge_index, hidden_dim=hidden_dim, latent_dim=self.latent_dim, num_state_vars=self.num_state_vars, m2m_layers=decoder_m2m_layers, hidden_layers=hidden_layers, g2m_gnn_type=g2m_gnn_type, m2g_gnn_type=m2g_gnn_type, output_std=bool(output_std), )
[docs] def check_graph_type(self, graph_name: str) -> None: """ Verify the loaded graph is flat. Parameters ---------- graph_name : str Name of the graph directory that was loaded, for the error message. Raises ------ ValueError If the loaded graph is hierarchical. """ if self.hierarchical: raise ValueError( f"{type(self).__name__} requires a flat mesh graph, " f"but graph '{graph_name}' is hierarchical" )
@property def latent_spatial_dim(self) -> int: """ Number of mesh nodes, where the latent variable lives. Returns ------- int Number of mesh nodes. """ return len(self.mesh_static_features)
[docs] def build_learnable_prior( self, latent_dim: int, hidden_dim: int, hidden_layers: int, g2m_gnn_type: str, prior_dist: str, prior_layers: int, ) -> GraphLatentEncoder: """ Build the flat-graph learnable prior encoder. Parameters ---------- latent_dim : int Dimensionality of the latent variable at each mesh node. hidden_dim : int Dimensionality of internal node and edge representations. hidden_layers : int Number of hidden layers in internal MLPs. g2m_gnn_type : str GNN type for the grid-to-mesh step of the prior. prior_dist : str Output distribution of the prior: ``"isotropic"`` or ``"diagonal"``. prior_layers : int Number of on-mesh (m2m) GNN layers in the prior. Returns ------- GraphLatentEncoder The learnable prior latent encoder. """ return GraphLatentEncoder( latent_dim=latent_dim, g2m_edge_index=self.g2m_edge_index, m2m_edge_index=self.m2m_edge_index, hidden_dim=hidden_dim, m2m_layers=prior_layers, hidden_layers=hidden_layers, g2m_gnn_type=g2m_gnn_type, output_dist=prior_dist, )
[docs] def embedd_mesh(self, batch_size: int) -> dict[str, Any]: """ Embed static mesh node and intra-mesh edge features. Parameters ---------- batch_size : int Batch size to expand the embeddings to. Returns ------- dict Entries ``mesh``: ``(B, num_mesh_nodes, d_h)`` and ``m2m``: ``(B, M_m2m, d_h)``. """ return { "mesh": self.expand_to_batch( self.mesh_embedder(self.mesh_static_features), batch_size ), # (B, num_mesh_nodes, d_h) "m2m": self.expand_to_batch( self.m2m_embedder(self.m2m_features), batch_size ), # (B, M_m2m, d_h) }