Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/networkx/algorithms/dag.py: 22%

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

261 statements  

1"""Algorithms for directed acyclic graphs (DAGs). 

2 

3Note that most of these functions are only guaranteed to work for DAGs. 

4In general, these functions do not check for acyclic-ness, so it is up 

5to the user to check for that. 

6""" 

7 

8import heapq 

9from collections import deque 

10from itertools import combinations, product 

11from math import gcd 

12 

13import networkx as nx 

14from networkx.utils import arbitrary_element, not_implemented_for, pairwise 

15 

16__all__ = [ 

17 "descendants", 

18 "ancestors", 

19 "topological_sort", 

20 "lexicographical_topological_sort", 

21 "all_topological_sorts", 

22 "topological_generations", 

23 "is_directed_acyclic_graph", 

24 "is_aperiodic", 

25 "transitive_closure", 

26 "transitive_closure_dag", 

27 "transitive_reduction", 

28 "antichains", 

29 "dag_longest_path", 

30 "dag_longest_path_length", 

31 "dag_to_branching", 

32] 

33 

34 

35@nx._dispatchable 

36def descendants(G, source): 

37 """Returns all nodes reachable from `source` in `G`. 

38 

39 Parameters 

40 ---------- 

41 G : NetworkX Graph 

42 source : node in `G` 

43 

44 Returns 

45 ------- 

46 set() 

47 The descendants of `source` in `G` 

48 

49 Raises 

50 ------ 

51 NetworkXError 

52 If node `source` is not in `G`. 

53 

54 Examples 

55 -------- 

56 >>> DG = nx.path_graph(5, create_using=nx.DiGraph) 

57 >>> sorted(nx.descendants(DG, 2)) 

58 [3, 4] 

59 

60 The `source` node is not a descendant of itself, but can be included manually: 

61 

62 >>> sorted(nx.descendants(DG, 2) | {2}) 

63 [2, 3, 4] 

64 

65 See also 

66 -------- 

67 ancestors 

68 """ 

69 return {child for parent, child in nx.bfs_edges(G, source)} 

70 

71 

72@nx._dispatchable 

73def ancestors(G, source): 

74 """Returns all nodes having a path to `source` in `G`. 

75 

76 Parameters 

77 ---------- 

78 G : NetworkX Graph 

79 source : node in `G` 

80 

81 Returns 

82 ------- 

83 set() 

84 The ancestors of `source` in `G` 

85 

86 Raises 

87 ------ 

88 NetworkXError 

89 If node `source` is not in `G`. 

90 

91 Examples 

92 -------- 

93 >>> DG = nx.path_graph(5, create_using=nx.DiGraph) 

94 >>> sorted(nx.ancestors(DG, 2)) 

95 [0, 1] 

96 

97 The `source` node is not an ancestor of itself, but can be included manually: 

98 

99 >>> sorted(nx.ancestors(DG, 2) | {2}) 

100 [0, 1, 2] 

101 

102 See also 

103 -------- 

104 descendants 

105 """ 

106 return {child for parent, child in nx.bfs_edges(G, source, reverse=True)} 

107 

108 

109@nx._dispatchable 

110def has_cycle(G): 

111 """Decides whether the directed graph has a cycle.""" 

112 try: 

113 # Feed the entire iterator into a zero-length deque. 

114 deque(topological_sort(G), maxlen=0) 

115 except nx.NetworkXUnfeasible: 

116 return True 

117 else: 

118 return False 

119 

120 

121@nx._dispatchable 

122def is_directed_acyclic_graph(G): 

123 """Returns True if the graph `G` is a directed acyclic graph (DAG) or 

124 False if not. 

125 

126 Parameters 

127 ---------- 

128 G : NetworkX graph 

129 

130 Returns 

131 ------- 

132 bool 

133 True if `G` is a DAG, False otherwise 

134 

135 Examples 

136 -------- 

137 Undirected graph:: 

138 

139 >>> G = nx.Graph([(1, 2), (2, 3)]) 

140 >>> nx.is_directed_acyclic_graph(G) 

141 False 

142 

143 Directed graph with cycle:: 

144 

145 >>> G = nx.DiGraph([(1, 2), (2, 3), (3, 1)]) 

146 >>> nx.is_directed_acyclic_graph(G) 

147 False 

148 

149 Directed acyclic graph:: 

150 

151 >>> G = nx.DiGraph([(1, 2), (2, 3)]) 

152 >>> nx.is_directed_acyclic_graph(G) 

153 True 

154 

155 See also 

156 -------- 

157 topological_sort 

158 """ 

159 return G.is_directed() and not has_cycle(G) 

160 

161 

162@nx._dispatchable 

163def topological_generations(G): 

164 """Stratifies a DAG into generations. 

165 

166 A topological generation is node collection in which ancestors of a node in each 

167 generation are guaranteed to be in a previous generation, and any descendants of 

168 a node are guaranteed to be in a following generation. Nodes are guaranteed to 

169 be in the earliest possible generation that they can belong to. 

170 

171 Parameters 

172 ---------- 

173 G : NetworkX digraph 

174 A directed acyclic graph (DAG) 

175 

176 Yields 

177 ------ 

178 sets of nodes 

179 Yields sets of nodes representing each generation. 

180 

181 Raises 

182 ------ 

183 NetworkXError 

184 Generations are defined for directed graphs only. If the graph 

185 `G` is undirected, a :exc:`NetworkXError` is raised. 

186 

187 NetworkXUnfeasible 

188 If `G` is not a directed acyclic graph (DAG) no topological generations 

189 exist and a :exc:`NetworkXUnfeasible` exception is raised. This can also 

190 be raised if `G` is changed while the returned iterator is being processed 

191 

192 RuntimeError 

193 If `G` is changed while the returned iterator is being processed. 

194 

195 Examples 

196 -------- 

197 >>> DG = nx.DiGraph([(2, 1), (3, 1)]) 

198 >>> [sorted(generation) for generation in nx.topological_generations(DG)] 

199 [[2, 3], [1]] 

200 

201 Notes 

202 ----- 

203 The generation in which a node resides can also be determined by taking the 

204 max-path-distance from the node to the farthest leaf node. That value can 

205 be obtained with this function using `enumerate(topological_generations(G))`. 

206 

207 See also 

208 -------- 

209 topological_sort 

210 """ 

211 if not G.is_directed(): 

212 raise nx.NetworkXError("Topological sort not defined on undirected graphs.") 

213 

214 multigraph = G.is_multigraph() 

215 indegree_map = {v: d for v, d in G.in_degree() if d > 0} 

