Source code for neural_lam.models.latent.hi_graph_encoder

"""Latent encoder for hierarchical graphs."""

# Standard library
from typing import Any

# Third-party
import torch
from torch import nn

# First-party
from neural_lam import utils
from neural_lam.gnn_layers import PropagationNet, get_gnn_class

# Local
from .base_encoder import BaseLatentEncoder


[docs] class HiGraphLatentEncoder(BaseLatentEncoder): """ Latent encoder for a hierarchical mesh: grid -> bottom mesh level via a g2m GNN (type set by ``g2m_gnn_type``), then propagates upward through mesh levels using mesh-up PropagationNets, with optional intra-level processing at each level. The latent distribution is read out from the top mesh level. """ def __init__( self, latent_dim: int, g2m_edge_index: torch.Tensor, m2m_edge_index: utils.BufferList, mesh_up_edge_index: utils.BufferList, hidden_dim: int, intra_level_layers: int, hidden_layers: int = 1, g2m_gnn_type: str = "InteractionNet", output_dist: str = "isotropic", ) -> None: """ Set up the g2m, mesh-up and intra-level GNNs and latent param map. Parameters ---------- latent_dim : int Dimensionality of the latent variable at each mesh node. g2m_edge_index : torch.Tensor Shape ``(2, M_g2m)``. Edge index of grid-to-mesh edges. m2m_edge_index : BufferList Per-level edge indices of intra-level mesh edges, each of shape ``(2, M_m2m[l])``. mesh_up_edge_index : BufferList Per-level edge indices of upward inter-level mesh edges, each of shape ``(2, M_up[l])``. hidden_dim : int Dimensionality of internal node and edge representations. intra_level_layers : int Number of intra-level GNN layers at each mesh level; 0 disables intra-level processing. hidden_layers : int Number of hidden layers in internal MLPs. g2m_gnn_type : str GNN type for the grid-to-mesh step (key in ``gnn_layers.GNN_TYPES``). Mesh-up edges are not configurable; they always use ``PropagationNet``. output_dist : str Type of output distribution: ``"isotropic"`` or ``"diagonal"``. """ super().__init__(latent_dim, output_dist) # Hierarchical encoder needs at least 2 mesh levels; with a single # level there is no upward propagation and the latent readout would # collapse to a flat encoder. Use GraphLatentEncoder instead. if len(m2m_edge_index) < 2: raise ValueError( "HiGraphLatentEncoder requires at least 2 mesh levels " f"(got {len(m2m_edge_index)}). Use GraphLatentEncoder for " "flat graphs." ) self.g2m_gnn = get_gnn_class(g2m_gnn_type)( g2m_edge_index, hidden_dim, hidden_layers=hidden_layers, update_edges=False, ) # Mesh-up edges must use PropagationNet: each upward step has to push # information into nodes of the next level even when those start from # their static embedding, so that grid information reaches the latent # readout at the top level. self.mesh_up_gnns = nn.ModuleList( [ PropagationNet( edge_index, hidden_dim, hidden_layers=hidden_layers, update_edges=False, ) for edge_index in mesh_up_edge_index ] ) # None if intra_level_layers == 0, in which case no intra-level # processing is done in compute_dist_params self.intra_level_gnns = ( nn.ModuleList( [ utils.make_gnn_seq( edge_index, intra_level_layers, hidden_layers, hidden_dim, ) for edge_index in m2m_edge_index ] ) if intra_level_layers > 0 else None ) self.latent_param_map = utils.make_mlp( [hidden_dim] * (hidden_layers + 1) + [self.output_dim], layer_norm=False, ) # pylint: disable-next=arguments-differ
[docs] def compute_dist_params( # type: ignore[override] self, grid_rep: torch.Tensor, graph_emb: dict[str, Any], **kwargs: Any, ) -> torch.Tensor: """ Compute distribution parameters on the top mesh level. Parameters ---------- grid_rep : torch.Tensor Shape ``(B, num_grid_nodes, d_h)``. Grid input representation. graph_emb : dict Embedded graph node and edge features, with at least entries ``mesh``: list of ``(B, num_mesh_nodes[l], d_h)``, ``g2m``: ``(B, M_g2m, d_h)``, ``m2m``: list of ``(B, M_m2m[l], d_h)`` and ``mesh_up``: list of ``(B, M_up[l], d_h)``. **kwargs Ignored. Returns ------- torch.Tensor Shape ``(B, num_mesh_nodes[L], output_dim)``. Raw parameters of the latent distribution on the top mesh level ``L``. """ current_mesh_rep = self.g2m_gnn( grid_rep, graph_emb["mesh"][0], graph_emb["g2m"] ) # Same-level processing on level 0 if self.intra_level_gnns is not None: current_mesh_rep, _ = self.intra_level_gnns[0]( current_mesh_rep, graph_emb["m2m"][0] ) # Walk up levels 1..L for level, (up_gnn, mesh_up_level_rep, mesh_level_rep) in enumerate( zip( self.mesh_up_gnns, graph_emb["mesh_up"], graph_emb["mesh"][1:], ), start=1, ): current_mesh_rep = up_gnn( current_mesh_rep, mesh_level_rep, mesh_up_level_rep ) if self.intra_level_gnns is not None: current_mesh_rep, _ = self.intra_level_gnns[level]( current_mesh_rep, graph_emb["m2m"][level] ) return self.latent_param_map(current_mesh_rep)