Skip to content

NN

hypertorch.nn

HyperedgeAggregator

Pool node embeddings into hyperedge embeddings using the incidence structure.

Each node-hyperedge incidence selects one node embedding row, then reduces those rows per hyperedge with the requested scatter aggregation.

Attributes:

Name Type Description
hyperedge_index_wrapper HyperedgeIndex

Wrapper around the hyperedge incidence tensor.

node_embeddings Tensor

Node embedding matrix of size (num_nodes, num_channels).

num_hyperedges int | None

Optional explicit hyperedge count. When provided, the pooled output preserves empty hyperedges that do not appear in hyperedge_index.

Source code in hypertorch/nn/aggregator.py
class HyperedgeAggregator:
    """
    Pool node embeddings into hyperedge embeddings using the incidence structure.

    Each node-hyperedge incidence selects one node embedding row, then reduces
    those rows per hyperedge with the requested scatter aggregation.

    Attributes:
        hyperedge_index_wrapper: Wrapper around the hyperedge incidence tensor.
        node_embeddings: Node embedding matrix of size ``(num_nodes, num_channels)``.
        num_hyperedges: Optional explicit hyperedge count.
            When provided, the pooled output preserves empty hyperedges that
            do not appear in ``hyperedge_index``.
    """

    def __init__(
        self,
        hyperedge_index: Tensor,
        node_embeddings: Tensor,
        num_hyperedges: int | None = None,
    ):
        """
        Initialize the hyperedge aggregator.

        Args:
            hyperedge_index: Hyperedge incidence in COO format of size ``(2, num_incidences)``.
            node_embeddings: Node embedding matrix of size ``(num_nodes, num_channels)``.
            num_hyperedges: Optional explicit hyperedge count. Defaults to ``None``.
        """
        self.hyperedge_index_wrapper: HyperedgeIndex = HyperedgeIndex(hyperedge_index)
        self.node_embeddings: Tensor = node_embeddings
        self.num_hyperedges: int | None = num_hyperedges

    def pool(self, aggregation: Literal["maxmin", "max", "min", "mean", "mul", "sum"]) -> Tensor:
        """
        Aggregate node embeddings for each hyperedge.

        ``hyperedge_index`` is the COO encoding of the nonzero entries of ``H``,
        so ``hyperedge_index[0, k] = v`` and ``hyperedge_index[1, k] = e`` means ``H[v, e] = 1``
        for incidence ``k``.

        Let ``H`` be the binary incidence matrix of shape ``(num_nodes, num_hyperedges)``
        and let ``X`` be the node embedding matrix of shape ``(num_nodes, num_channels)``.
        This method pools node features into hyperedge features using the incidence pattern in
        ``H``.

        Aggregations:
            - ``aggregation="sum"`` computes the equivalent of the standard
                sparse matrix product ``H^T X``.
            - ``aggregation="mean"`` computes ``D_e^{-1} H^T X``, where
                ``D_e[e, e] = sum_v H[v, e]`` is the hyperedge cardinality matrix.
            - ``aggregation in {"max", "min", "mul"}`` uses the same sparsity pattern as ``H^T X``,
                but replaces the summation over incident nodes with a channel-wise ``max``, ``min``,
                or product reduction.
            - ``aggregation="maxmin"`` computes the channel-wise range ``max - min``
                for each hyperedge.

        Examples:
            >>> hyperedge_index = [[0, 1, 2, 2, 3],
            ...                    [0, 0, 0, 1, 1]]
            >>> node_embeddings = [[1, 10], [2, 20], [3, 30], [4, 40]]
            >>> HyperedgeAggregator(hyperedge_index, node_embeddings).pool("mean")
            ... [[2, 20], [3.5, 35]]
            >>> HyperedgeAggregator(hyperedge_index, node_embeddings).pool("sum")
            ... [[6, 60], [7, 70]]
            >>> HyperedgeAggregator(hyperedge_index, node_embeddings).pool("max")
            ... [[3, 30], [4, 40]]
            >>> HyperedgeAggregator(hyperedge_index, node_embeddings).pool("maxmin")
            ... [[2, 20], [1, 10]]

        Args:
            aggregation: Reduction applied across the nodes belonging to each hyperedge.

        Returns:
            hyperedge_embeddings: A hyperedge embedding matrix of
                shape ``(num_hyperedges, num_channels)``.
        """
        # Gather the embeddings for each incidence.
        # A node appearing in multiple hyperedges is repeated, once per incidence.
        # Example: node_embeddings = [[1, 10],  # node 0
        #                             [2, 20],  # node 1
        #                             [3, 30],  # node 2
        #                             [4, 40]]  # node 3
        #          -> all_node_ids = [0, 1, 2, 2, 3]
        #          -> incidence_node_embeddings = [[1, 10],  # node 0 for hyperedge 0
        #                                          [2, 20],  # node 1 for hyperedge 0
        #                                          [3, 30],  # node 2 for hyperedge 0
        #                                          [3, 30],  # node 2 for hyperedge 1
        #                                          [4, 40]]  # node 3 for hyperedge 1
        #             shape: (num_incidences, num_channels)
        incidence_node_embeddings = self.node_embeddings[self.hyperedge_index_wrapper.all_node_ids]

        # Scatter-aggregate node embeddings into hyperedge embeddings.
        # Example: with aggregation="sum":
        #          [[1+2+3, 10+20+30],  # hyperedge 0 contains node 0, 1, 2
        #          [3+4, 30+40]]        # hyperedge 1 contains node 2, 3
        #          shape: (num_hyperedges, num_channels)
        #          with aggregation="max":
        #          [[max(1, 2, 3), max(10, 20, 30)],  # hyperedge 0 contains node 0, 1, 2
        #           [max(3, 4), max(30, 40)]]         # hyperedge 1 contains node 2, 3
        #          shape: (num_hyperedges, num_channels)
        num_hyperedges = (
            self.num_hyperedges
            if self.num_hyperedges is not None
            else self.hyperedge_index_wrapper.num_hyperedges
        )

        if aggregation == "maxmin":
            return maxmin_scatter(
                src=incidence_node_embeddings,
                index=self.hyperedge_index_wrapper.all_hyperedge_ids,
                dim=0,  # scatter along the hyperedge dimension
                dim_size=num_hyperedges,
            )

        return scatter(
            src=incidence_node_embeddings,
            index=self.hyperedge_index_wrapper.all_hyperedge_ids,
            dim=0,  # scatter along the hyperedge dimension
            dim_size=num_hyperedges,
            reduce=aggregation,
        )

__init__(hyperedge_index, node_embeddings, num_hyperedges=None)

Initialize the hyperedge aggregator.

Parameters:

Name Type Description Default
hyperedge_index Tensor

Hyperedge incidence in COO format of size (2, num_incidences).

required
node_embeddings Tensor

Node embedding matrix of size (num_nodes, num_channels).

required
num_hyperedges int | None

Optional explicit hyperedge count. Defaults to None.

None
Source code in hypertorch/nn/aggregator.py
def __init__(
    self,
    hyperedge_index: Tensor,
    node_embeddings: Tensor,
    num_hyperedges: int | None = None,
):
    """
    Initialize the hyperedge aggregator.

    Args:
        hyperedge_index: Hyperedge incidence in COO format of size ``(2, num_incidences)``.
        node_embeddings: Node embedding matrix of size ``(num_nodes, num_channels)``.
        num_hyperedges: Optional explicit hyperedge count. Defaults to ``None``.
    """
    self.hyperedge_index_wrapper: HyperedgeIndex = HyperedgeIndex(hyperedge_index)
    self.node_embeddings: Tensor = node_embeddings
    self.num_hyperedges: int | None = num_hyperedges

pool(aggregation)

Aggregate node embeddings for each hyperedge.

hyperedge_index is the COO encoding of the nonzero entries of H, so hyperedge_index[0, k] = v and hyperedge_index[1, k] = e means H[v, e] = 1 for incidence k.

Let H be the binary incidence matrix of shape (num_nodes, num_hyperedges) and let X be the node embedding matrix of shape (num_nodes, num_channels). This method pools node features into hyperedge features using the incidence pattern in H.

Aggregations
  • aggregation="sum" computes the equivalent of the standard sparse matrix product H^T X.
  • aggregation="mean" computes D_e^{-1} H^T X, where D_e[e, e] = sum_v H[v, e] is the hyperedge cardinality matrix.
  • aggregation in {"max", "min", "mul"} uses the same sparsity pattern as H^T X, but replaces the summation over incident nodes with a channel-wise max, min, or product reduction.
  • aggregation="maxmin" computes the channel-wise range max - min for each hyperedge.

Examples:

>>> hyperedge_index = [[0, 1, 2, 2, 3],
...                    [0, 0, 0, 1, 1]]
>>> node_embeddings = [[1, 10], [2, 20], [3, 30], [4, 40]]
>>> HyperedgeAggregator(hyperedge_index, node_embeddings).pool("mean")
... [[2, 20], [3.5, 35]]
>>> HyperedgeAggregator(hyperedge_index, node_embeddings).pool("sum")
... [[6, 60], [7, 70]]
>>> HyperedgeAggregator(hyperedge_index, node_embeddings).pool("max")
... [[3, 30], [4, 40]]
>>> HyperedgeAggregator(hyperedge_index, node_embeddings).pool("maxmin")
... [[2, 20], [1, 10]]

Parameters:

Name Type Description Default
aggregation Literal['maxmin', 'max', 'min', 'mean', 'mul', 'sum']

Reduction applied across the nodes belonging to each hyperedge.

required

Returns:

Name Type Description
hyperedge_embeddings Tensor

A hyperedge embedding matrix of shape (num_hyperedges, num_channels).

Source code in hypertorch/nn/aggregator.py
def pool(self, aggregation: Literal["maxmin", "max", "min", "mean", "mul", "sum"]) -> Tensor:
    """
    Aggregate node embeddings for each hyperedge.

    ``hyperedge_index`` is the COO encoding of the nonzero entries of ``H``,
    so ``hyperedge_index[0, k] = v`` and ``hyperedge_index[1, k] = e`` means ``H[v, e] = 1``
    for incidence ``k``.

    Let ``H`` be the binary incidence matrix of shape ``(num_nodes, num_hyperedges)``
    and let ``X`` be the node embedding matrix of shape ``(num_nodes, num_channels)``.
    This method pools node features into hyperedge features using the incidence pattern in
    ``H``.

    Aggregations:
        - ``aggregation="sum"`` computes the equivalent of the standard
            sparse matrix product ``H^T X``.
        - ``aggregation="mean"`` computes ``D_e^{-1} H^T X``, where
            ``D_e[e, e] = sum_v H[v, e]`` is the hyperedge cardinality matrix.
        - ``aggregation in {"max", "min", "mul"}`` uses the same sparsity pattern as ``H^T X``,
            but replaces the summation over incident nodes with a channel-wise ``max``, ``min``,
            or product reduction.
        - ``aggregation="maxmin"`` computes the channel-wise range ``max - min``
            for each hyperedge.

    Examples:
        >>> hyperedge_index = [[0, 1, 2, 2, 3],
        ...                    [0, 0, 0, 1, 1]]
        >>> node_embeddings = [[1, 10], [2, 20], [3, 30], [4, 40]]
        >>> HyperedgeAggregator(hyperedge_index, node_embeddings).pool("mean")
        ... [[2, 20], [3.5, 35]]
        >>> HyperedgeAggregator(hyperedge_index, node_embeddings).pool("sum")
        ... [[6, 60], [7, 70]]
        >>> HyperedgeAggregator(hyperedge_index, node_embeddings).pool("max")
        ... [[3, 30], [4, 40]]
        >>> HyperedgeAggregator(hyperedge_index, node_embeddings).pool("maxmin")
        ... [[2, 20], [1, 10]]

    Args:
        aggregation: Reduction applied across the nodes belonging to each hyperedge.

    Returns:
        hyperedge_embeddings: A hyperedge embedding matrix of
            shape ``(num_hyperedges, num_channels)``.
    """
    # Gather the embeddings for each incidence.
    # A node appearing in multiple hyperedges is repeated, once per incidence.
    # Example: node_embeddings = [[1, 10],  # node 0
    #                             [2, 20],  # node 1
    #                             [3, 30],  # node 2
    #                             [4, 40]]  # node 3
    #          -> all_node_ids = [0, 1, 2, 2, 3]
    #          -> incidence_node_embeddings = [[1, 10],  # node 0 for hyperedge 0
    #                                          [2, 20],  # node 1 for hyperedge 0
    #                                          [3, 30],  # node 2 for hyperedge 0
    #                                          [3, 30],  # node 2 for hyperedge 1
    #                                          [4, 40]]  # node 3 for hyperedge 1
    #             shape: (num_incidences, num_channels)
    incidence_node_embeddings = self.node_embeddings[self.hyperedge_index_wrapper.all_node_ids]

    # Scatter-aggregate node embeddings into hyperedge embeddings.
    # Example: with aggregation="sum":
    #          [[1+2+3, 10+20+30],  # hyperedge 0 contains node 0, 1, 2
    #          [3+4, 30+40]]        # hyperedge 1 contains node 2, 3
    #          shape: (num_hyperedges, num_channels)
    #          with aggregation="max":
    #          [[max(1, 2, 3), max(10, 20, 30)],  # hyperedge 0 contains node 0, 1, 2
    #           [max(3, 4), max(30, 40)]]         # hyperedge 1 contains node 2, 3
    #          shape: (num_hyperedges, num_channels)
    num_hyperedges = (
        self.num_hyperedges
        if self.num_hyperedges is not None
        else self.hyperedge_index_wrapper.num_hyperedges
    )

    if aggregation == "maxmin":
        return maxmin_scatter(
            src=incidence_node_embeddings,
            index=self.hyperedge_index_wrapper.all_hyperedge_ids,
            dim=0,  # scatter along the hyperedge dimension
            dim_size=num_hyperedges,
        )

    return scatter(
        src=incidence_node_embeddings,
        index=self.hyperedge_index_wrapper.all_hyperedge_ids,
        dim=0,  # scatter along the hyperedge dimension
        dim_size=num_hyperedges,
        reduce=aggregation,
    )

NodeAggregator

Pool hyperedge embeddings into node embeddings using the incidence structure.

Each node-hyperedge incidence selects one hyperedge embedding row, then reduces those rows per node with the requested scatter aggregation.

Attributes:

Name Type Description
hyperedge_index_wrapper HyperedgeIndex

Wrapper around the hyperedge incidence tensor.

hyperedge_embeddings Tensor

Hyperedge embedding matrix of size (num_hyperedges, num_channels).

num_nodes int | None

Optional explicit node count. When provided, the pooled output preserves isolated nodes that do not appear in hyperedge_index. Defaults to None.

