Skip to content

Commit

Permalink
Merge pull request #116 from jcrozum/attractor-benchmarks
Browse files Browse the repository at this point in the history
Attractor detection refactoring
  • Loading branch information
jcrozum authored Apr 18, 2024
2 parents 18343aa + 9f6de06 commit 0438418
Show file tree
Hide file tree
Showing 21 changed files with 2,000 additions and 724 deletions.
20 changes: 1 addition & 19 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ jobs:
- name: Lint with Black
uses: rickstaa/action-black@v1
with:
black_args: ". --check"
black_args: ". --check --diff"
- name: Check types
run: |
mypy biobalm
Expand Down Expand Up @@ -52,12 +52,6 @@ jobs:
run: |
wget https://github.com/pauleve/pint/releases/download//2019-05-24/pint_2019-05-24_amd64.deb
sudo apt install ./pint_2019-05-24_amd64.deb
- name: Fetch and install Mole
run: |
wget http://www.lsv.fr/~schwoon/tools/mole/mole-140428.tar.gz
tar -xvf mole-140428.tar.gz
(cd ./mole-140428 && make)
(cd ./mole-140428 && pwd >> $GITHUB_PATH)
- name: Install repo dependencies
run: pip install -r requirements.txt
- name: Run doctests
Expand All @@ -81,12 +75,6 @@ jobs:
run: |
wget https://github.com/pauleve/pint/releases/download//2019-05-24/pint_2019-05-24_amd64.deb
sudo apt install ./pint_2019-05-24_amd64.deb
- name: Fetch and install Mole
run: |
wget http://www.lsv.fr/~schwoon/tools/mole/mole-140428.tar.gz
tar -xvf mole-140428.tar.gz
(cd ./mole-140428 && make)
(cd ./mole-140428 && pwd >> $GITHUB_PATH)
- name: Install repo dependencies
run: pip install -r requirements.txt
- name: Install pytest
Expand Down Expand Up @@ -117,12 +105,6 @@ jobs:
run: |
wget https://github.com/pauleve/pint/releases/download//2019-05-24/pint_2019-05-24_amd64.deb
sudo apt install ./pint_2019-05-24_amd64.deb
- name: Fetch and install Mole
run: |
wget http://www.lsv.fr/~schwoon/tools/mole/mole-140428.tar.gz
tar -xvf mole-140428.tar.gz
(cd ./mole-140428 && make)
(cd ./mole-140428 && pwd >> $GITHUB_PATH)
- name: Install repo dependencies
run: pip install -r requirements.txt
- name: Install pytest
Expand Down
6 changes: 6 additions & 0 deletions benchmark/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@

# Ignore benchmark runs.
_run_*

# Ignore generated succession diagram files.
_sd_expand_*
13 changes: 10 additions & 3 deletions benchmark/bench_sd_expand_bfs.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
from biodivine_aeon import BooleanNetwork
from biobalm.SuccessionDiagram import SuccessionDiagram
from biobalm import SuccessionDiagram
import sys
import biobalm.SuccessionDiagram
import os
import pickle
import biobalm

# Print progress and succession diagram size.
biobalm.SuccessionDiagram.DEBUG = True
biobalm.succession_diagram.DEBUG = True

NODE_LIMIT = 1_000_000
DEPTH_LIMIT = 10_000
Expand All @@ -17,6 +19,11 @@
fully_expanded = sd.expand_bfs(bfs_level_limit=DEPTH_LIMIT, size_limit=NODE_LIMIT)
assert fully_expanded

model_name = os.path.basename(sys.argv[1])
sd_name = os.path.splitext(model_name)[0] + ".pickle"
with open(f"./_sd_expand_bfs/{sd_name}", "wb") as handle:
pickle.dump(sd, handle)

print(f"Succession diagram size:", len(sd))
print(f"Minimal traps:", len(sd.minimal_trap_spaces()))

Expand Down
13 changes: 10 additions & 3 deletions benchmark/bench_sd_expand_dfs.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
from biodivine_aeon import BooleanNetwork
from biobalm.SuccessionDiagram import SuccessionDiagram
from biobalm import SuccessionDiagram
import sys
import biobalm.SuccessionDiagram
import os
import pickle
import biobalm