216 zero_indegree = [v for v, d in G.in_degree() if d == 0] 

217 

218 while zero_indegree: 

219 this_generation = zero_indegree 

220 zero_indegree = [] 

221 for node in this_generation: 

222 if node not in G: 

223 raise RuntimeError("Graph changed during iteration") 

224 for child in G.neighbors(node): 

225 try: 

226 indegree_map[child] -= len(G[node][child]) if multigraph else 1 

227 except KeyError as err: 

228 raise RuntimeError("Graph changed during iteration") from err 

229 if indegree_map[child] == 0: 

230 zero_indegree.append(child) 

231 del indegree_map[child] 

232 yield this_generation 

233 

234 if indegree_map: 

235 raise nx.NetworkXUnfeasible( 

236 "Graph contains a cycle or graph changed during iteration" 

237 ) 

238 

239 

240@nx._dispatchable 

241def topological_sort(G): 

242 """Returns a generator of nodes in topologically sorted order. 

243 

244 A topological sort is a nonunique permutation of the nodes of a 

245 directed graph such that an edge from u to v implies that u 

246 appears before v in the topological sort order. This ordering is 

247 valid only if the graph has no directed cycles. 

248 

249 Parameters 

250 ---------- 

251 G : NetworkX digraph 

252 A directed acyclic graph (DAG) 

253 

254 Yields 

255 ------ 

256 nodes 

257 Yields the nodes in topological sorted order. 

258 

259 Raises 

260 ------ 

261 NetworkXError 

262 Topological sort is defined for directed graphs only. If the graph `G` 

263 is undirected, a :exc:`NetworkXError` is raised. 

264 

265 NetworkXUnfeasible 

266 If `G` is not a directed acyclic graph (DAG) no topological sort exists 

267 and a :exc:`NetworkXUnfeasible` exception is raised. This can also be 

268 raised if `G` is changed while the returned iterator is being processed 

269 

270 RuntimeError 

271 If `G` is changed while the returned iterator is being processed. 

272 

273 Examples 

274 -------- 

275 To get the reverse order of the topological sort: 

276 

277 >>> DG = nx.DiGraph([(1, 2), (2, 3)]) 

278 >>> list(reversed(list(nx.topological_sort(DG)))) 

279 [3, 2, 1] 

280 

281 If your DiGraph naturally has the edges representing tasks/inputs 

282 and nodes representing people/processes that initiate tasks, then 

283 topological_sort is not quite what you need. You will have to change 

284 the tasks to nodes with dependence reflected by edges. The result is 

285 a kind of topological sort of the edges. This can be done 

286 with :func:`networkx.line_graph` as follows: 

287 

288 >>> list(nx.topological_sort(nx.line_graph(DG))) 

289 [(1, 2), (2, 3)] 

290 

291 Notes 

292 ----- 

293 This algorithm is based on a description and proof in 

294 "Introduction to Algorithms: A Creative Approach" [1]_ . 

295 

296 See also 

297 -------- 

298 is_directed_acyclic_graph, lexicographical_topological_sort 

299 

300 References 

301 ---------- 

302 .. [1] Manber, U. (1989). 

303 *Introduction to Algorithms - A Creative Approach.* Addison-Wesley. 

304 """ 

305 for generation in nx.topological_generations(G): 

306 yield from generation 

307 

308 

309@nx._dispatchable 

310def lexicographical_topological_sort(G, key=None): 

311 """Generate the nodes in the unique lexicographical topological sort order. 

312 

313 Generates a unique ordering of nodes by first sorting topologically (for which there are often 

314 multiple valid orderings) and then additionally by sorting lexicographically. 

315 

316 A topological sort arranges the nodes of a directed graph so that the 

317 upstream node of each directed edge precedes the downstream node. 

318 It is always possible to find a solution for directed graphs that have no cycles. 

319 There may be more than one valid solution. 

320 

321 Lexicographical sorting is just sorting alphabetically. It is used here to break ties in the 

322 topological sort and to determine a single, unique ordering. This can be useful in comparing 

323 sort results. 

324 

325 The lexicographical order can be customized by providing a function to the `key=` parameter. 

326 The definition of the key function is the same as used in python's built-in `sort()`. 

327 The function takes a single argument and returns a key to use for sorting purposes. 

328 

329 Lexicographical sorting can fail if the node names are un-sortable. See the example below. 

330 The solution is to provide a function to the `key=` argument that returns sortable keys. 

331 

332 

333 Parameters 

334 ---------- 

335 G : NetworkX digraph 

336 A directed acyclic graph (DAG) 

337 

338 key : function, optional 

339 A function of one argument that converts a node name to a comparison key. 

340 It defines and resolves ambiguities in the sort order. Defaults to the identity function. 

341 

342 Yields 

343 ------ 

344 nodes 

345 Yields the nodes of G in lexicographical topological sort order. 

346 

347 Raises 

348 ------ 

349 NetworkXError 

350 Topological sort is defined for directed graphs only. If the graph `G` 

351 is undirected, a :exc:`NetworkXError` is raised. 

352 

353 NetworkXUnfeasible 

354 If `G` is not a directed acyclic graph (DAG) no topological sort exists 

355 and a :exc:`NetworkXUnfeasible` exception is raised. This can also be 

356 raised if `G` is changed while the returned iterator is being processed 

357 

358 RuntimeError 

359 If `G` is changed while the returned iterator is being processed. 

360 

361 TypeError 

362 Results from un-sortable node names. 

363 Consider using `key=` parameter to resolve ambiguities in the sort order. 

364 

365 Examples 

366 -------- 

367 >>> DG = nx.DiGraph([(2, 1), (2, 5), (1, 3), (1, 4), (5, 4)]) 

368 >>> list(nx.lexicographical_topological_sort(DG)) 

369 [2, 1, 3, 5, 4] 

370 >>> list(nx.lexicographical_topological_sort(DG, key=lambda x: -x)) 

371 [2, 5, 1, 4, 3] 

372 

373 The sort will fail for any graph with integer and string nodes. Comparison of integer to strings 

374 is not defined in python. Is 3 greater or less than 'red'? 

375 

376 >>> DG = nx.DiGraph([(1, "red"), (3, "red"), (1, "green"), (2, "blue")]) 

377 >>> list(nx.lexicographical_topological_sort(DG)) 

378 Traceback (most recent call last): 

379 ... 

380 TypeError: '<' not supported between instances of 'str' and 'int' 

381 ... 

382 

383 Incomparable nodes can be resolved using a `key` function. This example function 

384 allows comparison of integers and strings by returning a tuple where the first 

385 element is True for `str`, False otherwise. The second element is the node name. 

386 This groups the strings and integers separately so they can be compared only among themselves. 

387 

388 >>> key = lambda node: (isinstance(node, str), node) 

389 >>> list(nx.lexicographical_topological_sort(DG, key=key)) 

390 [1, 2, 3, 'blue', 'green', 'red'] 

391 

392 Notes 

393 ----- 

394 This algorithm is based on a description and proof in 

395 "Introduction to Algorithms: A Creative Approach" [1]_ . 

396 

397 See also 

398 -------- 

399 topological_sort 

400 

401 References 

402 ---------- 

403 .. [1] Manber, U. (1989). 

404 *Introduction to Algorithms - A Creative Approach.* Addison-Wesley. 

405 """ 

