diff --git a/test/transforms/liftings/simplicial/test_NeighborhoodComplexLifting.py b/test/transforms/liftings/simplicial/test_NeighborhoodComplexLifting.py index 1e3909d2..61a0361d 100644 --- a/test/transforms/liftings/simplicial/test_NeighborhoodComplexLifting.py +++ b/test/transforms/liftings/simplicial/test_NeighborhoodComplexLifting.py @@ -3,7 +3,8 @@ import torch import torch_geometric -from modules.transforms.liftings.graph2simplicial.neighborhood_lifting import ( +from topobenchmark.transforms.liftings import ( + Graph2SimplicialLiftingTransform, NeighborhoodComplexLifting, ) @@ -22,7 +23,7 @@ def create_test_graph(): ) -class TestSimplicialCliqueLifting: +class TestNeighborhoodComplexLifting: """Test the SimplicialCliqueLifting class.""" def setup_method(self): @@ -30,8 +31,14 @@ def setup_method(self): 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.""" @@ -39,7 +46,6 @@ def test_lift_topology(self): # 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( [ @@ -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() @@ -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() diff --git a/topobenchmark/transforms/liftings/graph2simplicial/neighborhood_lifting.py b/topobenchmark/transforms/liftings/graph2simplicial/neighborhood_lifting.py index 99bcb2b5..9a107ae7 100644 --- a/topobenchmark/transforms/liftings/graph2simplicial/neighborhood_lifting.py +++ b/topobenchmark/transforms/liftings/graph2simplicial/neighborhood_lifting.py @@ -1,37 +1,56 @@ +r"""This module implements the neighborhood/Dowker lifting. + +This lifting constructs a neighborhood simplicial complex as it is +`usually defined `_ +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 `_ +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 `_ +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( @@ -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