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

"""
Parallel hierarchical graph-based LAM model.
"""

# Third-party
import torch
import torch_geometric as pyg

# Local
from ....datastore import BaseDatastore
from ....gnn_layers import InteractionNet
from .hierarchical import BaseHiGraphModel


[docs] class HiLAMParallel(BaseHiGraphModel): """ Version of HiLAM where all message passing in the hierarchical mesh (up, down, inter-level) is ran in parallel. This is a somewhat simpler alternative to the sequential message passing of Hi-LAM. """ 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: """ Initialize the HiLAMParallel model. Parameters ---------- datastore : BaseDatastore The datastore providing grid metadata and data access. graph_name : str, default "multiscale" The name of the graph to load. hidden_dim : int, default 64 The dimension of the hidden representations. hidden_layers : int, default 1 The number of hidden layers in the MLPs. processor_layers : int, default 4 The number of processor layers in the GNN. mesh_aggr : str, default "sum" The aggregation method for mesh nodes. num_past_forcing_steps : int, default 1 The number of past forcing steps to include. num_future_forcing_steps : int, default 1 The number of future forcing steps to include. output_std : bool, default False Whether to output a predicted standard deviation. output_clamping_lower : dict, optional Lower clamping limits for state variables. output_clamping_upper : dict, optional Upper clamping limits for state variables. """ 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, mesh_up_gnn_type=mesh_up_gnn_type, mesh_down_gnn_type=mesh_down_gnn_type, ) # Processor GNNs # Compute start index of each level in the flattened mesh first_index_level = [0] for size in self.level_mesh_sizes[:-1]: first_index_level.append(first_index_level[-1] + size) # Offset node indices of edges to the global mesh index space offset_m2m = [ ei + offset for ei, offset in zip(self.m2m_edge_index, first_index_level) ] offset_up = [ torch.stack( ( ei[0] + first_index_level[level_idx], ei[1] + first_index_level[level_idx + 1], ), dim=0, ) for level_idx, ei in enumerate(self.mesh_up_edge_index) ] offset_down = [ torch.stack( ( ei[0] + first_index_level[level_idx + 1], ei[1] + first_index_level[level_idx], ), dim=0, ) for level_idx, ei in enumerate(self.mesh_down_edge_index) ] total_edge_index_list = offset_m2m + offset_up + offset_down total_edge_index = torch.cat(total_edge_index_list, dim=1) self.edge_split_sections = [ei.shape[1] for ei in total_edge_index_list] if processor_layers == 0: self.processor = lambda x, edge_attr: (x, edge_attr) else: processor_nets = [ InteractionNet( total_edge_index, hidden_dim, hidden_layers=hidden_layers, edge_chunk_sizes=self.edge_split_sections, aggr_chunk_sizes=self.level_mesh_sizes, ) for _ in range(processor_layers) ] self.processor = pyg.nn.Sequential( "mesh_rep, edge_rep", [ (net, "mesh_rep, mesh_rep, edge_rep -> mesh_rep, edge_rep") for net in processor_nets ], )
[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], ]: """ Run all processor steps in parallel across all edge types and hierarchy levels. 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. 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. 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. Returns ------- tuple of (list, list, list, list) Updated ``(mesh_rep_levels, mesh_same_rep, mesh_up_rep, mesh_down_rep)``. """ # First join all node and edge representations to single tensors mesh_rep = torch.cat( mesh_rep_levels, dim=1 ) # (B, num_mesh_nodes, hidden_dim) mesh_edge_rep = torch.cat( mesh_same_rep + mesh_up_rep + mesh_down_rep, dim=1 ) # (B, num_edges, hidden_dim) # Here, update mesh_*_rep and mesh_rep mesh_rep, mesh_edge_rep = self.processor(mesh_rep, mesh_edge_rep) # Split up again for read-out step mesh_rep_levels = list( torch.split(mesh_rep, self.level_mesh_sizes, dim=1) ) mesh_edge_rep_sections = torch.split( mesh_edge_rep, self.edge_split_sections, dim=1 ) mesh_same_rep = list(mesh_edge_rep_sections[: self.num_levels]) mesh_up_rep = list( mesh_edge_rep_sections[ self.num_levels : self.num_levels + (self.num_levels - 1) ] ) mesh_down_rep = list( mesh_edge_rep_sections[self.num_levels + (self.num_levels - 1) :] ) # Last are down edges # TODO: We return all, even though only down edges really are used # later return mesh_rep_levels, mesh_same_rep, mesh_up_rep, mesh_down_rep