diff --git a/balm/SuccessionDiagram.py b/balm/SuccessionDiagram.py index 9f14fe90..e1aadf63 100644 --- a/balm/SuccessionDiagram.py +++ b/balm/SuccessionDiagram.py @@ -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 @@ -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] = {} @@ -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 @@ -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: @@ -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 @@ -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]: """ @@ -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]: """ @@ -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 @@ -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"]) @@ -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): """ @@ -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): """ @@ -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 @@ -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, @@ -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 diff --git a/balm/_sd_algorithms/expand_source_SCCs.py b/balm/_sd_algorithms/expand_source_SCCs.py index 648b7a6c..dc271e81 100644 --- a/balm/_sd_algorithms/expand_source_SCCs.py +++ b/balm/_sd_algorithms/expand_source_SCCs.py @@ -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) @@ -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 = [] @@ -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) @@ -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) @@ -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 @@ -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 @@ -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 @@ -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 diff --git a/balm/control.py b/balm/control.py index 454182bd..6bd0f499 100644 --- a/balm/control.py +++ b/balm/control.py @@ -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, ), diff --git a/tests/source_SCC_test.py b/tests/source_SCC_test.py index 980eb9de..b1080413 100644 --- a/tests/source_SCC_test.py +++ b/tests/source_SCC_test.py @@ -1,5 +1,6 @@ from biodivine_aeon import BooleanNetwork +import balm.SuccessionDiagram from balm._sd_algorithms.expand_source_SCCs import ( expand_source_SCCs, find_scc_sd, @@ -7,7 +8,6 @@ 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(): @@ -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 @@ -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 @@ -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()] == [ @@ -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 diff --git a/tests/succession_diagram_test.py b/tests/succession_diagram_test.py index 634996ee..aeb0da9d 100644 --- a/tests/succession_diagram_test.py +++ b/tests/succession_diagram_test.py @@ -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( @@ -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(