Skip to content

Commit e62ad6f

Browse files
committed
Preparing v0.11.0
- Introduce AnalysisContext for efficient repeated analysis in the performance runner. - Remove deprecated max-flow methods and update related documentation. - Add tests for the new analysis context and ensure proper tracking of disabled nodes and links.
1 parent 649c651 commit e62ad6f

38 files changed

Lines changed: 4114 additions & 4514 deletions

README.md

Lines changed: 18 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -73,22 +73,24 @@ ngraph run scenarios/backbone_clos.yml --results clos.results.json
7373
### Python API
7474

7575
```python
76-
from ngraph.scenario import Scenario
77-
from ngraph.types.base import FlowPlacement
78-
from ngraph.solver.maxflow import max_flow
79-
80-
# Load scenario
81-
scenario = Scenario.from_yaml("""
82-
network:
83-
nodes: {A: {}, B: {}, C: {}}
84-
links:
85-
- {source: A, target: B, link_params: {capacity: 10, cost: 1}}
86-
- {source: B, target: C, link_params: {capacity: 10, cost: 1}}
87-
""")
88-
89-
# Compute max flow
90-
flow = max_flow(scenario.network, "A", "C", shortest_path=True)
91-
print(f"Max flow: {flow}")
76+
from ngraph import Network, Node, Link, analyze, Mode
77+
78+
# Build network programmatically
79+
network = Network()
80+
network.add_node(Node("A"))
81+
network.add_node(Node("B"))
82+
network.add_node(Node("C"))
83+
network.add_link(Link("A", "B", capacity=10.0, cost=1.0))
84+
network.add_link(Link("B", "C", capacity=10.0, cost=1.0))
85+
86+
# Compute max flow with the analyze() API
87+
flow = analyze(network).max_flow("^A$", "^C$", mode=Mode.COMBINE)
88+
print(f"Max flow: {flow}") # {('^A$', '^C$'): 10.0}
89+
90+
# Efficient repeated analysis with bound context
91+
ctx = analyze(network, source="^A$", sink="^C$", mode=Mode.COMBINE)
92+
baseline = ctx.max_flow()
93+
degraded = ctx.max_flow(excluded_nodes={"B"}) # Test failure scenario
9294
```
9395

9496
## Example Scenario

dev/perf/runner.py

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
import netgraph_core
1212
import networkx as nx
1313

14+
from ngraph.analysis import AnalysisContext
15+
1416
from .core import (
1517
BenchmarkCase,
1618
BenchmarkProfile,
@@ -89,11 +91,10 @@ def _execute_spf_benchmark(case: BenchmarkCase, iterations: int) -> BenchmarkSam
8991

9092
# Create network and Core graph once outside timing loop
9193
network = topology.create_network()
92-
graph_handle, multidigraph, edge_mapper, node_mapper = network.build_core_graph()
94+
ctx = AnalysisContext.from_network(network)
9395

94-
# Create Core backend and algorithms
95-
backend = netgraph_core.Backend.cpu()
96-
algs = netgraph_core.Algorithms(backend)
96+
# Use context's algorithms instance
97+
algs = ctx.algorithms
9798

9899
# Use first node (ID 0) as source for SPF
99100
source_id = 0
@@ -105,9 +106,9 @@ def _execute_spf_benchmark(case: BenchmarkCase, iterations: int) -> BenchmarkSam
105106
tie_break=netgraph_core.EdgeTieBreak.DETERMINISTIC,
106107
)
107108

108-
# Create a closure that captures the graph and source
109+
# Create a closure that captures the context and source
109110
def run_spf():
110-
return algs.spf(graph_handle, source_id, selection=edge_selection)
111+
return algs.spf(ctx.handle, source_id, selection=edge_selection)
111112

