|
| 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