Skip to content

Commit 94a7a04

Browse files
Enhance safe sequence handling and new drawing style
Added a 'points' style option to the graph drawing utility for improved visualization. Improved handling of edge multiplicities in safe sequence constraints, now supporting multiple traversals for SCC edges and validating multiplicity for non-SCC edges. Introduced a new test case in safe_seq_cycles.py to demonstrate safe sequence and incompatible sequence computation, and updated cycles_demo.py to enable additional output and drawing.
1 parent 3dbee58 commit 94a7a04

File tree

4 files changed

+133
-26
lines changed

4 files changed

+133
-26
lines changed

examples/cycles_demo.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
def test_min_flow_decomp(filename: str):
55
graph = fp.graphutils.read_graphs(filename)[0]
66
print("graph id", graph.graph["id"])
7-
# print("subset_constraints", graph.graph["constraints"])
7+
print("subset_constraints", graph.graph["constraints"])
88
# fp.utils.draw(
99
# G=graph,
1010
# filename=filename + ".pdf",
@@ -16,6 +16,7 @@ def test_min_flow_decomp(filename: str):
1616
# "show_path_weights": False,
1717
# "show_path_weight_on_first_edge": True,
1818
# "pathwidth": 2,
19+
# "style": "points",
1920
# })
2021

2122
print(graph.graph["n"], graph.graph["m"], graph.graph["w"])
@@ -154,6 +155,21 @@ def process_solution(model):
154155
else:
155156
print("Model could not be solved.")
156157

158+
fp.utils.draw(
159+
G=model.G,
160+
filename= "solution.pdf",
161+
flow_attr="flow",
162+
paths=model.get_solution().get('walks', None),
163+
weights=model.get_solution().get('weights', None),
164+
draw_options={
165+
"show_graph_edges": False,
166+
"show_edge_weights": False,
167+
"show_path_weights": False,
168+
"show_path_weight_on_first_edge": True,
169+
"pathwidth": 2,
170+
# "style": "points",
171+
})
172+
157173
solve_statistics = model.solve_statistics
158174
print(solve_statistics)
159175
print("node_number:", solve_statistics['node_number'])

examples/safe_seq_cycles.py

Lines changed: 72 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,72 @@ def test2():
114114
lae_model.solve()
115115
process_solution(graph, lae_model)
116116

117+
def test6():
118+
graph = nx.DiGraph()
119+
graph.add_edge("s", "a", flow=10)
120+
graph.add_edge("a", "a", flow=10)
121+
graph.add_edge("a", "b", flow=10)
122+
graph.add_edge("a", "f", flow=10)
123+
graph.add_edge("a", "h", flow=10)
124+
graph.add_edge("b", "c", flow=10)
125+
graph.add_edge("c", "d", flow=10)
126+
graph.add_edge("d", "b", flow=10)
127+
# graph.add_edge("d", "h", flow=10)
128+
graph.add_edge("c", "t", flow=10)
129+
graph.add_edge("s", "e", flow=10)
130+
graph.add_edge("e", "f", flow=10)
131+
graph.add_edge("f", "g", flow=10)
132+
graph.add_edge("g", "h", flow=10)
133+
graph.add_edge("h", "f", flow=10)
134+
graph.add_edge("g", "t", flow=10)
135+
136+
stDiGraph = fp.stDiGraph(graph)
137+
X = set(stDiGraph.edges())
138+
safe_seqs = safety.maximal_safe_sequences_via_dominators(stDiGraph, X)
139+
for seq in safe_seqs:
140+
print("Safe sequence:", seq)
141+
142+
# For every edge of stDiGraph, compute the length of the longest safe seq using it
143+
longest_safe_seq_length = dict()
144+
for edge in stDiGraph.edges():
145+
longest_safe_seq_length[edge] = max(len(seq) for seq in safe_seqs if edge in seq)
146+
print("Longest safe seq lengths:", longest_safe_seq_length)
147+
148+
incompatible_sequences = stDiGraph.get_longest_incompatible_sequences(safe_seqs)
149+
for seq in incompatible_sequences:
150+
print("Incompatible sequence:", seq)
151+
152+
fp.graphutils.draw(
153+
G=stDiGraph,
154+
filename="safe_sequences_example.pdf",
155+
subpath_constraints=incompatible_sequences,
156+
flow_attr="flow",
157+
draw_options={
158+
"show_graph_edges": True,
159+
"show_edge_weights": False,
160+
"show_path_weights": False,
161+
"show_path_weight_on_first_edge": True,
162+
"pathwidth": 2,
163+
"style": "points",
164+
}
165+
)
166+
167+
kmpe_model = fp.kMinPathErrorCycles(
168+
G=graph,
169+
flow_attr="flow",
170+
weight_type=float,
171+
optimization_options={
172+
"optimize_with_safe_sequences": True, # set to false to deactivate the safe sequences optimization
173+
},
174+
solver_options={
175+
"external_solver": "gurobi", # we can try also "highs" at some point
176+
"time_limit": 300, # 300s = 5min, is it ok?
177+
},
178+
)
179+
kmpe_model.solve()
180+
assert(kmpe_model.is_solved())
181+
assert(kmpe_model.is_valid_solution())
182+
117183
def process_solution(graph, model: fp.kLeastAbsErrors):
118184
if model.is_solved():
119185
print(model.get_solution())
@@ -135,11 +201,12 @@ def process_solution(graph, model: fp.kLeastAbsErrors):
135201
print("Model could not be solved.")
136202

