1"""Generates graphs with a given eigenvector structure"""
2
3import networkx as nx
4from networkx.utils import np_random_state
5
6__all__ = ["spectral_graph_forge"]
7
8
9@np_random_state(3)
10@nx._dispatchable(preserve_edge_attrs={"G": {"weight": 1}}, returns_graph=True)
11def spectral_graph_forge(G, alpha, transformation="identity", seed=None):
12 """Returns a random simple graph with spectrum resembling that of `G`
13
14 This algorithm, called Spectral Graph Forge (SGF), computes the
15 eigenvectors of a given graph adjacency matrix, filters them and
16 builds a random graph with a similar eigenstructure.
17 SGF has been proved to be particularly useful for synthesizing
18 realistic social networks and it can also be used to anonymize
19 graph sensitive data.
20
21 Parameters
22 ----------
23 G : Graph
24 alpha : float
25 Ratio representing the percentage of eigenvectors of G to consider,
26 values in [0,1].
27 transformation : string, optional
28 Represents the intended matrix linear transformation, possible values
29 are 'identity' and 'modularity'
30 seed : integer, random_state, or None (default)
31 Indicator of numpy random number generation state.
32 See :ref:`Randomness<randomness>`.
33
34 Returns
35 -------
36 H : Graph
37 A graph with a similar eigenvector structure of the input one.
38
39 Raises
40 ------
41 NetworkXError
42 If transformation has a value different from 'identity' or 'modularity'
43
44 Notes
45 -----
46 Spectral Graph Forge (SGF) generates a random simple graph resembling the
47 global properties of the given one.
48 It leverages the low-rank approximation of the associated adjacency matrix
49 driven by the *alpha* precision parameter.
50 SGF preserves the number of nodes of the input graph and their ordering.
51 This way, nodes of output graphs resemble the properties of the input one
52 and attributes can be directly mapped.
53
54 It considers the graph adjacency matrices which can optionally be
55 transformed to other symmetric real matrices (currently transformation
56 options include *identity* and *modularity*).
57 The *modularity* transformation, in the sense of Newman's modularity matrix
58 allows the focusing on community structure related properties of the graph.
59
60 SGF applies a low-rank approximation whose fixed rank is computed from the
61 ratio *alpha* of the input graph adjacency matrix dimension.
62 This step performs a filtering on the input eigenvectors similar to the low
63 pass filtering common in telecommunications.
64
65 The filtered values (after truncation) are used as input to a Bernoulli
66 sampling for constructing a random adjacency matrix.
67
68 References
69 ----------
70 .. [1] L. Baldesi, C. T. Butts, A. Markopoulou, "Spectral Graph Forge:
71 Graph Generation Targeting Modularity", IEEE Infocom, '18.
72 https://arxiv.org/abs/1801.01715
73 .. [2] M. Newman, "Networks: an introduction", Oxford university press,
74 2010
75
76 Examples
77 --------
78 >>> G = nx.karate_club_graph()
79 >>> H = nx.spectral_graph_forge(G, 0.3)
80 >>>
81 """
82 import numpy as np
83 import scipy as sp
84
85 available_transformations = ["identity", "modularity"]
86 alpha = np.clip(alpha, 0, 1)
87 A = nx.to_numpy_array(G)
88 n = A.shape[1]
89 level = round(n * alpha)
90
91 if transformation not in available_transformations:
92 msg = f"{transformation!r} is not a valid transformation. "
93 msg += f"Transformations: {available_transformations}"
94 raise nx.NetworkXError(msg)
95
96 K = np.ones((1, n)) @ A
97
98 B = A
99 if transformation == "modularity":
100 B -= K.T @ K / K.sum()
101
102 # Compute low-rank approximation of B
103 evals, evecs = np.linalg.eigh(B)
104 k = np.argsort(np.abs(evals))[::-1] # indices of evals in descending order
105 evecs[:, k[np.arange(level, n)]] = 0 # set smallest eigenvectors to 0
106 B = evecs @ np.diag(evals) @ evecs.T
107
108 if transformation == "modularity":
109 B += K.T @ K / K.sum()
110
111 B = np.clip(B, 0, 1)
112 np.fill_diagonal(B, 0)
113
114 for i in range(n - 1):
115 B[i, i + 1 :] = sp.stats.bernoulli.rvs(B[i, i + 1 :], random_state=seed)
116 B[i + 1 :, i] = np.transpose(B[i, i + 1 :])
117
118 H = nx.from_numpy_array(B)
119
120 return H