Skip to content

Commit 3b78128

Browse files
Add per-edge repetition bounds and safe walk pruning
Introduces per-edge upper bounds for edge variables via max_edge_repetition_dict, replacing global max_edge_repetition. Adds reachability-based pruning to fix edge variables to zero for incompatible edges in safe walks. Updates solver wrapper to support batched bound updates and per-index bounds for variable creation. Implements stDiGraph.compute_edge_max_reachable_value for efficient per-edge bound computation. Updates all relevant models and demo to use these features and options.
1 parent 589ada0 commit 3b78128

File tree

8 files changed

+435
-102
lines changed

8 files changed

+435
-102
lines changed

examples/cycles_demo.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,11 @@ def test_min_flow_decomp(filename: str):
2828
optimization_options={
2929
"optimize_with_safe_sequences": True, # set to false to deactivate the safe sequences optimization
3030
"optimize_with_safe_sequences_allow_geq_constraints": True,
31-
"optimize_with_safe_sequences_fix_via_bounds": False,
31+
"optimize_with_safe_sequences_fix_via_bounds": True,
32+
"optimize_with_safe_sequences_fix_zero_edges": True,
3233
},
3334
solver_options={
34-
"external_solver": "gurobi", # we can try also "highs" at some point
35+
"external_solver": "highs", # we can try also "highs" at some point
3536
"time_limit": 300, # 300s = 5min, is it ok?
3637
},
3738
)
@@ -141,6 +142,7 @@ def process_solution(model):
141142
print("edge_variables_total:", solve_statistics['edge_variables_total']) # number of edges * number of solution walks in the last iteration
142143
print("edge_variables=1:", solve_statistics['edge_variables=1'])
143144
print("edge_variables>=1:", solve_statistics['edge_variables>=1'])
145+
print("edge_variables=0:", solve_statistics['edge_variables=0'])
144146
print("graph_width:", solve_statistics['graph_width']) # the the minimum number of s-t walks needed to cover all edges
145147
print("model_status:", solve_statistics['model_status'])
146148
print("solve_time:", solve_statistics['solve_time']) # time taken by the ILP for a given k, or by MFD to iterate through k and do small internal things
@@ -149,10 +151,10 @@ def process_solution(model):
149151
print("avg_size_of_non_trivial_SCC:", solve_statistics['avg_size_of_non_trivial_SCC']) # size = number of edges
150152

151153
def main():
152-
# test_min_flow_decomp(filename = "tests/cyclic_graphs/gt3.kmer15.(130000.132000).V23.E32.cyc100.graph")
154+
test_min_flow_decomp(filename = "tests/cyclic_graphs/gt3.kmer15.(130000.132000).V23.E32.cyc100.graph")
153155
test_min_flow_decomp(filename = "tests/cyclic_graphs/gt5.kmer27.(1300000.1400000).V809.E1091.mincyc1000.graph")
154-
# test_least_abs_errors(filename = "tests/cyclic_graphs/gt5.kmer27.(655000.660000).V18.E27.mincyc4.e0.75.graph")
155-
# test_min_path_error(filename = "tests/cyclic_graphs/gt5.kmer27.(655000.660000).V18.E27.mincyc4.e0.75.graph")
156+
test_least_abs_errors(filename = "tests/cyclic_graphs/gt5.kmer27.(655000.660000).V18.E27.mincyc4.e0.75.graph")
157+
test_min_path_error(filename = "tests/cyclic_graphs/gt5.kmer27.(655000.660000).V18.E27.mincyc4.e0.75.graph")
156158

157159
if __name__ == "__main__":
158160
# Configure logging

flowpaths/abstractsourcesinkgraph.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,3 +133,4 @@ def get_max_flow_value_and_check_non_negative_flow(
133133
)
134134
w_max = max(w_max, data[flow_attr])
135135
return w_max
136+

flowpaths/abstractwalkmodeldigraph.py

Lines changed: 174 additions & 24 deletions
Large diffs are not rendered by default.

