Skip to content

Commit ac6333b

Browse files
authored
[DOC] Add some documentation describing Networkx vs PywhyGraphs (#66)
* Add additional statements on networkx * Add reference guide for algorithms --------- Signed-off-by: Adam Li <[email protected]>
1 parent 20661f1 commit ac6333b

File tree

13 files changed

+175
-296
lines changed

13 files changed

+175
-296
lines changed

docs/api.rst

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,8 @@ graphs encountered in the literature.
3232
IPAG
3333
PsiPAG
3434

35-
Algorithms for Markov Equivalence Classes
36-
=========================================
35+
:mod:`pywhy_graphs.algorithms`: Algorithms for Mixed-Edge Graphs
36+
================================================================
3737
Traditional graph algorithms operate over graphs with only one type of edge.
3838
Equivalence class graphs in causality generally consist of more than one type of
3939
edge. These algorithms are common algorithms used in a variety of different

docs/conf.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ def setup(app):
8282
"inherited-members": False,
8383
"private-members": False,
8484
}
85-
autodoc_inherit_docstrings = False
85+
autodoc_inherit_docstrings = True
8686
# autodoc_typehints = "signature"
8787

8888
# -- numpydoc
@@ -357,6 +357,8 @@ def setup(app):
357357
]
358358

359359

360+
doctest_global_setup = "import networkx as nx"
361+
360362
# -- Other extension configuration -------------------------------------------
361363

