Skip to content

Commit

Permalink
added SuccessionDiagram __slots__ and changed graph name
Browse files Browse the repository at this point in the history
  • Loading branch information
jcrozum committed Dec 12, 2023
1 parent e8b2dd9 commit 788accf
Show file tree
Hide file tree
Showing 5 changed files with 68 additions and 57 deletions.
54 changes: 31 additions & 23 deletions balm/SuccessionDiagram.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,14 @@ class SuccessionDiagram:
"""

__slots__ = (
"network",
"petri_net",
"nfvs",
"dag",
"node_indices",
)

def __init__(self, network: BooleanNetwork):
# Original Boolean network.
self.network = network
Expand All @@ -103,7 +111,7 @@ def __init__(self, network: BooleanNetwork):
network, parity="negative"
) # find_minimum_NFVS(network)
# A directed acyclic graph representing the succession diagram.
self.G = nx.DiGraph()
self.dag = nx.DiGraph()
# A dictionary used for uniqueness checks on the nodes of the succession
# diagram. See `SuccessionDiagram.ensure_node` for details.
self.node_indices: dict[int, int] = {}
Expand All @@ -116,7 +124,7 @@ def __getstate__(self) -> dict[str, str | nx.DiGraph | list[str] | dict[int, int
"network rules": self.network.to_aeon(),
"petri net": self.petri_net,
"nfvs": self.nfvs,
"G": self.G,
"G": self.dag,
"node_indices": self.node_indices,
}
return state
Expand All @@ -127,14 +135,14 @@ def __setstate__(
self.network = BooleanNetwork.from_aeon(str(state["network rules"]))
self.petri_net = cast(nx.DiGraph, state["petri net"])
self.nfvs = cast(list[str], state["nfvs"])
self.G = cast(nx.DiGraph, state["G"]) # type: ignore
self.dag = cast(nx.DiGraph, state["G"]) # type: ignore
self.node_indices = cast(dict[str, int], state["node_indices"]) # type: ignore

def __len__(self) -> int:
"""
Returns the number of nodes in this `SuccessionDiagram`.
"""
return self.G.number_of_nodes()
return self.dag.number_of_nodes()

@staticmethod
def from_aeon(model: str) -> SuccessionDiagram:
Expand Down Expand Up @@ -218,7 +226,7 @@ def depth(self) -> int:
Depth is counted from zero (root has depth zero).
"""
d = 0
for node in cast(set[int], self.G.nodes()):
for node in cast(set[int], self.dag.nodes()):
d = max(d, self.node_depth(int(node)))
return d

Expand Down Expand Up @@ -325,7 +333,7 @@ def node_depth(self, node_id: int) -> int:
Get the depth associated with the provided `node_id`. The depth is counted
as the longest path from the root node to the given node.
"""
return cast(int, self.G.nodes[node_id]["depth"])
return cast(int, self.dag.nodes[node_id]["depth"])

def node_space(self, node_id: int) -> dict[str, int]:
"""
Expand All @@ -334,21 +342,21 @@ def node_space(self, node_id: int) -> dict[str, int]:
Note that this is the space *after* percolation. Hence it can hold that
`|node_space(child)| < |node_space(parent)| + |stable_motif(parent, child)|`.
"""
return cast(dict[str, int], self.G.nodes[node_id]["space"])
return cast(dict[str, int], self.dag.nodes[node_id]["space"])

def node_is_expanded(self, node_id: int) -> bool:
"""
True if the successors of the given node are already computed.
"""
return cast(bool, self.G.nodes[node_id]["expanded"])
return cast(bool, self.dag.nodes[node_id]["expanded"])

def node_is_minimal(self, node_id: int) -> bool:
"""
True if the node is expanded and it has no successors, i.e. it is a
minimal trap space.
"""
is_leaf: bool = self.G.out_degree(node_id) == 0 # type: ignore
return is_leaf and self.G.nodes[node_id]["expanded"] # type: ignore
is_leaf: bool = self.dag.out_degree(node_id) == 0 # type: ignore
return is_leaf and self.dag.nodes[node_id]["expanded"] # type: ignore

def node_successors(self, node_id: int, compute: bool = False) -> list[int]:
"""
Expand All @@ -369,13 +377,13 @@ def node_successors(self, node_id: int, compute: bool = False) -> list[int]:
data but is not expanded, this data will be deleted as it is no longer
up to date.
"""
node = cast(dict[str, Any], self.G.nodes[node_id])
node = cast(dict[str, Any], self.dag.nodes[node_id])
if not node["expanded"] and not compute:
raise KeyError(f"Node {node_id} is not expanded.")
if not node["expanded"]:
self._expand_one_node(node_id)

return list(self.G.successors(node_id)) # type: ignore
return list(self.dag.successors(node_id)) # type: ignore

def node_attractor_seeds(
self, node_id: int, compute: bool = False
Expand All @@ -391,7 +399,7 @@ def node_attractor_seeds(
same attractor in multiple stub nodes, if the stub nodes intersect), and
(b) this data is erased if the stub node is expanded later on.
"""
node = cast(dict[str, Any], self.G.nodes[node_id])
node = cast(dict[str, Any], self.dag.nodes[node_id])