Source code in hypertorch/nn/aggregator.py
class NodeAggregator:
    """
    Pool hyperedge embeddings into node embeddings using the incidence structure.

    Each node-hyperedge incidence selects one hyperedge embedding row, then
    reduces those rows per node with the requested scatter aggregation.

    Attributes:
        hyperedge_index_wrapper: Wrapper around the hyperedge incidence tensor.
        hyperedge_embeddings: Hyperedge embedding matrix of size ``(num_hyperedges, num_channels)``.
        num_nodes: Optional explicit node count. When provided, the pooled output preserves
            isolated nodes that do not appear in ``hyperedge_index``. Defaults to ``None``.
    """

    def __init__(
        self,
        hyperedge_index: Tensor,
        hyperedge_embeddings: Tensor,
        num_nodes: int | None = None,
    ):
        """
        Initialize the node aggregator.

        Args:
            hyperedge_index: Hyperedge incidence in COO format of size ``(2, num_incidences)``.
            hyperedge_embeddings: Hyperedge embedding matrix of size
                ``(num_hyperedges, num_channels)``.
            num_nodes: Optional explicit node count. Defaults to ``None``.
        """
        self.hyperedge_index_wrapper: HyperedgeIndex = HyperedgeIndex(hyperedge_index)
        self.hyperedge_embeddings: Tensor = hyperedge_embeddings
        self.num_nodes: int | None = num_nodes

    def pool(self, aggregation: Literal["maxmin", "max", "min", "mean", "mul", "sum"]) -> Tensor:
        """
        Aggregate hyperedge embeddings for each node.

        ``hyperedge_index`` is the COO encoding of the nonzero entries of ``H``,
        so ``hyperedge_index[0, k] = v`` and ``hyperedge_index[1, k] = e`` means ``H[v, e] = 1``
        for incidence ``k``.

        Let ``H`` be the incidence matrix of shape ``(num_nodes, num_hyperedges)``
        and let ``E`` be the hyperedge embedding matrix of shape ``(num_hyperedges, num_channels)``.
        This method pools hyperedge features into node features using the incidence pattern
        in ``H``.

        Aggregations:
            - ``aggregation="sum"`` computes the equivalent of the standard
                sparse matrix product ``H E``.
            - ``aggregation="mean"`` computes ``D_v^{-1} H E``, where ``D_v[v, v] = sum_e H[v, e]``
                is the node degree matrix.
            - ``aggregation in {"max", "min", "mul"}`` uses the same sparsity pattern as ``H E``,
                but replaces the summation over incident hyperedges with a channel-wise
                ``max``, ``min``, or product reduction.

        Examples:
            >>> hyperedge_index = [[0, 1, 1, 2],
            ...                    [0, 0, 1, 1]]
            >>> hyperedge_embeddings = [[10, 100], [20, 200]]
            >>> NodeAggregator(hyperedge_index, hyperedge_embeddings).pool("mean")
            ... [[10, 100], [15, 150], [20, 200]]
            >>> NodeAggregator(hyperedge_index, hyperedge_embeddings).pool("sum")
            ... [[10, 100], [30, 300], [20, 200]]
            >>> NodeAggregator(hyperedge_index, hyperedge_embeddings).pool("max")
            ... [[10, 100], [20, 200], [20, 200]]

        Args:
            aggregation: Reduction applied across the hyperedges incident to each node.

        Returns:
            node_embeddings: A node embedding matrix of shape ``(num_nodes, num_channels)``.
        """
        # Gather the embeddings for each incidence.
        # A hyperedge appearing in multiple node incidences is repeated, once per incidence.
        # Example: hyperedge_embeddings = [[10, 100],  # hyperedge 0
        #                                  [20, 200]]  # hyperedge 1
        #          -> all_hyperedge_ids = [0, 0, 1, 1]
        #          -> incidence_hyperedge_embeddings = [[10, 100],   # hyperedge 0 for node 0
        #                                               [10, 100],   # hyperedge 0 for node 1
        #                                               [20, 200],   # hyperedge 1 for node 1
        #                                               [20, 200]]   # hyperedge 1 for node 2
        #             shape: (num_incidences, num_channels)
        incidence_hyperedge_embeddings = self.hyperedge_embeddings[
            self.hyperedge_index_wrapper.all_hyperedge_ids
        ]
        num_nodes = (
            self.num_nodes if self.num_nodes is not None else self.hyperedge_index_wrapper.num_nodes
        )

        if aggregation == "maxmin":
            return maxmin_scatter(
                src=incidence_hyperedge_embeddings,
                index=self.hyperedge_index_wrapper.all_node_ids,
                dim=0,  # scatter along the node dimension
                dim_size=num_nodes,
            )

        # Scatter-aggregate hyperedge embeddings into node embeddings.
        # Example: with aggregation="sum":
        #          [[10, 100],         # node 0 belongs to hyperedge 0
        #           [10+20, 100+200],  # node 1 belongs to hyperedge 0 and 1
        #           [20, 200]]         # node 2 belongs to hyperedge 1
        #          shape: (num_nodes, num_channels)
        #          with aggregation="max":
        #          [[10, 100],                     # node 0 belongs to hyperedge 0
        #           [max(10, 20), max(100, 200)],  # node 1 belongs to hyperedge 0 and 1
        #           [20, 200]]                     # node 2 belongs to hyperedge 1
        #         shape: (num_nodes, num_channels)
        return scatter(
            src=incidence_hyperedge_embeddings,
            index=self.hyperedge_index_wrapper.all_node_ids,
            dim=0,  # scatter along the node dimension
            dim_size=num_nodes,
            reduce=aggregation,
        )

__init__(hyperedge_index, hyperedge_embeddings, num_nodes=None)

Initialize the node aggregator.

Parameters:

Name Type Description Default
hyperedge_index Tensor

Hyperedge incidence in COO format of size (2, num_incidences).

required
hyperedge_embeddings Tensor

Hyperedge embedding matrix of size (num_hyperedges, num_channels).

required
num_nodes int | None

Optional explicit node count. Defaults to None.

None
Source code in hypertorch/nn/aggregator.py
def __init__(
    self,
    hyperedge_index: Tensor,
    hyperedge_embeddings: Tensor,
    num_nodes: int | None = None,
):
    """
    Initialize the node aggregator.

    Args:
        hyperedge_index: Hyperedge incidence in COO format of size ``(2, num_incidences)``.
        hyperedge_embeddings: Hyperedge embedding matrix of size
            ``(num_hyperedges, num_channels)``.
        num_nodes: Optional explicit node count. Defaults to ``None``.
    """
    self.hyperedge_index_wrapper: HyperedgeIndex = HyperedgeIndex(hyperedge_index)
    self.hyperedge_embeddings: Tensor = hyperedge_embeddings
    self.num_nodes: int | None = num_nodes

pool(aggregation)

Aggregate hyperedge embeddings for each node.

hyperedge_index is the COO encoding of the nonzero entries of H, so hyperedge_index[0, k] = v and hyperedge_index[1, k] = e means H[v, e] = 1 for incidence k.

Let H be the incidence matrix of shape (num_nodes, num_hyperedges) and let E be the hyperedge embedding matrix of shape (num_hyperedges, num_channels). This method pools hyperedge features into node features using the incidence pattern in H.

Aggregations
  • aggregation="sum" computes the equivalent of the standard sparse matrix product H E.
  • aggregation="mean" computes D_v^{-1} H E, where D_v[v, v] = sum_e H[v, e] is the node degree matrix.
  • aggregation in {"max", "min", "mul"} uses the same sparsity pattern as H E, but replaces the summation over incident hyperedges with a channel-wise max, min, or product reduction.

Examples:

>>> hyperedge_index = [[0, 1, 1, 2],
...                    [0, 0, 1, 1]]
>>> hyperedge_embeddings = [[10, 100], [20, 200]]
>>> NodeAggregator(hyperedge_index, hyperedge_embeddings).pool("mean")
... [[10, 100], [15, 150], [20, 200]]
>>> NodeAggregator(hyperedge_index, hyperedge_embeddings).pool("sum")
... [[10, 100], [30, 300], [20, 200]]
>>> NodeAggregator(hyperedge_index, hyperedge_embeddings).pool("max")
... [[10, 100], [20, 200], [20, 200]]

Parameters:

Name Type Description Default
aggregation Literal['maxmin', 'max', 'min', 'mean', 'mul', 'sum']

Reduction applied across the hyperedges incident to each node.

required

Returns:

Name Type Description
node_embeddings Tensor

A node embedding matrix of shape (num_nodes, num_channels).

Source code in hypertorch/nn/aggregator.py
def pool(self, aggregation: Literal["maxmin", "max", "min", "mean", "mul", "sum"]) -> Tensor:
    """
    Aggregate hyperedge embeddings for each node.

    ``hyperedge_index`` is the COO encoding of the nonzero entries of ``H``,
    so ``hyperedge_index[0, k] = v`` and ``hyperedge_index[1, k] = e`` means ``H[v, e] = 1``
    for incidence ``k``.

    Let ``H`` be the incidence matrix of shape ``(num_nodes, num_hyperedges)``
    and let ``E`` be the hyperedge embedding matrix of shape ``(num_hyperedges, num_channels)``.
    This method pools hyperedge features into node features using the incidence pattern
    in ``H``.

    Aggregations:
        - ``aggregation="sum"`` computes the equivalent of the standard
            sparse matrix product ``H E``.
        - ``aggregation="mean"`` computes ``D_v^{-1} H E``, where ``D_v[v, v] = sum_e H[v, e]``
            is the node degree matrix.
        - ``aggregation in {"max", "min", "mul"}`` uses the same sparsity pattern as ``H E``,
            but replaces the summation over incident hyperedges with a channel-wise
            ``max``, ``min``, or product reduction.

    Examples:
        >>> hyperedge_index = [[0, 1, 1, 2],
        ...                    [0, 0, 1, 1]]
        >>> hyperedge_embeddings = [[10, 100], [20, 200]]
        >>> NodeAggregator(hyperedge_index, hyperedge_embeddings).pool("mean")
        ... [[10, 100], [15, 150], [20, 200]]
        >>> NodeAggregator(hyperedge_index, hyperedge_embeddings).pool("sum")
        ... [[10, 100], [30, 300], [20, 200]]
        >>> NodeAggregator(hyperedge_index, hyperedge_embeddings).pool("max")
        ... [[10, 100], [20, 200], [20, 200]]

    Args:
        aggregation: Reduction applied across the hyperedges incident to each node.

    Returns:
        node_embeddings: A node embedding matrix of shape ``(num_nodes, num_channels)``.
    """
    # Gather the embeddings for each incidence.
    # A hyperedge appearing in multiple node incidences is repeated, once per incidence.
    # Example: hyperedge_embeddings = [[10, 100],  # hyperedge 0
    #                                  [20, 200]]  # hyperedge 1
    #          -> all_hyperedge_ids = [0, 0, 1, 1]
    #          -> incidence_hyperedge_embeddings = [[10, 100],   # hyperedge 0 for node 0
    #                                               [10, 100],   # hyperedge 0 for node 1
    #                                               [20, 200],   # hyperedge 1 for node 1
    #                                               [20, 200]]   # hyperedge 1 for node 2
    #             shape: (num_incidences, num_channels)
    incidence_hyperedge_embeddings = self.hyperedge_embeddings[
        self.hyperedge_index_wrapper.all_hyperedge_ids
    ]
    num_nodes = (
        self.num_nodes if self.num_nodes is not None else self.hyperedge_index_wrapper.num_nodes
    )

    if aggregation == "maxmin":
        return maxmin_scatter(
            src=incidence_hyperedge_embeddings,
            index=self.hyperedge_index_wrapper.all_node_ids,
            dim=0,  # scatter along the node dimension
            dim_size=num_nodes,
        )

    # Scatter-aggregate hyperedge embeddings into node embeddings.
    # Example: with aggregation="sum":
    #          [[10, 100],         # node 0 belongs to hyperedge 0
    #           [10+20, 100+200],  # node 1 belongs to hyperedge 0 and 1
    #           [20, 200]]         # node 2 belongs to hyperedge 1
    #          shape: (num_nodes, num_channels)
    #          with aggregation="max":
    #          [[10, 100],                     # node 0 belongs to hyperedge 0
    #           [max(10, 20), max(100, 200)],  # node 1 belongs to hyperedge 0 and 1
    #           [20, 200]]                     # node 2 belongs to hyperedge 1
    #         shape: (num_nodes, num_channels)
    return scatter(
        src=incidence_hyperedge_embeddings,
        index=self.hyperedge_index_wrapper.all_node_ids,
        dim=0,  # scatter along the node dimension
        dim_size=num_nodes,
        reduce=aggregation,
    )

HGNNConv

Bases: Module

References

Each layer performs: X' = sigma(L_HGNN X Theta) where L_HGNN = D_n^{-1/2} H D_e^{-1} H^T D_n^{-1/2} is the hypergraph Laplacian computed from the incidence matrix H. This smooths node features through the hypergraph structure (nodes -> hyperedges -> nodes) without reducing to a pairwise graph.

Unlike HyperGCNConv, which uses a GCN Laplacian on a graph reduced from the hypergraph, HGNNConv operates entirely in hypergraph space and preserves all higher-order relationships.

Attributes:

Name Type Description
is_last bool

Whether to skip the final activation and dropout. If True, the layer will not apply the final activation and dropout functions. Defaults to False.

batch_norm_1d BatchNorm1d | None

Optional batch normalization layer.

activation_fn ReLU

Activation function applied to hidden outputs.

dropout Dropout

Dropout layer applied to hidden outputs. Defaults to 0.5.

theta Linear

Learnable feature projection.