362364
linkcheck_request_headers = dict(

docs/index.rst

Lines changed: 51 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@
33

44
pywhy-graphs is a Python package for representing causal graphs. For example, Acyclic
55
Directed Mixed Graphs (ADMG), also known as causal DAGs and Partial Ancestral Graphs (PAGs).
6-
We build on top of ``networkx's`` ``MixedEdgeGraph`` such that we maintain all the well-tested and efficient
7-
algorithms and data structures of ``networkx``.
6+
We build on top of ``networkx`` supporting graphs with mixed-edges using a composition
7+
of networkx graph classes.
88

99
We encourage you to use the package for your causal inference research and also build on top
1010
with relevant Pull Requests. Also, see our `contributing guide <https://github.com/mne-tools/mne-icalabel/blob/main/CONTRIBUTING.md>`_.
@@ -23,6 +23,55 @@ eventually converge to a stable API that maintains the common software engineeri
2323
will be marked as "alpha" indicating that their might be drastic changes over different releases.
2424
One should use alpha functionality with caution.
2525

26+
How do we compare with NetworkX?
27+
--------------------------------
28+
We fashioned pywhy-graphs API based on NetworkX because NetworkX's API is stable, robust and
29+
has an existing community around it. Therefore, we expect all NetworkX users to have a relatively
30+
low learning curve when transitioning to pywhy-graphs. However, NetworkX does not currently support
31+
graphs with mixed-edges, so that is where the fundamental difference between the two APIs lie.
32+
33+
In all the "NetworkX-like" functions related to edges, such as ``add_edge``, ``has_edge``,
34+
``number_of_edges``, etc. all have an additional keyword parameter, ``edge_type``. We also
35+
add a handful of new functions to the basic :class:`pywhy_graphs.networkx.MixedEdgeGraph` class,
36+
which is summarized in the table below. The edge
37+
type is specified by this parameter and internally, all pywhy-graphs graph classes are a
38+
composition of different networkx base graphs, :class:`networkx.DiGraph` and :class:`networkx.Graph`
39+
that map to a user-specified edge type. For example,
40+
41+
.. code-block:: Python
42+
43+
import pywhy_graphs.networkx as pywhy_nx
44+
import networkx as nx
45+
46+
# a mixed-edge graph is a composition of networkx graphs
47+
# so we can create a representation of a graph with directed and
48+
# bidirected edges using the MixedEdgeGraph class
49+
G = pywhy_nx.MixedEdgeGraph()
50+
G.add_edge_type(nx.DiGraph(), edge_type='directed')
51+
G.add_edge_type(nx.Graph(), edge_type='bidirected')
52+
53+
assert 'directed' in G.edge_types
54+
assert 'bidirected' in G.edge_types
55+
56+
# when we use networkx-like API, we usually will have to specify the edge type
57+
G.add_edge(0, 1, edge_type='directed')
58+
59+
Because of this feature, not all NetworkX algorithms will work with pywhy-graphs because
60+
they implicitly assume a single edge type. We implement common graph algorithms for
61+
mixed-edge graphs that are focused on causal inference in the :mod:`pywhy_graphs.algorithms`
62+
submodule. This is a similar design to NetworkX, where all graph classes have a relatively
63+
lightweight API designed solely for interfacing with nodes and edges, while more complicated
64+
algorithms are implemented as functions that take in a mixed edge graph.
65+
66+
+---------------+----------------------------------------------------------+
67+
| New API | Description |
68+
+===============+==========================================================+
69+
| add_edge_type | Adds a new edge type to the graph |
70+
+---------------+----------------------------------------------------------+
71+
| get_graphs | Get a dictionary of edge types and their networkx graphs |
72+
+---------------+----------------------------------------------------------+
73+
74+
2675
Contents
2776
--------
2877

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
.. _algorithms:
2+
3+
********************************
4+
Causal Graph Algorithms in PyWhy
5+
********************************
6+
7+
.. automodule:: pywhy_graphs.algorithms
8+
:no-members:
9+
:no-inherited-members:
10+
11+
12+
Pywhy-graphs provides data structures and methods for storing causal graphs, which
13+
are documented in :ref:`classes`. We also provide a submodule for common graph
14+
algorithms in the form of functions that take a mixed-edge graph as input.
15+
16+
Core Algorithms
17+
---------------
18+
.. currentmodule:: pywhy_graphs.algorithms
19+
20+
.. autosummary::
21+
:toctree: ../../generated/
22+
23+
is_valid_mec_graph
24+
possible_ancestors
25+
possible_descendants
26+
discriminating_path
27+
is_definite_noncollider
28+
29+
.. currentmodule:: pywhy_graphs.networkx
30+
31+
.. autosummary::
32+
:toctree: ../../generated/
33+
34+
bidirected_to_unobserved_confounder
35+
m_separated
36+
is_minimal_m_separator
37+
minimal_m_separator
38+
39+
40+
Algorithms for Markov Equivalence Classes
41+
-----------------------------------------
42+
.. currentmodule:: pywhy_graphs.algorithms
43+
.. autosummary::
44+
:toctree: ../../generated/
45+
46+
pds
47+
pds_path
48+
uncovered_pd_path
49+
50+
Algorithms for Time-Series Graphs
51+
---------------------------------
52+
53+
.. autosummary::
54+
:toctree: ../../generated/
55+
56+
pds_t
57+
pds_t_path
58+
59+
Algorithms for handling acyclicity
60+
----------------------------------
61+
62+
.. autosummary::
63+
:toctree: ../../generated/
64+
65+
acyclification

docs/user_guide.rst

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,10 @@ User Guide
1212

1313
.. toctree::
1414
:numbered:
15-
:maxdepth: 3
15+
:maxdepth: 2
1616

17-
1817
reference/classes/index
18+
reference/algorithms/index
1919
reference/simulation/index
2020
reference/export/index
2121
glossary

examples/intro/intro_causal_graphs.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,10 @@ def clone(self):
172172
# Say we add a bidirected edge between 'z' and 'x', then they are no longer
173173
# d-separated.
174174
admg.add_edge("z", "x", admg.bidirected_edge_name)
175-
print(f"'z' is d-separated from 'x': {pywhy_nx.m_separated(admg, {'z'}, {'x'}, set())}")
175+
print(
176+
f"'z' is d-separated from 'x' after adding a bidirected edge z<->x: "
177+
f"{pywhy_nx.m_separated(admg, {'z'}, {'x'}, set())}"
178+
)
176179

177180
# Markov Equivalence Classes
178181
# --------------------------
@@ -228,7 +231,7 @@ def clone(self):
228231
# an ancestral relationship.
229232
#
230233
# Typically, PAGs are learnt using some variant of the FCI algorithm :footcite:`Spirtes1993` and
231-
# :footcite`Zhang2008`.
234+
# :footcite:`Zhang2008`.
232235
pag = PAG()
233236

234237
# let's assume all the undirected edges are formed from the earlier DAG

pywhy_graphs/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
from .algorithms import * # noqa: F403
1616
from .config import sys_info
1717

18+
from . import algorithms
1819
from . import export
1920
from . import classes
2021
from . import networkx

pywhy_graphs/classes/admg.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ class ADMG(pywhy_nx.MixedEdgeGraph, AncestralMixin):
3939
--------
4040
networkx.DiGraph
4141
networkx.Graph
42+
pywhy_graphs.networkx.MixedEdgeGraph
4243
4344
Notes
4445
-----

pywhy_graphs/classes/cpdag.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ class CPDAG(pywhy_nx.MixedEdgeGraph, AncestralMixin, ConservativeMixin):
3636
networkx.DiGraph
3737
networkx.Graph
3838
pywhy_graphs.ADMG
39+
pywhy_graphs.networkx.MixedEdgeGraph
3940
4041
Notes
4142
-----

pywhy_graphs/classes/intervention.py

Lines changed: 11 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ class InterventionMixin:
1515
nodes: NodeView
1616

1717
@abstractmethod
18-
def add_edge(self, u, v, edge_type: Optional[str]):
18+
def add_edge(self, u_of_edge, v_of_edge, edge_type="all", **attr):
1919
pass
2020

2121
@abstractmethod
@@ -82,6 +82,15 @@ def add_f_nodes_from(self, intervention_sets: List[Set[Node]]):
8282
for intervention_set in intervention_sets:
8383
self.add_f_node(intervention_set)
8484

85+
def set_f_node(self, f_node, targets: Optional[Set] = None):
86+
if f_node not in self.nodes:
87+
raise RuntimeError(f"{f_node} is not a node in the existing graph.")
88+
89+
if targets is not None and not all(target in self.nodes for target in targets):
90+
raise RuntimeError(f"Not all targets {targets} are in the existing graph.")
91+
92+
self.graph["F-nodes"][f_node] = targets
93+
8594
@property
8695
def f_nodes(self) -> Set[Node]:
8796
"""Return set of F-nodes."""
@@ -139,6 +148,7 @@ class AugmentedGraph(ADMG, InterventionMixin):
139148
networkx.DiGraph
140149
networkx.Graph
141150
ADMG
151+
pywhy_graphs.networkx.MixedEdgeGraph
142152
143153
Notes
144154
-----
@@ -190,20 +200,6 @@ def remove_node(self, n):
190200
del self.graph["F-nodes"][n]
191201
return super().remove_node(n)
192202

193-
def add_edge(self, u_of_edge, v_of_edge, edge_type="all", **attr):
194-
if u_of_edge in self.f_nodes or v_of_edge in self.f_nodes:
195-
raise RuntimeError("Adding edges to F-nodes is not allowed.")
196-
return super().add_edge(u_of_edge, v_of_edge, edge_type, **attr)
197-
198-
def remove_edge(self, u, v, edge_type="all"):
199-
if u in self.f_nodes or v in self.f_nodes:
200-
raise RuntimeError(
201-
"Removing edges from F-nodes is not allowed. "
202-
"Please just call `remove_node` to remove the F-node "
203-
"and its corresponding edges"
204-
)
205-
return super().remove_edge(u, v, edge_type)
206-
207203

208204
class IPAG(PAG, InterventionMixin):
209205
"""A I-PAG Markov equivalence class of causal graphs.
@@ -305,20 +301,6 @@ def remove_node(self, n):
305301
del self.graph["F-nodes"][n]
306302
return super().remove_node(n)
307303

308-
def add_edge(self, u_of_edge, v_of_edge, edge_type="all", **attr):
309-
if u_of_edge in self.f_nodes or v_of_edge in self.f_nodes:
310-
raise RuntimeError("Adding edges to F-nodes is not allowed.")
311-
return super().add_edge(u_of_edge, v_of_edge, edge_type, **attr)
312-
313-
def remove_edge(self, u, v, edge_type="all"):
314-
if u in self.f_nodes or v in self.f_nodes:
315-
raise RuntimeError(
316-
"Removing edges from F-nodes is not allowed. "
317-
"Please just call `remove_node` to remove the F-node "
318-
"and its corresponding edges"
319-
)
320-
return super().remove_edge(u, v, edge_type)
321-
322304

323305
class PsiPAG(PAG, InterventionMixin):
324306
"""A Psi-PAG Markov equivalence class of causal graphs.
@@ -416,17 +398,3 @@ def remove_node(self, n):
416398
if n in self.f_nodes:
417399
del self.graph["F-nodes"][n]
418400
return super().remove_node(n)
419-
420-
def add_edge(self, u_of_edge, v_of_edge, edge_type="all", **attr):
421-
if u_of_edge in self.f_nodes or v_of_edge in self.f_nodes:
422-
raise RuntimeError("Adding edges to F-nodes is not allowed.")
423-
return super().add_edge(u_of_edge, v_of_edge, edge_type, **attr)
424-
425-
def remove_edge(self, u, v, edge_type="all"):
426-
if u in self.f_nodes or v in self.f_nodes:
427-
raise RuntimeError(
428-
"Removing edges from F-nodes is not allowed. "
429-
"Please just call `remove_node` to remove the F-node "
430-
"and its corresponding edges"
431-
)
432-
return super().remove_edge(u, v, edge_type)

0 commit comments

Comments
 (0)