attractors = cast(list[dict[str, int]] | None, node["attractors"])

Expand Down Expand Up @@ -423,12 +431,12 @@ def edge_stable_motif(
dict[str, int],
{
k: v
for k, v in self.G.edges[parent_id, child_id]["motif"].items() # type: ignore
for k, v in self.dag.edges[parent_id, child_id]["motif"].items() # type: ignore
if k not in self.node_space(parent_id)
},
)
else:
return cast(dict[str, int], self.G.edges[parent_id, child_id]["motif"])
return cast(dict[str, int], self.dag.edges[parent_id, child_id]["motif"])

def build(self):
"""
Expand Down Expand Up @@ -549,10 +557,10 @@ def _update_node_depth(self, node_id: int, parent_id: int):
Note that the depth can only increase.
"""
assert self.G.edges[parent_id, node_id] is not None
parent_depth = cast(int, self.G.nodes[parent_id]["depth"])
current_depth = cast(int, self.G.nodes[node_id]["depth"])
self.G.nodes[node_id]["depth"] = max(current_depth, parent_depth + 1)
assert self.dag.edges[parent_id, node_id] is not None
parent_depth = cast(int, self.dag.nodes[parent_id]["depth"])
current_depth = cast(int, self.dag.nodes[node_id]["depth"])
self.dag.nodes[node_id]["depth"] = max(current_depth, parent_depth + 1)

def _expand_one_node(self, node_id: int):
"""
Expand All @@ -566,7 +574,7 @@ def _expand_one_node(self, node_id: int):
If there are already some attractor data for this node (stub nodes can
have associated attractor data), this data is erased.
"""
node = cast(dict[str, Any], self.G.nodes[node_id])
node = cast(dict[str, Any], self.dag.nodes[node_id])
if node["expanded"]:
return

Expand Down Expand Up @@ -631,8 +639,8 @@ def _ensure_node(self, parent_id: int | None, stable_motif: dict[str, int]) -> i

