Skip to content
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/whats_new/_contributors.rst
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,4 @@

.. _Adam Li: https://github.com/adam2392
.. _Julien Siebert: https://github.com/siebert-julien
.. _Jaron Lee: https://github.com/jaron-lee
4 changes: 3 additions & 1 deletion docs/whats_new/v0.1.rst
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ Version 0.1
Changelog
---------

- |Feature| Implement uncovered circle path finding inside the :func:`pywhy_graphs.algorithms.uncovered_pd_path`, by `Jaron Lee`_ (:pr:`42`)
- |Feature| Implement and test the :class:`pywhy_graphs.CPDAG` for CPDAGs, by `Adam Li`_ (:pr:`6`)
- |Feature| Implement and test the :class:`pywhy_graphs.PAG` for PAGs, by `Adam Li`_ (:pr:`9`)
- |Feature| Implement and test various PAG algorithms :func:`pywhy_graphs.algorithms.possible_ancestors`, :func:`pywhy_graphs.algorithms.possible_descendants`, :func:`pywhy_graphs.algorithms.discriminating_path`, :func:`pywhy_graphs.algorithms.pds`, :func:`pywhy_graphs.algorithms.pds_path`, and :func:`pywhy_graphs.algorithms.uncovered_pd_path`, by `Adam Li`_ (:pr:`10`)
Expand All @@ -42,4 +43,5 @@ Thanks to everyone who has contributed to the maintenance and improvement of
the project since version inception, including:

* `Adam Li`_
* `Julien Siebert`_
* `Julien Siebert`_
* `Jaron Lee`_
44 changes: 38 additions & 6 deletions pywhy_graphs/algorithms/pag.py
Original file line number Diff line number Diff line change
Expand Up @@ -318,14 +318,23 @@ def uncovered_pd_path(
max_path_length: Optional[int] = None,
first_node: Optional[Node] = None,
second_node: Optional[Node] = None,
force_circle: bool = False,
forbid_node: Optional[Node] = None,
) -> Tuple[List[Node], bool]:
"""Compute uncovered potentially directed path from u to c.
"""Compute uncovered potentially directed (pd) paths from u to c.

An uncovered pd path is one where: u o-> ... -> c. There are no
bidirected arrows, bidirected circle arrows, or opposite arrows.
In addition, every node beside the endpoints are unshielded,

In a pd path, the edge between V(i) and V(i+1) is not an arrowhead into V(i)
or a tail from V(i+1). An intuitive explanation given in :footcite:`Zhang2008`
notes that a pd path could be oriented into a directed path by changing circles
into tails or arrowheads.

In addition, the path is uncovered, meaning every node beside the endpoints are unshielded,
meaning V(i-1) and V(i+1) are not adjacent.

A special case of a uncovered pd path is an uncovered circle path, which appears
as u o-o ... o-o c.

