Source code for tgp.reduce.aggr_reduce

from typing import Optional, Tuple

import torch
from torch import Tensor

from tgp.select import SelectOutput
from tgp.utils.ops import build_pooled_batch

from .base_reduce import Reduce
from .get_aggr import has_pyg_aggregation, is_pyg_aggregation


def _sort_by_cluster_index(src: Tensor, cluster_index: Tensor) -> Tuple[Tensor, Tensor]:
    r"""Sort ``(src, cluster_index)`` by index for aggrs that require sorted input.

    Note:
        In :class:`~tgp.select.SelectOutput`, the index of pooled nodes is
        exposed as ``cluster_index``. In this module, "cluster" and
        "supernode" refer to the same concept.
    """
    cluster_index_sorted, perm = torch.sort(cluster_index, stable=True)
    src_sorted = src[perm]
    return src_sorted, cluster_index_sorted


def _aggregate_sorted(aggr, src: Tensor, index: Tensor, dim_size: int) -> Tensor:
    """Aggregate after sorting by index for aggrs that require sorted input."""
    src_sorted, index_sorted = _sort_by_cluster_index(src, index)
    return aggr(src_sorted, index=index_sorted, dim_size=dim_size, dim=0)


[docs] class AggrReduce(Reduce): r"""Reduce operator that wraps a PyG :class:`torch_geometric.nn.aggr.Aggregation`. Aggregates node features within each supernode using the given aggregation module. Supports sparse assignment matrices and graph-level readout mode (``so=None``). Dense :class:`~tgp.select.SelectOutput` assignments are not supported: use :class:`~tgp.reduce.BaseReduce` for dense/soft reductions. In :class:`~tgp.select.SelectOutput`, pooled-node indices are named ``cluster_index`` for historical reasons; here this is equivalent to "supernode index". ``so=None`` is supported for graph-level readout: all nodes are assigned to one supernode per graph (using ``batch`` as supernode index), or to a single supernode when ``batch`` is :obj:`None`. Args: aggr: A PyG Aggregation instance (e.g. :class:`torch_geometric.nn.aggr.SumAggregation`, :class:`torch_geometric.nn.aggr.MeanAggregation`). """ def __init__(self, aggr): super().__init__() if not has_pyg_aggregation(): raise ImportError( "AggrReduce requires torch_geometric.nn.aggr. " "Install PyTorch Geometric to use PyG aggregations." ) if not is_pyg_aggregation(aggr): raise TypeError(f"aggr must be a PyG Aggregation, got {type(aggr)}") self.aggr = aggr
[docs] def forward( self, x: Tensor, so: Optional[SelectOutput] = None, *, batch: Optional[Tensor] = None, size: Optional[int] = None, **kwargs, ) -> Tuple[Tensor, Optional[Tensor]]: r"""Aggregate node features according to the supernode assignments. Args: x (~torch.Tensor): Node features of shape :math:`[N, F]` or :math:`[B, N, F]`. so (~tgp.select.SelectOutput, optional): Select output containing assignment information. If :obj:`None`, performs graph-level readout using ``batch``. batch (~torch.Tensor, optional): Batch vector assigning each node to a graph. size (int, optional): Expected number of pooled nodes (readout groups). If :obj:`None`, inferred from ``so`` or ``batch``. Returns: tuple: A pair ``(x_pool, batch_pool)`` with pooled features and pooled batch indices. """ # Path 1: readout mode (`so=None`) aggregates directly to one output per graph. if so is None: return self._readout_without_select_output(x, batch=batch, size=size) if batch is None and so.batch is not None: batch = so.batch # Path 2: sparse assignment matrix. if so.s.is_sparse: src = x[so.node_index] * so.weight.view(-1, 1) x_pool = _aggregate_sorted( self.aggr, src, so.cluster_index, dim_size=so.num_supernodes ) batch_pool = self.reduce_batch(so, batch) return x_pool, batch_pool raise ValueError( "AggrReduce supports only sparse SelectOutput assignments. " "Dense assignments are not supported; use BaseReduce for dense/soft reductions." )
def _readout_without_select_output( self, x: Tensor, *, batch: Optional[Tensor] = None, size: Optional[int] = None, ) -> Tuple[Tensor, Optional[Tensor]]: r"""Readout fast-path for ``so=None`` (one pooled supernode per graph).""" if x.dim() == 3: B, N, _ = x.shape num_supernodes = size if size is not None else B x_pool = _aggregate_sorted( self.aggr, x.reshape(-1, x.size(-1)), build_pooled_batch(B, N, x.device), dim_size=num_supernodes, ) batch_pool = torch.arange(num_supernodes, device=x.device) return x_pool, batch_pool if x.dim() != 2: raise ValueError( "Readout mode expects x to be 2D [N, F] or 3D [B, N, F], " f"got ndim={x.dim()}." ) if batch is None: cluster_index = torch.zeros(x.size(0), dtype=torch.long, device=x.device) x_pool = _aggregate_sorted(self.aggr, x, cluster_index, dim_size=1) return x_pool, None if batch.numel() > 0: inferred_num_supernodes = int(batch.max().item()) + 1 else: # Preserve explicit graph cardinality (dim_size) even when there # are no real nodes (e.g. dense readout with all-false mask). inferred_num_supernodes = 1 num_supernodes = size if size is not None else inferred_num_supernodes x_pool = _aggregate_sorted(self.aggr, x, batch, dim_size=num_supernodes) batch_pool = torch.arange(num_supernodes, device=batch.device) return x_pool, batch_pool def __repr__(self) -> str: return f"{self.__class__.__name__}(aggr={self.aggr})"