Source code in hypertorch/nn/conv.py
class HGNNConv(nn.Module):
    """
    References:
        - The HGNNConv layer proposed in [Hypergraph Neural Networks](https://arxiv.org/pdf/1809.09401) paper (AAAI 2019).
        - Reference implementation: [Code](https://deephypergraph.readthedocs.io/en/latest/_modules/dhg/nn/convs/hypergraphs/hgnn_conv.html#HGNNConv).

    Each layer performs: ``X' = sigma(L_HGNN X Theta)``
    where ``L_HGNN = D_n^{-1/2} H D_e^{-1} H^T D_n^{-1/2}``
    is the hypergraph Laplacian computed from the incidence matrix H.
    This smooths node features through the hypergraph structure (nodes -> hyperedges -> nodes)
    without reducing to a pairwise graph.

    Unlike ``HyperGCNConv``, which uses a GCN Laplacian on a graph reduced from the hypergraph,
    ``HGNNConv`` operates entirely in hypergraph space and preserves all higher-order relationships.

    Attributes:
        is_last: Whether to skip the final activation and dropout.
            If ``True``, the layer will not apply the final activation and
            dropout functions. Defaults to ``False``.
        batch_norm_1d: Optional batch normalization layer.
        activation_fn: Activation function applied to hidden outputs.
        dropout: Dropout layer applied to hidden outputs. Defaults to ``0.5``.
        theta: Learnable feature projection.
    """  # noqa: E501

    def __init__(
        self,
        in_channels: int,
        out_channels: int,
        bias: bool = True,
        use_batch_normalization: bool = False,
        drop_rate: float = 0.5,
        is_last: bool = False,
    ):
        """
        Initialize the HGNN convolution layer.

        Args:
            in_channels: The number of input channels.
            out_channels: The number of output channels.
            bias: If set to ``False``, the layer will not learn the bias parameter.
                Defaults to ``True``.
            use_batch_normalization: If set to ``True``, the layer will use batch normalization.
                Defaults to ``False``.
            drop_rate: If set to a positive number, the layer will use dropout. Defaults to ``0.5``.
            is_last: If set to ``True``, the layer will not apply the final activation and
                dropout functions. Defaults to ``False``.
        """
        super().__init__()
        self.is_last: bool = is_last
        self.batch_norm_1d: nn.BatchNorm1d | None = (
            nn.BatchNorm1d(out_channels) if use_batch_normalization else None
        )
        self.activation_fn: nn.ReLU = nn.ReLU(inplace=True)
        self.dropout: nn.Dropout = nn.Dropout(drop_rate)
        self.theta: nn.Linear = nn.Linear(in_channels, out_channels, bias=bias)

    def forward(self, x: Tensor, hyperedge_index: Tensor) -> Tensor:
        """
        Apply one HGNN convolution layer: project features, smooth via hypergraph Laplacian,
        then apply activation, batch norm, and dropout (unless this is the last layer).

        The full per-layer formula is:
            ``X' = sigma( D_n^{-1/2} H D_e^{-1} H^T D_n^{-1/2} (X Theta) )``

        where the Laplacian ``L = D_n^{-1/2} H D_e^{-1} H^T D_n^{-1/2}`` is computed from
        the hyperedge_index and can be passed in precomputed as ``hgnn_laplacian_matrix``
        for efficiency when the hypergraph structure does not change across forward passes.

        Args:
            x: Input node feature matrix of size ``(num_nodes, in_channels)``.
            hyperedge_index: Hyperedge incidence in COO format of size ``(2, num_incidences)``.

        Returns:
            x: The output node feature matrix of size ``(num_nodes, out_channels)``.
        """
        x = self.theta(x)

        smoothing_matrix = HyperedgeIndex(hyperedge_index).get_sparse_hgnn_smoothing_matrix(
            num_nodes=x.size(0),
        )
        x = Hypergraph.smoothing_with_matrix(x, smoothing_matrix)

        if not self.is_last:
            x = self.activation_fn(x)
            if self.batch_norm_1d is not None:
                x = self.batch_norm_1d(x)
            x = self.dropout(x)

        return x

__init__(in_channels, out_channels, bias=True, use_batch_normalization=False, drop_rate=0.5, is_last=False)

Initialize the HGNN convolution layer.

Parameters:

Name Type Description Default
in_channels int

The number of input channels.

required
out_channels int

The number of output channels.

required
bias bool

If set to False, the layer will not learn the bias parameter. Defaults to True.

True
use_batch_normalization bool

If set to True, the layer will use batch normalization. Defaults to False.

False
drop_rate float

If set to a positive number, the layer will use dropout. Defaults to 0.5.

0.5
is_last bool

If set to True, the layer will not apply the final activation and dropout functions. Defaults to False.

False
Source code in hypertorch/nn/conv.py
def __init__(
    self,
    in_channels: int,
    out_channels: int,
    bias: bool = True,
    use_batch_normalization: bool = False,
    drop_rate: float = 0.5,
    is_last: bool = False,
):
    """
    Initialize the HGNN convolution layer.

    Args:
        in_channels: The number of input channels.
        out_channels: The number of output channels.
        bias: If set to ``False``, the layer will not learn the bias parameter.
            Defaults to ``True``.
        use_batch_normalization: If set to ``True``, the layer will use batch normalization.
            Defaults to ``False``.
        drop_rate: If set to a positive number, the layer will use dropout. Defaults to ``0.5``.
        is_last: If set to ``True``, the layer will not apply the final activation and
            dropout functions. Defaults to ``False``.
    """
    super().__init__()
    self.is_last: bool = is_last
    self.batch_norm_1d: nn.BatchNorm1d | None = (
        nn.BatchNorm1d(out_channels) if use_batch_normalization else None
    )
    self.activation_fn: nn.ReLU = nn.ReLU(inplace=True)
    self.dropout: nn.Dropout = nn.Dropout(drop_rate)
    self.theta: nn.Linear = nn.Linear(in_channels, out_channels, bias=bias)

forward(x, hyperedge_index)

Apply one HGNN convolution layer: project features, smooth via hypergraph Laplacian, then apply activation, batch norm, and dropout (unless this is the last layer).

The full per-layer formula is

X' = sigma( D_n^{-1/2} H D_e^{-1} H^T D_n^{-1/2} (X Theta) )

where the Laplacian L = D_n^{-1/2} H D_e^{-1} H^T D_n^{-1/2} is computed from the hyperedge_index and can be passed in precomputed as hgnn_laplacian_matrix for efficiency when the hypergraph structure does not change across forward passes.

Parameters:

Name Type Description Default
x Tensor

Input node feature matrix of size (num_nodes, in_channels).

required
hyperedge_index Tensor

Hyperedge incidence in COO format of size (2, num_incidences).

required

Returns:

Name Type Description
x Tensor

The output node feature matrix of size (num_nodes, out_channels).

Source code in hypertorch/nn/conv.py
def forward(self, x: Tensor, hyperedge_index: Tensor) -> Tensor:
    """
    Apply one HGNN convolution layer: project features, smooth via hypergraph Laplacian,
    then apply activation, batch norm, and dropout (unless this is the last layer).

    The full per-layer formula is:
        ``X' = sigma( D_n^{-1/2} H D_e^{-1} H^T D_n^{-1/2} (X Theta) )``

    where the Laplacian ``L = D_n^{-1/2} H D_e^{-1} H^T D_n^{-1/2}`` is computed from
    the hyperedge_index and can be passed in precomputed as ``hgnn_laplacian_matrix``
    for efficiency when the hypergraph structure does not change across forward passes.

    Args:
        x: Input node feature matrix of size ``(num_nodes, in_channels)``.
        hyperedge_index: Hyperedge incidence in COO format of size ``(2, num_incidences)``.

    Returns:
        x: The output node feature matrix of size ``(num_nodes, out_channels)``.
    """
    x = self.theta(x)

    smoothing_matrix = HyperedgeIndex(hyperedge_index).get_sparse_hgnn_smoothing_matrix(
        num_nodes=x.size(0),
    )
    x = Hypergraph.smoothing_with_matrix(x, smoothing_matrix)

    if not self.is_last:
        x = self.activation_fn(x)
        if self.batch_norm_1d is not None:
            x = self.batch_norm_1d(x)
        x = self.dropout(x)

    return x

HGNNPConv

Bases: Module

References

Each layer performs: X' = sigma(M_HGNN+ X Theta) where M_HGNN+ = D_v^{-1} H D_e^{-1} H^T is the HGNN+ smoothing matrix.

Unlike HGNNConv, which uses symmetric D_v^{-1/2} normalization for a spectral Laplacian, HGNNPConv uses plain inverse degrees and performs two-stage mean aggregation: nodes -> hyperedges -> nodes.

Attributes:

Name Type Description
is_last bool

Whether to skip the final activation and dropout. If True, the layer will not apply the final activation and dropout functions. Defaults to False.

batch_norm_1d BatchNorm1d | None

Optional batch normalization layer.

activation_fn ReLU

Activation function applied to hidden outputs.

dropout Dropout

Dropout layer applied to hidden outputs. Defaults to 0.5.

theta Linear

Learnable feature projection.

Source code in hypertorch/nn/conv.py
class HGNNPConv(nn.Module):
    """
    References:
        - The HGNNPConv layer proposed in [HGNN+: General Hypergraph Neural Networks](https://ieeexplore.ieee.org/document/9795251) paper (IEEE T-PAMI 2022).
        - Reference implementation: [Code](https://deephypergraph.readthedocs.io/en/latest/_modules/dhg/nn/convs/hypergraphs/hgnnp_conv.html#HGNNPConv).

    Each layer performs: ``X' = sigma(M_HGNN+ X Theta)`` where
    ``M_HGNN+ = D_v^{-1} H D_e^{-1} H^T`` is the HGNN+ smoothing matrix.

    Unlike ``HGNNConv``, which uses symmetric ``D_v^{-1/2}`` normalization for a
    spectral Laplacian, ``HGNNPConv`` uses plain inverse degrees and performs
    two-stage mean aggregation: nodes -> hyperedges -> nodes.

    Attributes:
        is_last: Whether to skip the final activation and dropout.
            If ``True``, the layer will not apply the final activation and
            dropout functions. Defaults to ``False``.
        batch_norm_1d: Optional batch normalization layer.
        activation_fn: Activation function applied to hidden outputs.
        dropout: Dropout layer applied to hidden outputs. Defaults to ``0.5``.
        theta: Learnable feature projection.
    """  # noqa: E501

    def __init__(
        self,
        in_channels: int,
        out_channels: int,
        bias: bool = True,
        use_batch_normalization: bool = False,
        drop_rate: float = 0.5,
        is_last: bool = False,
    ):
        """
        Initialize the HGNN+ convolution layer.

        Args:
            in_channels: The number of input channels.
            out_channels: The number of output channels.
            bias: If set to ``False``, the layer will not learn the bias parameter.
                Defaults to ``True``.
            use_batch_normalization: If set to ``True``, the layer will use batch normalization.
                Defaults to ``False``.
            drop_rate: If set to a positive number, the layer will use dropout. Defaults to ``0.5``.
            is_last: If set to ``True``, the layer will not apply the final activation and dropout
                functions. Defaults to ``False``.
        """
        super().__init__()
        self.is_last: bool = is_last
        self.batch_norm_1d: nn.BatchNorm1d | None = (
            nn.BatchNorm1d(out_channels) if use_batch_normalization else None
        )
        self.activation_fn: nn.ReLU = nn.ReLU(inplace=True)
        self.dropout: nn.Dropout = nn.Dropout(drop_rate)
        self.theta: nn.Linear = nn.Linear(in_channels, out_channels, bias=bias)

    def forward(self, x: Tensor, hyperedge_index: Tensor) -> Tensor:
        """
        Apply one HGNN+ convolution layer using row-stochastic hypergraph smoothing.

        The full per-layer formula is:
            ``X' = sigma( D_v^{-1} H D_e^{-1} H^T (X Theta) )``

        Args:
            x: Input node feature matrix of size ``(num_nodes, in_channels)``.
            hyperedge_index: Hyperedge incidence in COO format of size ``(2, num_incidences)``.

        Returns:
            x: The output node feature matrix of size ``(num_nodes, out_channels)``.
        """
        x = self.theta(x)

        smoothing_matrix = HyperedgeIndex(hyperedge_index).get_sparse_hgnnp_smoothing_matrix(
            num_nodes=x.size(0),
        )
        x = Hypergraph.smoothing_with_matrix(x, smoothing_matrix)

        if not self.is_last:
            x = self.activation_fn(x)
            if self.batch_norm_1d is not None:
                x = self.batch_norm_1d(x)
            x = self.dropout(x)

        return x

__init__(in_channels, out_channels, bias=True, use_batch_normalization=False, drop_rate=0.5, is_last=False)

Initialize the HGNN+ convolution layer.

Parameters:

Name Type Description Default
in_channels int

The number of input channels.

required
out_channels int

The number of output channels.

required
bias bool

If set to False, the layer will not learn the bias parameter. Defaults to True.

True
use_batch_normalization bool

If set to True, the layer will use batch normalization. Defaults to False.

False
drop_rate float

If set to a positive number, the layer will use dropout. Defaults to 0.5.

0.5
is_last bool

If set to True, the layer will not apply the final activation and dropout functions. Defaults to False.

False
Source code in hypertorch/nn/conv.py
def __init__(
    self,
    in_channels: int,
    out_channels: int,
    bias: bool = True,
    use_batch_normalization: bool = False,
    drop_rate: float = 0.5,
    is_last: bool = False,
):
    """
    Initialize the HGNN+ convolution layer.

    Args:
        in_channels: The number of input channels.
        out_channels: The number of output channels.
        bias: If set to ``False``, the layer will not learn the bias parameter.
            Defaults to ``True``.
        use_batch_normalization: If set to ``True``, the layer will use batch normalization.
            Defaults to ``False``.
        drop_rate: If set to a positive number, the layer will use dropout. Defaults to ``0.5``.
        is_last: If set to ``True``, the layer will not apply the final activation and dropout
            functions. Defaults to ``False``.
    """
    super().__init__()
    self.is_last: bool = is_last
    self.batch_norm_1d: nn.BatchNorm1d | None = (
        nn.BatchNorm1d(out_channels) if use_batch_normalization else None
    )
    self.activation_fn: nn.ReLU = nn.ReLU(inplace=True)
    self.dropout: nn.Dropout = nn.Dropout(drop_rate)
    self.theta: nn.Linear = nn.Linear(in_channels, out_channels, bias=bias)

forward(x, hyperedge_index)

Apply one HGNN+ convolution layer using row-stochastic hypergraph smoothing.

The full per-layer formula is

X' = sigma( D_v^{-1} H D_e^{-1} H^T (X Theta) )

Parameters:

Name Type Description Default
x Tensor

Input node feature matrix of size (num_nodes, in_channels).

required
hyperedge_index Tensor

Hyperedge incidence in COO format of size (2, num_incidences).

required

Returns:

Name Type Description
x Tensor

The output node feature matrix of size (num_nodes, out_channels).

Source code in hypertorch/nn/conv.py
def forward(self, x: Tensor, hyperedge_index: Tensor) -> Tensor:
    """
    Apply one HGNN+ convolution layer using row-stochastic hypergraph smoothing.

    The full per-layer formula is:
        ``X' = sigma( D_v^{-1} H D_e^{-1} H^T (X Theta) )``

    Args:
        x: Input node feature matrix of size ``(num_nodes, in_channels)``.
        hyperedge_index: Hyperedge incidence in COO format of size ``(2, num_incidences)``.

    Returns:
        x: The output node feature matrix of size ``(num_nodes, out_channels)``.
    """
    x = self.theta(x)

    smoothing_matrix = HyperedgeIndex(hyperedge_index).get_sparse_hgnnp_smoothing_matrix(
        num_nodes=x.size(0),
    )
    x = Hypergraph.smoothing_with_matrix(x, smoothing_matrix)

    if not self.is_last:
        x = self.activation_fn(x)
        if self.batch_norm_1d is not None:
            x = self.batch_norm_1d(x)
        x = self.dropout(x)

    return x

HNHNConv

Bases: Module

References

Attributes:

Name Type Description
is_last bool

Whether to skip the final activation and dropout. If True, the layer will not apply the final activation and dropout functions. Defaults to False.

batch_norm_1d BatchNorm1d | None

Optional batch normalization layer.

activation_fn ReLU

Activation function applied to hidden outputs.

dropout Dropout

Dropout layer applied to hidden outputs. Defaults to 0.5.

theta_v2e Linear

Learnable node-to-hyperedge projection.

theta_e2v Linear

Learnable hyperedge-to-node projection.

