Skip to content

Commit 08a0a59

Browse files
committed
fix of a max flow bug
1 parent c98d4b6 commit 08a0a59

File tree

4 files changed

+226
-5
lines changed

4 files changed

+226
-5
lines changed

ngraph/algorithms/capacity.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -100,8 +100,10 @@ def _init_graph_data(
100100
residual_cap[node][adj_node] = (
101101
fwd_capacity if fwd_capacity >= MIN_CAP else 0.0
102102
)
103-
# Reverse edge in the BFS sense starts with 0 capacity
104-
residual_cap[adj_node][node] = 0.0
103+
# Reverse edge in the BFS sense starts with 0 capacity.
104+
# Do not overwrite if a positive capacity was already assigned
105+
# when processing the opposite orientation (bidirectional links).
106+
residual_cap[adj_node].setdefault(node, 0.0)
105107

106108
elif flow_placement == FlowPlacement.EQUAL_BALANCED:
107109
# min(...) * number_of_parallel_edges
@@ -112,8 +114,9 @@ def _init_graph_data(
112114
)
113115
else:
114116
residual_cap[adj_node][node] = 0.0
115-
# The forward edge in reversed orientation starts at 0 capacity
116-
residual_cap[node][adj_node] = 0.0
117+
# The forward edge in reversed orientation starts at 0 capacity.
118+
# Do not overwrite non-zero value if already assigned earlier.
119+
residual_cap[node].setdefault(adj_node, 0.0)
117120

118121
else:
119122
raise ValueError(f"Unsupported flow placement: {flow_placement}")

tests/algorithms/test_calc_capacity.py

Lines changed: 137 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,11 @@
33

44
import pytest
55

6-
from ngraph.algorithms.capacity import FlowPlacement, calc_graph_capacity
6+
from ngraph.algorithms.capacity import (
7+
FlowPlacement,
8+
_init_graph_data,
9+
calc_graph_capacity,
10+
)
711
from ngraph.algorithms.flow_init import init_flow_graph
812
from ngraph.algorithms.spf import spf
913
from ngraph.graph.strict_multidigraph import EdgeID, NodeID, StrictMultiDiGraph
@@ -391,6 +395,138 @@ def test_calc_graph_capacity_self_loop_with_other_edges(self):
391395
)
392396
assert max_flow_regular == 5.0 # Should be limited by A->B capacity
393397

