Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/networkx/linalg/algebraicconnectivity.py: 14%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

203 statements  

1""" 

2Algebraic connectivity and Fiedler vectors of undirected graphs. 

3""" 

4 

5import networkx as nx 

6from networkx.utils import ( 

7 not_implemented_for, 

8 np_random_state, 

9 reverse_cuthill_mckee_ordering, 

10) 

11 

12__all__ = [ 

13 "algebraic_connectivity", 

14 "fiedler_vector", 

15 "spectral_ordering", 

16 "spectral_bisection", 

17] 

18 

19 

20class _PCGSolver: 

21 """Preconditioned conjugate gradient method. 

22 

23 To solve Ax = b: 

24 M = A.diagonal() # or some other preconditioner 

25 solver = _PCGSolver(lambda x: A * x, lambda x: M * x) 

26 x = solver.solve(b) 

27 

28 The inputs A and M are functions which compute 

29 matrix multiplication on the argument. 

30 A - multiply by the matrix A in Ax=b 

31 M - multiply by M, the preconditioner surrogate for A 

32 

33 Warning: There is no limit on number of iterations. 

34 """ 

35 

36 def __init__(self, A, M): 

37 self._A = A 

38 self._M = M 

39 

40 def solve(self, B, tol): 

41 import numpy as np 

42 

43 # Densifying step - can this be kept sparse? 

44 B = np.asarray(B) 

45 X = np.ndarray(B.shape, order="F") 

46 for j in range(B.shape[1]): 

47 X[:, j] = self._solve(B[:, j], tol) 

48 return X 

49 

50 def _solve(self, b, tol): 

51 import numpy as np 

52 import scipy as sp 

53 

54 A = self._A 

55 M = self._M 

56 tol *= sp.linalg.blas.dasum(b) 

57 # Initialize. 

58 x = np.zeros(b.shape) 

59 r = b.copy() 

60 z = M(r) 

61 rz = sp.linalg.blas.ddot(r, z) 

62 p = z.copy() 

63 # Iterate. 

64 while True: 

65 Ap = A(p) 

66 alpha = rz / sp.linalg.blas.ddot(p, Ap) 

67 x = sp.linalg.blas.daxpy(p, x, a=alpha) 

68 r = sp.linalg.blas.daxpy(Ap, r, a=-alpha) 

69 if sp.linalg.blas.dasum(r) < tol: 

70 return x 

71 z = M(r) 

72 beta = sp.linalg.blas.ddot(r, z) 

73 beta, rz = beta / rz, beta 

74 p = sp.linalg.blas.daxpy(p, z, a=beta) 

75 

76 

77class _LUSolver: 

78 """LU factorization. 

79 

80 To solve Ax = b: 

81 solver = _LUSolver(A) 

82 x = solver.solve(b) 

83 

84 optional argument `tol` on solve method is ignored but included 

85 to match _PCGsolver API. 

86 """ 

87 

88 def __init__(self, A): 

89 import scipy as sp 

90 

91 self._LU = sp.sparse.linalg.splu( 

92 A, 

93 permc_spec="MMD_AT_PLUS_A", 

94 diag_pivot_thresh=0.0, 

95 options={"Equil": True, "SymmetricMode": True}, 

96 ) 

97 

98 def solve(self, B, tol=None): 

99 import numpy as np 

100 

101 B = np.asarray(B) 

102 X = np.ndarray(B.shape, order="F") 

103 for j in range(B.shape[1]): 

104 X[:, j] = self._LU.solve(B[:, j]) 

105 return X 

106 

107 

108def _preprocess_graph(G, weight): 

109 """Compute edge weights and eliminate zero-weight edges.""" 

110 if G.is_directed(): 

111 H = nx.MultiGraph() 

112 H.add_nodes_from(G) 

113 H.add_weighted_edges_from( 

114 ((u, v, e.get(weight, 1.0)) for u, v, e in G.edges(data=True) if u != v), 

115 weight=weight, 

116 ) 

117 G = H 

118 if not G.is_multigraph(): 