Source code in hypertorch/nn/conv.py
class HNHNConv(nn.Module):
    """
    References:
        - The HNHNConv layer proposed in [HNHN: Hypergraph Networks with Hyperedge Neurons](https://arxiv.org/abs/2006.12278) paper.
        - Reference implementation: [Code](https://deephypergraph.readthedocs.io/en/latest/_modules/dhg/nn/convs/hypergraphs/hnhn_conv.html#HNHNConv).

    Attributes:
        is_last: Whether to skip the final activation and dropout.
            If ``True``, the layer will not apply the final activation and
            dropout functions. Defaults to ``False``.
        batch_norm_1d: Optional batch normalization layer.
        activation_fn: Activation function applied to hidden outputs.
        dropout: Dropout layer applied to hidden outputs. Defaults to ``0.5``.
        theta_v2e: Learnable node-to-hyperedge projection.
        theta_e2v: Learnable hyperedge-to-node projection.
    """  # noqa: E501

    __AGGREGATION: Literal["mean"] = "mean"

    def __init__(
        self,
        in_channels: int,
        out_channels: int,
        bias: bool = True,
        use_batch_normalization: bool = False,
        drop_rate: float = 0.5,
        is_last: bool = False,
    ):
        """
        Initialize the HNHN convolution layer.

        Args:
            in_channels: The number of input channels.
            out_channels: The number of output channels.
            bias: If set to ``False``, the layer will not learn the bias parameter.
                Defaults to ``True``.
            use_batch_normalization: If set to ``True``, the layer will use batch normalization.
                Defaults to ``False``.
            drop_rate: If set to a positive number, the layer will use dropout. Defaults to ``0.5``.
            is_last: If set to ``True``, the layer will not apply the final activation and
                dropout functions. Defaults to ``False``.
        """
        super().__init__()
        self.is_last: bool = is_last
        self.batch_norm_1d: nn.BatchNorm1d | None = (
            nn.BatchNorm1d(out_channels) if use_batch_normalization else None
        )
        self.activation_fn: nn.ReLU = nn.ReLU(inplace=True)
        self.dropout: nn.Dropout = nn.Dropout(drop_rate)
        self.theta_v2e: nn.Linear = nn.Linear(in_channels, out_channels, bias=bias)
        self.theta_e2v: nn.Linear = nn.Linear(out_channels, out_channels, bias=bias)

    def forward(self, x: Tensor, hyperedge_index: Tensor) -> Tensor:
        """
        Apply one HNHN convolution layer using two learned projections around node-to-hyperedge and
        hyperedge-to-node mean aggregation.

        Args:
            x: Input node feature matrix of size ``(num_nodes, in_channels)``.
            hyperedge_index: Hyperedge incidence in COO format of size ``(2, num_incidences)``.

        Returns:
            x: The output node feature matrix of size ``(num_nodes, out_channels)``.
        """
        x = self.theta_v2e(x)

        hyperedge_embeddings = HyperedgeAggregator(hyperedge_index, x).pool(self.__AGGREGATION)
        hyperedge_embeddings = self.activation_fn(hyperedge_embeddings)
        hyperedge_embeddings = self.theta_e2v(hyperedge_embeddings)

        x = NodeAggregator(
            hyperedge_index=hyperedge_index,
            hyperedge_embeddings=hyperedge_embeddings,
            num_nodes=x.size(0),
        ).pool(self.__AGGREGATION)

        if not self.is_last:
            x = self.activation_fn(x)
            if self.batch_norm_1d is not None:
                x = self.batch_norm_1d(x)
            x = self.dropout(x)

        return x

__init__(in_channels, out_channels, bias=True, use_batch_normalization=False, drop_rate=0.5, is_last=False)

Initialize the HNHN convolution layer.

Parameters:

Name Type Description Default
in_channels int

The number of input channels.

required
out_channels int

The number of output channels.

required
bias bool

If set to False, the layer will not learn the bias parameter. Defaults to True.

True
use_batch_normalization bool

If set to True, the layer will use batch normalization. Defaults to False.

False
drop_rate float

If set to a positive number, the layer will use dropout. Defaults to 0.5.

0.5
is_last bool

If set to True, the layer will not apply the final activation and dropout functions. Defaults to False.

False
Source code in hypertorch/nn/conv.py
def __init__(
    self,
    in_channels: int,
    out_channels: int,
    bias: bool = True,
    use_batch_normalization: bool = False,
    drop_rate: float = 0.5,
    is_last: bool = False,
):
    """
    Initialize the HNHN convolution layer.

    Args:
        in_channels: The number of input channels.
        out_channels: The number of output channels.
        bias: If set to ``False``, the layer will not learn the bias parameter.
            Defaults to ``True``.
        use_batch_normalization: If set to ``True``, the layer will use batch normalization.
            Defaults to ``False``.
        drop_rate: If set to a positive number, the layer will use dropout. Defaults to ``0.5``.
        is_last: If set to ``True``, the layer will not apply the final activation and
            dropout functions. Defaults to ``False``.
    """
    super().__init__()
    self.is_last: bool = is_last
    self.batch_norm_1d: nn.BatchNorm1d | None = (
        nn.BatchNorm1d(out_channels) if use_batch_normalization else None
    )
    self.activation_fn: nn.ReLU = nn.ReLU(inplace=True)
    self.dropout: nn.Dropout = nn.Dropout(drop_rate)
    self.theta_v2e: nn.Linear = nn.Linear(in_channels, out_channels, bias=bias)
    self.theta_e2v: nn.Linear = nn.Linear(out_channels, out_channels, bias=bias)

forward(x, hyperedge_index)

Apply one HNHN convolution layer using two learned projections around node-to-hyperedge and hyperedge-to-node mean aggregation.

Parameters:

Name Type Description Default
x Tensor

Input node feature matrix of size (num_nodes, in_channels).

required
hyperedge_index Tensor

Hyperedge incidence in COO format of size (2, num_incidences).

required

Returns:

Name Type Description
x Tensor

The output node feature matrix of size (num_nodes, out_channels).

Source code in hypertorch/nn/conv.py
def forward(self, x: Tensor, hyperedge_index: Tensor) -> Tensor:
    """
    Apply one HNHN convolution layer using two learned projections around node-to-hyperedge and
    hyperedge-to-node mean aggregation.

    Args:
        x: Input node feature matrix of size ``(num_nodes, in_channels)``.
        hyperedge_index: Hyperedge incidence in COO format of size ``(2, num_incidences)``.

    Returns:
        x: The output node feature matrix of size ``(num_nodes, out_channels)``.
    """
    x = self.theta_v2e(x)

    hyperedge_embeddings = HyperedgeAggregator(hyperedge_index, x).pool(self.__AGGREGATION)
    hyperedge_embeddings = self.activation_fn(hyperedge_embeddings)
    hyperedge_embeddings = self.theta_e2v(hyperedge_embeddings)

    x = NodeAggregator(
        hyperedge_index=hyperedge_index,
        hyperedge_embeddings=hyperedge_embeddings,
        num_nodes=x.size(0),
    ).pool(self.__AGGREGATION)

    if not self.is_last:
        x = self.activation_fn(x)
        if self.batch_norm_1d is not None:
            x = self.batch_norm_1d(x)
        x = self.dropout(x)

    return x

HyperGCNConv

Bases: Module

References

Attributes:

Name Type Description
is_last bool

Whether to skip the final activation and dropout. If True, the layer will not apply the final activation and dropout functions. Defaults to False.

use_mediator bool

Whether to use mediator to transform the hyperedges to edges in the graph. Defaults to False.

batch_norm_1d BatchNorm1d | None

Optional batch normalization layer.

activation_fn ReLU

Activation function applied to hidden outputs.

dropout Dropout

Dropout layer applied to hidden outputs. Defaults to 0.5.

theta Linear

Learnable feature projection.

seed int | None

Optional random seed for reducing hyperedges to graph edges. Defaults to None. If provided, it will be used for the random reduction of hyperedges to edges.

Source code in hypertorch/nn/conv.py
class HyperGCNConv(nn.Module):
    """
    References:
        - The HyperGCNConv layer proposed in [HyperGCN: A New Method of Training Graph Convolutional Networks on Hypergraphs](https://dl.acm.org/doi/10.5555/3454287.3454422) paper (NeurIPS 2019).
        - Reference implementation: [source](https://deephypergraph.readthedocs.io/en/latest/_modules/dhg/nn/convs/hypergraphs/hypergcn_conv.html#HyperGCNConv).

    Attributes:
        is_last: Whether to skip the final activation and dropout.
            If ``True``, the layer will not apply the final activation and
            dropout functions. Defaults to ``False``.
        use_mediator: Whether to use mediator to transform the hyperedges to edges in the graph.
            Defaults to ``False``.
        batch_norm_1d: Optional batch normalization layer.
        activation_fn: Activation function applied to hidden outputs.
        dropout: Dropout layer applied to hidden outputs. Defaults to ``0.5``.
        theta: Learnable feature projection.
        seed: Optional random seed for reducing hyperedges to graph edges.
            Defaults to ``None``. If provided, it will be used for the random
            reduction of hyperedges to edges.
    """  # noqa: E501

    def __init__(
        self,
        in_channels: int,
        out_channels: int,
        bias: bool = True,
        use_batch_normalization: bool = False,
        drop_rate: float = 0.5,
        use_mediator: bool = False,
        is_last: bool = False,
        seed: int | None = None,
    ):
        """
        Initialize the HyperGCN convolution layer.

        Args:
            in_channels: The number of input channels.
            out_channels: The number of output channels.
            bias: If set to ``False``, the layer will not learn the bias parameter.
                Defaults to ``True``.
            use_batch_normalization: If set to ``True``, the layer will use batch normalization.
                Defaults to ``False``.
            drop_rate: If set to a positive number, the layer will use dropout. Defaults to ``0.5``.
            use_mediator: Whether to use mediator to transform the hyperedges to edges in the graph.
                Defaults to ``False``.
            is_last: If set to ``True``, the layer will not apply the final activation and
                dropout functions. Defaults to ``False``.
            seed: Optional random seed for the random reduction of hyperedges to edges.
                Defaults to ``None``.
        """
        super().__init__()
        self.is_last: bool = is_last
        self.use_mediator: bool = use_mediator
        self.batch_norm_1d: nn.BatchNorm1d | None = (
            nn.BatchNorm1d(out_channels) if use_batch_normalization else None
        )
        self.activation_fn: nn.ReLU = nn.ReLU(inplace=True)
        self.dropout: nn.Dropout = nn.Dropout(drop_rate)

        # θ is the learnable weight matrix (as in the HyperGCN paper),
        # it projects node features from in_channels to out_channels and
        # learns how to mix feature channels
        self.theta: nn.Linear = nn.Linear(in_channels, out_channels, bias=bias)

        self.seed: int | None = seed

    def forward(
        self,
        x: Tensor,
        hyperedge_index: Tensor,
        gcn_laplacian_matrix: Tensor | None = None,
    ) -> Tensor:
        """
        The forward function.

        Args:
            x: Input node feature matrix. Size ``(num_nodes, in_channels)``.
            hyperedge_index: Hyperedge indices representing the hypergraph structure.
                Size ``(2, num_hyperedges)``.
            gcn_laplacian_matrix: Optional precomputed normalized GCN Laplacian matrix.
                Size ``(num_nodes, num_nodes)``. Defaults to ``None``.
                If provided, it will be used directly for smoothing, so we can skip computing
                it from edge_index.

        Returns:
            x: The output node feature matrix. Size ``(num_nodes, out_channels)``.
        """
        x = self.theta(x)

        if gcn_laplacian_matrix is not None:
            x = Graph.smoothing_with_laplacian_matrix(x, gcn_laplacian_matrix)
        else:
            edge_index, edge_weights = HyperedgeIndex(
                hyperedge_index
            ).reduce_to_edge_index_on_random_direction(
                x=x,
                with_mediators=self.use_mediator,
                return_weights=True,
                seed=self.seed,
            )

            normalized_gcn_laplacian_matrix = EdgeIndex(
                edge_index=edge_index,
                edge_weights=edge_weights,
            ).get_sparse_normalized_gcn_laplacian(num_nodes=x.size(0))

            x = Graph.smoothing_with_laplacian_matrix(x, normalized_gcn_laplacian_matrix)

        if not self.is_last:
            x = self.activation_fn(x)
            if self.batch_norm_1d is not None:
                x = self.batch_norm_1d(x)
            x = self.dropout(x)

        return x

__init__(in_channels, out_channels, bias=True, use_batch_normalization=False, drop_rate=0.5, use_mediator=False, is_last=False, seed=None)

Initialize the HyperGCN convolution layer.

Parameters:

Name Type Description Default
in_channels int

The number of input channels.

required
out_channels int

The number of output channels.

required
bias bool

If set to False, the layer will not learn the bias parameter. Defaults to True.

True
use_batch_normalization bool

If set to True, the layer will use batch normalization. Defaults to False.

False
drop_rate float

If set to a positive number, the layer will use dropout. Defaults to 0.5.

0.5
use_mediator bool

Whether to use mediator to transform the hyperedges to edges in the graph. Defaults to False.

False
is_last bool

If set to True, the layer will not apply the final activation and dropout functions. Defaults to False.

False
seed int | None

Optional random seed for the random reduction of hyperedges to edges. Defaults to None.

None
Source code in hypertorch/nn/conv.py
def __init__(
    self,
    in_channels: int,
    out_channels: int,
    bias: bool = True,
    use_batch_normalization: bool = False,
    drop_rate: float = 0.5,
    use_mediator: bool = False,
    is_last: bool = False,
    seed: int | None = None,
):
    """
    Initialize the HyperGCN convolution layer.

    Args:
        in_channels: The number of input channels.
        out_channels: The number of output channels.
        bias: If set to ``False``, the layer will not learn the bias parameter.
            Defaults to ``True``.
        use_batch_normalization: If set to ``True``, the layer will use batch normalization.
            Defaults to ``False``.
        drop_rate: If set to a positive number, the layer will use dropout. Defaults to ``0.5``.
        use_mediator: Whether to use mediator to transform the hyperedges to edges in the graph.
            Defaults to ``False``.
        is_last: If set to ``True``, the layer will not apply the final activation and
            dropout functions. Defaults to ``False``.
        seed: Optional random seed for the random reduction of hyperedges to edges.
            Defaults to ``None``.
    """
    super().__init__()
    self.is_last: bool = is_last
    self.use_mediator: bool = use_mediator
    self.batch_norm_1d: nn.BatchNorm1d | None = (
        nn.BatchNorm1d(out_channels) if use_batch_normalization else None
    )
    self.activation_fn: nn.ReLU = nn.ReLU(inplace=True)
    self.dropout: nn.Dropout = nn.Dropout(drop_rate)

    # θ is the learnable weight matrix (as in the HyperGCN paper),
    # it projects node features from in_channels to out_channels and
    # learns how to mix feature channels
    self.theta: nn.Linear = nn.Linear(in_channels, out_channels, bias=bias)

    self.seed: int | None = seed

forward(x, hyperedge_index, gcn_laplacian_matrix=None)

