Skip to content

Commit b7d0ae6

Browse files
committed
Add cost distribution analysis
1 parent 92d9e45 commit b7d0ae6

9 files changed

Lines changed: 341 additions & 7 deletions

File tree

docs/examples/basic.md

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,9 +121,71 @@ print(f"Equal-balanced flow: {max_flow_shortest_balanced}")
121121

122122
Note that `EQUAL_BALANCED` flow placement is only applicable when calculating MaxFlow on shortest paths.
123123

124+
## Cost Distribution Analysis
125+
126+
The cost distribution feature analyzes how flow is distributed across paths of different costs for latency span analysis and network performance characterization.
127+
128+
```python
129+
# Get flow analysis with cost distribution
130+
result = network.max_flow_with_summary(
131+
source_path="A",
132+
sink_path="C",
133+
mode="combine"
134+
)
135+
136+
# Extract flow value and summary
137+
(src_label, sink_label), (flow_value, summary) = next(iter(result.items()))
138+
139+
print(f"Total flow: {flow_value}")
140+
print(f"Cost distribution: {summary.cost_distribution}")
141+
142+
# Example output:
143+
# Total flow: 6.0
144+
# Cost distribution: {2.0: 3.0, 4.0: 3.0}
145+
#
146+
# This means:
147+
# - 3.0 units of flow use paths with total cost 2.0 (A→B→C path)
148+
# - 3.0 units of flow use paths with total cost 4.0 (A→D→C path)
149+
```
150+
151+
### Latency Span Analysis
152+
153+
When link costs represent latency (e.g., distance-based), the cost distribution provides insight into traffic latency characteristics:
154+
155+
```python
156+
def analyze_latency_span(cost_distribution):
157+
"""Analyze latency characteristics from cost distribution."""
158+
if not cost_distribution:
159+
return "No flow paths available"
160+
161+
total_flow = sum(cost_distribution.values())
162+
weighted_avg_latency = sum(cost * flow for cost, flow in cost_distribution.items()) / total_flow
163+
164+
min_latency = min(cost_distribution.keys())
165+
max_latency = max(cost_distribution.keys())
166+
latency_span = max_latency - min_latency
167+
168+
print(f"Latency Analysis:")
169+
print(f" Average latency: {weighted_avg_latency:.2f}")
170+
print(f" Latency range: {min_latency:.1f} - {max_latency:.1f}")
171+
print(f" Latency span: {latency_span:.1f}")
172+
print(f" Flow distribution:")
173+
for cost, flow in sorted(cost_distribution.items()):
174+
percentage = (flow / total_flow) * 100
175+
print(f" {percentage:.1f}% of traffic uses paths with latency {cost:.1f}")
176+
177+
# Example usage
178+
analyze_latency_span(summary.cost_distribution)
179+
```
180+
181+
This analysis helps identify:
182+
- **Traffic concentration**: How much traffic uses low vs. high latency paths
183+
- **Latency span**: The range of latencies experienced by traffic
184+
- **Performance bottlenecks**: When high-latency paths carry traffic due to capacity constraints
185+
124186
## Advanced Analysis: Sensitivity Analysis
125187

126-
For deeper network analysis, you can use the low-level graph algorithms to perform sensitivity analysis and identify bottleneck edges:
188+
For network analysis, you can use the low-level graph algorithms to run sensitivity analysis and identify bottleneck edges:
127189

128190
```python
129191
from ngraph.lib.algorithms.max_flow import calc_max_flow, saturated_edges, run_sensitivity

docs/reference/api-full.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ For a curated, example-driven API guide, see **[api.md](api.md)**.
1010
> - **[CLI Reference](cli.md)** - Command-line interface
1111
> - **[DSL Reference](dsl.md)** - YAML syntax guide
1212
13-
**Generated from source code on:** July 29, 2025 at 16:59 UTC
13+
**Generated from source code on:** July 29, 2025 at 17:52 UTC
1414

1515
**Modules auto-discovered:** 53
1616

@@ -2205,6 +2205,9 @@ Attributes:
22052205
residual_cap: Remaining capacity on each edge after flow placement.
22062206
reachable: Set of nodes reachable from source in residual graph.
22072207
min_cut: List of saturated edges that form the minimum cut.
2208+
cost_distribution: Distribution of flow volume over path costs.
2209+
Maps each cost value to the total volume of flow placed on
2210+
paths with that cost during sequential augmentation.
22082211

22092212
**Attributes:**
22102213

@@ -2213,6 +2216,7 @@ Attributes:
22132216
- `residual_cap` (Dict[Edge, float])
22142217
- `reachable` (Set[str])
22152218
- `min_cut` (List[Edge])
2219+
- `cost_distribution` (Dict[Cost, float])
22162220

22172221
---
22182222

docs/reference/api.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,18 @@ max_flow = network.max_flow(
142142
shortest_path=True, # Use only shortest paths
143143
flow_placement=FlowPlacement.PROPORTIONAL # UCMP load balancing
144144
)
145+
146+
# Detailed flow analysis with cost distribution
147+
result = network.max_flow_with_summary(
148+
source_path="datacenter.*",
149+
sink_path="edge.*",
150+
mode="combine"
151+
)
152+
(src_label, sink_label), (flow_value, summary) = next(iter(result.items()))
153+
154+
# Cost distribution shows flow volume per path cost (useful for latency analysis)
155+
print(f"Cost distribution: {summary.cost_distribution}")
156+
# Example: {2.0: 150.0, 4.0: 75.0} means 150 units on cost-2 paths, 75 on cost-4 paths
145157
```
146158

