Skip to content

Commit

Permalink
Update SimplicialLineLifting to work with new design
Browse files Browse the repository at this point in the history
  • Loading branch information
luisfpereira committed Jan 23, 2025
1 parent 34a4ec5 commit 54efc5c
Show file tree
Hide file tree
Showing 2 changed files with 88 additions and 39 deletions.
29 changes: 22 additions & 7 deletions test/transforms/liftings/simplicial/test_SimplicialLineLifting.py
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.line_lifting import (
from topobenchmark.transforms.liftings import (
Graph2SimplicialLiftingTransform,
SimplicialLineLifting,
)

Expand Down Expand Up @@ -31,9 +32,17 @@ def setup_method(self):
# Load the graph
self.data = create_test_graph() # load_manual_graph()

lifting_map = SimplicialLineLifting()

# Initialise the SimplicialCliqueLifting class
self.lifting_signed = SimplicialLineLifting(signed=True)
self.lifting_unsigned = SimplicialLineLifting(signed=False)
self.lifting_signed = Graph2SimplicialLiftingTransform(
lifting=lifting_map,
signed=True,
)
self.lifting_unsigned = Graph2SimplicialLiftingTransform(
lifting=lifting_map,
signed=False,
)

def test_lift_topology(self):
"""Test the lift_topology method."""
Expand All @@ -56,8 +65,11 @@ def test_lift_topology(self):
print(lifted_data_signed.incidence_1.to_dense())

assert (
abs(expected_incidence_1) == lifted_data_unsigned.incidence_1.to_dense()
).all(), "Something is wrong with unsigned incidence_1 (nodes to edges)."
abs(expected_incidence_1)
== lifted_data_unsigned.incidence_1.to_dense()
).all(), (
"Something is wrong with unsigned incidence_1 (nodes to edges)."
)
assert (
expected_incidence_1 == lifted_data_signed.incidence_1.to_dense()
).all(), "Something is wrong with signed incidence_1 (nodes to edges)."
Expand All @@ -77,8 +89,11 @@ 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(), "Something is wrong with unsigned incidence_2 (edges to triangles)."
assert (
expected_incidence_2 == lifted_data_signed.incidence_2.to_dense()
).all(), "Something is wrong with signed incidence_2 (edges to triangles)."
).all(), (
"Something is wrong with signed incidence_2 (edges to triangles)."
)
98 changes: 66 additions & 32 deletions topobenchmark/transforms/liftings/graph2simplicial/line_lifting.py
Original file line number Diff line number Diff line change
@@ -1,73 +1,107 @@
r"""This module implements the line lifting.
This lifting constructs a simplicial complex called the *Line simplicial complex*.
This is a generalization of the so-called
`Line graph <https://en.wikipedia.org/wiki/Line_graph_.
In vanilla line graph, nodes are edges from the original graph and two nodes are
connected with an edge if corresponding edges are adjacent in the original graph.
The line simplicial complex is a clique complex of the line graph.
That is, if several edges share a node in the original graph,
the corresponding vertices in the line complex are going to be connected with
a simplex.
So, the line lifting performs the following:
1. For a graph :math:`G` we create its *line graph* :math:`L(G)`.
The *line* (or *dual*) *graph* is a graph created from the initial one
by considering the edges in :math:`G` as the vertices in :math:`L(G)`
and the edges in :math:`L(G)` correspond to the vertices in :math:`G`.
2. During this procedure, we obtain a graph.
It is easy to see that such graph contains cliques for basically each
node in initial graph :math:`G`. That is, if :math:`v \in G`
has a degree :math:`d`, then there's a clique on :math:`d` vertices in
math:`L(G)`.
3. Therefore let's consider a clique complex
:math:`X(L(G))` of math:`L(G)` creating :math:`(d - 1)`-simplices for each
clique on :math:`d` vertices, that is, for each node in math:`G` of degree
:math:`d`.
When creating a line graph, we need to transfer the features from :math:`G` to
:math:`L(G)`. That is, for a vertex :math:`v \in L(G)`, which correponds to
an edge :math:`e \in G` we need to set a feature vector.
This is basically done as a mean feature of the nodes that are adjacent to :math:`e`,
that is, if `:math:`e = ( a , b )`, then
.. math::
f v := f a + f b 2 .
"""

import networkx as nx
import torch_geometric
from toponetx.classes import SimplicialComplex

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


class SimplicialLineLifting(Graph2SimplicialLifting):
class SimplicialLineLifting(LiftingMap):
r"""Lifts graphs to a simplicial complex domain by considering line simplicial complex.
Line simplicial complex is a clique complex of the line graph. Line graph is a graph, in which
the vertices are the edges in the initial graph, and two vertices are adjacent if the corresponding
edges are adjacent in the initial graph.
Parameters
----------
**kwargs : optional
Additional arguments for the class.
"""

def __init__(self, **kwargs):
super().__init__(**kwargs)

def lift_topology(self, data: torch_geometric.data.Data) -> dict:
r"""Lifts the topology of a graph to simplicial domain via line simplicial complex construction.
def lift(self, domain):
r"""Lift the topology of a graph to a simplicial complex.
Parameters
----------
data : torch_geometric.data.Data
The input data to be lifted.
domain : nx.Graph
Graph to be lifted.
Returns
----------
dict
The lifted topology.
-------
toponetx.SimplicialComplex
Lifted simplicial complex.
"""

graph = self._generate_graph_from_data(data)
graph = domain
line_graph = nx.line_graph(graph)

node_features = {
node: ((data.x[node[0], :] + data.x[node[1], :]) / 2)
node: (
(
graph.nodes[node[0]]["features"]
+ graph.nodes[node[1]]["features"]
)
/ 2
)
for node in list(line_graph.nodes)
}

cliques = nx.find_cliques(line_graph)
simplices = list(cliques) # list(map(lambda x: set(x), cliques))
simplices = list(cliques)

# we need to rename simplices here since now vertices are named as pairs
self.rename_vertices_dict = {node: i for i, node in enumerate(line_graph.nodes)}
self.rename_vertices_dict_inverse = {
i: node for node, i in self.rename_vertices_dict.items()
rename_vertices_dict = {
node: i for i, node in enumerate(line_graph.nodes)
}
renamed_line_graph = nx.relabel_nodes(line_graph, self.rename_vertices_dict)

renamed_simplices = [
{self.rename_vertices_dict[vertex] for vertex in simplex}
{rename_vertices_dict[vertex] for vertex in simplex}
for simplex in simplices
]

renamed_node_features = {
self.rename_vertices_dict[node]: value
rename_vertices_dict[node]: value
for node, value in node_features.items()
}

simplicial_complex = SimplicialComplex(simplices=renamed_simplices)
self.complex_dim = simplicial_complex.dim

simplicial_complex.set_simplex_attributes(
renamed_node_features, name="features"
)

return self._get_lifted_topology(simplicial_complex, renamed_line_graph)
return simplicial_complex

0 comments on commit 54efc5c

Please sign in to comment.