112113
# Time the SPF execution
113114
timing_stats = _time_func(run_spf, iterations)
@@ -208,20 +209,19 @@ def _execute_max_flow_benchmark(
208209
"""
209210
topology: Topology = case.inputs["topology"]
210211
network = topology.create_network()
211-
graph_handle, multidigraph, edge_mapper, node_mapper = network.build_core_graph()
212+
ctx = AnalysisContext.from_network(network)
212213

213-
# Create Core backend and algorithms
214-
backend = netgraph_core.Backend.cpu()
215-
algs = netgraph_core.Algorithms(backend)
214+
# Use context's algorithms instance
215+
algs = ctx.algorithms
216216

217217
# Use first node as source and last node as sink for maximum path length
218218
source_id = 0
219-
sink_id = multidigraph.num_nodes() - 1
219+
sink_id = ctx.multidigraph.num_nodes() - 1
220220

221-
# Create a closure that captures the graph handle and node IDs
221+
# Create a closure that captures the context handle and node IDs
222222
def run_max_flow():
223223
flow_value, _ = algs.max_flow(
224-
graph_handle,
224+
ctx.handle,
225225
source_id,
226226
sink_id,
227227
flow_placement=netgraph_core.FlowPlacement.PROPORTIONAL,

docs/examples/basic.md

Lines changed: 104 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,10 @@ See [Tutorial](../getting-started/tutorial.md) for CLI usage and bundled scenari
1010

1111
```text
1212
[1,1] & [1,2] [1,1] & [1,2]
13-
A ─────────────────── B ────────────── C
14-
15-
[2,3] [2,3]
16-
└──────────────────── D ───────────────┘
13+
A -------------------- B ---------------- C
14+
| |
15+
| [2,3] | [2,3]
16+
+-------------------- D -----------------+
1717
1818
[1,1] and [1,2] are parallel edges between A and B.
1919
They have the same metric of 1 but different capacities (1 and 2).
@@ -23,8 +23,7 @@ Let's create this network by using NetGraph's scenario system:
2323

2424
```python
2525
from ngraph.scenario import Scenario
26-
from ngraph.types.base import FlowPlacement
27-
from ngraph.solver.maxflow import max_flow, max_flow_with_details
26+
from ngraph import analyze, Mode, FlowPlacement
2827

2928
# Define network topology with parallel paths
3029
scenario_yaml = """
@@ -42,7 +41,7 @@ network:
4241
4342
# Create links with different capacities and costs
4443
links:
45-
# Parallel edges between AB
44+
# Parallel edges between A->B
4645
- source: A
4746
target: B
4847
link_params:
@@ -54,7 +53,7 @@ network:
5453
capacity: 2
5554
cost: 1
5655
57-
# Parallel edges between BC
56+
# Parallel edges between B->C
5857
- source: B
5958
target: C
6059
link_params:
@@ -66,7 +65,7 @@ network:
6665
capacity: 2
6766
cost: 1
6867
69-
# Alternative path A→D→C
68+
# Alternative path A->D->C
7069
- source: A
7170
target: D
7271
link_params:
@@ -88,34 +87,34 @@ Note that here we used a simple `nodes` and `links` structure to directly define
8887

8988
### Flow Analysis Variants
9089

91-
Now let's run MaxFlow using the high-level Network API:
90+
Now let's run MaxFlow using the `analyze()` API:
9291

9392
```python
9493
# 1. "True" maximum flow (uses all available paths)
95-
max_flow_all = max_flow(network, source_path="A", sink_path="C")
94+
max_flow_all = analyze(network).max_flow("^A$", "^C$", mode=Mode.COMBINE)
9695
print(f"Maximum flow (all paths): {max_flow_all}")
97-
# Result: {('A', 'C'): 6.0} (uses both A→B→C path capacity of 3 and A→D→C path capacity of 3)
96+
# Result: {('^A$', '^C$'): 6.0} (uses both A->B->C path capacity of 3 and A->D->C path capacity of 3)
9897

9998
# 2. Flow along shortest paths only
100-
max_flow_shortest = max_flow(
101-
network,
102-
source_path="A",
103-
sink_path="C",
99+
max_flow_shortest = analyze(network).max_flow(
100+
"^A$",
101+
"^C$",
102+
mode=Mode.COMBINE,
104103
shortest_path=True
105104
)
106105
print(f"Flow on shortest paths: {max_flow_shortest}")
107-
# Result: {('A', 'C'): 3.0} (only uses A→B→C path, ignoring higher-cost A→D→C path)
106+
# Result: {('^A$', '^C$'): 3.0} (only uses A->B->C path, ignoring higher-cost A->D->C path)
108107

109108
# 3. Equal-balanced flow placement on shortest paths
110-
max_flow_shortest_balanced = max_flow(
111-
network,
112-
source_path="A",
113-
sink_path="C",
109+
max_flow_shortest_balanced = analyze(network).max_flow(
110+
"^A$",
111+
"^C$",
112+
mode=Mode.COMBINE,
114113
shortest_path=True,
115114
flow_placement=FlowPlacement.EQUAL_BALANCED
116115
)
117116
print(f"Equal-balanced flow: {max_flow_shortest_balanced}")
118-
# Result: {('A', 'C'): 2.0} (splits flow equally across parallel edges in AB and BC)
117+
# Result: {('^A$', '^C$'): 2.0} (splits flow equally across parallel edges in A->B and B->C)
119118
```
120119

121120
## Results Interpretation
@@ -132,11 +131,10 @@ Cost distribution shows how flow splits across path costs for latency/span analy
132131

133132
```python
134133
# Get flow analysis with cost distribution
135-
result = max_flow_with_details(
136-
network,
137-
source_path="A",
138-
sink_path="C",
139-
mode="combine"
134+
result = analyze(network).max_flow_detailed(
135+
"^A$",
136+
"^C$",
137+
mode=Mode.COMBINE
140138
)
141139

142140
# Extract flow value and summary
@@ -150,83 +148,102 @@ print(f"Cost distribution: {summary.cost_distribution}")
150148
# Cost distribution: {2.0: 3.0, 4.0: 3.0}
151149
#
152150
# This means:
153-
# - 3.0 units of flow use paths with total cost 2.0 (A→B→C path)
154-
# - 3.0 units of flow use paths with total cost 4.0 (A→D→C path)
151+
# - 3.0 units of flow use paths with total cost 2.0 (A->B->C path)
152+
# - 3.0 units of flow use paths with total cost 4.0 (A->D->C path)
155153
```
156154

157155
### Latency Span Analysis
158156

159157
If link costs approximate latency, derive span summary from cost distribution:
160158

161159
```python
162-
def analyze_latency_span(cost_distribution):
163-
"""Analyze latency characteristics from cost distribution."""
164-
if not cost_distribution:
165-
return "No flow paths available"
166-
167-
total_flow = sum(cost_distribution.values())
168-
weighted_avg_latency = sum(
169-
cost * flow for cost, flow in cost_distribution.items()
170-
) / total_flow
171-
172-
min_latency = min(cost_distribution.keys())
173-
max_latency = max(cost_distribution.keys())
174-
latency_span = max_latency - min_latency
175-
176-
print(f"Latency Analysis:")
177-
print(f" Average latency: {weighted_avg_latency:.2f}")
178-
print(f" Latency range: {min_latency:.1f} - {max_latency:.1f}")
179-
print(f" Latency span: {latency_span:.1f}")
180-
print(f" Flow distribution:")
181-
for cost, flow in sorted(cost_distribution.items()):
182-
percentage = (flow / total_flow) * 100
183-
print(f" {percentage:.1f}% uses paths with latency {cost:.1f}")
184-
185-
# Example usage
186-
analyze_latency_span(summary.cost_distribution)
160+
# Example cost distribution analysis
161+
cost_dist = summary.cost_distribution # {2.0: 3.0, 4.0: 3.0}
162+
total_flow = summary.total_flow # 6.0
163+
164+
# Calculate weighted average latency
165+
avg_latency = sum(cost * flow for cost, flow in cost_dist.items()) / total_flow
166+
print(f"Average latency: {avg_latency}") # 3.0
167+
168+
# Find min/max latency tiers
169+
min_latency = min(cost_dist.keys())
170+
max_latency = max(cost_dist.keys())
171+
print(f"Latency range: {min_latency} - {max_latency}") # 2.0 - 4.0
187172
```
188173

189-
This helps identify traffic concentration, latency span, and potential bottlenecks.
174+
## Efficient Repeated Analysis
190175

191-
## Advanced Analysis: Failure Simulation
176+
For scenarios requiring multiple analyses with different exclusions (e.g., failure testing), use a bound context:
192177

193-
You can analyze the network under different failure scenarios by excluding nodes or links:
178+
```python
179+
# Create bound context - graph built once
180+
ctx = analyze(network, source="^A$", sink="^C$", mode=Mode.COMBINE)
181+
182+
# Baseline capacity
183+
baseline = ctx.max_flow()
184+
print(f"Baseline: {baseline}")
185+
186+
# Test various failure scenarios
187+
for node in ["B", "D"]:
188+
degraded = ctx.max_flow(excluded_nodes={node})
189+
print(f"Without {node}: {degraded}")
190+
191+
# Output:
192+
# Baseline: {('^A$', '^C$'): 6.0}
193+
# Without B: {('^A$', '^C$'): 3.0}
194+
# Without D: {('^A$', '^C$'): 3.0}
195+
```
196+
197+
## Sensitivity Analysis
198+
199+
Identify which edges are critical for the flow:
194200

195201
```python
196-
# Identify link to fail
197-
failed_links = set()
198-
for link_id, link in network.links.items():
199-
if link.source == "A" and link.target == "D":
200-
failed_links.add(link_id)
201-
break
202-
203-
# Compare flows: baseline vs. with failure
204-
baseline_flow_dict = max_flow(network, source_path="A", sink_path="C")
205-
baseline_flow = baseline_flow_dict[('A', 'C')]
206-
207-
degraded_flow_dict = max_flow(
208-
network,
209-
source_path="A",
210-
sink_path="C",
211-
excluded_links=failed_links
202+
# Get sensitivity analysis
203+
sensitivity = analyze(network).sensitivity(
204+
"^A$",
205+
"^C$",
206+
mode=Mode.COMBINE,
207+
shortest_path=False # Full max-flow mode
212208
)
213-
degraded_flow = degraded_flow_dict[('A', 'C')]
214209

215-
print(f"Baseline flow: {baseline_flow}")
216-
print(f"Flow with A->D link failed: {degraded_flow}")
217-
print(f"Impact: {baseline_flow - degraded_flow} units lost")
210+
for pair, edge_impacts in sensitivity.items():
211+
print(f"Critical edges for {pair}:")
212+
for edge_key, flow_reduction in sorted(edge_impacts.items(), key=lambda x: -x[1]):
213+
print(f" {edge_key}: -{flow_reduction:.1f}")
218214
```
219215

220-
This analysis helps identify:
216+
## Shortest Paths
221217

222-
- **Critical links**: Links whose failure significantly impacts flow
223-
- **Redundancy**: How well the network handles failures
224-
- **Vulnerability assessment**: Network resilience under different failure scenarios
218+
Get actual path objects for routing analysis:
225219

226-
## Next Steps
220+
```python
221+
from ngraph import EdgeSelect
222+
223+
# Get all equal-cost shortest paths
224+
paths = analyze(network).shortest_paths(
225+
"^A$",
226+
"^C$",
227+
mode=Mode.COMBINE,
228+
edge_select=EdgeSelect.ALL_MIN_COST
229+
)
230+
231+
for pair, path_list in paths.items():
232+
print(f"Paths from {pair[0]} to {pair[1]}:")
233+
for path in path_list:
234+
nodes = [elem[0] for elem in path.path]
235+
print(f" {' -> '.join(nodes)} (cost: {path.cost})")
236+
237+
# Get k-shortest paths
238+
k_paths = analyze(network).k_shortest_paths(
239+
"^A$",
240+
"^C$",
241+
max_k=3,
242+
mode=Mode.PAIRWISE
243+
)
227244

228-
- **[Bundled Scenarios](bundled-scenarios.md)** - Ready-to-run examples
229-
- **[Clos Fabric Analysis](clos-fabric.md)** - More complex example
230-
- **[Workflow Reference](../reference/workflow.md)** - Analysis workflows and Monte Carlo simulation
231-
- **[DSL Reference](../reference/dsl.md)** - Learn the full YAML syntax for scenarios
232-
- **[API Reference](../reference/api.md)** - Explore the Python API for advanced usage
245+
for pair, path_list in k_paths.items():
246+
print(f"Top {len(path_list)} paths from {pair[0]} to {pair[1]}:")
247+
for i, path in enumerate(path_list, 1):
248+
print(f" {i}. Cost: {path.cost}")
249+
```

0 commit comments

Comments
 (0)