Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/networkx/algorithms/connectivity/edge_augmentation.py: 16%

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

318 statements  

1""" 

2Algorithms for finding k-edge-augmentations 

3 

4A k-edge-augmentation is a set of edges, that once added to a graph, ensures 

5that the graph is k-edge-connected; i.e. the graph cannot be disconnected 

6unless k or more edges are removed. Typically, the goal is to find the 

7augmentation with minimum weight. In general, it is not guaranteed that a 

8k-edge-augmentation exists. 

9 

10See Also 

11-------- 

12:mod:`edge_kcomponents` : algorithms for finding k-edge-connected components 

13:mod:`connectivity` : algorithms for determining edge connectivity. 

14""" 

15 

16import itertools as it 

17import math 

18from collections import defaultdict, namedtuple 

19 

20import networkx as nx 

21from networkx.utils import not_implemented_for, py_random_state 

22 

23__all__ = ["k_edge_augmentation", "is_k_edge_connected", "is_locally_k_edge_connected"] 

24 

25 

26@not_implemented_for("directed") 

27@not_implemented_for("multigraph") 

28@nx._dispatchable 

29def is_k_edge_connected(G, k): 

30 """Tests to see if a graph is k-edge-connected. 

31 

32 Is it impossible to disconnect the graph by removing fewer than k edges? 

33 If so, then G is k-edge-connected. 

34 

35 Parameters 

36 ---------- 

37 G : NetworkX graph 

38 An undirected graph. 

39 

40 k : integer 

41 edge connectivity to test for 

42 

43 Returns 

44 ------- 

45 boolean 

46 True if G is k-edge-connected. 

47 

48 See Also 

49 -------- 

50 :func:`is_locally_k_edge_connected` 

51 

52 Examples 

53 -------- 

54 >>> G = nx.barbell_graph(10, 0) 

55 >>> nx.is_k_edge_connected(G, k=1) 

56 True 

57 >>> nx.is_k_edge_connected(G, k=2) 

58 False 

59 """ 

60 if k < 1: 

61 raise ValueError(f"k must be positive, not {k}") 

62 # First try to quickly determine if G is not k-edge-connected 

63 if G.number_of_nodes() < k + 1: 

64 return False 

65 elif any(d < k for n, d in G.degree()): 

66 return False 

67 else: 

68 # Otherwise perform the full check 

69 if k == 1: 

70 return nx.is_connected(G) 

71 elif k == 2: 

72 return nx.is_connected(G) and not nx.has_bridges(G) 

73 else: 

74 return nx.edge_connectivity(G, cutoff=k) >= k 

75 

76 

77@not_implemented_for("directed") 

78@not_implemented_for("multigraph") 

79@nx._dispatchable 

80def is_locally_k_edge_connected(G, s, t, k): 

81 """Tests to see if an edge in a graph is locally k-edge-connected. 

82 

83 Is it impossible to disconnect s and t by removing fewer than k edges? 

84 If so, then s and t are locally k-edge-connected in G. 

85 

86 Parameters 

87 ---------- 

88 G : NetworkX graph 

89 An undirected graph. 

90 

91 s : node 

92 Source node 

93 

94 t : node 

95 Target node 

96 

97 k : integer 

98 local edge connectivity for nodes s and t 

99 

100 Returns 

101 ------- 

102 boolean 

103 True if s and t are locally k-edge-connected in G. 

104 

105 See Also 

106 -------- 

107 :func:`is_k_edge_connected` 

108 

109 Examples 

110 -------- 

111 >>> from networkx.algorithms.connectivity import is_locally_k_edge_connected 

112 >>> G = nx.barbell_graph(10, 0) 

113 >>> is_locally_k_edge_connected(G, 5, 15, k=1) 

114 True 

115 >>> is_locally_k_edge_connected(G, 5, 15, k=2) 

116 False 

117 >>> is_locally_k_edge_connected(G, 1, 5, k=2) 

118 True 

119 """ 

120 if k < 1: 

121 raise ValueError(f"k must be positive, not {k}") 

122 

123 # First try to quickly determine s, t is not k-locally-edge-connected in G 

124 if G.degree(s) < k or G.degree(t) < k: 

125 return False 

126 else: 

127 # Otherwise perform the full check 

128 if k == 1: 

129 return nx.has_path(G, s, t) 

130 else: 

131 localk = nx.connectivity.local_edge_connectivity(G, s, t, cutoff=k) 

132 return localk >= k 

133 

134 

135@not_implemented_for("directed") 

136@not_implemented_for("multigraph") 

137@nx._dispatchable 

138def k_edge_augmentation(G, k, avail=None, weight=None, partial=False): 