119 edges = ( 

120 (u, v, abs(e.get(weight, 1.0))) for u, v, e in G.edges(data=True) if u != v 

121 ) 

122 else: 

123 edges = ( 

124 (u, v, sum(abs(e.get(weight, 1.0)) for e in G[u][v].values())) 

125 for u, v in G.edges() 

126 if u != v 

127 ) 

128 H = nx.Graph() 

129 H.add_nodes_from(G) 

130 H.add_weighted_edges_from((u, v, e) for u, v, e in edges if e != 0) 

131 return H 

132 

133 

134def _rcm_estimate(G, nodelist): 

135 """Estimate the Fiedler vector using the reverse Cuthill-McKee ordering.""" 

136 import numpy as np 

137 

138 G = G.subgraph(nodelist) 

139 order = reverse_cuthill_mckee_ordering(G) 

140 n = len(nodelist) 

141 index = dict(zip(nodelist, range(n))) 

142 x = np.ndarray(n, dtype=float) 

143 for i, u in enumerate(order): 

144 x[index[u]] = i 

145 x -= (n - 1) / 2.0 

146 return x 

147 

148 

149def _tracemin_fiedler(L, X, normalized, tol, method): 

150 """Compute the Fiedler vector of L using the TraceMIN-Fiedler algorithm. 

151 

152 The Fiedler vector of a connected undirected graph is the eigenvector 

153 corresponding to the second smallest eigenvalue of the Laplacian matrix 

154 of the graph. This function starts with the Laplacian L, not the Graph. 

155 

156 Parameters 

157 ---------- 

158 L : Laplacian of a possibly weighted or normalized, but undirected graph 

159 

160 X : Initial guess for a solution. Usually a matrix of random numbers. 

161 This function allows more than one column in X to identify more than 

162 one eigenvector if desired. 

163 

164 normalized : bool 

165 Whether the normalized Laplacian matrix is used. 

166 

167 tol : float 

168 Tolerance of relative residual in eigenvalue computation. 

169 Warning: There is no limit on number of iterations. 

170 

171 method : string 

172 Should be 'tracemin_pcg' or 'tracemin_lu'. 

173 Otherwise exception is raised. 

174 

175 Returns 

176 ------- 

177 sigma, X : Two NumPy arrays of floats. 

178 The lowest eigenvalues and corresponding eigenvectors of L. 

179 The size of input X determines the size of these outputs. 

180 As this is for Fiedler vectors, the zero eigenvalue (and 

181 constant eigenvector) are avoided. 

182 """ 

183 import numpy as np 

184 import scipy as sp 

185 

186 n = X.shape[0] 

187 

188 if normalized: 

189 # Form the normalized Laplacian matrix and determine the eigenvector of 

190 # its nullspace. 

191 e = np.sqrt(L.diagonal()) 

192 D = sp.sparse.dia_array((1 / e, 0), shape=(n, n)).tocsr() 

193 L = D @ L @ D 

194 e *= 1.0 / np.linalg.norm(e, 2) 

195 

196 if normalized: 

197 

198 def project(X): 

199 """Make X orthogonal to the nullspace of L.""" 

200 X = np.asarray(X) 

201 for j in range(X.shape[1]): 

202 X[:, j] -= (X[:, j] @ e) * e 

203 

204 else: 

205 

206 def project(X): 

207 """Make X orthogonal to the nullspace of L.""" 

208 X = np.asarray(X) 

209 for j in range(X.shape[1]): 

210 X[:, j] -= X[:, j].sum() / n 

211 

212 if method == "tracemin_pcg": 

213 D = L.diagonal().astype(float) 

214 solver = _PCGSolver(lambda x: L @ x, lambda x: D * x) 

215 elif method == "tracemin_lu": 

216 # Convert A to CSC to suppress SparseEfficiencyWarning. 

217 A = sp.sparse.csc_array(L, dtype=float, copy=True) 

218 # Force A to be nonsingular. Since A is the Laplacian matrix of a 

