1"""
2Algorithm for testing d-separation in DAGs.
3
4*d-separation* is a test for conditional independence in probability
5distributions that can be factorized using DAGs. It is a purely
6graphical test that uses the underlying graph and makes no reference
7to the actual distribution parameters. See [1]_ for a formal
8definition.
9
10The implementation is based on the conceptually simple linear time
11algorithm presented in [2]_. Refer to [3]_, [4]_ for a couple of
12alternative algorithms.
13
14The functional interface in NetworkX consists of three functions:
15
16- `find_minimal_d_separator` returns a minimal d-separator set ``z``.
17 That is, removing any node or nodes from it makes it no longer a d-separator.
18- `is_d_separator` checks if a given set is a d-separator.
19- `is_minimal_d_separator` checks if a given set is a minimal d-separator.
20
21D-separators
22------------
23
24Here, we provide a brief overview of d-separation and related concepts that
25are relevant for understanding it:
26
27The ideas of d-separation and d-connection relate to paths being open or blocked.
28
29- A "path" is a sequence of nodes connected in order by edges. Unlike for most
30 graph theory analysis, the direction of the edges is ignored. Thus the path
31 can be thought of as a traditional path on the undirected version of the graph.
32- A "candidate d-separator" ``z`` is a set of nodes being considered as
33 possibly blocking all paths between two prescribed sets ``x`` and ``y`` of nodes.
34 We refer to each node in the candidate d-separator as "known".
35- A "collider" node on a path is a node that is a successor of its two neighbor
36 nodes on the path. That is, ``c`` is a collider if the edge directions
37 along the path look like ``... u -> c <- v ...``.
38- If a collider node or any of its descendants are "known", the collider
39 is called an "open collider". Otherwise it is a "blocking collider".
40- Any path can be "blocked" in two ways. If the path contains a "known" node
41 that is not a collider, the path is blocked. Also, if the path contains a
42 collider that is not a "known" node, the path is blocked.
43- A path is "open" if it is not blocked. That is, it is open if every node is
44 either an open collider or not a "known". Said another way, every
45 "known" in the path is a collider and every collider is open (has a
46 "known" as a inclusive descendant). The concept of "open path" is meant to
47 demonstrate a probabilistic conditional dependence between two nodes given
48 prescribed knowledge ("known" nodes).
49- Two sets ``x`` and ``y`` of nodes are "d-separated" by a set of nodes ``z``
50 if all paths between nodes in ``x`` and nodes in ``y`` are blocked. That is,
51 if there are no open paths from any node in ``x`` to any node in ``y``.
52 Such a set ``z`` is a "d-separator" of ``x`` and ``y``.
53- A "minimal d-separator" is a d-separator ``z`` for which no node or subset
54 of nodes can be removed with it still being a d-separator.
55
56The d-separator blocks some paths between ``x`` and ``y`` but opens others.
57Nodes in the d-separator block paths if the nodes are not colliders.
58But if a collider or its descendant nodes are in the d-separation set, the
59colliders are open, allowing a path through that collider.
60
61Illustration of D-separation with examples
62------------------------------------------
63
64A pair of two nodes, ``u`` and ``v``, are d-connected if there is a path
65from ``u`` to ``v`` that is not blocked. That means, there is an open
66path from ``u`` to ``v``.
67
68For example, if the d-separating set is the empty set, then the following paths are
69open between ``u`` and ``v``:
70
71- u <- n -> v
72- u -> w -> ... -> n -> v
73
74If on the other hand, ``n`` is in the d-separating set, then ``n`` blocks
75those paths between ``u`` and ``v``.
76
77Colliders block a path if they and their descendants are not included
78in the d-separating set. An example of a path that is blocked when the
79d-separating set is empty is:
80
81- u -> w -> ... -> n <- v
82
83The node ``n`` is a collider in this path and is not in the d-separating set.
84So ``n`` blocks this path. However, if ``n`` or a descendant of ``n`` is
85included in the d-separating set, then the path through the collider
86at ``n`` (... -> n <- ...) is "open".
87
88D-separation is concerned with blocking all paths between nodes from ``x`` to ``y``.
89A d-separating set between ``x`` and ``y`` is one where all paths are blocked.
90
91D-separation and its applications in probability
92------------------------------------------------
93
94D-separation is commonly used in probabilistic causal-graph models. D-separation
95connects the idea of probabilistic "dependence" with separation in a graph. If
96one assumes the causal Markov condition [5]_, (every node is conditionally
97independent of its non-descendants, given its parents) then d-separation implies
98conditional independence in probability distributions.
99Symmetrically, d-connection implies dependence.
100
101The intuition is as follows. The edges on a causal graph indicate which nodes
102influence the outcome of other nodes directly. An edge from u to v
103implies that the outcome of event ``u`` influences the probabilities for
104the outcome of event ``v``. Certainly knowing ``u`` changes predictions for ``v``.
105But also knowing ``v`` changes predictions for ``u``. The outcomes are dependent.
106Furthermore, an edge from ``v`` to ``w`` would mean that ``w`` and ``v`` are dependent
107and thus that ``u`` could indirectly influence ``w``.
108
109Without any knowledge about the system (candidate d-separating set is empty)
110a causal graph ``u -> v -> w`` allows all three nodes to be dependent. But
111if we know the outcome of ``v``, the conditional probabilities of outcomes for
112``u`` and ``w`` are independent of each other. That is, once we know the outcome
113for ``v``, the probabilities for ``w`` do not depend on the outcome for ``u``.
114This is the idea behind ``v`` blocking the path if it is "known" (in the candidate
115d-separating set).
116
117The same argument works whether the direction of the edges are both
118left-going and when both arrows head out from the middle. Having a "known"
119node on a path blocks the collider-free path because those relationships
120make the conditional probabilities independent.
121
122The direction of the causal edges does impact dependence precisely in the
123case of a collider e.g. ``u -> v <- w``. In that situation, both ``u`` and ``w``
124influence ``v``. But they do not directly influence each other. So without any
125knowledge of any outcomes, ``u`` and ``w`` are independent. That is the idea behind
126colliders blocking the path. But, if ``v`` is known, the conditional probabilities
127of ``u`` and ``w`` can be dependent. This is the heart of Berkson's Paradox [6]_.
128For example, suppose ``u`` and ``w`` are boolean events (they either happen or do not)
129and ``v`` represents the outcome "at least one of ``u`` and ``w`` occur". Then knowing
130``v`` is true makes the conditional probabilities of ``u`` and ``w`` dependent.
131Essentially, knowing that at least one of them is true raises the probability of
132each. But further knowledge that ``w`` is true (or false) change the conditional
133probability of ``u`` to either the original value or 1. So the conditional
134probability of ``u`` depends on the outcome of ``w`` even though there is no
135causal relationship between them. When a collider is known, dependence can
136occur across paths through that collider. This is the reason open colliders
137do not block paths.
138
139Furthermore, even if ``v`` is not "known", if one of its descendants is "known"
140we can use that information to know more about ``v`` which again makes
141``u`` and ``w`` potentially dependent. Suppose the chance of ``n`` occurring
142is much higher when ``v`` occurs ("at least one of ``u`` and ``w`` occur").
143Then if we know ``n`` occurred, it is more likely that ``v`` occurred and that
144makes the chance of ``u`` and ``w`` dependent. This is the idea behind why
145a collider does no block a path if any descendant of the collider is "known".
146
147When two sets of nodes ``x`` and ``y`` are d-separated by a set ``z``,
148it means that given the outcomes of the nodes in ``z``, the probabilities
149of outcomes of the nodes in ``x`` are independent of the outcomes of the
150nodes in ``y`` and vice versa.
151
152Examples
153--------
154A Hidden Markov Model with 5 observed states and 5 hidden states
155where the hidden states have causal relationships resulting in
156a path results in the following causal network. We check that
157early states along the path are separated from late state in
158the path by the d-separator of the middle hidden state.
159Thus if we condition on the middle hidden state, the early
160state probabilities are independent of the late state outcomes.
161
162>>> G = nx.DiGraph()
163>>> G.add_edges_from(
164... [
165... ("H1", "H2"),
166... ("H2", "H3"),
167... ("H3", "H4"),
168... ("H4", "H5"),
169... ("H1", "O1"),
170... ("H2", "O2"),
171... ("H3", "O3"),
172... ("H4", "O4"),
173... ("H5", "O5"),
174... ]
175... )
176>>> x, y, z = ({"H1", "O1"}, {"H5", "O5"}, {"H3"})
177>>> nx.is_d_separator(G, x, y, z)
178True
179>>> nx.is_minimal_d_separator(G, x, y, z)
180True
181>>> nx.is_minimal_d_separator(G, x, y, z | {"O3"})
182False
183>>> z = nx.find_minimal_d_separator(G, x | y, {"O2", "O3", "O4"})
184>>> z == {"H2", "H4"}
185True
186
187If no minimal_d_separator exists, `None` is returned
188
189>>> other_z = nx.find_minimal_d_separator(G, x | y, {"H2", "H3"})
190>>> other_z is None
191True
192
193
194References
195----------
196
197.. [1] Pearl, J. (2009). Causality. Cambridge: Cambridge University Press.
198
199.. [2] Darwiche, A. (2009). Modeling and reasoning with Bayesian networks.
200 Cambridge: Cambridge University Press.
201
202.. [3] Shachter, Ross D. "Bayes-ball: The rational pastime (for
203 determining irrelevance and requisite information in belief networks
204 and influence diagrams)." In Proceedings of the Fourteenth Conference
205 on Uncertainty in Artificial Intelligence (UAI), (pp. 480–487). 1998.
206
207.. [4] Koller, D., & Friedman, N. (2009).
208 Probabilistic graphical models: principles and techniques. The MIT Press.
209
210.. [5] https://en.wikipedia.org/wiki/Causal_Markov_condition
211
212.. [6] https://en.wikipedia.org/wiki/Berkson%27s_paradox
213
214"""
215
216from collections import deque
217from itertools import chain
218
219import networkx as nx
220from networkx.utils import UnionFind, not_implemented_for
221
222__all__ = [
223 "is_d_separator",
224 "is_minimal_d_separator",
225 "find_minimal_d_separator",
226]
227
228
229@not_implemented_for("undirected")
230@nx._dispatchable
231def is_d_separator(G, x, y, z):
232 """Return whether node sets `x` and `y` are d-separated by `z`.
233
234 Parameters
235 ----------
236 G : nx.DiGraph
237 A NetworkX DAG.
238
239 x : node or set of nodes
240 First node or set of nodes in `G`.
241
242 y : node or set of nodes
243 Second node or set of nodes in `G`.
244
245 z : node or set of nodes
246 Potential separator (set of conditioning nodes in `G`). Can be empty set.
247
248 Returns
249 -------
250 b : bool
251 A boolean that is true if `x` is d-separated from `y` given `z` in `G`.
252
253 Raises
254 ------
255 NetworkXError
256 The *d-separation* test is commonly used on disjoint sets of
257 nodes in acyclic directed graphs. Accordingly, the algorithm
258 raises a :exc:`NetworkXError` if the node sets are not
259 disjoint or if the input graph is not a DAG.
260
261 NodeNotFound
262 If any of the input nodes are not found in the graph,
263 a :exc:`NodeNotFound` exception is raised
264
265 Notes
266 -----
267 A d-separating set in a DAG is a set of nodes that
268 blocks all paths between the two sets. Nodes in `z`
269 block a path if they are part of the path and are not a collider,
270 or a descendant of a collider. Also colliders that are not in `z`
271 block a path. A collider structure along a path
272 is ``... -> c <- ...`` where ``c`` is the collider node.
273
274 https://en.wikipedia.org/wiki/Bayesian_network#d-separation
275 """
276 try:
277 x = {x} if x in G else x
278 y = {y} if y in G else y
279 z = {z} if z in G else z
280
281 intersection = x & y or x & z or y & z
282 if intersection:
283 raise nx.NetworkXError(
284 f"The sets are not disjoint, with intersection {intersection}"
285 )
286
287 set_v = x | y | z
288 if set_v - G.nodes:
289 raise nx.NodeNotFound(f"The node(s) {set_v - G.nodes} are not found in G")
290 except TypeError:
291 raise nx.NodeNotFound("One of x, y, or z is not a node or a set of nodes in G")
292
293 if not nx.is_directed_acyclic_graph(G):
294 raise nx.NetworkXError("graph should be directed acyclic")
295
296 # contains -> and <-> edges from starting node T
297 forward_deque = deque([])
298 forward_visited = set()
299
300 # contains <- and - edges from starting node T
301 backward_deque = deque(x)
302 backward_visited = set()
303
304 ancestors_or_z = set().union(*[nx.ancestors(G, node) for node in x]) | z | x
305
306 while forward_deque or backward_deque:
307 if backward_deque:
308 node = backward_deque.popleft()
309 backward_visited.add(node)
310 if node in y:
311 return False
312 if node in z:
313 continue
314
315 # add <- edges to backward deque
316 backward_deque.extend(G.pred[node].keys() - backward_visited)
317 # add -> edges to forward deque
318 forward_deque.extend(G.succ[node].keys() - forward_visited)
319
320 if forward_deque:
321 node = forward_deque.popleft()
322 forward_visited.add(node)
323 if node in y:
324 return False
325
326 # Consider if -> node <- is opened due to ancestor of node in z
327 if node in ancestors_or_z:
328 # add <- edges to backward deque
329 backward_deque.extend(G.pred[node].keys() - backward_visited)
330 if node not in z:
331 # add -> edges to forward deque
332 forward_deque.extend(G.succ[node].keys() - forward_visited)
333
334 return True
335
336
337@not_implemented_for("undirected")
338@nx._dispatchable
339def find_minimal_d_separator(G, x, y, *, included=None, restricted=None):
340 """Returns a minimal d-separating set between `x` and `y` if possible
341
342 A d-separating set in a DAG is a set of nodes that blocks all
343 paths between the two sets of nodes, `x` and `y`. This function
344 constructs a d-separating set that is "minimal", meaning no nodes can
345 be removed without it losing the d-separating property for `x` and `y`.
346 If no d-separating sets exist for `x` and `y`, this returns `None`.
347
348 In a DAG there may be more than one minimal d-separator between two
349 sets of nodes. Minimal d-separators are not always unique. This function
350 returns one minimal d-separator, or `None` if no d-separator exists.
351
352 Uses the algorithm presented in [1]_. The complexity of the algorithm
353 is :math:`O(m)`, where :math:`m` stands for the number of edges in
354 the subgraph of G consisting of only the ancestors of `x` and `y`.
355 For full details, see [1]_.
356
357 Parameters
358 ----------
359 G : graph
360 A networkx DAG.
361 x : set | node
362 A node or set of nodes in the graph.
363 y : set | node
364 A node or set of nodes in the graph.
365 included : set | node | None
366 A node or set of nodes which must be included in the found separating set,
367 default is None, which means the empty set.
368 restricted : set | node | None
369 Restricted node or set of nodes to consider. Only these nodes can be in
370 the found separating set, default is None meaning all nodes in ``G``.
371
372 Returns
373 -------
374 z : set | None
375 The minimal d-separating set, if at least one d-separating set exists,
376 otherwise None.
377
378 Raises
379 ------
380 NetworkXError
381 Raises a :exc:`NetworkXError` if the input graph is not a DAG
382 or if node sets `x`, `y`, and `included` are not disjoint.
383
384 NodeNotFound
385 If any of the input nodes are not found in the graph,
386 a :exc:`NodeNotFound` exception is raised.
387
388 References
389 ----------
390 .. [1] van der Zander, Benito, and Maciej Liśkiewicz. "Finding
391 minimal d-separators in linear time and applications." In
392 Uncertainty in Artificial Intelligence, pp. 637-647. PMLR, 2020.
393 """
394 if not nx.is_directed_acyclic_graph(G):
395 raise nx.NetworkXError("graph should be directed acyclic")
396
397 try:
398 x = {x} if x in G else x
399 y = {y} if y in G else y
400
401 if included is None:
402 included = set()
403 elif included in G:
404 included = {included}
405
406 if restricted is None:
407 restricted = set(G)
408 elif restricted in G:
409 restricted = {restricted}
410
411 set_y = x | y | included | restricted
412 if set_y - G.nodes:
413 raise nx.NodeNotFound(f"The node(s) {set_y - G.nodes} are not found in G")
414 except TypeError:
415 raise nx.NodeNotFound(
416 "One of x, y, included or restricted is not a node or set of nodes in G"
417 )
418
419 if not included <= restricted:
420 raise nx.NetworkXError(
421 f"Included nodes {included} must be in restricted nodes {restricted}"
422 )
423
424 intersection = x & y or x & included or y & included
425 if intersection:
426 raise nx.NetworkXError(
427 f"The sets x, y, included are not disjoint. Overlap: {intersection}"
428 )
429
430 nodeset = x | y | included
431 ancestors_x_y_included = nodeset.union(*[nx.ancestors(G, node) for node in nodeset])
432
433 z_init = restricted & (ancestors_x_y_included - (x | y))
434
435 x_closure = _reachable(G, x, ancestors_x_y_included, z_init)
436 if x_closure & y:
437 return None
438
439 z_updated = z_init & (x_closure | included)
440 y_closure = _reachable(G, y, ancestors_x_y_included, z_updated)
441 return z_updated & (y_closure | included)
442
443
444@not_implemented_for("undirected")
445@nx._dispatchable
446def is_minimal_d_separator(G, x, y, z, *, included=None, restricted=None):
447 """Determine if `z` is a minimal d-separator for `x` and `y`.
448
449 A d-separator, `z`, in a DAG is a set of nodes that blocks
450 all paths from nodes in set `x` to nodes in set `y`.
451 A minimal d-separator is a d-separator `z` such that removing
452 any subset of nodes makes it no longer a d-separator.
453
454 Note: This function checks whether `z` is a d-separator AND is
455 minimal. One can use the function `is_d_separator` to only check if
456 `z` is a d-separator. See examples below.
457
458 Parameters
459 ----------
460 G : nx.DiGraph
461 A NetworkX DAG.
462 x : node | set
463 A node or set of nodes in the graph.
464 y : node | set
465 A node or set of nodes in the graph.
466 z : node | set
467 The node or set of nodes to check if it is a minimal d-separating set.
468 The function :func:`is_d_separator` is called inside this function
469 to verify that `z` is in fact a d-separator.
470 included : set | node | None
471 A node or set of nodes which must be included in the found separating set,
472 default is ``None``, which means the empty set.
473 restricted : set | node | None
474 Restricted node or set of nodes to consider. Only these nodes can be in
475 the found separating set, default is ``None`` meaning all nodes in ``G``.
476
477 Returns
478 -------
479 bool
480 Whether or not the set `z` is a minimal d-separator subject to
481 `restricted` nodes and `included` node constraints.
482
483 Examples
484 --------
485 >>> G = nx.path_graph([0, 1, 2, 3], create_using=nx.DiGraph)
486 >>> G.add_node(4)
487 >>> nx.is_minimal_d_separator(G, 0, 2, {1})
488 True
489 >>> # since {1} is the minimal d-separator, {1, 3, 4} is not minimal
490 >>> nx.is_minimal_d_separator(G, 0, 2, {1, 3, 4})
491 False
492 >>> # alternatively, if we only want to check that {1, 3, 4} is a d-separator
493 >>> nx.is_d_separator(G, 0, 2, {1, 3, 4})
494 True
495
496 Raises
497 ------
498 NetworkXError
499 Raises a :exc:`NetworkXError` if the input graph is not a DAG.
500
501 NodeNotFound
502 If any of the input nodes are not found in the graph,
503 a :exc:`NodeNotFound` exception is raised.
504
505 References
506 ----------
507 .. [1] van der Zander, Benito, and Maciej Liśkiewicz. "Finding
508 minimal d-separators in linear time and applications." In
509 Uncertainty in Artificial Intelligence, pp. 637-647. PMLR, 2020.
510
511 Notes
512 -----
513 This function works on verifying that a set is minimal and
514 d-separating between two nodes. Uses criterion (a), (b), (c) on
515 page 4 of [1]_. a) closure(`x`) and `y` are disjoint. b) `z` contains
516 all nodes from `included` and is contained in the `restricted`
517 nodes and in the union of ancestors of `x`, `y`, and `included`.
518 c) the nodes in `z` not in `included` are contained in both
519 closure(x) and closure(y). The closure of a set is the set of nodes
520 connected to the set by a directed path in G.
521
522 The complexity is :math:`O(m)`, where :math:`m` stands for the
523 number of edges in the subgraph of G consisting of only the
524 ancestors of `x` and `y`.
525
526 For full details, see [1]_.
527 """
528 if not nx.is_directed_acyclic_graph(G):
529 raise nx.NetworkXError("graph should be directed acyclic")
530
531 try:
532 x = {x} if x in G else x
533 y = {y} if y in G else y
534 z = {z} if z in G else z
535
536 if included is None:
537 included = set()
538 elif included in G:
539 included = {included}
540
541 if restricted is None:
542 restricted = set(G)
543 elif restricted in G:
544 restricted = {restricted}
545
546 set_y = x | y | included | restricted
547 if set_y - G.nodes:
548 raise nx.NodeNotFound(f"The node(s) {set_y - G.nodes} are not found in G")
549 except TypeError:
550 raise nx.NodeNotFound(
551 "One of x, y, z, included or restricted is not a node or set of nodes in G"
552 )
553
554 if not included <= z:
555 raise nx.NetworkXError(
556 f"Included nodes {included} must be in proposed separating set z {x}"
557 )
558 if not z <= restricted:
559 raise nx.NetworkXError(
560 f"Separating set {z} must be contained in restricted set {restricted}"
561 )
562
563 intersection = x.intersection(y) or x.intersection(z) or y.intersection(z)
564 if intersection:
565 raise nx.NetworkXError(
566 f"The sets are not disjoint, with intersection {intersection}"
567 )
568
569 nodeset = x | y | included
570 ancestors_x_y_included = nodeset.union(*[nx.ancestors(G, n) for n in nodeset])
571
572 # criterion (a) -- check that z is actually a separator
573 x_closure = _reachable(G, x, ancestors_x_y_included, z)
574 if x_closure & y:
575 return False
576
577 # criterion (b) -- basic constraint; included and restricted already checked above
578 if not (z <= ancestors_x_y_included):
579 return False
580
581 # criterion (c) -- check that z is minimal
582 y_closure = _reachable(G, y, ancestors_x_y_included, z)
583 if not ((z - included) <= (x_closure & y_closure)):
584 return False
585 return True
586
587
588@not_implemented_for("undirected")
589def _reachable(G, x, a, z):
590 """Modified Bayes-Ball algorithm for finding d-connected nodes.
591
592 Find all nodes in `a` that are d-connected to those in `x` by
593 those in `z`. This is an implementation of the function
594 `REACHABLE` in [1]_ (which is itself a modification of the
595 Bayes-Ball algorithm [2]_) when restricted to DAGs.
596
597 Parameters
598 ----------
599 G : nx.DiGraph
600 A NetworkX DAG.
601 x : node | set
602 A node in the DAG, or a set of nodes.
603 a : node | set
604 A (set of) node(s) in the DAG containing the ancestors of `x`.
605 z : node | set
606 The node or set of nodes conditioned on when checking d-connectedness.
607
608 Returns
609 -------
610 w : set
611 The closure of `x` in `a` with respect to d-connectedness
612 given `z`.
613
614 References
615 ----------
616 .. [1] van der Zander, Benito, and Maciej Liśkiewicz. "Finding
617 minimal d-separators in linear time and applications." In
618 Uncertainty in Artificial Intelligence, pp. 637-647. PMLR, 2020.
619
620 .. [2] Shachter, Ross D. "Bayes-ball: The rational pastime
621 (for determining irrelevance and requisite information in
622 belief networks and influence diagrams)." In Proceedings of the
623 Fourteenth Conference on Uncertainty in Artificial Intelligence
624 (UAI), (pp. 480–487). 1998.
625 """
626
627 def _pass(e, v, f, n):
628 """Whether a ball entering node `v` along edge `e` passes to `n` along `f`.
629
630 Boolean function defined on page 6 of [1]_.
631
632 Parameters
633 ----------
634 e : bool
635 Directed edge by which the ball got to node `v`; `True` iff directed into `v`.
636 v : node
637 Node where the ball is.
638 f : bool
639 Directed edge connecting nodes `v` and `n`; `True` iff directed `n`.
640 n : node
641 Checking whether the ball passes to this node.
642
643 Returns
644 -------
645 b : bool
646 Whether the ball passes or not.
647
648 References
649 ----------
650 .. [1] van der Zander, Benito, and Maciej Liśkiewicz. "Finding
651 minimal d-separators in linear time and applications." In
652 Uncertainty in Artificial Intelligence, pp. 637-647. PMLR, 2020.
653 """
654 is_element_of_A = n in a
655 # almost_definite_status = True # always true for DAGs; not so for RCGs
656 collider_if_in_Z = v not in z or (e and not f)
657 return is_element_of_A and collider_if_in_Z # and almost_definite_status
658
659 queue = deque([])
660 for node in x:
661 if bool(G.pred[node]):
662 queue.append((True, node))
663 if bool(G.succ[node]):
664 queue.append((False, node))
665 processed = queue.copy()
666
667 while any(queue):
668 e, v = queue.popleft()
669 preds = ((False, n) for n in G.pred[v])
670 succs = ((True, n) for n in G.succ[v])
671 f_n_pairs = chain(preds, succs)
672 for f, n in f_n_pairs:
673 if (f, n) not in processed and _pass(e, v, f, n):
674 queue.append((f, n))
675 processed.append((f, n))
676
677 return {w for (_, w) in processed}