139 """Finds set of edges to k-edge-connect G. 

140 

141 Adding edges from the augmentation to G make it impossible to disconnect G 

142 unless k or more edges are removed. This function uses the most efficient 

143 function available (depending on the value of k and if the problem is 

144 weighted or unweighted) to search for a minimum weight subset of available 

145 edges that k-edge-connects G. In general, finding a k-edge-augmentation is 

146 NP-hard, so solutions are not guaranteed to be minimal. Furthermore, a 

147 k-edge-augmentation may not exist. 

148 

149 Parameters 

150 ---------- 

151 G : NetworkX graph 

152 An undirected graph. 

153 

154 k : integer 

155 Desired edge connectivity 

156 

157 avail : dict or a set of 2 or 3 tuples 

158 The available edges that can be used in the augmentation. 

159 

160 If unspecified, then all edges in the complement of G are available. 

161 Otherwise, each item is an available edge (with an optional weight). 

162 

163 In the unweighted case, each item is an edge ``(u, v)``. 

164 

165 In the weighted case, each item is a 3-tuple ``(u, v, d)`` or a dict 

166 with items ``(u, v): d``. The third item, ``d``, can be a dictionary 

167 or a real number. If ``d`` is a dictionary ``d[weight]`` 

168 correspondings to the weight. 

169 

170 weight : string 

171 key to use to find weights if ``avail`` is a set of 3-tuples where the 

172 third item in each tuple is a dictionary. 

173 

174 partial : boolean 

175 If partial is True and no feasible k-edge-augmentation exists, then all 

176 a partial k-edge-augmentation is generated. Adding the edges in a 

177 partial augmentation to G, minimizes the number of k-edge-connected 

178 components and maximizes the edge connectivity between those 

179 components. For details, see :func:`partial_k_edge_augmentation`. 

180 

181 Yields 

182 ------ 

183 edge : tuple 

184 Edges that, once added to G, would cause G to become k-edge-connected. 

185 If partial is False, an error is raised if this is not possible. 

186 Otherwise, generated edges form a partial augmentation, which 

187 k-edge-connects any part of G where it is possible, and maximally 

188 connects the remaining parts. 

189 

190 Raises 

191 ------ 

192 NetworkXUnfeasible 

193 If partial is False and no k-edge-augmentation exists. 

194 

195 NetworkXNotImplemented 

196 If the input graph is directed or a multigraph. 

197 

198 ValueError: 

199 If k is less than 1 

200 

201 Notes 

202 ----- 

203 When k=1 this returns an optimal solution. 

204 

205 When k=2 and ``avail`` is None, this returns an optimal solution. 

206 Otherwise when k=2, this returns a 2-approximation of the optimal solution. 

207 

208 For k>3, this problem is NP-hard and this uses a randomized algorithm that 

209 produces a feasible solution, but provides no guarantees on the 

210 solution weight. 

211 

212 Examples 

213 -------- 

214 >>> # Unweighted cases 

215 >>> G = nx.path_graph((1, 2, 3, 4)) 

216 >>> G.add_node(5) 

217 >>> sorted(nx.k_edge_augmentation(G, k=1)) 

218 [(1, 5)] 

219 >>> sorted(nx.k_edge_augmentation(G, k=2)) 

220 [(1, 5), (5, 4)] 

221 >>> sorted(nx.k_edge_augmentation(G, k=3)) 

222 [(1, 4), (1, 5), (2, 5), (3, 5), (4, 5)] 

223 >>> complement = list(nx.k_edge_augmentation(G, k=5, partial=True)) 

224 >>> G.add_edges_from(complement) 

225 >>> nx.edge_connectivity(G) 

226 4 

227 

228 >>> # Weighted cases 

229 >>> G = nx.path_graph((1, 2, 3, 4)) 

230 >>> G.add_node(5) 

231 >>> # avail can be a tuple with a dict 

232 >>> avail = [(1, 5, {"weight": 11}), (2, 5, {"weight": 10})] 

233 >>> sorted(nx.k_edge_augmentation(G, k=1, avail=avail, weight="weight")) 

234 [(2, 5)] 

235 >>> # or avail can be a 3-tuple with a real number 

236 >>> avail = [(1, 5, 11), (2, 5, 10), (4, 3, 1), (4, 5, 51)] 

237 >>> sorted(nx.k_edge_augmentation(G, k=2, avail=avail)) 

238 [(1, 5), (2, 5), (4, 5)] 

239 >>> # or avail can be a dict 

240 >>> avail = {(1, 5): 11, (2, 5): 10, (4, 3): 1, (4, 5): 51} 

241 >>> sorted(nx.k_edge_augmentation(G, k=2, avail=avail)) 

242 [(1, 5), (2, 5), (4, 5)] 

243 >>> # If augmentation is infeasible, then a partial solution can be found 

244 >>> avail = {(1, 5): 11} 

245 >>> sorted(nx.k_edge_augmentation(G, k=2, avail=avail, partial=True)) 

246 [(1, 5)] 

247 """ 

248 try: 

249 if k <= 0: 

250 raise ValueError(f"k must be a positive integer, not {k}") 

251 elif G.number_of_nodes() < k + 1: 

252 msg = f"impossible to {k} connect in graph with less than {k + 1} nodes" 

253 raise nx.NetworkXUnfeasible(msg) 

254 elif avail is not None and len(avail) == 0: 

255 if not nx.is_k_edge_connected(G, k): 

256 raise nx.NetworkXUnfeasible("no available edges") 

257 aug_edges = [] 

258 elif k == 1: 

259 aug_edges = one_edge_augmentation( 

260 G, avail=avail, weight=weight, partial=partial 

261 ) 

262 elif k == 2: 

263 aug_edges = bridge_augmentation(G, avail=avail, weight=weight) 

264 else: 

265 # raise NotImplementedError(f'not implemented for k>2. k={k}') 

266 aug_edges = greedy_k_edge_augmentation( 

267 G, k=k, avail=avail, weight=weight, seed=0 

268 ) 

269 # Do eager evaluation so we can catch any exceptions 

270 # Before executing partial code. 

271 yield from list(aug_edges) 

272 except nx.NetworkXUnfeasible: 

273 if partial: 

274 # Return all available edges 

275 if avail is None: 

276 aug_edges = complement_edges(G) 

277 else: 

278 # If we can't k-edge-connect the entire graph, try to 

279 # k-edge-connect as much as possible 

280 aug_edges = partial_k_edge_augmentation( 

281 G, k=k, avail=avail, weight=weight 

282 ) 

283 yield from aug_edges 

284 else: 

285 raise 

286 

287 

288@nx._dispatchable 

289def partial_k_edge_augmentation(G, k, avail, weight=None): 

290 """Finds augmentation that k-edge-connects as much of the graph as possible. 

291 

292 When a k-edge-augmentation is not possible, we can still try to find a 

293 small set of edges that partially k-edge-connects as much of the graph as 

294 possible. All possible edges are generated between remaining parts. 

295 This minimizes the number of k-edge-connected subgraphs in the resulting 

296 graph and maximizes the edge connectivity between those subgraphs. 

297 

298 Parameters 

299 ---------- 

300 G : NetworkX graph 

301 An undirected graph. 

302 

303 k : integer 

304 Desired edge connectivity 

305 

306 avail : dict or a set of 2 or 3 tuples 

307 For more details, see :func:`k_edge_augmentation`. 

308 

309 weight : string 

310 key to use to find weights if ``avail`` is a set of 3-tuples. 

311 For more details, see :func:`k_edge_augmentation`. 

312 

313 Yields 

314 ------ 

315 edge : tuple 

316 Edges in the partial augmentation of G. These edges k-edge-connect any 

317 part of G where it is possible, and maximally connects the remaining 

318 parts. In other words, all edges from avail are generated except for 

319 those within subgraphs that have already become k-edge-connected. 

320 

321 Notes 

322 ----- 

323 Construct H that augments G with all edges in avail. 

324 Find the k-edge-subgraphs of H. 

325 For each k-edge-subgraph, if the number of nodes is more than k, then find 

326 the k-edge-augmentation of that graph and add it to the solution. Then add 

327 all edges in avail between k-edge subgraphs to the solution. 

328 

329 See Also 

330 -------- 

331 :func:`k_edge_augmentation` 

332 

333 Examples 

334 -------- 

335 >>> G = nx.path_graph((1, 2, 3, 4, 5, 6, 7)) 

336 >>> G.add_node(8) 

337 >>> avail = [(1, 3), (1, 4), (1, 5), (2, 4), (2, 5), (3, 5), (1, 8)] 

338 >>> sorted(partial_k_edge_augmentation(G, k=2, avail=avail)) 

339 [(1, 5), (1, 8)] 

340 """ 