219 # connected graph, its rank deficiency is one, and thus one diagonal 

220 # element needs to modified. Changing to infinity forces a zero in the 

221 # corresponding element in the solution. 

222 i = (A.indptr[1:] - A.indptr[:-1]).argmax() 

223 A[i, i] = np.inf 

224 solver = _LUSolver(A) 

225 else: 

226 raise nx.NetworkXError(f"Unknown linear system solver: {method}") 

227 

228 # Initialize. 

229 Lnorm = abs(L).sum(axis=1).flatten().max() 

230 project(X) 

231 W = np.ndarray(X.shape, order="F") 

232 

233 while True: 

234 # Orthonormalize X. 

235 X = np.linalg.qr(X)[0] 

236 # Compute iteration matrix H. 

237 W[:, :] = L @ X 

238 H = X.T @ W 

239 sigma, Y = sp.linalg.eigh(H, overwrite_a=True) 

240 # Compute the Ritz vectors. 

241 X = X @ Y 

242 # Test for convergence exploiting the fact that L * X == W * Y. 

243 res = sp.linalg.blas.dasum(W @ Y[:, 0] - sigma[0] * X[:, 0]) / Lnorm 

244 if res < tol: 

245 break 

246 # Compute X = L \ X / (X' * (L \ X)). 

247 # L \ X can have an arbitrary projection on the nullspace of L, 

248 # which will be eliminated. 

249 W[:, :] = solver.solve(X, tol) 

250 X = (sp.linalg.inv(W.T @ X) @ W.T).T # Preserves Fortran storage order. 

251 project(X) 

252 

253 return sigma, np.asarray(X) 

254 

255 

256def _get_fiedler_func(method): 

257 """Returns a function that solves the Fiedler eigenvalue problem.""" 

258 import numpy as np 

259 

260 if method == "tracemin": # old style keyword <v2.1 

261 method = "tracemin_pcg" 

262 if method in ("tracemin_pcg", "tracemin_lu"): 

263 

264 def find_fiedler(L, x, normalized, tol, seed): 

265 q = 1 if method == "tracemin_pcg" else min(4, L.shape[0] - 1) 

266 X = np.asarray(seed.normal(size=(q, L.shape[0]))).T 

267 sigma, X = _tracemin_fiedler(L, X, normalized, tol, method) 

268 return sigma[0], X[:, 0] 

269 

270 elif method == "lanczos" or method == "lobpcg": 

271 

272 def find_fiedler(L, x, normalized, tol, seed): 

273 import scipy as sp 

274 

275 L = sp.sparse.csc_array(L, dtype=float) 

276 n = L.shape[0] 

277 if normalized: 

278 D = sp.sparse.dia_array( 

279 (1.0 / np.sqrt(L.diagonal()), 0), shape=(n, n) 

280 ).tocsc() 

281 L = D @ L @ D 

282 if method == "lanczos" or n < 10: 

283 # Avoid LOBPCG when n < 10 due to 

284 # https://github.com/scipy/scipy/issues/3592 

285 # https://github.com/scipy/scipy/pull/3594 

286 sigma, X = sp.sparse.linalg.eigsh( 

287 L, 2, which="SM", tol=tol, return_eigenvectors=True 

288 ) 

289 return sigma[1], X[:, 1] 

290 else: 

291 X = np.asarray(np.atleast_2d(x).T) 

292 M = sp.sparse.dia_array((1.0 / L.diagonal(), 0), shape=(n, n)).tocsr() 

293 Y = np.ones(n) 

294 if normalized: 

295 Y /= D.diagonal() 

296 sigma, X = sp.sparse.linalg.lobpcg( 

297 L, X, M=M, Y=np.atleast_2d(Y).T, tol=tol, maxiter=n, largest=False 

298 ) 

299 return sigma[0], X[:, 0] 

300 

301 else: 

302 raise nx.NetworkXError(f"unknown method {method!r}.") 

303 

304 return find_fiedler 

305 

306 

307@not_implemented_for("directed") 

308@np_random_state(5) 