137203
def main():
138-
test1()
139-
test3()
140-
test2()
141-
test4()
142-
test5()
204+
# test1()
205+
# test3()
206+
# test2()
207+
# test4()
208+
# test5()
209+
test6()
143210

144211
if __name__ == "__main__":
145212
# Configure logging

flowpaths/abstractwalkmodeldigraph.py

Lines changed: 24 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import time
77
import copy
88
from abc import ABC, abstractmethod
9+
from collections import Counter
910

1011
class AbstractWalkModelDiGraph(ABC):
1112
# storing some defaults
@@ -407,32 +408,42 @@ def _apply_safety_optimizations(self):
407408

408409
# Otherwise, we fix variables using the walks to fix
409410
if self.optimize_with_safe_sequences:
410-
# iterating over safe lists
411+
# Iterate over walks to fix (up to k layers) and enforce per-edge multiplicity
411412
for i in range(min(len(self.walks_to_fix), self.k)):
412-
# print("Fixing variables for safe list #", i)
413-
# iterate over the edges in the safe list to fix variables to 1
414-
for u, v in self.walks_to_fix[i]:
413+
walk = self.walks_to_fix[i]
414+
if not walk:
415+
continue
416+
417+
# Count multiplicities of each edge in this safe walk
418+
edge_multiplicities = Counter(walk) # keys are (u,v), values are number of occurrences
419+
420+
for (u, v), m in edge_multiplicities.items():
415421
if self.G.is_scc_edge(u, v):
422+
# Inside an SCC we allow multiple traversals; enforce lower bound = m
416423
if self.optimize_with_safe_sequences_allow_geq_constraints:
417-
# Raise LB via bounds only when enabled; else add constraint
418424
if self.optimize_with_safe_sequences_fix_via_bounds:
419-
self.solver.queue_set_var_lower_bound(self.edge_vars[(u, v, i)], 1)
425+
# Tighten variable lower bound directly
426+
self.solver.queue_set_var_lower_bound(self.edge_vars[(u, v, i)], m)
420427
else:
428+
# Add inequality constraint x >= m
421429
self.solver.add_constraint(
422-
self.edge_vars[(u, v, i)] >= 1,
423-
name=f"safe_list_u={u}_v={v}_i={i}",
430+
self.edge_vars[(u, v, i)] >= m,
431+
name=f"safe_list_u={u}_v={v}_i={i}_geq{m}",
424432
)
425433
self.solve_statistics["edge_variables>=1"] += 1
434+
# If allow_geq_constraints is False, fall through without enforcing (original logic only acted when True)
426435
else:
427-
# Instead of adding an equality constraint x==1 we tighten bounds directly.
436+
# Edge not in SCC: x == 1 either via direct fix (bounds) or equality constraint
437+
if m != 1:
438+
utils.logger.critical(f"{__name__}: Unexpected multiplicity {m} != 1 for non-SCC edge ({u},{v})")
439+
raise ValueError(f"Unexpected multiplicity {m} != 1 for non-SCC edge ({u},{v})")
428440
if self.optimize_with_safe_sequences_fix_via_bounds:
429-
# Queue a fix to value 1
430-
self.solver.queue_fix_variable(self.edge_vars[(u, v, i)], int(1))
441+
# Since UB=1, fixing is safe and sets both LB & UB to m
442+
self.solver.queue_fix_variable(self.edge_vars[(u, v, i)], 1)
431443
else:
432-
# Keep old behaviour via equality constraint
433444
self.solver.add_constraint(
434445
self.edge_vars[(u, v, i)] == 1,
435-
name=f"safe_list_u={u}_v={v}_i={i}",
446+
name=f"safe_list_u={u}_v={v}_i={i}_eq{m}",
436447
)
437448
self.solve_statistics["edge_variables=1"] += 1
438449