341 

342 def _edges_between_disjoint(H, only1, only2): 

343 """finds edges between disjoint nodes""" 

344 only1_adj = {u: set(H.adj[u]) for u in only1} 

345 for u, neighbs in only1_adj.items(): 

346 # Find the neighbors of u in only1 that are also in only2 

347 neighbs12 = neighbs.intersection(only2) 

348 for v in neighbs12: 

349 yield (u, v) 

350 

351 avail_uv, avail_w = _unpack_available_edges(avail, weight=weight, G=G) 

352 

353 # Find which parts of the graph can be k-edge-connected 

354 H = G.copy() 

355 H.add_edges_from( 

356 ( 

357 (u, v, {"weight": w, "generator": (u, v)}) 

358 for (u, v), w in zip(avail, avail_w) 

359 ) 

360 ) 

361 k_edge_subgraphs = list(nx.k_edge_subgraphs(H, k=k)) 

362 

363 # Generate edges to k-edge-connect internal subgraphs 

364 for nodes in k_edge_subgraphs: 

365 if len(nodes) > 1: 

366 # Get the k-edge-connected subgraph 

367 C = H.subgraph(nodes).copy() 

368 # Find the internal edges that were available 

369 sub_avail = { 

370 d["generator"]: d["weight"] 

371 for (u, v, d) in C.edges(data=True) 

372 if "generator" in d 

373 } 

374 # Remove potential augmenting edges 

375 C.remove_edges_from(sub_avail.keys()) 

376 # Find a subset of these edges that makes the component 

377 # k-edge-connected and ignore the rest 

378 yield from nx.k_edge_augmentation(C, k=k, avail=sub_avail) 

379 

380 # Generate all edges between CCs that could not be k-edge-connected 

381 for cc1, cc2 in it.combinations(k_edge_subgraphs, 2): 

382 for u, v in _edges_between_disjoint(H, cc1, cc2): 

383 d = H.get_edge_data(u, v) 

384 edge = d.get("generator", None) 

385 if edge is not None: 

386 yield edge 

387 

388 

389@not_implemented_for("multigraph") 

390@not_implemented_for("directed") 

391@nx._dispatchable 

392def one_edge_augmentation(G, avail=None, weight=None, partial=False): 

393 """Finds minimum weight set of edges to connect G. 

394 

395 Equivalent to :func:`k_edge_augmentation` when k=1. Adding the resulting 

396 edges to G will make it 1-edge-connected. The solution is optimal for both 

397 weighted and non-weighted variants. 

398 

399 Parameters 

400 ---------- 

401 G : NetworkX graph 

402 An undirected graph. 

403 

404 avail : dict or a set of 2 or 3 tuples 

405 For more details, see :func:`k_edge_augmentation`. 

406 

407 weight : string 

408 key to use to find weights if ``avail`` is a set of 3-tuples. 

409 For more details, see :func:`k_edge_augmentation`. 

410 

411 partial : boolean 

412 If partial is True and no feasible k-edge-augmentation exists, then the 

413 augmenting edges minimize the number of connected components. 

414 

415 Yields 

416 ------ 

417 edge : tuple 

418 Edges in the one-augmentation of G 

419 

420 Raises 

421 ------ 

422 NetworkXUnfeasible 

423 If partial is False and no one-edge-augmentation exists. 

424 

425 Notes 

426 ----- 

427 Uses either :func:`unconstrained_one_edge_augmentation` or 

428 :func:`weighted_one_edge_augmentation` depending on whether ``avail`` is 

429 specified. Both algorithms are based on finding a minimum spanning tree. 

430 As such both algorithms find optimal solutions and run in linear time. 

431 

432 See Also 

433 -------- 

434 :func:`k_edge_augmentation` 

435 """ 

436 if avail is None: 

437 return unconstrained_one_edge_augmentation(G) 

438 else: 

439 return weighted_one_edge_augmentation( 

440 G, avail=avail, weight=weight, partial=partial 

441 ) 

442 

443 

444@not_implemented_for("multigraph") 

445@not_implemented_for("directed") 

446@nx._dispatchable 

447def bridge_augmentation(G, avail=None, weight=None): 

448 """Finds the a set of edges that bridge connects G. 

449 

450 Equivalent to :func:`k_edge_augmentation` when k=2, and partial=False. 

451 Adding the resulting edges to G will make it 2-edge-connected. If no 

452 constraints are specified the returned set of edges is minimum an optimal, 

453 otherwise the solution is approximated. 

454 

455 Parameters 

456 ---------- 

457 G : NetworkX graph 

458 An undirected graph. 

459 

460 avail : dict or a set of 2 or 3 tuples 

461 For more details, see :func:`k_edge_augmentation`. 

462 

463 weight : string 

464 key to use to find weights if ``avail`` is a set of 3-tuples. 

465 For more details, see :func:`k_edge_augmentation`. 

466 

467 Yields 

468 ------ 

469 edge : tuple 

470 Edges in the bridge-augmentation of G 

471 

472 Raises 

473 ------ 

474 NetworkXUnfeasible 

475 If no bridge-augmentation exists. 

476 

477 Notes 

478 ----- 

479 If there are no constraints the solution can be computed in linear time 

480 using :func:`unconstrained_bridge_augmentation`. Otherwise, the problem 

481 becomes NP-hard and is the solution is approximated by 

482 :func:`weighted_bridge_augmentation`. 

483 

484 See Also 

485 -------- 

486 :func:`k_edge_augmentation` 

487 """ 

488 if G.number_of_nodes() < 3: 

489 raise nx.NetworkXUnfeasible("impossible to bridge connect less than 3 nodes") 

490 if avail is None: 

491 return unconstrained_bridge_augmentation(G) 

492 else: 

493 return weighted_bridge_augmentation(G, avail, weight=weight) 

494 

495 

496# --- Algorithms and Helpers --- 

497 

498 

499def _ordered(u, v): 

500 """Returns the nodes in an undirected edge in lower-triangular order""" 

501 return (u, v) if u < v else (v, u) 

502 

503 

504def _unpack_available_edges(avail, weight=None, G=None): 

505 """Helper to separate avail into edges and corresponding weights""" 

506 if weight is None: 

507 weight = "weight" 

508 if isinstance(avail, dict): 

509 avail_uv = list(avail.keys()) 

510 avail_w = list(avail.values()) 

511 else: 

512 

513 def _try_getitem(d): 

514 try: 

515 return d[weight] 

516 except TypeError: 

517 return d 

518 

