Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.9/dist-packages/networkx/algorithms/sparsifiers.py: 13%

94 statements  

« prev     ^ index     » next       coverage.py v7.3.2, created at 2023-10-20 07:00 +0000

1"""Functions for computing sparsifiers of graphs.""" 

2import math 

3 

4import networkx as nx 

5from networkx.utils import not_implemented_for, py_random_state 

6 

7__all__ = ["spanner"] 

8 

9 

10@not_implemented_for("directed") 

11@not_implemented_for("multigraph") 

12@py_random_state(3) 

13@nx._dispatch(edge_attrs="weight") 

14def spanner(G, stretch, weight=None, seed=None): 

15 """Returns a spanner of the given graph with the given stretch. 

16 

17 A spanner of a graph G = (V, E) with stretch t is a subgraph 

18 H = (V, E_S) such that E_S is a subset of E and the distance between 

19 any pair of nodes in H is at most t times the distance between the 

20 nodes in G. 

21 

22 Parameters 

23 ---------- 

24 G : NetworkX graph 

25 An undirected simple graph. 

26 

27 stretch : float 

28 The stretch of the spanner. 

29 

30 weight : object 

31 The edge attribute to use as distance. 

32 

33 seed : integer, random_state, or None (default) 

34 Indicator of random number generation state. 

35 See :ref:`Randomness<randomness>`. 

36 

37 Returns 

38 ------- 

39 NetworkX graph 

40 A spanner of the given graph with the given stretch. 

41 

42 Raises 

43 ------ 

44 ValueError 

45 If a stretch less than 1 is given. 

46 

47 Notes 

48 ----- 

49 This function implements the spanner algorithm by Baswana and Sen, 

50 see [1]. 

51 

52 This algorithm is a randomized las vegas algorithm: The expected 

53 running time is O(km) where k = (stretch + 1) // 2 and m is the 

54 number of edges in G. The returned graph is always a spanner of the 

55 given graph with the specified stretch. For weighted graphs the 

56 number of edges in the spanner is O(k * n^(1 + 1 / k)) where k is 

57 defined as above and n is the number of nodes in G. For unweighted 

58 graphs the number of edges is O(n^(1 + 1 / k) + kn). 

59 

60 References 

61 ---------- 

62 [1] S. Baswana, S. Sen. A Simple and Linear Time Randomized 

63 Algorithm for Computing Sparse Spanners in Weighted Graphs. 

64 Random Struct. Algorithms 30(4): 532-563 (2007). 

65 """ 

66 if stretch < 1: 

67 raise ValueError("stretch must be at least 1") 

68 

69 k = (stretch + 1) // 2 

70 

71 # initialize spanner H with empty edge set 

72 H = nx.empty_graph() 

73 H.add_nodes_from(G.nodes) 

74 

75 # phase 1: forming the clusters 

76 # the residual graph has V' from the paper as its node set 

77 # and E' from the paper as its edge set 

78 residual_graph = _setup_residual_graph(G, weight) 

79 # clustering is a dictionary that maps nodes in a cluster to the 

80 # cluster center 

81 clustering = {v: v for v in G.nodes} 

82 sample_prob = math.pow(G.number_of_nodes(), -1 / k) 

83 size_limit = 2 * math.pow(G.number_of_nodes(), 1 + 1 / k) 

84 

85 i = 0 

86 while i < k - 1: 

87 # step 1: sample centers 

88 sampled_centers = set() 

89 for center in set(clustering.values()): 

90 if seed.random() < sample_prob: 

91 sampled_centers.add(center) 

92 

93 # combined loop for steps 2 and 3 

94 edges_to_add = set() 

95 edges_to_remove = set() 

96 new_clustering = {} 

97 for v in residual_graph.nodes: 

98 if clustering[v] in sampled_centers: 

99 continue 

100 

101 # step 2: find neighboring (sampled) clusters and 

102 # lightest edges to them 

103 lightest_edge_neighbor, lightest_edge_weight = _lightest_edge_dicts( 

104 residual_graph, clustering, v 

105 ) 

106 neighboring_sampled_centers = ( 

107 set(lightest_edge_weight.keys()) & sampled_centers 

108 ) 

109 

110 # step 3: add edges to spanner 

111 if not neighboring_sampled_centers: 

112 # connect to each neighboring center via lightest edge 

113 for neighbor in lightest_edge_neighbor.values(): 

114 edges_to_add.add((v, neighbor)) 

115 # remove all incident edges 

116 for neighbor in residual_graph.adj[v]: 

117 edges_to_remove.add((v, neighbor)) 

118 

119 else: # there is a neighboring sampled center 

120 closest_center = min( 

121 neighboring_sampled_centers, key=lightest_edge_weight.get 

122 ) 

123 closest_center_weight = lightest_edge_weight[closest_center] 

124 closest_center_neighbor = lightest_edge_neighbor[closest_center] 

125 

126 edges_to_add.add((v, closest_center_neighbor)) 

127 new_clustering[v] = closest_center 

128 

129 # connect to centers with edge weight less than 

130 # closest_center_weight 

131 for center, edge_weight in lightest_edge_weight.items(): 

132 if edge_weight < closest_center_weight: 

133 neighbor = lightest_edge_neighbor[center] 

134 edges_to_add.add((v, neighbor)) 

135 

136 # remove edges to centers with edge weight less than 

137 # closest_center_weight 

138 for neighbor in residual_graph.adj[v]: 

139 neighbor_cluster = clustering[neighbor] 

