Source code for gigl.distributed.utils.ablp

"""Utilities for remapping ABLP labels to sampled-subgraph indices.

Tensor dimensions used in this module:

- ``A``: anchors in one sampled batch.
- ``M``: padded label slots per anchor.
- ``S``: sampled nodes for one supervision node type.
- ``K``: non-padding candidate labels.
- ``E``: output label pairs; ``E == K`` because sampling always includes labels.

A label edge index has shape ``[2, E]``. Row 0 indexes anchors in ``[0, A)``;
row 1 indexes local nodes in the supervision node store in ``[0, S)``.

Example::

    # Padded global labels: [A=3, M=2]
    label_tensor = [[30, -1], [40, 10], [-1, -1]]

    # Sampled supervision nodes: local id -> global id, [S=3]
    local_id_to_global_id = [40, 10, 30]

    # Local label edge index: [2, E=3]
    label_edge_index = [[0, 1, 1], [2, 0, 1]]

    # Deprecated dictionary view of the same output
    labels_by_anchor = {0: [2], 1: [0, 1], 2: []}

The corresponding label relationships are shown below. Anchor 2 has no label
edge because both of its input slots are padding.

.. code-block:: dot

   digraph ablp_labels {
     rankdir=LR;
     node [fontname="Helvetica"];
     a0 [label="anchor 0", shape=box];
     a1 [label="anchor 1", shape=box];
     a2 [label="anchor 2", shape=box];
     n0 [label="local 0\\nglobal 40"];
     n1 [label="local 1\\nglobal 10"];
     n2 [label="local 2\\nglobal 30"];
     a0 -> n2 [label="global label 30", color="forestgreen", style=dashed];
     a1 -> n0 [label="global label 40", color="forestgreen", style=dashed];
     a1 -> n1 [label="global label 10", color="forestgreen", style=dashed];
   }
"""

import torch
from torch_geometric.typing import EdgeType

from gigl.src.common.types.graph_data import NodeType
from gigl.types.graph import label_edge_type_to_message_passing_edge_type
from gigl.utils.data_splitters import PADDING_NODE


