Skip to content

Commit

Permalink
Alternative two-way reachability method.
Browse files Browse the repository at this point in the history
  • Loading branch information
daemontus committed Sep 4, 2024
1 parent 0c6202e commit 1689082
Showing 1 changed file with 125 additions and 29 deletions.
154 changes: 125 additions & 29 deletions biobalm/_sd_attractors/attractor_symbolic.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
from biobalm.symbolic_utils import state_list_to_bdd
from biobalm.types import BooleanSpace
import biodivine_aeon
import copy


def symbolic_attractor_fallback(
Expand Down Expand Up @@ -262,6 +263,7 @@ def symbolic_attractor_test(
if not incompatible.is_false():
conflict_vars.append(var)
conflict_vars = sort_variable_list(conflict_vars)
all_conflict_vars = copy.copy(conflict_vars)

# Remaining network variables that are still relevant, but may not
# be necessary to reach `avoid`.
Expand All @@ -275,11 +277,29 @@ def symbolic_attractor_test(
f"[{node_id}] > Start symbolic reachability with {len(conflict_vars)} conflict variables and {len(other_vars)} other variables."
)

# This algorithm has two modes of operation: If `avoid` is `None`,
# then the only thing that we can do is compute forward reachability.
# However, if we have any avoid states, we can run two reachability
# operations that interleave each other: One forward from the
# pivot vertex, the other backward from the `avoid` set. If these two
# sets ever intersect, we know that there is a path from pivot into the
# `avoid` set. To further speed up the computation, we first prioritize
# the variables in which the pivot differs from the avoid set (so called
# conflict variables). If all such variables are covered, we then consider
# those that are close to the conflict variables in terms of regulations
# (i.e. if we can't update a conflict variable, we ideally want to update
# one that regulates it).

# True if the main cycle completes with all saturation procedures fully
# completed and no unprocessed variables remaining.
all_done = False

while not all_done:
all_done = True

# Saturate reach set with currently selected variables.
# Saturate reach_set with currently selected variables, but only if
# it's symbolic size is smaller than that of the avoid set (reach set
# tends to grow quite large and we'd like to avoid that).
saturation_done = False
while not saturation_done:
if avoid is not None and not avoid.intersect(reach_set).is_empty():
Expand All @@ -291,41 +311,117 @@ def symbolic_attractor_test(
for var in saturated_vars:
successors = graph.var_post_out(var, reach_set)
if not successors.is_empty():
reach_set = reach_set.union(successors)
saturation_done = False
if reach_set.symbolic_size() > 100_000 and sd.config["debug"]:
print(
f"[{node_id}] > Saturation({len(saturated_vars)}) Expanded reach_set: {reach_set}"
)
break
all_done = False # The main loop should continue.
updated = reach_set.union(successors)
no_avoid = avoid is None
avoid_is_larger = (
avoid is not None
) and avoid.symbolic_size() >= updated.symbolic_size()
all_variables_done = (
len(conflict_vars) == 0 and len(other_vars) == 0
)
if no_avoid or avoid_is_larger or all_variables_done:
reach_set = updated
saturation_done = False
if reach_set.symbolic_size() > 100_000 and sd.config["debug"]:
print(
f"[{node_id}] > Saturation({len(saturated_vars)}) Incremented forward reach set: {reach_set}"
)
break

if avoid is not None:
# If `avoid` is not `None`, we also want to expand it backwards.
saturation_done = False
while not saturation_done:
if not avoid.intersect(reach_set).is_empty():
if sd.config["debug"]:
print(f"[{node_id}] > Discovered avoid state. Done.")
return None

saturation_done = True
for var in saturated_vars:
predecessors = graph.var_pre_out(var, avoid)
if not predecessors.is_empty():
all_done = False
avoid = avoid.union(predecessors)
saturation_done = False
if avoid.symbolic_size() > 100_000 and sd.config["debug"]:
print(
f"[{node_id}] > Saturation({len(saturated_vars)}) Incremented backward avoid set: {avoid}"
)
break

if sd.config["debug"] and (
reach_set.symbolic_size() > 100_000
or (avoid is not None and avoid.symbolic_size() > 100_000)
):
print(
f"[{node_id}] > Saturation({len(saturated_vars)}) Finished with avoid set {avoid} and reach set {reach_set}."
)

# Once saturated, try to expand the saturated
# collection with either a conflict variable or
# other variable.

# First try conflict vars, then other vars.
for var in conflict_vars + other_vars:
successors = graph.var_post_out(var, reach_set)
if not successors.is_empty():
reach_set = reach_set.union(successors)
all_done = False

# This is a bit wasteful but at this point it
# should be irrelevant for performance.
if var in conflict_vars:
conflict_vars.remove(var)

if var in other_vars:
other_vars.remove(var)
# First, we sort the non-conflict variables based on their
# distance towards the conflict variables.
network = sd.node_percolated_network(node_id)
distances: dict[VariableId, int] = {
v: network.variable_count() for v in network.variables()
}
visited: set[VariableId]
if len(conflict_vars) > 0:
visited = set(conflict_vars)
current_level = set(conflict_vars)
else:
visited = set(all_conflict_vars)
current_level = set(all_conflict_vars)

next_level: set[VariableId] = set()

distance = 0
while len(current_level) > 0:
for var in current_level:
distances[var] = min(distance, distances[var])
for s in network.predecessors(var):
if s not in visited:
visited.add(s)
next_level.add(s)
current_level = next_level
next_level = set()
distance += 1

other_sorted = sorted(other_vars, key=lambda x: distances[x])
for var in conflict_vars + other_sorted:
can_go_fwd = graph.var_post_out(var, reach_set)
if avoid is not None:
can_go_bwd = graph.var_pre_out(var, avoid)
else:
can_go_bwd = graph.mk_empty_colored_vertices()

if can_go_fwd.is_empty() and can_go_bwd.is_empty():
continue

all_done = False

reach_set = reach_set.union(can_go_fwd)
if avoid is not None:
avoid = avoid.union(can_go_bwd)

if var in conflict_vars:
conflict_vars.remove(var)
if var in other_vars:
other_vars.remove(var)

saturated_vars.append(var)
saturated_vars = sort_variable_list(saturated_vars)

saturated_vars.append(var)
saturated_vars = sort_variable_list(saturated_vars)
if sd.config["debug"]:
print(
f"[{node_id}] > Saturation({len(saturated_vars)}) Added saturation variable. {len(conflict_vars)} conflict and {len(other_vars)} other variables remaining."
)

if sd.config["debug"]:
print(
f"[{node_id}] > Saturation({len(saturated_vars)}) Added saturation variable. {len(conflict_vars)} conflict and {len(other_vars)} other variables remaining."
)
break
break

if sd.config["debug"]:
print(f"[{node_id}] > Reachability completed with {reach_set}.")
Expand Down

1 comment on commit 1689082

@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.py3917481%6, 123, 213–218, 231, 278–285, 389–396, 413–414, 424, 430, 546, 633–639, 755, 758, 853–867, 898–916, 948, 958, 961, 1001, 1008, 1059, 1077, 1199, 1385, 1396, 1404, 1447, 1459, 1464, 1470
   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.py42393%6, 28, 42
   expand_source_SCCs.py1111686%11–13, 50, 69, 77, 82, 103, 112, 120, 131, 140, 143, 167, 179, 242–243
   expand_source_blocks.py1212083%10, 30, 42–45, 57, 67, 74, 79, 82, 141, 167, 176, 210–211, 215, 225, 231, 240
   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.py2123882%6–7, 38–39, 57, 66–82, 85, 90, 171, 184–188, 199, 208, 240, 276, 306–308, 327, 337–339, 349, 358, 400, 420, 427
TOTAL210438382% 

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

Please sign in to comment.