519 avail_uv = [tup[0:2] for tup in avail] 

520 avail_w = [1 if len(tup) == 2 else _try_getitem(tup[-1]) for tup in avail] 

521 

522 if G is not None: 

523 # Edges already in the graph are filtered 

524 flags = [not G.has_edge(u, v) for u, v in avail_uv] 

525 avail_uv = list(it.compress(avail_uv, flags)) 

526 avail_w = list(it.compress(avail_w, flags)) 

527 return avail_uv, avail_w 

528 

529 

530MetaEdge = namedtuple("MetaEdge", ("meta_uv", "uv", "w")) 

531 

532 

533def _lightest_meta_edges(mapping, avail_uv, avail_w): 

534 """Maps available edges in the original graph to edges in the metagraph. 

535 

536 Parameters 

537 ---------- 

538 mapping : dict 

539 mapping produced by :func:`collapse`, that maps each node in the 

540 original graph to a node in the meta graph 

541 

542 avail_uv : list 

543 list of edges 

544 

545 avail_w : list 

546 list of edge weights 

547 

548 Notes 

549 ----- 

550 Each node in the metagraph is a k-edge-connected component in the original 

551 graph. We don't care about any edge within the same k-edge-connected 

552 component, so we ignore self edges. We also are only interested in the 

553 minimum weight edge bridging each k-edge-connected component so, we group 

554 the edges by meta-edge and take the lightest in each group. 

555 

556 Examples 

557 -------- 

558 >>> # Each group represents a meta-node 

559 >>> groups = ([1, 2, 3], [4, 5], [6]) 

560 >>> mapping = {n: meta_n for meta_n, ns in enumerate(groups) for n in ns} 

561 >>> avail_uv = [(1, 2), (3, 6), (1, 4), (5, 2), (6, 1), (2, 6), (3, 1)] 

562 >>> avail_w = [20, 99, 20, 15, 50, 99, 20] 

563 >>> sorted(_lightest_meta_edges(mapping, avail_uv, avail_w)) 

564 [MetaEdge(meta_uv=(0, 1), uv=(5, 2), w=15), MetaEdge(meta_uv=(0, 2), uv=(6, 1), w=50)] 

565 """ 

566 grouped_wuv = defaultdict(list) 

567 for w, (u, v) in zip(avail_w, avail_uv): 

568 # Order the meta-edge so it can be used as a dict key 

569 meta_uv = _ordered(mapping[u], mapping[v]) 

570 # Group each available edge using the meta-edge as a key 

571 grouped_wuv[meta_uv].append((w, u, v)) 

572 

573 # Now that all available edges are grouped, choose one per group 

574 for (mu, mv), choices_wuv in grouped_wuv.items(): 

575 # Ignore available edges within the same meta-node 

576 if mu != mv: 

577 # Choose the lightest available edge belonging to each meta-edge 

578 w, u, v = min(choices_wuv) 

579 yield MetaEdge((mu, mv), (u, v), w) 

580 

581 

582@nx._dispatchable 

583def unconstrained_one_edge_augmentation(G): 

584 """Finds the smallest set of edges to connect G. 

585 

586 This is a variant of the unweighted MST problem. 

587 If G is not empty, a feasible solution always exists. 

588 

589 Parameters 

590 ---------- 

591 G : NetworkX graph 

592 An undirected graph. 

593 

594 Yields 

595 ------ 

596 edge : tuple 

597 Edges in the one-edge-augmentation of G 

598 

599 See Also 

600 -------- 

601 :func:`one_edge_augmentation` 

602 :func:`k_edge_augmentation` 

603 

604 Examples 

605 -------- 

606 >>> G = nx.Graph([(1, 2), (2, 3), (4, 5)]) 

607 >>> G.add_nodes_from([6, 7, 8]) 

608 >>> sorted(unconstrained_one_edge_augmentation(G)) 

609 [(1, 4), (4, 6), (6, 7), (7, 8)] 

610 """ 

611 ccs1 = list(nx.connected_components(G)) 

612 C = collapse(G, ccs1) 

613 # When we are not constrained, we can just make a meta graph tree. 

614 meta_nodes = list(C.nodes()) 

615 # build a path in the metagraph 

616 meta_aug = list(zip(meta_nodes, meta_nodes[1:])) 

617 # map that path to the original graph 

618 inverse = defaultdict(list) 

619 for k, v in C.graph["mapping"].items(): 

620 inverse[v].append(k) 

621 for mu, mv in meta_aug: 

622 yield (inverse[mu][0], inverse[mv][0]) 

623 

624 

625@nx._dispatchable 

626def weighted_one_edge_augmentation(G, avail, weight=None, partial=False): 

627 """Finds the minimum weight set of edges to connect G if one exists. 

628 

629 This is a variant of the weighted MST problem. 

630 

631 Parameters 

632 ---------- 

633 G : NetworkX graph 

634 An undirected graph. 

635 

636 avail : dict or a set of 2 or 3 tuples 

637 For more details, see :func:`k_edge_augmentation`. 

638 

639 weight : string 

640 key to use to find weights if ``avail`` is a set of 3-tuples. 

641 For more details, see :func:`k_edge_augmentation`. 

642 

643 partial : boolean 

644 If partial is True and no feasible k-edge-augmentation exists, then the 

645 augmenting edges minimize the number of connected components. 

646 

647 Yields 

648 ------ 

649 edge : tuple 

650 Edges in the subset of avail chosen to connect G. 

651 

652 See Also 

653 -------- 

654 :func:`one_edge_augmentation` 

655 :func:`k_edge_augmentation` 

656 

657 Examples 

658 -------- 

659 >>> G = nx.Graph([(1, 2), (2, 3), (4, 5)]) 

660 >>> G.add_nodes_from([6, 7, 8]) 

661 >>> # any edge not in avail has an implicit weight of infinity 

662 >>> avail = [(1, 3), (1, 5), (4, 7), (4, 8), (6, 1), (8, 1), (8, 2)] 

663 >>> sorted(weighted_one_edge_augmentation(G, avail)) 

664 [(1, 5), (4, 7), (6, 1), (8, 1)] 

665 >>> # find another solution by giving large weights to edges in the 

666 >>> # previous solution (note some of the old edges must be used) 

667 >>> avail = [(1, 3), (1, 5, 99), (4, 7, 9), (6, 1, 99), (8, 1, 99), (8, 2)] 

668 >>> sorted(weighted_one_edge_augmentation(G, avail)) 

669 [(1, 5), (4, 7), (6, 1), (8, 2)] 

670 """ 

671 avail_uv, avail_w = _unpack_available_edges(avail, weight=weight, G=G) 

