From d9159beaf96c215f6e878127525ecabc12e39b8f Mon Sep 17 00:00:00 2001 From: Andrey Golovanov Date: Sun, 13 Sep 2026 00:59:15 +0100 Subject: [PATCH] SHORTEST_PATHS_ECMP on Core EQUAL_BALANCED; netgraph-core>=0.10.0 (0.23.1) Core 0.10.0 folds the load-blind next-hop set into EQUAL_BALANCED and removes EQUAL_BALANCED_FIXED. The IGP ECMP preset and the fan-out path in the cached engine map to EQUAL_BALANCED; placement is unchanged except max_flow(EQUAL_BALANCED, shortest_path=False, require_capacity=False), which now places once. Co-Authored-By: Claude Fable 5.1 --- CHANGELOG.md | 6 ++++++ docs/reference/design.md | 13 ++++--------- ngraph/analysis/placement.py | 2 +- ngraph/model/flow/policy_config.py | 2 +- pyproject.toml | 4 ++-- tests/analysis/test_placement.py | 2 +- tests/model/flow/test_policy_config.py | 2 +- 7 files changed, 16 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2dcd152..a34fd13 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.23.1] - 2026-09-13 + +### Changed + +- Minimum `netgraph-core` raised to 0.10.0. Core's `EQUAL_BALANCED` now keeps every next hop with capacity in the split, the load-blind behaviour that 0.23.0 obtained from a separate Core mode, and that mode is gone. `SHORTEST_PATHS_ECMP` and the `TE_ECMP_*` presets place exactly as in 0.23.0. The one difference is `max_flow` with `flow_placement=EQUAL_BALANCED`, `shortest_path=False` and `require_capacity=False`: it filled the cost-only DAG in several passes and now places once, the single-pass answer that cost-only routing is documented to give. Use `require_capacity=True` for the residual-aware fill + ## [0.23.0] - 2026-09-13 ### Fixed diff --git a/docs/reference/design.md b/docs/reference/design.md index 8a44aa2..254bbf9 100644 --- a/docs/reference/design.md +++ b/docs/reference/design.md @@ -570,13 +570,8 @@ Beyond routing semantics, NetGraph controls how flow splits across equal-cost pa - Example: Two 100G links get 50/50; one 100G + one 10G still attempt 50/50 (10G saturates first) - Models IP hash-based load balancing (5-tuple hashing distributes flows uniformly) - Single-pass admission: computes one global scale factor to avoid oversubscription - - The split set is the DAG edges that still have residual, so a later placement on the same DAG hashes only over the members that are not yet full (progressive behaviour, which is what `place_max_flow` and the LSP policies rely on) - - For IP ECMP simulation: use with `require_capacity=false` + `shortest_path=true` - -- **EQUAL_BALANCED_FIXED** (lossless ECMP admission with a load-blind forwarding table): - - Same equal split and global scale, but the split set is every DAG edge with capacity, saturated or not - - A member filled by an earlier demand therefore drives the scale to 0: any further traffic hashed onto it would be lost, so nothing more is admitted losslessly - - Backs the `SHORTEST_PATHS_ECMP` preset + - The split set is every DAG edge with capacity, full or not, because a forwarding table does not react to load. A member filled by an earlier demand drives the scale to 0: any further traffic hashed onto it would be lost, so nothing more is admitted losslessly on that DAG. `place_max_flow` and the LSP policies progress past a full member by recomputing the DAG with a residual-aware SPF + - Backs the `SHORTEST_PATHS_ECMP` preset; for IP ECMP simulation use `require_capacity=false` + `shortest_path=true` - **EQUAL_BALANCED_LOSSY** (best-effort ECMP forwarding): - Same split set, no scale: every edge carries `min(share, residual)` and drops the rest; a deficit propagates downstream and the placed amount is what reaches the sink @@ -630,7 +625,7 @@ For traffic matrix placement, `FlowPolicyPreset` values bundle the routing seman | Preset | `require_capacity` | `shortest_path` | `multi_edge` | `max_flow_count` | `flow_placement` | | -------- | -------------------- | ----------------- | -------------- | ------------------ | ------------------ | -| `SHORTEST_PATHS_ECMP` | `false` | `true` | `true` | `1` | `EQUAL_BALANCED_FIXED` | +| `SHORTEST_PATHS_ECMP` | `false` | `true` | `true` | `1` | `EQUAL_BALANCED` | | `SHORTEST_PATHS_ECMP_LOSSY` | `false` | `true` | `true` | `1` | `EQUAL_BALANCED_LOSSY` | | `SHORTEST_PATHS_WCMP` | `false` | `true` | `true` | `1` | `PROPORTIONAL` | | `TE_WCMP_UNLIM` | `true` | `false` | `true` | unlimited | `PROPORTIONAL` | @@ -643,7 +638,7 @@ For traffic matrix placement, `FlowPolicyPreset` values bundle the routing seman - `shortest_path`: When `true`, each demand is placed in a single pass on its cost-only shortest-path DAG and the remainder is dropped; when `false`, the remainder is rerouted tier by tier on residual-aware paths. - `multi_edge`: When `true`, uses all parallel equal-cost edges (hop-by-hop ECMP); when `false`, each flow uses a single path (tunnel/LSP semantics). - `max_flow_count`: Internal per-preset limit on flows/LSPs for TE presets; not a user-facing parameter. -- `flow_placement`: `EQUAL_BALANCED_FIXED` splits equally over the topology's next hops and admits losslessly; `EQUAL_BALANCED_LOSSY` splits the same way and drops what does not fit; `EQUAL_BALANCED` splits equally over next hops with headroom (progressive, used by the LSP presets); `PROPORTIONAL` splits by residual capacity. +- `flow_placement`: `EQUAL_BALANCED` splits equally over the topology's next hops and admits losslessly; `EQUAL_BALANCED_LOSSY` splits the same way and drops what does not fit; `PROPORTIONAL` splits by residual capacity. **Example: Modeling IP vs MPLS Networks** diff --git a/ngraph/analysis/placement.py b/ngraph/analysis/placement.py index 14fd6c0..c472169 100644 --- a/ngraph/analysis/placement.py +++ b/ngraph/analysis/placement.py @@ -305,7 +305,7 @@ def place_demands( member_ids.append(member_id) if ( _preset_modes(demand.policy_preset)[1] - == netgraph_core.FlowPlacement.EQUAL_BALANCED_FIXED + == netgraph_core.FlowPlacement.EQUAL_BALANCED ): # Lossless admission applies to the demand as a whole: one # pass over a DAG that fans out evenly from the pseudo diff --git a/ngraph/model/flow/policy_config.py b/ngraph/model/flow/policy_config.py index 15c4f14..b9d48db 100644 --- a/ngraph/model/flow/policy_config.py +++ b/ngraph/model/flow/policy_config.py @@ -150,7 +150,7 @@ def preset_config(preset: FlowPolicyPreset) -> netgraph_core.FlowPolicyConfig: config.min_flow_count = 1 config.max_flow_count = 1 if preset == FlowPolicyPreset.SHORTEST_PATHS_ECMP: - config.flow_placement = netgraph_core.FlowPlacement.EQUAL_BALANCED_FIXED + config.flow_placement = netgraph_core.FlowPlacement.EQUAL_BALANCED elif preset == FlowPolicyPreset.SHORTEST_PATHS_ECMP_LOSSY: config.flow_placement = netgraph_core.FlowPlacement.EQUAL_BALANCED_LOSSY else: diff --git a/pyproject.toml b/pyproject.toml index 077c121..231bdc2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta" # --------------------------------------------------------------------- [project] name = "ngraph" -version = "0.23.0" +version = "0.23.1" description = "A tool and a library for network modeling and analysis." readme = "README.md" authors = [{ name = "Andrey Golovanov" }] @@ -35,7 +35,7 @@ dependencies = [ "pyyaml>=6.0", "pandas>=2.0", "jsonschema>=4.0", - "netgraph-core>=0.9.0", + "netgraph-core>=0.10.0", ] [project.urls] diff --git a/tests/analysis/test_placement.py b/tests/analysis/test_placement.py index ae2bce4..cec92d3 100644 --- a/tests/analysis/test_placement.py +++ b/tests/analysis/test_placement.py @@ -191,7 +191,7 @@ def test_get_placement_for_ecmp(self) -> None: placement = _get_flow_placement(FlowPolicyPreset.SHORTEST_PATHS_ECMP) # Lossless hash-ECMP admission with a load-blind next-hop set. - assert placement == netgraph_core.FlowPlacement.EQUAL_BALANCED_FIXED + assert placement == netgraph_core.FlowPlacement.EQUAL_BALANCED def test_get_placement_for_wcmp(self) -> None: """Test FlowPlacement for WCMP preset.""" diff --git a/tests/model/flow/test_policy_config.py b/tests/model/flow/test_policy_config.py index e8f7375..be7f1fc 100644 --- a/tests/model/flow/test_policy_config.py +++ b/tests/model/flow/test_policy_config.py @@ -169,7 +169,7 @@ def test_preset_config_hop_by_hop_presets_are_cost_only_single_pass(): from ngraph.model.flow.policy_config import HOP_BY_HOP_PRESETS, preset_config expected_placement = { - FlowPolicyPreset.SHORTEST_PATHS_ECMP: netgraph_core.FlowPlacement.EQUAL_BALANCED_FIXED, + FlowPolicyPreset.SHORTEST_PATHS_ECMP: netgraph_core.FlowPlacement.EQUAL_BALANCED, FlowPolicyPreset.SHORTEST_PATHS_WCMP: netgraph_core.FlowPlacement.PROPORTIONAL, FlowPolicyPreset.SHORTEST_PATHS_ECMP_LOSSY: netgraph_core.FlowPlacement.EQUAL_BALANCED_LOSSY, }