Skip to content

Commit a3d78f6

Browse files
Improve SCC edge handling
Refactored SCC edge checks to use a unified is_scc_edge method, updated edge upper bounds for non-SCC edges, and improved logging for initialization and solving steps. Enhanced solve statistics reporting in MinFlowDecompCycles and clarified time tracking for ILP and model phases. Also adjusted demo script to print new statistics and commented out some test calls for clarity.
1 parent 3b78128 commit a3d78f6

File tree

5 files changed

+48
-27
lines changed

5 files changed

+48
-27
lines changed

examples/cycles_demo.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ def test_min_flow_decomp(filename: str):
2929
"optimize_with_safe_sequences": True, # set to false to deactivate the safe sequences optimization
3030
"optimize_with_safe_sequences_allow_geq_constraints": True,
3131
"optimize_with_safe_sequences_fix_via_bounds": True,
32-
"optimize_with_safe_sequences_fix_zero_edges": True,
32+
"optimize_with_safe_sequences_fix_zero_edges": False,
3333
},
3434
solver_options={
3535
"external_solver": "highs", # we can try also "highs" at some point
@@ -146,15 +146,16 @@ def process_solution(model):
146146
print("graph_width:", solve_statistics['graph_width']) # the the minimum number of s-t walks needed to cover all edges
147147
print("model_status:", solve_statistics['model_status'])
148148
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+
print("solve_time_ilp:", solve_statistics['solve_time_ilp']) # time taken by the ILP for a given k, or by MFD to iterate through k and do small internal things
149150
print("number_of_nontrivial_SCCs:", solve_statistics['number_of_nontrivial_SCCs']) # trivial = at least one edge
150151
print("size_of_largest_SCC:", solve_statistics['size_of_largest_SCC']) # size = number of edges
151152
print("avg_size_of_non_trivial_SCC:", solve_statistics['avg_size_of_non_trivial_SCC']) # size = number of edges
152153

153154
def main():
154-
test_min_flow_decomp(filename = "tests/cyclic_graphs/gt3.kmer15.(130000.132000).V23.E32.cyc100.graph")
155+
# test_min_flow_decomp(filename = "tests/cyclic_graphs/gt3.kmer15.(130000.132000).V23.E32.cyc100.graph")
155156
test_min_flow_decomp(filename = "tests/cyclic_graphs/gt5.kmer27.(1300000.1400000).V809.E1091.mincyc1000.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")
157+
# test_least_abs_errors(filename = "tests/cyclic_graphs/gt5.kmer27.(655000.660000).V18.E27.mincyc4.e0.75.graph")
158+
# test_min_path_error(filename = "tests/cyclic_graphs/gt5.kmer27.(655000.660000).V18.E27.mincyc4.e0.75.graph")
158159

159160
if __name__ == "__main__":
160161
# Configure logging

flowpaths/abstractwalkmodeldigraph.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,11 @@ def __init__(
122122
if edge not in self.edge_upper_bounds:
123123
utils.logger.critical(f"{__name__}: Missing max_edge_repetition in max_edge_repetition_dict for edge {edge}")
124124
raise ValueError(f"Missing max_edge_repetition for edge {edge}")
125+
# We set to 1 in edge_upper_bounds if the edge is not inside an SCC of self.G,
126+
# because these edges cannot be traversed more than 1 times by any walk
127+
for edge in self.G.edges():
128+
if not self.G.is_scc_edge(edge[0], edge[1]):
129+
self.edge_upper_bounds[edge] = 1
125130

126131
self.subset_constraints = copy.deepcopy(subset_constraints)
127132
if self.subset_constraints is not None:
@@ -404,7 +409,7 @@ def _apply_safety_optimizations(self):
404409
# print("Fixing variables for safe list #", i)
405410
# iterate over the edges in the safe list to fix variables to 1
406411
for u, v in self.walks_to_fix[i]:
407-
if self.G._is_scc_edge(u, v):
412+
if self.G.is_scc_edge(u, v):
408413
if self.optimize_with_safe_sequences_allow_geq_constraints:
409414
# Raise LB via bounds only when enabled; else add constraint
410415
if self.optimize_with_safe_sequences_fix_via_bounds:
@@ -605,7 +610,7 @@ def solve(self) -> bool:
605610
# self.write_model(f"model-{self.id}.lp")
606611
start_time = time.perf_counter()
607612
self.solver.optimize()
608-
self.solve_statistics[f"solve_time"] = time.perf_counter() - start_time
613+
self.solve_statistics[f"solve_time_ilp"] = time.perf_counter() - start_time
609614
self.solve_statistics[f"model_status"] = self.solver.get_model_status()
610615
self.solve_statistics[f"number_of_nontrivial_SCCs"] = self.G.get_number_of_nontrivial_SCCs()
611616
self.solve_statistics[f"avg_size_of_non_trivial_SCC"] = self.G.get_avg_size_of_non_trivial_SCC()

flowpaths/kflowdecompcycles.py

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,8 @@ def __init__(
9595
- If the graph contains edges with negative flow values.
9696
- ValueError: If `flow_attr_origin` is not `node` or `edge`.
9797
"""
98-
98+
utils.logger.info(f"{__name__}: START initializing with graph id = {utils.fpid(G)}, k = {k}")
99+
99100
# Handling node-weighted graphs
100101
self.flow_attr_origin = flow_attr_origin
101102
if self.flow_attr_origin == "node":
@@ -164,32 +165,46 @@ def __init__(
164165
self.optimization_options["trusted_edges_for_safety"] = self.G.get_non_zero_flow_edges(flow_attr=self.flow_attr, edges_to_ignore=self.edges_to_ignore)
165166

166167
# Call the constructor of the parent class AbstractPathModelDAG
168+
# Build per-edge repetition upper bounds: use the edge flow when available,
169+
# otherwise fall back to self.w_max (e.g., for source/sink helper edges).
170+
self.edge_upper_bounds_dict = {
171+
(u, v): (data[self.flow_attr] if self.flow_attr in data else self.w_max)
172+
for u, v, data in self.G.edges(data=True)
173+
}
167174
super().__init__(
168175
G=self.G,
169176
k=self.k,
170177
# max_edge_repetition=self.w_max,
171-
max_edge_repetition_dict=self.G.compute_edge_max_reachable_value(flow_attr=self.flow_attr),
178+
max_edge_repetition_dict=self.edge_upper_bounds_dict,
172179
subset_constraints=self.subset_constraints,
173180
subset_constraints_coverage=self.subset_constraints_coverage,
174181
optimization_options=self.optimization_options,
175182
solver_options=solver_options,
176183
solve_statistics=self.solve_statistics
177184
)
178185

186+
utils.logger.debug(f"{__name__}: START create_solver_and_walks()")
179187
# This method is called from the super class AbstractWalkModelDiGraph
180188
self.create_solver_and_walks()
189+
utils.logger.debug(f"{__name__}: END create_solver_and_walks()")
181190

191+
utils.logger.debug(f"{__name__}: START encoding flow decomposition")
182192
# This method is called from the current class
183193
self._encode_flow_decomposition()
194+
utils.logger.debug(f"{__name__}: END encoding flow decomposition")
184195

196+
utils.logger.debug(f"{__name__}: START encoding given weights")
185197
# This method is called from the current class
186198
self._encode_given_weights()
199+
utils.logger.debug(f"{__name__}: END encoding given weights")
187200

188-
utils.logger.info(f"{__name__}: initialized with graph id = {utils.fpid(G)}, k = {self.k}")
201+
utils.logger.info(f"{__name__}: END initialized with graph id = {utils.fpid(G)}, k = {self.k}")
189202

190203
def _encode_flow_decomposition(self):
191204

205+
# print("edge_upper_bounds", self.edge_upper_bounds)
192206
# pi vars
207+
# edge_ubs = [self.edge_upper_bounds[(u, v)] for (u, v, i) in self.edge_indexes]
193208
self.pi_vars = self.solver.add_variables(
194209
self.edge_indexes,
195210
name_prefix="pi",

flowpaths/minflowdecompcycles.py

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,7 @@ def __init__(
165165
self.solver_options = solver_options
166166
self.time_limit = self.solver_options.get("time_limit", sw.SolverWrapper.time_limit)
167167
self.solve_time_start = None
168+
self.solve_time_ilp_total = 0
168169

169170
self.solve_statistics = {}
170171
self._solution = None
@@ -200,6 +201,7 @@ def solve(self) -> bool:
200201
This overloads the `solve()` method from `AbstractWalkModelDiGraph` class.
201202
"""
202203
self.solve_time_start = time.perf_counter()
204+
utils.logger.info(f"{__name__}: starting to solve the MinFlowDecompCycles model for graph id = {utils.fpid(self.G)}")
203205

204206
if self.optimization_options.get("optimize_with_given_weights", MinFlowDecompCycles.optimize_with_given_weights):
205207
self._solve_with_given_weights()
@@ -213,10 +215,9 @@ def solve(self) -> bool:
213215
if len(self._given_weights_model.get_solution(remove_empty_walks=True)["walks"]) == i:
214216
fd_model = self._given_weights_model
215217

216-
fd_solver_options = copy.deepcopy(self.solver_options)
217-
fd_solver_options["time_limit"] = self.time_limit - self.solve_time_elapsed
218-
219218
if fd_model is None:
219+
fd_solver_options = copy.deepcopy(self.solver_options)
220+
fd_solver_options["time_limit"] = self.time_limit - self.solve_time_elapsed
220221
fd_model = kflowdecompcycles.kFlowDecompCycles(
221222
G=self.G,
222223
flow_attr=self.flow_attr,
@@ -231,12 +232,17 @@ def solve(self) -> bool:
231232
solver_options=fd_solver_options,
232233
)
233234
fd_model.solve()
234-
self.solve_statistics = fd_model.solve_statistics
235235

236-
# If the previous run exceeded the time limit,
237-
# we still stop the search, even if we might have managed to solve it
238-
if self.solve_time_elapsed > self.time_limit:
239-
return False
236+
self.solve_statistics = fd_model.solve_statistics
237+
self.solve_time_ilp_total += self.solve_statistics.get("solve_time_ilp", 0)
238+
self.solve_statistics["solve_time"] = self.solve_time_elapsed
239+
self.solve_statistics["solve_time_ilp"] = self.solve_time_ilp_total
240+
self.solve_statistics["min_gen_set_solve_time"] = self._mingenset_model.solve_statistics.get("total_solve_time", 0) if self._mingenset_model is not None else 0
241+
242+
# If the previous run exceeded the time limit,
243+
# we still stop the search, even if we might have managed to solve it
244+
if self.solve_time_elapsed > self.time_limit:
245+
return False
240246

241247
if fd_model.is_solved():
242248
self._solution = fd_model.get_solution(remove_empty_walks=True)
@@ -245,17 +251,11 @@ def solve(self) -> bool:
245251
self._solution["_walks_internal"] = self._solution["walks"]
246252
self._solution["walks"] = self.G_internal.get_condensed_paths(self._solution["walks"])
247253
self.set_solved()
248-
self.solve_statistics = fd_model.solve_statistics
249-
# we overwrite solve_time with the total time MFD has taken
250-
self.solve_statistics["solve_time"] = time.perf_counter() - self.solve_time_start
251-
self.solve_statistics["min_gen_set_solve_time"] = self._mingenset_model.solve_statistics.get("total_solve_time", 0) if self._mingenset_model is not None else 0
252254
self.fd_model = fd_model
253255
return True
254256
elif fd_model.solver.get_model_status() == sw.SolverWrapper.infeasible_status:
255-
self.solve_statistics = fd_model.solve_statistics
256257
utils.logger.info(f"{__name__}: model is infeasible for k = {i}")
257258
else:
258-
self.solve_statistics = fd_model.solve_statistics
259259
# If the model is not solved and the status is not infeasible,
260260
# it means that the solver stopped because of an unexpected termination,
261261
# thus we cannot conclude that the model is infeasible.

flowpaths/stdigraph.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,7 @@ def _edge_to_condensation_node(self, u, v) -> str:
181181
- `ValueError` if the edge (u,v) is not inside an SCC.
182182
"""
183183

184-
if not self._is_scc_edge(u, v):
184+
if not self.is_scc_edge(u, v):
185185
utils.logger.error(f"{__name__}: Edge ({u},{v}) is not an edge inside an SCC.")
186186
raise ValueError(f"Edge ({u},{v}) is not an edge inside an SCC.")
187187

@@ -237,7 +237,7 @@ def get_width(self, edges_to_ignore: list = None) -> int:
237237
for u, v in (edges_to_ignore or []):
238238
# If (u,v) is an edge between different SCCs
239239
# Then the corresponding edge to ignore is between the two SCCs
240-
if not self._is_scc_edge(u, v):
240+
if not self.is_scc_edge(u, v):
241241
edge_multiplicity[self._edge_to_condensation_edge(u, v)] -= 1
242242
else:
243243
# (u,v) is an edge within the same SCC
@@ -286,9 +286,9 @@ def get_width(self, edges_to_ignore: list = None) -> int:
286286

287287
return width
288288

289-
def _is_scc_edge(self, u, v) -> bool:
289+
def is_scc_edge(self, u, v) -> bool:
290290
"""
291-
Returns True if (u,v) is an edge inside an SCCs, False otherwise.
291+
Returns True if (u,v) is an edge inside an SCC of self, False otherwise.
292292
"""
293293

294294
# Check if (u,v) is an edge of the graph

0 commit comments

Comments
 (0)