[docs] def label_edge_index_to_dict( label_edge_index: torch.Tensor, num_anchors: int ) -> dict[int, torch.Tensor]: """Convert a label edge index to the deprecated per-anchor dictionary. The edge index must be grouped by anchor, as guaranteed by :func:`remap_labels_to_local_edge_indices`. Args: label_edge_index: ``[2, E]`` local label edge index. Row 0 contains anchor indices in ``[0, A)``; row 1 contains local label-node indices. Pairs must be grouped by row-0 anchor index. num_anchors: ``A``, including anchors with no labels. Returns: Every local anchor index in ``[0, A)`` mapped to a ``[E_a]`` tensor of local label-node indices, where ``sum(E_a) == E``. Example: >>> label_edge_index_to_dict( ... torch.tensor([[0, 1, 1], [2, 0, 1]]), num_anchors=3 ... ) {0: tensor([2]), 1: tensor([0, 1]), 2: tensor([], dtype=torch.int64)} """ # counts and labels_by_anchor have shape [A] and A x [E_a], respectively. counts = torch.bincount(label_edge_index[0], minlength=num_anchors) labels_by_anchor = torch.split(label_edge_index[1], counts.tolist()) return {anchor: labels_by_anchor[anchor] for anchor in range(num_anchors)}
def _remap_label_tensor_to_local_edge_index( label_tensor: torch.Tensor, sorted_global_node_ids: torch.Tensor, local_ids_by_sorted_global_id: torch.Tensor, ) -> torch.Tensor: """Remap one padded global-label tensor to a local label edge index. Args: label_tensor: ``[A, M]`` global label-node ids, padded with ``PADDING_NODE``. sorted_global_node_ids: ``[S]`` sorted global ids for sampled supervision nodes. local_ids_by_sorted_global_id: ``[S]`` local node indices aligned with ``sorted_global_node_ids``. Returns: ``[2, E]`` long tensor on ``label_tensor.device``. Row 0 contains local anchor indices; row 1 contains local label-node indices. Every non-padding label must be present in the sampled subgraph, so ``E == K <= A * M``. Raises: ValueError: If the sampled node map contains duplicate global ids. ValueError: If a non-padding label was not sampled. Example: >>> _remap_label_tensor_to_local_edge_index( ... label_tensor=torch.tensor([[30, -1], [40, 10], [-1, -1]]), ... sorted_global_node_ids=torch.tensor([10, 30, 40]), ... local_ids_by_sorted_global_id=torch.tensor([1, 2, 0]), ... ) tensor([[0, 1, 1], [2, 0, 1]]) """ num_anchors = int(label_tensor.size(0)) num_nodes = int(sorted_global_node_ids.size(0)) # Empty label edge indices always retain the [2, E] contract. empty_edge_index = torch.empty((2, 0), dtype=torch.long, device=label_tensor.device) if num_anchors == 0: return empty_edge_index num_labels = int(label_tensor.size(1)) candidate_global_label_ids = label_tensor.reshape(-1) # [A * M] candidate_anchor_indices = torch.arange( num_anchors, device=label_tensor.device ).repeat_interleave(num_labels) # [A * M] is_not_padding = candidate_global_label_ids != PADDING_NODE # [A * M] candidate_global_label_ids = candidate_global_label_ids[is_not_padding] # [K] candidate_anchor_indices = candidate_anchor_indices[is_not_padding] # [K] if candidate_global_label_ids.numel() == 0: return empty_edge_index if num_nodes == 0: raise ValueError( "Every non-padding ABLP label must be present in the sampled subgraph." ) if not bool((sorted_global_node_ids[1:] > sorted_global_node_ids[:-1]).all()): raise ValueError( "Vectorized label remapping requires unique global ids in the " "sampled node map." ) insertion_indices = torch.searchsorted( sorted_global_node_ids, candidate_global_label_ids ) # [K] if bool((insertion_indices == num_nodes).any()) or not bool( (sorted_global_node_ids[insertion_indices] == candidate_global_label_ids).all() ): raise ValueError( "Every non-padding ABLP label must be present in the sampled subgraph." ) local_label_indices = local_ids_by_sorted_global_id[insertion_indices] # [E == K] anchor_indices = candidate_anchor_indices # [E == K] return torch.stack((anchor_indices, local_label_indices)) # [2, E] def _remap_labels_by_edge_type( labels_by_edge_type: dict[EdgeType, torch.Tensor], local_id_to_global_id_by_node_type: dict[NodeType, torch.Tensor], sorted_node_lookup_by_type: dict[NodeType, tuple[torch.Tensor, torch.Tensor]], ) -> dict[EdgeType, torch.Tensor]: """Remap positive or negative labels for each supervision edge type. Args: labels_by_edge_type: Per label edge type, ``[A_et, M_et]`` padded global label-node ids. local_id_to_global_id_by_node_type: Per supervision node type, ``[S_t]`` local-to-global node ids. sorted_node_lookup_by_type: Cached ``[S_t]`` sorted global ids and aligned local node ids for each supervision node type. Returns: Per message-passing edge type, a ``[2, E_et]`` local label edge index. Edge types with ``A_et == 0`` are omitted. Example: A ``{label_edge_type: [[30, -1]]}`` input becomes ``{message_passing_edge_type: [[0], [2]]}`` when the destination node map is ``[40, 10, 30]``. """ output: dict[EdgeType, torch.Tensor] = {} for label_edge_type, label_tensor in labels_by_edge_type.items(): if label_tensor.size(0) == 0: continue supervision_node_type = label_edge_type[2] if supervision_node_type not in sorted_node_lookup_by_type: sorted_node_lookup_by_type[supervision_node_type] = torch.sort( local_id_to_global_id_by_node_type[supervision_node_type] ) ( sorted_global_node_ids, local_ids_by_sorted_global_id, ) = sorted_node_lookup_by_type[supervision_node_type] output[label_edge_type_to_message_passing_edge_type(label_edge_type)] = ( _remap_label_tensor_to_local_edge_index( label_tensor=label_tensor, sorted_global_node_ids=sorted_global_node_ids, local_ids_by_sorted_global_id=local_ids_by_sorted_global_id, ) ) return output
[docs] def remap_labels_to_local_edge_indices( local_id_to_global_id_by_node_type: dict[NodeType, torch.Tensor], positive_labels_by_edge_type: dict[EdgeType, torch.Tensor], negative_labels_by_edge_type: dict[EdgeType, torch.Tensor], ) -> tuple[dict[EdgeType, torch.Tensor], dict[EdgeType, torch.Tensor]]: """Remap padded global ABLP labels to local label edge indices. Positive and negative labels share one sorted node lookup per supervision node type. Pair order within an anchor is unspecified; row-0 anchor indices remain grouped in ascending order. Args: local_id_to_global_id_by_node_type: Per node type ``t``, ``[S_t]`` global ids where index ``i`` is the local node id. positive_labels_by_edge_type: Per positive-label edge type ``et``, ``[A_et, M_et]`` padded global label-node ids. negative_labels_by_edge_type: Equivalent ``[A_et, M_et]`` negative label tensors. May be empty. Returns: Positive and negative mappings keyed by message-passing edge type. Each value is a ``[2, E_et]`` local label edge index whose rows index the corresponding anchor batch and supervision node store. Example: With destination local-to-global ids ``[40, 10, 30]``, positive labels ``[[30, -1]]`` map to ``[[0], [2]]`` and negative labels ``[[40, 10]]`` map to ``[[0, 0], [0, 1]]``. The returned positive and negative dictionaries retain these edge indices under their message-passing edge types. """ sorted_node_lookup_by_type: dict[NodeType, tuple[torch.Tensor, torch.Tensor]] = {} positive_label_edge_indices = _remap_labels_by_edge_type( labels_by_edge_type=positive_labels_by_edge_type, local_id_to_global_id_by_node_type=local_id_to_global_id_by_node_type, sorted_node_lookup_by_type=sorted_node_lookup_by_type, ) negative_label_edge_indices = _remap_labels_by_edge_type( labels_by_edge_type=negative_labels_by_edge_type, local_id_to_global_id_by_node_type=local_id_to_global_id_by_node_type, sorted_node_lookup_by_type=sorted_node_lookup_by_type, ) return positive_label_edge_indices, negative_label_edge_indices