Skip to content

Commit

Permalink
Update NeighborhoodComplexLifting to work with new design
Browse files Browse the repository at this point in the history
  • Loading branch information
luisfpereira committed Jan 24, 2025
1 parent 9a93e2b commit ac22a74
Show file tree
Hide file tree
Showing 2 changed files with 56 additions and 32 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
import torch
import torch_geometric

from modules.transforms.liftings.graph2simplicial.neighborhood_lifting import (
from topobenchmark.transforms.liftings import (
Graph2SimplicialLiftingTransform,
NeighborhoodComplexLifting,
)

Expand All @@ -22,24 +23,29 @@ def create_test_graph():
)


class TestSimplicialCliqueLifting:
class TestNeighborhoodComplexLifting:
"""Test the SimplicialCliqueLifting class."""

def setup_method(self):
# Load the graph
self.data = create_test_graph() # load_manual_graph()

# Initialise the SimplicialCliqueLifting class
self.lifting_signed = NeighborhoodComplexLifting(signed=True)
self.lifting_unsigned = NeighborhoodComplexLifting(signed=False)
lifting_map = NeighborhoodComplexLifting()

self.lifting_signed = Graph2SimplicialLiftingTransform(
lifting=lifting_map, signed=True, data2domain="Identity"
)
self.lifting_unsigned = Graph2SimplicialLiftingTransform(
lifting=lifting_map, signed=False, data2domain="Identity"
)

def test_lift_topology(self):
"""Test the lift_topology method."""

# Test the lift_topology method
lifted_data_signed = self.lifting_signed.forward(self.data.clone())
lifted_data_unsigned = self.lifting_unsigned.forward(self.data.clone())
print(lifted_data_signed)

expected_incidence_1 = torch.tensor(
[
Expand All @@ -52,7 +58,8 @@ def test_lift_topology(self):
)

assert (
abs(expected_incidence_1) == lifted_data_unsigned.incidence_1.to_dense()
abs(expected_incidence_1)
== lifted_data_unsigned.incidence_1.to_dense()
).all(), f"Something is wrong with unsigned incidence_1 (nodes to edges).\n{abs(expected_incidence_1) - lifted_data_unsigned.incidence_1.to_dense()}"
assert (
expected_incidence_1 == lifted_data_signed.incidence_1.to_dense()
Expand All @@ -72,7 +79,8 @@ def test_lift_topology(self):
)

assert (
abs(expected_incidence_2) == lifted_data_unsigned.incidence_2.to_dense()
abs(expected_incidence_2)
== lifted_data_unsigned.incidence_2.to_dense()
).all(), f"Something is wrong with unsigned incidence_2 (edges to triangles).\n{abs(expected_incidence_2) - lifted_data_unsigned.incidence_2.to_dense()}"
assert (
expected_incidence_2 == lifted_data_signed.incidence_2.to_dense()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,37 +1,56 @@
r"""This module implements the neighborhood/Dowker lifting.
This lifting constructs a neighborhood simplicial complex as it is
`usually defined <https://mathworld.wolfram.com/NeighborhoodComplex.html>`_
in the field of topological combinatorics.
In this lifting, for each vertex in the original graph, its neighborhood is
the subset of adjacent vertices, and the simplices are these subsets.
That is, if :math:`G = (V, E)` is a graph, then its neighborhood complex
:math:`N(G)` is a simplicial complex with the vertex set :math:`V` and
simplices given by subsets :math:`A \subseteq V` such, that
:math:`\forall a \in A ; \exists v:(a, v) \in E`.
That is, say, 3 vertices form a simplex iff there's another vertex which
is adjacent to each of these 3 vertices.
This construction differs from
`another lifting <https://github.com/pyt-team/challenge-icml-2024/pull/5>`_
with the similar naming.
The difference is, for example, that in this construction the edges of an
original graph doesn't present as the edges in the simplicial complex.
This lifting is a
`Dowker construction <https://ncatlab.org/nlab/show/Dowker%27s+theorem>`_
since an edge between two vertices in the graph can be considered as a
symmetric binary relation between these vertices.
"""

import torch
import torch_geometric
from toponetx.classes import SimplicialComplex

from modules.transforms.liftings.graph2simplicial.base import Graph2SimplicialLifting

from topobenchmark.transforms.liftings.base import LiftingMap

class NeighborhoodComplexLifting(Graph2SimplicialLifting):
r"""Lifts graphs to simplicial complex domain by constructing the neighborhood complex[1].

Parameters
----------
**kwargs : optional
Additional arguments for the class.
"""
class NeighborhoodComplexLifting(LiftingMap):
r"""Lifts graphs to simplicial complex domain by constructing the neighborhood complex."""

def __init__(self, **kwargs):
self.contains_edge_attr = False
super().__init__(**kwargs)

def lift_topology(self, data: torch_geometric.data.Data) -> dict:
r"""Lifts the topology of a graph to a simplicial complex by identifying the cliques as k-simplices.
def lift(self, domain):
r"""Lift the topology to simplicial complex domain.
Parameters
----------
data : torch_geometric.data.Data
domain : torch_geometric.data.Data
The input data to be lifted.
Returns
-------
dict
The lifted topology.
toponetx.SimplicialComplex
Lifted simplicial complex.
"""
undir_edge_index = torch_geometric.utils.to_undirected(data.edge_index)
undir_edge_index = torch_geometric.utils.to_undirected(
domain.edge_index
)

simplices = [
set(
Expand All @@ -41,12 +60,9 @@ def lift_topology(self, data: torch_geometric.data.Data) -> dict:
for i in torch.unique(undir_edge_index[0])
]

node_features = {i: data.x[i, :] for i in range(data.x.shape[0])}
node_features = {i: domain.x[i, :] for i in range(domain.x.shape[0])}

simplicial_complex = SimplicialComplex(simplices)
self.complex_dim = simplicial_complex.dim
simplicial_complex.set_simplex_attributes(node_features, name="features")

graph = simplicial_complex.graph_skeleton()
simplicial_complex.set_simplex_attributes(node_features, name="x")

return self._get_lifted_topology(simplicial_complex, graph)
return simplicial_complex

0 comments on commit ac22a74

Please sign in to comment.