398+
def test_reverse_residual_init_graph_data_proportional(self):
399+
"""_init_graph_data should expose dst->leaf residual capacity in PROPORTIONAL.
400+
401+
Build a tiny graph with forward edges leaf->dc and dc->sink, and reverse dc->leaf.
402+
SPF pred contains dc predecessors (leaves) and sink predecessor (dc).
403+
The reversed residual must have positive capacity dc->leaf equal to sum(capacity - flow).
404+
"""
405+
g = StrictMultiDiGraph()
406+
for n in ("source", "A/dc", "A/leaf", "sink"):
407+
g.add_node(n)
408+
# Forward edges
409+
e1 = g.add_edge("A/leaf", "A/dc", capacity=10.0, cost=1, flow=0.0, flows={})
410+
g.add_edge("A/dc", "sink", capacity=float("inf"), cost=0, flow=0.0, flows={})
411+
# Reverse edge to simulate bidirectional link
412+
g.add_edge("A/dc", "A/leaf", capacity=10.0, cost=1, flow=0.0, flows={})
413+
414+
# SPF-like predecessor dict: include both directions present in graph
415+
# sink<-A/dc, A/dc<-A/leaf, and A/leaf<-A/dc (reverse link)
416+
pred: PredDict = {
417+
"source": {},
418+
"A/dc": {"A/leaf": [e1]},
419+
"A/leaf": {"A/dc": list(g.edges_between("A/dc", "A/leaf"))},
420+
"sink": {"A/dc": list(g.edges_between("A/dc", "sink"))},
421+
}
422+
423+
# Run init
424+
succ, levels, residual_cap, flow_dict = _init_graph_data(
425+
g,
426+
pred,
427+
init_node="sink",
428+
flow_placement=FlowPlacement.PROPORTIONAL,
429+
capacity_attr="capacity",
430+
flow_attr="flow",
431+
)
432+
# residuals must reflect both forward directions, and zero-init must not overwrite
433+
assert residual_cap["A/dc"]["A/leaf"] == 10.0
434+
assert residual_cap["A/leaf"]["A/dc"] == 10.0
435+
436+
def test_reverse_residual_init_graph_data_equal_balanced(self):
437+
"""_init_graph_data should set reverse residual in EQUAL_BALANCED as min*count.
438+
439+
With two parallel edges leaf->dc with caps (5, 7), min=5 and count=2 -> reverse cap = 10.
440+
"""
441+
g = StrictMultiDiGraph()
442+
for n in ("source", "A/dc", "A/leaf", "sink"):
443+
g.add_node(n)
444+
# Two parallel forward edges leaf->dc
445+
e1 = g.add_edge("A/leaf", "A/dc", capacity=5.0, cost=1, flow=0.0, flows={})
446+
e2 = g.add_edge("A/leaf", "A/dc", capacity=7.0, cost=1, flow=0.0, flows={})
447+
g.add_edge("A/dc", "sink", capacity=float("inf"), cost=0, flow=0.0, flows={})
448+
# Reverse edge present too
449+
g.add_edge("A/dc", "A/leaf", capacity=7.0, cost=1, flow=0.0, flows={})
450+
451+
pred: PredDict = {
452+
"source": {},
453+
"A/dc": {"A/leaf": [e1, e2]},
454+
"sink": {"A/dc": list(g.edges_between("A/dc", "sink"))},
455+
}
456+
457+
succ, levels, residual_cap, flow_dict = _init_graph_data(
458+
g,
459+
pred,
460+
init_node="sink",
461+
flow_placement=FlowPlacement.EQUAL_BALANCED,
462+
capacity_attr="capacity",
463+
flow_attr="flow",
464+
)
465+
# In EQUAL_BALANCED, the reverse residual is assigned on leaf->dc orientation (adj->node)
466+
# i.e., residual_cap[leaf][dc] = min(capacities) * count = 5*2 = 10
467+
assert residual_cap["A/leaf"]["A/dc"] == 10.0
468+
# forward side initialized to 0 in reversed orientation
469+
assert residual_cap["A/dc"]["A/leaf"] == 0.0
470+
471+
def test_dc_to_dc_reverse_edge_first_hop_proportional(self):
472+
"""Reverse-edge-first hop at destination should yield positive flow.
473+
474+
Topology (with reverse edges to simulate bidirectional links):
475+
A_leaf -> A_dc (10)
476+
A_leaf -> B_leaf (10)
477+
B_leaf -> B_dc (10)
478+
A_dc -> A_leaf (10) # reverse present
479+
B_dc -> B_leaf (10) # reverse present
480+
481+
Pseudo nodes: source -> A_dc, B_dc -> sink
482+
Expected max_flow(source, sink) = 10.0 in PROPORTIONAL mode.
483+
"""
484+
g = StrictMultiDiGraph()
485+
for n in ("A_dc", "A_leaf", "B_leaf", "B_dc", "source", "sink"):
486+
g.add_node(n)
487+
488+
# Forward edges
489+
g.add_edge("A_leaf", "A_dc", capacity=10.0, cost=1)
490+
g.add_edge("A_leaf", "B_leaf", capacity=10.0, cost=1)
491+
g.add_edge("B_leaf", "B_dc", capacity=10.0, cost=1)
492+
# Reverse edges
493+
g.add_edge("A_dc", "A_leaf", capacity=10.0, cost=1)
494+
g.add_edge("B_dc", "B_leaf", capacity=10.0, cost=1)
495+
496+
# Pseudo source/sink
497+
g.add_edge("source", "A_dc", capacity=float("inf"), cost=0)
498+
g.add_edge("B_dc", "sink", capacity=float("inf"), cost=0)
499+
500+
r = init_flow_graph(g)
501+
# Compute SPF with dst_node to mirror real usage in calc_max_flow
502+
_costs, pred = spf(r, "source", dst_node="sink")
503+
max_flow, _flow_dict = calc_graph_capacity(
504+
r, "source", "sink", pred, flow_placement=FlowPlacement.PROPORTIONAL
505+
)
506+
assert max_flow == 10.0
507+
508+
def test_dc_to_dc_unidirectional_zero(self):
509+
"""Without reverse edges, DC cannot send to leaf; flow must be zero."""
510+
g = StrictMultiDiGraph()
511+
for n in ("A_dc", "A_leaf", "B_leaf", "B_dc", "source", "sink"):
512+
g.add_node(n)
513+
514+
# Forward edges only
515+
g.add_edge("A_leaf", "A_dc", capacity=10.0, cost=1)
516+
g.add_edge("A_leaf", "B_leaf", capacity=10.0, cost=1)
517+
g.add_edge("B_leaf", "B_dc", capacity=10.0, cost=1)
518+
519+
# Pseudo source/sink
520+
g.add_edge("source", "A_dc", capacity=float("inf"), cost=0)
521+
g.add_edge("B_dc", "sink", capacity=float("inf"), cost=0)
522+
523+
r = init_flow_graph(g)
524+
_costs, pred = spf(r, "source", dst_node="sink")
525+
max_flow, _flow_dict = calc_graph_capacity(
526+
r, "source", "sink", pred, flow_placement=FlowPlacement.PROPORTIONAL
527+
)
528+
assert max_flow == 0.0
529+
394530
def test_calc_graph_capacity_self_loop_empty_pred(self):
395531
"""
396532
Test self-loop behavior when pred is empty.

tests/algorithms/test_max_flow.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -514,6 +514,63 @@ def test_max_flow_overlapping_source_sink_with_bidirectional(self):
514514
max_flow_b_b = calc_max_flow(g, "B", "B")
515515
assert max_flow_b_b == 0.0
516516

517+
def test_dc_leaf_bidirectional_parallel_branches(self):
518+
"""DC→leaf reversed hop with two parallel branches; compare placements.
519+
520+
Topology (all costs=1 unless noted):
521+
S -> A_dc (inf)
522+
A_dc -> A_leaf1 (cap=50)
523+
A_dc -> A_leaf2 (cap=50)
524+
A_leaf1 -> B_leaf1 (cap=100)
525+
A_leaf2 -> B_leaf1 (cap=1)
526+
B_leaf1 -> B_dc (cap=100)
527+
B_dc -> T (inf)
528+
529+
Expected:
530+
- PROPORTIONAL: branch1 min(50,100)=50, branch2 min(50,1)=1 → total 51
531+
- EQUAL_BALANCED: nominal split 0.5 at A_dc; limiting ratio at edge (A_leaf2->B_leaf1): 1/0.5=2 → total 2
532+
"""
533+
g = StrictMultiDiGraph()
534+
for n in ("S", "A_dc", "A_leaf1", "A_leaf2", "B_leaf1", "B_dc", "T"):
535+
g.add_node(n)
536+
537+
# Pseudo edges
538+
g.add_edge("S", "A_dc", capacity=float("inf"), cost=0)
539+
g.add_edge("B_dc", "T", capacity=float("inf"), cost=0)
540+
541+
# Reversed hop from DC to leaves (present as real edges here)
542+
g.add_edge("A_dc", "A_leaf1", capacity=50.0, cost=1)
543+
g.add_edge("A_dc", "A_leaf2", capacity=50.0, cost=1)
544+
545+
# Leaf to leaf aggregation
546+
g.add_edge("A_leaf1", "B_leaf1", capacity=100.0, cost=1)
547+
g.add_edge("A_leaf2", "B_leaf1", capacity=1.0, cost=1)
548+
549+
# Final hop into destination DC
550+
g.add_edge("B_leaf1", "B_dc", capacity=100.0, cost=1)
551+
552+
# Full max flow: both placements reach the same min-cut (51)
553+
flow_prop = calc_max_flow(
554+
g, "S", "T", flow_placement=FlowPlacement.PROPORTIONAL
555+
)
556+
assert flow_prop == 51.0
557+
558+
flow_eq = calc_max_flow(
559+
g, "S", "T", flow_placement=FlowPlacement.EQUAL_BALANCED
560+
)
561+
assert flow_eq == 51.0
562+
563+
# Single augmentation differs by placement
564+
flow_prop_sp = calc_max_flow(
565+
g, "S", "T", shortest_path=True, flow_placement=FlowPlacement.PROPORTIONAL
566+
)
567+
assert flow_prop_sp == 51.0
568+
569+
flow_eq_sp = calc_max_flow(
570+
g, "S", "T", shortest_path=True, flow_placement=FlowPlacement.EQUAL_BALANCED
571+
)
572+
assert flow_eq_sp == 2.0
573+
517574
def test_max_flow_self_loop_all_return_modes(self):
518575
"""
519576
Test self-loop (s == t) behavior with all possible return value combinations.