# Print progress and succession diagram size.
biobalm.SuccessionDiagram.DEBUG = True
biobalm.succession_diagram.DEBUG = True

NODE_LIMIT = 1_000_000
DEPTH_LIMIT = 10_000
Expand All @@ -17,6 +19,11 @@
fully_expanded = sd.expand_dfs(dfs_stack_limit=DEPTH_LIMIT, size_limit=NODE_LIMIT)
assert fully_expanded

model_name = os.path.basename(sys.argv[1])
sd_name = os.path.splitext(model_name)[0] + ".pickle"
with open(f"./_sd_expand_dfs/{sd_name}", "wb") as handle:
pickle.dump(sd, handle)

print(f"Succession diagram size:", len(sd))
print(f"Minimal traps:", len(sd.minimal_trap_spaces()))

Expand Down
15 changes: 11 additions & 4 deletions benchmark/bench_sd_expand_min.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
from biodivine_aeon import BooleanNetwork
from biobalm.SuccessionDiagram import SuccessionDiagram
from biobalm import SuccessionDiagram
import sys
import biobalm.SuccessionDiagram
import os
import pickle
import biobalm

# Print progress and succession diagram size.
biobalm.SuccessionDiagram.DEBUG = True
biobalm.succession_diagram.DEBUG = True

NODE_LIMIT = 1_000_000
DEPTH_LIMIT = 10_000
Expand All @@ -16,8 +18,13 @@
sd = SuccessionDiagram(bn)
sd.expand_minimal_spaces()

model_name = os.path.basename(sys.argv[1])
sd_name = os.path.splitext(model_name)[0] + ".pickle"
with open(f"./_sd_expand_min/{sd_name}", "wb") as handle:
pickle.dump(sd, handle)

print(f"Succession diagram size:", len(sd))
print(f"Minimal traps:", len(sd.minimal_trap_spaces()))

print("size, expanded, minimal")
print(f"{len(sd)}, {len(list(sd.expanded_ids()))}, {len(sd.minimal_trap_spaces())}")
print(f"{len(sd)},{len(list(sd.expanded_ids()))},{len(sd.minimal_trap_spaces())}")
18 changes: 12 additions & 6 deletions benchmark/bench_sd_expand_scc.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import sys
from biodivine_aeon import BooleanNetwork
from biobalm.SuccessionDiagram import SuccessionDiagram
from biobalm._sd_algorithms.expand_source_SCCs import expand_source_SCCs
import biobalm.SuccessionDiagram
from biobalm import SuccessionDiagram
import sys
import os
import pickle
import biobalm

# Print progress and succession diagram size.
biobalm.SuccessionDiagram.DEBUG = True
biobalm.succession_diagram.DEBUG = True

NODE_LIMIT = 1_000_000
DEPTH_LIMIT = 10_000
Expand All @@ -18,8 +19,13 @@
fully_expanded = expand_source_SCCs(sd, check_maa=False)
assert fully_expanded

model_name = os.path.basename(sys.argv[1])
sd_name = os.path.splitext(model_name)[0] + ".pickle"
with open(f"./_sd_expand_scc/{sd_name}", "wb") as handle:
pickle.dump(sd, handle)

print(f"Succession diagram size:", len(sd))
print(f"Minimal traps:", len(sd.minimal_trap_spaces()))

print("size, expanded, minimal")
print(f"{len(sd)}, {len(list(sd.expanded_ids()))}, {len(sd.minimal_trap_spaces())}")
print(f"{len(sd)},{len(list(sd.expanded_ids()))},{len(sd.minimal_trap_spaces())}")
146 changes: 146 additions & 0 deletions biobalm/_pint_reachability.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
"""
A module which provides utility methods for reachability analysis using Pint.
Note that in biobalm, we try to treat pint as an *optional* dependency. Hence, if you
want to use any of these methods, make sure to import the module in a way that fails
safely when Pint is not installed.
"""

from __future__ import annotations

from functools import reduce
from typing import TYPE_CHECKING, cast, Iterable