Parameters
----------
graph : PAG
Expand All @@ -345,12 +354,24 @@ def uncovered_pd_path(
second_node : node, optional
The node after 'u' that the path must traverse. Both 'first_node'
and 'second_node' cannot be passed.
force_circle: bool
Whether to search for only circle paths (u o-o ... o-o c) or all
potentially directed paths. By default False, which searches for all potentially
directed paths.
forbid_node: node, optional
A node after 'u' which is forbidden to immediately traverse when searching for a path.
Comment on lines +361 to +362
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not familiar w/ when you would have a forbidden node. Would you be able to perhaps add a sentence or two to the Notes section?

And perhaps a test?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Completed


Notes
-----
The definition of an uncovered pd path is taken from :footcite:`Zhang2008`.

Typically uncovered potentially directed paths are defined by two nodes. However,
in its common use case within the FCI algorithm, it is usually defined relative
in one use case within the FCI algorithm, it is defined relative
to an adjacent third node that comes before 'u'.

References
----------
.. footbibliography::
"""
if first_node is not None and second_node is not None:
raise RuntimeError(
Expand Down Expand Up @@ -417,6 +438,11 @@ def uncovered_pd_path(

# get all adjacent nodes to 'this_node'
for next_node in graph.neighbors(this_node):
if this_node == start_node:
if forbid_node is not None:
if next_node == forbid_node:
continue

# if we have already explored this neighbor, then ignore
if next_node in explored_nodes:
continue
Expand All @@ -428,7 +454,13 @@ def uncovered_pd_path(

# now check that the triple is potentially directed, else
# we skip this node
if not graph.has_edge(this_node, next_node, graph.directed_edge_name):
condition = graph.has_edge(this_node, next_node, graph.circle_edge_name)
if not force_circle:
# If we do not restrict to circle paths then directed edges are also OK
condition = condition or graph.has_edge(
this_node, next_node, graph.directed_edge_name
)
if not condition:
continue

# now this next node is potentially directed, does not
Expand Down
60 changes: 60 additions & 0 deletions pywhy_graphs/algorithms/tests/test_pag.py
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,56 @@ def test_discriminating_path():
assert found_discriminating_path


def test_uncovered_pd_path_circle_path_only():
# Construct an uncovered circle path A o-o B o-o C
G = pywhy_graphs.PAG()
G.add_edge("A", "B", G.circle_edge_name)
G.add_edge("B", "A", G.circle_edge_name)
G.add_edge("B", "C", G.circle_edge_name)
G.add_edge("C", "B", G.circle_edge_name)
uncov_circle_path, found_uncovered_circle_path = uncovered_pd_path(
G, "B", "C", 10, "A", force_circle=True
)

assert found_uncovered_circle_path
assert uncov_circle_path == ["A", "B", "C"]

# Construct a non-circle path A o-o u o-o B o-> C
G = pywhy_graphs.PAG()
G.add_edge("A", "u", G.circle_edge_name)
G.add_edge("u", "A", G.circle_edge_name)
G.add_edge("B", "u", G.circle_edge_name)
G.add_edge("u", "B", G.circle_edge_name)
G.add_edge("B", "C", G.directed_edge_name)
G.add_edge("C", "B", G.circle_edge_name)
uncov_circle_path, found_uncovered_circle_path = uncovered_pd_path(
G, "u", "C", 10, "A", force_circle=True
)

assert not found_uncovered_circle_path

# Construct a potentially directed path that is not a circle path, and check that it does
# is not detected if force_circle=True
G = pywhy_graphs.PAG()
G.add_edge("A", "C", G.directed_edge_name)
G.add_edge("C", "A", G.circle_edge_name)
G.add_edges_from(
[("A", "u"), ("u", "x"), ("x", "y"), ("y", "z"), ("z", "C")], G.directed_edge_name
)
G.add_edge("y", "x", G.circle_edge_name)

# create a pd path from A to C through v
G.add_edges_from(
[("A", "v"), ("v", "x"), ("x", "y"), ("y", "z"), ("z", "C")], G.directed_edge_name
)
# with the bidirected edge, v,x,y is a shielded triple
G.add_edge("v", "y", G.bidirected_edge_name)

# check that this is asserted as not a circle path
_, found_uncovered_circle_path = uncovered_pd_path(G, "u", "C", 100, "A", force_circle=True)
assert not found_uncovered_circle_path


def test_uncovered_pd_path():
"""Test basic uncovered partially directed path."""
# If A o-> C and there is an undirected pd path
Expand Down Expand Up @@ -214,6 +264,16 @@ def test_uncovered_pd_path():
assert found_uncovered_pd_path
assert uncov_pd_path == ["A", "u", "x", "y", "z", "C"]

# Check that a circle path A o-o u o-o C is identified as an uncovered pd path
G = pywhy_graphs.PAG()
G.add_edge("A", "u", G.circle_edge_name)
G.add_edge("u", "A", G.circle_edge_name)
G.add_edge("u", "C", G.circle_edge_name)
G.add_edge("C", "u", G.circle_edge_name)
uncov_pd_path, found_uncovered_pd_path = uncovered_pd_path(G, "A", "C", 10)
assert found_uncovered_pd_path
assert uncov_pd_path == ["A", "u", "C"]

# check errors for running uncovered pd path
with pytest.raises(RuntimeError, match="Both first and second"):
uncovered_pd_path(G, "u", "C", 100, "A", "x")
Expand Down