The forward function.

Parameters:

Name Type Description Default
x Tensor

Input node feature matrix. Size (num_nodes, in_channels).

required
hyperedge_index Tensor

Hyperedge indices representing the hypergraph structure. Size (2, num_hyperedges).

required
gcn_laplacian_matrix Tensor | None

Optional precomputed normalized GCN Laplacian matrix. Size (num_nodes, num_nodes). Defaults to None. If provided, it will be used directly for smoothing, so we can skip computing it from edge_index.

None

Returns:

Name Type Description
x Tensor

The output node feature matrix. Size (num_nodes, out_channels).

Source code in hypertorch/nn/conv.py
def forward(
    self,
    x: Tensor,
    hyperedge_index: Tensor,
    gcn_laplacian_matrix: Tensor | None = None,
) -> Tensor:
    """
    The forward function.

    Args:
        x: Input node feature matrix. Size ``(num_nodes, in_channels)``.
        hyperedge_index: Hyperedge indices representing the hypergraph structure.
            Size ``(2, num_hyperedges)``.
        gcn_laplacian_matrix: Optional precomputed normalized GCN Laplacian matrix.
            Size ``(num_nodes, num_nodes)``. Defaults to ``None``.
            If provided, it will be used directly for smoothing, so we can skip computing
            it from edge_index.

    Returns:
        x: The output node feature matrix. Size ``(num_nodes, out_channels)``.
    """
    x = self.theta(x)

    if gcn_laplacian_matrix is not None:
        x = Graph.smoothing_with_laplacian_matrix(x, gcn_laplacian_matrix)
    else:
        edge_index, edge_weights = HyperedgeIndex(
            hyperedge_index
        ).reduce_to_edge_index_on_random_direction(
            x=x,
            with_mediators=self.use_mediator,
            return_weights=True,
            seed=self.seed,
        )

        normalized_gcn_laplacian_matrix = EdgeIndex(
            edge_index=edge_index,
            edge_weights=edge_weights,
        ).get_sparse_normalized_gcn_laplacian(num_nodes=x.size(0))

        x = Graph.smoothing_with_laplacian_matrix(x, normalized_gcn_laplacian_matrix)

    if not self.is_last:
        x = self.activation_fn(x)
        if self.batch_norm_1d is not None:
            x = self.batch_norm_1d(x)
        x = self.dropout(x)

    return x

NHPRankingLoss

Bases: Module

Ranking loss that pushes positive hyperedges above sampled negatives.

Examples:

>>> logits = [2.0, 1.0, -1.0]
>>> labels = [1.0, 1.0, 0.0]
>>> loss = NHPRankingLoss()
>>> loss(logits, labels)
>>> loss.ndim
... 0
Source code in hypertorch/nn/loss.py
class NHPRankingLoss(nn.Module):
    """
    Ranking loss that pushes positive hyperedges above sampled negatives.

    Examples:
        >>> logits = [2.0, 1.0, -1.0]
        >>> labels = [1.0, 1.0, 0.0]
        >>> loss = NHPRankingLoss()
        >>> loss(logits, labels)
        >>> loss.ndim
        ... 0
    """

    def forward(self, logits: Tensor, labels: Tensor) -> Tensor:
        """
        Compute the ranking loss.

        Args:
            logits: Logit scores for each candidate hyperedge, of shape ``(num_hyperedges,)``.
            labels: Binary labels indicating positive (1) and negative (0) hyperedges, of shape
                ``(num_hyperedges,)``.

        Returns:
            loss: Scalar loss value.

        Raises:
            ValueError: If ``logits`` and ``labels`` do not contain both positive and
                negative hyperedges.
        """
        # Split logits by label as we need to compare positive scores against negative scores.
        # Example: logits = [2.0, 1.0, -1.0]
        #          labels = [1.0, 1.0, 0.0]
        #          -> positive_logits = [2.0, 1.0]
        #          -> negative_logits = [-1.0]
        positive_logits = logits[labels == 1]
        negative_logits = logits[labels == 0]

        positive_scores = torch.sigmoid(positive_logits)
        negative_scores = torch.sigmoid(negative_logits)
        if positive_scores.numel() == 0 or negative_scores.numel() == 0:
            raise ValueError("NHPRankingLoss requires both positive and negative hyperedges.")

        # Objective: enforce that each positive score is higher than the average negative score.
        # For each positive score pos_i:
        #   margin_i = mean(negative_scores) - pos_i
        # We interpret margin_i as follows:
        # - If pos_i > mean(negatives), then margin_i < 0    -> desirable
        # - If pos_i <= mean(negatives), then margin_i >= 0  -> violation
        margins = negative_scores.mean(dtype=torch.float) - positive_scores

        # Then softplus(margin_i):
        # - Is ~0 when margin_i is strongly negative (good ranking).
        # - Grows smoothly when margin_i > 0 (penalizing violations).
        # Final loss is the average over all positive samples.
        return F.softplus(margins).mean(dtype=torch.float)

forward(logits, labels)

Compute the ranking loss.

Parameters:

Name Type Description Default
logits Tensor

Logit scores for each candidate hyperedge, of shape (num_hyperedges,).

required
labels Tensor

Binary labels indicating positive (1) and negative (0) hyperedges, of shape (num_hyperedges,).

required

Returns:

Name Type Description
loss Tensor

Scalar loss value.

Raises:

Type Description
ValueError

If logits and labels do not contain both positive and negative hyperedges.

Source code in hypertorch/nn/loss.py
def forward(self, logits: Tensor, labels: Tensor) -> Tensor:
    """
    Compute the ranking loss.

    Args:
        logits: Logit scores for each candidate hyperedge, of shape ``(num_hyperedges,)``.
        labels: Binary labels indicating positive (1) and negative (0) hyperedges, of shape
            ``(num_hyperedges,)``.

    Returns:
        loss: Scalar loss value.

    Raises:
        ValueError: If ``logits`` and ``labels`` do not contain both positive and
            negative hyperedges.
    """
    # Split logits by label as we need to compare positive scores against negative scores.
    # Example: logits = [2.0, 1.0, -1.0]
    #          labels = [1.0, 1.0, 0.0]
    #          -> positive_logits = [2.0, 1.0]
    #          -> negative_logits = [-1.0]
    positive_logits = logits[labels == 1]
    negative_logits = logits[labels == 0]

    positive_scores = torch.sigmoid(positive_logits)
    negative_scores = torch.sigmoid(negative_logits)
    if positive_scores.numel() == 0 or negative_scores.numel() == 0:
        raise ValueError("NHPRankingLoss requires both positive and negative hyperedges.")

    # Objective: enforce that each positive score is higher than the average negative score.
    # For each positive score pos_i:
    #   margin_i = mean(negative_scores) - pos_i
    # We interpret margin_i as follows:
    # - If pos_i > mean(negatives), then margin_i < 0    -> desirable
    # - If pos_i <= mean(negatives), then margin_i >= 0  -> violation
    margins = negative_scores.mean(dtype=torch.float) - positive_scores

    # Then softplus(margin_i):
    # - Is ~0 when margin_i is strongly negative (good ranking).
    # - Grows smoothly when margin_i > 0 (penalizing violations).
    # Final loss is the average over all positive samples.
    return F.softplus(margins).mean(dtype=torch.float)

VilLainLoss

VilLain self-supervised loss formulas.

This class is intentionally stateless with respect to propagation. The VilLain model owns message passing and accumulation over steps and this class owns the per-step formulas for local and global loss,

Attributes:

Name Type Description
num_subspaces int

Number of virtual-label subspaces in each embedding.

labels_per_subspace int

Number of virtual labels in each subspace.

eps float

Numerical stability constant used in logarithms and cosine similarity.

Source code in hypertorch/nn/loss.py
class VilLainLoss:
    """
    VilLain self-supervised loss formulas.

    This class is intentionally stateless with respect to propagation.
    The VilLain model owns message passing and accumulation over steps
    and this class owns the per-step formulas for local and global loss,

    Attributes:
        num_subspaces: Number of virtual-label subspaces in each embedding.
        labels_per_subspace: Number of virtual labels in each subspace.
        eps: Numerical stability constant used in logarithms and cosine similarity.
    """

    def __init__(
        self,
        num_subspaces: int,
        labels_per_subspace: int,
        eps: float = 1e-12,
    ) -> None:
        """
        Initialize the VilLain loss helper.

        Args:
            num_subspaces: Number of virtual-label subspaces in each embedding.
            labels_per_subspace: Number of virtual labels in each subspace.
            eps: Numerical stability constant. Defaults to ``1e-12``.
        """
        super().__init__()
        self.num_subspaces: int = num_subspaces
        self.labels_per_subspace: int = labels_per_subspace
        self.eps: float = eps

    def local_loss(self, node_embeddings: Tensor, hyperedge_embeddings: Tensor) -> Tensor:
        """
        Compute the local entropy loss for one propagation step.

        Local loss is minimized to encourage propagated node and hyperedge distributions
        to become confident within each virtual-label subspace.

        Args:
            node_embeddings: Propagated node states of shape
                ``(num_nodes, num_subspaces * labels_per_subspace)``.
            hyperedge_embeddings: Propagated hyperedge states with the same channel dimension
                as ``node_embeddings``.

        Returns:
            loss: Scalar tensor containing node plus hyperedge entropy losses.
        """
        return self.entropy_loss(node_embeddings) + self.entropy_loss(hyperedge_embeddings)

    def global_loss(self, node_embeddings: Tensor, hyperedge_embeddings: Tensor) -> Tensor:
        """
        Compute global anti-collapse losses for one propagation step.

        Global loss combines negative global entropy, which encourages balanced label usage
        with a distinctiveness term that separates label columns inside each subspace.

        Args:
            node_embeddings: Propagated node states of shape
                ``(num_nodes, num_subspaces * labels_per_subspace)``.
            hyperedge_embeddings: Propagated hyperedge states with the same channel dimension
                as ``node_embeddings``.

        Returns:
            loss: Scalar tensor containing node plus hyperedge global losses.
        """
        return (
            self.balance_loss(node_embeddings)
            + self.distinctiveness_loss(node_embeddings)
            + self.balance_loss(hyperedge_embeddings)
            + self.distinctiveness_loss(hyperedge_embeddings)
        )

    def total_loss(self, local_loss: Tensor, global_loss: Tensor) -> Tensor:
        """
        Combine accumulated local and global VilLain losses.

        Args:
            local_loss: Accumulated local entropy loss.
            global_loss: Accumulated balance plus distinctiveness loss.

        Returns:
            loss: Scalar tensor to minimize.
        """
        return local_loss + global_loss

    def entropy_loss(self, x: Tensor) -> Tensor:
        """
        Compute mean entropy within each virtual-label subspace.

        Args:
            x: Flattened virtual-label probabilities of shape
                ``(num_items, num_subspaces * labels_per_subspace)``.

        Returns:
            loss: Scalar entropy loss.
        """
        if x.size(0) == 0:
            return x.sum(dtype=torch.float) * 0.0

        # Example: x.shape = (num_nodes, 8)
        #          -> probs.shape = (num_nodes, 4, 2)
        #          probs[0, 0] = [0.12, 0.88] is node/hyperedge item 0's
        #          virtual-label distribution in subspace 0.
        probs = x.view(-1, self.num_subspaces, self.labels_per_subspace)

        # With this, we induce structurally close nodes (or hyperedges)
        # to be assigned to the same label.
        # Example: probs.shape = (num_nodes, 4, 2)
        #          -> entropy.shape = (num_nodes, 4), one entropy per item and subspace
        entropy = -(probs * torch.log(probs + self.eps)).sum(dim=2, dtype=torch.float)
        return entropy.mean(dtype=torch.float)

    def balance_loss(self, x: Tensor) -> Tensor:
        """
        Compute negative entropy of global virtual-label usage.

        This term is minimized, so the negative sign makes optimization maximize entropy
        of average label usage and reduces collapse to one virtual label.

        Args:
            x: Flattened virtual-label probabilities of shape
                ``(num_items, num_subspaces * labels_per_subspace)``.

        Returns:
            loss: Scalar balance loss.
        """
        if x.size(0) == 0:
            return x.sum(dtype=torch.float) * 0.0

        # Example: with raw_embedding_dim=8, num_subspaces=4, labels_per_subspace=2:
        #          x.shape = (num_nodes, 8)
        #          -> probs.shape = (num_nodes, 4, 2)
        #          -> mean_probs.shape = (4, 2)
        #          mean_probs[0] = average usage of the two labels in subspace 0
        #          across all num_nodes nodes/hyperedges in this tensor.
        probs = x.view(-1, self.num_subspaces, self.labels_per_subspace)
        mean_probs = probs.mean(dim=0, dtype=torch.float)

        # Negative entropy to maximize global label diversity and prevents collapse.
        # Example: mean_probs[0] = [0.50, 0.50] has higher entropy
        #                   than mean_probs[0] = [0.99, 0.01].
        entropy = -(mean_probs * torch.log(mean_probs + self.eps)).sum(dim=1, dtype=torch.float)
        return -entropy.mean(dtype=torch.float)

    def distinctiveness_loss(self, x: Tensor) -> Tensor:
        """
        Penalize similar virtual-label columns inside each subspace.

        For every subspace, this compares all label columns across items with cosine similarity
        and applies a diagonal classification objective.
        The diagonal target encourages each label column to be most similar to itself
        and less similar to other labels.

        Args:
            x: Flattened virtual-label probabilities of shape
                ``(num_items, num_subspaces * labels_per_subspace)``.

        Returns:
            loss: Scalar distinctiveness loss.
        """
        if x.size(0) == 0:
            return x.sum(dtype=torch.float) * 0.0

        # Distinctiveness compares virtual-label columns inside each subspace across all items.
        # Example: with raw_embedding_dim=8, num_subspaces=4, labels_per_subspace=2:
        #          x.shape = (num_nodes, 8)
        #          -> probs.shape = (num_nodes, 4, 2)
        probs = x.view(-1, self.num_subspaces, self.labels_per_subspace)

        # Build all ordered pairs of virtual-label column ids inside a subspace.
        # Example with num_subspaces=4 and labels_per_subspace=2:
        #         idx_i = [0, 1, 0, 1], shape = (4,)
        #         idx_j = [0, 0, 1, 1], shape = (4,)
        #         pairs are (0,0), (1,0), (0,1), (1,1)
        idx_i = torch.arange(self.labels_per_subspace, dtype=torch.long, device=x.device).repeat(
            self.labels_per_subspace
        )
        idx_j = torch.arange(
            self.labels_per_subspace, dtype=torch.long, device=x.device
        ).repeat_interleave(self.labels_per_subspace)

        # Compare every virtual-label column against every other column.
        # Two different labels in the same subspace should not describe
        # the same pattern of nodes/hyperedges.
        # Example: with num_subspaces=4:
        #          probs[:, :, idx_i] and probs[:, :, idx_j] both have shape (4, 4, 4),
        #          where the last dimension enumerates the four ordered label pairs above
        #          # node/hyperedge 0's label probabilities for the four pairs
        #          probs[:, :, idx_i] == [[[p00, p01, p00, p01],
        #                 # node/hyperedge 1's label probabilities for the four pairs
        #                [p10, p11, p10, p11],
        #                 # node/hyperedge 2's label probabilities for the four pairs
        #                [p20, p21, p20, p21],
        #                 # node/hyperedge 3's label probabilities for the four pairs
        #                [p30, p31, p30, p31]],
        #               ...]
        #          # node/hyperedge 0's label probabilities for the four pairs
        #          probs[:, :, idx_j] == [[[p00, p00, p01, p01],
        #                 # node/hyperedge 1's label probabilities for the four pairs
        #                [p10, p10, p11, p11],
        #                 # node/hyperedge 2's label probabilities for the four pairs
        #                [p20, p20, p21, p21],
        #                 # node/hyperedge 3's label probabilities for the four pairs
        #                [p30, p30, p31, p31]],
        #                ..]
        #          F.cosine_similarity(..., dim=0) compares each pair across the 4 items,
        #           producing shape (4, 4)
        #          view(-1, 2, 2) restores one 2x2 similarity matrix per subspace,
        #            so shape becomes (4, 2, 2)
        similarity = F.cosine_similarity(
            probs[:, :, idx_i],
            probs[:, :, idx_j],
            dim=0,
            eps=self.eps,
        ).view(-1, self.labels_per_subspace, self.labels_per_subspace)

        # Turn each similarity row into a classification distribution and keep the
        # diagonal self-match probabilities.
        # Example: similarity[subspace 0].shape = (2, 2)
        #          - row 0 scores how label 0 matches labels [0, 1]
        #          - row 1 scores how label 1 matches labels [0, 1]
        #          -> assignment_probs has rows summing to 1 via softmax(dim=2)
        #          -> diagonal_probs keeps P(label 0 matches 0) and P(label 1 matches 1).
        # Minimizing -log(diagonal_probs) encourages each label column to be:
        # - Most similar to itself
        # - Less similar to other label columns
        assignment_probs = torch.softmax(similarity, dim=2, dtype=torch.float)
        diagonal_probs = torch.diagonal(assignment_probs, dim1=1, dim2=2)
        return torch.mean(-torch.log(diagonal_probs + self.eps), dtype=torch.float)