672 # Collapse CCs in the original graph into nodes in a metagraph 

673 # Then find an MST of the metagraph instead of the original graph 

674 C = collapse(G, nx.connected_components(G)) 

675 mapping = C.graph["mapping"] 

676 # Assign each available edge to an edge in the metagraph 

677 candidate_mapping = _lightest_meta_edges(mapping, avail_uv, avail_w) 

678 # nx.set_edge_attributes(C, name='weight', values=0) 

679 C.add_edges_from( 

680 (mu, mv, {"weight": w, "generator": uv}) 

681 for (mu, mv), uv, w in candidate_mapping 

682 ) 

683 # Find MST of the meta graph 

684 meta_mst = nx.minimum_spanning_tree(C) 

685 if not partial and not nx.is_connected(meta_mst): 

686 raise nx.NetworkXUnfeasible("Not possible to connect G with available edges") 

687 # Yield the edge that generated the meta-edge 

688 for mu, mv, d in meta_mst.edges(data=True): 

689 if "generator" in d: 

690 edge = d["generator"] 

691 yield edge 

692 

693 

694@nx._dispatchable 

695def unconstrained_bridge_augmentation(G): 

696 """Finds an optimal 2-edge-augmentation of G using the fewest edges. 

697 

698 This is an implementation of the algorithm detailed in [1]_. 

699 The basic idea is to construct a meta-graph of bridge-ccs, connect leaf 

700 nodes of the trees to connect the entire graph, and finally connect the 

701 leafs of the tree in dfs-preorder to bridge connect the entire graph. 

702 

703 Parameters 

704 ---------- 

705 G : NetworkX graph 

706 An undirected graph. 

707 

708 Yields 

709 ------ 

710 edge : tuple 

711 Edges in the bridge augmentation of G 

712 

713 Notes 

714 ----- 

715 Input: a graph G. 

716 First find the bridge components of G and collapse each bridge-cc into a 

717 node of a metagraph graph C, which is guaranteed to be a forest of trees. 

718 

719 C contains p "leafs" --- nodes with exactly one incident edge. 

720 C contains q "isolated nodes" --- nodes with no incident edges. 

721 

722 Theorem: If p + q > 1, then at least :math:`ceil(p / 2) + q` edges are 

723 needed to bridge connect C. This algorithm achieves this min number. 

724 

725 The method first adds enough edges to make G into a tree and then pairs 

726 leafs in a simple fashion. 

727 

728 Let n be the number of trees in C. Let v(i) be an isolated vertex in the 

729 i-th tree if one exists, otherwise it is a pair of distinct leafs nodes 

730 in the i-th tree. Alternating edges from these sets (i.e. adding edges 

731 A1 = [(v(i)[0], v(i + 1)[1]), v(i + 1)[0], v(i + 2)[1])...]) connects C 

732 into a tree T. This tree has p' = p + 2q - 2(n -1) leafs and no isolated 

733 vertices. A1 has n - 1 edges. The next step finds ceil(p' / 2) edges to 

734 biconnect any tree with p' leafs. 

735 

736 Convert T into an arborescence T' by picking an arbitrary root node with 

737 degree >= 2 and directing all edges away from the root. Note the 

738 implementation implicitly constructs T'. 

739 

740 The leafs of T are the nodes with no existing edges in T'. 

741 Order the leafs of T' by DFS preorder. Then break this list in half 

742 and add the zipped pairs to A2. 

743 

744 The set A = A1 + A2 is the minimum augmentation in the metagraph. 

745 

746 To convert this to edges in the original graph 

747 

748 References 

749 ---------- 

750 .. [1] Eswaran, Kapali P., and R. Endre Tarjan. (1975) Augmentation problems. 

751 http://epubs.siam.org/doi/abs/10.1137/0205044 

752 

753 See Also 

754 -------- 

755 :func:`bridge_augmentation` 

756 :func:`k_edge_augmentation` 

757 

758 Examples 

759 -------- 

760 >>> G = nx.path_graph((1, 2, 3, 4, 5, 6, 7)) 

761 >>> sorted(unconstrained_bridge_augmentation(G)) 

762 [(1, 7)] 

763 >>> G = nx.path_graph((1, 2, 3, 2, 4, 5, 6, 7)) 

764 >>> sorted(unconstrained_bridge_augmentation(G)) 

765 [(1, 3), (3, 7)] 

766 >>> G = nx.Graph([(0, 1), (0, 2), (1, 2)]) 

767 >>> G.add_node(4) 

768 >>> sorted(unconstrained_bridge_augmentation(G)) 

769 [(1, 4), (4, 0)] 

770 """ 

771 # ----- 

772 # Mapping of terms from (Eswaran and Tarjan): 

773 # G = G_0 - the input graph 

774 # C = G_0' - the bridge condensation of G. (This is a forest of trees) 

775 # A1 = A_1 - the edges to connect the forest into a tree 

776 # leaf = pendant - a node with degree of 1 

777 

778 # alpha(v) = maps the node v in G to its meta-node in C 

779 # beta(x) = maps the meta-node x in C to any node in the bridge 

780 # component of G corresponding to x. 

781 

782 # find the 2-edge-connected components of G 

783 bridge_ccs = list(nx.connectivity.bridge_components(G)) 

784 # condense G into an forest C 

785 C = collapse(G, bridge_ccs) 

786 

787 # Choose pairs of distinct leaf nodes in each tree. If this is not 

788 # possible then make a pair using the single isolated node in the tree. 

789 vset1 = [ 

790 tuple(cc) * 2 # case1: an isolated node 

791 if len(cc) == 1 

792 else sorted(cc, key=C.degree)[0:2] # case2: pair of leaf nodes 

793 for cc in nx.connected_components(C) 

794 ] 

795 if len(vset1) > 1: 

796 # Use this set to construct edges that connect C into a tree. 

797 nodes1 = [vs[0] for vs in vset1] 

798 nodes2 = [vs[1] for vs in vset1] 

799 A1 = list(zip(nodes1[1:], nodes2)) 

800 else: 

801 A1 = [] 

802 # Connect each tree in the forest to construct an arborescence 

803 T = C.copy() 

804 T.add_edges_from(A1) 

805 

806 # If there are only two leaf nodes, we simply connect them. 

807 leafs = [n for n, d in T.degree() if d == 1] 

808 if len(leafs) == 1: 

809 A2 = [] 

810 if len(leafs) == 2: 

811 A2 = [tuple(leafs)] 

812 else: 

813 # Choose an arbitrary non-leaf root 

814 try: 

815 root = next(n for n, d in T.degree() if d > 1) 