from networkx import DiGraph # type: ignore
from pypint import Goal, InMemoryModel # type:ignore

from biobalm.petri_net_translation import (
place_to_variable,
optimized_recursive_dnf_generator,
)
from biobalm.types import BooleanSpace, SuccessionDiagramConfiguration

if TYPE_CHECKING:
from biodivine_aeon import Bdd


def pint_reachability(
petri_net: DiGraph,
initial_state: BooleanSpace,
target_states: Bdd,
config: SuccessionDiagramConfiguration,
) -> bool:
"""
Use Pint to check if a given `initial_state` can possibly reach some state
in the `target_states` BDD.
If the reachability analysis is inconclusive, the method
raises a `RuntimeError`.
"""
if target_states.is_false():
return False # Cannot reach a stat in an empty set.

# Build a Pint model through an automata network and copy
# over the initial condition.
pint_model = InMemoryModel(_petri_net_as_automata_network(petri_net))
for var, level in initial_state.items():
pint_model.initial_state[var] = level

goal = _pint_build_symbolic_goal(target_states, config)

def failed(*_):
raise RuntimeError("Cannot verify.")

return pint_model.reachability(goal=goal, fallback=failed) # type: ignore


def _pint_build_symbolic_goal(
states: Bdd, config: SuccessionDiagramConfiguration
) -> Goal:
"""
A helper method which (very explicitly) converts a set of states
represented through a BDD into a Pint `Goal`.
Note that if `GOAL_SIZE_LIMIT` is exceeded, a partial goal is returned
that may not cover all the states in the argument `Bdd`. This avoids
exceeding the argument list size limit, but introduces additional source
of incompleteness into the reachability process.
"""
assert not states.is_false()

goals: list[Goal] = []
limit = config["pint_goal_size_limit"]
for clause in optimized_recursive_dnf_generator(states):
named_clause = {
states.__ctx__().get_variable_name(var): int(value)
for var, value in clause.items()
}

limit -= len(named_clause)
if limit < 0:
# If the goal is too large to be passed as a command line argument,
# break here and don't continue. This is not ideal but I'm not sure
# how to fix this other than modifying `pint` itself.
if config["debug"]:
print(
"WARNING: `pint` goal size limit exceeded. A partial goal is used."
)
break

goal_atoms = [f"{var}={level}" for var, level in named_clause.items()]
goals.append(Goal(",".join(goal_atoms)))

return reduce(lambda a, b: a | b, goals)


def _petri_net_as_automata_network(petri_net: DiGraph) -> str:
"""
Takes a Petri net which was created by implicant encoding from a Boolean network,
and builds an automata network file (`.an`) compatible with the Pint tool.
"""
automata_network = ""

# Go through all PN places and save them as model variables.
variable_set: set[str] = set()
for place, kind in petri_net.nodes(data="kind"): # type: ignore
if kind != "place":
continue
variable_set.add(place_to_variable(place)[0]) # type: ignore[reportUnknownArgumentType] # noqa
variables = sorted(variable_set)

# Declare all variables with 0/1 domains.
for var in variables:
automata_network += f'"{var}" [0, 1]\n'

for transition, kind in petri_net.nodes(data="kind"): # type: ignore
if kind != "transition":
continue

predecessors = set(cast(Iterable[str], petri_net.predecessors(transition))) # type: ignore
successors = set(cast(Iterable[str], petri_net.successors(transition))) # type: ignore

# The value under modification is the only
# value that is different between successors and predecessors.
source_place = next(iter(predecessors - successors))
target_place = next(iter(successors - predecessors))

(s_var, s_level) = place_to_variable(source_place)
(t_var, t_level) = place_to_variable(target_place)
assert s_var == t_var

# The remaining places represent the necessary conditions.
# Here, we transform them into a text format.
condition_places = sorted(predecessors.intersection(successors))
condition_tuples = [place_to_variable(p) for p in condition_places]
conditions = [f'"{var}"={int(level)}' for var, level in condition_tuples]