flowpaths/utils/graphutils.py

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -368,7 +368,8 @@ def draw(
368368
"show_node_weights": False,
369369
"show_path_weights": False,
370370
"show_path_weight_on_first_edge": True,
371-
"pathwidth": 3.0
371+
"pathwidth": 3.0,
372+
"style": "default",
372373
},
373374
):
374375
"""
@@ -437,6 +438,10 @@ def draw(
437438
438439
The width of the path to be drawn. Default is `3.0`.
439440
441+
- `style`: str
442+
443+
The style of the drawing. Available options: `default`, `points`.
444+
440445
"""
441446

442447
if len(paths) != len(weights) and len(weights) > 0:
@@ -447,8 +452,16 @@ def draw(
447452

448453
dot = gv.Digraph(format="pdf")
449454
dot.graph_attr["rankdir"] = "LR" # Display the graph in landscape mode
450-
dot.node_attr["shape"] = "rectangle" # Rectangle nodes
451-
dot.node_attr["style"] = "rounded" # Rounded rectangle nodes
455+
456+
style = draw_options.get("style", "default")
457+
if style == "default":
458+
dot.node_attr["shape"] = "rectangle" # Rectangle nodes
459+
dot.node_attr["style"] = "rounded" # Rounded rectangle nodes
460+
elif style == "points":
461+
dot.node_attr["shape"] = "point" # Point nodes
462+
dot.node_attr["style"] = "filled" # Filled point nodes
463+
# dot.node_attr['label'] = ''
464+
dot.node_attr['width'] = '0.1'
452465

453466
colors = [
454467
"red",
@@ -483,17 +496,17 @@ def draw(
483496
elif node in additional_ends:
484497
color = "red"
485498
penwidth = "2.0"
486-
499+
487500
if draw_options.get("show_node_weights", False) and flow_attr is not None and flow_attr in G.nodes[node]:
501+
label = f"{G.nodes[node][flow_attr]}\\n{node}" if style != "points" else ""
488502
dot.node(
489503
name=str(node),
490-
#label=f"{{{node} | {G.nodes[node][flow_attr]}}}",
491-
label=f"{G.nodes[node][flow_attr]}\\n{node}",
492-
#xlabel=str(G.nodes[node][flow_attr]),
504+
label=label,
493505
shape="record",
494506
color=color,
495507
penwidth=penwidth)
496508
else:
509+
label = str(node) if style != "points" else ""
497510
dot.node(
498511
name=str(node),
499512
label=str(node),

0 commit comments

Comments
 (0)