406 if not G.is_directed(): 

407 msg = "Topological sort not defined on undirected graphs." 

408 raise nx.NetworkXError(msg) 

409 

410 if key is None: 

411 

412 def key(node): 

413 return node 

414 

415 nodeid_map = {n: i for i, n in enumerate(G)} 

416 

417 def create_tuple(node): 

418 return key(node), nodeid_map[node], node 

419 

420 indegree_map = {v: d for v, d in G.in_degree() if d > 0} 

421 # These nodes have zero indegree and ready to be returned. 

422 zero_indegree = [create_tuple(v) for v, d in G.in_degree() if d == 0] 

423 heapq.heapify(zero_indegree) 

424 

425 while zero_indegree: 

426 _, _, node = heapq.heappop(zero_indegree) 

427 

428 if node not in G: 

429 raise RuntimeError("Graph changed during iteration") 

430 for _, child in G.edges(node): 

431 try: 

432 indegree_map[child] -= 1 

433 except KeyError as err: 

434 raise RuntimeError("Graph changed during iteration") from err 

435 if indegree_map[child] == 0: 

436 try: 

437 heapq.heappush(zero_indegree, create_tuple(child)) 

438 except TypeError as err: 

439 raise TypeError( 

440 f"{err}\nConsider using `key=` parameter to resolve ambiguities in the sort order." 

441 ) 

442 del indegree_map[child] 

443 

444 yield node 

445 

446 if indegree_map: 

447 msg = "Graph contains a cycle or graph changed during iteration" 

448 raise nx.NetworkXUnfeasible(msg) 

449 

450 

451@not_implemented_for("undirected") 

452@nx._dispatchable 

453def all_topological_sorts(G): 

454 """Returns a generator of _all_ topological sorts of the directed graph G. 

455 

456 A topological sort is a nonunique permutation of the nodes such that an 

457 edge from u to v implies that u appears before v in the topological sort 

458 order. 

459 

460 Parameters 

461 ---------- 

462 G : NetworkX DiGraph 

463 A directed graph 

464 

465 Yields 

466 ------ 

467 topological_sort_order : list 

468 a list of nodes in `G`, representing one of the topological sort orders 

469 

470 Raises 

471 ------ 

472 NetworkXNotImplemented 

473 If `G` is not directed 

474 NetworkXUnfeasible 

475 If `G` is not acyclic 

476 

477 Examples 

478 -------- 

479 To enumerate all topological sorts of directed graph: 

480 

481 >>> DG = nx.DiGraph([(1, 2), (2, 3), (2, 4)]) 

482 >>> list(nx.all_topological_sorts(DG)) 

483 [[1, 2, 4, 3], [1, 2, 3, 4]] 

484 

485 Notes 

486 ----- 

487 Implements an iterative version of the algorithm given in [1]. 

488 

489 References 

490 ---------- 

491 .. [1] Knuth, Donald E., Szwarcfiter, Jayme L. (1974). 

492 "A Structured Program to Generate All Topological Sorting Arrangements" 

493 Information Processing Letters, Volume 2, Issue 6, 1974, Pages 153-157, 

494 ISSN 0020-0190, 

495 https://doi.org/10.1016/0020-0190(74)90001-5. 

496 Elsevier (North-Holland), Amsterdam 

497 """ 

498 if not G.is_directed(): 

499 raise nx.NetworkXError("Topological sort not defined on undirected graphs.") 

500 

501 # the names of count and D are chosen to match the global variables in [1] 

502 # number of edges originating in a vertex v 

503 count = dict(G.in_degree()) 

504 # vertices with indegree 0 

505 D = deque([v for v, d in G.in_degree() if d == 0]) 

506 # stack of first value chosen at a position k in the topological sort 

507 bases = [] 

508 current_sort = [] 

509 

510 # do-while construct 

511 while True: 

512 assert all(count[v] == 0 for v in D) 

513 

514 if len(current_sort) == len(G): 

515 yield list(current_sort) 

516 

517 # clean-up stack 

518 while len(current_sort) > 0: 

519 assert len(bases) == len(current_sort) 

520 q = current_sort.pop() 

521 

522 # "restores" all edges (q, x) 

523 # NOTE: it is important to iterate over edges instead 

524 # of successors, so count is updated correctly in multigraphs 

525 for _, j in G.out_edges(q): 

526 count[j] += 1 

527 assert count[j] >= 0 

528 # remove entries from D 

529 while len(D) > 0 and count[D[-1]] > 0: 

530 D.pop() 

531 

532 # corresponds to a circular shift of the values in D 

533 # if the first value chosen (the base) is in the first 

534 # position of D again, we are done and need to consider the 

535 # previous condition 

536 D.appendleft(q) 

537 if D[-1] == bases[-1]: 

538 # all possible values have been chosen at current position 

539 # remove corresponding marker 

540 bases.pop() 

541 else: 

542 # there are still elements that have not been fixed 

543 # at the current position in the topological sort 

544 # stop removing elements, escape inner loop 

545 break 

546 

547 else: 

548 if len(D) == 0: 

549 raise nx.NetworkXUnfeasible("Graph contains a cycle.") 

550 

551 # choose next node 

552 q = D.pop() 

553 # "erase" all edges (q, x) 

554 # NOTE: it is important to iterate over edges instead 

555 # of successors, so count is updated correctly in multigraphs 

556 for _, j in G.out_edges(q): 

557 count[j] -= 1 

558 assert count[j] >= 0 

559 if count[j] == 0: 

560 D.append(j) 

561 current_sort.append(q) 

562 

563 # base for current position might _not_ be fixed yet 

564 if len(bases) < len(current_sort): 

565 bases.append(q) 

566 

567 if len(bases) == 0: 

568 break 

569 

570 

571@not_implemented_for("undirected") 

572@nx._dispatchable 

573def is_aperiodic(G): 