309@nx._dispatchable(edge_attrs="weight") 

310def algebraic_connectivity( 

311 G, weight="weight", normalized=False, tol=1e-8, method="tracemin_pcg", seed=None 

312): 

313 r"""Returns the algebraic connectivity of an undirected graph. 

314 

315 The algebraic connectivity of a connected undirected graph is the second 

316 smallest eigenvalue of its Laplacian matrix. 

317 

318 Parameters 

319 ---------- 

320 G : NetworkX graph 

321 An undirected graph. 

322 

323 weight : object, optional (default: None) 

324 The data key used to determine the weight of each edge. If None, then 

325 each edge has unit weight. 

326 

327 normalized : bool, optional (default: False) 

328 Whether the normalized Laplacian matrix is used. 

329 

330 tol : float, optional (default: 1e-8) 

331 Tolerance of relative residual in eigenvalue computation. 

332 

333 method : string, optional (default: 'tracemin_pcg') 

334 Method of eigenvalue computation. It must be one of the tracemin 

335 options shown below (TraceMIN), 'lanczos' (Lanczos iteration) 

336 or 'lobpcg' (LOBPCG). 

337 

338 The TraceMIN algorithm uses a linear system solver. The following 

339 values allow specifying the solver to be used. 

340 

341 =============== ======================================== 

342 Value Solver 

343 =============== ======================================== 

344 'tracemin_pcg' Preconditioned conjugate gradient method 

345 'tracemin_lu' LU factorization 

346 =============== ======================================== 

347 

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

349 Indicator of random number generation state. 

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

351 

352 Returns 

353 ------- 

354 algebraic_connectivity : float 

355 Algebraic connectivity. 

356 

357 Raises 

358 ------ 

359 NetworkXNotImplemented 

360 If G is directed. 

361 

362 NetworkXError 

363 If G has less than two nodes. 

364 

365 Notes 

366 ----- 

367 Edge weights are interpreted by their absolute values. For MultiGraph's, 

368 weights of parallel edges are summed. Zero-weighted edges are ignored. 

369 

370 See Also 

371 -------- 

372 laplacian_matrix 

373 

374 Examples 

375 -------- 

376 For undirected graphs algebraic connectivity can tell us if a graph is connected or not 

377 `G` is connected iff ``algebraic_connectivity(G) > 0``: 

378 

379 >>> G = nx.complete_graph(5) 

380 >>> nx.algebraic_connectivity(G) > 0 

381 True 

382 >>> G.add_node(10) # G is no longer connected 

383 >>> nx.algebraic_connectivity(G) > 0 

384 False 

385 

386 """ 

387 if len(G) < 2: 

388 raise nx.NetworkXError("graph has less than two nodes.") 

389 G = _preprocess_graph(G, weight) 

390 if not nx.is_connected(G): 

391 return 0.0 

392 

393 L = nx.laplacian_matrix(G) 

394 if L.shape[0] == 2: 

395 return 2.0 * float(L[0, 0]) if not normalized else 2.0 

396 

397 find_fiedler = _get_fiedler_func(method) 

398 x = None if method != "lobpcg" else _rcm_estimate(G, G) 

399 sigma, fiedler = find_fiedler(L, x, normalized, tol, seed) 

400 return float(sigma) 

401 

402 

403@not_implemented_for("directed") 

404@np_random_state(5) 

405@nx._dispatchable(edge_attrs="weight") 

406def fiedler_vector( 

407 G, weight="weight", normalized=False, tol=1e-8, method="tracemin_pcg", seed=None 

408): 