child_id = None
if key not in self.node_indices:
child_id = self.G.number_of_nodes()
self.G.add_node( # type: ignore
child_id = self.dag.number_of_nodes()
self.dag.add_node( # type: ignore
child_id,
id=child_id, # In case we ever need it within the "node data" dictionary.
space=fixed_vars,
Expand All @@ -650,7 +658,7 @@ def _ensure_node(self, parent_id: int | None, stable_motif: dict[str, int]) -> i
# TODO: It seems that there are some networks where the same child
# can be reached through multiple stable motifs. Not sure how to
# approach these... but this is probably good enough for now.
self.G.add_edge(parent_id, child_id, motif=stable_motif) # type: ignore
self.dag.add_edge(parent_id, child_id, motif=stable_motif) # type: ignore
self._update_node_depth(child_id, parent_id)

return child_id
32 changes: 16 additions & 16 deletions balm/_sd_algorithms/expand_source_SCCs.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ def expand_source_SCCs(

# percolate constant nodes
perc_space = percolate_space(sd.network, {}, strict_percolation=False)
sd.G.nodes[root]["space"] = perc_space
sd.dag.nodes[root]["space"] = perc_space

# find source nodes
perc_bn = percolate_network(sd.network, perc_space)
Expand All @@ -78,8 +78,8 @@ def expand_source_SCCs(

next_level.append(sd._ensure_node(root, sub_space)) # type: ignore

sd.G.nodes[root]["expanded"] = True
sd.G.nodes[root]["attractors"] = [] # no need to look for attractors here
sd.dag.nodes[root]["expanded"] = True
sd.dag.nodes[root]["attractors"] = [] # no need to look for attractors here
current_level = next_level
next_level = []

Expand All @@ -89,7 +89,7 @@ def expand_source_SCCs(

# each level consists of one round of fixing all source SCCs
for node_id in current_level:
sub_space = cast(dict[str, int], sd.G.nodes[node_id]["space"])
sub_space = cast(dict[str, int], sd.dag.nodes[node_id]["space"])

# find source SCCs
clean_bnet, clean_bn = perc_and_remove_constants_from_bn(perc_bn, sub_space)
Expand Down Expand Up @@ -141,8 +141,8 @@ def expand_source_SCCs(
print(f"{final_level=}")
for node_id in final_level:
# These assertions should be unnecessary, but just to be sure.
assert not sd.G.nodes[node_id]["expanded"] # expand nodes from here
assert sd.G.nodes[node_id]["attractors"] is None # check attractors from here
assert not sd.dag.nodes[node_id]["expanded"] # expand nodes from here
assert sd.dag.nodes[node_id]["attractors"] is None # check attractors from here

# restore this once we allow all expansion algorithms to expand from a node
# expander(sd, node_id)
Expand Down Expand Up @@ -317,11 +317,11 @@ def find_scc_sd(
# delete the implicit parameters from the node subspaces and the edge motifs
for node_id in scc_sd.node_ids():
for implicit in implicit_parameters:
cast(dict[str, int], scc_sd.G.nodes[node_id]["space"]).pop(implicit, None)
cast(dict[str, int], scc_sd.dag.nodes[node_id]["space"]).pop(implicit, None)

for x, y in cast(Iterable[tuple[int, int]], scc_sd.G.edges):
for x, y in cast(Iterable[tuple[int, int]], scc_sd.dag.edges):
for implicit in implicit_parameters:
cast(dict[str, int], scc_sd.G.edges[x, y]["motif"]).pop(implicit, None)
cast(dict[str, int], scc_sd.dag.edges[x, y]["motif"]).pop(implicit, None)

return scc_sd, exist_maa

Expand All @@ -346,14 +346,14 @@ def attach_scc_sd(
return [branch]

next_branches: list[int] = []
size_before_attach = sd.G.number_of_nodes()
size_before_attach = sd.dag.number_of_nodes()
# first add all the nodes using their first parent
for scc_node_id in scc_sd.node_ids():
if scc_node_id == 0: # no need to add the root of scc_sd
continue

scc_parent_id = cast(
int, list(scc_sd.G.predecessors(scc_node_id))[0] # type: ignore
int, list(scc_sd.dag.predecessors(scc_node_id))[0] # type: ignore
) # get the first parent
assert scc_parent_id < scc_node_id

Expand All @@ -363,11 +363,11 @@ def attach_scc_sd(
parent_id = size_before_attach + scc_parent_id - 1

motif = scc_sd.edge_stable_motif(scc_parent_id, scc_node_id)
motif.update(cast(dict[str, int], sd.G.nodes[branch]["space"]))
motif.update(cast(dict[str, int], sd.dag.nodes[branch]["space"]))

child_id = sd._ensure_node(parent_id, motif) # type: ignore
if check_maa:
sd.G.nodes[parent_id][
sd.dag.nodes[parent_id][
"attractors"
] = [] # no need to check for attractors in these nodes

Expand All @@ -381,16 +381,16 @@ def attach_scc_sd(
else:
parent_id = size_before_attach + scc_node_id - 1

scc_child_ids = cast(list[int], list(scc_sd.G.successors(scc_node_id))) # type: ignore
scc_child_ids = cast(list[int], list(scc_sd.dag.successors(scc_node_id))) # type: ignore
for scc_child_id in scc_child_ids:
motif = scc_sd.edge_stable_motif(scc_node_id, scc_child_id)
motif.update(cast(dict[str, int], sd.G.nodes[branch]["space"]))
motif.update(cast(dict[str, int], sd.dag.nodes[branch]["space"]))

child_id = sd._ensure_node(parent_id, motif) # type: ignore
assert child_id == size_before_attach + scc_child_id - 1

# if the node had any child node, consider it expanded.
if len(scc_child_ids) > 0:
sd.G.nodes[parent_id]["expanded"] = True
sd.dag.nodes[parent_id]["expanded"] = True

return next_branches
2 changes: 1 addition & 1 deletion balm/control.py
Original file line number Diff line number Diff line change
Expand Up @@ -212,7 +212,7 @@ def successions_to_target(
for path in cast(
list[list[int]],
nx.all_simple_paths( # type: ignore
succession_diagram.G,
succession_diagram.dag,
source=succession_diagram.root(),
target=s,
),
Expand Down
25 changes: 14 additions & 11 deletions tests/source_SCC_test.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
from biodivine_aeon import BooleanNetwork

import balm.SuccessionDiagram
from balm._sd_algorithms.expand_source_SCCs import (
expand_source_SCCs,
find_scc_sd,
find_source_nodes,
perc_and_remove_constants_from_bn,
)
from balm.space_utils import percolate_network, percolate_space
from balm.SuccessionDiagram import SuccessionDiagram


def test_find_source_nodes():
Expand Down Expand Up @@ -68,16 +68,19 @@ def test_find_scc_sd():
B, A | A & C"""

scc_sd, _ = find_scc_sd(
bnet, ["A", "B"], expander=SuccessionDiagram.expand_bfs, check_maa=True
bnet,
["A", "B"],
expander=balm.SuccessionDiagram.SuccessionDiagram.expand_bfs,
check_maa=True,
)

assert scc_sd.G.nodes[0]["space"] == {}
assert scc_sd.G.nodes[1]["space"] == {"A": 0, "B": 0}
assert scc_sd.G.nodes[2]["space"] == {"A": 1, "B": 1}
assert scc_sd.dag.nodes[0]["space"] == {}
assert scc_sd.dag.nodes[1]["space"] == {"A": 0, "B": 0}
assert scc_sd.dag.nodes[2]["space"] == {"A": 1, "B": 1}


def expansion(bn: BooleanNetwork):
sd = SuccessionDiagram(bn)
sd = balm.SuccessionDiagram.SuccessionDiagram(bn)
fully_expanded = expand_source_SCCs(sd, check_maa=False)
assert fully_expanded

Expand Down Expand Up @@ -134,7 +137,7 @@ def test_expansion():


def attractor_search(bn: BooleanNetwork):
sd = SuccessionDiagram(bn)
sd = balm.SuccessionDiagram.SuccessionDiagram(bn)
fully_expanded = expand_source_SCCs(sd)
assert fully_expanded

Expand Down Expand Up @@ -358,10 +361,10 @@ def test_isomorph():
path = "models/bbm-bnet-inputs-true/005.bnet"
bn = BooleanNetwork.from_file(path)

sd_bfs = SuccessionDiagram(bn)
sd_bfs = balm.SuccessionDiagram.SuccessionDiagram(bn)
sd_bfs.expand_bfs()

sd_scc = SuccessionDiagram(bn)
sd_scc = balm.SuccessionDiagram.SuccessionDiagram(bn)
expand_source_SCCs(sd_scc)

assert [sd_bfs.node_space(id) for id in sd_bfs.node_ids()] == [
Expand All @@ -370,7 +373,7 @@ def test_isomorph():

assert sd_scc.is_isomorphic(sd_bfs)

edge_motifs_bfs = set(str(sorted(sd_bfs.edge_stable_motif(x, y).items())) for (x, y) in sd_bfs.G.edges) # type: ignore
edge_motifs_scc = set(str(sorted(sd_scc.edge_stable_motif(x, y).items())) for (x, y) in sd_scc.G.edges) # type: ignore
edge_motifs_bfs = set(str(sorted(sd_bfs.edge_stable_motif(x, y).items())) for (x, y) in sd_bfs.dag.edges) # type: ignore
edge_motifs_scc = set(str(sorted(sd_scc.edge_stable_motif(x, y).items())) for (x, y) in sd_scc.dag.edges) # type: ignore

assert edge_motifs_bfs == edge_motifs_scc
12 changes: 6 additions & 6 deletions tests/succession_diagram_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,9 @@ def test_succession_diagram_structure(self):

succession_diagram = SuccessionDiagram(bn)
succession_diagram.expand_bfs()
assert succession_diagram.G.number_of_nodes() == 3
assert succession_diagram.G.number_of_edges() == 2 # type: ignore
assert max(d["depth"] for _, d in succession_diagram.G.nodes(data=True)) == 1 # type: ignore
assert succession_diagram.dag.number_of_nodes() == 3
assert succession_diagram.dag.number_of_edges() == 2 # type: ignore
assert max(d["depth"] for _, d in succession_diagram.dag.nodes(data=True)) == 1 # type: ignore
assert succession_diagram.depth() == 1
assert (
max(
Expand Down Expand Up @@ -74,9 +74,9 @@ def test_succession_diagram_structure(self):

# Then expand the whole thing.
succession_diagram.expand_bfs()
assert succession_diagram.G.number_of_nodes() == 4
assert succession_diagram.G.number_of_edges() == 5 # type: ignore
assert max(d["depth"] for _, d in succession_diagram.G.nodes(data=True)) == 2 # type: ignore
assert succession_diagram.dag.number_of_nodes() == 4
assert succession_diagram.dag.number_of_edges() == 5 # type: ignore
assert max(d["depth"] for _, d in succession_diagram.dag.nodes(data=True)) == 2 # type: ignore
assert succession_diagram.depth() == 2
assert (
max(
Expand Down

1 comment on commit 788accf

@github-actions
Copy link

Choose a reason for hiding this comment

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

Coverage

Coverage Report
FileStmtsMissCoverMissing
balm
   SuccessionDiagram.py2104180%6, 123–130, 135–139, 152, 159, 166, 174, 177, 183–214, 305, 445–447, 453, 579
   control.py1201389%47, 56, 60, 66, 80, 89–105, 319, 334
   interaction_graph_utils.py142894%6–9, 57, 70, 95–96
   motif_avoidant.py163597%23–24, 130, 184, 309
   petri_net_translation.py84693%23–24, 52, 63–64, 94
   pyeda_utils.py953464%12, 56–66, 90, 95, 98–112, 140–144
   space_utils.py145696%15–16, 189, 218, 233, 290
   state_utils.py681282%15, 55–66, 98, 105, 114
   terminal_restriction_space.py44491%6–7, 80, 97
   trappist_core.py1862089%10–11, 39, 41, 81, 127, 192, 194, 196, 231–233, 259, 317, 319, 349, 389, 391, 422, 451
balm/FVSpython3
   FVS.py481079%93–94, 102, 138, 190–198
   FVS_localsearch_10_python.py90199%179
balm/_sd_algorithms
   compute_attractor_seeds.py29197%6
   expand_attractor_seeds.py51492%6, 95–100
   expand_bfs.py28196%6
   expand_dfs.py30197%6
   expand_minimal_spaces.py37197%6
   expand_source_SCCs.py187896%18, 88, 98, 141, 164–165, 170, 285
   expand_to_target.py30390%6, 37, 42
TOTAL188417990% 

Tests Skipped Failures Errors Time
366 0 💤 0 ❌ 0 🔥 4m 39s ⏱️

Please sign in to comment.