Skip to content

Commit

Permalink
Merge pull request #121 from jcrozum/expansion-limits
Browse files Browse the repository at this point in the history
Expansion limits
  • Loading branch information
daemontus authored Aug 23, 2024
2 parents 0f5cfc3 + 84b01ef commit b32bdd2
Show file tree
Hide file tree
Showing 5 changed files with 94 additions and 15 deletions.
2 changes: 1 addition & 1 deletion biobalm/_sd_algorithms/expand_attractor_seeds.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ def expand_attractor_seeds(sd: SuccessionDiagram, size_limit: int | None = None)
# because for every attractor in a minimal trap space, we already have the
# closest trap space, now we just need to do the same for (potential)
# motif-avoidant attractors.
sd.expand_minimal_spaces(size_limit)
sd.expand_minimal_spaces(size_limit=size_limit)

if sd.config["debug"]:
print(
Expand Down
16 changes: 11 additions & 5 deletions biobalm/_sd_algorithms/expand_minimal_spaces.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,24 @@
from biobalm.trappist_core import trappist


def expand_minimal_spaces(sd: SuccessionDiagram, size_limit: int | None = None) -> bool:
def expand_minimal_spaces(
sd: SuccessionDiagram, node_id: int | None, size_limit: int | None = None
) -> bool:
"""
See `SuccessionDiagram.expand_minimal_spaces` for documentation.
"""

minimal_traps = trappist(sd.petri_net, problem="min")
if node_id is None:
node_id = sd.root()

root = sd.root()
pn = sd.node_percolated_petri_net(node_id, compute=True)
node_space = sd.node_data(node_id)["space"]

seen = set([root])
minimal_traps = trappist(network=pn, problem="min", ensure_subspace=node_space)

stack: list[tuple[int, list[int] | None]] = [(root, None)]
seen = set([node_id])

stack: list[tuple[int, list[int] | None]] = [(node_id, None)]

while len(stack) > 0:
(node, successors) = stack.pop()
Expand Down
19 changes: 16 additions & 3 deletions biobalm/_sd_algorithms/expand_source_blocks.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,17 @@
def expand_source_blocks(
sd: SuccessionDiagram,
check_maa: bool = True,
size_limit: int | None = None,
optimize_source_nodes: bool = True,
) -> bool:
"""
Base correctness assumptions:
- Expanding two minimal blocks is always independent.
-
- Expanding two minimal blocks is always independent. Every minimal trap space is contained in one of the maximal trap
spaces of a single block. Furthermore, expanding a minimal block cannot intrfere with the expansion of the other
stable motifs.
- Any path in the "inner" succession diagram based on a single block can be reproduced in the larger succession diagram,
meaning absence of attractors in the inner diagram proves absence of attractors in the outer diagram.
"""

if sd.config["debug"]:
Expand All @@ -41,14 +46,17 @@ def expand_source_blocks(
print(f" > Computed source/input variable(s): {sources}")

# get source nodes combinations and expand root node
if len(sources) != 0:
if len(sources) != 0 and optimize_source_nodes:
# If there are too many source nodes, this can generate an absurdly large SD.
# This would be a problem even without the SCC expansion, but we can just
# stop the whole thing faster because we know how many nodes it generates.
if 2 ** len(sources) > sd.config["max_motifs_per_node"]:
raise RuntimeError(
f"Exceeded the maximum amount of stable motifs per node ({sd.config['max_motifs_per_node']}; see `SuccessionDiagramConfiguration.max_motifs_per_node`)."
)
elif size_limit is not None and 2 ** len(sources) > size_limit:
# Cannot expand. Size limit would be exceeded.
return False
else:
if sd.config["debug"]:
print(
Expand Down Expand Up @@ -84,6 +92,11 @@ def expand_source_blocks(
# We re-discovered a previously expanded node.
continue

# Only continue if the succession diagram isn't too large.
if (size_limit is not None) and (len(sd) >= size_limit):
# Size limit reached.
return False

# Compute successors as if this was a normal expansion procedure.
successors = sd.node_successors(node, compute=True)
# Sort successors to avoid non-determinism.
Expand Down
39 changes: 33 additions & 6 deletions biobalm/succession_diagram.py
Original file line number Diff line number Diff line change
Expand Up @@ -1193,17 +1193,35 @@ def expand_scc(self, find_motif_avoidant_attractors: bool = True) -> bool:
"""
return expand_source_SCCs(self, check_maa=find_motif_avoidant_attractors)

def expand_block(self, find_motif_avoidant_attractors: bool = True) -> bool:
def expand_block(
self,
find_motif_avoidant_attractors: bool = True,
size_limit: int | None = None,
optimize_source_nodes: bool = True,
) -> bool:
"""
Expand the succession diagram using the source block method.
There is a minor difference in behavior depending on `find_motif_avoidant_attractors`.
If set to `False`, the expansion only expands one "source block" for each node,
without checking any attractor properties. If set to `True`, the expansion might
expand some nodes fully to uncover nodes that precisely cover motif
avoidant attractors.
"""
return expand_source_blocks(self, find_motif_avoidant_attractors)
avoidant attractors. As a byproduct, if set to `True` and no motif avoidant attractors
are detected for some node, this is result is saved and the attractors don't
need to be recomputed later.
By default, the method also detects any source nodes and directly expands these
into trap spaces where all source nodes are fixed. This has no correctness impact on
attractor search and always produces a smaller succession diagram, but if you need to
obtain a succession diagram where this does not happen (e.g. for testing), you can turn
this off using `optimize_source_nodes`.
"""
return expand_source_blocks(
self,
find_motif_avoidant_attractors,
size_limit=size_limit,
optimize_source_nodes=optimize_source_nodes,
)

def expand_bfs(
self,
Expand Down Expand Up @@ -1257,7 +1275,9 @@ def expand_dfs(
"""
return expand_dfs(self, node_id, dfs_stack_limit, size_limit)

def expand_minimal_spaces(self, size_limit: int | None = None) -> bool:
def expand_minimal_spaces(
self, node_id: int | None = None, size_limit: int | None = None
) -> bool:
"""
Expands the succession diagram in a way that guarantees every minimal
trap space to be reachable from the root node, but otherwise (greedily)
Expand All @@ -1272,8 +1292,15 @@ def expand_minimal_spaces(self, size_limit: int | None = None) -> bool:
vary if some nodes are already expanded initially. In such case, the
procedure still tries to avoid expanding unnecessary nodes, which means
existing expanded nodes can be prioritised over the "canonical" ones.
Optionally, you can start the expansion from a specific node that is not
the root using `node_id`, or restrict the size of the succession diagram
with `size_limit`.
Returns `True` if the expansion procedure terminated without exceeding
the size limit.
"""
return expand_minimal_spaces(self, size_limit)
return expand_minimal_spaces(self, node_id, size_limit)

def expand_attractor_seeds(self, size_limit: int | None = None) -> bool:
"""
Expand Down
33 changes: 33 additions & 0 deletions tests/succession_diagram_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -171,13 +171,46 @@ def test_expansion_comparisons(network_file: str):
# succession_diagram is too large for this test.
return

# Normal minimal trap space expansion.
sd_min = SuccessionDiagram(bn)
assert sd_min.expand_minimal_spaces(size_limit=NODE_LIMIT)

assert sd_bfs.is_isomorphic(sd_dfs)
assert sd_min.is_subgraph(sd_bfs)
assert sd_min.is_subgraph(sd_dfs)

# Expand the first node fully, and then expand the rest
# until minimal trap spaces are found.
sd_min_larger = SuccessionDiagram(bn)
sd_min_larger._expand_one_node(sd_min_larger.root()) # type: ignore
for node_id in sd_min_larger.node_ids():
if not sd_min_larger.node_data(node_id)["expanded"]:
assert sd_min_larger.expand_minimal_spaces(node_id=node_id)

assert sd_min_larger.is_subgraph(sd_bfs)
assert sd_min_larger.is_subgraph(sd_dfs)
assert len(sd_min_larger) >= len(sd_min)

# Normal block expansion with size limit.
sd_block = SuccessionDiagram(bn)
assert sd_block.expand_block(
find_motif_avoidant_attractors=True,
size_limit=NODE_LIMIT,
optimize_source_nodes=False, # Needed for compatibility.
)

assert sd_block.is_subgraph(sd_bfs)
assert sd_block.is_subgraph(sd_dfs)

block_trap = [
sd_block.node_data(i)["space"] for i in sd_block.minimal_trap_spaces()
]
min_trap = [sd_min.node_data(i)["space"] for i in sd_min.minimal_trap_spaces()]
for t in block_trap:
assert t in min_trap
for t in min_trap:
assert t in block_trap

sd_attr = SuccessionDiagram(bn)
assert sd_attr.expand_attractor_seeds(size_limit=NODE_LIMIT)

Expand Down

1 comment on commit b32bdd2

@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.py52688%11–13, 151–152, 222–223
   petri_net_translation.py1491292%22–26, 79, 136, 234, 308–309, 333–334, 343, 452
   space_utils.py1322085%26–28, 104–110, 133–139, 347–350, 414, 462
   succession_diagram.py3806483%6, 120, 210–215, 228, 275–282, 386–393, 410–411, 421, 427, 543, 630–636, 752, 755, 873–891, 923, 933, 936, 976, 983, 1034, 1052, 1174, 1360, 1371, 1379, 1422, 1434, 1439, 1445
   symbolic_utils.py32584%10, 39–44, 100, 128
   trappist_core.py1842388%14–18, 55, 57, 92, 215, 217, 219, 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.py40295%6, 37
   expand_source_SCCs.py1111686%11–13, 50, 69, 77, 82, 103, 112, 120, 131, 140, 143, 167, 179, 242–243
   expand_source_blocks.py1151587%11, 31, 46, 54, 59, 62, 86, 98, 141, 167, 176, 209, 219, 225, 234
   expand_to_target.py31390%6, 38, 43
biobalm/_sd_attractors
   attractor_candidates.py2659066%13–15, 26–27, 93, 101, 107–108, 130, 152, 187, 193–204, 223, 239–320, 325, 329, 335, 341, 356, 383, 388, 392, 398, 400–438, 511, 582–583, 684
   attractor_symbolic.py1141686%6–7, 75, 88–92, 103, 112, 144, 179, 191–193, 202, 230, 236
TOTAL198734583% 

Tests Skipped Failures Errors Time
359 0 💤 0 ❌ 0 🔥 53.983s ⏱️

Please sign in to comment.