409 """Returns the Fiedler vector of a connected undirected graph. 

410 

411 The Fiedler vector of a connected undirected graph is the eigenvector 

412 corresponding to the second smallest eigenvalue of the Laplacian matrix 

413 of the graph. 

414 

415 Parameters 

416 ---------- 

417 G : NetworkX graph 

418 An undirected graph. 

419 

420 weight : object, optional (default: None) 

421 The data key used to determine the weight of each edge. If None, then 

422 each edge has unit weight. 

423 

424 normalized : bool, optional (default: False) 

425 Whether the normalized Laplacian matrix is used. 

426 

427 tol : float, optional (default: 1e-8) 

428 Tolerance of relative residual in eigenvalue computation. 

429 

430 method : string, optional (default: 'tracemin_pcg') 

431 Method of eigenvalue computation. It must be one of the tracemin 

432 options shown below (TraceMIN), 'lanczos' (Lanczos iteration) 

433 or 'lobpcg' (LOBPCG). 

434 

435 The TraceMIN algorithm uses a linear system solver. The following 

436 values allow specifying the solver to be used. 

437 

438 =============== ======================================== 

439 Value Solver 

440 =============== ======================================== 

441 'tracemin_pcg' Preconditioned conjugate gradient method 

442 'tracemin_lu' LU factorization 

443 =============== ======================================== 

444 

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

446 Indicator of random number generation state. 

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

448 

449 Returns 

450 ------- 

451 fiedler_vector : NumPy array of floats. 

452 Fiedler vector. 

453 

454 Raises 

455 ------ 

456 NetworkXNotImplemented 

457 If G is directed. 

458 

459 NetworkXError 

460 If G has less than two nodes or is not connected. 

461 

462 Notes 

463 ----- 

464 Edge weights are interpreted by their absolute values. For MultiGraph's, 

465 weights of parallel edges are summed. Zero-weighted edges are ignored. 

466 

467 See Also 

468 -------- 

469 laplacian_matrix 

470 

471 Examples 

472 -------- 

473 Given a connected graph the signs of the values in the Fiedler vector can be 

474 used to partition the graph into two components. 

475 

476 >>> G = nx.barbell_graph(5, 0) 

477 >>> nx.fiedler_vector(G, normalized=True, seed=1) 

478 array([-0.32864129, -0.32864129, -0.32864129, -0.32864129, -0.26072899, 

479 0.26072899, 0.32864129, 0.32864129, 0.32864129, 0.32864129]) 

480 

481 The connected components are the two 5-node cliques of the barbell graph. 

482 """ 

483 import numpy as np 

484 

485 if len(G) < 2: 

486 raise nx.NetworkXError("graph has less than two nodes.") 

487 G = _preprocess_graph(G, weight) 

488 if not nx.is_connected(G): 

489 raise nx.NetworkXError("graph is not connected.") 

490 

491 if len(G) == 2: 

492 return np.array([1.0, -1.0]) 

493 

494 find_fiedler = _get_fiedler_func(method) 

495 L = nx.laplacian_matrix(G) 

496 x = None if method != "lobpcg" else _rcm_estimate(G, G) 

497 sigma, fiedler = find_fiedler(L, x, normalized, tol, seed) 

498 return fiedler 

499 

500 

501@np_random_state(5) 

502@nx._dispatchable(edge_attrs="weight") 

503def spectral_ordering( 

504 G, weight="weight", normalized=False, tol=1e-8, method="tracemin_pcg", seed=None 

505): 

506 """Compute the spectral_ordering of a graph. 

507 

508 The spectral ordering of a graph is an ordering of its nodes where nodes 

509 in the same weakly connected components appear contiguous and ordered by 

510 their corresponding elements in the Fiedler vector of the component. 

511 

512 Parameters 

513 ---------- 

514 G : NetworkX graph 

515 A graph. 

516 

517 weight : object, optional (default: None) 

518 The data key used to determine the weight of each edge. If None, then 

519 each edge has unit weight. 

520 

521 normalized : bool, optional (default: False) 

522 Whether the normalized Laplacian matrix is used. 

523 

524 tol : float, optional (default: 1e-8) 

525 Tolerance of relative residual in eigenvalue computation. 

526 

527 method : string, optional (default: 'tracemin_pcg') 

528 Method of eigenvalue computation. It must be one of the tracemin 

529 options shown below (TraceMIN), 'lanczos' (Lanczos iteration) 

530 or 'lobpcg' (LOBPCG). 

531 

532 The TraceMIN algorithm uses a linear system solver. The following 

533 values allow specifying the solver to be used. 

534 

535 =============== ======================================== 

536 Value Solver 

537 =============== ======================================== 

538 'tracemin_pcg' Preconditioned conjugate gradient method 

539 'tracemin_lu' LU factorization 

540 =============== ======================================== 

541 

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

543 Indicator of random number generation state. 

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

545 

546 Returns 

547 ------- 

548 spectral_ordering : NumPy array of floats. 

549 Spectral ordering of nodes. 

550 

551 Raises 

552 ------ 

553 NetworkXError 

554 If G is empty. 

555 

556 Notes 

557 ----- 

558 Edge weights are interpreted by their absolute values. For MultiGraph's, 

559 weights of parallel edges are summed. Zero-weighted edges are ignored. 

560 

561 See Also 

562 -------- 

563 laplacian_matrix 

564 """ 