__init__(num_subspaces, labels_per_subspace, eps=1e-12)

Initialize the VilLain loss helper.

Parameters:

Name Type Description Default
num_subspaces int

Number of virtual-label subspaces in each embedding.

required
labels_per_subspace int

Number of virtual labels in each subspace.

required
eps float

Numerical stability constant. Defaults to 1e-12.

1e-12
Source code in hypertorch/nn/loss.py
def __init__(
    self,
    num_subspaces: int,
    labels_per_subspace: int,
    eps: float = 1e-12,
) -> None:
    """
    Initialize the VilLain loss helper.

    Args:
        num_subspaces: Number of virtual-label subspaces in each embedding.
        labels_per_subspace: Number of virtual labels in each subspace.
        eps: Numerical stability constant. Defaults to ``1e-12``.
    """
    super().__init__()
    self.num_subspaces: int = num_subspaces
    self.labels_per_subspace: int = labels_per_subspace
    self.eps: float = eps

local_loss(node_embeddings, hyperedge_embeddings)

Compute the local entropy loss for one propagation step.

Local loss is minimized to encourage propagated node and hyperedge distributions to become confident within each virtual-label subspace.

Parameters:

Name Type Description Default
node_embeddings Tensor

Propagated node states of shape (num_nodes, num_subspaces * labels_per_subspace).

required
hyperedge_embeddings Tensor

Propagated hyperedge states with the same channel dimension as node_embeddings.

required

Returns:

Name Type Description
loss Tensor

Scalar tensor containing node plus hyperedge entropy losses.

Source code in hypertorch/nn/loss.py
def local_loss(self, node_embeddings: Tensor, hyperedge_embeddings: Tensor) -> Tensor:
    """
    Compute the local entropy loss for one propagation step.

    Local loss is minimized to encourage propagated node and hyperedge distributions
    to become confident within each virtual-label subspace.

    Args:
        node_embeddings: Propagated node states of shape
            ``(num_nodes, num_subspaces * labels_per_subspace)``.
        hyperedge_embeddings: Propagated hyperedge states with the same channel dimension
            as ``node_embeddings``.

    Returns:
        loss: Scalar tensor containing node plus hyperedge entropy losses.
    """
    return self.entropy_loss(node_embeddings) + self.entropy_loss(hyperedge_embeddings)

global_loss(node_embeddings, hyperedge_embeddings)

Compute global anti-collapse losses for one propagation step.

Global loss combines negative global entropy, which encourages balanced label usage with a distinctiveness term that separates label columns inside each subspace.

Parameters:

Name Type Description Default
node_embeddings Tensor

Propagated node states of shape (num_nodes, num_subspaces * labels_per_subspace).

required
hyperedge_embeddings Tensor

Propagated hyperedge states with the same channel dimension as node_embeddings.

required

Returns:

Name Type Description
loss Tensor

Scalar tensor containing node plus hyperedge global losses.

Source code in hypertorch/nn/loss.py
def global_loss(self, node_embeddings: Tensor, hyperedge_embeddings: Tensor) -> Tensor:
    """
    Compute global anti-collapse losses for one propagation step.

    Global loss combines negative global entropy, which encourages balanced label usage
    with a distinctiveness term that separates label columns inside each subspace.

    Args:
        node_embeddings: Propagated node states of shape
            ``(num_nodes, num_subspaces * labels_per_subspace)``.
        hyperedge_embeddings: Propagated hyperedge states with the same channel dimension
            as ``node_embeddings``.

    Returns:
        loss: Scalar tensor containing node plus hyperedge global losses.
    """
    return (
        self.balance_loss(node_embeddings)
        + self.distinctiveness_loss(node_embeddings)
        + self.balance_loss(hyperedge_embeddings)
        + self.distinctiveness_loss(hyperedge_embeddings)
    )

total_loss(local_loss, global_loss)

Combine accumulated local and global VilLain losses.

Parameters:

Name Type Description Default
local_loss Tensor

Accumulated local entropy loss.

required
global_loss Tensor

Accumulated balance plus distinctiveness loss.

required

Returns:

Name Type Description
loss Tensor

Scalar tensor to minimize.

Source code in hypertorch/nn/loss.py
def total_loss(self, local_loss: Tensor, global_loss: Tensor) -> Tensor:
    """
    Combine accumulated local and global VilLain losses.

    Args:
        local_loss: Accumulated local entropy loss.
        global_loss: Accumulated balance plus distinctiveness loss.

    Returns:
        loss: Scalar tensor to minimize.
    """
    return local_loss + global_loss

entropy_loss(x)

Compute mean entropy within each virtual-label subspace.

Parameters:

Name Type Description Default
x Tensor

Flattened virtual-label probabilities of shape (num_items, num_subspaces * labels_per_subspace).

required

Returns:

Name Type Description
loss Tensor

Scalar entropy loss.

Source code in hypertorch/nn/loss.py
def entropy_loss(self, x: Tensor) -> Tensor:
    """
    Compute mean entropy within each virtual-label subspace.

    Args:
        x: Flattened virtual-label probabilities of shape
            ``(num_items, num_subspaces * labels_per_subspace)``.

    Returns:
        loss: Scalar entropy loss.
    """
    if x.size(0) == 0:
        return x.sum(dtype=torch.float) * 0.0

    # Example: x.shape = (num_nodes, 8)
    #          -> probs.shape = (num_nodes, 4, 2)
    #          probs[0, 0] = [0.12, 0.88] is node/hyperedge item 0's
    #          virtual-label distribution in subspace 0.
    probs = x.view(-1, self.num_subspaces, self.labels_per_subspace)

    # With this, we induce structurally close nodes (or hyperedges)
    # to be assigned to the same label.
    # Example: probs.shape = (num_nodes, 4, 2)
    #          -> entropy.shape = (num_nodes, 4), one entropy per item and subspace
    entropy = -(probs * torch.log(probs + self.eps)).sum(dim=2, dtype=torch.float)
    return entropy.mean(dtype=torch.float)

balance_loss(x)

Compute negative entropy of global virtual-label usage.

This term is minimized, so the negative sign makes optimization maximize entropy of average label usage and reduces collapse to one virtual label.

Parameters:

Name Type Description Default
x Tensor

Flattened virtual-label probabilities of shape (num_items, num_subspaces * labels_per_subspace).

required

Returns:

Name Type Description
loss Tensor

Scalar balance loss.

Source code in hypertorch/nn/loss.py
def balance_loss(self, x: Tensor) -> Tensor:
    """
    Compute negative entropy of global virtual-label usage.

    This term is minimized, so the negative sign makes optimization maximize entropy
    of average label usage and reduces collapse to one virtual label.

    Args:
        x: Flattened virtual-label probabilities of shape
            ``(num_items, num_subspaces * labels_per_subspace)``.

    Returns:
        loss: Scalar balance loss.
    """
    if x.size(0) == 0:
        return x.sum(dtype=torch.float) * 0.0

    # Example: with raw_embedding_dim=8, num_subspaces=4, labels_per_subspace=2:
    #          x.shape = (num_nodes, 8)
    #          -> probs.shape = (num_nodes, 4, 2)
    #          -> mean_probs.shape = (4, 2)
    #          mean_probs[0] = average usage of the two labels in subspace 0
    #          across all num_nodes nodes/hyperedges in this tensor.
    probs = x.view(-1, self.num_subspaces, self.labels_per_subspace)
    mean_probs = probs.mean(dim=0, dtype=torch.float)

    # Negative entropy to maximize global label diversity and prevents collapse.
    # Example: mean_probs[0] = [0.50, 0.50] has higher entropy
    #                   than mean_probs[0] = [0.99, 0.01].
    entropy = -(mean_probs * torch.log(mean_probs + self.eps)).sum(dim=1, dtype=torch.float)
    return -entropy.mean(dtype=torch.float)

distinctiveness_loss(x)

Penalize similar virtual-label columns inside each subspace.

For every subspace, this compares all label columns across items with cosine similarity and applies a diagonal classification objective. The diagonal target encourages each label column to be most similar to itself and less similar to other labels.

Parameters:

Name Type Description Default
x Tensor

Flattened virtual-label probabilities of shape (num_items, num_subspaces * labels_per_subspace).

required

Returns:

Name Type Description
loss Tensor

Scalar distinctiveness loss.

Source code in hypertorch/nn/loss.py
def distinctiveness_loss(self, x: Tensor) -> Tensor:
    """
    Penalize similar virtual-label columns inside each subspace.

    For every subspace, this compares all label columns across items with cosine similarity
    and applies a diagonal classification objective.
    The diagonal target encourages each label column to be most similar to itself
    and less similar to other labels.

    Args:
        x: Flattened virtual-label probabilities of shape
            ``(num_items, num_subspaces * labels_per_subspace)``.

    Returns:
        loss: Scalar distinctiveness loss.
    """
    if x.size(0) == 0:
        return x.sum(dtype=torch.float) * 0.0

    # Distinctiveness compares virtual-label columns inside each subspace across all items.
    # Example: with raw_embedding_dim=8, num_subspaces=4, labels_per_subspace=2:
    #          x.shape = (num_nodes, 8)
    #          -> probs.shape = (num_nodes, 4, 2)
    probs = x.view(-1, self.num_subspaces, self.labels_per_subspace)

    # Build all ordered pairs of virtual-label column ids inside a subspace.
    # Example with num_subspaces=4 and labels_per_subspace=2:
    #         idx_i = [0, 1, 0, 1], shape = (4,)
    #         idx_j = [0, 0, 1, 1], shape = (4,)
    #         pairs are (0,0), (1,0), (0,1), (1,1)
    idx_i = torch.arange(self.labels_per_subspace, dtype=torch.long, device=x.device).repeat(
        self.labels_per_subspace
    )
    idx_j = torch.arange(
        self.labels_per_subspace, dtype=torch.long, device=x.device
    ).repeat_interleave(self.labels_per_subspace)

    # Compare every virtual-label column against every other column.
    # Two different labels in the same subspace should not describe
    # the same pattern of nodes/hyperedges.
    # Example: with num_subspaces=4:
    #          probs[:, :, idx_i] and probs[:, :, idx_j] both have shape (4, 4, 4),
    #          where the last dimension enumerates the four ordered label pairs above
    #          # node/hyperedge 0's label probabilities for the four pairs
    #          probs[:, :, idx_i] == [[[p00, p01, p00, p01],
    #                 # node/hyperedge 1's label probabilities for the four pairs
    #                [p10, p11, p10, p11],
    #                 # node/hyperedge 2's label probabilities for the four pairs
    #                [p20, p21, p20, p21],
    #                 # node/hyperedge 3's label probabilities for the four pairs
    #                [p30, p31, p30, p31]],
    #               ...]
    #          # node/hyperedge 0's label probabilities for the four pairs
    #          probs[:, :, idx_j] == [[[p00, p00, p01, p01],
    #                 # node/hyperedge 1's label probabilities for the four pairs
    #                [p10, p10, p11, p11],
    #                 # node/hyperedge 2's label probabilities for the four pairs
    #                [p20, p20, p21, p21],
    #                 # node/hyperedge 3's label probabilities for the four pairs
    #                [p30, p30, p31, p31]],
    #                ..]
    #          F.cosine_similarity(..., dim=0) compares each pair across the 4 items,
    #           producing shape (4, 4)
    #          view(-1, 2, 2) restores one 2x2 similarity matrix per subspace,
    #            so shape becomes (4, 2, 2)
    similarity = F.cosine_similarity(
        probs[:, :, idx_i],
        probs[:, :, idx_j],
        dim=0,
        eps=self.eps,
    ).view(-1, self.labels_per_subspace, self.labels_per_subspace)

    # Turn each similarity row into a classification distribution and keep the
    # diagonal self-match probabilities.
    # Example: similarity[subspace 0].shape = (2, 2)
    #          - row 0 scores how label 0 matches labels [0, 1]
    #          - row 1 scores how label 1 matches labels [0, 1]
    #          -> assignment_probs has rows summing to 1 via softmax(dim=2)
    #          -> diagonal_probs keeps P(label 0 matches 0) and P(label 1 matches 1).
    # Minimizing -log(diagonal_probs) encourages each label column to be:
    # - Most similar to itself
    # - Less similar to other label columns
    assignment_probs = torch.softmax(similarity, dim=2, dtype=torch.float)
    diagonal_probs = torch.diagonal(assignment_probs, dim1=1, dim2=2)
    return torch.mean(-torch.log(diagonal_probs + self.eps), dtype=torch.float)

VilLainLossParts

Bases: TypedDict

Named VilLain self-supervised loss parts returned by VilLain.loss.

Attributes:

Name Type Description
local_loss Tensor

Sum of node and hyperedge local entropy losses over all training propagation steps.

global_loss Tensor

Sum of balance and distinctiveness losses over all training propagation steps.