574 """Determine whether a directed graph is aperiodic. 

575 

576 A strongly connected directed graph is aperiodic if there is no integer ``k > 1`` 

577 that divides the length of every cycle in the graph. 

578 

579 This function requires the graph `G` to be strongly connected and will raise 

580 an error if it's not. For graphs that are not strongly connected, you should 

581 first identify their strongly connected components 

582 (using :func:`~networkx.algorithms.components.strongly_connected_components`) 

583 or attracting components 

584 (using :func:`~networkx.algorithms.components.attracting_components`), 

585 and then apply this function to those individual components. 

586 

587 Parameters 

588 ---------- 

589 G : NetworkX graph 

590 A directed graph. 

591 

592 Returns 

593 ------- 

594 bool 

595 Whether the graph is aperiodic. 

596 

597 Raises 

598 ------ 

599 NetworkXNotImplemented 

600 If `G` is not directed. 

601 NetworkXError 

602 If `G` is not strongly connected. 

603 NetworkXPointlessConcept 

604 If `G` has no nodes. 

605 

606 Examples 

607 -------- 

608 A graph consisting of one cycle, the length of which is 2. Therefore ``k = 2`` 

609 divides the length of every cycle in the graph and thus the graph 

610 is *not aperiodic*:: 

611 

612 >>> DG = nx.DiGraph([(1, 2), (2, 1)]) 

613 >>> nx.is_aperiodic(DG) 

614 False 

615 

616 A graph consisting of two cycles: one of length 2 and the other of length 3. 

617 The cycle lengths are coprime, so there is no single value of k where ``k > 1`` 

618 that divides each cycle length and therefore the graph is *aperiodic*:: 

619 

620 >>> DG = nx.DiGraph([(1, 2), (2, 3), (3, 1), (1, 4), (4, 1)]) 

621 >>> nx.is_aperiodic(DG) 

622 True 

623 

624 A graph created from cycles of the same length can still be aperiodic since 

625 the cycles can overlap and form new cycles of different lengths. For example, 

626 the following graph contains a cycle ``[4, 2, 3, 1]`` of length 4, which is coprime 

627 with the explicitly added cycles of length 3, so the graph is aperiodic:: 

628 

629 >>> DG = nx.DiGraph() 

630 >>> nx.add_cycle(DG, [1, 2, 3]) 

631 >>> nx.add_cycle(DG, [2, 1, 4]) 

632 >>> nx.is_aperiodic(DG) 

633 True 

634 

635 A single-node graph's aperiodicity depends on whether it has a self-loop: 

636 it is aperiodic if a self-loop exists, and periodic otherwise:: 

637 

638 >>> G = nx.DiGraph() 

639 >>> G.add_node(1) 

640 >>> nx.is_aperiodic(G) 

641 False 

642 >>> G.add_edge(1, 1) 

643 >>> nx.is_aperiodic(G) 

644 True 

645 

646 A Markov chain can be modeled as a directed graph, with nodes representing 

647 states and edges representing transitions with non-zero probability. 

648 Aperiodicity is typically considered for irreducible Markov chains, 

649 which are those that are *strongly connected* as graphs. 

650 

651 The following Markov chain is irreducible and aperiodic, and thus 

652 ergodic. It is guaranteed to have a unique stationary distribution:: 

653 

654 >>> G = nx.DiGraph() 

655 >>> nx.add_cycle(G, [1, 2, 3, 4]) 

656 >>> G.add_edge(1, 3) 

657 >>> nx.is_aperiodic(G) 

658 True 

659 

660 Reducible Markov chains can sometimes have a unique stationary distribution. 

661 This occurs if the chain has exactly one closed communicating class and 

662 that class itself is aperiodic (see [1]_). You can use 

663 :func:`~networkx.algorithms.components.attracting_components` 

664 to find these closed communicating classes:: 

665 

666 >>> G = nx.DiGraph([(1, 3), (2, 3)]) 

667 >>> nx.add_cycle(G, [3, 4, 5, 6]) 

668 >>> nx.add_cycle(G, [3, 5, 6]) 

669 >>> communicating_classes = list(nx.strongly_connected_components(G)) 

670 >>> len(communicating_classes) 

671 3 

672 >>> closed_communicating_classes = list(nx.attracting_components(G)) 

673 >>> len(closed_communicating_classes) 

674 1 

675 >>> nx.is_aperiodic(G.subgraph(closed_communicating_classes[0])) 

676 True 

677 

678 Notes 

679 ----- 

680 This uses the method outlined in [1]_, which runs in $O(m)$ time 

681 given $m$ edges in `G`. 

682 

683 References 

684 ---------- 

685 .. [1] Jarvis, J. P.; Shier, D. R. (1996), 

686 "Graph-theoretic analysis of finite Markov chains," 

687 in Shier, D. R.; Wallenius, K. T., Applied Mathematical Modeling: 

688 A Multidisciplinary Approach, CRC Press. 

689 """ 

690 if len(G) == 0: 

691 raise nx.NetworkXPointlessConcept("Graph has no nodes.") 

692 if not nx.is_strongly_connected(G): 

693 raise nx.NetworkXError("Graph is not strongly connected.") 

694 

695 s = nx.utils.arbitrary_element(G) 

696 levels = {s: 0} 

697 g = 0 

698 # There are 3 relevant possible edge types in `dfs_labeled_edges`: 

699 # "forward", "reverse", and "nontree". 

700 for u, v, d in nx.dfs_labeled_edges(G, s): 

701 if d == "forward": 

702 # "forward" edges indicate a new node. 

703 levels[v] = levels[u] + 1 

704 elif d == "nontree" and (g := gcd(g, levels[u] - levels[v] + 1)) == 1: 

705 # "nontree" edges indicate a previously explored node. 

706 # Check whether this affects the common factor of cycle lengths. 

707 return True 

708 # "reverse" edges indicate backtracking in DFS and can be ignored. 

709 return False 

710 

711 

712@nx._dispatchable(preserve_all_attrs=True, returns_graph=True) 

713def transitive_closure(G, reflexive=False): 

