From 0863fc4921bc8ebb35e97f5f6a5c8689f65be9ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20F=2E=20Pereira?= Date: Mon, 25 Nov 2024 12:05:54 -0800 Subject: [PATCH 01/43] Add initial redesign of liftings maps --- .../test_SimplicialCliqueLifting.py | 19 +- topobenchmarkx/complex.py | 85 +++++ topobenchmarkx/transforms/converters.py | 313 ++++++++++++++++++ .../transforms/feature_liftings/base.py | 13 + .../transforms/feature_liftings/identity.py | 37 +-- .../feature_liftings/projection_sum.py | 67 +--- topobenchmarkx/transforms/liftings/base.py | 95 +++++- .../liftings/graph2simplicial/clique.py | 30 +- 8 files changed, 550 insertions(+), 109 deletions(-) create mode 100644 topobenchmarkx/complex.py create mode 100644 topobenchmarkx/transforms/converters.py create mode 100644 topobenchmarkx/transforms/feature_liftings/base.py diff --git a/test/transforms/liftings/simplicial/test_SimplicialCliqueLifting.py b/test/transforms/liftings/simplicial/test_SimplicialCliqueLifting.py index 41b8ac45..fc2f89f8 100644 --- a/test/transforms/liftings/simplicial/test_SimplicialCliqueLifting.py +++ b/test/transforms/liftings/simplicial/test_SimplicialCliqueLifting.py @@ -3,21 +3,24 @@ import torch from topobenchmarkx.transforms.liftings.graph2simplicial import ( - SimplicialCliqueLifting, + SimplicialCliqueLifting ) - +from topobenchmarkx.transforms.converters import Data2NxGraph, Complex2Dict +from topobenchmarkx.transforms.liftings.base import LiftingTransform class TestSimplicialCliqueLifting: """Test the SimplicialCliqueLifting class.""" def setup_method(self): # Initialise the SimplicialCliqueLifting class - self.lifting_signed = SimplicialCliqueLifting( - complex_dim=3, signed=True - ) - self.lifting_unsigned = SimplicialCliqueLifting( - complex_dim=3, signed=False - ) + data2graph = Data2NxGraph() + simplicial2dict_signed = Complex2Dict(signed=True) + simplicial2dict_unsigned = Complex2Dict(signed=False) + + lifting_map = SimplicialCliqueLifting(complex_dim=3) + + self.lifting_signed = LiftingTransform(data2graph, simplicial2dict_signed, lifting_map) + self.lifting_unsigned = LiftingTransform(data2graph, simplicial2dict_unsigned, lifting_map) def test_lift_topology(self, simple_graph_1): """Test the lift_topology method.""" diff --git a/topobenchmarkx/complex.py b/topobenchmarkx/complex.py new file mode 100644 index 00000000..8a2949f2 --- /dev/null +++ b/topobenchmarkx/complex.py @@ -0,0 +1,85 @@ +import torch + + +class PlainComplex: + def __init__( + self, + incidence, + down_laplacian, + up_laplacian, + adjacency, + coadjacency, + hodge_laplacian, + features=None, + ): + # TODO: allow None with nice error message if callable? + + # TODO: make this private? do not allow for changes in these values? + self.incidence = incidence + self.down_laplacian = down_laplacian + self.up_laplacian = up_laplacian + self.adjacency = adjacency + self.coadjacency = coadjacency + self.hodge_laplacian = hodge_laplacian + + if features is None: + features = [None for _ in range(len(self.incidence))] + else: + for rank, dim in enumerate(self.shape): + # TODO: make error message more informative + if ( + features[rank] is not None + and features[rank].shape[0] != dim + ): + raise ValueError("Features have wrong shape.") + + self.features = features + + @property + def shape(self): + """Shape of the complex. + + Returns + ------- + list[int] + """ + return [incidence.shape[-1] for incidence in self.incidence] + + @property + def max_rank(self): + """Maximum rank of the complex. + + Returns + ------- + int + """ + return len(self.incidence) + + def update_features(self, rank, values): + """Update features. + + Parameters + ---------- + rank : int + Rank of simplices the features belong to. + values : array-like + New features for the rank-simplices. + """ + self.features[rank] = values + + def reset_features(self): + """Reset features.""" + self.features = [None for _ in self.features] + + def propagate_values(self, rank, values): + """Propagate features from a rank to an upper one. + + Parameters + ---------- + rank : int + Rank of the simplices the values belong to. + values : array-like + Features for the rank-simplices. + """ + # TODO: can be made much better + return torch.matmul(torch.abs(self.incidence[rank + 1].t()), values) diff --git a/topobenchmarkx/transforms/converters.py b/topobenchmarkx/transforms/converters.py new file mode 100644 index 00000000..96920b74 --- /dev/null +++ b/topobenchmarkx/transforms/converters.py @@ -0,0 +1,313 @@ +import abc + +import networkx as nx +import numpy as np +import torch +import torch_geometric +from topomodelx.utils.sparse import from_sparse +from torch_geometric.utils.undirected import is_undirected, to_undirected + +from topobenchmarkx.complex import PlainComplex +from topobenchmarkx.data.utils.utils import ( + generate_zero_sparse_connectivity, + select_neighborhoods_of_interest, +) + + +class Converter(abc.ABC): + """Convert between data structures representing the same domain.""" + + def __call__(self, domain): + """Convert domain's data structure.""" + return self.convert(domain) + + @abc.abstractmethod + def convert(self, domain): + """Convert domain's data structure.""" + + +class IdentityConverter(Converter): + """Identity conversion. + + Retrieves same data structure for domain. + """ + + def convert(self, domain): + """Convert domain.""" + return domain + + +class Data2NxGraph(Converter): + """Data to nx.Graph conversion. + + Parameters + ---------- + preserve_edge_attr : bool + Whether to preserve edge attributes. + """ + + def __init__(self, preserve_edge_attr=False): + self.preserve_edge_attr = preserve_edge_attr + + def _data_has_edge_attr(self, data: torch_geometric.data.Data) -> bool: + r"""Check if the input data object has edge attributes. + + Parameters + ---------- + data : torch_geometric.data.Data + The input data. + + Returns + ------- + bool + Whether the data object has edge attributes. + """ + return hasattr(data, "edge_attr") and data.edge_attr is not None + + def convert(self, domain: torch_geometric.data.Data) -> nx.Graph: + r"""Generate a NetworkX graph from the input data object. + + Parameters + ---------- + domain : torch_geometric.data.Data + The input data. + + Returns + ------- + nx.Graph + The generated NetworkX graph. + """ + # Check if data object have edge_attr, return list of tuples as [(node_id, {'features':data}, 'dim':1)] or ?? + nodes = [ + (n, dict(features=domain.x[n], dim=0)) + for n in range(domain.x.shape[0]) + ] + + if self.preserve_edge_attr and self._data_has_edge_attr(domain): + # In case edge features are given, assign features to every edge + edge_index, edge_attr = ( + domain.edge_index, + ( + domain.edge_attr + if is_undirected(domain.edge_index, domain.edge_attr) + else to_undirected(domain.edge_index, domain.edge_attr) + ), + ) + edges = [ + (i.item(), j.item(), dict(features=edge_attr[edge_idx], dim=1)) + for edge_idx, (i, j) in enumerate( + zip(edge_index[0], edge_index[1], strict=False) + ) + ] + + else: + # If edge_attr is not present, return list list of edges + edges = [ + (i.item(), j.item(), {}) + for i, j in zip( + domain.edge_index[0], domain.edge_index[1], strict=False + ) + ] + graph = nx.Graph() + graph.add_nodes_from(nodes) + graph.add_edges_from(edges) + return graph + + +class Complex2PlainComplex(Converter): + """toponetx.Complex to PlainComplex conversion. + + NB: order of features plays a crucial role, as ``PlainComplex`` + simply stores them as lists (i.e. the reference to the indices + of the simplex are lost). + + Parameters + ---------- + max_rank : int + Maximum rank of the complex. + neighborhoods : list, optional + List of neighborhoods of interest. + signed : bool, optional + If True, returns signed connectivity matrices. + transfer_features : bool, optional + Whether to transfer features. + """ + + def __init__( + self, + max_rank=None, + neighborhoods=None, + signed=False, + transfer_features=True, + ): + super().__init__() + self.max_rank = max_rank + self.neighborhoods = neighborhoods + self.signed = signed + self.transfer_features = transfer_features + + def convert(self, domain): + """Convert toponetx.Complex to PlainComplex. + + Parameters + ---------- + domain : toponetx.Complex + + Returns + ------- + PlainComplex + """ + # NB: just a slightly rewriting of get_complex_connectivity + + max_rank = self.max_rank or domain.dim + signed = self.signed + neighborhoods = self.neighborhoods + + connectivity_infos = [ + "incidence", + "down_laplacian", + "up_laplacian", + "adjacency", + "coadjacency", + "hodge_laplacian", + ] + + practical_shape = list( + np.pad(list(domain.shape), (0, max_rank + 1 - len(domain.shape))) + ) + data = { + connectivity_info: [] for connectivity_info in connectivity_infos + } + for rank_idx in range(max_rank + 1): + for connectivity_info in connectivity_infos: + try: + data[connectivity_info].append( + from_sparse( + getattr(domain, f"{connectivity_info}_matrix")( + rank=rank_idx, signed=signed + ) + ) + ) + except ValueError: + if connectivity_info == "incidence": + data[connectivity_info].append( + generate_zero_sparse_connectivity( + m=practical_shape[rank_idx - 1], + n=practical_shape[rank_idx], + ) + ) + else: + data[connectivity_info].append( + generate_zero_sparse_connectivity( + m=practical_shape[rank_idx], + n=practical_shape[rank_idx], + ) + ) + + # TODO: handle this + if neighborhoods is not None: + data = select_neighborhoods_of_interest(data, neighborhoods) + + # TODO: simplex specific? + # TODO: how to do this for other? + if self.transfer_features and hasattr( + domain, "get_simplex_attributes" + ): + # TODO: confirm features are in the right order; update this + data["features"] = [] + for rank in range(max_rank + 1): + rank_features_dict = domain.get_simplex_attributes( + "features", rank + ) + if rank_features_dict: + rank_features = torch.stack( + list(rank_features_dict.values()) + ) + else: + rank_features = None + data["features"].append(rank_features) + + return PlainComplex(**data) + + +class PlainComplex2Dict(Converter): + """PlainComplex to dict conversion.""" + + def convert(self, domain): + """Convert PlainComplex to dict. + + Parameters + ---------- + domain : toponetx.Complex + + Returns + ------- + dict + """ + data = {} + connectivity_infos = [ + "incidence", + "down_laplacian", + "up_laplacian", + "adjacency", + "coadjacency", + "hodge_laplacian", + ] + for connectivity_info in connectivity_infos: + info = getattr(domain, connectivity_info) + for rank, rank_info in enumerate(info): + data[f"{connectivity_info}_{rank}"] = rank_info + + # TODO: handle neighborhoods + data["shape"] = domain.shape + + for index, values in enumerate(domain.features): + if values is not None: + data[f"x_{index}"] = values + + return data + + +class ConverterComposition(Converter): + def __init__(self, converters): + super().__init__() + self.converters = converters + + def convert(self, domain): + """Convert domain""" + for converter in self.converters: + domain = converter(domain) + + return domain + + +class Complex2Dict(ConverterComposition): + """Complex to dict conversion. + + Parameters + ---------- + max_rank : int + Maximum rank of the complex. + neighborhoods : list, optional + List of neighborhoods of interest. + signed : bool, optional + If True, returns signed connectivity matrices. + transfer_features : bool, optional + Whether to transfer features. + """ + + def __init__( + self, + max_rank=None, + neighborhoods=None, + signed=False, + transfer_features=True, + ): + complex2plain = Complex2PlainComplex( + max_rank=max_rank, + neighborhoods=neighborhoods, + signed=signed, + transfer_features=transfer_features, + ) + plain2dict = PlainComplex2Dict() + super().__init__(converters=(complex2plain, plain2dict)) diff --git a/topobenchmarkx/transforms/feature_liftings/base.py b/topobenchmarkx/transforms/feature_liftings/base.py new file mode 100644 index 00000000..c5969398 --- /dev/null +++ b/topobenchmarkx/transforms/feature_liftings/base.py @@ -0,0 +1,13 @@ +import abc + + +class FeatureLiftingMap(abc.ABC): + """Feature lifting map.""" + + def __call__(self, domain): + """Lift features of a domain.""" + return self.lift_features(domain) + + @abc.abstractmethod + def lift_features(self, domain): + """Lift features of a domain.""" diff --git a/topobenchmarkx/transforms/feature_liftings/identity.py b/topobenchmarkx/transforms/feature_liftings/identity.py index 93806f1d..9abf4e5d 100644 --- a/topobenchmarkx/transforms/feature_liftings/identity.py +++ b/topobenchmarkx/transforms/feature_liftings/identity.py @@ -1,36 +1,13 @@ """Identity transform that does nothing to the input data.""" -import torch_geometric +from .base import FeatureLiftingMap -class Identity(torch_geometric.transforms.BaseTransform): - r"""An identity transform that does nothing to the input data. +class Identity(FeatureLiftingMap): + """Identity feature lifting map.""" - Parameters - ---------- - **kwargs : optional - Parameters for the base transform. - """ + # TODO: rename to IdentityFeatureLifting - def __init__(self, **kwargs): - super().__init__() - self.type = "domain2domain" - self.parameters = kwargs - - def __repr__(self) -> str: - return f"{self.__class__.__name__}(type={self.type!r}, parameters={self.parameters!r})" - - def forward(self, data: torch_geometric.data.Data): - r"""Apply the transform to the input data. - - Parameters - ---------- - data : torch_geometric.data.Data - The input data. - - Returns - ------- - torch_geometric.data.Data - The same data. - """ - return data + def lift_features(self, domain): + """Lift features of a domain using identity map.""" + return domain diff --git a/topobenchmarkx/transforms/feature_liftings/projection_sum.py b/topobenchmarkx/transforms/feature_liftings/projection_sum.py index 3cce03eb..4d0c04b5 100644 --- a/topobenchmarkx/transforms/feature_liftings/projection_sum.py +++ b/topobenchmarkx/transforms/feature_liftings/projection_sum.py @@ -1,69 +1,30 @@ """ProjectionSum class.""" -import torch -import torch_geometric +from .base import FeatureLiftingMap -class ProjectionSum(torch_geometric.transforms.BaseTransform): - r"""Lift r-cell features to r+1-cells by projection. +class ProjectionSum(FeatureLiftingMap): + r"""Lift r-cell features to r+1-cells by projection.""" - Parameters - ---------- - **kwargs : optional - Additional arguments for the class. - """ - - def __init__(self, **kwargs): - super().__init__() - - def __repr__(self) -> str: - return f"{self.__class__.__name__}()" - - def lift_features( - self, data: torch_geometric.data.Data | dict - ) -> torch_geometric.data.Data | dict: + def lift_features(self, domain): r"""Project r-cell features of a graph to r+1-cell structures. Parameters ---------- - data : torch_geometric.data.Data | dict + data : PlainComplex The input data to be lifted. Returns ------- - torch_geometric.data.Data | dict - The data with the lifted features. + PlainComplex + Domain with the lifted features. """ - keys = sorted( - [ - key.split("_")[1] - for key in data - if ("incidence" in key and "-" not in key) - ] - ) - for elem in keys: - if f"x_{elem}" not in data: - idx_to_project = 0 if elem == "hyperedges" else int(elem) - 1 - data["x_" + elem] = torch.matmul( - abs(data["incidence_" + elem].t()), - data[f"x_{idx_to_project}"], - ) - return data + for rank in range(domain.max_rank - 1): + if domain.features[rank + 1] is not None: + continue - def forward( - self, data: torch_geometric.data.Data | dict - ) -> torch_geometric.data.Data | dict: - r"""Apply the lifting to the input data. - - Parameters - ---------- - data : torch_geometric.data.Data | dict - The input data to be lifted. + domain.features[rank + 1] = domain.propagate_values( + rank, domain.features[rank] + ) - Returns - ------- - torch_geometric.data.Data | dict - The lifted data. - """ - data = self.lift_features(data) - return data + return domain diff --git a/topobenchmarkx/transforms/liftings/base.py b/topobenchmarkx/transforms/liftings/base.py index c08a54e5..fa00e40e 100644 --- a/topobenchmarkx/transforms/liftings/base.py +++ b/topobenchmarkx/transforms/liftings/base.py @@ -1,10 +1,99 @@ """Abstract class for topological liftings.""" -from abc import abstractmethod +import abc import torch_geometric +from topobenchmarkx.transforms.converters import IdentityConverter from topobenchmarkx.transforms.feature_liftings import FEATURE_LIFTINGS +from topobenchmarkx.transforms.feature_liftings.identity import ( + Identity, +) + + +class LiftingTransform(torch_geometric.transforms.BaseTransform): + """Lifting transform. + + Parameters + ---------- + data2domain : Converter + Conversion between ``torch_geometric.Data`` into + domain for consumption by lifting. + domain2dict : Converter + Conversion between output domain of feature lifting + and ``torch_geometric.Data``. + lifting : LiftingMap + Lifting map. + domain2domain : Converter + Conversion between output domain of lifting + and input domain for feature lifting. + feature_lifting : FeatureLiftingMap + Feature lifting map. + """ + + # NB: emulates previous AbstractLifting + def __init__( + self, + data2domain, + domain2dict, + lifting, + domain2domain=None, + feature_lifting=None, + ): + if feature_lifting is None: + feature_lifting = Identity() + + if domain2domain is None: + domain2domain = IdentityConverter() + + self.data2domain = data2domain + self.domain2domain = domain2domain + self.domain2dict = domain2dict + self.lifting = lifting + self.feature_lifting = feature_lifting + + def forward( + self, data: torch_geometric.data.Data + ) -> torch_geometric.data.Data: + r"""Apply the full lifting (topology + features) to the input data. + + Parameters + ---------- + data : torch_geometric.data.Data + The input data to be lifted. + + Returns + ------- + torch_geometric.data.Data + The lifted data. + """ + initial_data = data.to_dict() + + domain = self.data2domain(data) + lifted_topology = self.lifting(domain) + lifted_topology = self.domain2domain(lifted_topology) + lifted_topology = self.feature_lifting(lifted_topology) + lifted_topology_dict = self.domain2dict(lifted_topology) + + # TODO: make this line more clear + return torch_geometric.data.Data( + **initial_data, **lifted_topology_dict + ) + + +class LiftingMap(abc.ABC): + """Lifting map. + + Lifts a domain into another. + """ + + def __call__(self, domain): + """Lift domain.""" + return self.lift(domain) + + @abc.abstractmethod + def lift(self, domain): + """Lift domain.""" class AbstractLifting(torch_geometric.transforms.BaseTransform): @@ -18,12 +107,14 @@ class AbstractLifting(torch_geometric.transforms.BaseTransform): Additional arguments for the class. """ + # TODO: delete + def __init__(self, feature_lifting=None, **kwargs): super().__init__() self.feature_lifting = FEATURE_LIFTINGS[feature_lifting]() self.neighborhoods = kwargs.get("neighborhoods") - @abstractmethod + @abc.abstractmethod def lift_topology(self, data: torch_geometric.data.Data) -> dict: r"""Lift the topology of a graph to higher-order topological domains. diff --git a/topobenchmarkx/transforms/liftings/graph2simplicial/clique.py b/topobenchmarkx/transforms/liftings/graph2simplicial/clique.py index af7d5cdf..990d2e6e 100755 --- a/topobenchmarkx/transforms/liftings/graph2simplicial/clique.py +++ b/topobenchmarkx/transforms/liftings/graph2simplicial/clique.py @@ -1,32 +1,30 @@ """This module implements the CliqueLifting class, which lifts graphs to simplicial complexes.""" from itertools import combinations -from typing import Any import networkx as nx -import torch_geometric from toponetx.classes import SimplicialComplex -from topobenchmarkx.transforms.liftings.graph2simplicial import ( - Graph2SimplicialLifting, -) +from topobenchmarkx.transforms.liftings.base import LiftingMap -class SimplicialCliqueLifting(Graph2SimplicialLifting): +class SimplicialCliqueLifting(LiftingMap): r"""Lift graphs to simplicial complex domain. The algorithm creates simplices by identifying the cliques and considering them as simplices of the same dimension. Parameters ---------- - **kwargs : optional - Additional arguments for the class. + complex_dim : int + Maximum rank of the complex. """ - def __init__(self, **kwargs): - super().__init__(**kwargs) + def __init__(self, complex_dim=2): + super().__init__() + # TODO: better naming + self.complex_dim = complex_dim - def lift_topology(self, data: torch_geometric.data.Data) -> dict: + def lift(self, domain): r"""Lift the topology of a graph to a simplicial complex. Parameters @@ -39,12 +37,11 @@ def lift_topology(self, data: torch_geometric.data.Data) -> dict: dict The lifted topology. """ - graph = self._generate_graph_from_data(data) + graph = domain + simplicial_complex = SimplicialComplex(graph) cliques = nx.find_cliques(graph) - simplices: list[set[tuple[Any, ...]]] = [ - set() for _ in range(2, self.complex_dim + 1) - ] + simplices = [set() for _ in range(2, self.complex_dim + 1)] for clique in cliques: for i in range(2, self.complex_dim + 1): for c in combinations(clique, i + 1): @@ -53,4 +50,5 @@ def lift_topology(self, data: torch_geometric.data.Data) -> dict: for set_k_simplices in simplices: simplicial_complex.add_simplices_from(list(set_k_simplices)) - return self._get_lifted_topology(simplicial_complex, graph) + # TODO: need to check for edge preservation + return simplicial_complex From e5ee0f338734f6e29b4059dff433a423853a4b66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20F=2E=20Pereira?= Date: Thu, 5 Dec 2024 18:56:36 -0800 Subject: [PATCH 02/43] Rename Complex and move propagate_values to projection sum feature lifting --- topobenchmarkx/complex.py | 18 +----------------- .../feature_liftings/projection_sum.py | 10 ++++++++-- 2 files changed, 9 insertions(+), 19 deletions(-) diff --git a/topobenchmarkx/complex.py b/topobenchmarkx/complex.py index 8a2949f2..531592dc 100644 --- a/topobenchmarkx/complex.py +++ b/topobenchmarkx/complex.py @@ -1,7 +1,4 @@ -import torch - - -class PlainComplex: +class Complex: def __init__( self, incidence, @@ -70,16 +67,3 @@ def update_features(self, rank, values): def reset_features(self): """Reset features.""" self.features = [None for _ in self.features] - - def propagate_values(self, rank, values): - """Propagate features from a rank to an upper one. - - Parameters - ---------- - rank : int - Rank of the simplices the values belong to. - values : array-like - Features for the rank-simplices. - """ - # TODO: can be made much better - return torch.matmul(torch.abs(self.incidence[rank + 1].t()), values) diff --git a/topobenchmarkx/transforms/feature_liftings/projection_sum.py b/topobenchmarkx/transforms/feature_liftings/projection_sum.py index 4d0c04b5..a02a1db5 100644 --- a/topobenchmarkx/transforms/feature_liftings/projection_sum.py +++ b/topobenchmarkx/transforms/feature_liftings/projection_sum.py @@ -1,5 +1,7 @@ """ProjectionSum class.""" +import torch + from .base import FeatureLiftingMap @@ -23,8 +25,12 @@ def lift_features(self, domain): if domain.features[rank + 1] is not None: continue - domain.features[rank + 1] = domain.propagate_values( - rank, domain.features[rank] + domain.update_features( + rank + 1, + torch.matmul( + torch.abs(domain.incidence[rank + 1].t()), + domain.features[rank], + ), ) return domain From 55b41207c19791a3216ce183ca2783983b3f6594 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20F=2E=20Pereira?= Date: Thu, 5 Dec 2024 18:57:15 -0800 Subject: [PATCH 03/43] Rename adapters --- topobenchmarkx/transforms/converters.py | 76 +++++++++++----------- topobenchmarkx/transforms/liftings/base.py | 4 +- 2 files changed, 40 insertions(+), 40 deletions(-) diff --git a/topobenchmarkx/transforms/converters.py b/topobenchmarkx/transforms/converters.py index 96920b74..bc31ef1f 100644 --- a/topobenchmarkx/transforms/converters.py +++ b/topobenchmarkx/transforms/converters.py @@ -7,38 +7,38 @@ from topomodelx.utils.sparse import from_sparse from torch_geometric.utils.undirected import is_undirected, to_undirected -from topobenchmarkx.complex import PlainComplex -from topobenchmarkx.data.utils.utils import ( +from topobenchmarkx.complex import Complex +from topobenchmarkx.data.utils import ( generate_zero_sparse_connectivity, select_neighborhoods_of_interest, ) -class Converter(abc.ABC): - """Convert between data structures representing the same domain.""" +class Adapter(abc.ABC): + """Adapt between data structures representing the same domain.""" def __call__(self, domain): - """Convert domain's data structure.""" - return self.convert(domain) + """Adapt domain's data structure.""" + return self.adapt(domain) @abc.abstractmethod - def convert(self, domain): - """Convert domain's data structure.""" + def adapt(self, domain): + """Adapt domain's data structure.""" -class IdentityConverter(Converter): - """Identity conversion. +class IdentityAdapter(Adapter): + """Identity adaptation. Retrieves same data structure for domain. """ - def convert(self, domain): - """Convert domain.""" + def adapt(self, domain): + """Adapt domain.""" return domain -class Data2NxGraph(Converter): - """Data to nx.Graph conversion. +class Data2NxGraph(Adapter): + """Data to nx.Graph adaptation. Parameters ---------- @@ -64,7 +64,7 @@ def _data_has_edge_attr(self, data: torch_geometric.data.Data) -> bool: """ return hasattr(data, "edge_attr") and data.edge_attr is not None - def convert(self, domain: torch_geometric.data.Data) -> nx.Graph: + def adapt(self, domain: torch_geometric.data.Data) -> nx.Graph: r"""Generate a NetworkX graph from the input data object. Parameters @@ -114,10 +114,10 @@ def convert(self, domain: torch_geometric.data.Data) -> nx.Graph: return graph -class Complex2PlainComplex(Converter): - """toponetx.Complex to PlainComplex conversion. +class TnxComplex2Complex(Adapter): + """toponetx.Complex to Complex adaptation. - NB: order of features plays a crucial role, as ``PlainComplex`` + NB: order of features plays a crucial role, as ``Complex`` simply stores them as lists (i.e. the reference to the indices of the simplex are lost). @@ -146,8 +146,8 @@ def __init__( self.signed = signed self.transfer_features = transfer_features - def convert(self, domain): - """Convert toponetx.Complex to PlainComplex. + def adapt(self, domain): + """Adapt toponetx.Complex to Complex. Parameters ---------- @@ -155,7 +155,7 @@ def convert(self, domain): Returns ------- - PlainComplex + Complex """ # NB: just a slightly rewriting of get_complex_connectivity @@ -227,14 +227,14 @@ def convert(self, domain): rank_features = None data["features"].append(rank_features) - return PlainComplex(**data) + return Complex(**data) -class PlainComplex2Dict(Converter): - """PlainComplex to dict conversion.""" +class Complex2Dict(Adapter): + """Complex to dict adaptation.""" - def convert(self, domain): - """Convert PlainComplex to dict. + def adapt(self, domain): + """Adapt Complex to dict. Parameters ---------- @@ -268,21 +268,21 @@ def convert(self, domain): return data -class ConverterComposition(Converter): - def __init__(self, converters): +class AdapterComposition(Adapter): + def __init__(self, adapters): super().__init__() - self.converters = converters + self.adapters = adapters - def convert(self, domain): - """Convert domain""" - for converter in self.converters: - domain = converter(domain) + def adapt(self, domain): + """Adapt domain""" + for adapter in self.adapters: + domain = adapter(domain) return domain -class Complex2Dict(ConverterComposition): - """Complex to dict conversion. +class TnxComplex2Dict(AdapterComposition): + """toponetx.Complex to dict adaptation. Parameters ---------- @@ -303,11 +303,11 @@ def __init__( signed=False, transfer_features=True, ): - complex2plain = Complex2PlainComplex( + complex2plain = TnxComplex2Complex( max_rank=max_rank, neighborhoods=neighborhoods, signed=signed, transfer_features=transfer_features, ) - plain2dict = PlainComplex2Dict() - super().__init__(converters=(complex2plain, plain2dict)) + plain2dict = Complex2Dict() + super().__init__(adapters=(complex2plain, plain2dict)) diff --git a/topobenchmarkx/transforms/liftings/base.py b/topobenchmarkx/transforms/liftings/base.py index fa00e40e..8dddde67 100644 --- a/topobenchmarkx/transforms/liftings/base.py +++ b/topobenchmarkx/transforms/liftings/base.py @@ -4,7 +4,7 @@ import torch_geometric -from topobenchmarkx.transforms.converters import IdentityConverter +from topobenchmarkx.transforms.converters import IdentityAdapter from topobenchmarkx.transforms.feature_liftings import FEATURE_LIFTINGS from topobenchmarkx.transforms.feature_liftings.identity import ( Identity, @@ -44,7 +44,7 @@ def __init__( feature_lifting = Identity() if domain2domain is None: - domain2domain = IdentityConverter() + domain2domain = IdentityAdapter() self.data2domain = data2domain self.domain2domain = domain2domain From 8c146f27a6d61ea139e549493a09429a8076ef4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20F=2E=20Pereira?= Date: Thu, 5 Dec 2024 18:58:34 -0800 Subject: [PATCH 04/43] Move Complex and adapters to data utils --- .../{transforms/converters.py => data/utils/adapters.py} | 0 topobenchmarkx/{complex.py => data/utils/domain.py} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename topobenchmarkx/{transforms/converters.py => data/utils/adapters.py} (100%) rename topobenchmarkx/{complex.py => data/utils/domain.py} (100%) diff --git a/topobenchmarkx/transforms/converters.py b/topobenchmarkx/data/utils/adapters.py similarity index 100% rename from topobenchmarkx/transforms/converters.py rename to topobenchmarkx/data/utils/adapters.py diff --git a/topobenchmarkx/complex.py b/topobenchmarkx/data/utils/domain.py similarity index 100% rename from topobenchmarkx/complex.py rename to topobenchmarkx/data/utils/domain.py From 59051c6c1f10e488e068727d32f500b4e97765d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20F=2E=20Pereira?= Date: Tue, 24 Dec 2024 16:05:42 -0800 Subject: [PATCH 05/43] Update imports --- topobenchmarkx/data/utils/__init__.py | 2 ++ topobenchmarkx/data/utils/adapters.py | 4 ++-- topobenchmarkx/transforms/liftings/base.py | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/topobenchmarkx/data/utils/__init__.py b/topobenchmarkx/data/utils/__init__.py index 74f57c96..01e77220 100644 --- a/topobenchmarkx/data/utils/__init__.py +++ b/topobenchmarkx/data/utils/__init__.py @@ -1,5 +1,7 @@ """Init file for data/utils module.""" +from .adapters import * +from .domain import Complex from .utils import ( ensure_serializable, # noqa: F401 generate_zero_sparse_connectivity, # noqa: F401 diff --git a/topobenchmarkx/data/utils/adapters.py b/topobenchmarkx/data/utils/adapters.py index bc31ef1f..6aa33fac 100644 --- a/topobenchmarkx/data/utils/adapters.py +++ b/topobenchmarkx/data/utils/adapters.py @@ -7,8 +7,8 @@ from topomodelx.utils.sparse import from_sparse from torch_geometric.utils.undirected import is_undirected, to_undirected -from topobenchmarkx.complex import Complex -from topobenchmarkx.data.utils import ( +from topobenchmarkx.data.utils.domain import Complex +from topobenchmarkx.data.utils.utils import ( generate_zero_sparse_connectivity, select_neighborhoods_of_interest, ) diff --git a/topobenchmarkx/transforms/liftings/base.py b/topobenchmarkx/transforms/liftings/base.py index 8dddde67..0a436585 100644 --- a/topobenchmarkx/transforms/liftings/base.py +++ b/topobenchmarkx/transforms/liftings/base.py @@ -4,7 +4,7 @@ import torch_geometric -from topobenchmarkx.transforms.converters import IdentityAdapter +from topobenchmarkx.data.utils import IdentityAdapter from topobenchmarkx.transforms.feature_liftings import FEATURE_LIFTINGS from topobenchmarkx.transforms.feature_liftings.identity import ( Identity, From e4165d81f83612dcee230f382ac8c9b80a806564 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20F=2E=20Pereira?= Date: Tue, 14 Jan 2025 16:05:29 -0800 Subject: [PATCH 06/43] Update SimplicialKHopLifting to work with new design --- .../liftings/graph2simplicial/khop.py | 38 +++++++++++-------- 1 file changed, 22 insertions(+), 16 deletions(-) diff --git a/topobenchmark/transforms/liftings/graph2simplicial/khop.py b/topobenchmark/transforms/liftings/graph2simplicial/khop.py index 50239f18..dc9e13e2 100755 --- a/topobenchmark/transforms/liftings/graph2simplicial/khop.py +++ b/topobenchmark/transforms/liftings/graph2simplicial/khop.py @@ -4,15 +4,14 @@ from itertools import combinations from typing import Any +import torch import torch_geometric from toponetx.classes import SimplicialComplex -from topobenchmark.transforms.liftings.graph2simplicial.base import ( - Graph2SimplicialLifting, -) +from topobenchmark.transforms.liftings.base import LiftingMap -class SimplicialKHopLifting(Graph2SimplicialLifting): +class SimplicialKHopLifting(LiftingMap): r"""Lift graphs to simplicial complex domain. The function lifts a graph to a simplicial complex by considering k-hop @@ -23,38 +22,43 @@ class SimplicialKHopLifting(Graph2SimplicialLifting): Parameters ---------- + complex_dim : int + Dimension of the desired complex. max_k_simplices : int, optional The maximum number of k-simplices to consider. Default is 5000. - **kwargs : optional - Additional arguments for the class. """ - def __init__(self, max_k_simplices=5000, **kwargs): - super().__init__(**kwargs) + def __init__(self, complex_dim=3, max_k_simplices=5000): + super().__init__() + self.complex_dim = complex_dim self.max_k_simplices = max_k_simplices def __repr__(self) -> str: return f"{self.__class__.__name__}(max_k_simplices={self.max_k_simplices!r})" - def lift_topology(self, data: torch_geometric.data.Data) -> dict: + def lift(self, domain): r"""Lift the topology to simplicial complex domain. 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.Complex + Lifted simplicial complex. """ - graph = self._generate_graph_from_data(data) + graph = domain + simplicial_complex = SimplicialComplex(graph) - edge_index = torch_geometric.utils.to_undirected(data.edge_index) + edge_index = torch_geometric.utils.to_undirected( + torch.tensor(list(zip(*graph.edges, strict=False))) + ) simplices: list[set[tuple[Any, ...]]] = [ set() for _ in range(2, self.complex_dim + 1) ] + for n in range(graph.number_of_nodes()): # Find 1-hop node n neighbors neighbors, _, _, _ = torch_geometric.utils.k_hop_subgraph( @@ -67,10 +71,12 @@ def lift_topology(self, data: torch_geometric.data.Data) -> dict: for i in range(1, self.complex_dim): for c in combinations(neighbors, i + 1): simplices[i - 1].add(tuple(c)) + for set_k_simplices in simplices: list_k_simplices = list(set_k_simplices) if len(set_k_simplices) > self.max_k_simplices: random.shuffle(list_k_simplices) list_k_simplices = list_k_simplices[: self.max_k_simplices] simplicial_complex.add_simplices_from(list_k_simplices) - return self._get_lifted_topology(simplicial_complex, graph) + + return simplicial_complex From 2777f9b607354f540d555af65088376bab7e1bfb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20F=2E=20Pereira?= Date: Tue, 14 Jan 2025 16:18:11 -0800 Subject: [PATCH 07/43] Add IdentityAdapter as default for all the adaptations in the LiftingTransform pipeline --- topobenchmark/transforms/liftings/base.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/topobenchmark/transforms/liftings/base.py b/topobenchmark/transforms/liftings/base.py index 6f5f35a7..d5c78dc9 100644 --- a/topobenchmark/transforms/liftings/base.py +++ b/topobenchmark/transforms/liftings/base.py @@ -32,15 +32,21 @@ class LiftingTransform(torch_geometric.transforms.BaseTransform): # NB: emulates previous AbstractLifting def __init__( self, - data2domain, - domain2dict, lifting, + data2domain=None, + domain2dict=None, domain2domain=None, feature_lifting=None, ): if feature_lifting is None: feature_lifting = Identity() + if data2domain is None: + data2domain = IdentityAdapter() + + if domain2dict is None: + domain2dict = IdentityAdapter() + if domain2domain is None: domain2domain = IdentityAdapter() From e5d48d3918354a0174c6748dc4d9697c579e0564 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20F=2E=20Pereira?= Date: Tue, 14 Jan 2025 16:19:46 -0800 Subject: [PATCH 08/43] Improve TnxComplex2Complex api and signatures; improve variable naming --- topobenchmark/data/utils/adapters.py | 32 +++++++++++++++------------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/topobenchmark/data/utils/adapters.py b/topobenchmark/data/utils/adapters.py index 9aa8355a..9db40c08 100644 --- a/topobenchmark/data/utils/adapters.py +++ b/topobenchmark/data/utils/adapters.py @@ -123,8 +123,9 @@ class TnxComplex2Complex(Adapter): Parameters ---------- - max_rank : int - Maximum rank of the complex. + complex_dim : int + Dimension of the desired subcomplex. + If ``None``, adapts the (full) complex. neighborhoods : list, optional List of neighborhoods of interest. signed : bool, optional @@ -135,13 +136,13 @@ class TnxComplex2Complex(Adapter): def __init__( self, - max_rank=None, + complex_dim=None, neighborhoods=None, signed=False, transfer_features=True, ): super().__init__() - self.max_rank = max_rank + self.complex_dim = complex_dim self.neighborhoods = neighborhoods self.signed = signed self.transfer_features = transfer_features @@ -159,7 +160,7 @@ def adapt(self, domain): """ # NB: just a slightly rewriting of get_complex_connectivity - max_rank = self.max_rank or domain.dim + dim = self.complex_dim or domain.dim signed = self.signed neighborhoods = self.neighborhoods @@ -173,12 +174,12 @@ def adapt(self, domain): ] practical_shape = list( - np.pad(list(domain.shape), (0, max_rank + 1 - len(domain.shape))) + np.pad(list(domain.shape), (0, dim + 1 - len(domain.shape))) ) data = { connectivity_info: [] for connectivity_info in connectivity_infos } - for rank_idx in range(max_rank + 1): + for rank_idx in range(dim + 1): for connectivity_info in connectivity_infos: try: data[connectivity_info].append( @@ -215,7 +216,7 @@ def adapt(self, domain): ): # TODO: confirm features are in the right order; update this data["features"] = [] - for rank in range(max_rank + 1): + for rank in range(dim + 1): rank_features_dict = domain.get_simplex_attributes( "features", rank ) @@ -286,8 +287,9 @@ class TnxComplex2Dict(AdapterComposition): Parameters ---------- - max_rank : int - Maximum rank of the complex. + complex_dim : int + Dimension of the desired subcomplex. + If ``None``, adapts the (full) complex. neighborhoods : list, optional List of neighborhoods of interest. signed : bool, optional @@ -298,16 +300,16 @@ class TnxComplex2Dict(AdapterComposition): def __init__( self, - max_rank=None, + complex_dim=None, neighborhoods=None, signed=False, transfer_features=True, ): - complex2plain = TnxComplex2Complex( - max_rank=max_rank, + tnxcomplex2complex = TnxComplex2Complex( + complex_dim=complex_dim, neighborhoods=neighborhoods, signed=signed, transfer_features=transfer_features, ) - plain2dict = Complex2Dict() - super().__init__(adapters=(complex2plain, plain2dict)) + complex2dict = Complex2Dict() + super().__init__(adapters=(tnxcomplex2complex, complex2dict)) From 601a0e1ad6e7288ce2020a8369fd832fb8e300c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20F=2E=20Pereira?= Date: Tue, 14 Jan 2025 17:38:13 -0800 Subject: [PATCH 09/43] Update graph2hypergraph liftings to work with new design --- .../liftings/graph2hypergraph/khop.py | 20 ++++++++----------- .../liftings/graph2hypergraph/knn.py | 20 +++++++------------ 2 files changed, 15 insertions(+), 25 deletions(-) diff --git a/topobenchmark/transforms/liftings/graph2hypergraph/khop.py b/topobenchmark/transforms/liftings/graph2hypergraph/khop.py index 298fa135..f8997e31 100755 --- a/topobenchmark/transforms/liftings/graph2hypergraph/khop.py +++ b/topobenchmark/transforms/liftings/graph2hypergraph/khop.py @@ -3,12 +3,10 @@ import torch import torch_geometric -from topobenchmark.transforms.liftings.graph2hypergraph import ( - Graph2HypergraphLifting, -) +from topobenchmark.transforms.liftings.base import LiftingMap -class HypergraphKHopLifting(Graph2HypergraphLifting): +class HypergraphKHopLifting(LiftingMap): r"""Lift graph to hypergraphs by considering k-hop neighborhoods. The class transforms graphs to hypergraph domain by considering k-hop neighborhoods of @@ -19,18 +17,16 @@ class HypergraphKHopLifting(Graph2HypergraphLifting): ---------- k_value : int, optional The number of hops to consider. Default is 1. - **kwargs : optional - Additional arguments for the class. """ - def __init__(self, k_value=1, **kwargs): - super().__init__(**kwargs) - self.k = k_value + def __init__(self, k_value=1): + super().__init__() + self.n_hops = k_value def __repr__(self) -> str: - return f"{self.__class__.__name__}(k={self.k!r})" + return f"{self.__class__.__name__}(k={self.n_hops!r})" - def lift_topology(self, data: torch_geometric.data.Data) -> dict: + def lift(self, data: torch_geometric.data.Data) -> dict: r"""Lift a graphs to hypergraphs by considering k-hop neighborhoods. Parameters @@ -70,7 +66,7 @@ def lift_topology(self, data: torch_geometric.data.Data) -> dict: for n in range(num_nodes): neighbors, _, _, _ = torch_geometric.utils.k_hop_subgraph( - n, self.k, edge_index + n, self.n_hops, edge_index ) incidence_1[n, neighbors] = 1 diff --git a/topobenchmark/transforms/liftings/graph2hypergraph/knn.py b/topobenchmark/transforms/liftings/graph2hypergraph/knn.py index 03d0a13a..5b0de672 100755 --- a/topobenchmark/transforms/liftings/graph2hypergraph/knn.py +++ b/topobenchmark/transforms/liftings/graph2hypergraph/knn.py @@ -3,12 +3,10 @@ import torch import torch_geometric -from topobenchmark.transforms.liftings.graph2hypergraph import ( - Graph2HypergraphLifting, -) +from topobenchmark.transforms.liftings.base import LiftingMap -class HypergraphKNNLifting(Graph2HypergraphLifting): +class HypergraphKNNLifting(LiftingMap): r"""Lift graphs to hypergraph domain by considering k-nearest neighbors. Parameters @@ -17,8 +15,6 @@ class HypergraphKNNLifting(Graph2HypergraphLifting): The number of nearest neighbors to consider. Must be positive. Default is 1. loop : bool, optional If True the hyperedges will contain the node they were created from. - **kwargs : optional - Additional arguments for the class. Raises ------ @@ -28,8 +24,8 @@ class HypergraphKNNLifting(Graph2HypergraphLifting): If k_value is not an integer or if loop is not a boolean. """ - def __init__(self, k_value=1, loop=True, **kwargs): - super().__init__(**kwargs) + def __init__(self, k_value=1, loop=True): + super().__init__() # Validate k_value if not isinstance(k_value, int): @@ -41,11 +37,9 @@ def __init__(self, k_value=1, loop=True, **kwargs): if not isinstance(loop, bool): raise TypeError("loop must be a boolean") - self.k = k_value - self.loop = loop - self.transform = torch_geometric.transforms.KNNGraph(self.k, self.loop) + self.transform = torch_geometric.transforms.KNNGraph(k_value, loop) - def lift_topology(self, data: torch_geometric.data.Data) -> dict: + def lift(self, data: torch_geometric.data.Data) -> dict: r"""Lift a graph to hypergraph by considering k-nearest neighbors. Parameters @@ -64,7 +58,7 @@ def lift_topology(self, data: torch_geometric.data.Data) -> dict: incidence_1 = torch.zeros(num_nodes, num_nodes) data_lifted = self.transform(data) # check for loops, since KNNGraph is inconsistent with nodes with equal features - if self.loop: + if self.transform.loop: for i in range(num_nodes): if not torch.any( torch.all( From 1f9b12d76095fc428617ab405852ece84aa7efe6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20F=2E=20Pereira?= Date: Tue, 14 Jan 2025 17:38:39 -0800 Subject: [PATCH 10/43] Update graph2cell liftings to work with new design --- .../transforms/liftings/graph2cell/cycle.py | 40 +++++++++---------- 1 file changed, 19 insertions(+), 21 deletions(-) diff --git a/topobenchmark/transforms/liftings/graph2cell/cycle.py b/topobenchmark/transforms/liftings/graph2cell/cycle.py index 31e94d8b..63160701 100755 --- a/topobenchmark/transforms/liftings/graph2cell/cycle.py +++ b/topobenchmark/transforms/liftings/graph2cell/cycle.py @@ -1,15 +1,12 @@ """This module implements the cycle lifting for graphs to cell complexes.""" import networkx as nx -import torch_geometric from toponetx.classes import CellComplex -from topobenchmark.transforms.liftings.graph2cell.base import ( - Graph2CellLifting, -) +from topobenchmark.transforms.liftings.base import LiftingMap -class CellCycleLifting(Graph2CellLifting): +class CellCycleLifting(LiftingMap): r"""Lift graphs to cell complexes. The algorithm creates 2-cells by identifying the cycles and considering them as 2-cells. @@ -18,39 +15,40 @@ class CellCycleLifting(Graph2CellLifting): ---------- max_cell_length : int, optional The maximum length of the cycles to be lifted. Default is None. - **kwargs : optional - Additional arguments for the class. """ - def __init__(self, max_cell_length=None, **kwargs): - super().__init__(**kwargs) - self.complex_dim = 2 + def __init__(self, max_cell_length=None): + super().__init__() + self._complex_dim = 2 self.max_cell_length = max_cell_length - def lift_topology(self, data: torch_geometric.data.Data) -> dict: + def lift(self, domain): r"""Find the cycles of a graph and lifts them to 2-cells. Parameters ---------- - data : torch_geometric.data.Data - The input data to be lifted. + domain : nx.Graph + Graph to be lifted. Returns ------- - dict - The lifted topology. + CellComplex + The cell complex. """ - G = self._generate_graph_from_data(data) - cycles = nx.cycle_basis(G) - cell_complex = CellComplex(G) + graph = domain + + cycles = nx.cycle_basis(graph) + cell_complex = CellComplex(graph) # Eliminate self-loop cycles cycles = [cycle for cycle in cycles if len(cycle) != 1] - # Eliminate cycles that are greater than the max_cell_lenght + + # Eliminate cycles that are greater than the max_cell_length if self.max_cell_length is not None: cycles = [ cycle for cycle in cycles if len(cycle) <= self.max_cell_length ] if len(cycles) != 0: - cell_complex.add_cells_from(cycles, rank=self.complex_dim) - return self._get_lifted_topology(cell_complex, G) + cell_complex.add_cells_from(cycles, rank=self._complex_dim) + + return cell_complex From f5604753268b667f4d6fe22d60442e461feb9d3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20F=2E=20Pereira?= Date: Tue, 14 Jan 2025 17:40:06 -0800 Subject: [PATCH 11/43] Improve SimplicialCliqueLifting docstrings --- .../liftings/graph2simplicial/clique.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/topobenchmark/transforms/liftings/graph2simplicial/clique.py b/topobenchmark/transforms/liftings/graph2simplicial/clique.py index 2bb8c405..37a5cc15 100755 --- a/topobenchmark/transforms/liftings/graph2simplicial/clique.py +++ b/topobenchmark/transforms/liftings/graph2simplicial/clique.py @@ -11,17 +11,17 @@ class SimplicialCliqueLifting(LiftingMap): r"""Lift graphs to simplicial complex domain. - The algorithm creates simplices by identifying the cliques and considering them as simplices of the same dimension. + The algorithm creates simplices by identifying the cliques + and considering them as simplices of the same dimension. Parameters ---------- complex_dim : int - Maximum rank of the complex. + Dimension of the subcomplex. """ def __init__(self, complex_dim=2): super().__init__() - # TODO: better naming self.complex_dim = complex_dim def lift(self, domain): @@ -29,13 +29,13 @@ def lift(self, domain): 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.Complex + Lifted simplicial complex. """ graph = domain @@ -50,5 +50,4 @@ def lift(self, domain): for set_k_simplices in simplices: simplicial_complex.add_simplices_from(list(set_k_simplices)) - # TODO: need to check for edge preservation return simplicial_complex From cfd7f458587e908b5bfccb97179e2f18aa9991b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20F=2E=20Pereira?= Date: Tue, 14 Jan 2025 17:41:35 -0800 Subject: [PATCH 12/43] Fix lifting tests (NB: same behavior, only adapted setup - with few exceptions) --- test/conftest.py | 48 +++--- .../liftings/cell/test_CellCyclesLifting.py | 10 +- .../hypergraph/test_HypergraphKHopLifting.py | 41 ++++-- ...test_HypergraphKNearestNeighborsLifting.py | 138 ++++++++++-------- .../test_SimplicialCliqueLifting.py | 38 ++++- .../test_SimplicialNeighborhoodLifting.py | 35 ++++- test/transforms/liftings/test_GraphLifting.py | 89 +++++------ 7 files changed, 243 insertions(+), 156 deletions(-) diff --git a/test/conftest.py b/test/conftest.py index c84a1b72..753d63b2 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -1,25 +1,25 @@ """Configuration file for pytest.""" + import networkx as nx import pytest import torch import torch_geometric -from topobenchmark.transforms.liftings.graph2simplicial import ( - SimplicialCliqueLifting -) -from topobenchmark.transforms.liftings.graph2cell import ( - CellCycleLifting + +from topobenchmark.transforms.liftings.graph2cell.cycle import CellCycleLifting +from topobenchmark.transforms.liftings.graph2simplicial.clique import ( + SimplicialCliqueLifting, ) @pytest.fixture def mocker_fixture(mocker): """Return pytest mocker, used when one want to use mocker in setup_method. - + Parameters ---------- mocker : pytest_mock.plugin.MockerFixture A pytest mocker. - + Returns ------- pytest_mock.plugin.MockerFixture @@ -31,7 +31,7 @@ def mocker_fixture(mocker): @pytest.fixture def simple_graph_0(): """Create a manual graph for testing purposes. - + Returns ------- torch_geometric.data.Data @@ -74,10 +74,11 @@ def simple_graph_0(): ) return data + @pytest.fixture def simple_graph_1(): """Create a manual graph for testing purposes. - + Returns ------- torch_geometric.data.Data @@ -133,37 +134,35 @@ def simple_graph_1(): return data - @pytest.fixture def sg1_clique_lifted(simple_graph_1): """Return a simple graph with a clique lifting. - + Parameters ---------- simple_graph_1 : torch_geometric.data.Data A simple graph data object. - + Returns ------- torch_geometric.data.Data A simple graph data object with a clique lifting. """ - lifting_signed = SimplicialCliqueLifting( - complex_dim=3, signed=True - ) + lifting_signed = SimplicialCliqueLifting(complex_dim=3, signed=True) data = lifting_signed(simple_graph_1) data.batch_0 = "null" return data + @pytest.fixture def sg1_cell_lifted(simple_graph_1): """Return a simple graph with a cell lifting. - + Parameters ---------- simple_graph_1 : torch_geometric.data.Data A simple graph data object. - + Returns ------- torch_geometric.data.Data @@ -178,7 +177,7 @@ def sg1_cell_lifted(simple_graph_1): @pytest.fixture def simple_graph_2(): """Create a manual graph for testing purposes. - + Returns ------- torch_geometric.data.Data @@ -244,7 +243,7 @@ def simple_graph_2(): @pytest.fixture def random_graph_input(): """Create a random graph for testing purposes. - + Returns ------- torch.Tensor @@ -261,13 +260,12 @@ def random_graph_input(): num_nodes = 8 d_feat = 12 x = torch.randn(num_nodes, 12) - edges_1 = torch.randint(0, num_nodes, (2, num_nodes*2)) - edges_2 = torch.randint(0, num_nodes, (2, num_nodes*2)) - + edges_1 = torch.randint(0, num_nodes, (2, num_nodes * 2)) + edges_2 = torch.randint(0, num_nodes, (2, num_nodes * 2)) + d_feat_1, d_feat_2 = 5, 17 - x_1 = torch.randn(num_nodes*2, d_feat_1) - x_2 = torch.randn(num_nodes*2, d_feat_2) + x_1 = torch.randn(num_nodes * 2, d_feat_1) + x_2 = torch.randn(num_nodes * 2, d_feat_2) return x, x_1, x_2, edges_1, edges_2 - diff --git a/test/transforms/liftings/cell/test_CellCyclesLifting.py b/test/transforms/liftings/cell/test_CellCyclesLifting.py index 54fd276f..c574992e 100644 --- a/test/transforms/liftings/cell/test_CellCyclesLifting.py +++ b/test/transforms/liftings/cell/test_CellCyclesLifting.py @@ -2,7 +2,9 @@ import torch -from topobenchmark.transforms.liftings.graph2cell import CellCycleLifting +from topobenchmark.data.utils import Data2NxGraph, TnxComplex2Dict +from topobenchmark.transforms.liftings.base import LiftingTransform +from topobenchmark.transforms.liftings.graph2cell.cycle import CellCycleLifting class TestCellCycleLifting: @@ -10,7 +12,11 @@ class TestCellCycleLifting: def setup_method(self): # Initialise the CellCycleLifting class - self.lifting = CellCycleLifting() + self.lifting = LiftingTransform( + CellCycleLifting(), + data2domain=Data2NxGraph(), + domain2dict=TnxComplex2Dict(), + ) def test_lift_topology(self, simple_graph_1): # Test the lift_topology method diff --git a/test/transforms/liftings/hypergraph/test_HypergraphKHopLifting.py b/test/transforms/liftings/hypergraph/test_HypergraphKHopLifting.py index 13285fc1..3fcc7ebb 100644 --- a/test/transforms/liftings/hypergraph/test_HypergraphKHopLifting.py +++ b/test/transforms/liftings/hypergraph/test_HypergraphKHopLifting.py @@ -2,7 +2,8 @@ import torch -from topobenchmark.transforms.liftings.graph2hypergraph import ( +from topobenchmark.transforms.liftings.base import LiftingTransform +from topobenchmark.transforms.liftings.graph2hypergraph.khop import ( HypergraphKHopLifting, ) @@ -11,15 +12,23 @@ class TestHypergraphKHopLifting: """Test the HypergraphKHopLifting class.""" def setup_method(self): - """ Setup the test.""" + """Setup the test.""" # Initialise the HypergraphKHopLifting class - self.lifting_k1 = HypergraphKHopLifting(k_value=1) - self.lifting_k2 = HypergraphKHopLifting(k_value=2) - self.lifting_edge_attr = HypergraphKHopLifting(k_value=1, preserve_edge_attr=True) + self.lifting_k1 = LiftingTransform(HypergraphKHopLifting(k_value=1)) + self.lifting_k2 = LiftingTransform(HypergraphKHopLifting(k_value=2)) + + # TODO: delete? + # NB: `preserve_edge_attr` is never used? therefore they're equivalent + # self.lifting_edge_attr = HypergraphKHopLifting( + # k_value=1, preserve_edge_attr=True + # ) + self.lifting_edge_attr = LiftingTransform( + HypergraphKHopLifting(k_value=1) + ) def test_lift_topology(self, simple_graph_2): - """ Test the lift_topology method. - + """Test the lift_topology method. + Parameters ---------- simple_graph_2 : Data @@ -78,10 +87,18 @@ def test_lift_topology(self, simple_graph_2): assert ( expected_n_hyperedges == lifted_data_k2.num_hyperedges ), "Something is wrong with the number of hyperedges (k=2)." - + self.data_edge_attr = simple_graph_2 - edge_attributes = torch.rand((self.data_edge_attr.edge_index.shape[1], 2)) + edge_attributes = torch.rand( + (self.data_edge_attr.edge_index.shape[1], 2) + ) self.data_edge_attr.edge_attr = edge_attributes - lifted_data_edge_attr = self.lifting_edge_attr.forward(self.data_edge_attr.clone()) - assert lifted_data_edge_attr.edge_attr is not None, "Edge attributes are not preserved." - assert torch.all(edge_attributes == lifted_data_edge_attr.edge_attr), "Edge attributes are not preserved correctly." + lifted_data_edge_attr = self.lifting_edge_attr.forward( + self.data_edge_attr.clone() + ) + assert ( + lifted_data_edge_attr.edge_attr is not None + ), "Edge attributes are not preserved." + assert torch.all( + edge_attributes == lifted_data_edge_attr.edge_attr + ), "Edge attributes are not preserved correctly." diff --git a/test/transforms/liftings/hypergraph/test_HypergraphKNearestNeighborsLifting.py b/test/transforms/liftings/hypergraph/test_HypergraphKNearestNeighborsLifting.py index 7e9d1216..069d7a3c 100644 --- a/test/transforms/liftings/hypergraph/test_HypergraphKNearestNeighborsLifting.py +++ b/test/transforms/liftings/hypergraph/test_HypergraphKNearestNeighborsLifting.py @@ -3,7 +3,8 @@ import pytest import torch from torch_geometric.data import Data -from topobenchmark.transforms.liftings.graph2hypergraph import ( + +from topobenchmark.transforms.liftings.graph2hypergraph.knn import ( HypergraphKNNLifting, ) @@ -13,7 +14,7 @@ class TestHypergraphKNNLifting: def setup_method(self): """Set up test fixtures before each test method. - + Creates instances of HypergraphKNNLifting with different k values and loop settings. """ @@ -23,88 +24,94 @@ def setup_method(self): def test_initialization(self): """Test initialization with different parameters.""" + # TODO: overkill, delete? + # Test default parameters lifting_default = HypergraphKNNLifting() - assert lifting_default.k == 1 - assert lifting_default.loop is True + assert lifting_default.transform.k == 1 + assert lifting_default.transform.loop is True # Test custom parameters lifting_custom = HypergraphKNNLifting(k_value=5, loop=False) - assert lifting_custom.k == 5 - assert lifting_custom.loop is False + assert lifting_custom.transform.k == 5 + assert lifting_custom.transform.loop is False def test_lift_topology_k2(self, simple_graph_2): """Test the lift_topology method with k=2. - + Parameters ---------- simple_graph_2 : torch_geometric.data.Data A simple graph fixture with 9 nodes arranged in a line pattern. """ - lifted_data_k2 = self.lifting_k2.lift_topology(simple_graph_2.clone()) + lifted_data_k2 = self.lifting_k2.lift(simple_graph_2.clone()) expected_n_hyperedges = 9 - expected_incidence_1 = torch.tensor([ - [1.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], - [1.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], - [0.0, 1.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], - [0.0, 0.0, 1.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0], - [0.0, 0.0, 0.0, 1.0, 1.0, 0.0, 0.0, 0.0, 0.0], - [0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 0.0, 0.0, 0.0], - [0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 0.0, 0.0], - [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 0.0], - [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 1.0], - ]) + expected_incidence_1 = torch.tensor( + [ + [1.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], + [1.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], + [0.0, 1.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], + [0.0, 0.0, 1.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0], + [0.0, 0.0, 0.0, 1.0, 1.0, 0.0, 0.0, 0.0, 0.0], + [0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 0.0, 0.0, 0.0], + [0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 0.0, 0.0], + [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 0.0], + [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 1.0], + ] + ) assert torch.equal( lifted_data_k2["incidence_hyperedges"].to_dense(), - expected_incidence_1 + expected_incidence_1, ), "Incorrect incidence_hyperedges for k=2" - + assert lifted_data_k2["num_hyperedges"] == expected_n_hyperedges assert torch.equal(lifted_data_k2["x_0"], simple_graph_2.x) def test_lift_topology_k3(self, simple_graph_2): """Test the lift_topology method with k=3. - + Parameters ---------- simple_graph_2 : torch_geometric.data.Data A simple graph fixture with 9 nodes arranged in a line pattern. """ - lifted_data_k3 = self.lifting_k3.lift_topology(simple_graph_2.clone()) + lifted_data_k3 = self.lifting_k3.lift(simple_graph_2.clone()) expected_n_hyperedges = 9 - expected_incidence_1 = torch.tensor([ - [1.0, 1.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], - [1.0, 1.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], - [1.0, 1.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], - [0.0, 1.0, 1.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0], - [0.0, 0.0, 1.0, 1.0, 1.0, 0.0, 0.0, 0.0, 0.0], - [0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 0.0, 0.0, 0.0], - [0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 0.0, 0.0], - [0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 0.0], - [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0], - ]) + expected_incidence_1 = torch.tensor( + [ + [1.0, 1.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], + [1.0, 1.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], + [1.0, 1.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], + [0.0, 1.0, 1.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0], + [0.0, 0.0, 1.0, 1.0, 1.0, 0.0, 0.0, 0.0, 0.0], + [0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 0.0, 0.0, 0.0], + [0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 0.0, 0.0], + [0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 0.0], + [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0], + ] + ) assert torch.equal( lifted_data_k3["incidence_hyperedges"].to_dense(), - expected_incidence_1 + expected_incidence_1, ), "Incorrect incidence_hyperedges for k=3" - + assert lifted_data_k3["num_hyperedges"] == expected_n_hyperedges assert torch.equal(lifted_data_k3["x_0"], simple_graph_2.x) def test_lift_topology_no_loop(self, simple_graph_2): """Test the lift_topology method with loop=False. - + Parameters ---------- simple_graph_2 : torch_geometric.data.Data A simple graph fixture with 9 nodes arranged in a line pattern. """ - lifted_data = self.lifting_no_loop.lift_topology(simple_graph_2.clone()) - + lifted_data = self.lifting_no_loop.lift(simple_graph_2.clone()) + # Verify no self-loops in the incidence matrix incidence_matrix = lifted_data["incidence_hyperedges"].to_dense() diagonal = torch.diag(incidence_matrix) @@ -115,11 +122,11 @@ def test_lift_topology_with_equal_features(self): # Create a graph where some nodes have identical features data = Data( x=torch.tensor([[1.0], [1.0], [2.0], [2.0]]), - edge_index=torch.tensor([[0, 1, 2, 3], [1, 0, 3, 2]]) + edge_index=torch.tensor([[0, 1, 2, 3], [1, 0, 3, 2]]), ) - - lifted_data = self.lifting_k2.lift_topology(data) - + + lifted_data = self.lifting_k2.lift(data) + # Verify the shape of the output assert lifted_data["incidence_hyperedges"].size() == (4, 4) assert lifted_data["num_hyperedges"] == 4 @@ -128,7 +135,7 @@ def test_lift_topology_with_equal_features(self): @pytest.mark.parametrize("k_value", [1, 2, 3, 4]) def test_different_k_values(self, k_value, simple_graph_2): """Test lift_topology with different k values. - + Parameters ---------- k_value : int @@ -137,29 +144,30 @@ def test_different_k_values(self, k_value, simple_graph_2): A simple graph fixture with 9 nodes arranged in a line pattern. """ lifting = HypergraphKNNLifting(k_value=k_value, loop=True) - lifted_data = lifting.lift_topology(simple_graph_2.clone()) - + lifted_data = lifting.lift(simple_graph_2.clone()) + # Verify basic properties assert lifted_data["num_hyperedges"] == simple_graph_2.x.size(0) incidence_matrix = lifted_data["incidence_hyperedges"].to_dense() - + # Check that each node is connected to at most k nodes - assert torch.all(incidence_matrix.sum(dim=1) <= k_value), \ - f"Some nodes are connected to more than {k_value} neighbors" + assert torch.all( + incidence_matrix.sum(dim=1) <= k_value + ), f"Some nodes are connected to more than {k_value} neighbors" def test_invalid_inputs(self): """Test handling of invalid inputs and edge cases.""" # Test with no x attribute (this should raise AttributeError) data_no_x = Data(edge_index=torch.tensor([[0, 1], [1, 0]])) with pytest.raises(AttributeError): - self.lifting_k2.lift_topology(data_no_x) + self.lifting_k2.lift(data_no_x) # Test single node case (edge case that should work) single_node_data = Data( x=torch.tensor([[1.0]], dtype=torch.float), - edge_index=torch.tensor([[0], [0]]) + edge_index=torch.tensor([[0], [0]]), ) - lifted_single = self.lifting_k2.lift_topology(single_node_data) + lifted_single = self.lifting_k2.lift(single_node_data) assert lifted_single["num_hyperedges"] == 1 assert lifted_single["incidence_hyperedges"].size() == (1, 1) assert torch.equal(lifted_single["x_0"], single_node_data.x) @@ -167,32 +175,30 @@ def test_invalid_inputs(self): # Test with identical nodes (edge case that should work) identical_nodes_data = Data( x=torch.tensor([[1.0], [1.0]], dtype=torch.float), - edge_index=torch.tensor([[0, 1], [1, 0]]) + edge_index=torch.tensor([[0, 1], [1, 0]]), ) - lifted_identical = self.lifting_k2.lift_topology(identical_nodes_data) + lifted_identical = self.lifting_k2.lift(identical_nodes_data) assert lifted_identical["num_hyperedges"] == 2 assert lifted_identical["incidence_hyperedges"].size() == (2, 2) assert torch.equal(lifted_identical["x_0"], identical_nodes_data.x) # Test with missing edge_index (this should work as KNNGraph will create edges) - data_no_edges = Data( - x=torch.tensor([[1.0], [2.0]], dtype=torch.float) - ) - lifted_no_edges = self.lifting_k2.lift_topology(data_no_edges) + data_no_edges = Data(x=torch.tensor([[1.0], [2.0]], dtype=torch.float)) + lifted_no_edges = self.lifting_k2.lift(data_no_edges) assert lifted_no_edges["num_hyperedges"] == 2 assert lifted_no_edges["incidence_hyperedges"].size() == (2, 2) assert torch.equal(lifted_no_edges["x_0"], data_no_edges.x) # Test with no data (should raise AttributeError) with pytest.raises(AttributeError): - self.lifting_k2.lift_topology(None) + self.lifting_k2.lift(None) # Test with empty tensor for x (should work but result in empty outputs) empty_data = Data( x=torch.tensor([], dtype=torch.float).reshape(0, 1), - edge_index=torch.tensor([], dtype=torch.long).reshape(2, 0) + edge_index=torch.tensor([], dtype=torch.long).reshape(2, 0), ) - lifted_empty = self.lifting_k2.lift_topology(empty_data) + lifted_empty = self.lifting_k2.lift(empty_data) assert lifted_empty["num_hyperedges"] == 0 assert lifted_empty["incidence_hyperedges"].size(0) == 0 @@ -203,13 +209,17 @@ def test_invalid_initialization(self): HypergraphKNNLifting(k_value=1.5) # Test with zero k_value - with pytest.raises(ValueError, match="k_value must be greater than or equal to 1"): + with pytest.raises( + ValueError, match="k_value must be greater than or equal to 1" + ): HypergraphKNNLifting(k_value=0) # Test with negative k_value - with pytest.raises(ValueError, match="k_value must be greater than or equal to 1"): + with pytest.raises( + ValueError, match="k_value must be greater than or equal to 1" + ): HypergraphKNNLifting(k_value=-1) # Test with non-boolean loop with pytest.raises(TypeError, match="loop must be a boolean"): - HypergraphKNNLifting(k_value=1, loop="True") \ No newline at end of file + HypergraphKNNLifting(k_value=1, loop="True") diff --git a/test/transforms/liftings/simplicial/test_SimplicialCliqueLifting.py b/test/transforms/liftings/simplicial/test_SimplicialCliqueLifting.py index 7d85b19e..fa36e072 100644 --- a/test/transforms/liftings/simplicial/test_SimplicialCliqueLifting.py +++ b/test/transforms/liftings/simplicial/test_SimplicialCliqueLifting.py @@ -2,11 +2,19 @@ import torch -from topobenchmark.transforms.liftings.graph2simplicial import ( - SimplicialCliqueLifting +from topobenchmark.data.utils import ( + Complex2Dict, + Data2NxGraph, + TnxComplex2Complex, +) +from topobenchmark.transforms.feature_liftings.projection_sum import ( + ProjectionSum, ) -from topobenchmark.transforms.converters import Data2NxGraph, Complex2Dict from topobenchmark.transforms.liftings.base import LiftingTransform +from topobenchmark.transforms.liftings.graph2simplicial.clique import ( + SimplicialCliqueLifting, +) + class TestSimplicialCliqueLifting: """Test the SimplicialCliqueLifting class.""" @@ -14,13 +22,25 @@ class TestSimplicialCliqueLifting: def setup_method(self): # Initialise the SimplicialCliqueLifting class data2graph = Data2NxGraph() - simplicial2dict_signed = Complex2Dict(signed=True) - simplicial2dict_unsigned = Complex2Dict(signed=False) lifting_map = SimplicialCliqueLifting(complex_dim=3) + feature_lifting = ProjectionSum() + domain2dict = Complex2Dict() - self.lifting_signed = LiftingTransform(data2graph, simplicial2dict_signed, lifting_map) - self.lifting_unsigned = LiftingTransform(data2graph, simplicial2dict_unsigned, lifting_map) + self.lifting_signed = LiftingTransform( + lifting=lifting_map, + feature_lifting=feature_lifting, + data2domain=data2graph, + domain2domain=TnxComplex2Complex(signed=True), + domain2dict=domain2dict, + ) + self.lifting_unsigned = LiftingTransform( + lifting=lifting_map, + feature_lifting=feature_lifting, + data2domain=data2graph, + domain2domain=TnxComplex2Complex(signed=False), + domain2dict=domain2dict, + ) def test_lift_topology(self, simple_graph_1): """Test the lift_topology method.""" @@ -207,6 +227,8 @@ def test_lift_topology(self, simple_graph_1): def test_lifted_features_signed(self, simple_graph_1): """Test the lift_features method in signed incidence cases.""" + # TODO: can be removed/moved; part of projection sum + self.data = simple_graph_1 # Test the lift_features method for signed case lifted_data = self.lifting_signed.forward(self.data) @@ -249,6 +271,8 @@ def test_lifted_features_signed(self, simple_graph_1): def test_lifted_features_unsigned(self, simple_graph_1): """Test the lift_features method in unsigned incidence cases.""" + # TODO: redundant. can be moved/removed + self.data = simple_graph_1 # Test the lift_features method for unsigned case lifted_data = self.lifting_unsigned.forward(self.data) diff --git a/test/transforms/liftings/simplicial/test_SimplicialNeighborhoodLifting.py b/test/transforms/liftings/simplicial/test_SimplicialNeighborhoodLifting.py index 5a03f67e..e21b8f99 100644 --- a/test/transforms/liftings/simplicial/test_SimplicialNeighborhoodLifting.py +++ b/test/transforms/liftings/simplicial/test_SimplicialNeighborhoodLifting.py @@ -2,19 +2,46 @@ import torch -from topobenchmark.transforms.liftings.graph2simplicial import ( +from topobenchmark.data.utils import ( + Complex2Dict, + Data2NxGraph, + TnxComplex2Complex, +) +from topobenchmark.transforms.feature_liftings.projection_sum import ( + ProjectionSum, +) +from topobenchmark.transforms.liftings.base import LiftingTransform +from topobenchmark.transforms.liftings.graph2simplicial.khop import ( SimplicialKHopLifting, ) +# TODO: rename for consistency? + class TestSimplicialKHopLifting: """Test the SimplicialKHopLifting class.""" def setup_method(self): # Initialise the SimplicialKHopLifting class - self.lifting_signed = SimplicialKHopLifting(complex_dim=3, signed=True) - self.lifting_unsigned = SimplicialKHopLifting( - complex_dim=3, signed=False + data2graph = Data2NxGraph() + feature_lifting = ProjectionSum() + domain2dict = Complex2Dict() + + lifting_map = SimplicialKHopLifting(complex_dim=3) + + self.lifting_signed = LiftingTransform( + lifting=lifting_map, + feature_lifting=feature_lifting, + data2domain=data2graph, + domain2domain=TnxComplex2Complex(signed=True), + domain2dict=domain2dict, + ) + self.lifting_unsigned = LiftingTransform( + lifting=lifting_map, + feature_lifting=feature_lifting, + data2domain=data2graph, + domain2domain=TnxComplex2Complex(signed=False), + domain2dict=domain2dict, ) def test_lift_topology(self, simple_graph_1): diff --git a/test/transforms/liftings/test_GraphLifting.py b/test/transforms/liftings/test_GraphLifting.py index c7acf454..546956c9 100644 --- a/test/transforms/liftings/test_GraphLifting.py +++ b/test/transforms/liftings/test_GraphLifting.py @@ -1,21 +1,42 @@ """Test the GraphLifting class.""" -import pytest + import torch +import torch_geometric from torch_geometric.data import Data -from topobenchmark.transforms.liftings import GraphLifting +from topobenchmark.transforms.feature_liftings.projection_sum import ( + ProjectionSum, +) +from topobenchmark.transforms.liftings.base import LiftingMap, LiftingTransform + + +def _data_has_edge_attr(data: torch_geometric.data.Data) -> bool: + r"""Check if the input data object has edge attributes. + + Parameters + ---------- + data : torch_geometric.data.Data + The input data. + + Returns + ------- + bool + Whether the data object has edge attributes. + """ + return hasattr(data, "edge_attr") and data.edge_attr is not None -class ConcreteGraphLifting(GraphLifting): + +class ConcreteGraphLifting(LiftingMap): """Concrete implementation of GraphLifting for testing.""" - - def lift_topology(self, data): + + def lift(self, data): """Implement the abstract lift_topology method. - + Parameters ---------- data : torch_geometric.data.Data The input data to be lifted. - + Returns ------- dict @@ -26,86 +47,70 @@ def lift_topology(self, data): class TestGraphLifting: """Test the GraphLifting class.""" - + def setup_method(self): """Set up test fixtures before each test method. - + Creates an instance of ConcreteGraphLifting with default parameters. """ - self.lifting = ConcreteGraphLifting( - feature_lifting="ProjectionSum", - preserve_edge_attr=False + self.lifting = LiftingTransform( + ConcreteGraphLifting(), feature_lifting=ProjectionSum() ) def test_data_has_edge_attr(self): """Test _data_has_edge_attr method with different data configurations.""" - + # Test case 1: Data with edge attributes data_with_edge_attr = Data( x=torch.tensor([[1.0], [2.0]]), edge_index=torch.tensor([[0, 1], [1, 0]]), - edge_attr=torch.tensor([[1.0], [1.0]]) + edge_attr=torch.tensor([[1.0], [1.0]]), ) - assert self.lifting._data_has_edge_attr(data_with_edge_attr) is True + assert _data_has_edge_attr(data_with_edge_attr) is True # Test case 2: Data without edge attributes data_without_edge_attr = Data( x=torch.tensor([[1.0], [2.0]]), - edge_index=torch.tensor([[0, 1], [1, 0]]) + edge_index=torch.tensor([[0, 1], [1, 0]]), ) - assert self.lifting._data_has_edge_attr(data_without_edge_attr) is False + assert _data_has_edge_attr(data_without_edge_attr) is False # Test case 3: Data with edge_attr set to None data_with_none_edge_attr = Data( x=torch.tensor([[1.0], [2.0]]), edge_index=torch.tensor([[0, 1], [1, 0]]), - edge_attr=None + edge_attr=None, ) - assert self.lifting._data_has_edge_attr(data_with_none_edge_attr) is False + assert _data_has_edge_attr(data_with_none_edge_attr) is False def test_data_has_edge_attr_empty_data(self): """Test _data_has_edge_attr method with empty data object.""" empty_data = Data() - assert self.lifting._data_has_edge_attr(empty_data) is False + assert _data_has_edge_attr(empty_data) is False def test_data_has_edge_attr_different_edge_formats(self): """Test _data_has_edge_attr method with different edge attribute formats.""" - + # Test with float edge attributes data_float_attr = Data( x=torch.tensor([[1.0], [2.0]]), edge_index=torch.tensor([[0, 1], [1, 0]]), - edge_attr=torch.tensor([[0.5], [0.5]]) + edge_attr=torch.tensor([[0.5], [0.5]]), ) - assert self.lifting._data_has_edge_attr(data_float_attr) is True + assert _data_has_edge_attr(data_float_attr) is True # Test with integer edge attributes data_int_attr = Data( x=torch.tensor([[1.0], [2.0]]), edge_index=torch.tensor([[0, 1], [1, 0]]), - edge_attr=torch.tensor([[1], [1]], dtype=torch.long) + edge_attr=torch.tensor([[1], [1]], dtype=torch.long), ) - assert self.lifting._data_has_edge_attr(data_int_attr) is True + assert _data_has_edge_attr(data_int_attr) is True # Test with multi-dimensional edge attributes data_multidim_attr = Data( x=torch.tensor([[1.0], [2.0]]), edge_index=torch.tensor([[0, 1], [1, 0]]), - edge_attr=torch.tensor([[1.0, 2.0], [2.0, 1.0]]) - ) - assert self.lifting._data_has_edge_attr(data_multidim_attr) is True - - @pytest.mark.parametrize("preserve_edge_attr", [True, False]) - def test_init_preserve_edge_attr(self, preserve_edge_attr): - """Test initialization with different preserve_edge_attr values. - - Parameters - ---------- - preserve_edge_attr : bool - Boolean value to test initialization with True and False values. - """ - lifting = ConcreteGraphLifting( - feature_lifting="ProjectionSum", - preserve_edge_attr=preserve_edge_attr + edge_attr=torch.tensor([[1.0, 2.0], [2.0, 1.0]]), ) - assert lifting.preserve_edge_attr == preserve_edge_attr \ No newline at end of file + assert _data_has_edge_attr(data_multidim_attr) is True From 5ac166f34f47a176931ff20d6c55830086bd77e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20F=2E=20Pereira?= Date: Tue, 14 Jan 2025 17:47:16 -0800 Subject: [PATCH 13/43] Remove dead code --- .../liftings/test_AbstractLifting.py | 53 ------ topobenchmark/transforms/liftings/__init__.py | 20 -- topobenchmark/transforms/liftings/base.py | 58 ------ .../transforms/liftings/graph2cell/base.py | 57 ------ .../liftings/graph2hypergraph/base.py | 17 -- .../liftings/graph2simplicial/base.py | 69 ------- topobenchmark/transforms/liftings/liftings.py | 172 ------------------ 7 files changed, 446 deletions(-) delete mode 100644 test/transforms/liftings/test_AbstractLifting.py delete mode 100755 topobenchmark/transforms/liftings/graph2cell/base.py delete mode 100755 topobenchmark/transforms/liftings/graph2hypergraph/base.py delete mode 100755 topobenchmark/transforms/liftings/graph2simplicial/base.py delete mode 100644 topobenchmark/transforms/liftings/liftings.py diff --git a/test/transforms/liftings/test_AbstractLifting.py b/test/transforms/liftings/test_AbstractLifting.py deleted file mode 100644 index 49167cb1..00000000 --- a/test/transforms/liftings/test_AbstractLifting.py +++ /dev/null @@ -1,53 +0,0 @@ -"""Test AbstractLifting module.""" - -import pytest -import torch -from torch_geometric.data import Data -from topobenchmark.transforms.liftings import AbstractLifting - -class TestAbstractLifting: - """Test the AbstractLifting class.""" - - def setup_method(self): - """Set up test fixtures for each test method. - - Creates a concrete subclass of AbstractLifting for testing purposes. - """ - class ConcreteLifting(AbstractLifting): - """Concrete implementation of AbstractLifting for testing.""" - - def lift_topology(self, data): - """Implementation of abstract method that calls parent's method. - - Parameters - ---------- - data : torch_geometric.data.Data - The input data to be lifted. - - Returns - ------- - dict - Empty dictionary as this is just for testing. - - Raises - ------ - NotImplementedError - Always raises this error as it calls the parent's abstract method. - """ - return super().lift_topology(data) - - self.lifting = ConcreteLifting(feature_lifting=None) - - def test_lift_topology_raises_not_implemented(self): - """Test that the abstract lift_topology method raises NotImplementedError. - - Verifies that calling lift_topology on an abstract class implementation - raises NotImplementedError as expected. - """ - dummy_data = Data( - x=torch.tensor([[1.0], [2.0]]), - edge_index=torch.tensor([[0, 1], [1, 0]]) - ) - - with pytest.raises(NotImplementedError): - self.lifting.lift_topology(dummy_data) \ No newline at end of file diff --git a/topobenchmark/transforms/liftings/__init__.py b/topobenchmark/transforms/liftings/__init__.py index 4692ceaf..0776fee4 100755 --- a/topobenchmark/transforms/liftings/__init__.py +++ b/topobenchmark/transforms/liftings/__init__.py @@ -1,21 +1 @@ """This module implements the liftings for the topological transforms.""" - -from .base import AbstractLifting -from .liftings import ( - CellComplexLifting, - CombinatorialLifting, - GraphLifting, - HypergraphLifting, - PointCloudLifting, - SimplicialLifting, -) - -__all__ = [ - "AbstractLifting", - "CellComplexLifting", - "CombinatorialLifting", - "GraphLifting", - "HypergraphLifting", - "PointCloudLifting", - "SimplicialLifting", -] diff --git a/topobenchmark/transforms/liftings/base.py b/topobenchmark/transforms/liftings/base.py index d5c78dc9..ce3d1ab4 100644 --- a/topobenchmark/transforms/liftings/base.py +++ b/topobenchmark/transforms/liftings/base.py @@ -5,7 +5,6 @@ import torch_geometric from topobenchmark.data.utils import IdentityAdapter -from topobenchmark.transforms.feature_liftings import FEATURE_LIFTINGS from topobenchmark.transforms.feature_liftings.identity import Identity @@ -29,7 +28,6 @@ class LiftingTransform(torch_geometric.transforms.BaseTransform): Feature lifting map. """ - # NB: emulates previous AbstractLifting def __init__( self, lifting, @@ -79,7 +77,6 @@ def forward( lifted_topology = self.feature_lifting(lifted_topology) lifted_topology_dict = self.domain2dict(lifted_topology) - # TODO: make this line more clear return torch_geometric.data.Data( **initial_data, **lifted_topology_dict ) @@ -98,58 +95,3 @@ def __call__(self, domain): @abc.abstractmethod def lift(self, domain): """Lift domain.""" - - -class AbstractLifting(torch_geometric.transforms.BaseTransform): - r"""Abstract class for topological liftings. - - Parameters - ---------- - feature_lifting : str, optional - The feature lifting method to be used. Default is 'ProjectionSum'. - **kwargs : optional - Additional arguments for the class. - """ - - # TODO: delete - - def __init__(self, feature_lifting=None, **kwargs): - super().__init__() - self.feature_lifting = FEATURE_LIFTINGS[feature_lifting]() - self.neighborhoods = kwargs.get("neighborhoods") - - @abc.abstractmethod - def lift_topology(self, data: torch_geometric.data.Data) -> dict: - r"""Lift the topology of a graph to higher-order topological domains. - - Parameters - ---------- - data : torch_geometric.data.Data - The input data to be lifted. - - Returns - ------- - dict - The lifted topology. - """ - raise NotImplementedError - - def forward( - self, data: torch_geometric.data.Data - ) -> torch_geometric.data.Data: - r"""Apply the full lifting (topology + features) to the input data. - - Parameters - ---------- - data : torch_geometric.data.Data - The input data to be lifted. - - Returns - ------- - torch_geometric.data.Data - The lifted data. - """ - initial_data = data.to_dict() - lifted_topology = self.lift_topology(data) - lifted_topology = self.feature_lifting(lifted_topology) - return torch_geometric.data.Data(**initial_data, **lifted_topology) diff --git a/topobenchmark/transforms/liftings/graph2cell/base.py b/topobenchmark/transforms/liftings/graph2cell/base.py deleted file mode 100755 index aeff3646..00000000 --- a/topobenchmark/transforms/liftings/graph2cell/base.py +++ /dev/null @@ -1,57 +0,0 @@ -"""Abstract class for lifting graphs to cell complexes.""" - -import networkx as nx -import torch -from toponetx.classes import CellComplex - -from topobenchmark.data.utils.utils import get_complex_connectivity -from topobenchmark.transforms.liftings import GraphLifting - - -class Graph2CellLifting(GraphLifting): - r"""Abstract class for lifting graphs to cell complexes. - - Parameters - ---------- - complex_dim : int, optional - The dimension of the cell complex to be generated. Default is 2. - **kwargs : optional - Additional arguments for the class. - """ - - def __init__(self, complex_dim=2, **kwargs): - super().__init__(**kwargs) - self.complex_dim = complex_dim - self.type = "graph2cell" - - def _get_lifted_topology( - self, cell_complex: CellComplex, graph: nx.Graph - ) -> dict: - r"""Return the lifted topology. - - Parameters - ---------- - cell_complex : CellComplex - The cell complex. - graph : nx.Graph - The input graph. - - Returns - ------- - dict - The lifted topology. - """ - lifted_topology = get_complex_connectivity( - cell_complex, self.complex_dim, neighborhoods=self.neighborhoods - ) - lifted_topology["x_0"] = torch.stack( - list(cell_complex.get_cell_attributes("features", 0).values()) - ) - # If new edges have been added during the lifting process, we discard the edge attributes - if self.contains_edge_attr and cell_complex.shape[1] == ( - graph.number_of_edges() - ): - lifted_topology["x_1"] = torch.stack( - list(cell_complex.get_cell_attributes("features", 1).values()) - ) - return lifted_topology diff --git a/topobenchmark/transforms/liftings/graph2hypergraph/base.py b/topobenchmark/transforms/liftings/graph2hypergraph/base.py deleted file mode 100755 index e060e30e..00000000 --- a/topobenchmark/transforms/liftings/graph2hypergraph/base.py +++ /dev/null @@ -1,17 +0,0 @@ -"""Abstract class for lifting graphs to hypergraphs.""" - -from topobenchmark.transforms.liftings import GraphLifting - - -class Graph2HypergraphLifting(GraphLifting): - r"""Abstract class for lifting graphs to hypergraphs. - - Parameters - ---------- - **kwargs : optional - Additional arguments for the class. - """ - - def __init__(self, **kwargs): - super().__init__(**kwargs) - self.type = "graph2hypergraph" diff --git a/topobenchmark/transforms/liftings/graph2simplicial/base.py b/topobenchmark/transforms/liftings/graph2simplicial/base.py deleted file mode 100755 index e52449dc..00000000 --- a/topobenchmark/transforms/liftings/graph2simplicial/base.py +++ /dev/null @@ -1,69 +0,0 @@ -"""Abstract class for lifting graphs to simplicial complexes.""" - -import networkx as nx -import torch -from toponetx.classes import SimplicialComplex - -from topobenchmark.data.utils.utils import get_complex_connectivity -from topobenchmark.transforms.liftings import GraphLifting - - -class Graph2SimplicialLifting(GraphLifting): - r"""Abstract class for lifting graphs to simplicial complexes. - - Parameters - ---------- - complex_dim : int, optional - The maximum dimension of the simplicial complex to be generated. Default is 2. - **kwargs : optional - Additional arguments for the class. - """ - - def __init__(self, complex_dim=2, **kwargs): - super().__init__(**kwargs) - self.complex_dim = complex_dim - self.type = "graph2simplicial" - self.signed = kwargs.get("signed", False) - - def _get_lifted_topology( - self, simplicial_complex: SimplicialComplex, graph: nx.Graph - ) -> dict: - r"""Return the lifted topology. - - Parameters - ---------- - simplicial_complex : SimplicialComplex - The simplicial complex. - graph : nx.Graph - The input graph. - - Returns - ------- - dict - The lifted topology. - """ - lifted_topology = get_complex_connectivity( - simplicial_complex, - self.complex_dim, - neighborhoods=self.neighborhoods, - signed=self.signed, - ) - lifted_topology["x_0"] = torch.stack( - list( - simplicial_complex.get_simplex_attributes( - "features", 0 - ).values() - ) - ) - # If new edges have been added during the lifting process, we discard the edge attributes - if self.contains_edge_attr and simplicial_complex.shape[1] == ( - graph.number_of_edges() - ): - lifted_topology["x_1"] = torch.stack( - list( - simplicial_complex.get_simplex_attributes( - "features", 1 - ).values() - ) - ) - return lifted_topology diff --git a/topobenchmark/transforms/liftings/liftings.py b/topobenchmark/transforms/liftings/liftings.py deleted file mode 100644 index 9453eaa3..00000000 --- a/topobenchmark/transforms/liftings/liftings.py +++ /dev/null @@ -1,172 +0,0 @@ -"""This module implements the abstract classes for lifting graphs.""" - -import networkx as nx -import torch_geometric -from torch_geometric.utils.undirected import is_undirected, to_undirected - -from topobenchmark.transforms.liftings import AbstractLifting - - -class GraphLifting(AbstractLifting): - r"""Abstract class for lifting graph topologies to other domains. - - Parameters - ---------- - feature_lifting : str, optional - The feature lifting method to be used. Default is 'ProjectionSum'. - preserve_edge_attr : bool, optional - Whether to preserve edge attributes. Default is False. - **kwargs : optional - Additional arguments for the class. - """ - - def __init__( - self, - feature_lifting="ProjectionSum", - preserve_edge_attr=False, - **kwargs, - ): - super().__init__(feature_lifting=feature_lifting, **kwargs) - self.preserve_edge_attr = preserve_edge_attr - - def _data_has_edge_attr(self, data: torch_geometric.data.Data) -> bool: - r"""Check if the input data object has edge attributes. - - Parameters - ---------- - data : torch_geometric.data.Data - The input data. - - Returns - ------- - bool - Whether the data object has edge attributes. - """ - return hasattr(data, "edge_attr") and data.edge_attr is not None - - def _generate_graph_from_data( - self, data: torch_geometric.data.Data - ) -> nx.Graph: - r"""Generate a NetworkX graph from the input data object. - - Parameters - ---------- - data : torch_geometric.data.Data - The input data. - - Returns - ------- - nx.Graph - The generated NetworkX graph. - """ - # Check if data object have edge_attr, return list of tuples as [(node_id, {'features':data}, 'dim':1)] or ?? - nodes = [ - (n, dict(features=data.x[n], dim=0)) - for n in range(data.x.shape[0]) - ] - - if self.preserve_edge_attr and self._data_has_edge_attr(data): - # In case edge features are given, assign features to every edge - edge_index, edge_attr = ( - data.edge_index, - ( - data.edge_attr - if is_undirected(data.edge_index, data.edge_attr) - else to_undirected(data.edge_index, data.edge_attr) - ), - ) - edges = [ - (i.item(), j.item(), dict(features=edge_attr[edge_idx], dim=1)) - for edge_idx, (i, j) in enumerate( - zip(edge_index[0], edge_index[1], strict=False) - ) - ] - self.contains_edge_attr = True - else: - # If edge_attr is not present, return list list of edges - edges = [ - (i.item(), j.item(), {}) - for i, j in zip( - data.edge_index[0], data.edge_index[1], strict=False - ) - ] - self.contains_edge_attr = False - graph = nx.Graph() - graph.add_nodes_from(nodes) - graph.add_edges_from(edges) - return graph - - -class PointCloudLifting(AbstractLifting): - r"""Abstract class for lifting point clouds to other topological domains. - - Parameters - ---------- - feature_lifting : str, optional - The feature lifting method to be used. Default is 'ProjectionSum'. - **kwargs : optional - Additional arguments for the class. - """ - - def __init__(self, feature_lifting="ProjectionSum", **kwargs): - super().__init__(feature_lifting=feature_lifting, **kwargs) - - -class CellComplexLifting(AbstractLifting): - r"""Abstract class for lifting cell complexes to other domains. - - Parameters - ---------- - feature_lifting : str, optional - The feature lifting method to be used. Default is 'ProjectionSum'. - **kwargs : optional - Additional arguments for the class. - """ - - def __init__(self, feature_lifting="ProjectionSum", **kwargs): - super().__init__(feature_lifting=feature_lifting, **kwargs) - - -class SimplicialLifting(AbstractLifting): - r"""Abstract class for lifting simplicial complexes to other domains. - - Parameters - ---------- - feature_lifting : str, optional - The feature lifting method to be used. Default is 'ProjectionSum'. - **kwargs : optional - Additional arguments for the class. - """ - - def __init__(self, feature_lifting="ProjectionSum", **kwargs): - super().__init__(feature_lifting=feature_lifting, **kwargs) - - -class HypergraphLifting(AbstractLifting): - r"""Abstract class for lifting hypergraphs to other domains. - - Parameters - ---------- - feature_lifting : str, optional - The feature lifting method to be used. Default is 'ProjectionSum'. - **kwargs : optional - Additional arguments for the class. - """ - - def __init__(self, feature_lifting="ProjectionSum", **kwargs): - super().__init__(feature_lifting=feature_lifting, **kwargs) - - -class CombinatorialLifting(AbstractLifting): - r"""Abstract class for lifting combinatorial complexes to other domains. - - Parameters - ---------- - feature_lifting : str, optional - The feature lifting method to be used. Default is 'ProjectionSum'. - **kwargs : optional - Additional arguments for the class. - """ - - def __init__(self, feature_lifting="ProjectionSum", **kwargs): - super().__init__(feature_lifting=feature_lifting, **kwargs) From 305f4861670240717d677f87454c1344250e4286 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20F=2E=20Pereira?= Date: Wed, 15 Jan 2025 19:10:25 -0800 Subject: [PATCH 14/43] Update TRANSFORMS automatically dict creation/imports --- topobenchmark/transforms/__init__.py | 29 +---- topobenchmark/transforms/_utils.py | 53 +++++++++ .../transforms/data_manipulations/__init__.py | 83 +------------- .../transforms/feature_liftings/__init__.py | 104 +----------------- .../transforms/feature_liftings/identity.py | 2 +- .../feature_liftings/projection_sum.py | 6 +- topobenchmark/transforms/liftings/__init__.py | 14 +++ .../liftings/graph2cell/__init__.py | 99 ++--------------- .../liftings/graph2hypergraph/__init__.py | 99 ++--------------- .../liftings/graph2simplicial/__init__.py | 99 ++--------------- 10 files changed, 104 insertions(+), 484 deletions(-) create mode 100644 topobenchmark/transforms/_utils.py diff --git a/topobenchmark/transforms/__init__.py b/topobenchmark/transforms/__init__.py index 3f568814..62f8d85e 100755 --- a/topobenchmark/transforms/__init__.py +++ b/topobenchmark/transforms/__init__.py @@ -1,32 +1,11 @@ """This module contains the transforms for the topobenchmark package.""" -from typing import Any +from .data_manipulations import DATA_MANIPULATIONS +from .feature_liftings import FEATURE_LIFTINGS +from .liftings import LIFTINGS -from topobenchmark.transforms.data_manipulations import DATA_MANIPULATIONS -from topobenchmark.transforms.feature_liftings import FEATURE_LIFTINGS -from topobenchmark.transforms.liftings.graph2cell import GRAPH2CELL_LIFTINGS -from topobenchmark.transforms.liftings.graph2hypergraph import ( - GRAPH2HYPERGRAPH_LIFTINGS, -) -from topobenchmark.transforms.liftings.graph2simplicial import ( - GRAPH2SIMPLICIAL_LIFTINGS, -) - -LIFTINGS = { - **GRAPH2CELL_LIFTINGS, - **GRAPH2HYPERGRAPH_LIFTINGS, - **GRAPH2SIMPLICIAL_LIFTINGS, -} - -TRANSFORMS: dict[Any, Any] = { +TRANSFORMS = { **LIFTINGS, **FEATURE_LIFTINGS, **DATA_MANIPULATIONS, } - -__all__ = [ - "DATA_MANIPULATIONS", - "FEATURE_LIFTINGS", - "LIFTINGS", - "TRANSFORMS", -] diff --git a/topobenchmark/transforms/_utils.py b/topobenchmark/transforms/_utils.py new file mode 100644 index 00000000..f14d156e --- /dev/null +++ b/topobenchmark/transforms/_utils.py @@ -0,0 +1,53 @@ +import inspect +from importlib import util +from pathlib import Path + + +def discover_objs(package_path, condition=None): + """Dynamically discover all manipulation classes in the package. + + Parameters + ---------- + package_path : str + Path to the package's __init__.py file. + condition : callable + `(name, obj) -> bool` + + Returns + ------- + dict[str, type] + Dictionary mapping class names to their corresponding class objects. + """ + if condition is None: + condition = lambda name, obj: True + + objs = {} + + # Get the directory containing the manipulation modules + package_dir = Path(package_path).parent + + # Iterate through all .py files in the directory + for file_path in package_dir.glob("*.py"): + if file_path.stem == "__init__": + continue + + # Import the module + module_name = f"{Path(package_path).stem}.{file_path.stem}" + spec = util.spec_from_file_location(module_name, file_path) + if spec and spec.loader: + module = util.module_from_spec(spec) + spec.loader.exec_module(module) + + # Find all manipulation classes in the module + for name, obj in inspect.getmembers(module): + if ( + not inspect.isclass(obj) + or name.startswith("_") + or obj.__module__ != module.__name__ + ): + continue + + if condition(name, obj): + objs[name] = obj + + return objs diff --git a/topobenchmark/transforms/data_manipulations/__init__.py b/topobenchmark/transforms/data_manipulations/__init__.py index a17e506d..314d5fa6 100644 --- a/topobenchmark/transforms/data_manipulations/__init__.py +++ b/topobenchmark/transforms/data_manipulations/__init__.py @@ -1,86 +1,7 @@ """Data manipulations module with automated exports.""" -import inspect -from importlib import util -from pathlib import Path -from typing import Any +from topobenchmark.transforms._utils import discover_objs +DATA_MANIPULATIONS = discover_objs(__file__) -class ModuleExportsManager: - """Manages automatic discovery and registration of data manipulation classes.""" - - @staticmethod - def is_manipulation_class(obj: Any) -> bool: - """Check if an object is a valid manipulation class. - - Parameters - ---------- - obj : Any - The object to check if it's a valid manipulation class. - - Returns - ------- - bool - True if the object is a valid manipulation class (non-private class - defined in __main__), False otherwise. - """ - return ( - inspect.isclass(obj) - and obj.__module__ == "__main__" - and not obj.__name__.startswith("_") - ) - - @classmethod - def discover_manipulations(cls, package_path: str) -> dict[str, type]: - """Dynamically discover all manipulation classes in the package. - - Parameters - ---------- - package_path : str - Path to the package's __init__.py file. - - Returns - ------- - dict[str, type] - Dictionary mapping class names to their corresponding class objects. - """ - manipulations = {} - - # Get the directory containing the manipulation modules - package_dir = Path(package_path).parent - - # Iterate through all .py files in the directory - for file_path in package_dir.glob("*.py"): - if file_path.stem == "__init__": - continue - - # Import the module - module_name = f"{Path(package_path).stem}.{file_path.stem}" - spec = util.spec_from_file_location(module_name, file_path) - if spec and spec.loader: - module = util.module_from_spec(spec) - spec.loader.exec_module(module) - - # Find all manipulation classes in the module - for name, obj in inspect.getmembers(module): - if ( - inspect.isclass(obj) - and obj.__module__ == module.__name__ - and not name.startswith("_") - ): - manipulations[name] = obj # noqa: PERF403 - - return manipulations - - -# Create the exports manager -manager = ModuleExportsManager() - -# Automatically discover and populate DATA_MANIPULATIONS -DATA_MANIPULATIONS = manager.discover_manipulations(__file__) - -# Automatically generate __all__ -__all__ = [*DATA_MANIPULATIONS.keys(), "DATA_MANIPULATIONS"] - -# For backwards compatibility, also create individual imports locals().update(DATA_MANIPULATIONS) diff --git a/topobenchmark/transforms/feature_liftings/__init__.py b/topobenchmark/transforms/feature_liftings/__init__.py index ec4f763c..6e047683 100644 --- a/topobenchmark/transforms/feature_liftings/__init__.py +++ b/topobenchmark/transforms/feature_liftings/__init__.py @@ -1,104 +1,12 @@ """Feature lifting transforms with automated exports.""" -import inspect -from importlib import util -from pathlib import Path -from typing import Any +from topobenchmark.transforms._utils import discover_objs -from .identity import Identity # Import Identity for special case +from .base import FeatureLiftingMap - -class ModuleExportsManager: - """Manages automatic discovery and registration of feature lifting classes.""" - - @staticmethod - def is_lifting_class(obj: Any) -> bool: - """Check if an object is a valid lifting class. - - Parameters - ---------- - obj : Any - The object to check if it's a valid lifting class. - - Returns - ------- - bool - True if the object is a valid lifting class (non-private class - defined in __main__), False otherwise. - """ - return ( - inspect.isclass(obj) - and obj.__module__ == "__main__" - and not obj.__name__.startswith("_") - ) - - @classmethod - def discover_liftings( - cls, package_path: str, special_cases: dict[Any, type] | None = None - ) -> dict[str, type]: - """Dynamically discover all lifting classes in the package. - - Parameters - ---------- - package_path : str - Path to the package's __init__.py file. - special_cases : Optional[dict[Any, type]] - Dictionary of special case mappings (e.g., {None: Identity}), - by default None. - - Returns - ------- - dict[str, type] - Dictionary mapping class names to their corresponding class objects, - including any special cases if provided. - """ - liftings = {} - - # Get the directory containing the lifting modules - package_dir = Path(package_path).parent - - # Iterate through all .py files in the directory - for file_path in package_dir.glob("*.py"): - if file_path.stem == "__init__": - continue - - # Import the module - module_name = f"{Path(package_path).stem}.{file_path.stem}" - spec = util.spec_from_file_location(module_name, file_path) - if spec and spec.loader: - module = util.module_from_spec(spec) - spec.loader.exec_module(module) - - # Find all lifting classes in the module - for name, obj in inspect.getmembers(module): - if ( - inspect.isclass(obj) - and obj.__module__ == module.__name__ - and not name.startswith("_") - ): - liftings[name] = obj # noqa: PERF403 - - # Add special cases if provided - if special_cases: - liftings.update(special_cases) - - return liftings - - -# Create the exports manager -manager = ModuleExportsManager() - -# Automatically discover and populate FEATURE_LIFTINGS with special case for None -FEATURE_LIFTINGS = manager.discover_liftings( - __file__, special_cases={None: Identity} +FEATURE_LIFTINGS = discover_objs( + __file__, + condition=lambda name, obj: issubclass(obj, FeatureLiftingMap), ) -# Automatically generate __all__ (excluding None key) -__all__ = [name for name in FEATURE_LIFTINGS if isinstance(name, str)] + [ - "FEATURE_LIFTINGS" -] - -# For backwards compatibility, create individual imports (excluding None key) -locals().update( - {k: v for k, v in FEATURE_LIFTINGS.items() if isinstance(k, str)} -) +locals().update(FEATURE_LIFTINGS) diff --git a/topobenchmark/transforms/feature_liftings/identity.py b/topobenchmark/transforms/feature_liftings/identity.py index 9abf4e5d..e640bd06 100644 --- a/topobenchmark/transforms/feature_liftings/identity.py +++ b/topobenchmark/transforms/feature_liftings/identity.py @@ -1,6 +1,6 @@ """Identity transform that does nothing to the input data.""" -from .base import FeatureLiftingMap +from topobenchmark.transforms.feature_liftings.base import FeatureLiftingMap class Identity(FeatureLiftingMap): diff --git a/topobenchmark/transforms/feature_liftings/projection_sum.py b/topobenchmark/transforms/feature_liftings/projection_sum.py index a02a1db5..a756fd0e 100644 --- a/topobenchmark/transforms/feature_liftings/projection_sum.py +++ b/topobenchmark/transforms/feature_liftings/projection_sum.py @@ -2,7 +2,7 @@ import torch -from .base import FeatureLiftingMap +from topobenchmark.transforms.feature_liftings.base import FeatureLiftingMap class ProjectionSum(FeatureLiftingMap): @@ -13,12 +13,12 @@ def lift_features(self, domain): Parameters ---------- - data : PlainComplex + data : Complex The input data to be lifted. Returns ------- - PlainComplex + Complex Domain with the lifted features. """ for rank in range(domain.max_rank - 1): diff --git a/topobenchmark/transforms/liftings/__init__.py b/topobenchmark/transforms/liftings/__init__.py index 0776fee4..513f5035 100755 --- a/topobenchmark/transforms/liftings/__init__.py +++ b/topobenchmark/transforms/liftings/__init__.py @@ -1 +1,15 @@ """This module implements the liftings for the topological transforms.""" + +from .base import LiftingTransform # noqa: F401 +from .graph2cell import GRAPH2CELL_LIFTINGS +from .graph2hypergraph import GRAPH2HYPERGRAPH_LIFTINGS +from .graph2simplicial import GRAPH2SIMPLICIAL_LIFTINGS + +LIFTINGS = { + **GRAPH2CELL_LIFTINGS, + **GRAPH2HYPERGRAPH_LIFTINGS, + **GRAPH2SIMPLICIAL_LIFTINGS, +} + + +locals().update(LIFTINGS) diff --git a/topobenchmark/transforms/liftings/graph2cell/__init__.py b/topobenchmark/transforms/liftings/graph2cell/__init__.py index d0faae96..480ada64 100755 --- a/topobenchmark/transforms/liftings/graph2cell/__init__.py +++ b/topobenchmark/transforms/liftings/graph2cell/__init__.py @@ -1,96 +1,11 @@ """Graph2Cell liftings with automated exports.""" -import inspect -from importlib import util -from pathlib import Path -from typing import Any +from topobenchmark.transforms._utils import discover_objs +from topobenchmark.transforms.liftings.base import LiftingMap -from .base import Graph2CellLifting +GRAPH2CELL_LIFTINGS = discover_objs( + __file__, + condition=lambda name, obj: issubclass(obj, LiftingMap), +) - -class ModuleExportsManager: - """Manages automatic discovery and registration of Graph2Cell lifting classes.""" - - @staticmethod - def is_lifting_class(obj: Any) -> bool: - """Check if an object is a valid Graph2Cell lifting class. - - Parameters - ---------- - obj : Any - The object to check if it's a valid lifting class. - - Returns - ------- - bool - True if the object is a valid Graph2Cell lifting class (non-private class - inheriting from Graph2CellLifting), False otherwise. - """ - return ( - inspect.isclass(obj) - and obj.__module__ == "__main__" - and not obj.__name__.startswith("_") - and issubclass(obj, Graph2CellLifting) - and obj != Graph2CellLifting - ) - - @classmethod - def discover_liftings(cls, package_path: str) -> dict[str, type]: - """Dynamically discover all Graph2Cell lifting classes in the package. - - Parameters - ---------- - package_path : str - Path to the package's __init__.py file. - - Returns - ------- - dict[str, type] - Dictionary mapping class names to their corresponding class objects. - """ - liftings = {} - - # Get the directory containing the lifting modules - package_dir = Path(package_path).parent - - # Iterate through all .py files in the directory - for file_path in package_dir.glob("*.py"): - if file_path.stem == "__init__": - continue - - # Import the module - module_name = f"{Path(package_path).stem}.{file_path.stem}" - spec = util.spec_from_file_location(module_name, file_path) - if spec and spec.loader: - module = util.module_from_spec(spec) - spec.loader.exec_module(module) - - # Find all lifting classes in the module - for name, obj in inspect.getmembers(module): - if ( - inspect.isclass(obj) - and obj.__module__ == module.__name__ - and not name.startswith("_") - and issubclass(obj, Graph2CellLifting) - and obj != Graph2CellLifting - ): - liftings[name] = obj # noqa: PERF403 - - return liftings - - -# Create the exports manager -manager = ModuleExportsManager() - -# Automatically discover and populate GRAPH2CELL_LIFTINGS -GRAPH2CELL_LIFTINGS = manager.discover_liftings(__file__) - -# Automatically generate __all__ -__all__ = [ - *GRAPH2CELL_LIFTINGS.keys(), - "Graph2CellLifting", - "GRAPH2CELL_LIFTINGS", -] - -# For backwards compatibility, create individual imports -locals().update(**GRAPH2CELL_LIFTINGS) +locals().update(GRAPH2CELL_LIFTINGS) diff --git a/topobenchmark/transforms/liftings/graph2hypergraph/__init__.py b/topobenchmark/transforms/liftings/graph2hypergraph/__init__.py index acb89e0c..e7a5a815 100755 --- a/topobenchmark/transforms/liftings/graph2hypergraph/__init__.py +++ b/topobenchmark/transforms/liftings/graph2hypergraph/__init__.py @@ -1,96 +1,11 @@ """Graph2HypergraphLifting module with automated exports.""" -import inspect -from importlib import util -from pathlib import Path -from typing import Any +from topobenchmark.transforms._utils import discover_objs +from topobenchmark.transforms.liftings.base import LiftingMap -from .base import Graph2HypergraphLifting +GRAPH2HYPERGRAPH_LIFTINGS = discover_objs( + __file__, + condition=lambda name, obj: issubclass(obj, LiftingMap), +) - -class ModuleExportsManager: - """Manages automatic discovery and registration of Graph2Hypergraph lifting classes.""" - - @staticmethod - def is_lifting_class(obj: Any) -> bool: - """Check if an object is a valid Graph2Hypergraph lifting class. - - Parameters - ---------- - obj : Any - The object to check if it's a valid lifting class. - - Returns - ------- - bool - True if the object is a valid Graph2Hypergraph lifting class (non-private class - inheriting from Graph2HypergraphLifting), False otherwise. - """ - return ( - inspect.isclass(obj) - and obj.__module__ == "__main__" - and not obj.__name__.startswith("_") - and issubclass(obj, Graph2HypergraphLifting) - and obj != Graph2HypergraphLifting - ) - - @classmethod - def discover_liftings(cls, package_path: str) -> dict[str, type]: - """Dynamically discover all Graph2Hypergraph lifting classes in the package. - - Parameters - ---------- - package_path : str - Path to the package's __init__.py file. - - Returns - ------- - dict[str, type] - Dictionary mapping class names to their corresponding class objects. - """ - liftings = {} - - # Get the directory containing the lifting modules - package_dir = Path(package_path).parent - - # Iterate through all .py files in the directory - for file_path in package_dir.glob("*.py"): - if file_path.stem == "__init__": - continue - - # Import the module - module_name = f"{Path(package_path).stem}.{file_path.stem}" - spec = util.spec_from_file_location(module_name, file_path) - if spec and spec.loader: - module = util.module_from_spec(spec) - spec.loader.exec_module(module) - - # Find all lifting classes in the module - for name, obj in inspect.getmembers(module): - if ( - inspect.isclass(obj) - and obj.__module__ == module.__name__ - and not name.startswith("_") - and issubclass(obj, Graph2HypergraphLifting) - and obj != Graph2HypergraphLifting - ): - liftings[name] = obj # noqa: PERF403 - - return liftings - - -# Create the exports manager -manager = ModuleExportsManager() - -# Automatically discover and populate GRAPH2HYPERGRAPH_LIFTINGS -GRAPH2HYPERGRAPH_LIFTINGS = manager.discover_liftings(__file__) - -# Automatically generate __all__ -__all__ = [ - *GRAPH2HYPERGRAPH_LIFTINGS.keys(), - "Graph2HypergraphLifting", - "GRAPH2HYPERGRAPH_LIFTINGS", -] - -# For backwards compatibility, create individual imports -locals().update(**GRAPH2HYPERGRAPH_LIFTINGS) +locals().update(GRAPH2HYPERGRAPH_LIFTINGS) diff --git a/topobenchmark/transforms/liftings/graph2simplicial/__init__.py b/topobenchmark/transforms/liftings/graph2simplicial/__init__.py index 238691cd..9e77797b 100755 --- a/topobenchmark/transforms/liftings/graph2simplicial/__init__.py +++ b/topobenchmark/transforms/liftings/graph2simplicial/__init__.py @@ -1,96 +1,11 @@ """Graph2SimplicialLifting module with automated exports.""" -import inspect -from importlib import util -from pathlib import Path -from typing import Any +from topobenchmark.transforms._utils import discover_objs +from topobenchmark.transforms.liftings.base import LiftingMap -from .base import Graph2SimplicialLifting +GRAPH2SIMPLICIAL_LIFTINGS = discover_objs( + __file__, + condition=lambda name, obj: issubclass(obj, LiftingMap), +) - -class ModuleExportsManager: - """Manages automatic discovery and registration of Graph2Simplicial lifting classes.""" - - @staticmethod - def is_lifting_class(obj: Any) -> bool: - """Check if an object is a valid Graph2Simplicial lifting class. - - Parameters - ---------- - obj : Any - The object to check if it's a valid lifting class. - - Returns - ------- - bool - True if the object is a valid Graph2Simplicial lifting class (non-private class - inheriting from Graph2SimplicialLifting), False otherwise. - """ - return ( - inspect.isclass(obj) - and obj.__module__ == "__main__" - and not obj.__name__.startswith("_") - and issubclass(obj, Graph2SimplicialLifting) - and obj != Graph2SimplicialLifting - ) - - @classmethod - def discover_liftings(cls, package_path: str) -> dict[str, type]: - """Dynamically discover all Graph2Simplicial lifting classes in the package. - - Parameters - ---------- - package_path : str - Path to the package's __init__.py file. - - Returns - ------- - dict[str, type] - Dictionary mapping class names to their corresponding class objects. - """ - liftings = {} - - # Get the directory containing the lifting modules - package_dir = Path(package_path).parent - - # Iterate through all .py files in the directory - for file_path in package_dir.glob("*.py"): - if file_path.stem == "__init__": - continue - - # Import the module - module_name = f"{Path(package_path).stem}.{file_path.stem}" - spec = util.spec_from_file_location(module_name, file_path) - if spec and spec.loader: - module = util.module_from_spec(spec) - spec.loader.exec_module(module) - - # Find all lifting classes in the module - for name, obj in inspect.getmembers(module): - if ( - inspect.isclass(obj) - and obj.__module__ == module.__name__ - and not name.startswith("_") - and issubclass(obj, Graph2SimplicialLifting) - and obj != Graph2SimplicialLifting - ): - liftings[name] = obj # noqa: PERF403 - - return liftings - - -# Create the exports manager -manager = ModuleExportsManager() - -# Automatically discover and populate GRAPH2SIMPLICIAL_LIFTINGS -GRAPH2SIMPLICIAL_LIFTINGS = manager.discover_liftings(__file__) - -# Automatically generate __all__ -__all__ = [ - *GRAPH2SIMPLICIAL_LIFTINGS.keys(), - "Graph2SimplicialLifting", - "GRAPH2SIMPLICIAL_LIFTINGS", -] - -# For backwards compatibility, create individual imports -locals().update(**GRAPH2SIMPLICIAL_LIFTINGS) +locals().update(GRAPH2SIMPLICIAL_LIFTINGS) From f77ad640d9687bb253d75becae6ea35550ea6a36 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20F=2E=20Pereira?= Date: Wed, 15 Jan 2025 19:11:52 -0800 Subject: [PATCH 15/43] Fix handling of empty matrices due to inexisting dimension --- topobenchmark/data/utils/adapters.py | 37 +++++++++++-------- topobenchmark/data/utils/domain.py | 3 ++ .../liftings/graph2simplicial/clique.py | 3 ++ 3 files changed, 27 insertions(+), 16 deletions(-) diff --git a/topobenchmark/data/utils/adapters.py b/topobenchmark/data/utils/adapters.py index 9db40c08..342a2622 100644 --- a/topobenchmark/data/utils/adapters.py +++ b/topobenchmark/data/utils/adapters.py @@ -124,8 +124,9 @@ class TnxComplex2Complex(Adapter): Parameters ---------- complex_dim : int - Dimension of the desired subcomplex. + Dimension of the (sub)complex. If ``None``, adapts the (full) complex. + If greater than dimension of complex, pads with empty matrices. neighborhoods : list, optional List of neighborhoods of interest. signed : bool, optional @@ -136,13 +137,11 @@ class TnxComplex2Complex(Adapter): def __init__( self, - complex_dim=None, neighborhoods=None, signed=False, transfer_features=True, ): super().__init__() - self.complex_dim = complex_dim self.neighborhoods = neighborhoods self.signed = signed self.transfer_features = transfer_features @@ -160,7 +159,13 @@ def adapt(self, domain): """ # NB: just a slightly rewriting of get_complex_connectivity - dim = self.complex_dim or domain.dim + practical_dim = ( + domain.practical_dim + if hasattr(domain, "practical_dim") + else domain.dim + ) + dim = domain.dim + signed = self.signed neighborhoods = self.neighborhoods @@ -174,18 +179,20 @@ def adapt(self, domain): ] practical_shape = list( - np.pad(list(domain.shape), (0, dim + 1 - len(domain.shape))) + np.pad( + list(domain.shape), (0, practical_dim + 1 - len(domain.shape)) + ) ) data = { connectivity_info: [] for connectivity_info in connectivity_infos } - for rank_idx in range(dim + 1): + for rank in range(practical_dim + 1): for connectivity_info in connectivity_infos: try: data[connectivity_info].append( from_sparse( getattr(domain, f"{connectivity_info}_matrix")( - rank=rank_idx, signed=signed + rank=rank, signed=signed ) ) ) @@ -193,15 +200,15 @@ def adapt(self, domain): if connectivity_info == "incidence": data[connectivity_info].append( generate_zero_sparse_connectivity( - m=practical_shape[rank_idx - 1], - n=practical_shape[rank_idx], + m=practical_shape[rank - 1], + n=practical_shape[rank], ) ) else: data[connectivity_info].append( generate_zero_sparse_connectivity( - m=practical_shape[rank_idx], - n=practical_shape[rank_idx], + m=practical_shape[rank], + n=practical_shape[rank], ) ) @@ -228,6 +235,9 @@ def adapt(self, domain): rank_features = None data["features"].append(rank_features) + for _ in range(dim + 1, practical_dim + 1): + data["features"].append(None) + return Complex(**data) @@ -287,9 +297,6 @@ class TnxComplex2Dict(AdapterComposition): Parameters ---------- - complex_dim : int - Dimension of the desired subcomplex. - If ``None``, adapts the (full) complex. neighborhoods : list, optional List of neighborhoods of interest. signed : bool, optional @@ -300,13 +307,11 @@ class TnxComplex2Dict(AdapterComposition): def __init__( self, - complex_dim=None, neighborhoods=None, signed=False, transfer_features=True, ): tnxcomplex2complex = TnxComplex2Complex( - complex_dim=complex_dim, neighborhoods=neighborhoods, signed=signed, transfer_features=transfer_features, diff --git a/topobenchmark/data/utils/domain.py b/topobenchmark/data/utils/domain.py index 531592dc..8bf4f1d7 100644 --- a/topobenchmark/data/utils/domain.py +++ b/topobenchmark/data/utils/domain.py @@ -46,6 +46,9 @@ def shape(self): def max_rank(self): """Maximum rank of the complex. + NB: may differ from mathematical definition due to empty + matrices. + Returns ------- int diff --git a/topobenchmark/transforms/liftings/graph2simplicial/clique.py b/topobenchmark/transforms/liftings/graph2simplicial/clique.py index 37a5cc15..04baa1ef 100755 --- a/topobenchmark/transforms/liftings/graph2simplicial/clique.py +++ b/topobenchmark/transforms/liftings/graph2simplicial/clique.py @@ -50,4 +50,7 @@ def lift(self, domain): for set_k_simplices in simplices: simplicial_complex.add_simplices_from(list(set_k_simplices)) + # because Complex pads unexisting dimensions with empty matrices + simplicial_complex.practical_dim = self.complex_dim + return simplicial_complex From 0668f97821c607f4ccff4acdcec9f1c4c0e928ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20F=2E=20Pereira?= Date: Wed, 15 Jan 2025 19:12:30 -0800 Subject: [PATCH 16/43] Update feature liftings due to new design --- .../feature_liftings/test_Concatenation.py | 24 ++-- .../feature_liftings/test_ProjectionSum.py | 59 +++++----- .../feature_liftings/test_SetLifting.py | 18 ++- .../feature_liftings/concatenation.py | 86 +++++---------- .../transforms/feature_liftings/set.py | 103 +++++++----------- 5 files changed, 125 insertions(+), 165 deletions(-) diff --git a/test/transforms/feature_liftings/test_Concatenation.py b/test/transforms/feature_liftings/test_Concatenation.py index a8f83d78..aff3a2c1 100644 --- a/test/transforms/feature_liftings/test_Concatenation.py +++ b/test/transforms/feature_liftings/test_Concatenation.py @@ -2,24 +2,34 @@ import torch -from topobenchmark.transforms.liftings.graph2simplicial import ( +from topobenchmark.data.utils import ( + Complex2Dict, + Data2NxGraph, + TnxComplex2Complex, +) +from topobenchmark.transforms.liftings import ( + LiftingTransform, SimplicialCliqueLifting, ) -class TestConcatention: +class TestConcatenation: """Test the Concatention feature lifting class.""" def setup_method(self): """Set up the test.""" # Initialize a lifting class - self.lifting = SimplicialCliqueLifting( - feature_lifting="Concatenation", complex_dim=3 + self.lifting = LiftingTransform( + SimplicialCliqueLifting(complex_dim=3), + feature_lifting="Concatenation", + data2domain=Data2NxGraph(), + domain2domain=TnxComplex2Complex(signed=False), + domain2dict=Complex2Dict(), ) def test_lift_features(self, simple_graph_0, simple_graph_1): """Test the lift_features method. - + Parameters ---------- simple_graph_0 : torch_geometric.data.Data @@ -27,12 +37,12 @@ def test_lift_features(self, simple_graph_0, simple_graph_1): simple_graph_1 : torch_geometric.data.Data A simple graph data object. """ - + data = simple_graph_0 # Test the lift_features method lifted_data = self.lifting.forward(data.clone()) assert lifted_data.x_2.shape == torch.Size([0, 6]) - + data = simple_graph_1 # Test the lift_features method lifted_data = self.lifting.forward(data.clone()) diff --git a/test/transforms/feature_liftings/test_ProjectionSum.py b/test/transforms/feature_liftings/test_ProjectionSum.py index 935a5148..b14ea5e8 100644 --- a/test/transforms/feature_liftings/test_ProjectionSum.py +++ b/test/transforms/feature_liftings/test_ProjectionSum.py @@ -2,7 +2,13 @@ import torch -from topobenchmark.transforms.liftings.graph2simplicial import ( +from topobenchmark.data.utils import ( + Complex2Dict, + Data2NxGraph, + TnxComplex2Complex, +) +from topobenchmark.transforms.liftings import ( + LiftingTransform, SimplicialCliqueLifting, ) @@ -13,13 +19,17 @@ class TestProjectionSum: def setup_method(self): """Set up the test.""" # Initialize a lifting class - self.lifting = SimplicialCliqueLifting( - feature_lifting="ProjectionSum", complex_dim=3 + self.lifting = LiftingTransform( + lifting=SimplicialCliqueLifting(complex_dim=3), + feature_lifting="ProjectionSum", + data2domain=Data2NxGraph(), + domain2domain=TnxComplex2Complex(), + domain2dict=Complex2Dict(), ) def test_lift_features(self, simple_graph_1): """Test the lift_features method. - + Parameters ---------- simple_graph_1 : torch_geometric.data.Data @@ -31,38 +41,27 @@ def test_lift_features(self, simple_graph_1): expected_x1 = torch.tensor( [ - [ 6.], - [ 11.], - [ 101.], - [5001.], - [ 15.], - [ 105.], - [ 60.], - [ 110.], - [ 510.], - [5010.], - [1050.], - [1500.], - [5500.] + [6.0], + [11.0], + [101.0], + [5001.0], + [15.0], + [105.0], + [60.0], + [110.0], + [510.0], + [5010.0], + [1050.0], + [1500.0], + [5500.0], ] ) expected_x2 = torch.tensor( - [ - [ 32.], - [ 212.], - [ 222.], - [10022.], - [ 230.], - [11020.] - ] + [[32.0], [212.0], [222.0], [10022.0], [230.0], [11020.0]] ) - expected_x3 = torch.tensor( - [ - [696.] - ] - ) + expected_x3 = torch.tensor([[696.0]]) assert ( expected_x1 == lifted_data.x_1 diff --git a/test/transforms/feature_liftings/test_SetLifting.py b/test/transforms/feature_liftings/test_SetLifting.py index 9b71816f..bf0c621f 100644 --- a/test/transforms/feature_liftings/test_SetLifting.py +++ b/test/transforms/feature_liftings/test_SetLifting.py @@ -2,7 +2,13 @@ import torch -from topobenchmark.transforms.liftings.graph2simplicial import ( +from topobenchmark.data.utils import ( + Complex2Dict, + Data2NxGraph, + TnxComplex2Complex, +) +from topobenchmark.transforms.liftings import ( + LiftingTransform, SimplicialCliqueLifting, ) @@ -13,13 +19,17 @@ class TestSetLifting: def setup_method(self): """Set up the test.""" # Initialize a lifting class - self.lifting = SimplicialCliqueLifting( - feature_lifting="Set", complex_dim=3 + self.lifting = LiftingTransform( + SimplicialCliqueLifting(complex_dim=3), + feature_lifting="Set", + data2domain=Data2NxGraph(), + domain2domain=TnxComplex2Complex(signed=False), + domain2dict=Complex2Dict(), ) def test_lift_features(self, simple_graph_1): """Test the lift_features method. - + Parameters ---------- simple_graph_1 : torch_geometric.data.Data diff --git a/topobenchmark/transforms/feature_liftings/concatenation.py b/topobenchmark/transforms/feature_liftings/concatenation.py index 5a69f46d..b26509d9 100644 --- a/topobenchmark/transforms/feature_liftings/concatenation.py +++ b/topobenchmark/transforms/feature_liftings/concatenation.py @@ -1,83 +1,53 @@ """Concatenation feature lifting.""" import torch -import torch_geometric +from topobenchmark.transforms.feature_liftings.base import FeatureLiftingMap -class Concatenation(torch_geometric.transforms.BaseTransform): - r"""Lift r-cell features to r+1-cells by concatenation. - Parameters - ---------- - **kwargs : optional - Additional arguments for the class. - """ - - def __init__(self, **kwargs): - super().__init__() +class Concatenation(FeatureLiftingMap): + """Lift r-cell features to r+1-cells by concatenation.""" def __repr__(self) -> str: return f"{self.__class__.__name__}()" - def lift_features( - self, data: torch_geometric.data.Data | dict - ) -> torch_geometric.data.Data | dict: + def lift_features(self, domain): r"""Concatenate r-cell features to obtain r+1-cell features. Parameters ---------- - data : torch_geometric.data.Data | dict + data : Complex The input data to be lifted. Returns ------- - torch_geometric.data.Data | dict - The lifted data. + Complex + Domain with the lifted features. """ - keys = sorted( - [ - key.split("_")[1] - for key in data - if "incidence" in key and "-" not in key - ] - ) - for elem in keys: - if f"x_{elem}" not in data: - idx_to_project = 0 if elem == "hyperedges" else int(elem) - 1 - incidence = data["incidence_" + elem] - _, n = incidence.shape + for rank in range(domain.max_rank - 1): + if domain.features[rank + 1] is not None: + continue - if n != 0: - idxs_list = [] - for n_feature in range(n): - idxs_for_feature = incidence.indices()[ - 0, incidence.indices()[1, :] == n_feature - ] - idxs_list.append(torch.sort(idxs_for_feature)[0]) + # TODO: different if hyperedges? + idx_to_project = rank - idxs = torch.stack(idxs_list, dim=0) - values = data[f"x_{idx_to_project}"][idxs].view(n, -1) - else: - m = data[f"x_{int(elem)-1}"].shape[1] * (int(elem) + 1) - values = torch.zeros([0, m]) + incidence = domain.incidence[rank + 1] + _, n = incidence.shape - data["x_" + elem] = values - return data + if n != 0: + idxs_list = [] + for n_feature in range(n): + idxs_for_feature = incidence.indices()[ + 0, incidence.indices()[1, :] == n_feature + ] + idxs_list.append(torch.sort(idxs_for_feature)[0]) - def forward( - self, data: torch_geometric.data.Data | dict - ) -> torch_geometric.data.Data | dict: - r"""Apply the lifting to the input data. + idxs = torch.stack(idxs_list, dim=0) + values = domain.features[idx_to_project][idxs].view(n, -1) + else: + m = domain.features[rank].shape[1] * (rank + 2) + values = torch.zeros([0, m]) - Parameters - ---------- - data : torch_geometric.data.Data | dict - The input data to be lifted. + domain.update_features(rank + 1, values) - Returns - ------- - torch_geometric.data.Data | dict - The lifted data. - """ - data = self.lift_features(data) - return data + return domain diff --git a/topobenchmark/transforms/feature_liftings/set.py b/topobenchmark/transforms/feature_liftings/set.py index 28ccd0cc..1886e25b 100644 --- a/topobenchmark/transforms/feature_liftings/set.py +++ b/topobenchmark/transforms/feature_liftings/set.py @@ -1,89 +1,60 @@ """Set lifting for r-cell features to r+1-cell features.""" import torch -import torch_geometric +from topobenchmark.transforms.feature_liftings.base import FeatureLiftingMap -class Set(torch_geometric.transforms.BaseTransform): - r"""Lift r-cell features to r+1-cells by set operations. - Parameters - ---------- - **kwargs : optional - Additional arguments for the class. - """ - - def __init__(self, **kwargs): - super().__init__() +class Set(FeatureLiftingMap): + """Lift r-cell features to r+1-cells by set operations.""" def __repr__(self) -> str: return f"{self.__class__.__name__}()" - def lift_features( - self, data: torch_geometric.data.Data | dict - ) -> torch_geometric.data.Data | dict: + def lift_features(self, domain): r"""Concatenate r-cell features to r+1-cell structures. Parameters ---------- - data : torch_geometric.data.Data | dict + data : Complex The input data to be lifted. Returns ------- - torch_geometric.data.Data | dict - The lifted data. + Complex + Domain with the lifted features. """ - keys = sorted( - [key.split("_")[1] for key in data if "incidence" in key] - ) - for elem in keys: - if f"x_{elem}" not in data: - # idx_to_project = 0 if elem == "hyperedges" else int(elem) - 1 - incidence = data["incidence_" + elem] - _, n = incidence.shape - - if n != 0: - idxs_list = [] - for n_feature in range(n): - idxs_for_feature = incidence.indices()[ - 0, incidence.indices()[1, :] == n_feature - ] - idxs_list.append(torch.sort(idxs_for_feature)[0]) - - idxs = torch.stack(idxs_list, dim=0) - if elem == "1": - values = idxs - else: - values = torch.sort( - torch.unique( - data["x_" + str(int(elem) - 1)][idxs].view( - idxs.shape[0], -1 - ), - dim=1, + for rank in range(domain.max_rank - 1): + if domain.features[rank + 1] is not None: + continue + + incidence = domain.incidence[rank + 1] + _, n = incidence.shape + + if n != 0: + idxs_list = [] + for n_feature in range(n): + idxs_for_feature = incidence.indices()[ + 0, incidence.indices()[1, :] == n_feature + ] + idxs_list.append(torch.sort(idxs_for_feature)[0]) + + idxs = torch.stack(idxs_list, dim=0) + if rank == 0: + values = idxs + else: + values = torch.sort( + torch.unique( + domain.features[rank][idxs].view( + idxs.shape[0], -1 ), dim=1, - )[0] - else: - values = torch.tensor([]) - - data["x_" + elem] = values - return data + ), + dim=1, + )[0] + else: + values = torch.tensor([]) - def forward( - self, data: torch_geometric.data.Data | dict - ) -> torch_geometric.data.Data | dict: - r"""Apply the lifting to the input data. + domain.update_features(rank + 1, values) - Parameters - ---------- - data : torch_geometric.data.Data | dict - The input data to be lifted. - - Returns - ------- - torch_geometric.data.Data | dict - The lifted data. - """ - data = self.lift_features(data) - return data + return domain From 190e2b89b7de4e47504211b5a6867a09025571e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20F=2E=20Pereira?= Date: Wed, 15 Jan 2025 19:13:00 -0800 Subject: [PATCH 17/43] Add str-based instantiation to LiftingMap for backwards compatibility --- topobenchmark/transforms/liftings/base.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/topobenchmark/transforms/liftings/base.py b/topobenchmark/transforms/liftings/base.py index ce3d1ab4..d9fb0593 100644 --- a/topobenchmark/transforms/liftings/base.py +++ b/topobenchmark/transforms/liftings/base.py @@ -48,6 +48,16 @@ def __init__( if domain2domain is None: domain2domain = IdentityAdapter() + if isinstance(lifting, str): + from topobenchmark.transforms import TRANSFORMS + + lifting = TRANSFORMS[lifting]() + + if isinstance(feature_lifting, str): + from topobenchmark.transforms import TRANSFORMS + + feature_lifting = TRANSFORMS[feature_lifting]() + self.data2domain = data2domain self.domain2domain = domain2domain self.domain2dict = domain2dict From f774c97b192ee6e60ebe11938069b33789a57545 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20F=2E=20Pereira?= Date: Wed, 15 Jan 2025 19:13:46 -0800 Subject: [PATCH 18/43] Fix Data2NxGraph adapter --- topobenchmark/data/utils/adapters.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/topobenchmark/data/utils/adapters.py b/topobenchmark/data/utils/adapters.py index 342a2622..1dc4328f 100644 --- a/topobenchmark/data/utils/adapters.py +++ b/topobenchmark/data/utils/adapters.py @@ -85,14 +85,14 @@ def adapt(self, domain: torch_geometric.data.Data) -> nx.Graph: if self.preserve_edge_attr and self._data_has_edge_attr(domain): # In case edge features are given, assign features to every edge - edge_index, edge_attr = ( - domain.edge_index, - ( - domain.edge_attr - if is_undirected(domain.edge_index, domain.edge_attr) - else to_undirected(domain.edge_index, domain.edge_attr) - ), - ) + # TODO: confirm this is the desired behavior + if is_undirected(domain.edge_index, domain.edge_attr): + edge_index, edge_attr = (domain.edge_index, domain.edge_attr) + else: + edge_index, edge_attr = to_undirected( + domain.edge_index, domain.edge_attr + ) + edges = [ (i.item(), j.item(), dict(features=edge_attr[edge_idx], dim=1)) for edge_idx, (i, j) in enumerate( From bd243871552d45f0449800bd55891e563017fe10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20F=2E=20Pereira?= Date: Wed, 15 Jan 2025 19:14:19 -0800 Subject: [PATCH 19/43] Fix failing data manipulation test (only setup) --- .../test_SimplicialCurvature.py | 45 ++++++++++++------- 1 file changed, 29 insertions(+), 16 deletions(-) diff --git a/test/transforms/data_manipulations/test_SimplicialCurvature.py b/test/transforms/data_manipulations/test_SimplicialCurvature.py index e4cb517b..e8199beb 100644 --- a/test/transforms/data_manipulations/test_SimplicialCurvature.py +++ b/test/transforms/data_manipulations/test_SimplicialCurvature.py @@ -2,8 +2,19 @@ import torch from torch_geometric.data import Data -from topobenchmark.transforms.data_manipulations import CalculateSimplicialCurvature -from topobenchmark.transforms.liftings.graph2simplicial import SimplicialCliqueLifting + +from topobenchmark.data.utils import ( + Complex2Dict, + Data2NxGraph, + TnxComplex2Complex, +) +from topobenchmark.transforms.data_manipulations import ( + CalculateSimplicialCurvature, +) +from topobenchmark.transforms.liftings import ( + LiftingTransform, + SimplicialCliqueLifting, +) class TestSimplicialCurvature: @@ -11,29 +22,31 @@ class TestSimplicialCurvature: def test_simplicial_curvature(self, simple_graph_1): """Test simplicial curvature calculation. - + Parameters ---------- simple_graph_1 : torch_geometric.data.Data A simple graph fixture. """ simplicial_curvature = CalculateSimplicialCurvature() - lifting_unsigned = SimplicialCliqueLifting( - complex_dim=3, signed=False + + lifting_unsigned = LiftingTransform( + lifting=SimplicialCliqueLifting(complex_dim=3), + data2domain=Data2NxGraph(), + domain2domain=TnxComplex2Complex(signed=False), + domain2dict=Complex2Dict(), ) + data = lifting_unsigned(simple_graph_1) - data['0_cell_degrees'] = torch.unsqueeze( - torch.sum(data['incidence_1'], dim=1).to_dense(), - dim=1 + data["0_cell_degrees"] = torch.unsqueeze( + torch.sum(data["incidence_1"], dim=1).to_dense(), dim=1 ) - data['1_cell_degrees'] = torch.unsqueeze( - torch.sum(data['incidence_2'], dim=1).to_dense(), - dim=1 + data["1_cell_degrees"] = torch.unsqueeze( + torch.sum(data["incidence_2"], dim=1).to_dense(), dim=1 ) - data['2_cell_degrees'] = torch.unsqueeze( - torch.sum(data['incidence_3'], dim=1).to_dense(), - dim=1 + data["2_cell_degrees"] = torch.unsqueeze( + torch.sum(data["incidence_3"], dim=1).to_dense(), dim=1 ) - + res = simplicial_curvature(data) - assert isinstance(res, Data) \ No newline at end of file + assert isinstance(res, Data) From a7a87553df3b7c1e82b2f94c12c582cc440d16e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20F=2E=20Pereira?= Date: Thu, 16 Jan 2025 11:21:59 -0800 Subject: [PATCH 20/43] Fix TnxComplex2Complex adapter to handle CellComplex features --- topobenchmark/data/utils/adapters.py | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/topobenchmark/data/utils/adapters.py b/topobenchmark/data/utils/adapters.py index 1dc4328f..80295e69 100644 --- a/topobenchmark/data/utils/adapters.py +++ b/topobenchmark/data/utils/adapters.py @@ -5,6 +5,7 @@ import torch import torch_geometric from topomodelx.utils.sparse import from_sparse +from toponetx.classes import CellComplex, SimplicialComplex from torch_geometric.utils.undirected import is_undirected, to_undirected from topobenchmark.data.utils.domain import Complex @@ -123,10 +124,6 @@ class TnxComplex2Complex(Adapter): Parameters ---------- - complex_dim : int - Dimension of the (sub)complex. - If ``None``, adapts the (full) complex. - If greater than dimension of complex, pads with empty matrices. neighborhoods : list, optional List of neighborhoods of interest. signed : bool, optional @@ -216,17 +213,18 @@ def adapt(self, domain): if neighborhoods is not None: data = select_neighborhoods_of_interest(data, neighborhoods) - # TODO: simplex specific? - # TODO: how to do this for other? - if self.transfer_features and hasattr( - domain, "get_simplex_attributes" - ): + if self.transfer_features: + if isinstance(domain, SimplicialComplex): + get_features = domain.get_simplex_attributes + elif isinstance(domain, CellComplex): + get_features = domain.get_cell_attributes + else: + raise ValueError("Can't transfer features.") + # TODO: confirm features are in the right order; update this data["features"] = [] for rank in range(dim + 1): - rank_features_dict = domain.get_simplex_attributes( - "features", rank - ) + rank_features_dict = get_features("features", rank) if rank_features_dict: rank_features = torch.stack( list(rank_features_dict.values()) From 2ccbea30d59686f69199eb911eb373811f28c5d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20F=2E=20Pereira?= Date: Thu, 16 Jan 2025 11:23:10 -0800 Subject: [PATCH 21/43] Add syntax sugar to instantiate graph2complex/simplicial lifting transforms --- topobenchmark/transforms/liftings/__init__.py | 6 +- topobenchmark/transforms/liftings/base.py | 55 ++++++++++++++++++- 2 files changed, 57 insertions(+), 4 deletions(-) diff --git a/topobenchmark/transforms/liftings/__init__.py b/topobenchmark/transforms/liftings/__init__.py index 513f5035..2c759ac3 100755 --- a/topobenchmark/transforms/liftings/__init__.py +++ b/topobenchmark/transforms/liftings/__init__.py @@ -1,6 +1,10 @@ """This module implements the liftings for the topological transforms.""" -from .base import LiftingTransform # noqa: F401 +from .base import ( # noqa: F401 + Graph2ComplexLiftingTransform, + Graph2SimplicialLiftingTransform, + LiftingTransform, +) from .graph2cell import GRAPH2CELL_LIFTINGS from .graph2hypergraph import GRAPH2HYPERGRAPH_LIFTINGS from .graph2simplicial import GRAPH2SIMPLICIAL_LIFTINGS diff --git a/topobenchmark/transforms/liftings/base.py b/topobenchmark/transforms/liftings/base.py index d9fb0593..3637f564 100644 --- a/topobenchmark/transforms/liftings/base.py +++ b/topobenchmark/transforms/liftings/base.py @@ -4,7 +4,12 @@ import torch_geometric -from topobenchmark.data.utils import IdentityAdapter +from topobenchmark.data.utils import ( + Complex2Dict, + Data2NxGraph, + IdentityAdapter, + TnxComplex2Complex, +) from topobenchmark.transforms.feature_liftings.identity import Identity @@ -13,14 +18,14 @@ class LiftingTransform(torch_geometric.transforms.BaseTransform): Parameters ---------- + lifting : LiftingMap + Lifting map. data2domain : Converter Conversion between ``torch_geometric.Data`` into domain for consumption by lifting. domain2dict : Converter Conversion between output domain of feature lifting and ``torch_geometric.Data``. - lifting : LiftingMap - Lifting map. domain2domain : Converter Conversion between output domain of lifting and input domain for feature lifting. @@ -92,6 +97,50 @@ def forward( ) +class Graph2ComplexLiftingTransform(LiftingTransform): + """Graph to complex lifting transform. + + Parameters + ---------- + lifting : LiftingMap + Lifting map. + feature_lifting : FeatureLiftingMap + Feature lifting map. + preserve_edge_attr : bool + Whether to preserve edge attributes. + neighborhoods : list, optional + List of neighborhoods of interest. + signed : bool, optional + If True, returns signed connectivity matrices. + transfer_features : bool, optional + Whether to transfer features. + """ + + def __init__( + self, + lifting, + feature_lifting="ProjectionSum", + preserve_edge_attr=False, + neighborhoods=None, + signed=False, + transfer_features=True, + ): + super().__init__( + lifting, + feature_lifting=feature_lifting, + data2domain=Data2NxGraph(preserve_edge_attr), + domain2domain=TnxComplex2Complex( + neighborhoods=neighborhoods, + signed=signed, + transfer_features=transfer_features, + ), + domain2dict=Complex2Dict(), + ) + + +Graph2SimplicialLiftingTransform = Graph2ComplexLiftingTransform + + class LiftingMap(abc.ABC): """Lifting map. From 7682feffad4b73fcc370266ce59cd7c6abc87d44 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20F=2E=20Pereira?= Date: Thu, 16 Jan 2025 11:24:25 -0800 Subject: [PATCH 22/43] Make imports shorter and use newly added syntax sugar --- test/conftest.py | 4 ++-- .../test_SimplicialCurvature.py | 14 +++-------- .../feature_liftings/test_Concatenation.py | 15 ++++-------- .../feature_liftings/test_ProjectionSum.py | 12 ++-------- .../feature_liftings/test_SetLifting.py | 14 +++-------- .../liftings/cell/test_CellCyclesLifting.py | 13 ++++------- .../hypergraph/test_HypergraphKHopLifting.py | 4 ++-- ...test_HypergraphKNearestNeighborsLifting.py | 4 +--- .../test_SimplicialCliqueLifting.py | 23 +++++-------------- .../test_SimplicialNeighborhoodLifting.py | 23 +++++-------------- topobenchmark/data/utils/__init__.py | 2 +- 11 files changed, 35 insertions(+), 93 deletions(-) diff --git a/test/conftest.py b/test/conftest.py index 753d63b2..9a70c6a1 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -5,8 +5,8 @@ import torch import torch_geometric -from topobenchmark.transforms.liftings.graph2cell.cycle import CellCycleLifting -from topobenchmark.transforms.liftings.graph2simplicial.clique import ( +from topobenchmark.transforms.liftings import ( + CellCycleLifting, SimplicialCliqueLifting, ) diff --git a/test/transforms/data_manipulations/test_SimplicialCurvature.py b/test/transforms/data_manipulations/test_SimplicialCurvature.py index e8199beb..e90d4e68 100644 --- a/test/transforms/data_manipulations/test_SimplicialCurvature.py +++ b/test/transforms/data_manipulations/test_SimplicialCurvature.py @@ -3,16 +3,11 @@ import torch from torch_geometric.data import Data -from topobenchmark.data.utils import ( - Complex2Dict, - Data2NxGraph, - TnxComplex2Complex, -) from topobenchmark.transforms.data_manipulations import ( CalculateSimplicialCurvature, ) from topobenchmark.transforms.liftings import ( - LiftingTransform, + Graph2SimplicialLiftingTransform, SimplicialCliqueLifting, ) @@ -30,11 +25,8 @@ def test_simplicial_curvature(self, simple_graph_1): """ simplicial_curvature = CalculateSimplicialCurvature() - lifting_unsigned = LiftingTransform( - lifting=SimplicialCliqueLifting(complex_dim=3), - data2domain=Data2NxGraph(), - domain2domain=TnxComplex2Complex(signed=False), - domain2dict=Complex2Dict(), + lifting_unsigned = Graph2SimplicialLiftingTransform( + lifting=SimplicialCliqueLifting(complex_dim=3) ) data = lifting_unsigned(simple_graph_1) diff --git a/test/transforms/feature_liftings/test_Concatenation.py b/test/transforms/feature_liftings/test_Concatenation.py index aff3a2c1..9474e8da 100644 --- a/test/transforms/feature_liftings/test_Concatenation.py +++ b/test/transforms/feature_liftings/test_Concatenation.py @@ -2,13 +2,8 @@ import torch -from topobenchmark.data.utils import ( - Complex2Dict, - Data2NxGraph, - TnxComplex2Complex, -) from topobenchmark.transforms.liftings import ( - LiftingTransform, + Graph2SimplicialLiftingTransform, SimplicialCliqueLifting, ) @@ -19,12 +14,10 @@ class TestConcatenation: def setup_method(self): """Set up the test.""" # Initialize a lifting class - self.lifting = LiftingTransform( - SimplicialCliqueLifting(complex_dim=3), + + self.lifting = Graph2SimplicialLiftingTransform( + lifting=SimplicialCliqueLifting(complex_dim=3), feature_lifting="Concatenation", - data2domain=Data2NxGraph(), - domain2domain=TnxComplex2Complex(signed=False), - domain2dict=Complex2Dict(), ) def test_lift_features(self, simple_graph_0, simple_graph_1): diff --git a/test/transforms/feature_liftings/test_ProjectionSum.py b/test/transforms/feature_liftings/test_ProjectionSum.py index b14ea5e8..a6ad8cdf 100644 --- a/test/transforms/feature_liftings/test_ProjectionSum.py +++ b/test/transforms/feature_liftings/test_ProjectionSum.py @@ -2,13 +2,8 @@ import torch -from topobenchmark.data.utils import ( - Complex2Dict, - Data2NxGraph, - TnxComplex2Complex, -) from topobenchmark.transforms.liftings import ( - LiftingTransform, + Graph2SimplicialLiftingTransform, SimplicialCliqueLifting, ) @@ -19,12 +14,9 @@ class TestProjectionSum: def setup_method(self): """Set up the test.""" # Initialize a lifting class - self.lifting = LiftingTransform( + self.lifting = Graph2SimplicialLiftingTransform( lifting=SimplicialCliqueLifting(complex_dim=3), feature_lifting="ProjectionSum", - data2domain=Data2NxGraph(), - domain2domain=TnxComplex2Complex(), - domain2dict=Complex2Dict(), ) def test_lift_features(self, simple_graph_1): diff --git a/test/transforms/feature_liftings/test_SetLifting.py b/test/transforms/feature_liftings/test_SetLifting.py index bf0c621f..584f9724 100644 --- a/test/transforms/feature_liftings/test_SetLifting.py +++ b/test/transforms/feature_liftings/test_SetLifting.py @@ -2,13 +2,8 @@ import torch -from topobenchmark.data.utils import ( - Complex2Dict, - Data2NxGraph, - TnxComplex2Complex, -) from topobenchmark.transforms.liftings import ( - LiftingTransform, + Graph2SimplicialLiftingTransform, SimplicialCliqueLifting, ) @@ -19,12 +14,9 @@ class TestSetLifting: def setup_method(self): """Set up the test.""" # Initialize a lifting class - self.lifting = LiftingTransform( - SimplicialCliqueLifting(complex_dim=3), + self.lifting = Graph2SimplicialLiftingTransform( + lifting=SimplicialCliqueLifting(complex_dim=3), feature_lifting="Set", - data2domain=Data2NxGraph(), - domain2domain=TnxComplex2Complex(signed=False), - domain2dict=Complex2Dict(), ) def test_lift_features(self, simple_graph_1): diff --git a/test/transforms/liftings/cell/test_CellCyclesLifting.py b/test/transforms/liftings/cell/test_CellCyclesLifting.py index c574992e..7235b20f 100644 --- a/test/transforms/liftings/cell/test_CellCyclesLifting.py +++ b/test/transforms/liftings/cell/test_CellCyclesLifting.py @@ -2,9 +2,10 @@ import torch -from topobenchmark.data.utils import Data2NxGraph, TnxComplex2Dict -from topobenchmark.transforms.liftings.base import LiftingTransform -from topobenchmark.transforms.liftings.graph2cell.cycle import CellCycleLifting +from topobenchmark.transforms.liftings import ( + CellCycleLifting, + Graph2ComplexLiftingTransform, +) class TestCellCycleLifting: @@ -12,11 +13,7 @@ class TestCellCycleLifting: def setup_method(self): # Initialise the CellCycleLifting class - self.lifting = LiftingTransform( - CellCycleLifting(), - data2domain=Data2NxGraph(), - domain2dict=TnxComplex2Dict(), - ) + self.lifting = Graph2ComplexLiftingTransform(CellCycleLifting()) def test_lift_topology(self, simple_graph_1): # Test the lift_topology method diff --git a/test/transforms/liftings/hypergraph/test_HypergraphKHopLifting.py b/test/transforms/liftings/hypergraph/test_HypergraphKHopLifting.py index 3fcc7ebb..8fd1b75b 100644 --- a/test/transforms/liftings/hypergraph/test_HypergraphKHopLifting.py +++ b/test/transforms/liftings/hypergraph/test_HypergraphKHopLifting.py @@ -2,9 +2,9 @@ import torch -from topobenchmark.transforms.liftings.base import LiftingTransform -from topobenchmark.transforms.liftings.graph2hypergraph.khop import ( +from topobenchmark.transforms.liftings import ( HypergraphKHopLifting, + LiftingTransform, ) diff --git a/test/transforms/liftings/hypergraph/test_HypergraphKNearestNeighborsLifting.py b/test/transforms/liftings/hypergraph/test_HypergraphKNearestNeighborsLifting.py index 069d7a3c..23dc5d35 100644 --- a/test/transforms/liftings/hypergraph/test_HypergraphKNearestNeighborsLifting.py +++ b/test/transforms/liftings/hypergraph/test_HypergraphKNearestNeighborsLifting.py @@ -4,9 +4,7 @@ import torch from torch_geometric.data import Data -from topobenchmark.transforms.liftings.graph2hypergraph.knn import ( - HypergraphKNNLifting, -) +from topobenchmark.transforms.liftings import HypergraphKNNLifting class TestHypergraphKNNLifting: diff --git a/test/transforms/liftings/simplicial/test_SimplicialCliqueLifting.py b/test/transforms/liftings/simplicial/test_SimplicialCliqueLifting.py index fa36e072..a2c32ebf 100644 --- a/test/transforms/liftings/simplicial/test_SimplicialCliqueLifting.py +++ b/test/transforms/liftings/simplicial/test_SimplicialCliqueLifting.py @@ -2,16 +2,11 @@ import torch -from topobenchmark.data.utils import ( - Complex2Dict, - Data2NxGraph, - TnxComplex2Complex, -) from topobenchmark.transforms.feature_liftings.projection_sum import ( ProjectionSum, ) -from topobenchmark.transforms.liftings.base import LiftingTransform -from topobenchmark.transforms.liftings.graph2simplicial.clique import ( +from topobenchmark.transforms.liftings import ( + Graph2SimplicialLiftingTransform, SimplicialCliqueLifting, ) @@ -21,25 +16,19 @@ class TestSimplicialCliqueLifting: def setup_method(self): # Initialise the SimplicialCliqueLifting class - data2graph = Data2NxGraph() lifting_map = SimplicialCliqueLifting(complex_dim=3) feature_lifting = ProjectionSum() - domain2dict = Complex2Dict() - self.lifting_signed = LiftingTransform( + self.lifting_signed = Graph2SimplicialLiftingTransform( lifting=lifting_map, feature_lifting=feature_lifting, - data2domain=data2graph, - domain2domain=TnxComplex2Complex(signed=True), - domain2dict=domain2dict, + signed=True, ) - self.lifting_unsigned = LiftingTransform( + self.lifting_unsigned = Graph2SimplicialLiftingTransform( lifting=lifting_map, feature_lifting=feature_lifting, - data2domain=data2graph, - domain2domain=TnxComplex2Complex(signed=False), - domain2dict=domain2dict, + signed=False, ) def test_lift_topology(self, simple_graph_1): diff --git a/test/transforms/liftings/simplicial/test_SimplicialNeighborhoodLifting.py b/test/transforms/liftings/simplicial/test_SimplicialNeighborhoodLifting.py index e21b8f99..6a81d9f2 100644 --- a/test/transforms/liftings/simplicial/test_SimplicialNeighborhoodLifting.py +++ b/test/transforms/liftings/simplicial/test_SimplicialNeighborhoodLifting.py @@ -2,16 +2,11 @@ import torch -from topobenchmark.data.utils import ( - Complex2Dict, - Data2NxGraph, - TnxComplex2Complex, -) from topobenchmark.transforms.feature_liftings.projection_sum import ( ProjectionSum, ) -from topobenchmark.transforms.liftings.base import LiftingTransform -from topobenchmark.transforms.liftings.graph2simplicial.khop import ( +from topobenchmark.transforms.liftings import ( + Graph2SimplicialLiftingTransform, SimplicialKHopLifting, ) @@ -23,25 +18,19 @@ class TestSimplicialKHopLifting: def setup_method(self): # Initialise the SimplicialKHopLifting class - data2graph = Data2NxGraph() feature_lifting = ProjectionSum() - domain2dict = Complex2Dict() lifting_map = SimplicialKHopLifting(complex_dim=3) - self.lifting_signed = LiftingTransform( + self.lifting_signed = Graph2SimplicialLiftingTransform( lifting=lifting_map, feature_lifting=feature_lifting, - data2domain=data2graph, - domain2domain=TnxComplex2Complex(signed=True), - domain2dict=domain2dict, + signed=True, ) - self.lifting_unsigned = LiftingTransform( + self.lifting_unsigned = Graph2SimplicialLiftingTransform( lifting=lifting_map, feature_lifting=feature_lifting, - data2domain=data2graph, - domain2domain=TnxComplex2Complex(signed=False), - domain2dict=domain2dict, + signed=False, ) def test_lift_topology(self, simple_graph_1): diff --git a/topobenchmark/data/utils/__init__.py b/topobenchmark/data/utils/__init__.py index d7010c2b..34fc79f3 100644 --- a/topobenchmark/data/utils/__init__.py +++ b/topobenchmark/data/utils/__init__.py @@ -1,7 +1,7 @@ """Init file for data/utils module.""" from .adapters import * -from .domain import Complex +from .domain import Complex # noqa: F401 from .utils import ( ensure_serializable, # noqa: F401 generate_zero_sparse_connectivity, # noqa: F401 From f3e3f88b5b503ef84570bc3b7fcb384a405ace31 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20F=2E=20Pereira?= Date: Thu, 16 Jan 2025 15:31:25 -0800 Subject: [PATCH 23/43] Update domain to accomodate hypergraph data --- .../liftings/cell/test_CellCyclesLifting.py | 4 +- .../hypergraph/test_HypergraphKHopLifting.py | 12 +- topobenchmark/data/utils/__init__.py | 2 +- topobenchmark/data/utils/adapters.py | 39 ++++-- topobenchmark/data/utils/domain.py | 113 +++++++++++------- .../feature_liftings/concatenation.py | 18 +-- .../feature_liftings/projection_sum.py | 16 +-- .../transforms/feature_liftings/set.py | 16 +-- topobenchmark/transforms/liftings/__init__.py | 2 + topobenchmark/transforms/liftings/base.py | 29 +++-- .../liftings/graph2hypergraph/khop.py | 13 +- 11 files changed, 166 insertions(+), 98 deletions(-) diff --git a/test/transforms/liftings/cell/test_CellCyclesLifting.py b/test/transforms/liftings/cell/test_CellCyclesLifting.py index 7235b20f..706e1f9d 100644 --- a/test/transforms/liftings/cell/test_CellCyclesLifting.py +++ b/test/transforms/liftings/cell/test_CellCyclesLifting.py @@ -4,7 +4,7 @@ from topobenchmark.transforms.liftings import ( CellCycleLifting, - Graph2ComplexLiftingTransform, + Graph2CellLiftingTransform, ) @@ -13,7 +13,7 @@ class TestCellCycleLifting: def setup_method(self): # Initialise the CellCycleLifting class - self.lifting = Graph2ComplexLiftingTransform(CellCycleLifting()) + self.lifting = Graph2CellLiftingTransform(CellCycleLifting()) def test_lift_topology(self, simple_graph_1): # Test the lift_topology method diff --git a/test/transforms/liftings/hypergraph/test_HypergraphKHopLifting.py b/test/transforms/liftings/hypergraph/test_HypergraphKHopLifting.py index 8fd1b75b..68326f11 100644 --- a/test/transforms/liftings/hypergraph/test_HypergraphKHopLifting.py +++ b/test/transforms/liftings/hypergraph/test_HypergraphKHopLifting.py @@ -3,8 +3,8 @@ import torch from topobenchmark.transforms.liftings import ( + Graph2HypergraphLiftingTransform, HypergraphKHopLifting, - LiftingTransform, ) @@ -14,15 +14,19 @@ class TestHypergraphKHopLifting: def setup_method(self): """Setup the test.""" # Initialise the HypergraphKHopLifting class - self.lifting_k1 = LiftingTransform(HypergraphKHopLifting(k_value=1)) - self.lifting_k2 = LiftingTransform(HypergraphKHopLifting(k_value=2)) + self.lifting_k1 = Graph2HypergraphLiftingTransform( + HypergraphKHopLifting(k_value=1) + ) + self.lifting_k2 = Graph2HypergraphLiftingTransform( + HypergraphKHopLifting(k_value=2) + ) # TODO: delete? # NB: `preserve_edge_attr` is never used? therefore they're equivalent # self.lifting_edge_attr = HypergraphKHopLifting( # k_value=1, preserve_edge_attr=True # ) - self.lifting_edge_attr = LiftingTransform( + self.lifting_edge_attr = Graph2HypergraphLiftingTransform( HypergraphKHopLifting(k_value=1) ) diff --git a/topobenchmark/data/utils/__init__.py b/topobenchmark/data/utils/__init__.py index 34fc79f3..de796c1d 100644 --- a/topobenchmark/data/utils/__init__.py +++ b/topobenchmark/data/utils/__init__.py @@ -1,7 +1,7 @@ """Init file for data/utils module.""" from .adapters import * -from .domain import Complex # noqa: F401 +from .domain import ComplexData, HypergraphData # noqa: F401 from .utils import ( ensure_serializable, # noqa: F401 generate_zero_sparse_connectivity, # noqa: F401 diff --git a/topobenchmark/data/utils/adapters.py b/topobenchmark/data/utils/adapters.py index 80295e69..b049d49c 100644 --- a/topobenchmark/data/utils/adapters.py +++ b/topobenchmark/data/utils/adapters.py @@ -8,7 +8,7 @@ from toponetx.classes import CellComplex, SimplicialComplex from torch_geometric.utils.undirected import is_undirected, to_undirected -from topobenchmark.data.utils.domain import Complex +from topobenchmark.data.utils.domain import ComplexData from topobenchmark.data.utils.utils import ( generate_zero_sparse_connectivity, select_neighborhoods_of_interest, @@ -115,7 +115,7 @@ def adapt(self, domain: torch_geometric.data.Data) -> nx.Graph: return graph -class TnxComplex2Complex(Adapter): +class TnxComplex2ComplexData(Adapter): """toponetx.Complex to Complex adaptation. NB: order of features plays a crucial role, as ``Complex`` @@ -236,18 +236,18 @@ def adapt(self, domain): for _ in range(dim + 1, practical_dim + 1): data["features"].append(None) - return Complex(**data) + return ComplexData(**data) -class Complex2Dict(Adapter): - """Complex to dict adaptation.""" +class ComplexData2Dict(Adapter): + """ComplexData to dict adaptation.""" def adapt(self, domain): """Adapt Complex to dict. Parameters ---------- - domain : toponetx.Complex + domain : ComplexData Returns ------- @@ -277,6 +277,29 @@ def adapt(self, domain): return data +class HypergraphData2Dict(Adapter): + """HypergraphData to dict adaptation.""" + + def adapt(self, domain): + """Adapt HypergraphData to dict. + + Parameters + ---------- + domain : HypergraphData + + Returns + ------- + dict + """ + hyperedges_key = domain.keys()[-1] + return { + "incidence_hyperedges": domain.incidence[hyperedges_key], + "num_hyperedges": domain.num_hyperedges, + "x_0": domain.features[0], + "x_hyperedges": domain.features[hyperedges_key], + } + + class AdapterComposition(Adapter): def __init__(self, adapters): super().__init__() @@ -309,10 +332,10 @@ def __init__( signed=False, transfer_features=True, ): - tnxcomplex2complex = TnxComplex2Complex( + tnxcomplex2complex = TnxComplex2ComplexData( neighborhoods=neighborhoods, signed=signed, transfer_features=transfer_features, ) - complex2dict = Complex2Dict() + complex2dict = ComplexData2Dict() super().__init__(adapters=(tnxcomplex2complex, complex2dict)) diff --git a/topobenchmark/data/utils/domain.py b/topobenchmark/data/utils/domain.py index 8bf4f1d7..57790162 100644 --- a/topobenchmark/data/utils/domain.py +++ b/topobenchmark/data/utils/domain.py @@ -1,4 +1,44 @@ -class Complex: +import abc + + +class Data(abc.ABC): + def __init__(self, incidence, features): + self.incidence = incidence + self.features = features + + @abc.abstractmethod + def keys(self): + pass + + def update_features(self, rank, values): + """Update features. + + Parameters + ---------- + rank : int + Rank of simplices the features belong to. + values : array-like + New features for the rank-simplices. + """ + self.features[rank] = values + + @property + def shape(self): + """Shape of the complex. + + Returns + ------- + list[int] + """ + return [ + None + if self.incidence[key] is None + else self.incidence[key].shape[-1] + for key in self.keys() + ] + + +class ComplexData(Data): def __init__( self, incidence, @@ -9,10 +49,6 @@ def __init__( hodge_laplacian, features=None, ): - # TODO: allow None with nice error message if callable? - - # TODO: make this private? do not allow for changes in these values? - self.incidence = incidence self.down_laplacian = down_laplacian self.up_laplacian = up_laplacian self.adjacency = adjacency @@ -20,53 +56,42 @@ def __init__( self.hodge_laplacian = hodge_laplacian if features is None: - features = [None for _ in range(len(self.incidence))] + features = [None for _ in range(len(incidence))] else: - for rank, dim in enumerate(self.shape): + for rank, incidence_ in enumerate(incidence): # TODO: make error message more informative if ( features[rank] is not None - and features[rank].shape[0] != dim + and features[rank].shape[0] != incidence_.shape[-1] ): raise ValueError("Features have wrong shape.") - self.features = features + super().__init__(incidence, features) - @property - def shape(self): - """Shape of the complex. + def keys(self): + return list(range(len(self.incidence))) - Returns - ------- - list[int] - """ - return [incidence.shape[-1] for incidence in self.incidence] - - @property - def max_rank(self): - """Maximum rank of the complex. - - NB: may differ from mathematical definition due to empty - matrices. - - Returns - ------- - int - """ - return len(self.incidence) - def update_features(self, rank, values): - """Update features. - - Parameters - ---------- - rank : int - Rank of simplices the features belong to. - values : array-like - New features for the rank-simplices. - """ - self.features[rank] = values +class HypergraphData(Data): + def __init__( + self, + incidence_hyperedges, + num_hyperedges, + incidence_0=None, + x_0=None, + x_hyperedges=None, + ): + self._hyperedges_key = 1 + incidence = { + 0: incidence_0, + self._hyperedges_key: incidence_hyperedges, + } + features = { + 0: x_0, + self._hyperedges_key: x_hyperedges, + } + super().__init__(incidence, features) + self.num_hyperedges = num_hyperedges - def reset_features(self): - """Reset features.""" - self.features = [None for _ in self.features] + def keys(self): + return [0, self._hyperedges_key] diff --git a/topobenchmark/transforms/feature_liftings/concatenation.py b/topobenchmark/transforms/feature_liftings/concatenation.py index b26509d9..44e3b192 100644 --- a/topobenchmark/transforms/feature_liftings/concatenation.py +++ b/topobenchmark/transforms/feature_liftings/concatenation.py @@ -24,14 +24,13 @@ def lift_features(self, domain): Complex Domain with the lifted features. """ - for rank in range(domain.max_rank - 1): - if domain.features[rank + 1] is not None: + for key, next_key in zip( + domain.keys(), domain.keys()[1:], strict=False + ): + if domain.features[next_key] is not None: continue - # TODO: different if hyperedges? - idx_to_project = rank - - incidence = domain.incidence[rank + 1] + incidence = domain.incidence[next_key] _, n = incidence.shape if n != 0: @@ -43,11 +42,12 @@ def lift_features(self, domain): idxs_list.append(torch.sort(idxs_for_feature)[0]) idxs = torch.stack(idxs_list, dim=0) - values = domain.features[idx_to_project][idxs].view(n, -1) + values = domain.features[key][idxs].view(n, -1) else: - m = domain.features[rank].shape[1] * (rank + 2) + # NB: only works if key represents rank + m = domain.features[key].shape[1] * (next_key + 1) values = torch.zeros([0, m]) - domain.update_features(rank + 1, values) + domain.update_features(next_key, values) return domain diff --git a/topobenchmark/transforms/feature_liftings/projection_sum.py b/topobenchmark/transforms/feature_liftings/projection_sum.py index a756fd0e..757234a7 100644 --- a/topobenchmark/transforms/feature_liftings/projection_sum.py +++ b/topobenchmark/transforms/feature_liftings/projection_sum.py @@ -13,23 +13,25 @@ def lift_features(self, domain): Parameters ---------- - data : Complex + data : Data The input data to be lifted. Returns ------- - Complex + Data Domain with the lifted features. """ - for rank in range(domain.max_rank - 1): - if domain.features[rank + 1] is not None: + for key, next_key in zip( + domain.keys(), domain.keys()[1:], strict=False + ): + if domain.features[next_key] is not None: continue domain.update_features( - rank + 1, + next_key, torch.matmul( - torch.abs(domain.incidence[rank + 1].t()), - domain.features[rank], + torch.abs(domain.incidence[next_key].t()), + domain.features[key], ), ) diff --git a/topobenchmark/transforms/feature_liftings/set.py b/topobenchmark/transforms/feature_liftings/set.py index 1886e25b..54ac1b9d 100644 --- a/topobenchmark/transforms/feature_liftings/set.py +++ b/topobenchmark/transforms/feature_liftings/set.py @@ -24,11 +24,13 @@ def lift_features(self, domain): Complex Domain with the lifted features. """ - for rank in range(domain.max_rank - 1): - if domain.features[rank + 1] is not None: + for key, next_key in zip( + domain.keys(), domain.keys()[1:], strict=False + ): + if domain.features[next_key] is not None: continue - incidence = domain.incidence[rank + 1] + incidence = domain.incidence[next_key] _, n = incidence.shape if n != 0: @@ -40,14 +42,12 @@ def lift_features(self, domain): idxs_list.append(torch.sort(idxs_for_feature)[0]) idxs = torch.stack(idxs_list, dim=0) - if rank == 0: + if key == 0: values = idxs else: values = torch.sort( torch.unique( - domain.features[rank][idxs].view( - idxs.shape[0], -1 - ), + domain.features[key][idxs].view(idxs.shape[0], -1), dim=1, ), dim=1, @@ -55,6 +55,6 @@ def lift_features(self, domain): else: values = torch.tensor([]) - domain.update_features(rank + 1, values) + domain.update_features(next_key, values) return domain diff --git a/topobenchmark/transforms/liftings/__init__.py b/topobenchmark/transforms/liftings/__init__.py index 2c759ac3..322c43a1 100755 --- a/topobenchmark/transforms/liftings/__init__.py +++ b/topobenchmark/transforms/liftings/__init__.py @@ -1,7 +1,9 @@ """This module implements the liftings for the topological transforms.""" from .base import ( # noqa: F401 + Graph2CellLiftingTransform, Graph2ComplexLiftingTransform, + Graph2HypergraphLiftingTransform, Graph2SimplicialLiftingTransform, LiftingTransform, ) diff --git a/topobenchmark/transforms/liftings/base.py b/topobenchmark/transforms/liftings/base.py index 3637f564..13f1f443 100644 --- a/topobenchmark/transforms/liftings/base.py +++ b/topobenchmark/transforms/liftings/base.py @@ -5,12 +5,12 @@ import torch_geometric from topobenchmark.data.utils import ( - Complex2Dict, + ComplexData2Dict, Data2NxGraph, + HypergraphData2Dict, IdentityAdapter, - TnxComplex2Complex, + TnxComplex2ComplexData, ) -from topobenchmark.transforms.feature_liftings.identity import Identity class LiftingTransform(torch_geometric.transforms.BaseTransform): @@ -39,11 +39,8 @@ def __init__( data2domain=None, domain2dict=None, domain2domain=None, - feature_lifting=None, + feature_lifting="ProjectionSum", ): - if feature_lifting is None: - feature_lifting = Identity() - if data2domain is None: data2domain = IdentityAdapter() @@ -129,16 +126,30 @@ def __init__( lifting, feature_lifting=feature_lifting, data2domain=Data2NxGraph(preserve_edge_attr), - domain2domain=TnxComplex2Complex( + domain2domain=TnxComplex2ComplexData( neighborhoods=neighborhoods, signed=signed, transfer_features=transfer_features, ), - domain2dict=Complex2Dict(), + domain2dict=ComplexData2Dict(), ) Graph2SimplicialLiftingTransform = Graph2ComplexLiftingTransform +Graph2CellLiftingTransform = Graph2ComplexLiftingTransform + + +class Graph2HypergraphLiftingTransform(LiftingTransform): + def __init__( + self, + lifting, + feature_lifting="ProjectionSum", + ): + super().__init__( + lifting, + feature_lifting=feature_lifting, + domain2dict=HypergraphData2Dict(), + ) class LiftingMap(abc.ABC): diff --git a/topobenchmark/transforms/liftings/graph2hypergraph/khop.py b/topobenchmark/transforms/liftings/graph2hypergraph/khop.py index f8997e31..7c56006c 100755 --- a/topobenchmark/transforms/liftings/graph2hypergraph/khop.py +++ b/topobenchmark/transforms/liftings/graph2hypergraph/khop.py @@ -3,6 +3,7 @@ import torch import torch_geometric +from topobenchmark.data.utils import HypergraphData from topobenchmark.transforms.liftings.base import LiftingMap @@ -36,7 +37,7 @@ def lift(self, data: torch_geometric.data.Data) -> dict: Returns ------- - dict + HypergraphData The lifted topology. """ # Check if data has instance x: @@ -72,8 +73,8 @@ def lift(self, data: torch_geometric.data.Data) -> dict: num_hyperedges = incidence_1.shape[1] incidence_1 = torch.Tensor(incidence_1).to_sparse_coo() - return { - "incidence_hyperedges": incidence_1, - "num_hyperedges": num_hyperedges, - "x_0": data.x, - } + return HypergraphData( + incidence_hyperedges=incidence_1, + num_hyperedges=num_hyperedges, + x_0=data.x, + ) From f4010fad4856a3423d59354ea4a2adf31a455d8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20F=2E=20Pereira?= Date: Thu, 16 Jan 2025 15:31:50 -0800 Subject: [PATCH 24/43] Update data_transform to handle new liftings design --- topobenchmark/transforms/data_transform.py | 69 +++++++++++++++++++--- 1 file changed, 61 insertions(+), 8 deletions(-) diff --git a/topobenchmark/transforms/data_transform.py b/topobenchmark/transforms/data_transform.py index da9e883b..8106f829 100755 --- a/topobenchmark/transforms/data_transform.py +++ b/topobenchmark/transforms/data_transform.py @@ -1,8 +1,54 @@ """DataTransform class.""" +import inspect + import torch_geometric -from topobenchmark.transforms import TRANSFORMS +from topobenchmark.transforms import LIFTINGS, TRANSFORMS +from topobenchmark.transforms.liftings import ( + GRAPH2CELL_LIFTINGS, + GRAPH2HYPERGRAPH_LIFTINGS, + GRAPH2SIMPLICIAL_LIFTINGS, + Graph2CellLiftingTransform, + Graph2HypergraphLiftingTransform, + Graph2SimplicialLiftingTransform, + LiftingTransform, +) + +_map_lifting_types = { + "graph2cell": (GRAPH2CELL_LIFTINGS, Graph2CellLiftingTransform), + "graph2hypergraph": ( + GRAPH2HYPERGRAPH_LIFTINGS, + Graph2HypergraphLiftingTransform, + ), + "graph2simplicial": ( + GRAPH2SIMPLICIAL_LIFTINGS, + Graph2SimplicialLiftingTransform, + ), +} + + +def _map_lifting_name(lifting_name): + for liftings_dict, Transform in _map_lifting_types.values(): + if lifting_name in liftings_dict: + return Transform + + return LiftingTransform + + +def _route_lifting_kwargs(kwargs, LiftingMap): + lifting_map_sign = inspect.signature(LiftingMap) + + lifting_map_kwargs = {} + transform_kwargs = {} + + for key, value in kwargs.items(): + if key in lifting_map_sign.parameters: + lifting_map_kwargs[key] = value + else: + transform_kwargs[key] = value + + return lifting_map_kwargs, transform_kwargs class DataTransform(torch_geometric.transforms.BaseTransform): @@ -19,14 +65,21 @@ class DataTransform(torch_geometric.transforms.BaseTransform): def __init__(self, transform_name, **kwargs): super().__init__() - kwargs["transform_name"] = transform_name - self.parameters = kwargs + if transform_name not in LIFTINGS: + kwargs["transform_name"] = transform_name + transform = TRANSFORMS[transform_name](**kwargs) + else: + LiftingMap_ = TRANSFORMS[transform_name] + Transform = _map_lifting_name(transform_name) + lifting_map_kwargs, transform_kwargs = _route_lifting_kwargs( + kwargs, LiftingMap_ + ) + + lifting_map = LiftingMap_(**lifting_map_kwargs) + transform = Transform(lifting_map, **transform_kwargs) - self.transform = ( - TRANSFORMS[transform_name](**kwargs) - if transform_name is not None - else None - ) + self.parameters = kwargs + self.transform = transform def forward( self, data: torch_geometric.data.Data From 27962fc1e7c75c6a4b1cc99a951451c09b4c3d6f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20F=2E=20Pereira?= Date: Thu, 16 Jan 2025 15:49:23 -0800 Subject: [PATCH 25/43] Fix failing tests --- test/conftest.py | 8 ++- test/nn/backbones/simplicial/test_sccnn.py | 59 +++++++++++------- test/nn/wrappers/cell/test_cell_wrappers.py | 46 ++++++-------- .../wrappers/simplicial/test_SCCNNWrapper.py | 62 ++++++++++--------- topobenchmark/transforms/data_transform.py | 7 ++- 5 files changed, 97 insertions(+), 85 deletions(-) diff --git a/test/conftest.py b/test/conftest.py index 9a70c6a1..d8cf94d0 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -7,6 +7,8 @@ from topobenchmark.transforms.liftings import ( CellCycleLifting, + Graph2CellLiftingTransform, + Graph2SimplicialLiftingTransform, SimplicialCliqueLifting, ) @@ -148,7 +150,9 @@ def sg1_clique_lifted(simple_graph_1): torch_geometric.data.Data A simple graph data object with a clique lifting. """ - lifting_signed = SimplicialCliqueLifting(complex_dim=3, signed=True) + lifting_signed = Graph2SimplicialLiftingTransform( + SimplicialCliqueLifting(complex_dim=3), signed=True + ) data = lifting_signed(simple_graph_1) data.batch_0 = "null" return data @@ -168,7 +172,7 @@ def sg1_cell_lifted(simple_graph_1): torch_geometric.data.Data A simple graph data object with a cell lifting. """ - lifting = CellCycleLifting() + lifting = Graph2CellLiftingTransform(CellCycleLifting()) data = lifting(simple_graph_1) data.batch_0 = "null" return data diff --git a/test/nn/backbones/simplicial/test_sccnn.py b/test/nn/backbones/simplicial/test_sccnn.py index 19e2b774..09e86342 100644 --- a/test/nn/backbones/simplicial/test_sccnn.py +++ b/test/nn/backbones/simplicial/test_sccnn.py @@ -1,38 +1,53 @@ """Unit tests for SCCNN""" -import torch -from torch_geometric.utils import get_laplacian -from ...._utils.nn_module_auto_test import NNModuleAutoTest from topobenchmark.nn.backbones.simplicial import SCCNNCustom -from topobenchmark.transforms.liftings.graph2simplicial import ( +from topobenchmark.transforms.liftings import ( + Graph2SimplicialLiftingTransform, SimplicialCliqueLifting, ) +from ...._utils.nn_module_auto_test import NNModuleAutoTest + def test_SCCNNCustom(simple_graph_1): - lifting_signed = SimplicialCliqueLifting( - complex_dim=3, signed=True - ) + lifting_signed = Graph2SimplicialLiftingTransform( + SimplicialCliqueLifting(complex_dim=3), signed=True + ) data = lifting_signed(simple_graph_1) out_dim = 4 conv_order = 1 sc_order = 3 laplacian_all = ( - data.hodge_laplacian_0, - data.down_laplacian_1, - data.up_laplacian_1, - data.down_laplacian_2, - data.up_laplacian_2, - ) + data.hodge_laplacian_0, + data.down_laplacian_1, + data.up_laplacian_1, + data.down_laplacian_2, + data.up_laplacian_2, + ) incidence_all = (data.incidence_1, data.incidence_2) - expected_shapes = [(data.x.shape[0], out_dim), (data.x_1.shape[0], out_dim), (data.x_2.shape[0], out_dim)] + expected_shapes = [ + (data.x.shape[0], out_dim), + (data.x_1.shape[0], out_dim), + (data.x_2.shape[0], out_dim), + ] - auto_test = NNModuleAutoTest([ - { - "module" : SCCNNCustom, - "init": ((data.x.shape[1], data.x_1.shape[1], data.x_2.shape[1]), (out_dim, out_dim, out_dim), conv_order, sc_order), - "forward": ((data.x, data.x_1, data.x_2), laplacian_all, incidence_all), - "assert_shape": expected_shapes - }, - ]) + auto_test = NNModuleAutoTest( + [ + { + "module": SCCNNCustom, + "init": ( + (data.x.shape[1], data.x_1.shape[1], data.x_2.shape[1]), + (out_dim, out_dim, out_dim), + conv_order, + sc_order, + ), + "forward": ( + (data.x, data.x_1, data.x_2), + laplacian_all, + incidence_all, + ), + "assert_shape": expected_shapes, + }, + ] + ) auto_test.run() diff --git a/test/nn/wrappers/cell/test_cell_wrappers.py b/test/nn/wrappers/cell/test_cell_wrappers.py index 45b69888..fb551a67 100644 --- a/test/nn/wrappers/cell/test_cell_wrappers.py +++ b/test/nn/wrappers/cell/test_cell_wrappers.py @@ -1,23 +1,14 @@ """Unit tests for cell model wrappers""" -import torch -from torch_geometric.utils import get_laplacian -from ...._utils.nn_module_auto_test import NNModuleAutoTest -from ...._utils.flow_mocker import FlowMocker -from unittest.mock import MagicMock +from topomodelx.nn.cell.ccxn import CCXN +from topomodelx.nn.cell.cwn import CWN +from topobenchmark.nn.backbones.cell.cccn import CCCN from topobenchmark.nn.wrappers import ( - AbstractWrapper, CCCNWrapper, - CANWrapper, CCXNWrapper, - CWNWrapper + CWNWrapper, ) -from topomodelx.nn.cell.can import CAN -from topomodelx.nn.cell.ccxn import CCXN -from topomodelx.nn.cell.cwn import CWN -from topobenchmark.nn.backbones.cell.cccn import CCCN -from unittest.mock import MagicMock class TestCellWrappers: @@ -27,11 +18,9 @@ def test_CCCNWrapper(self, sg1_clique_lifted): num_cell_dimensions = 2 wrapper = CCCNWrapper( - CCCN( - data.x_1.shape[1] - ), - out_channels=out_channels, - num_cell_dimensions=num_cell_dimensions + CCCN(data.x_1.shape[1]), + out_channels=out_channels, + num_cell_dimensions=num_cell_dimensions, ) out = wrapper(data) @@ -44,11 +33,9 @@ def test_CCXNWrapper(self, sg1_cell_lifted): num_cell_dimensions = 2 wrapper = CCXNWrapper( - CCXN( - data.x_0.shape[1], data.x_1.shape[1], out_channels - ), - out_channels=out_channels, - num_cell_dimensions=num_cell_dimensions + CCXN(data.x_0.shape[1], data.x_1.shape[1], out_channels), + out_channels=out_channels, + num_cell_dimensions=num_cell_dimensions, ) out = wrapper(data) @@ -63,13 +50,16 @@ def test_CWNWrapper(self, sg1_cell_lifted): wrapper = CWNWrapper( CWN( - data.x_0.shape[1], data.x_1.shape[1], data.x_2.shape[1], hid_channels, 2 - ), - out_channels=out_channels, - num_cell_dimensions=num_cell_dimensions + data.x_0.shape[1], + data.x_1.shape[1], + data.x_2.shape[1], + hid_channels, + 2, + ), + out_channels=out_channels, + num_cell_dimensions=num_cell_dimensions, ) out = wrapper(data) for key in ["labels", "batch_0", "x_0", "x_1", "x_2"]: assert key in out - diff --git a/test/nn/wrappers/simplicial/test_SCCNNWrapper.py b/test/nn/wrappers/simplicial/test_SCCNNWrapper.py index f3614a7b..bc3e1807 100644 --- a/test/nn/wrappers/simplicial/test_SCCNNWrapper.py +++ b/test/nn/wrappers/simplicial/test_SCCNNWrapper.py @@ -1,26 +1,24 @@ """Unit tests for simplicial model wrappers""" -import torch -from torch_geometric.utils import get_laplacian -from ...._utils.nn_module_auto_test import NNModuleAutoTest -from ...._utils.flow_mocker import FlowMocker -from topobenchmark.nn.backbones.simplicial import SCCNNCustom from topomodelx.nn.simplicial.san import SAN -from topomodelx.nn.simplicial.scn2 import SCN2 from topomodelx.nn.simplicial.sccn import SCCN +from topomodelx.nn.simplicial.scn2 import SCN2 + +from topobenchmark.nn.backbones.simplicial import SCCNNCustom from topobenchmark.nn.wrappers import ( - SCCNWrapper, - SCCNNWrapper, SANWrapper, - SCNWrapper + SCCNNWrapper, + SCCNWrapper, + SCNWrapper, ) + class TestSimplicialWrappers: """Test simplicial model wrappers.""" def test_SCCNNWrapper(self, sg1_clique_lifted): """Test SCCNNWrapper. - + Parameters ---------- sg1_clique_lifted : torch_geometric.data.Data @@ -30,12 +28,17 @@ def test_SCCNNWrapper(self, sg1_clique_lifted): out_dim = 4 conv_order = 1 sc_order = 3 - init_args = (data.x_0.shape[1], data.x_1.shape[1], data.x_2.shape[1]), (out_dim, out_dim, out_dim), conv_order, sc_order + init_args = ( + (data.x_0.shape[1], data.x_1.shape[1], data.x_2.shape[1]), + (out_dim, out_dim, out_dim), + conv_order, + sc_order, + ) wrapper = SCCNNWrapper( - SCCNNCustom(*init_args), - out_channels=out_dim, - num_cell_dimensions=3 + SCCNNCustom(*init_args), + out_channels=out_dim, + num_cell_dimensions=3, ) out = wrapper(data) # Assert keys in output @@ -44,20 +47,20 @@ def test_SCCNNWrapper(self, sg1_clique_lifted): def test_SANWarpper(self, sg1_clique_lifted): """Test SANWarpper. - + Parameters ---------- sg1_clique_lifted : torch_geometric.data.Data - A fixture of simple graph 1 lifted with SimlicialCliqueLifting + A fixture of simple graph 1 lifted with SimlicialCliqueLifting """ data = sg1_clique_lifted out_dim = data.x_0.shape[1] hidden_channels = data.x_0.shape[1] wrapper = SANWrapper( - SAN(data.x_0.shape[1], hidden_channels), - out_channels=out_dim, - num_cell_dimensions=3 + SAN(data.x_0.shape[1], hidden_channels), + out_channels=out_dim, + num_cell_dimensions=3, ) out = wrapper(data) # Assert keys in output @@ -66,19 +69,19 @@ def test_SANWarpper(self, sg1_clique_lifted): def test_SCNWrapper(self, sg1_clique_lifted): """Test SCNWrapper. - + Parameters ---------- sg1_clique_lifted : torch_geometric.data.Data - A fixture of simple graph 1 lifted with SimlicialCliqueLifting + A fixture of simple graph 1 lifted with SimlicialCliqueLifting """ data = sg1_clique_lifted out_dim = data.x_0.shape[1] wrapper = SCNWrapper( - SCN2(data.x_0.shape[1], data.x_1.shape[1], data.x_2.shape[1]), - out_channels=out_dim, - num_cell_dimensions=3 + SCN2(data.x_0.shape[1], data.x_1.shape[1], data.x_2.shape[1]), + out_channels=out_dim, + num_cell_dimensions=3, ) out = wrapper(data) # Assert keys in output @@ -87,23 +90,22 @@ def test_SCNWrapper(self, sg1_clique_lifted): def test_SCCNWrapper(self, sg1_clique_lifted): """Test SCCNWrapper. - + Parameters ---------- sg1_clique_lifted : torch_geometric.data.Data - A fixture of simple graph 1 lifted with SimlicialCliqueLifting + A fixture of simple graph 1 lifted with SimlicialCliqueLifting """ data = sg1_clique_lifted out_dim = data.x_0.shape[1] max_rank = 2 wrapper = SCCNWrapper( - SCCN(data.x_0.shape[1], max_rank), - out_channels=out_dim, - num_cell_dimensions=3 + SCCN(data.x_0.shape[1], max_rank), + out_channels=out_dim, + num_cell_dimensions=3, ) out = wrapper(data) # Assert keys in output for key in ["labels", "batch_0", "x_0", "x_1", "x_2"]: assert key in out - diff --git a/topobenchmark/transforms/data_transform.py b/topobenchmark/transforms/data_transform.py index 8106f829..af48dc88 100755 --- a/topobenchmark/transforms/data_transform.py +++ b/topobenchmark/transforms/data_transform.py @@ -36,8 +36,9 @@ def _map_lifting_name(lifting_name): return LiftingTransform -def _route_lifting_kwargs(kwargs, LiftingMap): +def _route_lifting_kwargs(kwargs, LiftingMap, Transform): lifting_map_sign = inspect.signature(LiftingMap) + transform_sign = inspect.signature(Transform) lifting_map_kwargs = {} transform_kwargs = {} @@ -45,7 +46,7 @@ def _route_lifting_kwargs(kwargs, LiftingMap): for key, value in kwargs.items(): if key in lifting_map_sign.parameters: lifting_map_kwargs[key] = value - else: + elif key in transform_sign.parameters: transform_kwargs[key] = value return lifting_map_kwargs, transform_kwargs @@ -72,7 +73,7 @@ def __init__(self, transform_name, **kwargs): LiftingMap_ = TRANSFORMS[transform_name] Transform = _map_lifting_name(transform_name) lifting_map_kwargs, transform_kwargs = _route_lifting_kwargs( - kwargs, LiftingMap_ + kwargs, LiftingMap_, Transform ) lifting_map = LiftingMap_(**lifting_map_kwargs) From 1f5ad563eba1c84100e8c7561244625cb79b9fa2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20F=2E=20Pereira?= Date: Thu, 16 Jan 2025 16:26:15 -0800 Subject: [PATCH 26/43] Fix tutorial_lifting --- topobenchmark/transforms/__init__.py | 24 +++- topobenchmark/transforms/data_transform.py | 31 ++--- topobenchmark/transforms/liftings/__init__.py | 1 + .../liftings/graph2simplicial/clique.py | 2 +- tutorials/tutorial_lifting.ipynb | 124 ++++++++++-------- 5 files changed, 110 insertions(+), 72 deletions(-) diff --git a/topobenchmark/transforms/__init__.py b/topobenchmark/transforms/__init__.py index 62f8d85e..20840dfe 100755 --- a/topobenchmark/transforms/__init__.py +++ b/topobenchmark/transforms/__init__.py @@ -2,10 +2,32 @@ from .data_manipulations import DATA_MANIPULATIONS from .feature_liftings import FEATURE_LIFTINGS -from .liftings import LIFTINGS +from .liftings import ( + GRAPH2CELL_LIFTINGS, + GRAPH2HYPERGRAPH_LIFTINGS, + GRAPH2SIMPLICIAL_LIFTINGS, + LIFTINGS, +) TRANSFORMS = { **LIFTINGS, **FEATURE_LIFTINGS, **DATA_MANIPULATIONS, } + + +_map_lifting_type_to_dict = { + "graph2cell": GRAPH2CELL_LIFTINGS, + "graph2hypergraph": GRAPH2HYPERGRAPH_LIFTINGS, + "graph2simplicial": GRAPH2SIMPLICIAL_LIFTINGS, +} + + +def add_lifting_map(LiftingMap, lifting_type, name=None): + if name is None: + name = LiftingMap.__name__ + + liftings_dict = _map_lifting_type_to_dict[lifting_type] + + for dict_ in (liftings_dict, LIFTINGS, TRANSFORMS): + dict_[name] = LiftingMap diff --git a/topobenchmark/transforms/data_transform.py b/topobenchmark/transforms/data_transform.py index af48dc88..c1cda424 100755 --- a/topobenchmark/transforms/data_transform.py +++ b/topobenchmark/transforms/data_transform.py @@ -4,34 +4,29 @@ import torch_geometric -from topobenchmark.transforms import LIFTINGS, TRANSFORMS +from topobenchmark.transforms import ( + LIFTINGS, + TRANSFORMS, + _map_lifting_type_to_dict, +) from topobenchmark.transforms.liftings import ( - GRAPH2CELL_LIFTINGS, - GRAPH2HYPERGRAPH_LIFTINGS, - GRAPH2SIMPLICIAL_LIFTINGS, Graph2CellLiftingTransform, Graph2HypergraphLiftingTransform, Graph2SimplicialLiftingTransform, LiftingTransform, ) -_map_lifting_types = { - "graph2cell": (GRAPH2CELL_LIFTINGS, Graph2CellLiftingTransform), - "graph2hypergraph": ( - GRAPH2HYPERGRAPH_LIFTINGS, - Graph2HypergraphLiftingTransform, - ), - "graph2simplicial": ( - GRAPH2SIMPLICIAL_LIFTINGS, - Graph2SimplicialLiftingTransform, - ), +_map_lifting_type_to_transform = { + "graph2cell": Graph2CellLiftingTransform, + "graph2hypergraph": Graph2HypergraphLiftingTransform, + "graph2simplicial": Graph2SimplicialLiftingTransform, } -def _map_lifting_name(lifting_name): - for liftings_dict, Transform in _map_lifting_types.values(): +def _map_lifting_to_transform(lifting_name): + for key, liftings_dict in _map_lifting_type_to_dict.items(): if lifting_name in liftings_dict: - return Transform + return _map_lifting_type_to_transform[key] return LiftingTransform @@ -71,7 +66,7 @@ def __init__(self, transform_name, **kwargs): transform = TRANSFORMS[transform_name](**kwargs) else: LiftingMap_ = TRANSFORMS[transform_name] - Transform = _map_lifting_name(transform_name) + Transform = _map_lifting_to_transform(transform_name) lifting_map_kwargs, transform_kwargs = _route_lifting_kwargs( kwargs, LiftingMap_, Transform ) diff --git a/topobenchmark/transforms/liftings/__init__.py b/topobenchmark/transforms/liftings/__init__.py index 322c43a1..10e1e3c1 100755 --- a/topobenchmark/transforms/liftings/__init__.py +++ b/topobenchmark/transforms/liftings/__init__.py @@ -5,6 +5,7 @@ Graph2ComplexLiftingTransform, Graph2HypergraphLiftingTransform, Graph2SimplicialLiftingTransform, + LiftingMap, LiftingTransform, ) from .graph2cell import GRAPH2CELL_LIFTINGS diff --git a/topobenchmark/transforms/liftings/graph2simplicial/clique.py b/topobenchmark/transforms/liftings/graph2simplicial/clique.py index 04baa1ef..41047a62 100755 --- a/topobenchmark/transforms/liftings/graph2simplicial/clique.py +++ b/topobenchmark/transforms/liftings/graph2simplicial/clique.py @@ -50,7 +50,7 @@ def lift(self, domain): for set_k_simplices in simplices: simplicial_complex.add_simplices_from(list(set_k_simplices)) - # because Complex pads unexisting dimensions with empty matrices + # because ComplexData pads unexisting dimensions with empty matrices simplicial_complex.practical_dim = self.complex_dim return simplicial_complex diff --git a/tutorials/tutorial_lifting.ipynb b/tutorials/tutorial_lifting.ipynb index d1a77003..af533a1b 100644 --- a/tutorials/tutorial_lifting.ipynb +++ b/tutorials/tutorial_lifting.ipynb @@ -56,8 +56,6 @@ "\n", "import lightning as pl\n", "import networkx as nx\n", - "import hydra\n", - "import torch_geometric\n", "from omegaconf import OmegaConf\n", "from topomodelx.nn.simplicial.scn2 import SCN2\n", "from toponetx.classes import SimplicialComplex\n", @@ -72,8 +70,8 @@ "from topobenchmark.nn.readouts import PropagateSignalDown\n", "from topobenchmark.nn.wrappers.simplicial import SCNWrapper\n", "from topobenchmark.optimizer import TBOptimizer\n", - "from topobenchmark.transforms.liftings.graph2simplicial import (\n", - " Graph2SimplicialLifting,\n", + "from topobenchmark.transforms.liftings import (\n", + " LiftingMap,\n", ")" ] }, @@ -101,14 +99,17 @@ " \"data_domain\": \"graph\",\n", " \"data_type\": \"TUDataset\",\n", " \"data_name\": \"MUTAG\",\n", - " \"data_dir\": \"./data/MUTAG/\"}\n", + " \"data_dir\": \"./data/MUTAG/\",\n", + "}\n", "\n", "\n", - "transform_config = { \"clique_lifting\":\n", - " {\"_target_\": \"__main__.SimplicialCliquesLEQLifting\",\n", - " \"transform_name\": \"SimplicialCliquesLEQLifting\",\n", - " \"transform_type\": \"lifting\",\n", - " \"complex_dim\": 3,}\n", + "transform_config = {\n", + " \"clique_lifting\": {\n", + " \"_target_\": \"topobenchmark.transforms.data_transform.DataTransform\",\n", + " \"transform_name\": \"SimplicialCliquesLEQLifting\",\n", + " \"transform_type\": \"lifting\",\n", + " \"complex_dim\": 3,\n", + " }\n", "}\n", "\n", "split_config = {\n", @@ -138,21 +139,19 @@ "}\n", "\n", "loss_config = {\n", - " \"dataset_loss\": \n", - " {\n", - " \"task\": \"classification\", \n", - " \"loss_type\": \"cross_entropy\"\n", - " }\n", + " \"dataset_loss\": {\"task\": \"classification\", \"loss_type\": \"cross_entropy\"}\n", "}\n", "\n", - "evaluator_config = {\"task\": \"classification\",\n", - " \"num_classes\": out_channels,\n", - " \"metrics\": [\"accuracy\", \"precision\", \"recall\"]}\n", + "evaluator_config = {\n", + " \"task\": \"classification\",\n", + " \"num_classes\": out_channels,\n", + " \"metrics\": [\"accuracy\", \"precision\", \"recall\"],\n", + "}\n", "\n", - "optimizer_config = {\"optimizer_id\": \"Adam\",\n", - " \"parameters\":\n", - " {\"lr\": 0.001,\"weight_decay\": 0.0005}\n", - " }\n", + "optimizer_config = {\n", + " \"optimizer_id\": \"Adam\",\n", + " \"parameters\": {\"lr\": 0.001, \"weight_decay\": 0.0005},\n", + "}\n", "\n", "\n", "loader_config = OmegaConf.create(loader_config)\n", @@ -174,6 +173,7 @@ "def wrapper(**factory_kwargs):\n", " def factory(backbone):\n", " return SCNWrapper(backbone, **factory_kwargs)\n", + "\n", " return factory" ] }, @@ -197,16 +197,15 @@ "metadata": {}, "outputs": [], "source": [ - "class SimplicialCliquesLEQLifting(Graph2SimplicialLifting):\n", + "class SimplicialCliquesLEQLifting(LiftingMap):\n", " r\"\"\"Lifts graphs to simplicial complex domain by identifying the cliques as k-simplices. Only the cliques with size smaller or equal to the max complex dimension are considered.\n", - " \n", - " Args:\n", - " kwargs (optional): Additional arguments for the class.\n", " \"\"\"\n", - " def __init__(self, **kwargs):\n", - " super().__init__(**kwargs)\n", + " def __init__(self, complex_dim=2):\n", + " super().__init__()\n", + " self.complex_dim = complex_dim\n", + "\n", "\n", - " def lift_topology(self, data: torch_geometric.data.Data) -> dict:\n", + " def lift(self, domain) -> dict:\n", " r\"\"\"Lifts the topology of a graph to a simplicial complex by identifying the cliques as k-simplices. Only the cliques with size smaller or equal to the max complex dimension are considered.\n", "\n", " Args:\n", @@ -214,11 +213,14 @@ " Returns:\n", " dict: The lifted topology.\n", " \"\"\"\n", - " graph = self._generate_graph_from_data(data)\n", + " graph = domain\n", + "\n", " simplicial_complex = SimplicialComplex(graph)\n", " cliques = nx.find_cliques(graph)\n", - " \n", - " simplices: list[set[tuple[Any, ...]]] = [set() for _ in range(2, self.complex_dim + 1)]\n", + "\n", + " simplices: list[set[tuple[Any, ...]]] = [\n", + " set() for _ in range(2, self.complex_dim + 1)\n", + " ]\n", " for clique in cliques:\n", " if len(clique) <= self.complex_dim + 1:\n", " for i in range(2, self.complex_dim + 1):\n", @@ -227,8 +229,11 @@ "\n", " for set_k_simplices in simplices:\n", " simplicial_complex.add_simplices_from(list(set_k_simplices))\n", + " \n", + " # because ComplexData pads unexisting dimensions with empty matrices\n", + " simplicial_complex.practical_dim = self.complex_dim\n", "\n", - " return self._get_lifted_topology(simplicial_complex, graph)\n" + " return simplicial_complex" ] }, { @@ -251,9 +256,9 @@ "metadata": {}, "outputs": [], "source": [ - "from topobenchmark.transforms import TRANSFORMS\n", + "from topobenchmark.transforms import add_lifting_map\n", "\n", - "TRANSFORMS[\"SimplicialCliquesLEQLifting\"] = SimplicialCliquesLEQLifting" + "add_lifting_map(SimplicialCliquesLEQLifting, \"graph2simplicial\")" ] }, { @@ -275,8 +280,12 @@ "dataset, dataset_dir = graph_loader.load()\n", "\n", "preprocessor = PreProcessor(dataset, dataset_dir, transform_config)\n", - "dataset_train, dataset_val, dataset_test = preprocessor.load_dataset_splits(split_config)\n", - "datamodule = TBDataloader(dataset_train, dataset_val, dataset_test, batch_size=32)" + "dataset_train, dataset_val, dataset_test = preprocessor.load_dataset_splits(\n", + " split_config\n", + ")\n", + "datamodule = TBDataloader(\n", + " dataset_train, dataset_val, dataset_test, batch_size=32\n", + ")" ] }, { @@ -299,12 +308,19 @@ "metadata": {}, "outputs": [], "source": [ - "backbone = SCN2(in_channels_0=dim_hidden,in_channels_1=dim_hidden,in_channels_2=dim_hidden)\n", + "backbone = SCN2(\n", + " in_channels_0=dim_hidden,\n", + " in_channels_1=dim_hidden,\n", + " in_channels_2=dim_hidden,\n", + ")\n", "backbone_wrapper = wrapper(**wrapper_config)\n", "\n", "readout = PropagateSignalDown(**readout_config)\n", "loss = TBLoss(**loss_config)\n", - "feature_encoder = AllCellFeatureEncoder(in_channels=[in_channels, in_channels, in_channels], out_channels=dim_hidden)\n", + "feature_encoder = AllCellFeatureEncoder(\n", + " in_channels=[in_channels, in_channels, in_channels],\n", + " out_channels=dim_hidden,\n", + ")\n", "\n", "evaluator = TBEvaluator(**evaluator_config)\n", "optimizer = TBOptimizer(**optimizer_config)" @@ -316,14 +332,16 @@ "metadata": {}, "outputs": [], "source": [ - "model = TBModel(backbone=backbone,\n", - " backbone_wrapper=backbone_wrapper,\n", - " readout=readout,\n", - " loss=loss,\n", - " feature_encoder=feature_encoder,\n", - " evaluator=evaluator,\n", - " optimizer=optimizer,\n", - " compile=False,)" + "model = TBModel(\n", + " backbone=backbone,\n", + " backbone_wrapper=backbone_wrapper,\n", + " readout=readout,\n", + " loss=loss,\n", + " feature_encoder=feature_encoder,\n", + " evaluator=evaluator,\n", + " optimizer=optimizer,\n", + " compile=False,\n", + ")" ] }, { @@ -386,7 +404,9 @@ ], "source": [ "# Increase the number of epochs to get better results\n", - "trainer = pl.Trainer(max_epochs=50, accelerator=\"cpu\", enable_progress_bar=False)\n", + "trainer = pl.Trainer(\n", + " max_epochs=50, accelerator=\"cpu\", enable_progress_bar=False\n", + ")\n", "\n", "trainer.fit(model, datamodule)\n", "train_metrics = trainer.callback_metrics" @@ -415,9 +435,9 @@ } ], "source": [ - "print(' Training metrics\\n', '-'*26)\n", + "print(\" Training metrics\\n\", \"-\" * 26)\n", "for key in train_metrics:\n", - " print('{:<21s} {:>5.4f}'.format(key+':', train_metrics[key].item()))" + " print(\"{:<21s} {:>5.4f}\".format(key + \":\", train_metrics[key].item()))" ] }, { @@ -505,9 +525,9 @@ } ], "source": [ - "print(' Testing metrics\\n', '-'*25)\n", + "print(\" Testing metrics\\n\", \"-\" * 25)\n", "for key in test_metrics:\n", - " print('{:<20s} {:>5.4f}'.format(key+':', test_metrics[key].item()))" + " print(\"{:<20s} {:>5.4f}\".format(key + \":\", test_metrics[key].item()))" ] }, { From 81df9ac6e05a75d633079dbc03a5d3a094e6534e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20F=2E=20Pereira?= Date: Thu, 16 Jan 2025 19:48:35 -0800 Subject: [PATCH 27/43] Remove use of lambda func --- topobenchmark/transforms/_utils.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/topobenchmark/transforms/_utils.py b/topobenchmark/transforms/_utils.py index f14d156e..c2e0750c 100644 --- a/topobenchmark/transforms/_utils.py +++ b/topobenchmark/transforms/_utils.py @@ -19,7 +19,9 @@ def discover_objs(package_path, condition=None): Dictionary mapping class names to their corresponding class objects. """ if condition is None: - condition = lambda name, obj: True + + def condition(name, obj): + return True objs = {} From 4f10f7b6125895d11e9dc3f28aa369b789bae139 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20F=2E=20Pereira?= Date: Tue, 21 Jan 2025 18:05:39 -0800 Subject: [PATCH 28/43] Bump codecov to v5 --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 20ebf7d8..e6dbd429 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -44,7 +44,7 @@ jobs: pytest --cov --cov-report=xml:coverage.xml test/ - name: Upload coverage reports to Codecov - uses: codecov/codecov-action@v4.0.1 + uses: codecov/codecov-action@v5 with: token: ${{ secrets.CODECOV_TOKEN }} file: coverage.xml From 203676eed0259b1fdca191b610cadefad35d2eb4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20F=2E=20Pereira?= Date: Thu, 23 Jan 2025 10:34:21 -0800 Subject: [PATCH 29/43] Improve naming and docstrings; remove leftovers; remove redundant/unncessary test --- .../hypergraph/test_HypergraphKHopLifting.py | 9 +--- ...test_HypergraphKNearestNeighborsLifting.py | 14 ------ .../test_SimplicialCliqueLifting.py | 4 -- ...fting.py => test_SimplicialKHopLifting.py} | 2 - topobenchmark/data/utils/adapters.py | 26 +++++----- topobenchmark/data/utils/domain.py | 49 ++++++++++++++++--- .../feature_liftings/concatenation.py | 8 +-- .../transforms/feature_liftings/identity.py | 4 +- .../feature_liftings/projection_sum.py | 2 +- .../transforms/feature_liftings/set.py | 6 +-- topobenchmark/transforms/liftings/base.py | 8 +-- .../liftings/graph2hypergraph/khop.py | 2 - 12 files changed, 67 insertions(+), 67 deletions(-) rename test/transforms/liftings/simplicial/{test_SimplicialNeighborhoodLifting.py => test_SimplicialKHopLifting.py} (99%) diff --git a/test/transforms/liftings/hypergraph/test_HypergraphKHopLifting.py b/test/transforms/liftings/hypergraph/test_HypergraphKHopLifting.py index 68326f11..1106a2d7 100644 --- a/test/transforms/liftings/hypergraph/test_HypergraphKHopLifting.py +++ b/test/transforms/liftings/hypergraph/test_HypergraphKHopLifting.py @@ -21,14 +21,7 @@ def setup_method(self): HypergraphKHopLifting(k_value=2) ) - # TODO: delete? - # NB: `preserve_edge_attr` is never used? therefore they're equivalent - # self.lifting_edge_attr = HypergraphKHopLifting( - # k_value=1, preserve_edge_attr=True - # ) - self.lifting_edge_attr = Graph2HypergraphLiftingTransform( - HypergraphKHopLifting(k_value=1) - ) + self.lifting_edge_attr = self.lifting_k1 def test_lift_topology(self, simple_graph_2): """Test the lift_topology method. diff --git a/test/transforms/liftings/hypergraph/test_HypergraphKNearestNeighborsLifting.py b/test/transforms/liftings/hypergraph/test_HypergraphKNearestNeighborsLifting.py index 23dc5d35..95c0ae93 100644 --- a/test/transforms/liftings/hypergraph/test_HypergraphKNearestNeighborsLifting.py +++ b/test/transforms/liftings/hypergraph/test_HypergraphKNearestNeighborsLifting.py @@ -20,20 +20,6 @@ def setup_method(self): self.lifting_k3 = HypergraphKNNLifting(k_value=3, loop=True) self.lifting_no_loop = HypergraphKNNLifting(k_value=2, loop=False) - def test_initialization(self): - """Test initialization with different parameters.""" - # TODO: overkill, delete? - - # Test default parameters - lifting_default = HypergraphKNNLifting() - assert lifting_default.transform.k == 1 - assert lifting_default.transform.loop is True - - # Test custom parameters - lifting_custom = HypergraphKNNLifting(k_value=5, loop=False) - assert lifting_custom.transform.k == 5 - assert lifting_custom.transform.loop is False - def test_lift_topology_k2(self, simple_graph_2): """Test the lift_topology method with k=2. diff --git a/test/transforms/liftings/simplicial/test_SimplicialCliqueLifting.py b/test/transforms/liftings/simplicial/test_SimplicialCliqueLifting.py index a2c32ebf..1e84263b 100644 --- a/test/transforms/liftings/simplicial/test_SimplicialCliqueLifting.py +++ b/test/transforms/liftings/simplicial/test_SimplicialCliqueLifting.py @@ -216,8 +216,6 @@ def test_lift_topology(self, simple_graph_1): def test_lifted_features_signed(self, simple_graph_1): """Test the lift_features method in signed incidence cases.""" - # TODO: can be removed/moved; part of projection sum - self.data = simple_graph_1 # Test the lift_features method for signed case lifted_data = self.lifting_signed.forward(self.data) @@ -260,8 +258,6 @@ def test_lifted_features_signed(self, simple_graph_1): def test_lifted_features_unsigned(self, simple_graph_1): """Test the lift_features method in unsigned incidence cases.""" - # TODO: redundant. can be moved/removed - self.data = simple_graph_1 # Test the lift_features method for unsigned case lifted_data = self.lifting_unsigned.forward(self.data) diff --git a/test/transforms/liftings/simplicial/test_SimplicialNeighborhoodLifting.py b/test/transforms/liftings/simplicial/test_SimplicialKHopLifting.py similarity index 99% rename from test/transforms/liftings/simplicial/test_SimplicialNeighborhoodLifting.py rename to test/transforms/liftings/simplicial/test_SimplicialKHopLifting.py index 6a81d9f2..c46890ec 100644 --- a/test/transforms/liftings/simplicial/test_SimplicialNeighborhoodLifting.py +++ b/test/transforms/liftings/simplicial/test_SimplicialKHopLifting.py @@ -10,8 +10,6 @@ SimplicialKHopLifting, ) -# TODO: rename for consistency? - class TestSimplicialKHopLifting: """Test the SimplicialKHopLifting class.""" diff --git a/topobenchmark/data/utils/adapters.py b/topobenchmark/data/utils/adapters.py index b049d49c..f4e520b3 100644 --- a/topobenchmark/data/utils/adapters.py +++ b/topobenchmark/data/utils/adapters.py @@ -38,7 +38,7 @@ def adapt(self, domain): return domain -class Data2NxGraph(Adapter): +class Data2Graph(Adapter): """Data to nx.Graph adaptation. Parameters @@ -115,10 +115,10 @@ def adapt(self, domain: torch_geometric.data.Data) -> nx.Graph: return graph -class TnxComplex2ComplexData(Adapter): - """toponetx.Complex to Complex adaptation. +class Complex2ComplexData(Adapter): + """toponetx.Complex to ComplexData adaptation. - NB: order of features plays a crucial role, as ``Complex`` + NB: order of features plays a crucial role, as ``ComplexData`` simply stores them as lists (i.e. the reference to the indices of the simplex are lost). @@ -144,7 +144,7 @@ def __init__( self.transfer_features = transfer_features def adapt(self, domain): - """Adapt toponetx.Complex to Complex. + """Adapt toponetx.Complex to ComplexData. Parameters ---------- @@ -152,10 +152,8 @@ def adapt(self, domain): Returns ------- - Complex + ComplexData """ - # NB: just a slightly rewriting of get_complex_connectivity - practical_dim = ( domain.practical_dim if hasattr(domain, "practical_dim") @@ -221,7 +219,6 @@ def adapt(self, domain): else: raise ValueError("Can't transfer features.") - # TODO: confirm features are in the right order; update this data["features"] = [] for rank in range(dim + 1): rank_features_dict = get_features("features", rank) @@ -243,7 +240,7 @@ class ComplexData2Dict(Adapter): """ComplexData to dict adaptation.""" def adapt(self, domain): - """Adapt Complex to dict. + """Adapt ComplexData to dict. Parameters ---------- @@ -267,7 +264,6 @@ def adapt(self, domain): for rank, rank_info in enumerate(info): data[f"{connectivity_info}_{rank}"] = rank_info - # TODO: handle neighborhoods data["shape"] = domain.shape for index, values in enumerate(domain.features): @@ -291,7 +287,7 @@ def adapt(self, domain): ------- dict """ - hyperedges_key = domain.keys()[-1] + hyperedges_key = domain.rank_keys()[-1] return { "incidence_hyperedges": domain.incidence[hyperedges_key], "num_hyperedges": domain.num_hyperedges, @@ -301,6 +297,8 @@ def adapt(self, domain): class AdapterComposition(Adapter): + """Composed adapter.""" + def __init__(self, adapters): super().__init__() self.adapters = adapters @@ -313,7 +311,7 @@ def adapt(self, domain): return domain -class TnxComplex2Dict(AdapterComposition): +class Complex2Dict(AdapterComposition): """toponetx.Complex to dict adaptation. Parameters @@ -332,7 +330,7 @@ def __init__( signed=False, transfer_features=True, ): - tnxcomplex2complex = TnxComplex2ComplexData( + tnxcomplex2complex = Complex2ComplexData( neighborhoods=neighborhoods, signed=signed, transfer_features=transfer_features, diff --git a/topobenchmark/data/utils/domain.py b/topobenchmark/data/utils/domain.py index 57790162..1f14467d 100644 --- a/topobenchmark/data/utils/domain.py +++ b/topobenchmark/data/utils/domain.py @@ -2,13 +2,23 @@ class Data(abc.ABC): + """Topological data. + + Parameters + ---------- + incidence : collection[array-like] + Incidence matrices. + features : collection[array-like] + Features. + """ + def __init__(self, incidence, features): self.incidence = incidence self.features = features @abc.abstractmethod - def keys(self): - pass + def rank_keys(self): + """Keys to access different rank information.""" def update_features(self, rank, values): """Update features. @@ -34,11 +44,13 @@ def shape(self): None if self.incidence[key] is None else self.incidence[key].shape[-1] - for key in self.keys() + for key in self.rank_keys() ] class ComplexData(Data): + """Complex.""" + def __init__( self, incidence, @@ -59,7 +71,6 @@ def __init__( features = [None for _ in range(len(incidence))] else: for rank, incidence_ in enumerate(incidence): - # TODO: make error message more informative if ( features[rank] is not None and features[rank].shape[0] != incidence_.shape[-1] @@ -68,15 +79,22 @@ def __init__( super().__init__(incidence, features) - def keys(self): + def rank_keys(self): + """Keys to access different rank information. + + Returns + ------- + list[int] + """ return list(range(len(self.incidence))) class HypergraphData(Data): + """Hypergraph.""" + def __init__( self, incidence_hyperedges, - num_hyperedges, incidence_0=None, x_0=None, x_hyperedges=None, @@ -91,7 +109,22 @@ def __init__( self._hyperedges_key: x_hyperedges, } super().__init__(incidence, features) - self.num_hyperedges = num_hyperedges - def keys(self): + @property + def num_hyperedges(self): + """Number of hyperedges. + + Returns + ------- + int + """ + return self.incidence[self._hyperedges_key].shape[1] + + def rank_keys(self): + """Keys to access different rank information. + + Returns + ------- + list[int] + """ return [0, self._hyperedges_key] diff --git a/topobenchmark/transforms/feature_liftings/concatenation.py b/topobenchmark/transforms/feature_liftings/concatenation.py index 44e3b192..d147c392 100644 --- a/topobenchmark/transforms/feature_liftings/concatenation.py +++ b/topobenchmark/transforms/feature_liftings/concatenation.py @@ -16,16 +16,16 @@ def lift_features(self, domain): Parameters ---------- - data : Complex + data : Data The input data to be lifted. Returns ------- - Complex + Data Domain with the lifted features. """ for key, next_key in zip( - domain.keys(), domain.keys()[1:], strict=False + domain.rank_keys(), domain.rank_keys()[1:], strict=False ): if domain.features[next_key] is not None: continue @@ -44,7 +44,7 @@ def lift_features(self, domain): idxs = torch.stack(idxs_list, dim=0) values = domain.features[key][idxs].view(n, -1) else: - # NB: only works if key represents rank + # NB: only works if key is an int representing rank m = domain.features[key].shape[1] * (next_key + 1) values = torch.zeros([0, m]) diff --git a/topobenchmark/transforms/feature_liftings/identity.py b/topobenchmark/transforms/feature_liftings/identity.py index e640bd06..4128af1a 100644 --- a/topobenchmark/transforms/feature_liftings/identity.py +++ b/topobenchmark/transforms/feature_liftings/identity.py @@ -3,11 +3,9 @@ from topobenchmark.transforms.feature_liftings.base import FeatureLiftingMap -class Identity(FeatureLiftingMap): +class IdentityFeatureLifting(FeatureLiftingMap): """Identity feature lifting map.""" - # TODO: rename to IdentityFeatureLifting - def lift_features(self, domain): """Lift features of a domain using identity map.""" return domain diff --git a/topobenchmark/transforms/feature_liftings/projection_sum.py b/topobenchmark/transforms/feature_liftings/projection_sum.py index 757234a7..cc1b77e7 100644 --- a/topobenchmark/transforms/feature_liftings/projection_sum.py +++ b/topobenchmark/transforms/feature_liftings/projection_sum.py @@ -22,7 +22,7 @@ def lift_features(self, domain): Domain with the lifted features. """ for key, next_key in zip( - domain.keys(), domain.keys()[1:], strict=False + domain.rank_keys(), domain.rank_keys()[1:], strict=False ): if domain.features[next_key] is not None: continue diff --git a/topobenchmark/transforms/feature_liftings/set.py b/topobenchmark/transforms/feature_liftings/set.py index 54ac1b9d..db5d2c93 100644 --- a/topobenchmark/transforms/feature_liftings/set.py +++ b/topobenchmark/transforms/feature_liftings/set.py @@ -16,16 +16,16 @@ def lift_features(self, domain): Parameters ---------- - data : Complex + data : Data The input data to be lifted. Returns ------- - Complex + Data Domain with the lifted features. """ for key, next_key in zip( - domain.keys(), domain.keys()[1:], strict=False + domain.rank_keys(), domain.rank_keys()[1:], strict=False ): if domain.features[next_key] is not None: continue diff --git a/topobenchmark/transforms/liftings/base.py b/topobenchmark/transforms/liftings/base.py index 13f1f443..5709f0a1 100644 --- a/topobenchmark/transforms/liftings/base.py +++ b/topobenchmark/transforms/liftings/base.py @@ -5,11 +5,11 @@ import torch_geometric from topobenchmark.data.utils import ( + Complex2ComplexData, ComplexData2Dict, - Data2NxGraph, + Data2Graph, HypergraphData2Dict, IdentityAdapter, - TnxComplex2ComplexData, ) @@ -125,8 +125,8 @@ def __init__( super().__init__( lifting, feature_lifting=feature_lifting, - data2domain=Data2NxGraph(preserve_edge_attr), - domain2domain=TnxComplex2ComplexData( + data2domain=Data2Graph(preserve_edge_attr), + domain2domain=Complex2ComplexData( neighborhoods=neighborhoods, signed=signed, transfer_features=transfer_features, diff --git a/topobenchmark/transforms/liftings/graph2hypergraph/khop.py b/topobenchmark/transforms/liftings/graph2hypergraph/khop.py index 7c56006c..26b297ff 100755 --- a/topobenchmark/transforms/liftings/graph2hypergraph/khop.py +++ b/topobenchmark/transforms/liftings/graph2hypergraph/khop.py @@ -71,10 +71,8 @@ def lift(self, data: torch_geometric.data.Data) -> dict: ) incidence_1[n, neighbors] = 1 - num_hyperedges = incidence_1.shape[1] incidence_1 = torch.Tensor(incidence_1).to_sparse_coo() return HypergraphData( incidence_hyperedges=incidence_1, - num_hyperedges=num_hyperedges, x_0=data.x, ) From fd9e73177f7ff5b3b5b6f843176c62d3ecb5c9f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20F=2E=20Pereira?= Date: Thu, 23 Jan 2025 12:26:50 -0800 Subject: [PATCH 30/43] Allow for data2domain to be passed to Graph2ComplexLiftingTransform --- topobenchmark/transforms/liftings/base.py | 16 ++++++++++++---- .../liftings/graph2simplicial/clique.py | 2 +- .../transforms/liftings/graph2simplicial/khop.py | 2 +- 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/topobenchmark/transforms/liftings/base.py b/topobenchmark/transforms/liftings/base.py index 5709f0a1..d892db71 100644 --- a/topobenchmark/transforms/liftings/base.py +++ b/topobenchmark/transforms/liftings/base.py @@ -41,13 +41,13 @@ def __init__( domain2domain=None, feature_lifting="ProjectionSum", ): - if data2domain is None: + if data2domain is None or data2domain == "Identity": data2domain = IdentityAdapter() - if domain2dict is None: + if domain2dict is None or data2domain == "Identity": domain2dict = IdentityAdapter() - if domain2domain is None: + if domain2domain is None or data2domain == "Identity": domain2domain = IdentityAdapter() if isinstance(lifting, str): @@ -105,12 +105,16 @@ class Graph2ComplexLiftingTransform(LiftingTransform): Feature lifting map. preserve_edge_attr : bool Whether to preserve edge attributes. + Ignored if ``data2domain`` is not None. neighborhoods : list, optional List of neighborhoods of interest. signed : bool, optional If True, returns signed connectivity matrices. transfer_features : bool, optional Whether to transfer features. + data2domain : Converter + Conversion between ``torch_geometric.Data`` into + domain for consumption by lifting. """ def __init__( @@ -121,11 +125,15 @@ def __init__( neighborhoods=None, signed=False, transfer_features=True, + data2domain=None, ): + if data2domain is None: + data2domain = Data2Graph(preserve_edge_attr) + super().__init__( lifting, feature_lifting=feature_lifting, - data2domain=Data2Graph(preserve_edge_attr), + data2domain=data2domain, domain2domain=Complex2ComplexData( neighborhoods=neighborhoods, signed=signed, diff --git a/topobenchmark/transforms/liftings/graph2simplicial/clique.py b/topobenchmark/transforms/liftings/graph2simplicial/clique.py index 41047a62..448ae33a 100755 --- a/topobenchmark/transforms/liftings/graph2simplicial/clique.py +++ b/topobenchmark/transforms/liftings/graph2simplicial/clique.py @@ -34,7 +34,7 @@ def lift(self, domain): Returns ------- - toponetx.Complex + toponetx.SimplicialComplex Lifted simplicial complex. """ graph = domain diff --git a/topobenchmark/transforms/liftings/graph2simplicial/khop.py b/topobenchmark/transforms/liftings/graph2simplicial/khop.py index dc9e13e2..b4fb8f6e 100755 --- a/topobenchmark/transforms/liftings/graph2simplicial/khop.py +++ b/topobenchmark/transforms/liftings/graph2simplicial/khop.py @@ -46,7 +46,7 @@ def lift(self, domain): Returns ------- - toponetx.Complex + toponetx.SimplicialComplex Lifted simplicial complex. """ graph = domain From c83a584dd04d4c51e6a78087bf2397da025e7156 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20F=2E=20Pereira?= Date: Fri, 24 Jan 2025 12:14:18 -0800 Subject: [PATCH 31/43] Simplify Data2GraphAdapter --- topobenchmark/data/utils/adapters.py | 81 +++++++------------ topobenchmark/transforms/liftings/base.py | 19 ++++- .../liftings/graph2simplicial/khop.py | 3 + 3 files changed, 47 insertions(+), 56 deletions(-) diff --git a/topobenchmark/data/utils/adapters.py b/topobenchmark/data/utils/adapters.py index f4e520b3..99b375dc 100644 --- a/topobenchmark/data/utils/adapters.py +++ b/topobenchmark/data/utils/adapters.py @@ -6,7 +6,7 @@ import torch_geometric from topomodelx.utils.sparse import from_sparse from toponetx.classes import CellComplex, SimplicialComplex -from torch_geometric.utils.undirected import is_undirected, to_undirected +from torch_geometric.utils.convert import to_networkx from topobenchmark.data.utils.domain import ComplexData from topobenchmark.data.utils.utils import ( @@ -45,25 +45,21 @@ class Data2Graph(Adapter): ---------- preserve_edge_attr : bool Whether to preserve edge attributes. + to_undirected (bool or str, optional): If set to :obj:`True`, will + return a :class:`networkx.Graph` instead of a + :class:`networkx.DiGraph`. + By default, will include all edges and make them undirected. + If set to :obj:`"upper"`, the undirected graph will only correspond + to the upper triangle of the input adjacency matrix. + If set to :obj:`"lower"`, the undirected graph will only correspond + to the lower triangle of the input adjacency matrix. + Only applicable in case the :obj:`data` object holds a homogeneous + graph. (default: :obj:`False`) """ - def __init__(self, preserve_edge_attr=False): + def __init__(self, preserve_edge_attr=False, to_undirected=True): self.preserve_edge_attr = preserve_edge_attr - - def _data_has_edge_attr(self, data: torch_geometric.data.Data) -> bool: - r"""Check if the input data object has edge attributes. - - Parameters - ---------- - data : torch_geometric.data.Data - The input data. - - Returns - ------- - bool - Whether the data object has edge attributes. - """ - return hasattr(data, "edge_attr") and data.edge_attr is not None + self.to_undirected = to_undirected def adapt(self, domain: torch_geometric.data.Data) -> nx.Graph: r"""Generate a NetworkX graph from the input data object. @@ -78,41 +74,17 @@ def adapt(self, domain: torch_geometric.data.Data) -> nx.Graph: nx.Graph The generated NetworkX graph. """ - # Check if data object have edge_attr, return list of tuples as [(node_id, {'features':data}, 'dim':1)] or ?? - nodes = [ - (n, dict(features=domain.x[n], dim=0)) - for n in range(domain.x.shape[0]) - ] - - if self.preserve_edge_attr and self._data_has_edge_attr(domain): - # In case edge features are given, assign features to every edge - # TODO: confirm this is the desired behavior - if is_undirected(domain.edge_index, domain.edge_attr): - edge_index, edge_attr = (domain.edge_index, domain.edge_attr) - else: - edge_index, edge_attr = to_undirected( - domain.edge_index, domain.edge_attr - ) - - edges = [ - (i.item(), j.item(), dict(features=edge_attr[edge_idx], dim=1)) - for edge_idx, (i, j) in enumerate( - zip(edge_index[0], edge_index[1], strict=False) - ) - ] - - else: - # If edge_attr is not present, return list list of edges - edges = [ - (i.item(), j.item(), {}) - for i, j in zip( - domain.edge_index[0], domain.edge_index[1], strict=False - ) - ] - graph = nx.Graph() - graph.add_nodes_from(nodes) - graph.add_edges_from(edges) - return graph + edge_attrs = ( + "edge_attr" + if self.preserve_edge_attr and hasattr(domain, "edge_attr") + else None + ) + return to_networkx( + domain, + to_undirected=self.to_undirected, + node_attrs="x", + edge_attrs=edge_attrs, + ) class Complex2ComplexData(Adapter): @@ -142,6 +114,7 @@ def __init__( self.neighborhoods = neighborhoods self.signed = signed self.transfer_features = transfer_features + self._features_key = "x" def adapt(self, domain): """Adapt toponetx.Complex to ComplexData. @@ -221,9 +194,9 @@ def adapt(self, domain): data["features"] = [] for rank in range(dim + 1): - rank_features_dict = get_features("features", rank) + rank_features_dict = get_features(self._features_key, rank) if rank_features_dict: - rank_features = torch.stack( + rank_features = torch.tensor( list(rank_features_dict.values()) ) else: diff --git a/topobenchmark/transforms/liftings/base.py b/topobenchmark/transforms/liftings/base.py index d892db71..c78ab49c 100644 --- a/topobenchmark/transforms/liftings/base.py +++ b/topobenchmark/transforms/liftings/base.py @@ -39,7 +39,7 @@ def __init__( data2domain=None, domain2dict=None, domain2domain=None, - feature_lifting="ProjectionSum", + feature_lifting=None, ): if data2domain is None or data2domain == "Identity": data2domain = IdentityAdapter() @@ -55,6 +55,9 @@ def __init__( lifting = TRANSFORMS[lifting]() + if feature_lifting is None: + feature_lifting = "IdentityFeatureLifting" + if isinstance(feature_lifting, str): from topobenchmark.transforms import TRANSFORMS @@ -106,6 +109,17 @@ class Graph2ComplexLiftingTransform(LiftingTransform): preserve_edge_attr : bool Whether to preserve edge attributes. Ignored if ``data2domain`` is not None. + to_undirected (bool or str, optional): If set to :obj:`True`, will + return a :class:`networkx.Graph` instead of a + :class:`networkx.DiGraph`. + By default, will include all edges and make them undirected. + If set to :obj:`"upper"`, the undirected graph will only correspond + to the upper triangle of the input adjacency matrix. + If set to :obj:`"lower"`, the undirected graph will only correspond + to the lower triangle of the input adjacency matrix. + Only applicable in case the :obj:`data` object holds a homogeneous + graph. (default: :obj:`False`) + Ignored if ``data2domain`` is not None. neighborhoods : list, optional List of neighborhoods of interest. signed : bool, optional @@ -122,13 +136,14 @@ def __init__( lifting, feature_lifting="ProjectionSum", preserve_edge_attr=False, + to_undirected=True, neighborhoods=None, signed=False, transfer_features=True, data2domain=None, ): if data2domain is None: - data2domain = Data2Graph(preserve_edge_attr) + data2domain = Data2Graph(preserve_edge_attr, to_undirected) super().__init__( lifting, diff --git a/topobenchmark/transforms/liftings/graph2simplicial/khop.py b/topobenchmark/transforms/liftings/graph2simplicial/khop.py index b4fb8f6e..c70cf64d 100755 --- a/topobenchmark/transforms/liftings/graph2simplicial/khop.py +++ b/topobenchmark/transforms/liftings/graph2simplicial/khop.py @@ -79,4 +79,7 @@ def lift(self, domain): list_k_simplices = list_k_simplices[: self.max_k_simplices] simplicial_complex.add_simplices_from(list_k_simplices) + # because ComplexData pads unexisting dimensions with empty matrices + simplicial_complex.practical_dim = self.complex_dim + return simplicial_complex From 746332f7c4fb0401a5c18ad00268465e8a364871 Mon Sep 17 00:00:00 2001 From: Snopoff Date: Thu, 30 May 2024 22:16:24 +0300 Subject: [PATCH 32/43] added test, finished main part --- .../graph2simplicial/neighborhood_lifting.py | 56 +++ .../test_neighborhood_lifting.py | 90 +++++ .../neighborhood_lifting.ipynb | 364 ++++++++++++++++++ 3 files changed, 510 insertions(+) create mode 100644 modules/transforms/liftings/graph2simplicial/neighborhood_lifting.py create mode 100644 test/transforms/liftings/graph2simplicial/test_neighborhood_lifting.py create mode 100644 tutorials/graph2simplicial/neighborhood_lifting.ipynb diff --git a/modules/transforms/liftings/graph2simplicial/neighborhood_lifting.py b/modules/transforms/liftings/graph2simplicial/neighborhood_lifting.py new file mode 100644 index 00000000..cc227e1d --- /dev/null +++ b/modules/transforms/liftings/graph2simplicial/neighborhood_lifting.py @@ -0,0 +1,56 @@ +import networkx as nx +import torch +import torch_geometric +from toponetx.classes import SimplicialComplex + +from modules.data.utils.utils import get_complex_connectivity +from modules.transforms.liftings.graph2simplicial.base import Graph2SimplicialLifting + + +class NeighborhoodComplexLifting(Graph2SimplicialLifting): + r"""Lifts graphs to simplicial complex domain by constructing the neighborhood complex[1]. + + Parameters + ---------- + **kwargs : optional + Additional arguments for the class. + """ + + 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. + + Parameters + ---------- + data : torch_geometric.data.Data + The input data to be lifted. + + Returns + ------- + dict + The lifted topology. + """ + undir_edge_index = torch_geometric.utils.to_undirected(data.edge_index) + + simplices = [ + set( + undir_edge_index[1, j].tolist() + for j in torch.nonzero(undir_edge_index[0] == i).squeeze() + ) + for i in torch.unique(undir_edge_index[0]) + ] + + node_features = {i: data.x[i, :] for i in range(data.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() + + lifted_topology = self._get_lifted_topology(simplicial_complex, graph) + + return lifted_topology diff --git a/test/transforms/liftings/graph2simplicial/test_neighborhood_lifting.py b/test/transforms/liftings/graph2simplicial/test_neighborhood_lifting.py new file mode 100644 index 00000000..b8409cbe --- /dev/null +++ b/test/transforms/liftings/graph2simplicial/test_neighborhood_lifting.py @@ -0,0 +1,90 @@ +"""Test the message passing module.""" + +import torch + +from modules.data.utils.utils import load_manual_graph +from modules.transforms.liftings.graph2simplicial.neighborhood_lifting import ( + NeighborhoodComplexLifting, +) + + +class TestSimplicialCliqueLifting: + """Test the SimplicialCliqueLifting class.""" + + def setup_method(self): + # Load the graph + self.data = load_manual_graph() + print(self.data) + + # Initialise the SimplicialCliqueLifting class + self.lifting_signed = NeighborhoodComplexLifting(signed=True) + self.lifting_unsigned = NeighborhoodComplexLifting(signed=False) + + 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()) + + expected_incidence_1 = torch.tensor( + [ + [-1.0, -1.0, -1.0, -1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], + [1.0, 0.0, 0.0, 0.0, -1.0, -1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], + [0.0, 1.0, 0.0, 0.0, 1.0, 0.0, -1.0, -1.0, -1.0, -1.0, 0.0, 0.0, 0.0], + [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, -1.0, 0.0, 0.0], + [0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0], + [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, -1.0, -1.0], + [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 0.0], + [0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0], + ] + ) + + print(lifted_data_unsigned.incidence_1.to_dense()) + + assert ( + 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() + ).all(), "Something is wrong with signed incidence_1 (nodes to edges)." + + expected_incidence_2 = torch.tensor( + [ + [1.0, 1.0, 0.0, 0.0, 0.0, 0.0], + [-1.0, 0.0, 1.0, 1.0, 0.0, 0.0], + [0.0, -1.0, -1.0, 0.0, 0.0, 0.0], + [0.0, 0.0, 0.0, -1.0, 0.0, 0.0], + [1.0, 0.0, 0.0, 0.0, 1.0, 0.0], + [0.0, 1.0, 0.0, 0.0, -1.0, 0.0], + [0.0, 0.0, 0.0, 0.0, 0.0, 0.0], + [0.0, 0.0, 1.0, 0.0, 1.0, 0.0], + [0.0, 0.0, 0.0, 0.0, 0.0, 1.0], + [0.0, 0.0, 0.0, 1.0, 0.0, -1.0], + [0.0, 0.0, 0.0, 0.0, 0.0, 0.0], + [0.0, 0.0, 0.0, 0.0, 0.0, 0.0], + [0.0, 0.0, 0.0, 0.0, 0.0, 1.0], + ] + ) + + assert ( + 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)." + + expected_incidence_3 = torch.tensor( + [[-1.0], [1.0], [-1.0], [0.0], [1.0], [0.0]] + ) + + assert ( + abs(expected_incidence_3) == lifted_data_unsigned.incidence_3.to_dense() + ).all(), ( + "Something is wrong with unsigned incidence_3 (triangles to tetrahedrons)." + ) + assert ( + expected_incidence_3 == lifted_data_signed.incidence_3.to_dense() + ).all(), ( + "Something is wrong with signed incidence_3 (triangles to tetrahedrons)." + ) diff --git a/tutorials/graph2simplicial/neighborhood_lifting.ipynb b/tutorials/graph2simplicial/neighborhood_lifting.ipynb new file mode 100644 index 00000000..034fc3af --- /dev/null +++ b/tutorials/graph2simplicial/neighborhood_lifting.ipynb @@ -0,0 +1,364 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Graph-to-Simplicial Neighborhood Lifting Tutorial" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "***\n", + "This notebook shows how to import a dataset, with the desired lifting, and how to run a neural network using the loaded data.\n", + "\n", + "The notebook is divided into sections:\n", + "\n", + "- [Loading the dataset](#loading-the-dataset) loads the config files for the data and the desired tranformation, createsa a dataset object and visualizes it.\n", + "- [Loading and applying the lifting](#loading-and-applying-the-lifting) defines a simple neural network to test that the lifting creates the expected incidence matrices.\n", + "- [Create and run a simplicial nn model](#create-and-run-a-simplicial-nn-model) simply runs a forward pass of the model to check that everything is working as expected.\n", + "\n", + "***\n", + "***\n", + "\n", + "Note that for simplicity the notebook is setup to use a simple graph. However, there is a set of available datasets that you can play with.\n", + "\n", + "To switch to one of the available datasets, simply change the *dataset_name* variable in [Dataset config](#dataset-config) to one of the following names:\n", + "\n", + "* cocitation_cora\n", + "* cocitation_citeseer\n", + "* cocitation_pubmed\n", + "* MUTAG\n", + "* NCI1\n", + "* NCI109\n", + "* PROTEINS_TU\n", + "* AQSOL\n", + "* ZINC\n", + "***" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Imports and utilities" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "# With this cell any imported module is reloaded before each cell execution\n", + "%load_ext autoreload\n", + "%autoreload 2\n", + "from modules.data.load.loaders import GraphLoader\n", + "from modules.data.preprocess.preprocessor import PreProcessor\n", + "from modules.utils.utils import (\n", + " describe_data,\n", + " load_dataset_config,\n", + " load_model_config,\n", + " load_transform_config,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Loading the Dataset" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Here we just need to spicify the name of the available dataset that we want to load. First, the dataset config is read from the corresponding yaml file (located at `/configs/datasets/` directory), and then the data is loaded via the implemented `Loaders`.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "Dataset configuration for manual_dataset:\n", + "\n", + "{'data_domain': 'graph',\n", + " 'data_type': 'toy_dataset',\n", + " 'data_name': 'manual',\n", + " 'data_dir': 'datasets/graph/toy_dataset',\n", + " 'num_features': 1,\n", + " 'num_classes': 2,\n", + " 'task': 'classification',\n", + " 'loss_type': 'cross_entropy',\n", + " 'monitor_metric': 'accuracy',\n", + " 'task_level': 'node'}\n" + ] + } + ], + "source": [ + "dataset_name = \"manual_dataset\" # \"manual_dataset\"#\"PROTEINS_TU\"\n", + "dataset_config = load_dataset_config(dataset_name)\n", + "loader = GraphLoader(dataset_config)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We can then access to the data through the `load()`method:" + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "Dataset only contains 1 sample:\n" + ] + }, + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + " - Graph with 8 vertices and 13 edges.\n", + " - Features dimensions: [1, 0]\n", + " - There are 0 isolated nodes.\n", + "\n" + ] + } + ], + "source": [ + "dataset = loader.load()\n", + "describe_data(dataset)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Loading and Applying the Lifting" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "In this section we will instantiate the lifting we want to apply to the data. For this example the clique lifting was chosen. For a clique of n nodes the algorithm for $m=3,...,max(n, complex\\_dim)$ will create simplicials for every possible combinations containing m nodes of the clique. $complex\\_dim$ is a parameter of the lifting. This is a deterministic lifting, based on connectivity, that does not modify the initial connectivity of the graph. The problem of extracting all the cliques in a graph is NP-hard, on in some formulaitons NP-complete (clique decision problem). The computational complexity of this algorithm is $O(n^k k^2)$[[1]](https://www.sciencedirect.com/science/article/pii/S0019995885800413), where $n$ is the number of nodes in the graph and $k$ is the highest clique dimension considered.\n", + "\n", + "***\n", + "[[1]](https://www.sciencedirect.com/science/article/pii/S0019995885800413) Cook, S. A. (1985). A taxonomy of problems with fast parallel algorithms. Information and control, 64(1-3), 2-22.\n", + "***\n", + "\n", + "For simplicial complexes creating a lifting involves creating a `SimplicialComplex` object from topomodelx and adding simplices to it using the method `add_simplices_from`. The `SimplicialComplex` class then takes care of creating all the needed matrices.\n", + "\n", + "Similarly to before, we can specify the transformation we want to apply through its type and id --the correxponding config files located at `/configs/transforms`. \n", + "\n", + "Note that the *tranform_config* dictionary generated below can contain a sequence of tranforms if it is needed.\n", + "\n", + "This can also be used to explore liftings from one topological domain to another, for example using two liftings it is possible to achieve a sequence such as: graph -> simplicial complex -> hypergraph. " + ] + }, + { + "cell_type": "code", + "execution_count": 25, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "Transform configuration for graph2simplicial/neighborhood_lifting:\n", + "\n", + "{'transform_type': 'lifting',\n", + " 'transform_name': 'NeighborhoodComplexLifting',\n", + " 'preserve_edge_attr': False,\n", + " 'signed': True,\n", + " 'feature_lifting': 'ProjectionSum'}\n" + ] + } + ], + "source": [ + "# Define transformation type and id\n", + "transform_type = \"liftings\"\n", + "# If the transform is a topological lifting, it should include both the type of the lifting and the identifier\n", + "transform_id = \"graph2simplicial/neighborhood_lifting\"\n", + "\n", + "# Read yaml file\n", + "transform_config = {\n", + " \"lifting\": load_transform_config(transform_type, transform_id)\n", + " # other transforms (e.g. data manipulations, feature liftings) can be added here\n", + "}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We than apply the transform via our `PreProcessor`:" + ] + }, + { + "cell_type": "code", + "execution_count": 26, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Transform parameters are the same, using existing data_dir: /Users/snopoff/git_repos/challenge-icml-2024/datasets/graph/toy_dataset/manual/lifting/2172744449\n", + "\n", + "Dataset only contains 1 sample:\n", + "torch.Size([8, 1])\n", + "torch.Size([22, 1])\n", + "torch.Size([27, 1])\n", + "torch.Size([16, 1])\n", + "torch.Size([6, 1])\n", + "torch.Size([1, 1])\n" + ] + }, + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAgcAAAIbCAYAAAB/tT3bAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjkuMCwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy80BEi2AAAACXBIWXMAAA9hAAAPYQGoP6dpAADQxUlEQVR4nOydeZwcZZ3/308dfU9Pz5X7gIT71CTIZUQhCQjIZUZAcF1/ciiurqKCx+7quh4L6rrqogICAvHg8AYVElRAQIWEGC4FkpCQ+5iZvo+qep7fH9U9mZnMPT0zPZPn/XrN0d3VVU9Xd9fzeb6nUEopNBqNRqPRaMoY4z0AjUaj0Wg0tYUWBxqNRqPRaLqhxYFGo9FoNJpuaHGg0Wg0Go2mG1ocaDQajUaj6YYWBxqNRqPRaLqhxYFGo9FoNJpuaHGg0Wg0Go2mG1ocHOBcf/31NDQ09LvNwoULufrqq7vdt2rVKubPn48Qguuvv74qY1mzZg1CiEFvf/XVV9PQ0IAQgvnz53P11VezYcOGqoylP5YuXcqNN944avvv7XyPxzj6Oma13u/BMtTPxXhSi2Pt+Z6Nx3uomXhocaAZkE9/+tO0trZ23u7o6KC1tZX77rsPpRSf/vSnx3Q8GzZsYP78+WzYsIH77ruP9vZ2br75Ztra2rj//vvHdCyjQc/zrRmYymeyoaGB+fPnT4jJryJuGxoaBiUGNZqxRIsDzYAsX76cJUuWdN5etWoVjY2NLFiwAIBEItFpSRgLWltbmTdvHitXrmTJkiUkEgmWLFnCfffdx3XXXTcmYxhNejvfY3VuJypXXnklS5cuZePGjdx8883cf//9NS2wli5dSiKRoL29nY0bN7Jhw4aaHq/mwMMa7wFoNEPh/vvvZ82aNaxfv368h6KpETZs2EBHRwdXXXUVAEuWLOHmm29m6dKldHR0kEgkxneAvdDa2to53kQiQWtr64SwdmgOHLTlQDMgXX2U119/Pa2trWzYsAEhBFdffTWtra0sXbq08z4hBB0dHZ3Pr5hP58+fzy233NJ5f0dHB0uXLkUIwcKFC1m1atWAY7nnnntYsGAB8+bNG3Db1tZWbrnlFm655Rbmz5/fuf/777+fhQsXdsYq9HRFXH311Vx//fXdzL49t9m7d2+319Xf2FtbW7uZjSt+6a7xEZXzCt3P90Dndijj6MqNN97YGTPS89xff/31zJ8/f9Dm7v627+s9gOp9LubNm8fNN9/c7b5FixYB8Mwzz/T73IE+C62trdx44419nuPhfIaBTmFQYeXKlZ1j7o/+3re+zudAVOKOhvoaNJMcpTmgue6661Qikeh3myVLlqjrrruu8/Z9992n5s2b122b3u5TSqnly5erJUuWqPb2drV+/XqVSCTU6tWrO/e7YMECtX79etXe3q6WL1+uBvpILliwQF111VWDem2V/c+bN0/dd999nffffPPNnWNYuXKlAjpvV56XSCTUypUrVXt7u7rqqqsUoNavX9/5OND5+PLly3t97V3PTddzfN1116l58+apG264ofO+rmMczPkezjgqXHXVVWrBggVq9erVqr29Xa1cubLz2JX3q+t7smTJkm7H7Dq2wWzf23tQ7c9FTyrva3t7e7/bDeaz0N85HulY29vb1Q033ND5eeuPwbxvfZ3Pru9Z19srV65U8+bN6zxPK1eu7Pb6NQcuWhwc4IymOFi/fv1+F+ibb75ZXXfddZ2PVSZcpZRavXr1gBfWefPmdRvLQONOJBIDThA9J+qer7fncSsTQoXKpNIX7e3t3V7rggUL1A033NA5iVYer4xzKOJgKOPobSxdqZz/nuer68TVdWyD3b7nezAan4ue9PYeDobePgt9neORjrWyL2DAsfb3vvV3PiuvoS9x0FO4ajQVtFtBM2qsWbMGgIMPPrjTPH/99dezZs0a1qxZQyKRGJR7oCvz5s3r3G+F1tbWTpP70qVLuz1WCVjsyS233EJraysLFy4cVPrjkiVLum3X1QTc2NjY73MTiQQLFizoNNdu2LCBq666qvP2vffey7x584blGx/KOMAPbuzrvD/zzDO9jmPRokWsXLly2Nv3fA9G43PRldbWVhYsWMANN9wwqO0H+iz0dY5HOtYlS5aglGL9+vWsWbNmv89uV/p73/o7n4MZQ2NjY+d3ZzJk+2iqgxYHmlFlwYIFtLe3d/vpbaIZLEuXLmXVqlXd/O6VlMreMhV6u5guXLiQ++67j6uvvprVq1d3Zl0MhaFO5EuWLGHlypWsWrWqc7KsCJ2VK1eyfPnyIY9hOOPoj67ntJrb9/YeVPtzUaGSyTJYYTCYz8JoBzTOmzeP++67j1WrVnWLgaj8DKaWxXDPZyKRYP369dx8882dgZFjXTtDU5tocaAZNRYs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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + " - The complex has 8 0-cells.\n", + " - The 0-cells have features dimension 1\n", + " - The complex has 22 1-cells.\n", + " - The 1-cells have features dimension 1\n", + " - The complex has 27 2-cells.\n", + " - The 2-cells have features dimension 1\n", + " - The complex has 16 3-cells.\n", + " - The 3-cells have features dimension 1\n", + " - The complex has 6 4-cells.\n", + " - The 4-cells have features dimension 1\n", + " - The complex has 1 5-cells.\n", + " - The 5-cells have features dimension 1\n", + "\n" + ] + } + ], + "source": [ + "lifted_dataset = PreProcessor(dataset, transform_config, loader.data_dir)\n", + "describe_data(lifted_dataset)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Create and Run a Simplicial NN Model" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "In this section a simple model is created to test that the used lifting works as intended. In this case the model uses the `up_laplacian_1` and the `down_laplacian_1` so the lifting should make sure to add them to the data." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "Model configuration for simplicial SAN:\n", + "\n", + "{'in_channels': None,\n", + " 'hidden_channels': 32,\n", + " 'out_channels': None,\n", + " 'n_layers': 2,\n", + " 'n_filters': 2,\n", + " 'order_harmonic': 5,\n", + " 'epsilon_harmonic': 0.1}\n" + ] + } + ], + "source": [ + "from modules.models.simplicial.san import SANModel\n", + "\n", + "model_type = \"simplicial\"\n", + "model_id = \"san\"\n", + "model_config = load_model_config(model_type, model_id)\n", + "\n", + "model = SANModel(model_config, dataset_config)" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [], + "source": [ + "y_hat = model(lifted_dataset.get(0))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "If everything is correct the cell above should execute without errors. " + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "venv_topox", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.3" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} From 8859164824422016c1679cf68b0c6c3e2ccf041c Mon Sep 17 00:00:00 2001 From: Snopoff Date: Thu, 13 Jun 2024 12:27:34 +0300 Subject: [PATCH 33/43] Added tests --- .../test_neighborhood_lifting.py | 74 ++++++++----------- 1 file changed, 32 insertions(+), 42 deletions(-) diff --git a/test/transforms/liftings/graph2simplicial/test_neighborhood_lifting.py b/test/transforms/liftings/graph2simplicial/test_neighborhood_lifting.py index b8409cbe..e285147e 100644 --- a/test/transforms/liftings/graph2simplicial/test_neighborhood_lifting.py +++ b/test/transforms/liftings/graph2simplicial/test_neighborhood_lifting.py @@ -1,6 +1,7 @@ """Test the message passing module.""" import torch +import torch_geometric from modules.data.utils.utils import load_manual_graph from modules.transforms.liftings.graph2simplicial.neighborhood_lifting import ( @@ -8,13 +9,26 @@ ) +def create_test_graph(): + num_nodes = 5 + x = [1] * num_nodes + edge_index = [[0, 0, 1, 1, 2, 2, 3], [1, 4, 2, 3, 3, 4, 4]] + y = [0, 0, 1, 1, 0] + + return torch_geometric.data.Data( + x=torch.tensor(x).float().reshape(-1, 1), + edge_index=torch.Tensor(edge_index), + num_nodes=num_nodes, + y=torch.tensor(y), + ) + + class TestSimplicialCliqueLifting: """Test the SimplicialCliqueLifting class.""" def setup_method(self): # Load the graph - self.data = load_manual_graph() - print(self.data) + self.data = create_test_graph() # load_manual_graph() # Initialise the SimplicialCliqueLifting class self.lifting_signed = NeighborhoodComplexLifting(signed=True) @@ -26,22 +40,18 @@ 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( [ - [-1.0, -1.0, -1.0, -1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], - [1.0, 0.0, 0.0, 0.0, -1.0, -1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], - [0.0, 1.0, 0.0, 0.0, 1.0, 0.0, -1.0, -1.0, -1.0, -1.0, 0.0, 0.0, 0.0], - [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, -1.0, 0.0, 0.0], - [0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0], - [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, -1.0, -1.0], - [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 0.0], - [0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0], + [-1.0, -1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], + [0.0, 0.0, -1.0, -1.0, -1.0, 0.0, 0.0, 0.0], + [1.0, 0.0, 1.0, 0.0, 0.0, -1.0, -1.0, 0.0], + [0.0, 1.0, 0.0, 1.0, 0.0, 1.0, 0.0, -1.0], + [0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 1.0, 1.0], ] ) - print(lifted_data_unsigned.incidence_1.to_dense()) - assert ( 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()}" @@ -51,40 +61,20 @@ def test_lift_topology(self): expected_incidence_2 = torch.tensor( [ - [1.0, 1.0, 0.0, 0.0, 0.0, 0.0], - [-1.0, 0.0, 1.0, 1.0, 0.0, 0.0], - [0.0, -1.0, -1.0, 0.0, 0.0, 0.0], - [0.0, 0.0, 0.0, -1.0, 0.0, 0.0], - [1.0, 0.0, 0.0, 0.0, 1.0, 0.0], - [0.0, 1.0, 0.0, 0.0, -1.0, 0.0], - [0.0, 0.0, 0.0, 0.0, 0.0, 0.0], - [0.0, 0.0, 1.0, 0.0, 1.0, 0.0], - [0.0, 0.0, 0.0, 0.0, 0.0, 1.0], - [0.0, 0.0, 0.0, 1.0, 0.0, -1.0], - [0.0, 0.0, 0.0, 0.0, 0.0, 0.0], - [0.0, 0.0, 0.0, 0.0, 0.0, 0.0], - [0.0, 0.0, 0.0, 0.0, 0.0, 1.0], + [1.0, 0.0, 0.0], + [-1.0, 0.0, 0.0], + [0.0, 1.0, 0.0], + [0.0, 0.0, 1.0], + [0.0, -1.0, -1.0], + [1.0, 0.0, 0.0], + [0.0, 1.0, 0.0], + [0.0, 0.0, 1.0], ] ) assert ( abs(expected_incidence_2) == lifted_data_unsigned.incidence_2.to_dense() - ).all(), "Something is wrong with unsigned incidence_2 (edges to triangles)." + ).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() - ).all(), "Something is wrong with signed incidence_2 (edges to triangles)." - - expected_incidence_3 = torch.tensor( - [[-1.0], [1.0], [-1.0], [0.0], [1.0], [0.0]] - ) - - assert ( - abs(expected_incidence_3) == lifted_data_unsigned.incidence_3.to_dense() - ).all(), ( - "Something is wrong with unsigned incidence_3 (triangles to tetrahedrons)." - ) - assert ( - expected_incidence_3 == lifted_data_signed.incidence_3.to_dense() - ).all(), ( - "Something is wrong with signed incidence_3 (triangles to tetrahedrons)." - ) + ).all(), f"Something is wrong with signed incidence_2 (edges to triangles).\n{lifted_data_signed.incidence_2.to_dense()}" From 51dbddf067e542e979e9a91c4ca0f7fff13ad81e Mon Sep 17 00:00:00 2001 From: Snopoff Date: Fri, 21 Jun 2024 23:32:53 +0300 Subject: [PATCH 34/43] hope now it passes linting --- .../liftings/graph2simplicial/neighborhood_lifting.py | 6 +----- .../liftings/graph2simplicial/test_neighborhood_lifting.py | 1 - 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/modules/transforms/liftings/graph2simplicial/neighborhood_lifting.py b/modules/transforms/liftings/graph2simplicial/neighborhood_lifting.py index cc227e1d..99bcb2b5 100644 --- a/modules/transforms/liftings/graph2simplicial/neighborhood_lifting.py +++ b/modules/transforms/liftings/graph2simplicial/neighborhood_lifting.py @@ -1,9 +1,7 @@ -import networkx as nx import torch import torch_geometric from toponetx.classes import SimplicialComplex -from modules.data.utils.utils import get_complex_connectivity from modules.transforms.liftings.graph2simplicial.base import Graph2SimplicialLifting @@ -51,6 +49,4 @@ def lift_topology(self, data: torch_geometric.data.Data) -> dict: graph = simplicial_complex.graph_skeleton() - lifted_topology = self._get_lifted_topology(simplicial_complex, graph) - - return lifted_topology + return self._get_lifted_topology(simplicial_complex, graph) diff --git a/test/transforms/liftings/graph2simplicial/test_neighborhood_lifting.py b/test/transforms/liftings/graph2simplicial/test_neighborhood_lifting.py index e285147e..1e3909d2 100644 --- a/test/transforms/liftings/graph2simplicial/test_neighborhood_lifting.py +++ b/test/transforms/liftings/graph2simplicial/test_neighborhood_lifting.py @@ -3,7 +3,6 @@ import torch import torch_geometric -from modules.data.utils.utils import load_manual_graph from modules.transforms.liftings.graph2simplicial.neighborhood_lifting import ( NeighborhoodComplexLifting, ) From d5ecb79b0cf829a8e8512f1c574d34b7eb1564f5 Mon Sep 17 00:00:00 2001 From: Germandev55 Date: Wed, 26 Jun 2024 21:22:50 +0900 Subject: [PATCH 35/43] Update neighborhood_lifting.py test request --- .../liftings/graph2simplicial/neighborhood_lifting.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/modules/transforms/liftings/graph2simplicial/neighborhood_lifting.py b/modules/transforms/liftings/graph2simplicial/neighborhood_lifting.py index 99bcb2b5..f6082af6 100644 --- a/modules/transforms/liftings/graph2simplicial/neighborhood_lifting.py +++ b/modules/transforms/liftings/graph2simplicial/neighborhood_lifting.py @@ -1,12 +1,10 @@ import torch import torch_geometric from toponetx.classes import SimplicialComplex - from modules.transforms.liftings.graph2simplicial.base import Graph2SimplicialLifting - class NeighborhoodComplexLifting(Graph2SimplicialLifting): - r"""Lifts graphs to simplicial complex domain by constructing the neighborhood complex[1]. + r"""Liftss graphs to simplicial complex domain by constructing the neighborhood complex[1]. Parameters ---------- From 6186a23d8c1b7a16f01f61c97ea69a265376a0c4 Mon Sep 17 00:00:00 2001 From: Germandev55 Date: Wed, 26 Jun 2024 21:59:16 +0900 Subject: [PATCH 36/43] Update neighborhood_lifting.py test --- .../liftings/graph2simplicial/neighborhood_lifting.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/transforms/liftings/graph2simplicial/neighborhood_lifting.py b/modules/transforms/liftings/graph2simplicial/neighborhood_lifting.py index f6082af6..b7e949a4 100644 --- a/modules/transforms/liftings/graph2simplicial/neighborhood_lifting.py +++ b/modules/transforms/liftings/graph2simplicial/neighborhood_lifting.py @@ -21,7 +21,7 @@ def lift_topology(self, data: torch_geometric.data.Data) -> dict: Parameters ---------- - data : torch_geometric.data.Data + data : torch_geometric.data.Dataa The input data to be lifted. Returns From dfbcef509a1ac0285980a5642c206599b1036d1e Mon Sep 17 00:00:00 2001 From: Snopoff Date: Sun, 23 Jun 2024 12:49:01 +0300 Subject: [PATCH 37/43] ipynb changes --- .../graph2simplicial/neighborhood_lifting.ipynb | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/tutorials/graph2simplicial/neighborhood_lifting.ipynb b/tutorials/graph2simplicial/neighborhood_lifting.ipynb index 034fc3af..e070791a 100644 --- a/tutorials/graph2simplicial/neighborhood_lifting.ipynb +++ b/tutorials/graph2simplicial/neighborhood_lifting.ipynb @@ -81,7 +81,7 @@ }, { "cell_type": "code", - "execution_count": 23, + "execution_count": 2, "metadata": {}, "outputs": [ { @@ -119,7 +119,7 @@ }, { "cell_type": "code", - "execution_count": 24, + "execution_count": 3, "metadata": {}, "outputs": [ { @@ -184,7 +184,7 @@ }, { "cell_type": "code", - "execution_count": 25, + "execution_count": 4, "metadata": {}, "outputs": [ { @@ -224,7 +224,7 @@ }, { "cell_type": "code", - "execution_count": 26, + "execution_count": 7, "metadata": {}, "outputs": [ { @@ -233,13 +233,7 @@ "text": [ "Transform parameters are the same, using existing data_dir: /Users/snopoff/git_repos/challenge-icml-2024/datasets/graph/toy_dataset/manual/lifting/2172744449\n", "\n", - "Dataset only contains 1 sample:\n", - "torch.Size([8, 1])\n", - "torch.Size([22, 1])\n", - "torch.Size([27, 1])\n", - "torch.Size([16, 1])\n", - "torch.Size([6, 1])\n", - "torch.Size([1, 1])\n" + "Dataset only contains 1 sample:\n" ] }, { From 7d5dc1c9b9e62045351181fc2fcede9fa464a9a7 Mon Sep 17 00:00:00 2001 From: Snopoff Date: Wed, 26 Jun 2024 16:11:41 +0300 Subject: [PATCH 38/43] ruff pls --- .../liftings/graph2simplicial/neighborhood_lifting.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/modules/transforms/liftings/graph2simplicial/neighborhood_lifting.py b/modules/transforms/liftings/graph2simplicial/neighborhood_lifting.py index b7e949a4..99bcb2b5 100644 --- a/modules/transforms/liftings/graph2simplicial/neighborhood_lifting.py +++ b/modules/transforms/liftings/graph2simplicial/neighborhood_lifting.py @@ -1,10 +1,12 @@ import torch import torch_geometric from toponetx.classes import SimplicialComplex + from modules.transforms.liftings.graph2simplicial.base import Graph2SimplicialLifting + class NeighborhoodComplexLifting(Graph2SimplicialLifting): - r"""Liftss graphs to simplicial complex domain by constructing the neighborhood complex[1]. + r"""Lifts graphs to simplicial complex domain by constructing the neighborhood complex[1]. Parameters ---------- @@ -21,7 +23,7 @@ def lift_topology(self, data: torch_geometric.data.Data) -> dict: Parameters ---------- - data : torch_geometric.data.Dataa + data : torch_geometric.data.Data The input data to be lifted. Returns From 6be1175d8ab9ebb659d48981212009364e1eac27 Mon Sep 17 00:00:00 2001 From: Snopoff Date: Sat, 6 Jul 2024 15:38:49 +0300 Subject: [PATCH 39/43] finished ipynb --- .../neighborhood_lifting.ipynb | 29 ++++++++++++------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/tutorials/graph2simplicial/neighborhood_lifting.ipynb b/tutorials/graph2simplicial/neighborhood_lifting.ipynb index e070791a..100cf0a1 100644 --- a/tutorials/graph2simplicial/neighborhood_lifting.ipynb +++ b/tutorials/graph2simplicial/neighborhood_lifting.ipynb @@ -164,22 +164,29 @@ ] }, { + "attachments": { + "Neighborhood complex.png": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAABIUAAAJXCAYAAAANauSzAAAAinpUWHRSYXcgcHJvZmlsZSB0eXBlIGV4aWYAAHjaVY7LDcNACETvVJESZvlTTmTZUjpI+WG9lhy/A8wgNED793PQazKgpBbp5Y5GS4vfLRILAQZjzN51cXUZrbjt5Ul4Ca8M6L2oigcmnn6ERrj55ht3Ou8iZx0COrdm2Hyl7qA0rCh5zi3A/wfoB/bnLPLubZBkAAAKCGlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4KPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iWE1QIENvcmUgNC40LjAtRXhpdjIiPgogPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4KICA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIgogICAgeG1sbnM6ZXhpZj0iaHR0cDovL25zLmFkb2JlLmNvbS9leGlmLzEuMC8iCiAgICB4bWxuczp0aWZmPSJodHRwOi8vbnMuYWRvYmUuY29tL3RpZmYvMS4wLyIKICAgZXhpZjpQaXhlbFhEaW1lbnNpb249IjExNTciCiAgIGV4aWY6UGl4ZWxZRGltZW5zaW9uPSI1OTkiCiAgIHRpZmY6SW1hZ2VXaWR0aD0iMTE1NyIKICAgdGlmZjpJbWFnZUhlaWdodD0iNTk5IgogICB0aWZmOk9yaWVudGF0aW9uPSIxIi8+CiA8L3JkZjpSREY+CjwveDp4bXBtZXRhPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg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qLLlykqP9Q30U1zXwMAAcrkcYWFhrDgiytUYFCIiIiKdIZPJUKpUKfz111/JrvPu3Tvp8cZNG2FoaAgAGDliJFq3bs1KJNJRNWvWlB5/+fwlxXXlcjkAoFixYqw4IsrVGBQiIiIinVGkSBEcO3osxXV279mNMWPGAADWrV3HcYeICABgU8YG1tbWePXqFQICApJdLz4+XgoKValShRVHRLkaxxQiIiIiIiJKg969ewMAfO/6JrvO/fv3pcc9uvdgpRFRrsagEBEREVESiviv007Hx8ezQohI4jrQFSYmJrh//7403ti3Ll2+BABo27YtnJycWGlEpFZcXFzC//FxOVoOBoWIiIiIACgUCoSEhODKlSvSc2fOnEFERAQrh4gAAKamplixYgXi4uIwedJkCCGUlgcGBmLNmjUoUaIEli9bzgojIiVCCERFReGff/6RJrbw8fGBr68vZDJZjpQpn/i2JyMiIiLSMdOmTcP2HdthYGAAA30D5NfLDyEEFAoFYmJiEBsbi00bN/FXfyICABw7fgzjx49HlSpVMGjQIFiWtIT3HW+sWLECjRs1xvIVy2FqYsqKIiJJs+bNEBQUBH19feTLl0/6J4SQrjni4uLw/fff4+BfB7OtXAwKERERERERpVNYWBjc3d1xx+cOZNEyWFlZoU2bNmjcuDErh4i0BoNCREREREREREQ6iGMKERERERERERHpIAaFiIiIiIiIiIh0EINCREREREREREQ6iEEhIiIiIiIiIiIdxKAQEREREREREZEOYlCIiIiIiIiIiEgHMShERERERERERKSDGBQiIiIiIiIiItJBDAoREREREREREekgBoWIiIiIiIiIiHQQg0JERERERERERDqIQSEiIiIiIiIiIh3EoBARERERERERkQ5iUIiIiIiIiIiISAcxKEREREREREREpIMYFCIiIiIiIiIi0kEMChERERERERER6SAGhYiIiIiIiIiIdBCDQkREREREREREOohBISIiIiIiIiIiHcSgEBERERERERGRDmJQiIiIiIiIiIhIBzEoRERERERERESkgxgUIiIiIiIiIiLSQQwKERERERERERHpIAaFiIiIiIiIiIh0EINCREREREREREQ6iEEhIiIiIiIiIiIdxKAQEREREREREZEOYlCIiIiIiIiIiEgHMShERERERERERKSDGBQiIiIiIiIiItJBDAoREREREREREekgBoWIiIiIiIiIiHQQg0JERERERERERDqIQSEiIiIiIiIiIh3EoBARERERERERkQ5iUIiIiIiIiIiISAcxKEREREREREREpIMYFCIiIiIiIiIi0kEMChERERERERER6SAGhYiIiIiIiIiIdBCDQkREREREREREOohBISIiIiIiIiIiHcSgEBERERERERGRDmJQiIiIiIiIiIhIBzEoRERERERERESkgxgUIiIiIiIiIiLSQQwKERERERERERHpIAaFiIiIiIiIiIh0EINCREREREREREQ6iEEhIiIiIiIiIiIdxKAQEREREREREZEOYlCIiIiIiIiIiEgHMShEpGHR0dHw8fFBaGioVpY/KioKL1++hFwuz/LXefHihdbWE2mnmJgYPH/+HP7+/oiIiFBZLpPJEBwczIoiIiIiIp1QgFVAuubQoUPYvn27RvbVqnUrjB0zVvrbx8cHfX7sg5CQEBgYGGDJ4iXo27evVtTL3n17sWH9Bjx89BAAkD9/flSrVg0jRoxAp46doKenp5HXOX7iONasWYO7d+9CoVAAAAwNDdGqVSuMGjkKtWvXzrE6OH36NGQyGbp168Y3Sib5P/TH6dOncebMGcycMRPNmzfPkXIoFArcuHEDBw4cwMVLF/HhwwcAgJGRESIjI2FhYQE7Ozs0bNgQZW3LYs/ePWjVshVGjBiR7WWNiIjAtWvXcPnKZVy+fBlHjxyFlZUVG1MWiYuLw48//ggvby/89ttv+GXwL7m6vWuyvDyfOde/xcfH4/bt2zh77izOnDmDfXv3oXz58uzTcwkhBHx8fHD+/HmcPXcWs2fNRosWLfgG4+ceUZ7GoBDpnPbt28PBwQEnT57E8hXLIYRQWad1q9Zo06YNDAoaIDIyEqEhoXj46CE8PDwQFRUlrVe2bFml7Sb/NhkhISEAALlcjsm/TUanTp1QtGjRXFsfCoUCv035Ddu2bVN5/t69exg8eDC21tuKPXv2ZOo4goODMWLkCFy7dk1lWXR0NE6ePInTp09j546daNeuXY7UxZKlSxARHoEuXbogf37eSJkRO3fuxB9//IGgV0Ff25JQ5EhZPDw8MGHiBAQGBkJfXx+9evZCnx/7oJJdJRQrVgwhISG4e/cuZs2ehSVLlkjbTZwwMVvLee/ePcxfMB/Xrl1DbGys0pcT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" + } + }, "cell_type": "markdown", "metadata": {}, "source": [ - "In this section we will instantiate the lifting we want to apply to the data. For this example the clique lifting was chosen. For a clique of n nodes the algorithm for $m=3,...,max(n, complex\\_dim)$ will create simplicials for every possible combinations containing m nodes of the clique. $complex\\_dim$ is a parameter of the lifting. This is a deterministic lifting, based on connectivity, that does not modify the initial connectivity of the graph. The problem of extracting all the cliques in a graph is NP-hard, on in some formulaitons NP-complete (clique decision problem). The computational complexity of this algorithm is $O(n^k k^2)$[[1]](https://www.sciencedirect.com/science/article/pii/S0019995885800413), where $n$ is the number of nodes in the graph and $k$ is the highest clique dimension considered.\n", + "In this section we will instantiate the neighborhood lifting. This lifting constructs a simplicial complex, called the **Neighborhood complex**, as it is usually defined in the field of **topological combinatorics**. Let me briefly describe this construction, for more details please see [[1]](https://doi.org/10.1007/978-3-540-76649-0).\n", "\n", - "***\n", - "[[1]](https://www.sciencedirect.com/science/article/pii/S0019995885800413) Cook, S. A. (1985). A taxonomy of problems with fast parallel algorithms. Information and control, 64(1-3), 2-22.\n", - "***\n", + "Consider a graph $G=(V,E)$. Its neighborhood complex $N(G)$ is a simplicial complex with the vertex set $V$ and simplices given by subsets $A\\subseteq V$ such, that $\\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. Below is an example of the constructed neighborhood complex from a graph.\n", + "\n", + "![Neighborhood complex.png]()\n", "\n", - "For simplicial complexes creating a lifting involves creating a `SimplicialComplex` object from topomodelx and adding simplices to it using the method `add_simplices_from`. The `SimplicialComplex` class then takes care of creating all the needed matrices.\n", + "This complex in fact can be seen as a special case of **Dowker's complex** [[2]](https://www.jstor.org/stable/1969768) (or see [this nLab page](https://ncatlab.org/nlab/show/Dowker%27s+theorem) for more details): given a graph $G=(V,E)$, consider the following symmetric relation $R$ on the set $V$ of vertices: $$ xRy \\iff (x,y)\\in E. $$ The Dowker's complex consists of simplices $\\{x_0, ..., x_n\\}$ such that $\\exists y: \\forall i: x_iRy$.\n", "\n", - "Similarly to before, we can specify the transformation we want to apply through its type and id --the correxponding config files located at `/configs/transforms`. \n", + "Then, just following the Dowker's construction, one can obtain a neighborhood complex: indeed, an existence of a simplex $\\sigma$ in a neighborhood complex on the set of vertices $\\{v_0, ..., v_n\\}$ constitutes the existence of a vertex $w$ such that $\\forall i: (v_i, w)\\in E$. But then $\\forall i: v_iRw$ hence there's a simplex on the vertices $\\{v_0, ..., v_n\\}$ in the Dowker's complex. Converse holds as well.\n", "\n", - "Note that the *tranform_config* dictionary generated below can contain a sequence of tranforms if it is needed.\n", + "***\n", + "[[1]](https://doi.org/10.1007/978-3-540-76649-0) Matoušek, J. (2008). Using the Borsuk–Ulam Theorem. Springer Berlin Heidelberg.\n", "\n", - "This can also be used to explore liftings from one topological domain to another, for example using two liftings it is possible to achieve a sequence such as: graph -> simplicial complex -> hypergraph. " + "[[2]](https://www.jstor.org/stable/1969768) C. H. Dowker, Homology Groups of Relations, Annals of Math. 56 (1952), 84–95.\n", + "***" ] }, { @@ -224,7 +231,7 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 5, "metadata": {}, "outputs": [ { @@ -287,7 +294,7 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 6, "metadata": {}, "outputs": [ { @@ -319,7 +326,7 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 7, "metadata": {}, "outputs": [], "source": [ From a57c1aa7f31bf15cb51cd6af71550a3ab519b76a Mon Sep 17 00:00:00 2001 From: Snopoff Date: Wed, 10 Jul 2024 00:51:46 +0300 Subject: [PATCH 40/43] remove invalid picture --- tutorials/graph2simplicial/neighborhood_lifting.ipynb | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/tutorials/graph2simplicial/neighborhood_lifting.ipynb b/tutorials/graph2simplicial/neighborhood_lifting.ipynb index 100cf0a1..fa5c7ceb 100644 --- a/tutorials/graph2simplicial/neighborhood_lifting.ipynb +++ b/tutorials/graph2simplicial/neighborhood_lifting.ipynb @@ -164,19 +164,12 @@ ] }, { - "attachments": { - "Neighborhood complex.png": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAABIUAAAJXCAYAAAANauSzAAAAinpUWHRSYXcgcHJvZmlsZSB0eXBlIGV4aWYAAHjaVY7LDcNACETvVJESZvlTTmTZUjpI+WG9lhy/A8wgNED793PQazKgpBbp5Y5GS4vfLRILAQZjzN51cXUZrbjt5Ul4Ca8M6L2oigcmnn6ERrj55ht3Ou8iZx0COrdm2Hyl7qA0rCh5zi3A/wfoB/bnLPLubZBkAAAKCGlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4KPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iWE1QIENvcmUgNC40LjAtRXhpdjIiPgogPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4KICA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIgogICAgeG1sbnM6ZXhpZj0iaHR0cDovL25zLmFkb2JlLmNvbS9leGlmLzEuMC8iCiAgICB4bWxuczp0aWZmPSJodHRwOi8vbnMuYWRvYmUuY29tL3RpZmYvMS4wLyIKICAgZXhpZjpQaXhlbFhEaW1lbnNpb249IjExNTciCiAgIGV4aWY6UGl4ZWxZRGltZW5zaW9uPSI1OTkiCiAgIHRpZmY6SW1hZ2VXaWR0aD0iMTE1NyIKICAgdGlmZjpJbWFnZUhlaWdodD0iNTk5IgogICB0aWZmOk9yaWVudGF0aW9uPSIxIi8+CiA8L3JkZjpSREY+CjwveDp4bXBtZXRhPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg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qLLlykqP9Q30U1zXwMAAcrkcYWFhrDgiytUYFCIiIiKdIZPJUKpUKfz111/JrvPu3Tvp8cZNG2FoaAgAGDliJFq3bs1KJNJRNWvWlB5/+fwlxXXlcjkAoFixYqw4IsrVGBQiIiIinVGkSBEcO3osxXV279mNMWPGAADWrV3HcYeICABgU8YG1tbWePXqFQICApJdLz4+XgoKValShRVHRLkaxxQiIiIiIiJKg969ewMAfO/6JrvO/fv3pcc9uvdgpRFRrsagEBEREVESiviv007Hx8ezQohI4jrQFSYmJrh//7403ti3Ll2+BABo27YtnJycWGlEpFZcXFzC//FxOVoOBoWIiIiIACgUCoSEhODKlSvSc2fOnEFERAQrh4gAAKamplixYgXi4uIwedJkCCGUlgcGBmLNmjUoUaIEli9bzgojIiVCCERFReGff/6RJrbw8fGBr68vZDJZjpQpn/i2JyMiIiLSMdOmTcP2HdthYGAAA30D5NfLDyEEFAoFYmJiEBsbi00bN/FXfyICABw7fgzjx49HlSpVMGjQIFiWtIT3HW+sWLECjRs1xvIVy2FqYsqKIiJJs+bNEBQUBH19feTLl0/6J4SQrjni4uLw/fff4+BfB7OtXAwKERERERERpVNYWBjc3d1xx+cOZNEyWFlZoU2bNmjcuDErh4i0BoNCREREREREREQ6iGMKERERERERERHpIAaFiIiIiIiIiIh0EINCREREREREREQ6iEEhIiIiIiIiIiIdxKAQEREREREREZEOYlCIiIiIiIiIiEgHMShERERERERERKSDGBQiIiIiIiIiItJBDAoREREREREREekgBoWIiIiIiIiIiHQQg0JERERERERERDqIQSEiIiIiIiIiIh3EoBARERERERERkQ5iUIiIiIiIiIiISAcxKEREREREREREpIMYFCIiIiIiIiIi0kEMChERERERERER6SAGhYiIiIiIiIiIdBCDQkREREREREREOohBISIiIiIiIiIiHcSgEBERERERERGRDmJQiIiIiIiIiIhIBzEoRERERERERESkgxgUIiIiIiIiIiLSQQwKERERERERERHpIAaFiIiIiIiIiIh0EINCREREREREREQ6iEEhIiIiIiIiIiIdxKAQEREREREREZEOYlCIiIiIiIiIiEgHMShERERERERERKSDGBQiIiIiIiIiItJBDAoREREREREREekgBoWIiIiIiIiIiHQQg0JERERERERERDqIQSEiIiIiIiIiIh3EoBARERERERERkQ5iUIiIiIiIiIiISAcxKEREREREREREpIMYFCIiIiIiIiIi0kEMChERERERERER6SAGhYiIiIiIiIiIdBCDQkREREREREREOohBISIiIiIiIiIiHcSgEBERERERERGRDmJQiIiIiIiIiIhIBzEoRERERERERESkgxgUIiIiIiIiIiLSQQwKERERERERERHpIAaFiIiIiIiIiIh0EINCREREREREREQ6iEEhIiIiIiIiIiIdxKAQEREREREREZEOYlCIiIiIiIiIiEgHMShEpGHR0dHw8fFBaGioVpY/KioKL1++hFwuz/LXefHihdbWE2mnmJgYPH/+HP7+/oiIiFBZLpPJEBwczIoiIiIiIp1QgFVAuubQoUPYvn27RvbVqnUrjB0zVvrbx8cHfX7sg5CQEBgYGGDJ4iXo27evVtTL3n17sWH9Bjx89BAAkD9/flSrVg0jRoxAp46doKenp5HXOX7iONasWYO7d+9CoVAAAAwNDdGqVSuMGjkKtWvXzrE6OH36NGQyGbp168Y3Sib5P/TH6dOncebMGcycMRPNmzfPkXIoFArcuHEDBw4cwMVLF/HhwwcAgJGRESIjI2FhYQE7Ozs0bNgQZW3LYs/ePWjVshVGjBiR7WWNiIjAtWvXcPnKZVy+fBlHjxyFlZUVG1MWiYuLw48//ggvby/89ttv+GXwL7m6vWuyvDyfOde/xcfH4/bt2zh77izOnDmDfXv3oXz58uzTcwkhBHx8fHD+/HmcPXcWs2fNRosWLfgG4+ceUZ7GoBDpnPbt28PBwQEnT57E8hXLIYRQWad1q9Zo06YNDAoaIDIyEqEhoXj46CE8PDwQFRUlrVe2bFml7Sb/NhkhISEAALlcjsm/TUanTp1QtGjRXFsfCoUCv035Ddu2bVN5/t69exg8eDC21tuKPXv2ZOo4goODMWLkCFy7dk1lWXR0NE6ePInTp09j546daNeuXY7UxZKlSxARHoEuXbogf37eSJkRO3fuxB9//IGgV0Ff25JQ5EhZPDw8MGHiBAQGBkJfXx+9evZCnx/7oJJdJRQrVgwhISG4e/cuZs2ehSVLlkjbTZwwMVvLee/ePcxfMB/Xrl1DbGys0pcT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" - } - }, "cell_type": "markdown", "metadata": {}, "source": [ "In this section we will instantiate the neighborhood lifting. This lifting constructs a simplicial complex, called the **Neighborhood complex**, as it is usually defined in the field of **topological combinatorics**. Let me briefly describe this construction, for more details please see [[1]](https://doi.org/10.1007/978-3-540-76649-0).\n", "\n", - "Consider a graph $G=(V,E)$. Its neighborhood complex $N(G)$ is a simplicial complex with the vertex set $V$ and simplices given by subsets $A\\subseteq V$ such, that $\\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. Below is an example of the constructed neighborhood complex from a graph.\n", - "\n", - "![Neighborhood complex.png]()\n", + "Consider a graph $G=(V,E)$. Its neighborhood complex $N(G)$ is a simplicial complex with the vertex set $V$ and simplices given by subsets $A\\subseteq V$ such, that $\\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.\n", "\n", "This complex in fact can be seen as a special case of **Dowker's complex** [[2]](https://www.jstor.org/stable/1969768) (or see [this nLab page](https://ncatlab.org/nlab/show/Dowker%27s+theorem) for more details): given a graph $G=(V,E)$, consider the following symmetric relation $R$ on the set $V$ of vertices: $$ xRy \\iff (x,y)\\in E. $$ The Dowker's complex consists of simplices $\\{x_0, ..., x_n\\}$ such that $\\exists y: \\forall i: x_iRy$.\n", "\n", From 9a93e2b215303baf9e114b4b48366e18421ccc46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20F=2E=20Pereira?= Date: Thu, 23 Jan 2025 11:40:35 -0800 Subject: [PATCH 41/43] Move icml2024-pr6 files --- .../test_NeighborhoodComplexLifting.py} | 0 .../graph2simplicial/neighborhood_lifting.py | 0 .../neighborhood_lifting.ipynb | 358 ------------------ 3 files changed, 358 deletions(-) rename test/transforms/liftings/{graph2simplicial/test_neighborhood_lifting.py => simplicial/test_NeighborhoodComplexLifting.py} (100%) rename {modules => topobenchmark}/transforms/liftings/graph2simplicial/neighborhood_lifting.py (100%) delete mode 100644 tutorials/graph2simplicial/neighborhood_lifting.ipynb diff --git a/test/transforms/liftings/graph2simplicial/test_neighborhood_lifting.py b/test/transforms/liftings/simplicial/test_NeighborhoodComplexLifting.py similarity index 100% rename from test/transforms/liftings/graph2simplicial/test_neighborhood_lifting.py rename to test/transforms/liftings/simplicial/test_NeighborhoodComplexLifting.py diff --git a/modules/transforms/liftings/graph2simplicial/neighborhood_lifting.py b/topobenchmark/transforms/liftings/graph2simplicial/neighborhood_lifting.py similarity index 100% rename from modules/transforms/liftings/graph2simplicial/neighborhood_lifting.py rename to topobenchmark/transforms/liftings/graph2simplicial/neighborhood_lifting.py diff --git a/tutorials/graph2simplicial/neighborhood_lifting.ipynb b/tutorials/graph2simplicial/neighborhood_lifting.ipynb deleted file mode 100644 index fa5c7ceb..00000000 --- a/tutorials/graph2simplicial/neighborhood_lifting.ipynb +++ /dev/null @@ -1,358 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Graph-to-Simplicial Neighborhood Lifting Tutorial" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "***\n", - "This notebook shows how to import a dataset, with the desired lifting, and how to run a neural network using the loaded data.\n", - "\n", - "The notebook is divided into sections:\n", - "\n", - "- [Loading the dataset](#loading-the-dataset) loads the config files for the data and the desired tranformation, createsa a dataset object and visualizes it.\n", - "- [Loading and applying the lifting](#loading-and-applying-the-lifting) defines a simple neural network to test that the lifting creates the expected incidence matrices.\n", - "- [Create and run a simplicial nn model](#create-and-run-a-simplicial-nn-model) simply runs a forward pass of the model to check that everything is working as expected.\n", - "\n", - "***\n", - "***\n", - "\n", - "Note that for simplicity the notebook is setup to use a simple graph. However, there is a set of available datasets that you can play with.\n", - "\n", - "To switch to one of the available datasets, simply change the *dataset_name* variable in [Dataset config](#dataset-config) to one of the following names:\n", - "\n", - "* cocitation_cora\n", - "* cocitation_citeseer\n", - "* cocitation_pubmed\n", - "* MUTAG\n", - "* NCI1\n", - "* NCI109\n", - "* PROTEINS_TU\n", - "* AQSOL\n", - "* ZINC\n", - "***" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Imports and utilities" - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "metadata": {}, - "outputs": [], - "source": [ - "# With this cell any imported module is reloaded before each cell execution\n", - "%load_ext autoreload\n", - "%autoreload 2\n", - "from modules.data.load.loaders import GraphLoader\n", - "from modules.data.preprocess.preprocessor import PreProcessor\n", - "from modules.utils.utils import (\n", - " describe_data,\n", - " load_dataset_config,\n", - " load_model_config,\n", - " load_transform_config,\n", - ")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Loading the Dataset" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Here we just need to spicify the name of the available dataset that we want to load. First, the dataset config is read from the corresponding yaml file (located at `/configs/datasets/` directory), and then the data is loaded via the implemented `Loaders`.\n" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\n", - "Dataset configuration for manual_dataset:\n", - "\n", - "{'data_domain': 'graph',\n", - " 'data_type': 'toy_dataset',\n", - " 'data_name': 'manual',\n", - " 'data_dir': 'datasets/graph/toy_dataset',\n", - " 'num_features': 1,\n", - " 'num_classes': 2,\n", - " 'task': 'classification',\n", - " 'loss_type': 'cross_entropy',\n", - " 'monitor_metric': 'accuracy',\n", - " 'task_level': 'node'}\n" - ] - } - ], - "source": [ - "dataset_name = \"manual_dataset\" # \"manual_dataset\"#\"PROTEINS_TU\"\n", - "dataset_config = load_dataset_config(dataset_name)\n", - "loader = GraphLoader(dataset_config)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "We can then access to the data through the `load()`method:" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\n", - "Dataset only contains 1 sample:\n" - ] - }, - { - "data": { - "image/png": "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", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - " - Graph with 8 vertices and 13 edges.\n", - " - Features dimensions: [1, 0]\n", - " - There are 0 isolated nodes.\n", - "\n" - ] - } - ], - "source": [ - "dataset = loader.load()\n", - "describe_data(dataset)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Loading and Applying the Lifting" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "In this section we will instantiate the neighborhood lifting. This lifting constructs a simplicial complex, called the **Neighborhood complex**, as it is usually defined in the field of **topological combinatorics**. Let me briefly describe this construction, for more details please see [[1]](https://doi.org/10.1007/978-3-540-76649-0).\n", - "\n", - "Consider a graph $G=(V,E)$. Its neighborhood complex $N(G)$ is a simplicial complex with the vertex set $V$ and simplices given by subsets $A\\subseteq V$ such, that $\\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.\n", - "\n", - "This complex in fact can be seen as a special case of **Dowker's complex** [[2]](https://www.jstor.org/stable/1969768) (or see [this nLab page](https://ncatlab.org/nlab/show/Dowker%27s+theorem) for more details): given a graph $G=(V,E)$, consider the following symmetric relation $R$ on the set $V$ of vertices: $$ xRy \\iff (x,y)\\in E. $$ The Dowker's complex consists of simplices $\\{x_0, ..., x_n\\}$ such that $\\exists y: \\forall i: x_iRy$.\n", - "\n", - "Then, just following the Dowker's construction, one can obtain a neighborhood complex: indeed, an existence of a simplex $\\sigma$ in a neighborhood complex on the set of vertices $\\{v_0, ..., v_n\\}$ constitutes the existence of a vertex $w$ such that $\\forall i: (v_i, w)\\in E$. But then $\\forall i: v_iRw$ hence there's a simplex on the vertices $\\{v_0, ..., v_n\\}$ in the Dowker's complex. Converse holds as well.\n", - "\n", - "***\n", - "[[1]](https://doi.org/10.1007/978-3-540-76649-0) Matoušek, J. (2008). Using the Borsuk–Ulam Theorem. Springer Berlin Heidelberg.\n", - "\n", - "[[2]](https://www.jstor.org/stable/1969768) C. H. Dowker, Homology Groups of Relations, Annals of Math. 56 (1952), 84–95.\n", - "***" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\n", - "Transform configuration for graph2simplicial/neighborhood_lifting:\n", - "\n", - "{'transform_type': 'lifting',\n", - " 'transform_name': 'NeighborhoodComplexLifting',\n", - " 'preserve_edge_attr': False,\n", - " 'signed': True,\n", - " 'feature_lifting': 'ProjectionSum'}\n" - ] - } - ], - "source": [ - "# Define transformation type and id\n", - "transform_type = \"liftings\"\n", - "# If the transform is a topological lifting, it should include both the type of the lifting and the identifier\n", - "transform_id = \"graph2simplicial/neighborhood_lifting\"\n", - "\n", - "# Read yaml file\n", - "transform_config = {\n", - " \"lifting\": load_transform_config(transform_type, transform_id)\n", - " # other transforms (e.g. data manipulations, feature liftings) can be added here\n", - "}" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "We than apply the transform via our `PreProcessor`:" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Transform parameters are the same, using existing data_dir: /Users/snopoff/git_repos/challenge-icml-2024/datasets/graph/toy_dataset/manual/lifting/2172744449\n", - "\n", - "Dataset only contains 1 sample:\n" - ] - }, - { - "data": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAAAgcAAAIbCAYAAAB/tT3bAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjkuMCwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy80BEi2AAAACXBIWXMAAA9hAAAPYQGoP6dpAADQxUlEQVR4nOydeZwcZZ3/308dfU9Pz5X7gIT71CTIZUQhCQjIZUZAcF1/ciiurqKCx+7quh4L6rrqogICAvHg8AYVElRAQIWEGC4FkpCQ+5iZvo+qep7fH9U9mZnMPT0zPZPn/XrN0d3VVU9Xd9fzeb6nUEopNBqNRqPRaMoY4z0AjUaj0Wg0tYUWBxqNRqPRaLqhxYFGo9FoNJpuaHGg0Wg0Go2mG1ocaDQajUaj6YYWBxqNRqPRaLqhxYFGo9FoNJpuaHGg0Wg0Go2mG1ocHOBcf/31NDQ09LvNwoULufrqq7vdt2rVKubPn48Qguuvv74qY1mzZg1CiEFvf/XVV9PQ0IAQgvnz53P11VezYcOGqoylP5YuXcqNN944avvv7XyPxzj6Oma13u/BMtTPxXhSi2Pt+Z6Nx3uomXhocaAZkE9/+tO0trZ23u7o6KC1tZX77rsPpRSf/vSnx3Q8GzZsYP78+WzYsIH77ruP9vZ2br75Ztra2rj//vvHdCyjQc/zrRmYymeyoaGB+fPnT4jJryJuGxoaBiUGNZqxRIsDzYAsX76cJUuWdN5etWoVjY2NLFiwAIBEItFpSRgLWltbmTdvHitXrmTJkiUkEgmWLFnCfffdx3XXXTcmYxhNejvfY3VuJypXXnklS5cuZePGjdx8883cf//9NS2wli5dSiKRoL29nY0bN7Jhw4aaHq/mwMMa7wFoNEPh/vvvZ82aNaxfv368h6KpETZs2EBHRwdXXXUVAEuWLOHmm29m6dKldHR0kEgkxneAvdDa2to53kQiQWtr64SwdmgOHLTlQDMgXX2U119/Pa2trWzYsAEhBFdffTWtra0sXbq08z4hBB0dHZ3Pr5hP58+fzy233NJ5f0dHB0uXLkUIwcKFC1m1atWAY7nnnntYsGAB8+bNG3Db1tZWbrnlFm655Rbmz5/fuf/777+fhQsXdsYq9HRFXH311Vx//fXdzL49t9m7d2+319Xf2FtbW7uZjSt+6a7xEZXzCt3P90Dndijj6MqNN97YGTPS89xff/31zJ8/f9Dm7v627+s9gOp9LubNm8fNN9/c7b5FixYB8Mwzz/T73IE+C62trdx44419nuPhfIaBTmFQYeXKlZ1j7o/+3re+zudAVOKOhvoaNJMcpTmgue6661Qikeh3myVLlqjrrruu8/Z9992n5s2b122b3u5TSqnly5erJUuWqPb2drV+/XqVSCTU6tWrO/e7YMECtX79etXe3q6WL1+uBvpILliwQF111VWDem2V/c+bN0/dd999nffffPPNnWNYuXKlAjpvV56XSCTUypUrVXt7u7rqqqsUoNavX9/5OND5+PLly3t97V3PTddzfN1116l58+apG264ofO+rmMczPkezjgqXHXVVWrBggVq9erVqr29Xa1cubLz2JX3q+t7smTJkm7H7Dq2wWzf23tQ7c9FTyrva3t7e7/bDeaz0N85HulY29vb1Q033ND5eeuPwbxvfZ3Pru9Z19srV65U8+bN6zxPK1eu7Pb6NQcuWhwc4IymOFi/fv1+F+ibb75ZXXfddZ2PVSZcpZRavXr1gBfWefPmdRvLQONOJBIDThA9J+qer7fncSsTQoXKpNIX7e3t3V7rggUL1A033NA5iVYer4xzKOJgKOPobSxdqZz/nuer68TVdWyD3b7nezAan4ue9PYeDobePgt9neORjrWyL2DAsfb3vvV3PiuvoS9x0FO4ajQVtFtBM2qsWbMGgIMPPrjTPH/99dezZs0a1qxZQyKRGJR7oCvz5s3r3G+F1tbWTpP70qVLuz1WCVjsyS233EJraysLFy4cVPrjkiVLum3X1QTc2NjY73MTiQQLFizoNNdu2LCBq666qvP2vffey7x584blGx/KOMAPbuzrvD/zzDO9jmPRokWsXLly2Nv3fA9G43PRldbWVhYsWMANN9wwqO0H+iz0dY5HOtYlS5aglGL9+vWsWbNmv89uV/p73/o7n4MZQ2NjY+d3ZzJk+2iqgxYHmlFlwYIFtLe3d/vpbaIZLEuXLmXVqlXd/O6VlMreMhV6u5guXLiQ++67j6uvvprVq1d3Zl0MhaFO5EuWLGHlypWsWrWqc7KsCJ2VK1eyfPnyIY9hOOPoj67ntJrb9/YeVPtzUaGSyTJYYTCYz8JoBzTOmzeP++67j1WrVnWLgaj8DKaWxXDPZyKRYP369dx8882dgZFjXTtDU5tocaAZNRYs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", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - " - The complex has 8 0-cells.\n", - " - The 0-cells have features dimension 1\n", - " - The complex has 22 1-cells.\n", - " - The 1-cells have features dimension 1\n", - " - The complex has 27 2-cells.\n", - " - The 2-cells have features dimension 1\n", - " - The complex has 16 3-cells.\n", - " - The 3-cells have features dimension 1\n", - " - The complex has 6 4-cells.\n", - " - The 4-cells have features dimension 1\n", - " - The complex has 1 5-cells.\n", - " - The 5-cells have features dimension 1\n", - "\n" - ] - } - ], - "source": [ - "lifted_dataset = PreProcessor(dataset, transform_config, loader.data_dir)\n", - "describe_data(lifted_dataset)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Create and Run a Simplicial NN Model" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "In this section a simple model is created to test that the used lifting works as intended. In this case the model uses the `up_laplacian_1` and the `down_laplacian_1` so the lifting should make sure to add them to the data." - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\n", - "Model configuration for simplicial SAN:\n", - "\n", - "{'in_channels': None,\n", - " 'hidden_channels': 32,\n", - " 'out_channels': None,\n", - " 'n_layers': 2,\n", - " 'n_filters': 2,\n", - " 'order_harmonic': 5,\n", - " 'epsilon_harmonic': 0.1}\n" - ] - } - ], - "source": [ - "from modules.models.simplicial.san import SANModel\n", - "\n", - "model_type = \"simplicial\"\n", - "model_id = \"san\"\n", - "model_config = load_model_config(model_type, model_id)\n", - "\n", - "model = SANModel(model_config, dataset_config)" - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "metadata": {}, - "outputs": [], - "source": [ - "y_hat = model(lifted_dataset.get(0))" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "If everything is correct the cell above should execute without errors. " - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "venv_topox", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.11.3" - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} From ac22a74b716c58b023c6f76bf466e9198257761a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20F=2E=20Pereira?= Date: Thu, 23 Jan 2025 12:24:05 -0800 Subject: [PATCH 42/43] Update NeighborhoodComplexLifting to work with new design --- .../test_NeighborhoodComplexLifting.py | 22 +++++-- .../graph2simplicial/neighborhood_lifting.py | 66 ++++++++++++------- 2 files changed, 56 insertions(+), 32 deletions(-) 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 From 8b9c0c6eabade8541c64ffa72c21d8e91ae09f91 Mon Sep 17 00:00:00 2001 From: Coerulatus Date: Thu, 13 Feb 2025 11:14:05 +0000 Subject: [PATCH 43/43] fixed pr --- .../simplicial/test_NeighborhoodComplexLifting.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/test/transforms/liftings/simplicial/test_NeighborhoodComplexLifting.py b/test/transforms/liftings/simplicial/test_NeighborhoodComplexLifting.py index 61a0361d..d50c441d 100644 --- a/test/transforms/liftings/simplicial/test_NeighborhoodComplexLifting.py +++ b/test/transforms/liftings/simplicial/test_NeighborhoodComplexLifting.py @@ -10,6 +10,13 @@ def create_test_graph(): + """Create a simple test graph. + + Returns + ------- + torch_geometric.data.Data + A simple test graph. + """ num_nodes = 5 x = [1] * num_nodes edge_index = [[0, 0, 1, 1, 2, 2, 3], [1, 4, 2, 3, 3, 4, 4]] @@ -17,7 +24,7 @@ def create_test_graph(): return torch_geometric.data.Data( x=torch.tensor(x).float().reshape(-1, 1), - edge_index=torch.Tensor(edge_index), + edge_index=torch.tensor(edge_index, dtype=torch.long), num_nodes=num_nodes, y=torch.tensor(y), ) @@ -27,6 +34,7 @@ class TestNeighborhoodComplexLifting: """Test the SimplicialCliqueLifting class.""" def setup_method(self): + """Initialise the test class.""" # Load the graph self.data = create_test_graph() # load_manual_graph()