tests/solver/test_maxflow_api.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,3 +174,28 @@ def test_sensitivity_analysis_keys_align_with_saturated_edges() -> None:
174174
sat = net.saturated_edges("^S$", "^T$", mode="combine")[("^S$", "^T$")]
175175
assert set((u, v, k) for (u, v, k) in sat) == set(delta_by_edge.keys())
176176
assert all(isinstance(delta, (int, float)) for delta in delta_by_edge.values())
177+
178+
179+
def test_network_dc_to_dc_reverse_edge_first_hop() -> None:
180+
"""Integration: DC->DC flow that requires a reverse edge on first hop.
181+
182+
Nodes: A/dc, A/leaf, B/leaf, B/dc. Links (forward):
183+
A/leaf->A/dc (10), A/leaf->B/leaf (10), B/leaf->B/dc (10)
184+
185+
The wrapper builds a StrictMultiDiGraph with add_reverse=True, creating
186+
reverse DC->leaf edges, so A/dc can reach B/dc via DC->leaf->leaf->DC.
187+
188+
Expect positive flow (10.0) in combine mode.
189+
"""
190+
net = Network()
191+
for name in ["A/dc", "A/leaf", "B/leaf", "B/dc"]:
192+
net.add_node(Node(name))
193+
194+
# Forward links only; the graph builder will add reverse edges
195+
net.add_link(Link("A/leaf", "A/dc", capacity=10.0, cost=1.0))
196+
net.add_link(Link("A/leaf", "B/leaf", capacity=10.0, cost=1.0))
197+
net.add_link(Link("B/leaf", "B/dc", capacity=10.0, cost=1.0))
198+
199+
res = net.max_flow(r"^A/dc$", r"^B/dc$", mode="combine")
200+
assert (r"^A/dc$", r"^B/dc$") in res
201+
assert res[(r"^A/dc$", r"^B/dc$")] == 10.0

0 commit comments

Comments
 (0)