714 """Returns transitive closure of a graph 

715 

716 The transitive closure of G = (V,E) is a graph G+ = (V,E+) such that 

717 for all v, w in V there is an edge (v, w) in E+ if and only if there 

718 is a path from v to w in G. 

719 

720 Handling of paths from v to v has some flexibility within this definition. 

721 A reflexive transitive closure creates a self-loop for the path 

722 from v to v of length 0. The usual transitive closure creates a 

723 self-loop only if a cycle exists (a path from v to v with length > 0). 

724 We also allow an option for no self-loops. 

725 

726 Parameters 

727 ---------- 

728 G : NetworkX Graph 

729 A directed/undirected graph/multigraph. 

730 reflexive : Bool or None, optional (default: False) 

731 Determines when cycles create self-loops in the Transitive Closure. 

732 If True, trivial cycles (length 0) create self-loops. The result 

733 is a reflexive transitive closure of G. 

734 If False (the default) non-trivial cycles create self-loops. 

735 If None, self-loops are not created. 

736 

737 Returns 

738 ------- 

739 NetworkX graph 

740 The transitive closure of `G` 

741 

742 Raises 

743 ------ 

744 NetworkXError 

745 If `reflexive` not in `{None, True, False}` 

746 

747 Examples 

748 -------- 

749 The treatment of trivial (i.e. length 0) cycles is controlled by the 

750 `reflexive` parameter. 

751 

752 Trivial (i.e. length 0) cycles do not create self-loops when 

753 ``reflexive=False`` (the default):: 

754 

755 >>> DG = nx.DiGraph([(1, 2), (2, 3)]) 

756 >>> TC = nx.transitive_closure(DG, reflexive=False) 

757 >>> TC.edges() 

758 OutEdgeView([(1, 2), (1, 3), (2, 3)]) 

759 

760 However, nontrivial (i.e. length greater than 0) cycles create self-loops 

761 when ``reflexive=False`` (the default):: 

762 

763 >>> DG = nx.DiGraph([(1, 2), (2, 3), (3, 1)]) 

764 >>> TC = nx.transitive_closure(DG, reflexive=False) 

765 >>> TC.edges() 

766 OutEdgeView([(1, 2), (1, 3), (1, 1), (2, 3), (2, 1), (2, 2), (3, 1), (3, 2), (3, 3)]) 

767 

768 Trivial cycles (length 0) create self-loops when ``reflexive=True``:: 

769 

770 >>> DG = nx.DiGraph([(1, 2), (2, 3)]) 

771 >>> TC = nx.transitive_closure(DG, reflexive=True) 

772 >>> TC.edges() 

773 OutEdgeView([(1, 2), (1, 1), (1, 3), (2, 3), (2, 2), (3, 3)]) 

774 

775 And the third option is not to create self-loops at all when ``reflexive=None``:: 

776 

777 >>> DG = nx.DiGraph([(1, 2), (2, 3), (3, 1)]) 

778 >>> TC = nx.transitive_closure(DG, reflexive=None) 

779 >>> TC.edges() 

780 OutEdgeView([(1, 2), (1, 3), (2, 3), (2, 1), (3, 1), (3, 2)]) 

781 

782 References 

783 ---------- 

784 .. [1] https://www.ics.uci.edu/~eppstein/PADS/PartialOrder.py 

785 """ 

786 TC = G.copy() 

787 

788 if reflexive not in {None, True, False}: 

789 raise nx.NetworkXError("Incorrect value for the parameter `reflexive`") 

790 

791 for v in G: 

792 if reflexive is None: 

793 TC.add_edges_from((v, u) for u in nx.descendants(G, v) if u not in TC[v]) 

794 elif reflexive is True: 

795 TC.add_edges_from( 

796 (v, u) for u in nx.descendants(G, v) | {v} if u not in TC[v] 

797 ) 

798 elif reflexive is False: 

799 TC.add_edges_from((v, e[1]) for e in nx.edge_bfs(G, v) if e[1] not in TC[v]) 

800 

801 return TC 

802 

803 

804@not_implemented_for("undirected") 

805@nx._dispatchable(preserve_all_attrs=True, returns_graph=True) 

806def transitive_closure_dag(G, topo_order=None): 

807 """Returns the transitive closure of a directed acyclic graph. 

808 

809 This function is faster than the function `transitive_closure`, but fails 

810 if the graph has a cycle. 

811 

812 The transitive closure of G = (V,E) is a graph G+ = (V,E+) such that 

813 for all v, w in V there is an edge (v, w) in E+ if and only if there 

814 is a non-null path from v to w in G. 

815 

816 Parameters 

817 ---------- 

818 G : NetworkX DiGraph 

819 A directed acyclic graph (DAG) 

820 

821 topo_order: list or tuple, optional 

822 A topological order for G (if None, the function will compute one) 

823 

824 Returns 

825 ------- 

826 NetworkX DiGraph 

827 The transitive closure of `G` 

828 

829 Raises 

830 ------ 

831 NetworkXNotImplemented 

832 If `G` is not directed 

833 NetworkXUnfeasible 

834 If `G` has a cycle 

835 

836 Examples 

837 -------- 

838 >>> DG = nx.DiGraph([(1, 2), (2, 3)]) 

839 >>> TC = nx.transitive_closure_dag(DG) 

840 >>> TC.edges() 

841 OutEdgeView([(1, 2), (1, 3), (2, 3)]) 

842 

843 Notes 

844 ----- 

845 This algorithm is probably simple enough to be well-known but I didn't find 

846 a mention in the literature. 

847 """ 

848 if topo_order is None: 

849 topo_order = list(topological_sort(G)) 

850 

851 TC = G.copy() 

852 

853 # idea: traverse vertices following a reverse topological order, connecting 

854 # each vertex to its descendants at distance 2 as we go 

855 for v in reversed(topo_order): 

856 TC.add_edges_from((v, u) for u in nx.descendants_at_distance(TC, v, 2)) 

857 

858 return TC 

859 

860 

861@not_implemented_for("undirected") 

862@nx._dispatchable(returns_graph=True) 

863def transitive_reduction(G): 

864 """Returns transitive reduction of a directed graph 

865 

866 The transitive reduction of G = (V,E) is a graph G- = (V,E-) such that 

867 for all v,w in V there is an edge (v,w) in E- if and only if (v,w) is 

868 in E and there is no path from v to w in G with length greater than 1. 

869 

870 Parameters 

871 ---------- 

872 G : NetworkX DiGraph 

873 A directed acyclic graph (DAG) 

874 

875 Returns 

876 ------- 

877 NetworkX DiGraph 

878 The transitive reduction of `G` 

879 

880 Raises 

881 ------ 

882 NetworkXError 

883 If `G` is not a directed acyclic graph (DAG) transitive reduction is 

884 not uniquely defined and a :exc:`NetworkXError` exception is raised. 

885 

886 Examples 

887 -------- 

888 To perform transitive reduction on a DiGraph: 

889 

890 >>> DG = nx.DiGraph([(1, 2), (2, 3), (1, 3)]) 

891 >>> TR = nx.transitive_reduction(DG) 

892 >>> list(TR.edges) 

893 [(1, 2), (2, 3)] 

894 

895 To avoid unnecessary data copies, this implementation does not return a 

896 DiGraph with node/edge data. 

897 To perform transitive reduction on a DiGraph and transfer node/edge data: 

898 

899 >>> DG = nx.DiGraph() 

900 >>> DG.add_edges_from([(1, 2), (2, 3), (1, 3)], color="red") 

901 >>> TR = nx.transitive_reduction(DG) 

902 >>> TR.add_nodes_from(DG.nodes(data=True)) 

903 >>> TR.add_edges_from((u, v, DG.edges[u, v]) for u, v in TR.edges) 

904 >>> list(TR.edges(data=True)) 

905 [(1, 2, {'color': 'red'}), (2, 3, {'color': 'red'})] 

906 

907 References 

908 ---------- 

909 https://en.wikipedia.org/wiki/Transitive_reduction 

910 

911 """ 