# A pint rule consists of a variable name, value transition,
# and a list of necessary conditions for the transition (if any).
if len(conditions) == 0:
rule = f'"{s_var}" {int(s_level)} -> {int(t_level)}\n'
else:
rule = f"\"{s_var}\" {int(s_level)} -> {int(t_level)} when {' and '.join(conditions)}\n"

automata_network += rule

return automata_network
Loading

2 comments on commit 0438418

@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
biobalm
   _pint_reachability.py615018%24, 40–54, 69–93, 101–146
   control.py1141488%107, 119, 125, 129, 134, 143–159, 477, 480, 493
   interaction_graph_utils.py38489%11–13, 151–152
   petri_net_translation.py1491193%22–26, 79, 136, 308–309, 333–334, 343, 452
   space_utils.py1321688%26–28, 104–110, 133–139, 414, 462
   succession_diagram.py3816683%6, 118, 207–212, 225, 272–279, 383–390, 407–408, 418, 424, 540, 627–633, 749, 752, 870–888, 920, 930, 933, 970, 973, 980, 1031, 1045, 1125, 1308, 1319, 1327, 1355, 1370, 1382, 1387, 1393
   symbolic_utils.py32584%10, 39–44, 100, 128
   trappist_core.py1832785%14–18, 55, 57, 92, 215, 217, 219, 247–250, 254–256, 276–282, 340, 342, 372, 420, 422
biobalm/_sd_algorithms
   expand_attractor_seeds.py60788%6, 28, 42, 109–114, 119
   expand_bfs.py28196%6
   expand_dfs.py30197%6
   expand_minimal_spaces.py37295%6, 31
   expand_source_SCCs.py122497%14–16, 88, 133
   expand_to_target.py31390%6, 38, 43
biobalm/_sd_attractors
   attractor_candidates.py2638966%13–15, 26–27, 93, 101, 107–108, 130, 152, 187, 193–204, 223, 239–316, 321, 325, 331, 337, 348, 372, 377, 381, 387, 389–427, 500, 571–572, 673
   attractor_symbolic.py1141686%6–7, 75, 88–92, 103, 112, 144, 179, 191–193, 202, 230, 236
TOTAL186031683% 

Tests Skipped Failures Errors Time
357 0 💤 0 ❌ 0 🔥 44.988s ⏱️

@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
biobalm
   _pint_reachability.py615018%24, 40–54, 69–93, 101–146
   control.py1141488%107, 119, 125, 129, 134, 143–159, 477, 480, 493
   interaction_graph_utils.py38489%11–13, 151–152
   petri_net_translation.py1491193%22–26, 79, 136, 308–309, 333–334, 343, 452
   space_utils.py1321688%26–28, 104–110, 133–139, 414, 462
   succession_diagram.py3816683%6, 118, 207–212, 225, 272–279, 383–390, 407–408, 418, 424, 540, 627–633, 749, 752, 870–888, 920, 930, 933, 970, 973, 980, 1031, 1045, 1125, 1308, 1319, 1327, 1355, 1370, 1382, 1387, 1393
   symbolic_utils.py32584%10, 39–44, 100, 128
   trappist_core.py1832785%14–18, 55, 57, 92, 215, 217, 219, 247–250, 254–256, 276–282, 340, 342, 372, 420, 422
biobalm/_sd_algorithms
   expand_attractor_seeds.py60788%6, 28, 42, 109–114, 119
   expand_bfs.py28196%6
   expand_dfs.py30197%6
   expand_minimal_spaces.py37295%6, 31
   expand_source_SCCs.py122497%14–16, 88, 133
   expand_to_target.py31390%6, 38, 43
biobalm/_sd_attractors
   attractor_candidates.py2638966%13–15, 26–27, 93, 101, 107–108, 130, 152, 187, 193–204, 223, 239–316, 321, 325, 331, 337, 348, 372, 377, 381, 387, 389–427, 500, 571–572, 673
   attractor_symbolic.py1141686%6–7, 75, 88–92, 103, 112, 144, 179, 191–193, 202, 230, 236
TOTAL186031683% 

Tests Skipped Failures Errors Time
357 0 💤 0 ❌ 0 🔥 44.571s ⏱️

Please sign in to comment.