from typing import Optional, Union
from torch import Tensor
from tgp.utils.ops import apply_dense_node_mask
from .aggr_reduce import AggrReduce
from .get_aggr import resolve_reduce_op
def _validate_dense_mask(mask: Optional[Tensor], x: Tensor) -> None:
"""Validate dense readout mask shape against ``x``."""
if mask is None:
return
if mask.dim() != 2 or tuple(mask.shape) != tuple(x.shape[:2]):
raise ValueError(
"mask must have shape [B, N] matching x.shape[:2] for dense readout."
)
[docs]
class GlobalReduce(AggrReduce):
r"""Graph-level readout as a module wrapping :class:`AggrReduce`.
This module aggregates node features to one vector per graph, supporting both
sparse inputs ``[N, F]`` with an optional ``batch`` vector and dense inputs
``[B, N, F]`` with an optional boolean ``mask`` of shape ``[B, N]``.
Args:
reduce_op: Aggregation to use: a string alias (e.g. ``\"sum\"``, ``\"mean\"``,
``\"max\"``, ``\"min\"``, ``\"lstm\"``, ``\"set2set\"``) or a PyG
:class:`torch_geometric.nn.aggr.Aggregation` instance. Strings are
resolved via :func:`~tgp.reduce.get_aggr`.
**aggr_kwargs: Passed to :func:`~tgp.reduce.get_aggr` when ``reduce_op``
is a string (e.g. ``in_channels``, ``out_channels``, ``processing_steps``).
"""
def __init__(self, reduce_op: Union[str, object] = "sum", **aggr_kwargs):
aggr = resolve_reduce_op(reduce_op, **aggr_kwargs)
super().__init__(aggr)
[docs]
def forward(
self,
x: Tensor,
batch: Optional[Tensor] = None,
size: Optional[int] = None,
mask: Optional[Tensor] = None,
) -> Tensor:
r"""Aggregate node features to one vector per graph.
Infers sparse vs dense from ``x.ndim``: 2D ``[N, F]`` is sparse (use
``batch`` for grouping); 3D ``[B, N, F]`` is dense (reduce over the
node dimension). Nodes must be on the second-to-last dimension.
Args:
x: Node features. Shape ``[N, F]`` (sparse) or ``[B, N, F]`` (dense).
batch: Batch vector for sparse ``x``, shape ``[N]``. Ignored for dense.
size: Number of graphs for sparse readout when ``batch`` is provided.
Passing ``size`` with sparse ``x`` and ``batch=None`` raises
:class:`ValueError`.
mask: Input-node validity mask for batched (dense) ``x`` only, shape
``[B, N]``. Passing ``mask`` with sparse ``x`` or with a
mismatched shape raises :class:`ValueError`.
Returns:
Tensor of shape ``[B, F]`` (or ``[1, F]`` for single graph sparse).
"""
if x.dim() not in (2, 3):
raise ValueError(
f"readout expects x to be 2D [N, F] or 3D [B, N, F], got ndim={x.dim()}"
)
# Path 1: dense masked readout [B, N, F] + [B, N].
if x.dim() == 3 and mask is not None:
_validate_dense_mask(mask, x)
x_valid, batch_valid = apply_dense_node_mask(x, mask)
B = mask.size(0)
x_pool, _ = super().forward(x_valid, so=None, batch=batch_valid, size=B)
return x_pool
# Path 2: dense unmasked readout [B, N, F].
if x.dim() == 3:
x_pool, _ = super().forward(x, so=None, batch=None, size=x.size(0))
return x_pool
# Path 3: sparse-style readout [N, F] (+ optional batch vector).
if mask is not None:
raise ValueError("mask is only supported for dense x with shape [B, N, F].")
if batch is None and size is not None:
raise ValueError(
"size is only supported for sparse readout when batch is provided."
)
x_pool, _ = super().forward(x, so=None, batch=batch, size=size)
return x_pool