|
3 | 3 |
|
4 | 4 | import pytest |
5 | 5 |
|
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 | +) |
7 | 11 | from ngraph.algorithms.flow_init import init_flow_graph |
8 | 12 | from ngraph.algorithms.spf import spf |
9 | 13 | from ngraph.graph.strict_multidigraph import EdgeID, NodeID, StrictMultiDiGraph |
@@ -391,6 +395,138 @@ def test_calc_graph_capacity_self_loop_with_other_edges(self): |
391 | 395 | ) |
392 | 396 | assert max_flow_regular == 5.0 # Should be limited by A->B capacity |
393 | 397 |
|
| 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 | + |
394 | 530 | def test_calc_graph_capacity_self_loop_empty_pred(self): |
395 | 531 | """ |
396 | 532 | Test self-loop behavior when pred is empty. |
|
0 commit comments