912 if not is_directed_acyclic_graph(G): 

913 msg = "Directed Acyclic Graph required for transitive_reduction" 

914 raise nx.NetworkXError(msg) 

915 TR = nx.DiGraph() 

916 TR.add_nodes_from(G.nodes()) 

917 descendants = {} 

918 # count before removing set stored in descendants 

919 check_count = dict(G.in_degree) 

920 for u in G: 

921 u_nbrs = set(G[u]) 

922 for v in G[u]: 

923 if v in u_nbrs: 

924 if v not in descendants: 

925 descendants[v] = {y for x, y in nx.dfs_edges(G, v)} 

926 u_nbrs -= descendants[v] 

927 check_count[v] -= 1 

928 if check_count[v] == 0: 

929 del descendants[v] 

930 TR.add_edges_from((u, v) for v in u_nbrs) 

931 return TR 

932 

933 

934@not_implemented_for("undirected") 

935@nx._dispatchable 

936def antichains(G, topo_order=None): 

937 """Generates antichains from a directed acyclic graph (DAG). 

938 

939 An antichain is a subset of a partially ordered set such that any 

940 two elements in the subset are incomparable. 

941 

942 Parameters 

943 ---------- 

944 G : NetworkX DiGraph 

945 A directed acyclic graph (DAG) 

946 

947 topo_order: list or tuple, optional 

948 A topological order for G (if None, the function will compute one) 

949 

950 Yields 

951 ------ 

952 antichain : list 

953 a list of nodes in `G` representing an antichain 

954 

955 Raises 

956 ------ 

957 NetworkXNotImplemented 

958 If `G` is not directed 

959 

960 NetworkXUnfeasible 

961 If `G` contains a cycle 

962 

963 Examples 

964 -------- 

965 >>> DG = nx.DiGraph([(1, 2), (1, 3)]) 

966 >>> list(nx.antichains(DG)) 

967 [[], [3], [2], [2, 3], [1]] 

968 

969 Notes 

970 ----- 

971 This function was originally developed by Peter Jipsen and Franco Saliola 

972 for the SAGE project. It's included in NetworkX with permission from the 

973 authors. Original SAGE code at: 

974 

975 https://github.com/sagemath/sage/blob/master/src/sage/combinat/posets/hasse_diagram.py 

976 

977 References 

978 ---------- 

979 .. [1] Free Lattices, by R. Freese, J. Jezek and J. B. Nation, 

980 AMS, Vol 42, 1995, p. 226. 

981 """ 

982 if topo_order is None: 

983 topo_order = list(nx.topological_sort(G)) 

984 

985 TC = nx.transitive_closure_dag(G, topo_order) 

986 antichains_stacks = [([], list(reversed(topo_order)))] 

987 

988 while antichains_stacks: 

989 (antichain, stack) = antichains_stacks.pop() 

990 # Invariant: 

991 # - the elements of antichain are independent 

992 # - the elements of stack are independent from those of antichain 

993 yield antichain 

994 while stack: 

995 x = stack.pop() 

996 new_antichain = antichain + [x] 

997 new_stack = [t for t in stack if not ((t in TC[x]) or (x in TC[t]))] 

998 antichains_stacks.append((new_antichain, new_stack)) 

999 

1000 

1001@not_implemented_for("undirected") 

1002@nx._dispatchable(edge_attrs={"weight": "default_weight"}) 

1003def dag_longest_path(G, weight="weight", default_weight=1, topo_order=None): 

1004 """Returns the longest path in a directed acyclic graph (DAG). 

1005 

1006 If `G` has edges with `weight` attribute the edge data are used as 

1007 weight values. 

1008 

1009 Parameters 

1010 ---------- 

1011 G : NetworkX DiGraph 

1012 A directed acyclic graph (DAG) 

1013 

1014 weight : str, optional 

1015 Edge data key to use for weight 

1016 

1017 default_weight : int, optional 

1018 The weight of edges that do not have a weight attribute 

1019 

1020 topo_order: list or tuple, optional 

1021 A topological order for `G` (if None, the function will compute one) 

1022 

1023 Returns 

1024 ------- 

1025 list 

1026 Longest path 

1027 

1028 Raises 

1029 ------ 

1030 NetworkXNotImplemented 

1031 If `G` is not directed 

1032 

1033 Examples 

1034 -------- 

1035 >>> DG = nx.DiGraph( 

1036 ... [(0, 1, {"cost": 1}), (1, 2, {"cost": 1}), (0, 2, {"cost": 42})] 

1037 ... ) 

1038 >>> list(nx.all_simple_paths(DG, 0, 2)) 

1039 [[0, 1, 2], [0, 2]] 

1040 >>> nx.dag_longest_path(DG) 

1041 [0, 1, 2] 

1042 >>> nx.dag_longest_path(DG, weight="cost") 

1043 [0, 2] 

1044 

1045 In the case where multiple valid topological orderings exist, `topo_order` 

1046 can be used to specify a specific ordering: 

1047 

1048 >>> DG = nx.DiGraph([(0, 1), (0, 2)]) 

1049 >>> sorted(nx.all_topological_sorts(DG)) # Valid topological orderings 

1050 [[0, 1, 2], [0, 2, 1]] 

1051 >>> nx.dag_longest_path(DG, topo_order=[0, 1, 2]) 

1052 [0, 1] 

1053 >>> nx.dag_longest_path(DG, topo_order=[0, 2, 1]) 

1054 [0, 2] 

1055 

1056 See also 

1057 -------- 

1058 dag_longest_path_length 

1059 

1060 """ 

1061 if not G: 

1062 return [] 

1063 

1064 if topo_order is None: 

1065 topo_order = nx.topological_sort(G) 

1066 