816 except StopIteration: # no nodes found with degree > 1 

817 return 

818 # order the leaves of C by (induced directed) preorder 

819 v2 = [n for n in nx.dfs_preorder_nodes(T, root) if T.degree(n) == 1] 

820 # connecting first half of the leafs in pre-order to the second 

821 # half will bridge connect the tree with the fewest edges. 

822 half = math.ceil(len(v2) / 2) 

823 A2 = list(zip(v2[:half], v2[-half:])) 

824 

825 # collect the edges used to augment the original forest 

826 aug_tree_edges = A1 + A2 

827 

828 # Construct the mapping (beta) from meta-nodes to regular nodes 

829 inverse = defaultdict(list) 

830 for k, v in C.graph["mapping"].items(): 

831 inverse[v].append(k) 

832 # sort so we choose minimum degree nodes first 

833 inverse = { 

834 mu: sorted(mapped, key=lambda u: (G.degree(u), u)) 

835 for mu, mapped in inverse.items() 

836 } 

837 

838 # For each meta-edge, map back to an arbitrary pair in the original graph 

839 G2 = G.copy() 

840 for mu, mv in aug_tree_edges: 

841 # Find the first available edge that doesn't exist and return it 

842 for u, v in it.product(inverse[mu], inverse[mv]): 

843 if not G2.has_edge(u, v): 

844 G2.add_edge(u, v) 

845 yield u, v 

846 break 

847 

848 

849@nx._dispatchable 

850def weighted_bridge_augmentation(G, avail, weight=None): 

851 """Finds an approximate min-weight 2-edge-augmentation of G. 

852 

853 This is an implementation of the approximation algorithm detailed in [1]_. 

854 It chooses a set of edges from avail to add to G that renders it 

855 2-edge-connected if such a subset exists. This is done by finding a 

856 minimum spanning arborescence of a specially constructed metagraph. 

857 

858 Parameters 

859 ---------- 

860 G : NetworkX graph 

861 An undirected graph. 

862 

863 avail : set of 2 or 3 tuples. 

864 candidate edges (with optional weights) to choose from 

865 

866 weight : string 

867 key to use to find weights if avail is a set of 3-tuples where the 

868 third item in each tuple is a dictionary. 

869 

870 Yields 

871 ------ 

872 edge : tuple 

873 Edges in the subset of avail chosen to bridge augment G. 

874 

875 Notes 

876 ----- 

877 Finding a weighted 2-edge-augmentation is NP-hard. 

878 Any edge not in ``avail`` is considered to have a weight of infinity. 

879 The approximation factor is 2 if ``G`` is connected and 3 if it is not. 

880 Runs in :math:`O(m + n log(n))` time 

881 

882 References 

883 ---------- 

884 .. [1] Khuller, Samir, and Ramakrishna Thurimella. (1993) Approximation 

885 algorithms for graph augmentation. 

886 http://www.sciencedirect.com/science/article/pii/S0196677483710102 

887 

888 See Also 

889 -------- 

890 :func:`bridge_augmentation` 

891 :func:`k_edge_augmentation` 

892 

893 Examples 

894 -------- 

895 >>> G = nx.path_graph((1, 2, 3, 4)) 

896 >>> # When the weights are equal, (1, 4) is the best 

897 >>> avail = [(1, 4, 1), (1, 3, 1), (2, 4, 1)] 

898 >>> sorted(weighted_bridge_augmentation(G, avail)) 

899 [(1, 4)] 

900 >>> # Giving (1, 4) a high weight makes the two edge solution the best. 

901 >>> avail = [(1, 4, 1000), (1, 3, 1), (2, 4, 1)] 

902 >>> sorted(weighted_bridge_augmentation(G, avail)) 

903 [(1, 3), (2, 4)] 

904 >>> # ------ 

905 >>> G = nx.path_graph((1, 2, 3, 4)) 

906 >>> G.add_node(5) 

907 >>> avail = [(1, 5, 11), (2, 5, 10), (4, 3, 1), (4, 5, 1)] 

908 >>> sorted(weighted_bridge_augmentation(G, avail=avail)) 

909 [(1, 5), (4, 5)] 

910 >>> avail = [(1, 5, 11), (2, 5, 10), (4, 3, 1), (4, 5, 51)] 

911 >>> sorted(weighted_bridge_augmentation(G, avail=avail)) 

912 [(1, 5), (2, 5), (4, 5)] 

913 """ 

914 

915 if weight is None: 

916 weight = "weight" 

917 

918 # If input G is not connected the approximation factor increases to 3 

919 if not nx.is_connected(G): 

920 H = G.copy() 

921 connectors = list(one_edge_augmentation(H, avail=avail, weight=weight)) 

922 H.add_edges_from(connectors) 

923 

924 yield from connectors 

925 else: 

926 connectors = [] 

927 H = G 

928 

929 if len(avail) == 0: 

930 if nx.has_bridges(H): 

931 raise nx.NetworkXUnfeasible("no augmentation possible") 

932 

933 avail_uv, avail_w = _unpack_available_edges(avail, weight=weight, G=H) 

934 

935 # Collapse input into a metagraph. Meta nodes are bridge-ccs 

936 bridge_ccs = nx.connectivity.bridge_components(H) 

937 C = collapse(H, bridge_ccs) 

938 

939 # Use the meta graph to shrink avail to a small feasible subset 

940 mapping = C.graph["mapping"] 

941 # Choose the minimum weight feasible edge in each group 

942 meta_to_wuv = { 

943 (mu, mv): (w, uv) 

944 for (mu, mv), uv, w in _lightest_meta_edges(mapping, avail_uv, avail_w) 

945 } 

946 

947 # Mapping of terms from (Khuller and Thurimella): 

948 # C : G_0 = (V, E^0) 

949 # This is the metagraph where each node is a 2-edge-cc in G. 

950 # The edges in C represent bridges in the original graph. 

951 # (mu, mv) : E - E^0 # they group both avail and given edges in E 

952 # T : \Gamma 

953 # D : G^D = (V, E_D) 

954 

955 # The paper uses ancestor because children point to parents, which is 

956 # contrary to networkx standards. So, we actually need to run 

957 # nx.least_common_ancestor on the reversed Tree. 

958 

959 # Pick an arbitrary leaf from C as the root 

960 try: 

961 root = next(n for n, d in C.degree() if d == 1) 

962 except StopIteration: # no nodes found with degree == 1 

963 return 

964 # Root C into a tree TR by directing all edges away from the root 

965 # Note in their paper T directs edges towards the root 

966 TR = nx.dfs_tree(C, root) 

967 

968 # Add to D the directed edges of T and set their weight to zero 

