Skip to content

Commit

Permalink
feat(graph): added directed_path_exists and induced_path_exists
Browse files Browse the repository at this point in the history
also: added some documentation and TODOs
  • Loading branch information
this-is-sofia committed Oct 18, 2023
1 parent ae9587c commit 910f539
Show file tree
Hide file tree
Showing 4 changed files with 78 additions and 3 deletions.
1 change: 1 addition & 0 deletions causy/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ def load_json(pipeline_file: str):


def create_pipeline(pipeline_config: dict):
# clean up, add different generators, add loops
pipeline = []
for step in pipeline_config["steps"]:
path = ".".join(step["step"].split(".")[:-1])
Expand Down
78 changes: 75 additions & 3 deletions causy/graph.py
Original file line number Diff line number Diff line change
Expand Up @@ -157,14 +157,20 @@ def update_edge(self, u: Node, v: Node, value: Dict):
if v.name not in self.nodes:
raise UndirectedGraphError(f"Node {v} does not exist")
if u not in self.edges:
raise UndirectedGraphError(f"Node {u} does not have any nodes")
raise UndirectedGraphError(f"Node {u} does not have any edges")
if v not in self.edges:
raise UndirectedGraphError(f"Node {v} does not have any nodes")
raise UndirectedGraphError(f"Node {v} does not have any edges")

self.edges[u][v] = value
self.edges[v][u] = value

def edge_exists(self, u: Node, v: Node):
"""
Check if any edge exists between u and v. Cases: u -> v, u <-> v, u <- v
:param u: node u
:param v: node v
:return: True if any edge exists, False otherwise
"""
if u.name not in self.nodes:
return False
if v.name not in self.nodes:
Expand All @@ -176,6 +182,12 @@ def edge_exists(self, u: Node, v: Node):
return False

def directed_edge_exists(self, u: Node, v: Node):
"""
Check if a directed edge exists between u and v. Cases: u -> v, u <-> v
:param u: node u
:param v: node v
:return: True if a directed edge exists, False otherwise
"""
if u.name not in self.nodes:
return False
if v.name not in self.nodes:
Expand All @@ -187,11 +199,42 @@ def directed_edge_exists(self, u: Node, v: Node):
return True

def only_directed_edge_exists(self, u: Node, v: Node):
"""
Check if a directed edge exists between u and v, but no directed edge exists between v and u. Case: u -> v
:param u: node u
:param v: node v
:return: True if only directed edge exists, False otherwise
"""
if self.directed_edge_exists(u, v) and not self.directed_edge_exists(v, u):
return True
return False

def undirected_edge_exists(self, u: Node, v: Node):
"""
Check if an undirected edge exists between u and v. Note: currently, an undirected edges is implemented just as
a directed edge. However, they are two functions as they mean different things in different algorithms.
Currently, this function is used in the PC algorithm, where an undirected edge is an edge which could not be
oriented in any direction by orientation rules.
Later, a cohersive naming scheme should be implemented.
:param u: node u
:param v: node v
:return: True if an undirected edge exists, False otherwise
"""
if self.directed_edge_exists(u, v) and self.directed_edge_exists(v, u):
return True
return False

def bidirected_edge_exists(self, u: Node, v: Node):
"""
Check if a bidirected edge exists between u and v. Note: currently, a bidirected edges is implemented just as
an undirected edge. However, they are two functions as they mean different things in different algorithms.
This function will be used for the FCI algorithm for now, where a bidirected edge is an edge between two nodes
that have been identified to have a common cause by orientation rules.
Later, a cohersive naming scheme should be implemented.
:param u: node u
:param v: node v
:return: True if a bidirected edge exists, False otherwise
"""
if self.directed_edge_exists(u, v) and self.directed_edge_exists(v, u):
return True
return False
Expand All @@ -210,6 +253,35 @@ def add_node(self, name: str, values: List[float]):
"""
self.nodes[name] = Node(name, values)

def directed_path_exists(self, u:Node, v:Node):
"""
Check if a directed path from u to v exists
:param u: node u
:param v: node v
:return: True if a directed path exists, False otherwise
"""
if self.directed_edge_exists(u, v):
return True
for w in self.edges[u]:
if self.directed_path_exists(w, v):
return True
return False

def inducing_path_exists(self, u:Node, v:Node):
"""
Check if an inducing path from u to v exists.
An inducing path from u to v is a directed path from u to v on which all mediators are colliders.
:param u: node u
:param v: node v
:return: True if an inducing path exists, False otherwise
"""
if not self.directed_path_exists(u, v):
return False
for w in self.edges[u]:
if self.directed_path_exists(w, v):
return True
return False


def unpack_run(args):
tst = args[0]
Expand Down Expand Up @@ -399,7 +471,7 @@ def __init__(
PartialCorrelationTest(threshold=0.1),
ExtendedPartialCorrelationTestMatrix(threshold=0.1),
ColliderTest(),
# check replacing it with a loop of ExtendedPartialCorrelationTest
# check adding a loop on orientation rules
# Loop(
# pipeline_steps=[
# PlaceholderTest(),
Expand Down
1 change: 1 addition & 0 deletions causy/independence_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
ComparisonSettings,
)

# TODO: make tests configurable (choosing different generators for different algorithms)

class CalculateCorrelations(IndependenceTestInterface):
GENERATOR = AllCombinationsGenerator(
Expand Down
1 change: 1 addition & 0 deletions tests/test_pc_graph.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@


# TODO: generate larger toy model to test quadruple orientation rules.
# TODO: build loop over last four orientation rules in pc.json
def generate_data_minimal_example(a, b, c, d, sample_size):
V = d * np.random.normal(0, 1, sample_size)
W = c * np.random.normal(0, 1, sample_size)
Expand Down

0 comments on commit 910f539

Please sign in to comment.