1067 dist = {} # stores {v : (length, u)} 

1068 for v in topo_order: 

1069 us = [ 

1070 ( 

1071 dist[u][0] 

1072 + ( 

1073 max(data.values(), key=lambda x: x.get(weight, default_weight)) 

1074 if G.is_multigraph() 

1075 else data 

1076 ).get(weight, default_weight), 

1077 u, 

1078 ) 

1079 for u, data in G.pred[v].items() 

1080 ] 

1081 

1082 # Use the best predecessor if there is one and its distance is 

1083 # non-negative, otherwise terminate. 

1084 maxu = max(us, key=lambda x: x[0]) if us else (0, v) 

1085 dist[v] = maxu if maxu[0] >= 0 else (0, v) 

1086 

1087 u = None 

1088 v = max(dist, key=lambda x: dist[x][0]) 

1089 path = [] 

1090 while u != v: 

1091 path.append(v) 

1092 u = v 

1093 v = dist[v][1] 

1094 

1095 path.reverse() 

1096 return path 

1097 

1098 

1099@not_implemented_for("undirected") 

1100@nx._dispatchable(edge_attrs={"weight": "default_weight"}) 

1101def dag_longest_path_length(G, weight="weight", default_weight=1): 

1102 """Returns the longest path length in a DAG 

1103 

1104 Parameters 

1105 ---------- 

1106 G : NetworkX DiGraph 

1107 A directed acyclic graph (DAG) 

1108 

1109 weight : string, optional 

1110 Edge data key to use for weight 

1111 

1112 default_weight : int, optional 

1113 The weight of edges that do not have a weight attribute 

1114 

1115 Returns 

1116 ------- 

1117 int 

1118 Longest path length 

1119 

1120 Raises 

1121 ------ 

1122 NetworkXNotImplemented 

1123 If `G` is not directed 

1124 

1125 Examples 

1126 -------- 

1127 >>> DG = nx.DiGraph( 

1128 ... [(0, 1, {"cost": 1}), (1, 2, {"cost": 1}), (0, 2, {"cost": 42})] 

1129 ... ) 

1130 >>> list(nx.all_simple_paths(DG, 0, 2)) 

1131 [[0, 1, 2], [0, 2]] 

1132 >>> nx.dag_longest_path_length(DG) 

1133 2 

1134 >>> nx.dag_longest_path_length(DG, weight="cost") 

1135 42 

1136 

1137 See also 

1138 -------- 

1139 dag_longest_path 

1140 """ 

1141 path = nx.dag_longest_path(G, weight, default_weight) 

1142 path_length = 0 

1143 if G.is_multigraph(): 

1144 for u, v in pairwise(path): 

1145 i = max(G[u][v], key=lambda x: G[u][v][x].get(weight, default_weight)) 

1146 path_length += G[u][v][i].get(weight, default_weight) 

1147 else: 

1148 for u, v in pairwise(path): 

1149 path_length += G[u][v].get(weight, default_weight) 

1150 

1151 return path_length 

1152 

1153 

1154@nx._dispatchable 

1155def root_to_leaf_paths(G): 

1156 """Yields root-to-leaf paths in a directed acyclic graph. 

1157 

1158 `G` must be a directed acyclic graph. If not, the behavior of this 

1159 function is undefined. A "root" in this graph is a node of in-degree 

1160 zero and a "leaf" a node of out-degree zero. 

1161 

1162 When invoked, this function iterates over each path from any root to 

1163 any leaf. A path is a list of nodes. 

1164 

1165 """ 

1166 roots = (v for v, d in G.in_degree() if d == 0) 

1167 leaves = (v for v, d in G.out_degree() if d == 0) 

1168 

1169 for root, leaf in product(roots, leaves): 

1170 yield from nx.all_simple_paths(G, root, leaf) 

1171 

1172 

1173@not_implemented_for("multigraph") 

1174@not_implemented_for("undirected") 

1175@nx._dispatchable(returns_graph=True) 

1176def dag_to_branching(G): 

1177 """Returns a branching representing all (overlapping) paths from 

1178 root nodes to leaf nodes in the given directed acyclic graph. 

1179 

1180 As described in :mod:`networkx.algorithms.tree.recognition`, a 

1181 *branching* is a directed forest in which each node has at most one 

1182 parent. In other words, a branching is a disjoint union of 

1183 *arborescences*. For this function, each node of in-degree zero in 

1184 `G` becomes a root of one of the arborescences, and there will be 

1185 one leaf node for each distinct path from that root to a leaf node 

1186 in `G`. 

1187 

1188 Each node `v` in `G` with *k* parents becomes *k* distinct nodes in 

1189 the returned branching, one for each parent, and the sub-DAG rooted 

1190 at `v` is duplicated for each copy. The algorithm then recurses on 

1191 the children of each copy of `v`. 

1192 

1193 Parameters 

1194 ---------- 

1195 G : NetworkX graph 

1196 A directed acyclic graph. 

1197 

1198 Returns 

1199 ------- 

1200 DiGraph 

1201 The branching in which there is a bijection between root-to-leaf 

1202 paths in `G` (in which multiple paths may share the same leaf) 

1203 and root-to-leaf paths in the branching (in which there is a 

1204 unique path from a root to a leaf). 

1205 

1206 Each node has an attribute 'source' whose value is the original 

1207 node to which this node corresponds. No other graph, node, or 

1208 edge attributes are copied into this new graph. 

1209 

1210 Raises 

1211 ------ 

1212 NetworkXNotImplemented 

1213 If `G` is not directed, or if `G` is a multigraph. 

1214 

1215 HasACycle 

1216 If `G` is not acyclic. 

1217 

1218 Examples 

1219 -------- 

1220 To examine which nodes in the returned branching were produced by 

1221 which original node in the directed acyclic graph, we can collect 

1222 the mapping from source node to new nodes into a dictionary. For 

1223 example, consider the directed diamond graph:: 

1224 

1225 >>> from collections import defaultdict 

1226 >>> from operator import itemgetter 

1227 >>> 

1228 >>> G = nx.DiGraph(nx.utils.pairwise("abd")) 

1229 >>> G.add_edges_from(nx.utils.pairwise("acd")) 

1230 >>> B = nx.dag_to_branching(G) 

1231 >>> 

1232 >>> sources = defaultdict(set) 

1233 >>> for v, source in B.nodes(data="source"): 

1234 ... sources[source].add(v) 

1235 >>> len(sources["a"]) 

1236 1 

1237 >>> len(sources["d"]) 

1238 2 

1239 

1240 To copy node attributes from the original graph to the new graph, 

1241 you can use a dictionary like the one constructed in the above 

1242 example:: 

1243 

1244 >>> for source, nodes in sources.items(): 

1245 ... for v in nodes: 

1246 ... B.nodes[v].update(G.nodes[source]) 

1247 

1248 Notes 

1249 ----- 

1250 This function is not idempotent in the sense that the node labels in 

1251 the returned branching may be uniquely generated each time the 

1252 function is invoked. In fact, the node labels may not be integers; 

1253 in order to relabel the nodes to be more readable, you can use the 

1254 :func:`networkx.convert_node_labels_to_integers` function. 

1255 

1256 The current implementation of this function uses 

1257 :func:`networkx.prefix_tree`, so it is subject to the limitations of 

1258 that function. 

1259 

1260 """ 