969 # This indicates that it costs nothing to use edges that were given. 

970 D = nx.reverse(TR).copy() 

971 

972 nx.set_edge_attributes(D, name="weight", values=0) 

973 

974 # The LCA of mu and mv in T is the shared ancestor of mu and mv that is 

975 # located farthest from the root. 

976 lca_gen = nx.tree_all_pairs_lowest_common_ancestor( 

977 TR, root=root, pairs=meta_to_wuv.keys() 

978 ) 

979 

980 for (mu, mv), lca in lca_gen: 

981 w, uv = meta_to_wuv[(mu, mv)] 

982 if lca == mu: 

983 # If u is an ancestor of v in TR, then add edge u->v to D 

984 D.add_edge(lca, mv, weight=w, generator=uv) 

985 elif lca == mv: 

986 # If v is an ancestor of u in TR, then add edge v->u to D 

987 D.add_edge(lca, mu, weight=w, generator=uv) 

988 else: 

989 # If neither u nor v is a ancestor of the other in TR 

990 # let t = lca(TR, u, v) and add edges t->u and t->v 

991 # Track the original edge that GENERATED these edges. 

992 D.add_edge(lca, mu, weight=w, generator=uv) 

993 D.add_edge(lca, mv, weight=w, generator=uv) 

994 

995 # Then compute a minimum rooted branching 

996 try: 

997 # Note the original edges must be directed towards to root for the 

998 # branching to give us a bridge-augmentation. 

999 A = _minimum_rooted_branching(D, root) 

1000 except nx.NetworkXException as err: 

1001 # If there is no branching then augmentation is not possible 

1002 raise nx.NetworkXUnfeasible("no 2-edge-augmentation possible") from err 

1003 

1004 # For each edge e, in the branching that did not belong to the directed 

1005 # tree T, add the corresponding edge that **GENERATED** it (this is not 

1006 # necessarily e itself!) 

1007 

1008 # ensure the third case does not generate edges twice 

1009 bridge_connectors = set() 

1010 for mu, mv in A.edges(): 

1011 data = D.get_edge_data(mu, mv) 

1012 if "generator" in data: 

1013 # Add the avail edge that generated the branching edge. 

1014 edge = data["generator"] 

1015 bridge_connectors.add(edge) 

1016 

1017 yield from bridge_connectors 

1018 

1019 

1020def _minimum_rooted_branching(D, root): 

1021 """Helper function to compute a minimum rooted branching (aka rooted 

1022 arborescence) 

1023 

1024 Before the branching can be computed, the directed graph must be rooted by 

1025 removing the predecessors of root. 

1026 

1027 A branching / arborescence of rooted graph G is a subgraph that contains a 

1028 directed path from the root to every other vertex. It is the directed 

1029 analog of the minimum spanning tree problem. 

1030 

1031 References 

1032 ---------- 

1033 [1] Khuller, Samir (2002) Advanced Algorithms Lecture 24 Notes. 

1034 https://web.archive.org/web/20121030033722/https://www.cs.umd.edu/class/spring2011/cmsc651/lec07.pdf 

1035 """ 

1036 rooted = D.copy() 

1037 # root the graph by removing all predecessors to `root`. 

1038 rooted.remove_edges_from([(u, root) for u in D.predecessors(root)]) 

1039 # Then compute the branching / arborescence. 

1040 A = nx.minimum_spanning_arborescence(rooted) 

1041 return A 

1042 

1043 

1044@nx._dispatchable(returns_graph=True) 

1045def collapse(G, grouped_nodes): 

1046 """Collapses each group of nodes into a single node. 

1047 

1048 This is similar to condensation, but works on undirected graphs. 

1049 

1050 Parameters 

1051 ---------- 

1052 G : NetworkX Graph 

1053 

1054 grouped_nodes: list or generator 

1055 Grouping of nodes to collapse. The grouping must be disjoint. 

1056 If grouped_nodes are strongly_connected_components then this is 

1057 equivalent to :func:`condensation`. 

1058 

1059 Returns 

1060 ------- 

1061 C : NetworkX Graph 

1062 The collapsed graph C of G with respect to the node grouping. The node 

1063 labels are integers corresponding to the index of the component in the 

1064 list of grouped_nodes. C has a graph attribute named 'mapping' with a 

1065 dictionary mapping the original nodes to the nodes in C to which they 

1066 belong. Each node in C also has a node attribute 'members' with the set 

1067 of original nodes in G that form the group that the node in C 

1068 represents. 

1069 

1070 Examples 

1071 -------- 

1072 >>> # Collapses a graph using disjoint groups, but not necessarily connected 

1073 >>> G = nx.Graph([(1, 0), (2, 3), (3, 1), (3, 4), (4, 5), (5, 6), (5, 7)]) 

1074 >>> G.add_node("A") 

1075 >>> grouped_nodes = [{0, 1, 2, 3}, {5, 6, 7}] 

1076 >>> C = collapse(G, grouped_nodes) 

1077 >>> members = nx.get_node_attributes(C, "members") 

1078 >>> sorted(members.keys()) 

1079 [0, 1, 2, 3] 

1080 >>> member_values = set(map(frozenset, members.values())) 

1081 >>> assert {0, 1, 2, 3} in member_values 

1082 >>> assert {4} in member_values 

1083 >>> assert {5, 6, 7} in member_values 

1084 >>> assert {"A"} in member_values 

1085 """ 

1086 mapping = {} 

1087 members = {} 

1088 C = G.__class__() 

1089 i = 0 # required if G is empty 

1090 remaining = set(G.nodes()) 

1091 for i, group in enumerate(grouped_nodes): 

1092 group = set(group) 

1093 assert remaining.issuperset(group), ( 

1094 "grouped nodes must exist in G and be disjoint" 

1095 ) 

1096 remaining.difference_update(group) 

1097 members[i] = group 

1098 mapping.update((n, i) for n in group) 

1099 # remaining nodes are in their own group 

1100 for i, node in enumerate(remaining, start=i + 1): 

1101 group = {node} 

1102 members[i] = group 

1103 mapping.update((n, i) for n in group) 

1104 number_of_groups = i + 1 

1105 C.add_nodes_from(range(number_of_groups)) 

1106 C.add_edges_from( 

1107 (mapping[u], mapping[v]) for u, v in G.edges() if mapping[u] != mapping[v] 

1108 ) 

1109 # Add a list of members (ie original nodes) to each node (ie scc) in C. 

1110 nx.set_node_attributes(C, name="members", values=members) 

1111 # Add mapping dict as graph attribute 

1112 C.graph["mapping"] = mapping 

1113 return C 

1114 

