Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
13 changes: 4 additions & 9 deletions docs/reference/design.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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` |
Expand All @@ -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**

Expand Down
2 changes: 1 addition & 1 deletion ngraph/analysis/placement.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion ngraph/model/flow/policy_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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" }]
Expand Down Expand Up @@ -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]
Expand Down
2 changes: 1 addition & 1 deletion tests/analysis/test_placement.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""
Expand Down
2 changes: 1 addition & 1 deletion tests/model/flow/test_policy_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
Expand Down
Loading