flowpaths/kflowdecompcycles.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,8 @@ def __init__(
167167
super().__init__(
168168
G=self.G,
169169
k=self.k,
170-
max_edge_repetition=self.w_max,
170+
# max_edge_repetition=self.w_max,
171+
max_edge_repetition_dict=self.G.compute_edge_max_reachable_value(flow_attr=self.flow_attr),
171172
subset_constraints=self.subset_constraints,
172173
subset_constraints_coverage=self.subset_constraints_coverage,
173174
optimization_options=self.optimization_options,

flowpaths/kleastabserrorscycles.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -229,7 +229,8 @@ def __init__(
229229
super().__init__(
230230
G=self.G,
231231
k=self.k,
232-
max_edge_repetition=self.w_max,
232+
# max_edge_repetition=self.w_max,
233+
max_edge_repetition_dict=self.G.compute_edge_max_reachable_value(flow_attr=self.flow_attr),
233234
subset_constraints=self.subset_constraints,
234235
subset_constraints_coverage=self.subset_constraints_coverage,
235236
optimization_options=self.optimization_options,

flowpaths/kminpatherrorcycles.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -226,7 +226,8 @@ def __init__(
226226
super().__init__(
227227
G=self.G,
228228
k=self.k,
229-
max_edge_repetition=self.w_max,
229+
# max_edge_repetition=self.w_max,
230+
max_edge_repetition_dict=self.G.compute_edge_max_reachable_value(flow_attr=self.flow_attr),
230231
subset_constraints=self.subset_constraints,
231232
subset_constraints_coverage=self.subset_constraints_coverage,
232233
optimization_options=self.optimization_options,

flowpaths/stdigraph.py

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from flowpaths.stdag import stDAG
55
import flowpaths.utils as utils
66
from flowpaths.abstractsourcesinkgraph import AbstractSourceSinkGraph
7+
from typing import Dict, Tuple
78

89

910
class stDiGraph(AbstractSourceSinkGraph):
@@ -374,4 +375,95 @@ def get_longest_incompatible_sequences(self, sequences: list) -> list:
374375
seq_idx_set.add(seq_idx)
375376

376377
return incompatible_sequences
378+
379+
def compute_edge_max_reachable_value(self, flow_attr: str) -> Dict[Tuple[str, str], float]:
380+
"""For each base edge (u,v), compute the maximum ``flow_attr`` over:
381+
- the edge (u,v) itself,
382+
- any edge reachable forward from v,
383+
- any edge whose head can reach u (i.e., backward reachability to u).
384+
385+
Efficiently uses the precomputed SCC condensation and runs dynamic programming on
386+
the condensation DAG.
387+
388+
Returns a dict mapping each original edge (u,v) to the computed float.
389+
390+
If an edge has a missing ``flow_attr'' (the source and sink edges) we treat its flow value as 0.
391+
392+
Examples
393+
--------
394+
>>> import networkx as nx
395+
>>> from flowpaths.stdigraph import stDiGraph
396+
>>> G = nx.DiGraph()
397+
>>> G.add_edge("a", "b", flow=1)
398+
>>> G.add_edge("b", "c", flow=5)
399+
>>> G.add_edge("c", "a", flow=3) # cycle among a,b,c
400+
>>> G.add_edge("c", "d", flow=2)
401+
>>> H = stDiGraph(G)
402+
>>> res = H.compute_edge_max_reachable_value("flow")
403+
>>> # Every edge can reach an edge of weight 5 within the SCC or forward
404+
>>> res[("a", "b")] == 5 and res[("b", "c")] == 5 and res[("c", "a")] == 5 and res[("c", "d")] == 5
405+
True
406+
"""
407+
C: nx.DiGraph = self._condensation
408+
mapping = C.graph["mapping"] # original node -> condensation node (int)
409+
410+
# 2) Precompute per-SCC local maxima and per-edge weights.
411+
#
412+
# - local_out[c]: the max weight of any edge whose TAIL is inside SCC `c`.
413+
# This represents the best edge you can reach by going forward from any
414+
# node in this SCC (including edges that stay inside the SCC or that exit it).
415+
#
416+
# - local_in[c]: the max weight of any edge whose HEAD is inside SCC `c`.
417+
# This captures the best edge that can reach this SCC (used for backward reachability).
418+
#
419+
# We also record each edge's own weight so the final answer includes (u,v) itself.
420+
local_out = {c: 0.0 for c in C.nodes()}
421+
local_in = {c: 0.0 for c in C.nodes()}
422+
423+
edge_weight: Dict[Tuple[str, str], float] = {}
424+
for u, v, data in self.edges(data=True):
425+
w = float(data.get(flow_attr, 0.0))
426+
edge_weight[(u, v)] = w
427+
cu = mapping[u]
428+
cv = mapping[v]
429+
if w > local_out[cu]:
430+
local_out[cu] = w
431+
if w > local_in[cv]:
432+
local_in[cv] = w
433+
434+
topological_sort = list(nx.topological_sort(C))
435+
436+
# 3) Forward DP over the condensation DAG to compute, for each SCC `c`, the
437+
# maximum edge weight reachable from `c` by moving forward along DAG edges.
438+
# Because SCCs are contracted, this also correctly accounts for reachability
439+
# within cycles (inside a single SCC).
440+
max_desc = {c: local_out[c] for c in C.nodes()}
441+
for c in reversed(topological_sort):
442+
for s in C.successors(c):
443+
if max_desc[s] > max_desc[c]:
444+
max_desc[c] = max_desc[s]
445+
446+
# 4) Backward DP (propagated forward along edges) to compute, for each SCC `c`, the
447+
# maximum edge weight among edges whose HEAD can reach `c` (i.e. along the
448+
# reversed condensation DAG). We propagate `local_in` from ancestors to successors
449+
# in topological order.
450+
max_anc = {c: local_in[c] for c in C.nodes()}
451+
for c in topological_sort:
452+
for s in C.successors(c):
453+
if max_anc[c] > max_anc[s]:
454+
max_anc[s] = max_anc[c]
455+
456+
# 5) For each original edge (u,v), combine:
457+
# - the edge's own weight,
458+
# - the best reachable-from-SCC(v) weight (forward), and
459+
# - the best reaching-SCC(u) weight (backward).
460+
#
461+
# If no reachable candidate exists, the value is 0.0 by construction of locals.
462+
result: Dict[Tuple[str, str], float] = {}
463+
for u, v in self.edges():
464+
cu = mapping[u]
465+
cv = mapping[v]
466+
result[(u, v)] = max(edge_weight[(u, v)], max_desc[cv], max_anc[cu])
467+
468+
return result
377469

0 commit comments

Comments
 (0)