1115 

1116@nx._dispatchable 

1117def complement_edges(G): 

1118 """Returns only the edges in the complement of G 

1119 

1120 Parameters 

1121 ---------- 

1122 G : NetworkX Graph 

1123 

1124 Yields 

1125 ------ 

1126 edge : tuple 

1127 Edges in the complement of G 

1128 

1129 Examples 

1130 -------- 

1131 >>> G = nx.path_graph((1, 2, 3, 4)) 

1132 >>> sorted(complement_edges(G)) 

1133 [(1, 3), (1, 4), (2, 4)] 

1134 >>> G = nx.path_graph((1, 2, 3, 4), nx.DiGraph()) 

1135 >>> sorted(complement_edges(G)) 

1136 [(1, 3), (1, 4), (2, 1), (2, 4), (3, 1), (3, 2), (4, 1), (4, 2), (4, 3)] 

1137 >>> G = nx.complete_graph(1000) 

1138 >>> sorted(complement_edges(G)) 

1139 [] 

1140 """ 

1141 G_adj = G._adj # Store as a variable to eliminate attribute lookup 

1142 if G.is_directed(): 

1143 for u, v in it.combinations(G.nodes(), 2): 

1144 if v not in G_adj[u]: 

1145 yield (u, v) 

1146 if u not in G_adj[v]: 

1147 yield (v, u) 

1148 else: 

1149 for u, v in it.combinations(G.nodes(), 2): 

1150 if v not in G_adj[u]: 

1151 yield (u, v) 

1152 

1153 

1154def _compat_shuffle(rng, input): 

1155 """wrapper around rng.shuffle for python 2 compatibility reasons""" 

1156 rng.shuffle(input) 

1157 

1158 

1159@not_implemented_for("multigraph") 

1160@not_implemented_for("directed") 

1161@py_random_state(4) 

1162@nx._dispatchable 

1163def greedy_k_edge_augmentation(G, k, avail=None, weight=None, seed=None): 

1164 """Greedy algorithm for finding a k-edge-augmentation 

1165 

1166 Parameters 

1167 ---------- 

1168 G : NetworkX graph 

1169 An undirected graph. 

1170 

1171 k : integer 

1172 Desired edge connectivity 

1173 

1174 avail : dict or a set of 2 or 3 tuples 

1175 For more details, see :func:`k_edge_augmentation`. 

1176 

1177 weight : string 

1178 key to use to find weights if ``avail`` is a set of 3-tuples. 

1179 For more details, see :func:`k_edge_augmentation`. 

1180 

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

1182 Indicator of random number generation state. 

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

1184 

1185 Yields 

1186 ------ 

1187 edge : tuple 

1188 Edges in the greedy augmentation of G 

1189 

1190 Notes 

1191 ----- 

1192 The algorithm is simple. Edges are incrementally added between parts of the 

1193 graph that are not yet locally k-edge-connected. Then edges are from the 

1194 augmenting set are pruned as long as local-edge-connectivity is not broken. 

1195 

1196 This algorithm is greedy and does not provide optimality guarantees. It 

1197 exists only to provide :func:`k_edge_augmentation` with the ability to 

1198 generate a feasible solution for arbitrary k. 

1199 

1200 See Also 

1201 -------- 

1202 :func:`k_edge_augmentation` 

1203 

1204 Examples 

1205 -------- 

1206 >>> G = nx.path_graph((1, 2, 3, 4, 5, 6, 7)) 

1207 >>> sorted(greedy_k_edge_augmentation(G, k=2)) 

1208 [(1, 7)] 

1209 >>> sorted(greedy_k_edge_augmentation(G, k=1, avail=[])) 

1210 [] 

1211 >>> G = nx.path_graph((1, 2, 3, 4, 5, 6, 7)) 

1212 >>> avail = {(u, v): 1 for (u, v) in complement_edges(G)} 

1213 >>> # randomized pruning process can produce different solutions 

1214 >>> sorted(greedy_k_edge_augmentation(G, k=4, avail=avail, seed=2)) 

1215 [(1, 3), (1, 4), (1, 5), (1, 6), (1, 7), (2, 4), (2, 6), (3, 7), (5, 7)] 

1216 >>> sorted(greedy_k_edge_augmentation(G, k=4, avail=avail, seed=3)) 

1217 [(1, 3), (1, 5), (1, 6), (2, 4), (2, 6), (3, 7), (4, 7), (5, 7)] 

1218 """ 

1219 # Result set 

1220 aug_edges = [] 

1221 

1222 done = is_k_edge_connected(G, k) 

1223 if done: 

1224 return 

1225 if avail is None: 

1226 # all edges are available 

1227 avail_uv = list(complement_edges(G)) 

1228 avail_w = [1] * len(avail_uv) 

1229 else: 

1230 # Get the unique set of unweighted edges 

1231 avail_uv, avail_w = _unpack_available_edges(avail, weight=weight, G=G) 

1232 

1233 # Greedy: order lightest edges. Use degree sum to tie-break 

1234 tiebreaker = [sum(map(G.degree, uv)) for uv in avail_uv] 

1235 avail_wduv = sorted(zip(avail_w, tiebreaker, avail_uv)) 

1236 avail_uv = [uv for w, d, uv in avail_wduv] 

1237 

1238 # Incrementally add edges in until we are k-connected 

1239 H = G.copy() 

1240 for u, v in avail_uv: 

1241 done = False 

1242 if not is_locally_k_edge_connected(H, u, v, k=k): 

1243 # Only add edges in parts that are not yet locally k-edge-connected 

1244 aug_edges.append((u, v)) 

1245 H.add_edge(u, v) 

1246 # Did adding this edge help? 

1247 if H.degree(u) >= k and H.degree(v) >= k: 

1248 done = is_k_edge_connected(H, k) 

1249 if done: 

1250 break 

1251 

1252 # Check for feasibility 

1253 if not done: 

1254 raise nx.NetworkXUnfeasible("not able to k-edge-connect with available edges") 

1255 

1256 # Randomized attempt to reduce the size of the solution 

1257 _compat_shuffle(seed, aug_edges) 

1258 for u, v in list(aug_edges): 

1259 # Don't remove if we know it would break connectivity 

1260 if H.degree(u) <= k or H.degree(v) <= k: 

1261 continue 

1262 H.remove_edge(u, v) 

1263 aug_edges.remove((u, v)) 

1264 if not is_k_edge_connected(H, k=k): 

1265 # If removing this edge breaks feasibility, undo 

1266 H.add_edge(u, v) 

1267 aug_edges.append((u, v)) 

1268 

1269 # Generate results 

1270 yield from aug_edges