1261 if has_cycle(G): 

1262 msg = "dag_to_branching is only defined for acyclic graphs" 

1263 raise nx.HasACycle(msg) 

1264 paths = root_to_leaf_paths(G) 

1265 B = nx.prefix_tree(paths) 

1266 # Remove the synthetic `root`(0) and `NIL`(-1) nodes from the tree 

1267 B.remove_node(0) 

1268 B.remove_node(-1) 

1269 return B 

1270 

1271 

1272@not_implemented_for("undirected") 

1273@nx._dispatchable 

1274def v_structures(G): 

1275 """Yields 3-node tuples that represent the v-structures in `G`. 

1276 

1277 Colliders are triples in the directed acyclic graph (DAG) where two parent nodes 

1278 point to the same child node. V-structures are colliders where the two parent 

1279 nodes are not adjacent. In a causal graph setting, the parents do not directly 

1280 depend on each other, but conditioning on the child node provides an association. 

1281 

1282 Parameters 

1283 ---------- 

1284 G : graph 

1285 A networkx `~networkx.DiGraph`. 

1286 

1287 Yields 

1288 ------ 

1289 A 3-tuple representation of a v-structure 

1290 Each v-structure is a 3-tuple with the parent, collider, and other parent. 

1291 

1292 Raises 

1293 ------ 

1294 NetworkXNotImplemented 

1295 If `G` is an undirected graph. 

1296 

1297 Examples 

1298 -------- 

1299 >>> G = nx.DiGraph([(1, 2), (0, 4), (3, 1), (2, 4), (0, 5), (4, 5), (1, 5)]) 

1300 >>> nx.is_directed_acyclic_graph(G) 

1301 True 

1302 >>> list(nx.dag.v_structures(G)) 

1303 [(0, 4, 2), (0, 5, 1), (4, 5, 1)] 

1304 

1305 See Also 

1306 -------- 

1307 colliders 

1308 

1309 Notes 

1310 ----- 

1311 This function was written to be used on DAGs, however it works on cyclic graphs 

1312 too. Since colliders are referred to in the cyclic causal graph literature 

1313 [2]_ we allow cyclic graphs in this function. It is suggested that you test if 

1314 your input graph is acyclic as in the example if you want that property. 

1315 

1316 References 

1317 ---------- 

1318 .. [1] `Pearl's PRIMER <https://bayes.cs.ucla.edu/PRIMER/primer-ch2.pdf>`_ 

1319 Ch-2 page 50: v-structures def. 

1320 .. [2] A Hyttinen, P.O. Hoyer, F. Eberhardt, M J ̈arvisalo, (2013) 

1321 "Discovering cyclic causal models with latent variables: 

1322 a general SAT-based procedure", UAI'13: Proceedings of the Twenty-Ninth 

1323 Conference on Uncertainty in Artificial Intelligence, pg 301–310, 

1324 `doi:10.5555/3023638.3023669 <https://dl.acm.org/doi/10.5555/3023638.3023669>`_ 

1325 """ 

1326 for p1, c, p2 in colliders(G): 

1327 if not (G.has_edge(p1, p2) or G.has_edge(p2, p1)): 

1328 yield (p1, c, p2) 

1329 

1330 

1331@not_implemented_for("undirected") 

1332@nx._dispatchable 

1333def colliders(G): 

1334 """Yields 3-node tuples that represent the colliders in `G`. 

1335 

1336 In a Directed Acyclic Graph (DAG), if you have three nodes A, B, and C, and 

1337 there are edges from A to C and from B to C, then C is a collider [1]_ . In 

1338 a causal graph setting, this means that both events A and B are "causing" C, 

1339 and conditioning on C provide an association between A and B even if 

1340 no direct causal relationship exists between A and B. 

1341 

1342 Parameters 

1343 ---------- 

1344 G : graph 

1345 A networkx `~networkx.DiGraph`. 

1346 

1347 Yields 

1348 ------ 

1349 A 3-tuple representation of a collider 

1350 Each collider is a 3-tuple with the parent, collider, and other parent. 

1351 

1352 Raises 

1353 ------ 

1354 NetworkXNotImplemented 

1355 If `G` is an undirected graph. 

1356 

1357 Examples 

1358 -------- 

1359 >>> G = nx.DiGraph([(1, 2), (0, 4), (3, 1), (2, 4), (0, 5), (4, 5), (1, 5)]) 

1360 >>> nx.is_directed_acyclic_graph(G) 

1361 True 

1362 >>> list(nx.dag.colliders(G)) 

1363 [(0, 4, 2), (0, 5, 4), (0, 5, 1), (4, 5, 1)] 

1364 

1365 See Also 

1366 -------- 

1367 v_structures 

1368 

1369 Notes 

1370 ----- 

1371 This function was written to be used on DAGs, however it works on cyclic graphs 

1372 too. Since colliders are referred to in the cyclic causal graph literature 

1373 [2]_ we allow cyclic graphs in this function. It is suggested that you test if 

1374 your input graph is acyclic as in the example if you want that property. 

1375 

1376 References 

1377 ---------- 

1378 .. [1] `Wikipedia: Collider in causal graphs <https://en.wikipedia.org/wiki/Collider_(statistics)>`_ 

1379 .. [2] A Hyttinen, P.O. Hoyer, F. Eberhardt, M J ̈arvisalo, (2013) 

1380 "Discovering cyclic causal models with latent variables: 

1381 a general SAT-based procedure", UAI'13: Proceedings of the Twenty-Ninth 

1382 Conference on Uncertainty in Artificial Intelligence, pg 301–310, 

1383 `doi:10.5555/3023638.3023669 <https://dl.acm.org/doi/10.5555/3023638.3023669>`_ 

1384 """ 

1385 for node in G.nodes: 

1386 for p1, p2 in combinations(G.predecessors(node), 2): 

1387 yield (p1, node, p2)