Skip to content

Commit 5f650c1

Browse files
committed
Merge branch 'sukjulian-feature-lifting-expander't push origin main
2 parents 193fe77 + 49e5109 commit 5f650c1

File tree

6 files changed

+661
-1
lines changed

6 files changed

+661
-1
lines changed
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
transform_type: 'lifting'
2+
transform_name: "ExpanderGraphLifting"
3+
node_degree: 2
4+
feature_lifting: ProjectionSum

modules/data/load/loaders.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -291,7 +291,10 @@ def load(self) -> torch_geometric.data.Dataset:
291291
feature_generator=self.feature_generator,
292292
target_generator=self.target_generator,
293293
)
294-
elif self.parameters.data_name == "random_points":
294+
elif (
295+
self.parameters.data_name == "random_points"
296+
or self.parameters.data_name == "toy_point_cloud"
297+
):
295298
data = load_random_points(
296299
dim=self.parameters["dim"],
297300
num_classes=self.parameters["num_classes"],

modules/transforms/data_transform.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@
1414
from modules.transforms.liftings.graph2combinatorial.ring_close_atoms_lifting import (
1515
CombinatorialRingCloseAtomsLifting,
1616
)
17+
from modules.transforms.liftings.graph2hypergraph.expander_graph_lifting import (
18+
ExpanderGraphLifting,
19+
)
1720
from modules.transforms.liftings.graph2hypergraph.knn_lifting import (
1821
HypergraphKNNLifting,
1922
)
@@ -39,6 +42,7 @@
3942
TRANSFORMS = {
4043
# Graph -> Hypergraph
4144
"HypergraphKNNLifting": HypergraphKNNLifting,
45+
"ExpanderGraphLifting": ExpanderGraphLifting,
4246
# Graph -> Simplicial Complex
4347
"SimplicialCliqueLifting": SimplicialCliqueLifting,
4448
"SimplicialLineLifting": SimplicialLineLifting,
Lines changed: 355 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,355 @@
1+
import warnings
2+
3+
import networkx
4+
import torch
5+
import torch_geometric
6+
7+
from modules.transforms.liftings.graph2hypergraph.base import (
8+
Graph2HypergraphLifting,
9+
)
10+
11+
12+
class ExpanderGraphLifting(Graph2HypergraphLifting):
13+
r"""Lifts graphs to expander (hyper)graph. More precisely, the expander is a random Ramanujan graph.
14+
15+
Parameters
16+
----------
17+
node_degree : int
18+
The desired node degree of the expander graph. Must be even.
19+
**kwargs : optional
20+
Additional arguments for the class.
21+
"""
22+
23+
def __init__(self, node_degree: int, **kwargs):
24+
super().__init__(**kwargs)
25+
26+
assert node_degree % 2 == 0, "Only even node degree is supported."
27+
28+
self.node_degree = node_degree
29+
30+
def lift_topology(self, data: torch_geometric.data.Data) -> dict:
31+
r"""Lifts the topology of a graph to an expander hypergraph.
32+
33+
Parameters
34+
----------
35+
data : torch_geometric.data.Data
36+
The input data to be lifted.
37+
38+
Returns
39+
-------
40+
dict
41+
The lifted topology.
42+
"""
43+
44+
expander_graph = random_regular_expander_graph(
45+
data.num_nodes, self.node_degree
46+
)
47+
48+
# Catch superfluous warning
49+
with warnings.catch_warnings():
50+
warnings.simplefilter(action="ignore", category=FutureWarning)
51+
52+
incidence_matrix = networkx.incidence_matrix(
53+
expander_graph
54+
).tocoo()
55+
56+
coo_indices = torch.stack(
57+
(
58+
torch.from_numpy(incidence_matrix.row),
59+
torch.from_numpy(incidence_matrix.col),
60+
)
61+
)
62+
coo_values = torch.from_numpy(
63+
incidence_matrix.data.astype("f4")
64+
) # 4 bytes floating point number (single precision)
65+
66+
incidence_matrix = torch.sparse_coo_tensor(coo_indices, coo_values)
67+
68+
return {
69+
"incidence_hyperedges": incidence_matrix,
70+
"num_hyperedges": incidence_matrix.size(1),
71+
"x_0": data.x,
72+
}
73+
74+
75+
"""
76+
Random regular expander graphs are available from networkx >= 3.3 which currently conflicts dependencies. Thus we include the networkx
77+
implementation here. After upgrade to networkx >= 3.3 this should be removed. Upgrading should also get rid of the FutureWarnings.
78+
"""
79+
80+
if "random_regular_expander_graph" in networkx.generators.expanders.__all__:
81+
from networkx.generators.expanders import random_regular_expander_graph
82+
83+
else:
84+
nx = networkx
85+
86+
@nx.utils.decorators.np_random_state("seed")
87+
# @nx._dispatchable(graphs=None, returns_graph=True)
88+
def maybe_regular_expander(
89+
n, d, *, create_using=None, max_tries=100, seed=None
90+
):
91+
r"""Utility for creating a random regular expander.
92+
93+
Returns a random $d$-regular graph on $n$ nodes which is an expander
94+
graph with very good probability.
95+
96+
Parameters
97+
----------
98+
n : int
99+
The number of nodes.
100+
d : int
101+
The degree of each node.
102+
create_using : Graph Instance or Constructor
103+
Indicator of type of graph to return.
104+
If a Graph-type instance, then clear and use it.
105+
If a constructor, call it to create an empty graph.
106+
Use the Graph constructor by default.
107+
max_tries : int. (default: 100)
108+
The number of allowed loops when generating each independent cycle
109+
seed : (default: None)
110+
Seed used to set random number generation state. See :ref`Randomness<randomness>`.
111+
112+
Notes
113+
-----
114+
The nodes are numbered from $0$ to $n - 1$.
115+
116+
The graph is generated by taking $d / 2$ random independent cycles.
117+
118+
Joel Friedman proved that in this model the resulting
119+
graph is an expander with probability
120+
$1 - O(n^{-\tau})$ where $\tau = \lceil (\sqrt{d - 1}) / 2 \rceil - 1$. [1]_
121+
122+
Examples
123+
--------
124+
>>> G = nx.maybe_regular_expander(n=200, d=6, seed=8020)
125+
126+
Returns
127+
-------
128+
G : graph
129+
The constructed undirected graph.
130+
131+
Raises
132+
------
133+
NetworkXError
134+
If $d % 2 != 0$ as the degree must be even.
135+
If $n - 1$ is less than $ 2d $ as the graph is complete at most.
136+
If max_tries is reached
137+
138+
See Also
139+
--------
140+
is_regular_expander
141+
random_regular_expander_graph
142+
143+
References
144+
----------
145+
.. [1] Joel Friedman,
146+
A Proof of Alon's Second Eigenvalue Conjecture and Related Problems, 2004
147+
https://arxiv.org/abs/cs/0405020
148+
149+
"""
150+
151+
# import numpy as np
152+
153+
if n < 1:
154+
raise nx.NetworkXError("n must be a positive integer")
155+
156+
if not (d >= 2):
157+
raise nx.NetworkXError("d must be greater than or equal to 2")
158+
159+
if not (d % 2 == 0):
160+
raise nx.NetworkXError("d must be even")
161+
162+
if not (n - 1 >= d):
163+
raise nx.NetworkXError(
164+
f"Need n-1>= d to have room for {d//2} independent cycles with {n} nodes"
165+
)
166+
167+
G = nx.empty_graph(n, create_using)
168+
169+
if n < 2:
170+
return G
171+
172+
cycles = []
173+
edges = set()
174+
175+
# Create d / 2 cycles
176+
for i in range(d // 2):
177+
iterations = max_tries
178+
# Make sure the cycles are independent to have a regular graph
179+
while len(edges) != (i + 1) * n:
180+
iterations -= 1
181+
# Faster than random.permutation(n) since there are only
182+
# (n-1)! distinct cycles against n! permutations of size n
183+
cycle = seed.permutation(n - 1).tolist()
184+
cycle.append(n - 1)
185+
186+
new_edges = {
187+
(u, v)
188+
for u, v in nx.utils.pairwise(cycle, cyclic=True)
189+
if (u, v) not in edges and (v, u) not in edges
190+
}
191+
# If the new cycle has no edges in common with previous cycles
192+
# then add it to the list otherwise try again
193+
if len(new_edges) == n:
194+
cycles.append(cycle)
195+
edges.update(new_edges)
196+
197+
if iterations == 0:
198+
raise nx.NetworkXError(
199+
"Too many iterations in maybe_regular_expander"
200+
)
201+
202+
G.add_edges_from(edges)
203+
204+
return G
205+
206+
@nx.utils.not_implemented_for("directed")
207+
@nx.utils.not_implemented_for("multigraph")
208+
# @nx._dispatchable(preserve_edge_attrs={"G": {"weight": 1}})
209+
def is_regular_expander(G, *, epsilon=0):
210+
r"""Determines whether the graph G is a regular expander. [1]_
211+
212+
An expander graph is a sparse graph with strong connectivity properties.
213+
214+
More precisely, this helper checks whether the graph is a
215+
regular $(n, d, \lambda)$-expander with $\lambda$ close to
216+
the Alon-Boppana bound and given by
217+
$\lambda = 2 \sqrt{d - 1} + \epsilon$. [2]_
218+
219+
In the case where $\epsilon = 0$ then if the graph successfully passes the test
220+
it is a Ramanujan graph. [3]_
221+
222+
A Ramanujan graph has spectral gap almost as large as possible, which makes them
223+
excellent expanders.
224+
225+
Parameters
226+
----------
227+
G : NetworkX graph
228+
epsilon : int, float, default=0
229+
230+
Returns
231+
-------
232+
bool
233+
Whether the given graph is a regular $(n, d, \lambda)$-expander
234+
where $\lambda = 2 \sqrt{d - 1} + \epsilon$.
235+
236+
Examples
237+
--------
238+
>>> G = nx.random_regular_expander_graph(20, 4)
239+
>>> nx.is_regular_expander(G)
240+
True
241+
242+
See Also
243+
--------
244+
maybe_regular_expander
245+
random_regular_expander_graph
246+
247+
References
248+
----------
249+
.. [1] Expander graph, https://en.wikipedia.org/wiki/Expander_graph
250+
.. [2] Alon-Boppana bound, https://en.wikipedia.org/wiki/Alon%E2%80%93Boppana_bound
251+
.. [3] Ramanujan graphs, https://en.wikipedia.org/wiki/Ramanujan_graph
252+
253+
"""
254+
255+
import numpy as np
256+
from scipy.sparse.linalg import eigsh
257+
258+
if epsilon < 0:
259+
raise nx.NetworkXError("epsilon must be non negative")
260+
261+
if not nx.is_regular(G):
262+
return False
263+
264+
_, d = nx.utils.arbitrary_element(G.degree)
265+
266+
# Catch superfluous warning
267+
with warnings.catch_warnings():
268+
warnings.simplefilter(action="ignore", category=FutureWarning)
269+
270+
A = nx.adjacency_matrix(G, dtype=float)
271+
lams = eigsh(A, which="LM", k=2, return_eigenvectors=False)
272+
273+
# lambda2 is the second biggest eigenvalue
274+
lambda2 = min(lams)
275+
276+
# Use bool() to convert numpy scalar to Python Boolean
277+
return bool(abs(lambda2) < 2 ** np.sqrt(d - 1) + epsilon)
278+
279+
@nx.utils.decorators.np_random_state("seed")
280+
# @nx._dispatchable(graphs=None, returns_graph=True)
281+
def random_regular_expander_graph(
282+
n, d, *, epsilon=0, create_using=None, max_tries=100, seed=None
283+
):
284+
r"""Returns a random regular expander graph on $n$ nodes with degree $d$.
285+
286+
An expander graph is a sparse graph with strong connectivity properties. [1]_
287+
288+
More precisely the returned graph is a $(n, d, \lambda)$-expander with
289+
$\lambda = 2 \sqrt{d - 1} + \epsilon$, close to the Alon-Boppana bound. [2]_
290+
291+
In the case where $\epsilon = 0$ it returns a Ramanujan graph.
292+
A Ramanujan graph has spectral gap almost as large as possible,
293+
which makes them excellent expanders. [3]_
294+
295+
Parameters
296+
----------
297+
n : int
298+
The number of nodes.
299+
d : int
300+
The degree of each node.
301+
epsilon : int, float, default=0
302+
max_tries : int, (default: 100)
303+
The number of allowed loops, also used in the maybe_regular_expander utility
304+
seed : (default: None)
305+
Seed used to set random number generation state. See :ref`Randomness<randomness>`.
306+
307+
Raises
308+
------
309+
NetworkXError
310+
If max_tries is reached
311+
312+
Examples
313+
--------
314+
>>> G = nx.random_regular_expander_graph(20, 4)
315+
>>> nx.is_regular_expander(G)
316+
True
317+
318+
Notes
319+
-----
320+
This loops over `maybe_regular_expander` and can be slow when
321+
$n$ is too big or $\epsilon$ too small.
322+
323+
See Also
324+
--------
325+
maybe_regular_expander
326+
is_regular_expander
327+
328+
References
329+
----------
330+
.. [1] Expander graph, https://en.wikipedia.org/wiki/Expander_graph
331+
.. [2] Alon-Boppana bound, https://en.wikipedia.org/wiki/Alon%E2%80%93Boppana_bound
332+
.. [3] Ramanujan graphs, https://en.wikipedia.org/wiki/Ramanujan_graph
333+
334+
"""
335+
G = maybe_regular_expander(
336+
n, d, create_using=create_using, max_tries=max_tries, seed=seed
337+
)
338+
iterations = max_tries
339+
340+
while not is_regular_expander(G, epsilon=epsilon):
341+
iterations -= 1
342+
G = maybe_regular_expander(
343+
n=n,
344+
d=d,
345+
create_using=create_using,
346+
max_tries=max_tries,
347+
seed=seed,
348+
)
349+
350+
if iterations == 0:
351+
raise nx.NetworkXError(
352+
"Too many iterations in random_regular_expander_graph"
353+
)
354+
355+
return G

0 commit comments

Comments
 (0)