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

"""Base implementations for hierarchical (multi-level) graph models."""

# Standard library

# Third-party
import torch
from torch import nn

# Local
from .... import utils
from ....datastore import BaseDatastore
from ....gnn_layers import get_gnn_class
from .base import BaseGraphModel


[docs] class BaseHiGraphModel(BaseGraphModel): """ Base class for hierarchical graph models. """ # Buffers or graph features that are lists of tensors in hierarchical models mesh_static_features: list[torch.Tensor] m2m_features: list[torch.Tensor] mesh_up_features: list[torch.Tensor] mesh_down_features: list[torch.Tensor] m2m_edge_index: list[torch.Tensor] mesh_up_edge_index: list[torch.Tensor] mesh_down_edge_index: list[torch.Tensor] def __init__( self, datastore: BaseDatastore, graph_name: str = "multiscale", hidden_dim: int = 64, hidden_layers: int = 1, processor_layers: int = 4, mesh_aggr: str = "sum", num_past_forcing_steps: int = 1, num_future_forcing_steps: int = 1, output_std: bool = False, output_clamping_lower: dict[str, float] | None = None, output_clamping_upper: dict[str, float] | None = None, g2m_gnn_type: str = "InteractionNet", m2g_gnn_type: str = "InteractionNet", mesh_up_gnn_type: str = "InteractionNet", mesh_down_gnn_type: str = "InteractionNet", ) -> None: """Extend :class:`BaseGraphModel` with hierarchical mesh structures.""" super().__init__( datastore=datastore, graph_name=graph_name, hidden_dim=hidden_dim, hidden_layers=hidden_layers, processor_layers=processor_layers, mesh_aggr=mesh_aggr, 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, g2m_gnn_type=g2m_gnn_type, m2g_gnn_type=m2g_gnn_type, ) self.mesh_up_gnn_type = mesh_up_gnn_type self.mesh_down_gnn_type = mesh_down_gnn_type # Track number of nodes, edges on each level # Flatten lists for efficient embedding self.num_levels = len(self.mesh_static_features) # Number of mesh nodes at each level self.level_mesh_sizes = [ mesh_feat.shape[0] for mesh_feat in self.mesh_static_features ] # Needs as python list for later # Print some useful info utils.log_on_rank_zero("Loaded hierarchical graph with structure:") for level_index, level_mesh_size in enumerate(self.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 < (self.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 have same static feature dimensionality mesh_dim = self.mesh_static_features[0].shape[1] mesh_same_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(self.num_levels) ] ) self.mesh_same_embedders = nn.ModuleList( [ utils.make_mlp([mesh_same_dim] + self.mlp_blueprint_end) for _ in range(self.num_levels) ] ) self.mesh_up_embedders = nn.ModuleList( [ utils.make_mlp([mesh_up_dim] + self.mlp_blueprint_end) for _ in range(self.num_levels - 1) ] ) self.mesh_down_embedders = nn.ModuleList( [ utils.make_mlp([mesh_down_dim] + self.mlp_blueprint_end) for _ in range(self.num_levels - 1) ] ) # Instantiate GNNs # Init GNNs (message passing up the hierarchy) mesh_up_class = get_gnn_class(mesh_up_gnn_type) self.mesh_init_gnns = nn.ModuleList( [ mesh_up_class( edge_index, hidden_dim, hidden_layers=hidden_layers, ) for edge_index in self.mesh_up_edge_index ] ) # Read out GNNs (message passing down the hierarchy) mesh_down_class = get_gnn_class(mesh_down_gnn_type) self.mesh_read_gnns = nn.ModuleList( [ mesh_down_class( edge_index, hidden_dim, hidden_layers=hidden_layers, update_edges=False, ) for edge_index in self.mesh_down_edge_index ] )
[docs] def get_num_mesh(self) -> tuple[int, int]: """ Compute mesh node counts used for encoding and decoding. Returns ------- tuple[int, int] Total number of mesh nodes and the number to ignore during encoding/decoding. """ num_mesh_nodes = sum( node_feat.shape[0] for node_feat in self.mesh_static_features ) num_mesh_nodes_ignore = ( num_mesh_nodes - self.mesh_static_features[0].shape[0] ) return num_mesh_nodes, num_mesh_nodes_ignore
[docs] def embedd_mesh_nodes(self) -> torch.Tensor: """ Embed static mesh node features for the bottom level only; remaining levels are embedded at the start of ``process_step``. Returns ------- torch.Tensor Shape ``(num_mesh_nodes[0], hidden_dim)``. Embedded bottom-level mesh node representations. Dims: ``num_mesh_nodes[0]`` is the number of nodes at level 0 and ``hidden_dim`` is the hidden dimension. """ return self.mesh_embedders[0](self.mesh_static_features[0])
[docs] def process_step(self, mesh_rep: torch.Tensor) -> torch.Tensor: """ Process the mesh representation across all hierarchy levels, implementing the full init-process-readout cycle. Parameters ---------- mesh_rep : torch.Tensor Shape ``(B, num_mesh_nodes[0], hidden_dim)``. Bottom-level mesh node representations from the encoder. Dims: ``B`` is batch size, ``num_mesh_nodes[0]`` is the number of nodes at level 0, and ``hidden_dim`` is the hidden dimension. Returns ------- torch.Tensor Shape ``(B, num_mesh_nodes[0], hidden_dim)``. Updated bottom-level mesh node representations. Dims: same as ``mesh_rep``. """ batch_size = mesh_rep.shape[0] # EMBED REMAINING MESH NODES (levels >= 1) - # Create list of mesh node representations for each level, # each of size (B, num_mesh_nodes[l], hidden_dim) mesh_rep_levels = [mesh_rep] + [ self.expand_to_batch(emb(node_static_features), batch_size) for emb, node_static_features in zip( self.mesh_embedders[1:], self.mesh_static_features[1:], ) ] # - EMBED EDGES - # Embed edges, expand with batch dimension mesh_same_rep = [ self.expand_to_batch(emb(edge_feat), batch_size) for emb, edge_feat in zip( self.mesh_same_embedders, self.m2m_features ) ] mesh_up_rep = [ 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_rep = [ self.expand_to_batch(emb(edge_feat), batch_size) for emb, edge_feat in zip( self.mesh_down_embedders, self.mesh_down_features ) ] # - MESH INIT. - # Let level_l go from 1 to L for level_l, gnn in enumerate(self.mesh_init_gnns, start=1): # Extract representations send_node_rep = mesh_rep_levels[ level_l - 1 ] # (B, num_mesh_nodes[l-1], hidden_dim) rec_node_rep = mesh_rep_levels[ level_l ] # (B, num_mesh_nodes[l], hidden_dim) edge_rep = mesh_up_rep[level_l - 1] # Apply GNN new_node_rep, new_edge_rep = gnn( send_node_rep, rec_node_rep, edge_rep ) # Update node and edge vectors in lists mesh_rep_levels[level_l] = ( new_node_rep # (B, num_mesh_nodes[l], hidden_dim) ) mesh_up_rep[level_l - 1] = ( new_edge_rep # (B, num_edges[l-1], hidden_dim) ) # - PROCESSOR - mesh_rep_levels, _, _, mesh_down_rep = self.hi_processor_step( mesh_rep_levels, mesh_same_rep, mesh_up_rep, mesh_down_rep ) # - MESH READ OUT. - # Let level_l go from L-1 to 0 for level_l, gnn in zip( range(self.num_levels - 2, -1, -1), reversed(self.mesh_read_gnns) ): # Extract representations send_node_rep = mesh_rep_levels[ level_l + 1 ] # (B, num_mesh_nodes[l+1], hidden_dim) rec_node_rep = mesh_rep_levels[ level_l ] # (B, num_mesh_nodes[l], hidden_dim) edge_rep = mesh_down_rep[level_l] # Apply GNN new_node_rep = gnn(send_node_rep, rec_node_rep, edge_rep) # Update node and edge vectors in lists mesh_rep_levels[level_l] = ( new_node_rep # (B, num_mesh_nodes[l], hidden_dim) ) # Return only bottom level representation return mesh_rep_levels[0] # (B, num_mesh_nodes[0], hidden_dim)
[docs] def hi_processor_step( self, mesh_rep_levels: list[torch.Tensor], mesh_same_rep: list[torch.Tensor], mesh_up_rep: list[torch.Tensor], mesh_down_rep: list[torch.Tensor], ) -> tuple[ list[torch.Tensor], list[torch.Tensor], list[torch.Tensor], list[torch.Tensor], ]: """ Internal processor step between mesh init and read-out. Parameters ---------- mesh_rep_levels : list of torch.Tensor One tensor per level, each of shape ``(B, num_mesh_nodes[l], hidden_dim)``. Node representations at each hierarchy level. Dims: ``B`` is batch size, ``num_mesh_nodes[l]`` is the node count at level ``l``, and ``hidden_dim`` is the hidden dimension. mesh_same_rep : list of torch.Tensor One tensor per level, each of shape ``(B, num_edges[l], hidden_dim)``. Same-level edge representations. ``num_edges[l]`` is the edge count at level ``l``. mesh_up_rep : list of torch.Tensor One tensor per inter-level gap, each of shape ``(B, num_edges[l], hidden_dim)``. Upward edge representations from level ``l`` to ``l+1``. mesh_down_rep : list of torch.Tensor One tensor per inter-level gap, each of shape ``(B, num_edges[l], hidden_dim)``. Downward edge representations from level ``l+1`` to ``l``. Returns ------- tuple of (list, list, list, list) Updated ``(mesh_rep_levels, mesh_same_rep, mesh_up_rep, mesh_down_rep)`` in the same order as the inputs. """ raise NotImplementedError("hi_process_step not implemented")