Source code in hypertorch/nn/loss.py
class VilLainLossParts(TypedDict):
    """
    Named VilLain self-supervised loss parts returned by ``VilLain.loss``.

    Attributes:
        local_loss: Sum of node and hyperedge local entropy losses over all training
            propagation steps.
        global_loss: Sum of balance and distinctiveness losses over all training
            propagation steps.
    """

    local_loss: Tensor
    global_loss: Tensor

CommonNeighborsNodeScorer

Bases: NeighborScorer

Scorer for computing the Common Neighbors (CN) score for nodes.

Attributes:

Name Type Description
num_classes int

Number of node classes to score.

class_to_node_ids dict[int, list[int]]

Mapping from class IDs to labeled reference node IDs.

aggregation Literal['mean', 'min', 'sum']

Method used to aggregate pairwise node-reference scores per class.

exclude_self_reference bool

Whether a node should ignore itself when it also appears among labeled reference nodes.

Source code in hypertorch/nn/scorer.py
class CommonNeighborsNodeScorer(NeighborScorer):
    """
    Scorer for computing the Common Neighbors (CN) score for nodes.

    Attributes:
        num_classes: Number of node classes to score.
        class_to_node_ids: Mapping from class IDs to labeled reference node IDs.
        aggregation: Method used to aggregate pairwise node-reference scores per class.
        exclude_self_reference: Whether a node should ignore itself when it also
            appears among labeled reference nodes.
    """

    __DEFAULT_SCORE = 0.0

    def __init__(
        self,
        num_classes: int,
        class_to_node_ids: dict[int, list[int]] | None = None,
        aggregation: Literal["mean", "min", "sum"] = "sum",
        exclude_self_reference: bool = True,
    ) -> None:
        """
        Initialize the common-neighbors node scorer.

        Args:
            num_classes: Number of node classes to score.
            class_to_node_ids: Mapping from class IDs to labeled reference node IDs.
                Defaults to an empty mapping for each class.
            aggregation: Method used to aggregate pairwise node-reference scores
                per class. Defaults to ``"sum"``.
            exclude_self_reference: Whether a node should ignore itself when it also
                appears among labeled reference nodes. Defaults to ``True``.
        """
        self.num_classes: int = num_classes
        self.class_to_node_ids: dict[int, list[int]] = (
            class_to_node_ids
            if class_to_node_ids is not None
            else {class_id: [] for class_id in range(num_classes)}
        )
        self.aggregation: Literal["mean", "min", "sum"] = aggregation
        self.exclude_self_reference: bool = exclude_self_reference

    def score(
        self,
        candidate_nodes: list[int],
        candidate_to_neighbors: dict[int, Neighborhood],
    ) -> float:
        """
        Score one node against reference nodes from a single class.

        The first item in ``candidate_nodes`` is the node being classified.
        Remaining items are labeled reference nodes for one class.

        Args:
            candidate_nodes: Node IDs containing one target node followed by
                reference nodes for one class.
            candidate_to_neighbors: Mapping from node IDs to their neighborhoods.

        Returns:
            score: Aggregated node-class score.
        """
        if len(candidate_nodes) < 2:
            return self.__DEFAULT_SCORE

        node_id = candidate_nodes[0]
        reference_node_ids = candidate_nodes[1:]
        return self.__score_node_class(
            node_id=node_id,
            reference_node_ids=reference_node_ids,
            node_to_neighbors=candidate_to_neighbors,
        )

    def score_batch(
        self,
        candidate_nodes: Tensor,
        hyperedge_index: Tensor | None = None,
        node_to_neighbors: dict[int, Neighborhood] | None = None,
    ) -> Tensor:
        """
        Score nodes against labeled reference nodes grouped by class.
        A reference node is a training node labeled with a class ID, and the
        score of a node for each class is computed by aggregating the
        pairwise common-neighbor counts between the node being
        scored and each reference node for a class.

        Args:
            candidate_nodes: Tensor containing node IDs to score of shape ``(num_nodes,)``.
            hyperedge_index: Tensor of shape ``(2, num_hyperedges)`` containing the hyperedge index.
                Defaults to ``None``. If provided, it raises. a ValueError since the hyperedge index
                is not used in this scorer.
            node_to_neighbors: Mapping from node IDs to their training-world neighborhoods.

        Returns:
            scores: Raw node-class scores of shape ``(num_nodes, num_classes)``.

        Raises:
            ValueError: If ``hyperedge_index`` is provided, since it is not used in this scorer.
        """
        if hyperedge_index is not None:
            raise ValueError(
                "'hyperedge_index' must not be provided, as it is not used in this scorer."
            )

        node_to_neighbors = node_to_neighbors if node_to_neighbors is not None else {}

        # Convert the 1-D node tensor into Python IDs so each output row follows
        # the same node order as the input.
        # Example: data = tensor([3, 0, 2])
        #          -> data.tolist() = [3, 0, 2]
        #          -> output rows score node 3, then node 0, then node 2.
        node_ids = candidate_nodes.tolist()

        # Materialize the nested Python score matrix as a floating tensor.
        #                   class 0    1
        # Example: raw scores = [[0.0, 2.0],  node 0
        #                        [1.0, 0.0]]  node 1
        #          -> tensor shape = (2, 2), one row per node and one column per class.
        return torch.tensor(
            [
                [
                    # Score one node against all reference nodes for one class
                    # Example: node_id = 0, class_id = 1
                    #          -> reference_node_ids = [1, 2]
                    #          -> score = CN(node 0, reference nodes [1, 2])
                    #          -> output row = [CN(node 0, class 0), CN(node 0, class 1)]
                    self.__score_node_class(
                        node_id=int(node_id),
                        reference_node_ids=self.class_to_node_ids.get(class_id, []),
                        node_to_neighbors=node_to_neighbors,
                    )
                    for class_id in range(self.num_classes)
                ]
                for node_id in node_ids  # Score each node in the input tensor
            ],
            dtype=torch.float32,
            device=candidate_nodes.device,
        )

    def __score_node_class(
        self,
        node_id: int,
        reference_node_ids: list[int],
        node_to_neighbors: dict[int, Neighborhood],
    ) -> float:
        """
        Score one node against labeled reference nodes for a single class.
        The score is computed by aggregating the pairwise common-neighbor
        counts between the node being scored and each reference node.

        Args:
            node_id: Node ID to classify.
            reference_node_ids: Labeled reference node IDs for one class.
            node_to_neighbors: Mapping from node IDs to their neighborhoods.

        Returns:
            score: Aggregated node-class score.
        """
        pairwise_counts: list[int] = [
            self.__score_pair(node_id, reference_node_id, node_to_neighbors)
            for reference_node_id in reference_node_ids
            # If no self-reference is allowed, skip the reference node
            # if it is the same as the node being scored.
            if not self.exclude_self_reference or reference_node_id != node_id
        ]
        return _to_score_by_aggregation(
            pairwise_counts=pairwise_counts,
            aggregation=self.aggregation,
            default_score=self.__DEFAULT_SCORE,
        )

    def __score_pair(
        self,
        node_id: int,
        reference_node_id: int,
        node_to_neighbors: dict[int, Neighborhood],
    ) -> int:
        """
        Compute the pairwise common-neighbor count for two nodes.
        The count is the number of nodes that are neighbors of
        both the node being scored and the reference node.

        Args:
            node_id: Node ID to classify.
            reference_node_id: Labeled reference node ID for one class.
            node_to_neighbors: Mapping from node IDs to their neighborhoods.

        Returns:
            score: Pairwise common-neighbor count.
        """
        node_neighbors = node_to_neighbors.get(node_id, set())
        reference_neighbors = node_to_neighbors.get(reference_node_id, set())
        score = len(node_neighbors & reference_neighbors)
        return score

__init__(num_classes, class_to_node_ids=None, aggregation='sum', exclude_self_reference=True)

Initialize the common-neighbors node scorer.

Parameters:

Name Type Description Default
num_classes int

Number of node classes to score.

required
class_to_node_ids dict[int, list[int]] | None

Mapping from class IDs to labeled reference node IDs. Defaults to an empty mapping for each class.

None
aggregation Literal['mean', 'min', 'sum']

Method used to aggregate pairwise node-reference scores per class. Defaults to "sum".

'sum'
exclude_self_reference bool

Whether a node should ignore itself when it also appears among labeled reference nodes. Defaults to True.

True
Source code in hypertorch/nn/scorer.py
def __init__(
    self,
    num_classes: int,
    class_to_node_ids: dict[int, list[int]] | None = None,
    aggregation: Literal["mean", "min", "sum"] = "sum",
    exclude_self_reference: bool = True,
) -> None:
    """
    Initialize the common-neighbors node scorer.

    Args:
        num_classes: Number of node classes to score.
        class_to_node_ids: Mapping from class IDs to labeled reference node IDs.
            Defaults to an empty mapping for each class.
        aggregation: Method used to aggregate pairwise node-reference scores
            per class. Defaults to ``"sum"``.
        exclude_self_reference: Whether a node should ignore itself when it also
            appears among labeled reference nodes. Defaults to ``True``.
    """
    self.num_classes: int = num_classes
    self.class_to_node_ids: dict[int, list[int]] = (
        class_to_node_ids
        if class_to_node_ids is not None
        else {class_id: [] for class_id in range(num_classes)}
    )
    self.aggregation: Literal["mean", "min", "sum"] = aggregation
    self.exclude_self_reference: bool = exclude_self_reference

score(candidate_nodes, candidate_to_neighbors)

Score one node against reference nodes from a single class.

The first item in candidate_nodes is the node being classified. Remaining items are labeled reference nodes for one class.

Parameters:

Name Type Description Default
candidate_nodes list[int]

Node IDs containing one target node followed by reference nodes for one class.

required
candidate_to_neighbors dict[int, Neighborhood]

Mapping from node IDs to their neighborhoods.

required

Returns:

Name Type Description
score float

Aggregated node-class score.

Source code in hypertorch/nn/scorer.py
def score(
    self,
    candidate_nodes: list[int],
    candidate_to_neighbors: dict[int, Neighborhood],
) -> float:
    """
    Score one node against reference nodes from a single class.

    The first item in ``candidate_nodes`` is the node being classified.
    Remaining items are labeled reference nodes for one class.

    Args:
        candidate_nodes: Node IDs containing one target node followed by
            reference nodes for one class.
        candidate_to_neighbors: Mapping from node IDs to their neighborhoods.

    Returns:
        score: Aggregated node-class score.
    """
    if len(candidate_nodes) < 2:
        return self.__DEFAULT_SCORE

    node_id = candidate_nodes[0]
    reference_node_ids = candidate_nodes[1:]
    return self.__score_node_class(
        node_id=node_id,
        reference_node_ids=reference_node_ids,
        node_to_neighbors=candidate_to_neighbors,
    )

score_batch(candidate_nodes, hyperedge_index=None, node_to_neighbors=None)

Score nodes against labeled reference nodes grouped by class. A reference node is a training node labeled with a class ID, and the score of a node for each class is computed by aggregating the pairwise common-neighbor counts between the node being scored and each reference node for a class.

Parameters:

Name Type Description Default
candidate_nodes Tensor

Tensor containing node IDs to score of shape (num_nodes,).

required
hyperedge_index Tensor | None

Tensor of shape (2, num_hyperedges) containing the hyperedge index. Defaults to None. If provided, it raises. a ValueError since the hyperedge index is not used in this scorer.

None
node_to_neighbors dict[int, Neighborhood] | None

Mapping from node IDs to their training-world neighborhoods.

None

Returns:

Name Type Description
scores Tensor

Raw node-class scores of shape (num_nodes, num_classes).

Raises:

Type Description
ValueError

If hyperedge_index is provided, since it is not used in this scorer.

Source code in hypertorch/nn/scorer.py
def score_batch(
    self,
    candidate_nodes: Tensor,
    hyperedge_index: Tensor | None = None,
    node_to_neighbors: dict[int, Neighborhood] | None = None,
) -> Tensor:
    """
    Score nodes against labeled reference nodes grouped by class.
    A reference node is a training node labeled with a class ID, and the
    score of a node for each class is computed by aggregating the
    pairwise common-neighbor counts between the node being
    scored and each reference node for a class.

    Args:
        candidate_nodes: Tensor containing node IDs to score of shape ``(num_nodes,)``.
        hyperedge_index: Tensor of shape ``(2, num_hyperedges)`` containing the hyperedge index.
            Defaults to ``None``. If provided, it raises. a ValueError since the hyperedge index
            is not used in this scorer.
        node_to_neighbors: Mapping from node IDs to their training-world neighborhoods.

    Returns:
        scores: Raw node-class scores of shape ``(num_nodes, num_classes)``.

    Raises:
        ValueError: If ``hyperedge_index`` is provided, since it is not used in this scorer.
    """
    if hyperedge_index is not None:
        raise ValueError(
            "'hyperedge_index' must not be provided, as it is not used in this scorer."
        )

    node_to_neighbors = node_to_neighbors if node_to_neighbors is not None else {}

    # Convert the 1-D node tensor into Python IDs so each output row follows
    # the same node order as the input.
    # Example: data = tensor([3, 0, 2])
    #          -> data.tolist() = [3, 0, 2]
    #          -> output rows score node 3, then node 0, then node 2.
    node_ids = candidate_nodes.tolist()

    # Materialize the nested Python score matrix as a floating tensor.
    #                   class 0    1
    # Example: raw scores = [[0.0, 2.0],  node 0
    #                        [1.0, 0.0]]  node 1
    #          -> tensor shape = (2, 2), one row per node and one column per class.
    return torch.tensor(
        [
            [
                # Score one node against all reference nodes for one class
                # Example: node_id = 0, class_id = 1
                #          -> reference_node_ids = [1, 2]
                #          -> score = CN(node 0, reference nodes [1, 2])
                #          -> output row = [CN(node 0, class 0), CN(node 0, class 1)]
                self.__score_node_class(
                    node_id=int(node_id),
                    reference_node_ids=self.class_to_node_ids.get(class_id, []),
                    node_to_neighbors=node_to_neighbors,
                )
                for class_id in range(self.num_classes)
            ]
            for node_id in node_ids  # Score each node in the input tensor
        ],
        dtype=torch.float32,
        device=candidate_nodes.device,
    )

__score_node_class(node_id, reference_node_ids, node_to_neighbors)

Score one node against labeled reference nodes for a single class. The score is computed by aggregating the pairwise common-neighbor counts between the node being scored and each reference node.

Parameters:

Name Type Description Default
node_id int

Node ID to classify.

required
reference_node_ids list[int]

Labeled reference node IDs for one class.

required
node_to_neighbors dict[int, Neighborhood]

Mapping from node IDs to their neighborhoods.

required

Returns:

Name Type Description
score float

Aggregated node-class score.