147159
**Key Options:**
@@ -150,6 +162,11 @@ max_flow = network.max_flow(
150162
- `shortest_path`: `True` (shortest only) or `False` (all available paths)
151163
- `flow_placement`: `FlowPlacement.PROPORTIONAL` (UCMP) or `FlowPlacement.EQUAL_BALANCED` (ECMP)
152164

165+
**Advanced Features:**
166+
167+
- **Cost Distribution**: `FlowSummary.cost_distribution` provides flow volume breakdown by path cost for latency span analysis and performance characterization
168+
- **Analytics**: Edge flows, residual capacities, min-cut analysis, and reachability information
169+
153170
**Integration:** Available on both Network and NetworkView objects. Foundation for FailureManager Monte Carlo analysis.
154171

155172
### NetworkView

ngraph/lib/algorithms/max_flow.py

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
"""Maximum flow algorithms and network flow computations."""
22

3-
from typing import Literal, Union, overload
3+
from typing import Dict, Literal, Union, overload
44

55
from ngraph.lib.algorithms.base import EdgeSelect, FlowPlacement
66
from ngraph.lib.algorithms.flow_init import init_flow_graph
77
from ngraph.lib.algorithms.place_flow import place_flow_on_graph
8-
from ngraph.lib.algorithms.spf import spf
8+
from ngraph.lib.algorithms.spf import Cost, spf
99
from ngraph.lib.algorithms.types import FlowSummary
1010
from ngraph.lib.graph import NodeID, StrictMultiDiGraph
1111

@@ -208,6 +208,7 @@ def calc_max_flow(
208208
capacity_attr,
209209
flow_attr,
210210
tolerance,
211+
{}, # Empty cost distribution for self-loop case
211212
)
212213
else:
213214
return 0.0
@@ -220,8 +221,11 @@ def calc_max_flow(
220221
reset_flow_graph,
221222
)
222223

224+
# Initialize cost distribution tracking
225+
cost_distribution: Dict[Cost, float] = {}
226+
223227
# First path-finding iteration.
224-
_, pred = spf(
228+
costs, pred = spf(
225229
flow_graph, src_node, edge_select=EdgeSelect.ALL_MIN_COST_WITH_CAP_REMAINING
226230
)
227231
flow_meta = place_flow_on_graph(
@@ -236,6 +240,13 @@ def calc_max_flow(
236240
)
237241
max_flow = flow_meta.placed_flow
238242

243+
# Track cost distribution for first iteration
244+
if dst_node in costs and flow_meta.placed_flow > 0:
245+
path_cost = costs[dst_node]
246+
cost_distribution[path_cost] = (
247+
cost_distribution.get(path_cost, 0.0) + flow_meta.placed_flow
248+
)
249+
239250
# If only one path (single augmentation) is desired, return early.
240251
if shortest_path:
241252
return _build_return_value(
@@ -247,11 +258,12 @@ def calc_max_flow(
247258
capacity_attr,
248259
flow_attr,
249260
tolerance,
261+
cost_distribution,
250262
)
251263

252264
# Otherwise, repeatedly find augmenting paths until no new flow can be placed.
253265
while True:
254-
_, pred = spf(
266+
costs, pred = spf(
255267
flow_graph, src_node, edge_select=EdgeSelect.ALL_MIN_COST_WITH_CAP_REMAINING
256268
)
257269
if dst_node not in pred:
@@ -274,6 +286,13 @@ def calc_max_flow(
274286

275287
max_flow += flow_meta.placed_flow
276288

289+
# Track cost distribution for this iteration
290+
if dst_node in costs and flow_meta.placed_flow > 0:
291+
path_cost = costs[dst_node]
292+
cost_distribution[path_cost] = (
293+
cost_distribution.get(path_cost, 0.0) + flow_meta.placed_flow
294+
)
295+
277296
return _build_return_value(
278297
max_flow,
279298
flow_graph,
@@ -283,6 +302,7 @@ def calc_max_flow(
283302
capacity_attr,
284303
flow_attr,
285304
tolerance,
305+
cost_distribution,
286306
)
287307

288308

@@ -295,6 +315,7 @@ def _build_return_value(
295315
capacity_attr: str,
296316
flow_attr: str,
297317
tolerance: float,
318+
cost_distribution: Dict[Cost, float],
298319
) -> Union[float, tuple]:
299320
"""Build the appropriate return value based on the requested flags."""
300321
if not (return_summary or return_graph):
@@ -303,7 +324,13 @@ def _build_return_value(
303324
summary = None
304325
if return_summary:
305326
summary = _build_flow_summary(
306-
max_flow, flow_graph, src_node, capacity_attr, flow_attr, tolerance
327+
max_flow,
328+
flow_graph,
329+
src_node,
330+
capacity_attr,
331+
flow_attr,
332+
tolerance,
333+
cost_distribution,
307334
)
308335

309336
ret: list = [max_flow]
@@ -322,6 +349,7 @@ def _build_flow_summary(
322349
capacity_attr: str,
323350
flow_attr: str,
324351
tolerance: float,
352+
cost_distribution: Dict[Cost, float],
325353
) -> FlowSummary:
326354
"""Build a FlowSummary from the flow graph state."""
327355
edge_flow = {}
@@ -364,6 +392,7 @@ def _build_flow_summary(
364392
residual_cap=residual_cap,
365393
reachable=reachable,
366394
min_cut=min_cut,
395+
cost_distribution=cost_distribution,
367396
)
368397

369398

ngraph/lib/algorithms/types.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
from dataclasses import dataclass
66
from typing import Dict, List, Set, Tuple
77

8+
from ngraph.lib.algorithms.base import Cost
9+
810
# Edge identifier tuple: (source_node, destination_node, edge_key)
911
Edge = Tuple[str, str, str]
1012

@@ -23,10 +25,14 @@ class FlowSummary:
2325
residual_cap: Remaining capacity on each edge after flow placement.
2426
reachable: Set of nodes reachable from source in residual graph.
2527
min_cut: List of saturated edges that form the minimum cut.
28+
cost_distribution: Distribution of flow volume over path costs.
29+
Maps each cost value to the total volume of flow placed on
30+
paths with that cost during sequential augmentation.
2631
"""
2732

2833
total_flow: float
2934
edge_flow: Dict[Edge, float]
3035
residual_cap: Dict[Edge, float]
3136
reachable: Set[str]
3237
min_cut: List[Edge]
38+
cost_distribution: Dict[Cost, float]

ngraph/network.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -463,6 +463,7 @@ def _compute_flow_with_summary_single_group(
463463
residual_cap={},
464464
reachable=set(),
465465
min_cut=[],
466+
cost_distribution={},
466467
)
467468
return 0.0, empty_summary
468469

@@ -584,6 +585,7 @@ def _compute_flow_detailed_single_group(
584585
residual_cap={},
585586
reachable=set(),
586587
min_cut=[],
588+
cost_distribution={},
587589
)
588590
return 0.0, empty_summary, base_graph
589591

@@ -1202,6 +1204,7 @@ def _max_flow_with_summary_internal(
12021204
residual_cap={},
12031205
reachable=set(),
12041206
min_cut=[],
1207+
cost_distribution={},
12051208
)
12061209
return {(combined_src_label, combined_snk_label): (0.0, empty_summary)}
12071210

@@ -1215,6 +1218,7 @@ def _max_flow_with_summary_internal(
12151218
residual_cap={},
12161219
reachable=set(),
12171220
min_cut=[],
1221+
cost_distribution={},
12181222
)
12191223
return {(combined_src_label, combined_snk_label): (0.0, empty_summary)}
12201224
else:
@@ -1240,6 +1244,7 @@ def _max_flow_with_summary_internal(
12401244
residual_cap={},
12411245
reachable=set(),
12421246
min_cut=[],
1247+
cost_distribution={},
12431248
)
12441249
flow_val, summary = 0.0, empty_summary
12451250
else:
@@ -1255,6 +1260,7 @@ def _max_flow_with_summary_internal(
12551260
residual_cap={},
12561261
reachable=set(),
12571262
min_cut=[],
1263+
cost_distribution={},
12581264
)
12591265
flow_val, summary = 0.0, empty_summary
12601266
results[(src_label, snk_label)] = (flow_val, summary)
@@ -1439,6 +1445,7 @@ def _max_flow_detailed_internal(
14391445
residual_cap={},
14401446
reachable=set(),
14411447
min_cut=[],
1448+
cost_distribution={},
14421449
)
14431450
return {
14441451
(combined_src_label, combined_snk_label): (
@@ -1459,6 +1466,7 @@ def _max_flow_detailed_internal(
14591466
residual_cap={},
14601467
reachable=set(),
14611468
min_cut=[],
1469+
cost_distribution={},
14621470
)
14631471
return {
14641472
(combined_src_label, combined_snk_label): (
@@ -1501,6 +1509,7 @@ def _max_flow_detailed_internal(
15011509
residual_cap={},
15021510
reachable=set(),
15031511
min_cut=[],
1512+
cost_distribution={},
15041513
)
15051514
flow_val, summary, flow_graph = (
15061515
0.0,
@@ -1521,6 +1530,7 @@ def _max_flow_detailed_internal(
15211530
residual_cap={},
15221531
reachable=set(),
15231532
min_cut=[],
1533+
cost_distribution={},
15241534
)
15251535
flow_val, summary, flow_graph = 0.0, empty_summary, base_graph
15261536
results[(src_label, snk_label)] = (flow_val, summary, flow_graph)

0 commit comments

Comments
 (0)