565 if len(G) == 0: 

566 raise nx.NetworkXError("graph is empty.") 

567 G = _preprocess_graph(G, weight) 

568 

569 find_fiedler = _get_fiedler_func(method) 

570 order = [] 

571 for component in nx.connected_components(G): 

572 size = len(component) 

573 if size > 2: 

574 L = nx.laplacian_matrix(G, component) 

575 x = None if method != "lobpcg" else _rcm_estimate(G, component) 

576 sigma, fiedler = find_fiedler(L, x, normalized, tol, seed) 

577 sort_info = zip(fiedler, range(size), component) 

578 order.extend(u for x, c, u in sorted(sort_info)) 

579 else: 

580 order.extend(component) 

581 

582 return order 

583 

584 

585@nx._dispatchable(edge_attrs="weight") 

586def spectral_bisection( 

587 G, weight="weight", normalized=False, tol=1e-8, method="tracemin_pcg", seed=None 

588): 

589 """Bisect the graph using the Fiedler vector. 

590 

591 This method uses the Fiedler vector to bisect a graph. 

592 The partition is defined by the nodes which are associated with 

593 either positive or negative values in the vector. 

594 

595 Parameters 

596 ---------- 

597 G : NetworkX Graph 

598 

599 weight : str, optional (default: weight) 

600 The data key used to determine the weight of each edge. If None, then 

601 each edge has unit weight. 

602 

603 normalized : bool, optional (default: False) 

604 Whether the normalized Laplacian matrix is used. 

605 

606 tol : float, optional (default: 1e-8) 

607 Tolerance of relative residual in eigenvalue computation. 

608 

609 method : string, optional (default: 'tracemin_pcg') 

610 Method of eigenvalue computation. It must be one of the tracemin 

611 options shown below (TraceMIN), 'lanczos' (Lanczos iteration) 

612 or 'lobpcg' (LOBPCG). 

613 

614 The TraceMIN algorithm uses a linear system solver. The following 

615 values allow specifying the solver to be used. 

616 

617 =============== ======================================== 

618 Value Solver 

619 =============== ======================================== 

620 'tracemin_pcg' Preconditioned conjugate gradient method 

621 'tracemin_lu' LU factorization 

622 =============== ======================================== 

623 

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

625 Indicator of random number generation state. 

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

627 

628 Returns 

629 ------- 

630 bisection : tuple of sets 

631 Sets with the bisection of nodes 

632 

633 Examples 

634 -------- 

635 >>> G = nx.barbell_graph(3, 0) 

636 >>> nx.spectral_bisection(G) 

637 ({0, 1, 2}, {3, 4, 5}) 

638 

639 References 

640 ---------- 

641 .. [1] M. E. J Newman 'Networks: An Introduction', pages 364-370 

642 Oxford University Press 2011. 

643 """ 

644 import numpy as np 

645 

646 v = nx.fiedler_vector(G, weight, normalized, tol, method, seed) 

647 nodes = np.array(list(G)) 

648 pos_vals = v >= 0 

649 

650 return set(nodes[~pos_vals].tolist()), set(nodes[pos_vals].tolist())