Source code in hypertorch/nn/scorer.py
def __score_node_class(
    self,
    node_id: int,
    reference_node_ids: list[int],
    node_to_neighbors: dict[int, Neighborhood],
) -> float:
    """
    Score one node against labeled reference nodes for a single class.
    The score is computed by aggregating the pairwise common-neighbor
    counts between the node being scored and each reference node.

    Args:
        node_id: Node ID to classify.
        reference_node_ids: Labeled reference node IDs for one class.
        node_to_neighbors: Mapping from node IDs to their neighborhoods.

    Returns:
        score: Aggregated node-class score.
    """
    pairwise_counts: list[int] = [
        self.__score_pair(node_id, reference_node_id, node_to_neighbors)
        for reference_node_id in reference_node_ids
        # If no self-reference is allowed, skip the reference node
        # if it is the same as the node being scored.
        if not self.exclude_self_reference or reference_node_id != node_id
    ]
    return _to_score_by_aggregation(
        pairwise_counts=pairwise_counts,
        aggregation=self.aggregation,
        default_score=self.__DEFAULT_SCORE,
    )

__score_pair(node_id, reference_node_id, node_to_neighbors)

Compute the pairwise common-neighbor count for two nodes. The count is the number of nodes that are neighbors of both the node being scored and the reference node.

Parameters:

Name Type Description Default
node_id int

Node ID to classify.

required
reference_node_id int

Labeled reference node ID for one class.

required
node_to_neighbors dict[int, Neighborhood]

Mapping from node IDs to their neighborhoods.

required

Returns:

Name Type Description
score int

Pairwise common-neighbor count.

Source code in hypertorch/nn/scorer.py
def __score_pair(
    self,
    node_id: int,
    reference_node_id: int,
    node_to_neighbors: dict[int, Neighborhood],
) -> int:
    """
    Compute the pairwise common-neighbor count for two nodes.
    The count is the number of nodes that are neighbors of
    both the node being scored and the reference node.

    Args:
        node_id: Node ID to classify.
        reference_node_id: Labeled reference node ID for one class.
        node_to_neighbors: Mapping from node IDs to their neighborhoods.

    Returns:
        score: Pairwise common-neighbor count.
    """
    node_neighbors = node_to_neighbors.get(node_id, set())
    reference_neighbors = node_to_neighbors.get(reference_node_id, set())
    score = len(node_neighbors & reference_neighbors)
    return score

CommonNeighborsHyperedgeScorer

Bases: NeighborScorer

Scorer for computing the Common Neighbors (CN) score for hyperedges.

Attributes:

Name Type Description
aggregation Literal['mean', 'min', 'sum']

Method to aggregate node embeddings per hyperedge. Can be one of "mean", "min", or "sum".

Source code in hypertorch/nn/scorer.py
class CommonNeighborsHyperedgeScorer(NeighborScorer):
    """
    Scorer for computing the Common Neighbors (CN) score for hyperedges.

    Attributes:
        aggregation: Method to aggregate node embeddings per hyperedge. Can be one of
            ``"mean"``, ``"min"``, or ``"sum"``.
    """

    __DEFAULT_SCORE = 0.0

    def __init__(self, aggregation: Literal["mean", "min", "sum"]) -> None:
        """
        Initialize the common-neighbors scorer.

        Args:
            aggregation: Method used to aggregate pairwise common-neighbor counts.
        """
        self.aggregation: Literal["mean", "min", "sum"] = aggregation

    def score(
        self,
        candidate_nodes: list[int],
        candidate_to_neighbors: dict[int, Neighborhood],
    ) -> float:
        """
        Compute the CN score for a single candidate hyperedge.

        Args:
            candidate_nodes: List of node IDs forming the candidate hyperedge.
                If less than 2 nodes are provided, the function returns a default score of ``0.0``.
            candidate_to_neighbors: Mapping from node IDs to their set of neighbors.

        Returns:
            score: The aggregated common neighbors score.
        """
        if len(candidate_nodes) < 2:
            return self.__DEFAULT_SCORE

        pairwise_counts: list[int] = []
        candidates_tensor = torch.tensor(candidate_nodes, dtype=torch.long)

        # Example: candidate_nodes = [1, 2, 3]
        #          -> compute common neighbors for pairs (1, 2), (1, 3), and (2, 3)
        for u, v in torch.combinations(candidates_tensor, 2):
            neighbors_u: Neighborhood = candidate_to_neighbors.get(u.item(), set())
            neighbors_v: Neighborhood = candidate_to_neighbors.get(v.item(), set())

            common_neighbors = neighbors_u & neighbors_v
            pairwise_counts.append(len(common_neighbors))

        return _to_score_by_aggregation(
            pairwise_counts=pairwise_counts,
            aggregation=self.aggregation,
            default_score=self.__DEFAULT_SCORE,
        )

    def score_batch(
        self,
        candidate_nodes: Tensor,
        hyperedge_index: Tensor | None = None,
        node_to_neighbors: dict[int, Neighborhood] | None = None,
    ) -> Tensor:
        """
        Score a batch of hyperedges.

        Args:
            candidate_nodes: Tensor containing gloal node IDs so that nodes in the batch
                can be scored by mapping them to the ID space of the training hypergraph.
                The tensor shape is ``(num_nodes,)``.
            hyperedge_index: Tensor containing the hyperedge index. Defaults to ``None``.
            node_to_neighbors: Optional precomputed node to neighborhood mapping.
                Defaults to ``None``.
            node_to_neighbors: Optional precomputed node to neighborhood mapping.
                If ``None``, it will be computed from ``data``.
                Defaults to ``None``.

        Returns:
            scores: A 1-D tensor of shape ``(num_hyperedges,)`` with the CN score
                or each hyperedge.
        """
        if hyperedge_index is None:
            raise ValueError("'hyperedge_index' must be provided.")

        hyperedge_index_wrapper = HyperedgeIndex(hyperedge_index).to_global(candidate_nodes)

        if node_to_neighbors is None:
            node_to_neighbors = Hypergraph.from_hyperedge_index(
                hyperedge_index=hyperedge_index_wrapper.item,
            ).neighbors_of_all()

        scores: list[float] = []
        for hyperedge_id in range(hyperedge_index_wrapper.num_hyperedges):
            node_ids = hyperedge_index_wrapper.nodes_in(hyperedge_id)
            hyperedge_score = self.score(node_ids, node_to_neighbors)
            scores.append(hyperedge_score)

        return torch.tensor(scores, dtype=torch.float32, device=hyperedge_index.device)

__init__(aggregation)

Initialize the common-neighbors scorer.

Parameters:

Name Type Description Default
aggregation Literal['mean', 'min', 'sum']

Method used to aggregate pairwise common-neighbor counts.

required
Source code in hypertorch/nn/scorer.py
def __init__(self, aggregation: Literal["mean", "min", "sum"]) -> None:
    """
    Initialize the common-neighbors scorer.

    Args:
        aggregation: Method used to aggregate pairwise common-neighbor counts.
    """
    self.aggregation: Literal["mean", "min", "sum"] = aggregation

score(candidate_nodes, candidate_to_neighbors)

Compute the CN score for a single candidate hyperedge.

Parameters:

Name Type Description Default
candidate_nodes list[int]

List of node IDs forming the candidate hyperedge. If less than 2 nodes are provided, the function returns a default score of 0.0.

required
candidate_to_neighbors dict[int, Neighborhood]

Mapping from node IDs to their set of neighbors.

required

Returns:

Name Type Description
score float

The aggregated common neighbors score.

Source code in hypertorch/nn/scorer.py
def score(
    self,
    candidate_nodes: list[int],
    candidate_to_neighbors: dict[int, Neighborhood],
) -> float:
    """
    Compute the CN score for a single candidate hyperedge.

    Args:
        candidate_nodes: List of node IDs forming the candidate hyperedge.
            If less than 2 nodes are provided, the function returns a default score of ``0.0``.
        candidate_to_neighbors: Mapping from node IDs to their set of neighbors.

    Returns:
        score: The aggregated common neighbors score.
    """
    if len(candidate_nodes) < 2:
        return self.__DEFAULT_SCORE

    pairwise_counts: list[int] = []
    candidates_tensor = torch.tensor(candidate_nodes, dtype=torch.long)

    # Example: candidate_nodes = [1, 2, 3]
    #          -> compute common neighbors for pairs (1, 2), (1, 3), and (2, 3)
    for u, v in torch.combinations(candidates_tensor, 2):
        neighbors_u: Neighborhood = candidate_to_neighbors.get(u.item(), set())
        neighbors_v: Neighborhood = candidate_to_neighbors.get(v.item(), set())

        common_neighbors = neighbors_u & neighbors_v
        pairwise_counts.append(len(common_neighbors))

    return _to_score_by_aggregation(
        pairwise_counts=pairwise_counts,
        aggregation=self.aggregation,
        default_score=self.__DEFAULT_SCORE,
    )

score_batch(candidate_nodes, hyperedge_index=None, node_to_neighbors=None)

Score a batch of hyperedges.

Parameters:

Name Type Description Default
candidate_nodes Tensor

Tensor containing gloal node IDs so that nodes in the batch can be scored by mapping them to the ID space of the training hypergraph. The tensor shape is (num_nodes,).

required
hyperedge_index Tensor | None

Tensor containing the hyperedge index. Defaults to None.

None
node_to_neighbors dict[int, Neighborhood] | None

Optional precomputed node to neighborhood mapping. Defaults to None.

None
node_to_neighbors dict[int, Neighborhood] | None

Optional precomputed node to neighborhood mapping. If None, it will be computed from data. Defaults to None.

None

Returns:

Name Type Description
scores Tensor

A 1-D tensor of shape (num_hyperedges,) with the CN score or each hyperedge.

Source code in hypertorch/nn/scorer.py
def score_batch(
    self,
    candidate_nodes: Tensor,
    hyperedge_index: Tensor | None = None,
    node_to_neighbors: dict[int, Neighborhood] | None = None,
) -> Tensor:
    """
    Score a batch of hyperedges.

    Args:
        candidate_nodes: Tensor containing gloal node IDs so that nodes in the batch
            can be scored by mapping them to the ID space of the training hypergraph.
            The tensor shape is ``(num_nodes,)``.
        hyperedge_index: Tensor containing the hyperedge index. Defaults to ``None``.
        node_to_neighbors: Optional precomputed node to neighborhood mapping.
            Defaults to ``None``.
        node_to_neighbors: Optional precomputed node to neighborhood mapping.
            If ``None``, it will be computed from ``data``.
            Defaults to ``None``.

    Returns:
        scores: A 1-D tensor of shape ``(num_hyperedges,)`` with the CN score
            or each hyperedge.
    """
    if hyperedge_index is None:
        raise ValueError("'hyperedge_index' must be provided.")

    hyperedge_index_wrapper = HyperedgeIndex(hyperedge_index).to_global(candidate_nodes)

    if node_to_neighbors is None:
        node_to_neighbors = Hypergraph.from_hyperedge_index(
            hyperedge_index=hyperedge_index_wrapper.item,
        ).neighbors_of_all()

    scores: list[float] = []
    for hyperedge_id in range(hyperedge_index_wrapper.num_hyperedges):
        node_ids = hyperedge_index_wrapper.nodes_in(hyperedge_id)
        hyperedge_score = self.score(node_ids, node_to_neighbors)
        scores.append(hyperedge_score)

    return torch.tensor(scores, dtype=torch.float32, device=hyperedge_index.device)

NeighborScorer

Bases: ABC

Abstract base class for neighbor scorers.

Source code in hypertorch/nn/scorer.py
class NeighborScorer(ABC):
    """
    Abstract base class for neighbor scorers.
    """

    @abstractmethod
    def score(
        self,
        candidate_nodes: list[int],
        candidate_to_neighbors: dict[int, Neighborhood],
    ) -> float:
        """
        Score a single candidate hyperedge.

        Args:
            candidate_nodes: Node IDs in the candidate hyperedge.
            candidate_to_neighbors: Mapping from node IDs to their neighborhoods.

        Returns:
            score: Candidate score.

        Raises:
            NotImplementedError: If the method is not implemented by a subclass.
        """
        raise NotImplementedError

    @abstractmethod
    def score_batch(
        self,
        candidate_nodes: Tensor,
        hyperedge_index: Tensor | None = None,
        node_to_neighbors: dict[int, Neighborhood] | None = None,
    ) -> Tensor:
        """
        Score a batch of hyperedges or nodes.

        Args:
            candidate_nodes: Tensor containing node IDs to score of shape ``(num_nodes,)``.
            hyperedge_index: Optional tensor containing the hyperedge index. Defaults to ``None``.
            node_to_neighbors: Optional precomputed node-neighborhood mapping. Defaults to ``None``.

        Returns:
            scores: Score tensor with per-hyperedge or per-node scores.

        Raises:
            NotImplementedError: If the method is not implemented by a subclass.
        """
        raise NotImplementedError

score(candidate_nodes, candidate_to_neighbors) abstractmethod

Score a single candidate hyperedge.

Parameters:

Name Type Description Default
candidate_nodes list[int]

Node IDs in the candidate hyperedge.

required
candidate_to_neighbors dict[int, Neighborhood]

Mapping from node IDs to their neighborhoods.

required

Returns:

Name Type Description
score float

Candidate score.

Raises:

Type Description
NotImplementedError

If the method is not implemented by a subclass.

Source code in hypertorch/nn/scorer.py
@abstractmethod
def score(
    self,
    candidate_nodes: list[int],
    candidate_to_neighbors: dict[int, Neighborhood],
) -> float:
    """
    Score a single candidate hyperedge.

    Args:
        candidate_nodes: Node IDs in the candidate hyperedge.
        candidate_to_neighbors: Mapping from node IDs to their neighborhoods.

    Returns:
        score: Candidate score.

    Raises:
        NotImplementedError: If the method is not implemented by a subclass.
    """
    raise NotImplementedError

score_batch(candidate_nodes, hyperedge_index=None, node_to_neighbors=None) abstractmethod

Score a batch of hyperedges or nodes.

Parameters:

Name Type Description Default
candidate_nodes Tensor

Tensor containing node IDs to score of shape (num_nodes,).

required
hyperedge_index Tensor | None

Optional tensor containing the hyperedge index. Defaults to None.

None
node_to_neighbors dict[int, Neighborhood] | None

Optional precomputed node-neighborhood mapping. Defaults to None.

None

Returns:

Name Type Description
scores Tensor

Score tensor with per-hyperedge or per-node scores.

Raises:

Type Description
NotImplementedError

If the method is not implemented by a subclass.

Source code in hypertorch/nn/scorer.py
@abstractmethod
def score_batch(
    self,
    candidate_nodes: Tensor,
    hyperedge_index: Tensor | None = None,
    node_to_neighbors: dict[int, Neighborhood] | None = None,
) -> Tensor:
    """
    Score a batch of hyperedges or nodes.

    Args:
        candidate_nodes: Tensor containing node IDs to score of shape ``(num_nodes,)``.
        hyperedge_index: Optional tensor containing the hyperedge index. Defaults to ``None``.
        node_to_neighbors: Optional precomputed node-neighborhood mapping. Defaults to ``None``.

    Returns:
        scores: Score tensor with per-hyperedge or per-node scores.

    Raises:
        NotImplementedError: If the method is not implemented by a subclass.
    """
    raise NotImplementedError