diff --git a/balm/SuccessionDiagram.py b/balm/SuccessionDiagram.py index e1aadf63..40319084 100644 --- a/balm/SuccessionDiagram.py +++ b/balm/SuccessionDiagram.py @@ -19,6 +19,7 @@ from balm.petri_net_translation import network_to_petrinet from balm.space_utils import percolate_space, space_unique_key from balm.trappist_core import trappist +from balm.types import space_type # Enables helpful "progress" messages. DEBUG = False @@ -136,7 +137,7 @@ def __setstate__( self.petri_net = cast(nx.DiGraph, state["petri net"]) self.nfvs = cast(list[str], state["nfvs"]) self.dag = cast(nx.DiGraph, state["G"]) # type: ignore - self.node_indices = cast(dict[str, int], state["node_indices"]) # type: ignore + self.node_indices = cast(space_type, state["node_indices"]) # type: ignore def __len__(self) -> int: """ @@ -173,7 +174,7 @@ def from_file(path: str) -> SuccessionDiagram: """ return SuccessionDiagram(BooleanNetwork.from_file(path)) - def expanded_attractor_seeds(self) -> list[list[dict[str, int]]]: + def expanded_attractor_seeds(self) -> list[list[space_type]]: return [self.node_attractor_seeds(id) for id in self.expanded_ids()] def summary(self) -> str: @@ -262,7 +263,7 @@ def minimal_trap_spaces(self) -> list[int]: """ return [i for i in self.expanded_ids() if self.node_is_minimal(i)] - def find_node(self, node_space: dict[str, int]) -> int | None: + def find_node(self, node_space: space_type) -> int | None: """ Return the ID of the node matching the provided `node_space`, or `None` if no such node exists in this succession diagram. @@ -335,14 +336,14 @@ def node_depth(self, node_id: int) -> int: """ return cast(int, self.dag.nodes[node_id]["depth"]) - def node_space(self, node_id: int) -> dict[str, int]: + def node_space(self, node_id: int) -> space_type: """ Get the sub-space associated with the provided `node_id`. 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.dag.nodes[node_id]["space"]) + return cast(space_type, self.dag.nodes[node_id]["space"]) def node_is_expanded(self, node_id: int) -> bool: """ @@ -387,7 +388,7 @@ def node_successors(self, node_id: int, compute: bool = False) -> list[int]: def node_attractor_seeds( self, node_id: int, compute: bool = False - ) -> list[dict[str, int]]: + ) -> list[space_type]: """ Return the list of attractor seed states corresponding to the given `node_id`. Similar to `node_successors`, the method either computes the @@ -401,7 +402,7 @@ def node_attractor_seeds( """ node = cast(dict[str, Any], self.dag.nodes[node_id]) - attractors = cast(list[dict[str, int]] | None, node["attractors"]) + attractors = cast(list[space_type] | None, node["attractors"]) if attractors is None and not compute: raise KeyError(f"Attractor data not computed for node {node_id}.") @@ -414,7 +415,7 @@ def node_attractor_seeds( def edge_stable_motif( self, parent_id: int, child_id: int, reduced: bool = False - ) -> dict[str, int]: + ) -> space_type: """ Return the *stable motif* associated with the specified parent-child edge. If `reduced` is set to `False` (default), the unpercolated stable @@ -428,7 +429,7 @@ def edge_stable_motif( if reduced: return cast( - dict[str, int], + space_type, { k: v for k, v in self.dag.edges[parent_id, child_id]["motif"].items() # type: ignore @@ -436,7 +437,7 @@ def edge_stable_motif( }, ) else: - return cast(dict[str, int], self.dag.edges[parent_id, child_id]["motif"]) + return cast(space_type, self.dag.edges[parent_id, child_id]["motif"]) def build(self): """ @@ -537,7 +538,7 @@ def expand_attractor_seeds(self, size_limit: int | None = None) -> bool: return expand_attractor_seeds(self, size_limit) def expand_to_target( - self, target: dict[str, int], size_limit: int | None = None + self, target: space_type, size_limit: int | None = None ) -> bool: """ Expands the succession diagram using BFS in such a way that only nodes @@ -618,7 +619,7 @@ def _expand_one_node(self, node_id: int): if DEBUG: print(f"[{node_id}] Created edge into node {child_id}.") - def _ensure_node(self, parent_id: int | None, stable_motif: dict[str, int]) -> int: + def _ensure_node(self, parent_id: int | None, stable_motif: space_type) -> int: """ Internal method that ensures the provided node is present in this succession diagram as a child of the given `parent_id`. diff --git a/balm/_sd_algorithms/compute_attractor_seeds.py b/balm/_sd_algorithms/compute_attractor_seeds.py index 9475973d..2cd1ed23 100644 --- a/balm/_sd_algorithms/compute_attractor_seeds.py +++ b/balm/_sd_algorithms/compute_attractor_seeds.py @@ -10,12 +10,13 @@ from balm.motif_avoidant import detect_motif_avoidant_attractors, make_retained_set from balm.terminal_restriction_space import get_terminal_restriction_space from balm.trappist_core import compute_fixed_point_reduced_STG +from balm.types import space_type def compute_attractor_seeds( sd: SuccessionDiagram, node_id: int, -) -> list[dict[str, int]]: +) -> list[space_type]: """ Compute the list of vertices such that each attractor within the subspace of the given `node_id` is covered by exactly one vertex. diff --git a/balm/_sd_algorithms/expand_source_SCCs.py b/balm/_sd_algorithms/expand_source_SCCs.py index dc271e81..91ca5ebc 100644 --- a/balm/_sd_algorithms/expand_source_SCCs.py +++ b/balm/_sd_algorithms/expand_source_SCCs.py @@ -13,6 +13,7 @@ from balm.interaction_graph_utils import infer_signed_interaction_graph from balm.petri_net_translation import extract_variable_names, network_to_petrinet from balm.space_utils import percolate_network, percolate_space +from balm.types import space_type if TYPE_CHECKING: expander_function_type = Callable[ @@ -71,7 +72,7 @@ def expand_source_SCCs( if len(source_nodes) != 0: bin_values_iter = it.product(range(2), repeat=len(source_nodes)) for bin_values in bin_values_iter: - source_comb = dict(zip(source_nodes, bin_values)) + source_comb = cast(space_type, dict(zip(source_nodes, bin_values))) sub_space = source_comb sub_space.update(perc_space) @@ -89,7 +90,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.dag.nodes[node_id]["space"]) + sub_space = cast(space_type, sd.dag.nodes[node_id]["space"]) # find source SCCs clean_bnet, clean_bn = perc_and_remove_constants_from_bn(perc_bn, sub_space) @@ -183,7 +184,7 @@ def find_source_nodes(network: BooleanNetwork | DiGraph) -> list[str]: def perc_and_remove_constants_from_bn( - bn: BooleanNetwork, space: dict[str, int] + bn: BooleanNetwork, space: space_type ) -> tuple[str, BooleanNetwork]: """ Take a BooleanNetwork and percolate given space. @@ -317,11 +318,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.dag.nodes[node_id]["space"]).pop(implicit, None) + cast(space_type, scc_sd.dag.nodes[node_id]["space"]).pop(implicit, None) for x, y in cast(Iterable[tuple[int, int]], scc_sd.dag.edges): for implicit in implicit_parameters: - cast(dict[str, int], scc_sd.dag.edges[x, y]["motif"]).pop(implicit, None) + cast(space_type, scc_sd.dag.edges[x, y]["motif"]).pop(implicit, None) return scc_sd, exist_maa @@ -363,7 +364,7 @@ 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.dag.nodes[branch]["space"])) + motif.update(cast(space_type, sd.dag.nodes[branch]["space"])) child_id = sd._ensure_node(parent_id, motif) # type: ignore if check_maa: @@ -384,7 +385,7 @@ def attach_scc_sd( 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.dag.nodes[branch]["space"])) + motif.update(cast(space_type, 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 diff --git a/balm/_sd_algorithms/expand_to_target.py b/balm/_sd_algorithms/expand_to_target.py index d1418674..32794162 100644 --- a/balm/_sd_algorithms/expand_to_target.py +++ b/balm/_sd_algorithms/expand_to_target.py @@ -6,10 +6,11 @@ from balm.SuccessionDiagram import SuccessionDiagram from balm.space_utils import intersect, is_subspace +from balm.types import space_type def expand_to_target( - sd: SuccessionDiagram, target: dict[str, int], size_limit: int | None = None + sd: SuccessionDiagram, target: space_type, size_limit: int | None = None ): """ See `SuccessionDiagram.exapnd_to_target` for documentation. diff --git a/balm/control.py b/balm/control.py index 6bd0f499..25856240 100644 --- a/balm/control.py +++ b/balm/control.py @@ -1,16 +1,14 @@ from __future__ import annotations from itertools import combinations, product -from typing import cast +from typing import Literal, cast import networkx as nx # type: ignore from biodivine_aeon import BooleanNetwork from balm.space_utils import is_subspace, percolate_space from balm.SuccessionDiagram import SuccessionDiagram - -SuccessionType = list[dict[str, int]] # sequence of stable motifs -ControlType = list[dict[str, int]] # ways of locking in an individual stable motif +from balm.types import ControlType, SuccessionType, space_type def controls_are_equal(a: ControlType, b: ControlType) -> bool: @@ -107,7 +105,7 @@ def __str__(self): def succession_control( bn: BooleanNetwork, - target: dict[str, int], + target: space_type, strategy: str = "internal", succession_diagram: SuccessionDiagram | None = None, max_drivers_per_succession_node: int | None = None, @@ -120,7 +118,7 @@ def succession_control( ---------- bn : BooleanNetwork The network to analyze, which contains the Boolean update functions. - target : dict[str, int] + target : space_type The target subspace. strategy : str, optional The searching strategy to use to look for driver nodes. Options are @@ -174,7 +172,7 @@ def succession_control( def successions_to_target( succession_diagram: SuccessionDiagram, - target: dict[str, int], + target: space_type, expand_diagram: bool = True, ) -> list[SuccessionType]: """Find lists of nested trap spaces (successions) that lead to the @@ -184,7 +182,7 @@ def successions_to_target( ---------- succession_diagram : SuccessionDiagram The succession diagram from which successions will be extracted. - target : dict[str, int] + target : space_type The target subspace. expand_diagram: bool Whether to ensure that the succession diagram is expanded enough to @@ -228,7 +226,7 @@ def successions_to_target( def drivers_of_succession( bn: BooleanNetwork, - succession: list[dict[str, int]], + succession: list[space_type], strategy: str = "internal", max_drivers_per_succession_node: int | None = None, forbidden_drivers: set[str] | None = None, @@ -239,7 +237,7 @@ def drivers_of_succession( ---------- bn : BooleanNetwork The network to analyze, which contains the Boolean update functions. - succession : list[dict[str, int]] + succession : list[space_type] A list of sequentially nested trap spaces that specify the target. strategy: str The searching strategy to use to look for driver nodes. Options are @@ -260,7 +258,7 @@ def drivers_of_succession( of drivers for the corresponding trap space in the succession. """ control_strategies: list[ControlType] = [] - assume_fixed: dict[str, int] = {} + assume_fixed: space_type = {} for ts in succession: control_strategies.append( find_drivers( @@ -280,9 +278,9 @@ def drivers_of_succession( def find_drivers( bn: BooleanNetwork, - target_trap_space: dict[str, int], + target_trap_space: space_type, strategy: str = "internal", - assume_fixed: dict[str, int] | None = None, + assume_fixed: space_type | None = None, max_drivers_per_succession_node: int | None = None, forbidden_drivers: set[str] | None = None, ) -> ControlType: @@ -292,7 +290,7 @@ def find_drivers( ---------- bn : BooleanNetwork The network to analyze, which contains the Boolean update functions. - target_trap_space : dict[str, int] + target_trap_space : space_type The trap space we want to find drivers for. strategy: str The searching strategy to use to look for driver nodes. Options are @@ -343,7 +341,10 @@ def find_drivers( continue if strategy == "internal": - driver_dict = {k: target_trap_space_inner[k] for k in driver_set} + driver_dict: space_type = { + k: cast(Literal[0, 1], target_trap_space_inner[k]) + for k in driver_set + } ldoi = percolate_space( bn, driver_dict | assume_fixed, strict_percolation=False ) @@ -352,7 +353,8 @@ def find_drivers( elif strategy == "all": for vals in product([0, 1], repeat=driver_set_size): driver_dict = { - driver: value for driver, value in zip(driver_set, vals) + driver: cast(Literal[0, 1], value) + for driver, value in zip(driver_set, vals) } ldoi = percolate_space( bn, driver_dict | assume_fixed, strict_percolation=False diff --git a/balm/drivers.py b/balm/drivers.py index 8b1ef43f..7266f34f 100644 --- a/balm/drivers.py +++ b/balm/drivers.py @@ -1,16 +1,19 @@ from __future__ import annotations +from typing import Literal, cast + from biodivine_aeon import BooleanNetwork from balm.space_utils import percolate_space +from balm.types import space_type -def find_single_node_LDOIs(bn: BooleanNetwork) -> dict[tuple[str, int], dict[str, int]]: +def find_single_node_LDOIs(bn: BooleanNetwork) -> dict[tuple[str, int], space_type]: """ finds LDOIs of every single node state TODO: take an initial set of LDOIs (e.g., of the original system) as an argument for speed-up """ - LDOIs: dict[tuple[str, int], dict[str, int]] = {} + LDOIs: dict[tuple[str, int], space_type] = {} for var in bn.variables(): name = bn.get_variable_name(var) function = bn.get_update_function(var) @@ -19,16 +22,16 @@ def find_single_node_LDOIs(bn: BooleanNetwork) -> dict[tuple[str, int], dict[str continue for i in range(2): fix = (name, i) - space = {name: i} + space: space_type = {name: cast(Literal[0, 1], i)} LDOIs[fix] = percolate_space(bn, space) return LDOIs def find_single_drivers( - target_subspace: dict[str, int], + target_subspace: space_type, bn: BooleanNetwork, - LDOIs: dict[tuple[str, int], dict[str, int]] | None = None, + LDOIs: dict[tuple[str, int], space_type] | None = None, ) -> set[tuple[str, int]]: """ find all the single node drivers for a given target_subspace, diff --git a/balm/motif_avoidant.py b/balm/motif_avoidant.py index 5043b091..eaec5e73 100644 --- a/balm/motif_avoidant.py +++ b/balm/motif_avoidant.py @@ -18,6 +18,7 @@ state_list_to_bdd, state_to_bdd, ) +from balm.types import space_type if TYPE_CHECKING: from biodivine_aeon import BooleanNetwork @@ -32,9 +33,9 @@ def make_retained_set( network: BooleanNetwork, nfvs: list[str], - space: dict[str, int], - child_spaces: list[dict[str, int]] | None = None, -) -> dict[str, int]: + space: space_type, + child_spaces: list[space_type] | None = None, +) -> space_type: """ Calculate the retained set. @@ -101,12 +102,12 @@ def make_retained_set( def detect_motif_avoidant_attractors( network: BooleanNetwork, petri_net: DiGraph, - candidates: list[dict[str, int]], + candidates: list[space_type], terminal_restriction_space: BinaryDecisionDiagram, max_iterations: int, - ensure_subspace: dict[str, int] | None = None, + ensure_subspace: space_type | None = None, is_in_an_mts: bool = False, -) -> list[dict[str, int]]: +) -> list[space_type]: """ Compute a sub-list of `candidates` which correspond to motif-avoidant attractors. Other method inputs: @@ -149,12 +150,12 @@ def detect_motif_avoidant_attractors( def _preprocess_candidates( network: BooleanNetwork, - candidates: list[dict[str, int]], + candidates: list[space_type], terminal_restriction_space: BinaryDecisionDiagram, max_iterations: int, - ensure_subspace: dict[str, int] | None = None, + ensure_subspace: space_type | None = None, is_in_an_mts: bool = False, -) -> list[dict[str, int]]: +) -> list[space_type]: """ A fast but incomplete method for eliminating spurious attractor candidates. @@ -200,7 +201,7 @@ def _preprocess_candidates( if not is_in_an_mts: # Copy is sufficient because we won't be modifying the states within the set. candidates_dnf = candidates.copy() - filtered_candidates: list[dict[str, int]] = [] + filtered_candidates: list[space_type] = [] for state in candidates: # Remove the state from the candidates. If we can prove that is # is not an attractor, we will put it back. @@ -268,15 +269,15 @@ def _preprocess_candidates( def _filter_candidates( petri_net: DiGraph, - candidates: list[dict[str, int]], + candidates: list[space_type], terminal_restriction_space: BinaryDecisionDiagram, -) -> list[dict[str, int]]: +) -> list[space_type]: """ Filter candidate states using reachability procedure in Pint. """ avoid_states = ~terminal_restriction_space | state_list_to_bdd(candidates) - filtered_candidates: list[dict[str, int]] = [] + filtered_candidates: list[space_type] = [] for state in candidates: state_bdd = state_to_bdd(state) @@ -294,7 +295,7 @@ def _filter_candidates( def _Pint_reachability( petri_net: DiGraph, - initial_state: dict[str, int], + initial_state: space_type, target_states: BinaryDecisionDiagram, ) -> bool: """ diff --git a/balm/pyeda_utils.py b/balm/pyeda_utils.py index 775ccb28..4a4d035d 100644 --- a/balm/pyeda_utils.py +++ b/balm/pyeda_utils.py @@ -10,10 +10,11 @@ if TYPE_CHECKING: from pyeda.boolalg.expr import Expression + from pyeda.boolalg.expr import Literal as PyedaLiteral import pyeda.boolalg.expr as pyeda_expression from pyeda.boolalg.bdd import BinaryDecisionDiagram, expr2bdd -from pyeda.boolalg.expr import And, Equal, Implies, Literal, Not, Or, Xor +from pyeda.boolalg.expr import And, Equal, Implies, Not, Or, Xor PYEDA_TRUE: Expression = pyeda_expression.expr(1) PYEDA_FALSE: Expression = pyeda_expression.expr(0) @@ -133,12 +134,12 @@ def aeon_to_bdd(expression: str) -> BinaryDecisionDiagram: return expr2bdd(aeon_to_pyeda(expression)) -def expression_literals(expression: Expression) -> set[Literal]: +def expression_literals(expression: Expression) -> set[PyedaLiteral]: """ Compute the set of all literals appearing in the given PyEDA expression. """ - result: set[Literal] = set() + result: set[PyedaLiteral] = set() for sub_expression in expression.iter_dfs(): - if isinstance(sub_expression, Literal): + if isinstance(sub_expression, PyedaLiteral): result.add(sub_expression) return result diff --git a/balm/space_utils.py b/balm/space_utils.py index 367f383f..40cfc7b0 100644 --- a/balm/space_utils.py +++ b/balm/space_utils.py @@ -12,11 +12,14 @@ from typing import TYPE_CHECKING if TYPE_CHECKING: - from pyeda.boolalg.expr import Expression from pyeda.boolalg.bdd import BinaryDecisionDiagram + from pyeda.boolalg.expr import Expression + from balm.types import space_type from biodivine_aeon import BooleanNetwork, RegulatoryGraph -from pyeda.boolalg.expr import Complement, Literal, Variable +from pyeda.boolalg.expr import Complement +from pyeda.boolalg.expr import Literal as PyedaPyedaLiteral +from pyeda.boolalg.expr import Variable from balm.pyeda_utils import ( PYEDA_FALSE, @@ -28,12 +31,12 @@ ) -def intersect(x: dict[str, int], y: dict[str, int]) -> dict[str, int] | None: +def intersect(x: space_type, y: space_type) -> space_type | None: """ Compute the space which is the intersection of two spaces, or `None` if the spaces don't intersect. """ - result: dict[str, int] = {} + result: space_type = {} for k, v in x.items(): result[k] = v for k, v in y.items(): @@ -43,7 +46,7 @@ def intersect(x: dict[str, int], y: dict[str, int]) -> dict[str, int] | None: return result -def is_subspace(x: dict[str, int], y: dict[str, int]) -> bool: +def is_subspace(x: space_type, y: space_type) -> bool: """ Checks if `x` is a subspace of `y`. """ @@ -55,7 +58,7 @@ def is_subspace(x: dict[str, int], y: dict[str, int]) -> bool: return True -def is_syntactic_trap_space(bn: BooleanNetwork, space: dict[str, int]) -> bool: +def is_syntactic_trap_space(bn: BooleanNetwork, space: space_type) -> bool: """ Uses percolation to check if the given `space` is a trap space in the given `BooleanNetwork`. @@ -82,9 +85,9 @@ def is_syntactic_trap_space(bn: BooleanNetwork, space: dict[str, int]) -> bool: def percolate_space( network: BooleanNetwork, - space: dict[str, int], + space: space_type, strict_percolation: bool = True, -) -> dict[str, int]: +) -> space_type: """ Takes a Boolean network and a space (partial assignment of `0`/`1` to the network variables). It then percolates the values in the given `space` to @@ -106,11 +109,11 @@ def percolate_space( """ if strict_percolation: - result: dict[str, int] = {} + result: space_type = {} else: result = {var: space[var] for var in space} - fixed = {var: space[var] for var in space} + fixed: space_type = {var: space[var] for var in space} fixed_bddvars = {bddvar_cache(k): v for k, v in fixed.items()} bdds: dict[str, BinaryDecisionDiagram] = {} bdd_inputs = {} @@ -150,24 +153,23 @@ def percolate_space( elif bdds[var_name].is_zero(): r = 0 else: - r = -1 continue if var_name not in fixed: - fixed[var_name] = r - fixed_bddvars[bddvar_cache(var_name)] = r - result[var_name] = r + fixed[var_name] = r # type: ignore # (mypy bug? mypy cannot see that r is 0 or 1, but pylance can) + fixed_bddvars[bddvar_cache(var_name)] = r # type: ignore + result[var_name] = r # type: ignore deletion_list.append(var_name) done = False elif fixed[var_name] == r and var_name not in result: - result[var_name] = r + result[var_name] = r # type: ignore return result def percolation_conflicts( network: BooleanNetwork, - space: dict[str, int], + space: space_type, strict_percolation: bool = True, ) -> set[str]: """ @@ -191,7 +193,7 @@ def percolation_conflicts( return conflicts -def percolate_network(bn: BooleanNetwork, space: dict[str, int]) -> BooleanNetwork: +def percolate_network(bn: BooleanNetwork, space: space_type) -> BooleanNetwork: """ Takes an AEON.py Boolean network and a space (partial assignment of network variables to `0`/`1`). It then produces a new network with @@ -241,9 +243,7 @@ def percolate_network(bn: BooleanNetwork, space: dict[str, int]) -> BooleanNetwo return new_bn -def percolate_pyeda_expression( - expression: Expression, space: dict[str, int] -) -> Expression: +def percolate_pyeda_expression(expression: Expression, space: space_type) -> Expression: """ Takes a PyEDA expression and a subspace (dictionary assigning `1`/`0` to a subset of variables). Returns a simplified expression that is valid @@ -256,7 +256,7 @@ def percolate_pyeda_expression( return expression.simplify() -def expression_to_space_list(expression: Expression) -> list[dict[str, int]]: +def expression_to_space_list(expression: Expression) -> list[space_type]: """ Convert a PyEDA expression to a list of subspaces whose union represents an equivalent set of network states. @@ -270,16 +270,16 @@ def expression_to_space_list(expression: Expression) -> list[dict[str, int]]: # (or at least in some sense canonical) DNF. In the future, we might # want to either enforce this explicitly or relax this requirement. - sub_spaces: list[dict[str, int]] = [] + sub_spaces: list[space_type] = [] expression_dnf = expression.to_dnf() for clause in expression_dnf.xs: # type: ignore - sub_space: dict[str, int] = {} + sub_space: space_type = {} # Since we know this is a DNF clause, it can only be # a literal, or a conjunction of literals. # TODO: investigate the types here more closely... something strange is going on - literals = [clause] if isinstance(clause, Literal) else clause.xs # type: ignore # noqa + literals = [clause] if isinstance(clause, PyedaPyedaLiteral) else clause.xs # type: ignore # noqa for literal in literals: # type: ignore var = str(literal.inputs[0]) # type: ignore if isinstance(literal, Variable): @@ -296,7 +296,7 @@ def expression_to_space_list(expression: Expression) -> list[dict[str, int]]: return sub_spaces -def space_unique_key(space: dict[str, int], network: BooleanNetwork) -> int: +def space_unique_key(space: space_type, network: BooleanNetwork) -> int: """ Computes an integer which is a unique representation of the provided `space` (with respect to the given `network`). diff --git a/balm/state_utils.py b/balm/state_utils.py index f715d540..a342bc39 100644 --- a/balm/state_utils.py +++ b/balm/state_utils.py @@ -7,15 +7,17 @@ variables mapping keys to 0/1 values. """ -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Literal from pyeda.boolalg.bdd import BDDONE, BDDZERO, bddvar # type:ignore if TYPE_CHECKING: from pyeda.boolalg.bdd import BDDVariable, BinaryDecisionDiagram +from balm.types import space_type -def _state_dict_to_bdd_valuation(state: dict[str, int]) -> dict[BDDVariable, int]: + +def _state_dict_to_bdd_valuation(state: space_type) -> dict[BDDVariable, int]: """ Convert state variables in a dictionary to their BDD counterparts. """ @@ -46,7 +48,7 @@ def state_to_bdd_cacheable(state: frozenset[tuple[str, int]]) -> BinaryDecisionD return state_bdd -def state_to_bdd(state: dict[str, int], usecache: bool = True) -> BinaryDecisionDiagram: +def state_to_bdd(state: space_type, usecache: bool = True) -> BinaryDecisionDiagram: """ Convert a state variables to a BDD encoding the state singleton. """ @@ -66,7 +68,7 @@ def state_to_bdd(state: dict[str, int], usecache: bool = True) -> BinaryDecision return state_bdd -def state_list_to_bdd(states: list[dict[str, int]]) -> BinaryDecisionDiagram: +def state_list_to_bdd(states: list[space_type]) -> BinaryDecisionDiagram: """ Convert a list of state dictionaries to a BDD representation. """ @@ -78,7 +80,7 @@ def state_list_to_bdd(states: list[dict[str, int]]) -> BinaryDecisionDiagram: def function_restrict( - f: BinaryDecisionDiagram, state: dict[str, int] + f: BinaryDecisionDiagram, state: space_type ) -> BinaryDecisionDiagram: """ Restrict the validity of the given BDD function to valuations which @@ -88,7 +90,7 @@ def function_restrict( return f.restrict(bdd_state) -def function_eval(f: BinaryDecisionDiagram, state: dict[str, int]) -> int | None: +def function_eval(f: BinaryDecisionDiagram, state: space_type) -> Literal[0, 1] | None: """ Evaluate a BDD function in the given state to an integer value. If the state is incomplete (i.e. it is a space), the function may not evaluate to an exact integer. In such case, @@ -105,7 +107,7 @@ def function_eval(f: BinaryDecisionDiagram, state: dict[str, int]) -> int | None return None -def function_is_true(f: BinaryDecisionDiagram, state: dict[str, int]) -> bool: +def function_is_true(f: BinaryDecisionDiagram, state: space_type) -> bool: """ Returns `True` if the given BDD function evaluates to `1` for the given state (or space). @@ -116,7 +118,7 @@ def function_is_true(f: BinaryDecisionDiagram, state: dict[str, int]) -> bool: return function_restrict(f, state).is_one() -def dnf_function_is_true(dnf: list[dict[str, int]], state: dict[str, int]) -> bool: +def dnf_function_is_true(dnf: list[space_type], state: space_type) -> bool: """ Returns `True` if the given DNF function evaluates to `1` for the given state (or space). @@ -130,13 +132,11 @@ def dnf_function_is_true(dnf: list[dict[str, int]], state: dict[str, int]) -> bo return False -def remove_state_from_dnf( - dnf: list[dict[str, int]], state: dict[str, int] -) -> list[dict[str, int]]: +def remove_state_from_dnf(dnf: list[space_type], state: space_type) -> list[space_type]: """ Removes all conjunctions that are True in the state """ - modified_dnf: list[dict[str, int]] = [] + modified_dnf: list[space_type] = [] for conjunction in dnf: if conjunction.items() <= state.items(): pass diff --git a/balm/terminal_restriction_space.py b/balm/terminal_restriction_space.py index d0eb0559..668622e7 100644 --- a/balm/terminal_restriction_space.py +++ b/balm/terminal_restriction_space.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, cast if TYPE_CHECKING: from biodivine_aeon import BooleanNetwork @@ -14,16 +14,17 @@ from balm.space_utils import percolate_network, percolation_conflicts from balm.state_utils import state_list_to_bdd, state_to_bdd from balm.trappist_core import trappist +from balm.types import space_type -def get_self_neg_tr_trap_spaces(network: BooleanNetwork) -> list[dict[str, int]]: +def get_self_neg_tr_trap_spaces(network: BooleanNetwork) -> list[space_type]: """ Takes a Boolean network and gets its self-negating time-reversal trap spaces. To find time-reversal trap spaces in a specific trap space, percolated network should be given as input. """ tr_trap_spaces = trappist(network, problem="max", reverse_time=True) - self_neg_tr_trap_spaces: list[dict[str, int]] = [] + self_neg_tr_trap_spaces: list[space_type] = [] for tr_trap_space in tr_trap_spaces: conflicts = percolation_conflicts(network, tr_trap_space) if conflicts: @@ -33,9 +34,9 @@ def get_self_neg_tr_trap_spaces(network: BooleanNetwork) -> list[dict[str, int]] def get_terminal_restriction_space( - stable_motifs: list[dict[str, int]], + stable_motifs: list[space_type], network: BooleanNetwork, - ensure_subspace: dict[str, int], + ensure_subspace: space_type, use_single_node_drivers: bool = True, use_tr_trapspaces: bool = True, ) -> BinaryDecisionDiagram: @@ -69,7 +70,9 @@ def get_terminal_restriction_space( continue # ~R(X) includes delta - result_bdd = result_bdd | state_to_bdd(dict(single_node_drivers)) + result_bdd = result_bdd | state_to_bdd( + cast(space_type, dict(single_node_drivers)) + ) for single_node_driver in single_node_drivers: # ~R(X) includes the F(delta) diff --git a/balm/trappist_core.py b/balm/trappist_core.py index 42572daa..5141e135 100644 --- a/balm/trappist_core.py +++ b/balm/trappist_core.py @@ -9,6 +9,7 @@ if TYPE_CHECKING: from typing import Callable from clingo import Model + from balm.types import space_type from biodivine_aeon import BooleanNetwork from clingo import Control, SolveHandle @@ -24,11 +25,11 @@ def trappist_async( network: BooleanNetwork | DiGraph, - on_solution: Callable[[dict[str, int]], bool], + on_solution: Callable[[space_type], bool], problem: str = "min", reverse_time: bool = False, - ensure_subspace: dict[str, int] | None = None, - avoid_subspaces: list[dict[str, int]] | None = None, + ensure_subspace: space_type | None = None, + avoid_subspaces: list[space_type] | None = None, ): """ The same as the `trappist` method, but instead of returning a list of spaces @@ -87,9 +88,9 @@ def trappist( problem: str = "min", reverse_time: bool = False, solution_limit: int | None = None, - ensure_subspace: dict[str, int] | None = None, - avoid_subspaces: list[dict[str, int]] | None = None, -) -> list[dict[str, int]]: + ensure_subspace: space_type | None = None, + avoid_subspaces: list[space_type] | None = None, +) -> list[space_type]: """ Solve the given `problem` for the given `network` using the Trappist algorithm, internally relying on the Python bindings of the `clingo` ASP @@ -117,9 +118,9 @@ def trappist( if avoid_subspaces is None: avoid_subspaces = [] - results: list[dict[str, int]] = [] + results: list[space_type] = [] - def save_result(x: dict[str, int]) -> bool: + def save_result(x: space_type) -> bool: results.append(x) if solution_limit is None: return True @@ -138,8 +139,8 @@ def save_result(x: dict[str, int]) -> bool: return results -def _clingo_model_to_space(model: Model) -> dict[str, int]: - space: dict[str, int] = {} +def _clingo_model_to_space(model: Model) -> space_type: + space: space_type = {} for atom in model.symbols(atoms=True): atom_str = str(atom) (variable, is_positive) = place_to_variable(atom_str) @@ -159,8 +160,8 @@ def _create_clingo_constraints( petri_net: DiGraph, problem: str = "min", reverse_time: bool = False, - ensure_subspace: dict[str, int] | None = None, - avoid_subspaces: list[dict[str, int]] | None = None, + ensure_subspace: space_type | None = None, + avoid_subspaces: list[space_type] | None = None, optimize_source_variables: list[str] | None = None, ) -> Control: """ @@ -275,7 +276,7 @@ def _create_clingo_constraints( return ctl -def _clingo_model_to_fixed_point(model: Model) -> dict[str, int]: +def _clingo_model_to_fixed_point(model: Model) -> space_type: """ Convert a clingo `Model` to a subspace representing a single fixed point. That is, the space should have all model variables fixed. @@ -284,7 +285,7 @@ def _clingo_model_to_fixed_point(model: Model) -> dict[str, int]: produces "positive" models (i.e. the space is represented by positive atoms present in the model). """ - space: dict[str, int] = {} + space: space_type = {} for atom in model.symbols(atoms=True): atom_str = str(atom) @@ -306,8 +307,8 @@ def _clingo_model_to_fixed_point(model: Model) -> dict[str, int]: def _create_clingo_fixed_point_constraints( variables: list[str], petri_net: DiGraph, - ensure_subspace: dict[str, int] | None = None, - avoid_subspaces: list[dict[str, int]] | None = None, + ensure_subspace: space_type | None = None, + avoid_subspaces: list[space_type] | None = None, ) -> Control: """ Generate the ASP characterizing all deadlocks of the Petri net (equivalently all @@ -374,10 +375,10 @@ def _create_clingo_fixed_point_constraints( def compute_fixed_point_reduced_STG_async( petri_net: DiGraph, - retained_set: dict[str, int], - on_solution: Callable[[dict[str, int]], bool], - ensure_subspace: dict[str, int] | None = None, - avoid_subspaces: list[dict[str, int]] | None = None, + retained_set: space_type, + on_solution: Callable[[space_type], bool], + ensure_subspace: space_type | None = None, + avoid_subspaces: list[space_type] | None = None, ): """ The same as the `compute_fixed_point_reduced_STG`, but instead of returning a @@ -425,11 +426,11 @@ def compute_fixed_point_reduced_STG_async( def compute_fixed_point_reduced_STG( petri_net: DiGraph, - retained_set: dict[str, int], - ensure_subspace: dict[str, int] = {}, - avoid_subspaces: list[dict[str, int]] = [], + retained_set: space_type, + ensure_subspace: space_type = {}, + avoid_subspaces: list[space_type] = [], solution_limit: int | None = None, -) -> list[dict[str, int]]: +) -> list[space_type]: """ This method computes the fixed points of the given Petri-net-encoded Boolean network. This makes it possible to modify the Petri net instead of @@ -441,9 +442,9 @@ def compute_fixed_point_reduced_STG( the Petri net, forcing given variables to retain the specified values. """ - results: list[dict[str, int]] = [] + results: list[space_type] = [] - def save_result(x: dict[str, int]) -> bool: + def save_result(x: space_type) -> bool: results.append(x) if solution_limit is None: return True diff --git a/balm/types.py b/balm/types.py new file mode 100644 index 00000000..b0f9dca3 --- /dev/null +++ b/balm/types.py @@ -0,0 +1,5 @@ +from typing import Literal + +space_type = dict[str, Literal[0, 1]] +SuccessionType = list[space_type] # sequence of stable motifs +ControlType = list[space_type] # ways of locking in an individual stable motif diff --git a/tests/control_test.py b/tests/control_test.py index 92b74a3f..bfd3552e 100644 --- a/tests/control_test.py +++ b/tests/control_test.py @@ -8,6 +8,7 @@ successions_to_target, ) from balm.SuccessionDiagram import SuccessionDiagram +from balm.types import space_type def test_intervention_equality_and_equivalence(): @@ -41,13 +42,17 @@ def test_basic_succession_control(): E, false """ ) - target_succession = [ + target_succession: list[space_type] = [ {"S": 0}, {"A": 0, "B": 0}, {"C": 1, "D": 1}, ] - cs = [[{"S": 0}], [{"A": 0}, {"B": 0}], [{"C": 1}, {"D": 1}]] + cs: list[list[space_type]] = [ + [{"S": 0}], + [{"A": 0}, {"B": 0}], + [{"C": 1}, {"D": 1}], + ] drivers = drivers_of_succession(bn, target_succession) assert all([controls_are_equal(a, b) for a, b in zip(cs, drivers)]) @@ -99,7 +104,7 @@ def test_basic_succession_finding(): {"A": 0, "B": 0}, ], ] - target = {"S": 0, "E": 0, "A": 0, "B": 0, "C": 1, "D": 1} + target: space_type = {"S": 0, "E": 0, "A": 0, "B": 0, "C": 1, "D": 1} succession_diagram = SuccessionDiagram(bn) successions = successions_to_target(succession_diagram, target) @@ -126,14 +131,14 @@ def test_internal_succession_control(): E, false """ ) - target = {"S": 0, "E": 0, "A": 0, "B": 0, "C": 1, "D": 1} + target: space_type = {"S": 0, "E": 0, "A": 0, "B": 0, "C": 1, "D": 1} - true_controls = [ + true_controls: list[list[list[space_type]]] = [ [[{"S": 0}], [{"A": 0}, {"B": 0}], [{"C": 1}, {"D": 1}]], [[{"S": 0}], [{"C": 1}, {"D": 1}], [{"A": 0}, {"B": 0}]], ] - true_successions = [ + true_successions: list[list[space_type]] = [ [ {"S": 0}, {"A": 0, "B": 0}, @@ -168,14 +173,14 @@ def test_all_succession_control(): E, false """ ) - target = {"S": 0, "E": 0, "A": 0, "B": 0, "C": 1, "D": 1} + target: space_type = {"S": 0, "E": 0, "A": 0, "B": 0, "C": 1, "D": 1} - true_controls = [ + true_controls: list[list[list[space_type]]] = [ [[{"S": 0}], [{"A": 0}, {"B": 0}], [{"C": 1}, {"D": 1}]], [[{"S": 0}], [{"A": 1}, {"B": 1}, {"C": 1}, {"D": 1}], [{"A": 0}, {"B": 0}]], ] - true_successions = [ + true_successions: list[list[space_type]] = [ [ {"S": 0}, {"A": 0, "B": 0}, @@ -207,11 +212,13 @@ def test_forbidden_drivers(): C, A & B """ ) - target = {"A": 1, "B": 1, "C": 1} + target: space_type = {"A": 1, "B": 1, "C": 1} # Test with no forbidden drivers first - true_controls = [[[{"A": 1, "B": 1}, {"A": 1, "C": 1}, {"B": 1, "C": 1}]]] - true_successions = [[{"A": 1, "B": 1, "C": 1}]] + true_controls: list[list[list[space_type]]] = [ + [[{"A": 1, "B": 1}, {"A": 1, "C": 1}, {"B": 1, "C": 1}]] + ] + true_successions: list[list[space_type]] = [[{"A": 1, "B": 1, "C": 1}]] true_interventions = [ Intervention(c, "internal", s) for c, s in zip(true_controls, true_successions) @@ -274,11 +281,13 @@ def test_size_restriction(): C, A & B """ ) - target = {"A": 1, "B": 1, "C": 1} + target: space_type = {"A": 1, "B": 1, "C": 1} # Test with no restrictions - true_controls = [[[{"A": 1, "B": 1}, {"A": 1, "C": 1}, {"B": 1, "C": 1}]]] - true_successions = [[{"A": 1, "B": 1, "C": 1}]] + true_controls: list[list[list[space_type]]] = [ + [[{"A": 1, "B": 1}, {"A": 1, "C": 1}, {"B": 1, "C": 1}]] + ] + true_successions: list[list[space_type]] = [[{"A": 1, "B": 1, "C": 1}]] true_interventions = [ Intervention(c, "internal", s) for c, s in zip(true_controls, true_successions) diff --git a/tests/motif_avoidant_test.py b/tests/motif_avoidant_test.py index 04030668..59f643ce 100644 --- a/tests/motif_avoidant_test.py +++ b/tests/motif_avoidant_test.py @@ -5,6 +5,7 @@ from balm.motif_avoidant import _preprocess_candidates # type: ignore from balm.petri_net_translation import network_to_petrinet from balm.state_utils import state_list_to_bdd +from balm.types import space_type def test_preprocessing_ssf_not_optimal(): @@ -15,9 +16,9 @@ def test_preprocessing_ssf_not_optimal(): """ ) - s0 = {"x1": 0, "x2": 0} - s1 = {"x1": 0, "x2": 1} - s2 = {"x1": 1, "x2": 0} + s0: space_type = {"x1": 0, "x2": 0} + s1: space_type = {"x1": 0, "x2": 1} + s2: space_type = {"x1": 1, "x2": 0} # s3 = {"x1": 1, "x2": 1} """ @@ -59,14 +60,14 @@ def test_preprocessing_ssf_optimal(): """ ) - s0 = {"A": 0, "B": 0, "C": 0} + s0: space_type = {"A": 0, "B": 0, "C": 0} # s1 = {"A": 0, "B": 0, "C": 1} - s2 = {"A": 0, "B": 1, "C": 0} - # s3 = {"A": 0, "B": 1, "C": 1} - s4 = {"A": 1, "B": 0, "C": 0} - # s5 = {"A": 1, "B": 0, "C": 1} - # s6 = {"A": 1, "B": 1, "C": 0} - s7 = {"A": 1, "B": 1, "C": 1} + s2: space_type = {"A": 0, "B": 1, "C": 0} + # s3: space_type = {"A": 0, "B": 1, "C": 1} + s4: space_type = {"A": 1, "B": 0, "C": 0} + # s5: space_type = {"A": 1, "B": 0, "C": 1} + # s6: space_type = {"A": 1, "B": 1, "C": 0} + s7: space_type = {"A": 1, "B": 1, "C": 1} """ This BN has two minimal trap spaces: 101 + 011. @@ -98,10 +99,10 @@ def test_ABNReach_current_version(): """ ) - s0 = {"x1": 0, "x2": 0, "x3": 1} - s1 = {"x1": 0, "x2": 1, "x3": 1} - # s2 = {"x1": 1, "x2": 0, "x3": 1} - s3 = {"x1": 1, "x2": 1, "x3": 1} + s0: space_type = {"x1": 0, "x2": 0, "x3": 1} + s1: space_type = {"x1": 0, "x2": 1, "x3": 1} + # s2: space_type = {"x1": 1, "x2": 0, "x3": 1} + s3: space_type = {"x1": 1, "x2": 1, "x3": 1} petri_net = network_to_petrinet(bn) @@ -132,9 +133,9 @@ def test_FilteringProcess(): """ ) - s0 = {"x1": 0, "x2": 0} - s1 = {"x1": 0, "x2": 1} - s2 = {"x1": 1, "x2": 0} + s0: space_type = {"x1": 0, "x2": 0} + s1: space_type = {"x1": 0, "x2": 1} + s2: space_type = {"x1": 1, "x2": 0} # s3 = {"x1": 1, "x2": 1} terminal_res_space = state_list_to_bdd([s0, s1, s2]) diff --git a/tests/succession_diagram_test.py b/tests/succession_diagram_test.py index aeb0da9d..4867efff 100644 --- a/tests/succession_diagram_test.py +++ b/tests/succession_diagram_test.py @@ -7,6 +7,7 @@ import balm import balm.SuccessionDiagram from balm.SuccessionDiagram import SuccessionDiagram +from balm.types import space_type # This just ensures that the debug outputs are a part of the test output. balm.SuccessionDiagram.DEBUG = True @@ -234,7 +235,7 @@ def test_attractor_detection(network_file: str): # Compute attractors in diagram nodes. # TODO: There will probably be a method that does this in one "go". - nfvs_attractors: list[dict[str, int]] = [] + nfvs_attractors: list[space_type] = [] for i in sd.node_ids(): attr = sd.node_attractor_seeds(i, compute=True) for a in attr: @@ -311,7 +312,7 @@ def test_attractor_expansion(network_file: str): # Compute attractors in diagram nodes. # TODO: There will probably be a method that does this in one "go". - nfvs_attractors: list[dict[str, int]] = [] + nfvs_attractors: list[space_type] = [] # This is an important change compared to the original test: Here, we only # care about expanded nodes, everything else is ignored. for i in sd.expanded_ids(): diff --git a/tests/terminal_restriction_space_test.py b/tests/terminal_restriction_space_test.py index 2c3cdf65..4a7e8663 100644 --- a/tests/terminal_restriction_space_test.py +++ b/tests/terminal_restriction_space_test.py @@ -6,6 +6,7 @@ state_list_to_bdd, ) from balm.trappist_core import trappist +from balm.types import space_type def test_tr_trap_spaces(): @@ -35,7 +36,7 @@ def test_get_terminal_restriction_space(): C, A & B """ ) - stable_motifs = [{"A": 1, "B": 1, "C": 1}] + stable_motifs: list[space_type] = [{"A": 1, "B": 1, "C": 1}] trs = get_terminal_restriction_space( stable_motifs, @@ -61,7 +62,7 @@ def test_get_terminal_restriction_space2(): F, B """ ) - stable_motifs = [{"A": 1, "B": 0, "C": 1}] + stable_motifs: list[space_type] = [{"A": 1, "B": 0, "C": 1}] trs = get_terminal_restriction_space( stable_motifs,