140 neighbor_weight = lightest_edge_weight[neighbor_cluster] 

141 if ( 

142 neighbor_cluster == closest_center 

143 or neighbor_weight < closest_center_weight 

144 ): 

145 edges_to_remove.add((v, neighbor)) 

146 

147 # check whether iteration added too many edges to spanner, 

148 # if so repeat 

149 if len(edges_to_add) > size_limit: 

150 # an iteration is repeated O(1) times on expectation 

151 continue 

152 

153 # iteration succeeded 

154 i = i + 1 

155 

156 # actually add edges to spanner 

157 for u, v in edges_to_add: 

158 _add_edge_to_spanner(H, residual_graph, u, v, weight) 

159 

160 # actually delete edges from residual graph 

161 residual_graph.remove_edges_from(edges_to_remove) 

162 

163 # copy old clustering data to new_clustering 

164 for node, center in clustering.items(): 

165 if center in sampled_centers: 

166 new_clustering[node] = center 

167 clustering = new_clustering 

168 

169 # step 4: remove intra-cluster edges 

170 for u in residual_graph.nodes: 

171 for v in list(residual_graph.adj[u]): 

172 if clustering[u] == clustering[v]: 

173 residual_graph.remove_edge(u, v) 

174 

175 # update residual graph node set 

176 for v in list(residual_graph.nodes): 

177 if v not in clustering: 

178 residual_graph.remove_node(v) 

179 

180 # phase 2: vertex-cluster joining 

181 for v in residual_graph.nodes: 

182 lightest_edge_neighbor, _ = _lightest_edge_dicts(residual_graph, clustering, v) 

183 for neighbor in lightest_edge_neighbor.values(): 

184 _add_edge_to_spanner(H, residual_graph, v, neighbor, weight) 

185 

186 return H 

187 

188 

189def _setup_residual_graph(G, weight): 

190 """Setup residual graph as a copy of G with unique edges weights. 

191 

192 The node set of the residual graph corresponds to the set V' from 

193 the Baswana-Sen paper and the edge set corresponds to the set E' 

194 from the paper. 

195 

196 This function associates distinct weights to the edges of the 

197 residual graph (even for unweighted input graphs), as required by 

198 the algorithm. 

199 

200 Parameters 

201 ---------- 

202 G : NetworkX graph 

203 An undirected simple graph. 

204 

205 weight : object 

206 The edge attribute to use as distance. 

207 

208 Returns 

209 ------- 

210 NetworkX graph 

211 The residual graph used for the Baswana-Sen algorithm. 

212 """ 

213 residual_graph = G.copy() 

214 

215 # establish unique edge weights, even for unweighted graphs 

216 for u, v in G.edges(): 

217 if not weight: 

218 residual_graph[u][v]["weight"] = (id(u), id(v)) 

219 else: 

220 residual_graph[u][v]["weight"] = (G[u][v][weight], id(u), id(v)) 

221 

222 return residual_graph 

223 

224 

225def _lightest_edge_dicts(residual_graph, clustering, node): 

226 """Find the lightest edge to each cluster. 

227 

228 Searches for the minimum-weight edge to each cluster adjacent to 

229 the given node. 

230 

231 Parameters 

232 ---------- 

233 residual_graph : NetworkX graph 

234 The residual graph used by the Baswana-Sen algorithm. 

235 

236 clustering : dictionary 

237 The current clustering of the nodes. 

238 

239 node : node 

240 The node from which the search originates. 

241 

242 Returns 

243 ------- 

244 lightest_edge_neighbor, lightest_edge_weight : dictionary, dictionary 

245 lightest_edge_neighbor is a dictionary that maps a center C to 

246 a node v in the corresponding cluster such that the edge from 

247 the given node to v is the lightest edge from the given node to 

248 any node in cluster. lightest_edge_weight maps a center C to the 

249 weight of the aforementioned edge. 

250 

251 Notes 

252 ----- 

253 If a cluster has no node that is adjacent to the given node in the 

254 residual graph then the center of the cluster is not a key in the 

255 returned dictionaries. 

256 """ 

257 lightest_edge_neighbor = {} 

258 lightest_edge_weight = {} 

259 for neighbor in residual_graph.adj[node]: 

260 neighbor_center = clustering[neighbor] 

261 weight = residual_graph[node][neighbor]["weight"] 

262 if ( 

263 neighbor_center not in lightest_edge_weight 

264 or weight < lightest_edge_weight[neighbor_center] 

265 ): 

266 lightest_edge_neighbor[neighbor_center] = neighbor 

267 lightest_edge_weight[neighbor_center] = weight 

268 return lightest_edge_neighbor, lightest_edge_weight 

269 

270 

271def _add_edge_to_spanner(H, residual_graph, u, v, weight): 

272 """Add the edge {u, v} to the spanner H and take weight from 

273 the residual graph. 

274 

275 Parameters 

276 ---------- 

277 H : NetworkX graph 

278 The spanner under construction. 

279 

280 residual_graph : NetworkX graph 

281 The residual graph used by the Baswana-Sen algorithm. The weight 

282 for the edge is taken from this graph. 

283 

284 u : node 

285 One endpoint of the edge. 

286 

287 v : node 

288 The other endpoint of the edge. 

289 

290 weight : object 

291 The edge attribute to use as distance. 

292 """ 

293 H.add_edge(u, v) 

294 if weight: 

295 H[u][v][weight] = residual_graph[u][v]["weight"][0]