netgraph-core 0.7.2__tar.gz → 0.9.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {netgraph_core-0.7.2 → netgraph_core-0.9.0}/.github/workflows/release.yml +20 -1
- {netgraph_core-0.7.2 → netgraph_core-0.9.0}/.gitignore +3 -0
- netgraph_core-0.9.0/.superset/config.json +4 -0
- netgraph_core-0.9.0/.superset/workspace.sh +29 -0
- netgraph_core-0.9.0/AGENTS.md +15 -0
- netgraph_core-0.9.0/CHANGELOG.md +233 -0
- netgraph_core-0.9.0/CLAUDE.md +1 -0
- {netgraph_core-0.7.2 → netgraph_core-0.9.0}/CMakeLists.txt +28 -4
- {netgraph_core-0.7.2 → netgraph_core-0.9.0}/CONTRIBUTING.md +13 -7
- {netgraph_core-0.7.2 → netgraph_core-0.9.0}/Makefile +4 -24
- {netgraph_core-0.7.2 → netgraph_core-0.9.0}/PKG-INFO +46 -9
- {netgraph_core-0.7.2 → netgraph_core-0.9.0}/README.md +44 -7
- {netgraph_core-0.7.2 → netgraph_core-0.9.0}/bindings/python/module.cpp +121 -31
- netgraph_core-0.9.0/dev/cpp-tests.sh +27 -0
- {netgraph_core-0.7.2 → netgraph_core-0.9.0}/include/netgraph/core/algorithms.hpp +5 -4
- {netgraph_core-0.7.2 → netgraph_core-0.9.0}/include/netgraph/core/backend.hpp +12 -14
- {netgraph_core-0.7.2 → netgraph_core-0.9.0}/include/netgraph/core/flow_graph.hpp +34 -2
- {netgraph_core-0.7.2 → netgraph_core-0.9.0}/include/netgraph/core/flow_policy.hpp +101 -7
- {netgraph_core-0.7.2 → netgraph_core-0.9.0}/include/netgraph/core/flow_state.hpp +17 -2
- {netgraph_core-0.7.2 → netgraph_core-0.9.0}/include/netgraph/core/max_flow.hpp +19 -3
- {netgraph_core-0.7.2 → netgraph_core-0.9.0}/include/netgraph/core/options.hpp +11 -0
- {netgraph_core-0.7.2 → netgraph_core-0.9.0}/include/netgraph/core/profiling.hpp +3 -5
- netgraph_core-0.9.0/include/netgraph/core/shortest_paths.hpp +131 -0
- {netgraph_core-0.7.2 → netgraph_core-0.9.0}/include/netgraph/core/strict_multidigraph.hpp +12 -0
- {netgraph_core-0.7.2 → netgraph_core-0.9.0}/include/netgraph/core/types.hpp +21 -2
- {netgraph_core-0.7.2 → netgraph_core-0.9.0}/pyproject.toml +3 -3
- {netgraph_core-0.7.2 → netgraph_core-0.9.0}/python/netgraph_core/__init__.py +27 -15
- {netgraph_core-0.7.2 → netgraph_core-0.9.0}/python/netgraph_core/_docs.py +368 -59
- {netgraph_core-0.7.2 → netgraph_core-0.9.0}/src/cpu_backend.cpp +18 -6
- {netgraph_core-0.7.2 → netgraph_core-0.9.0}/src/flow_graph.cpp +52 -3
- netgraph_core-0.9.0/src/flow_policy.cpp +841 -0
- {netgraph_core-0.7.2 → netgraph_core-0.9.0}/src/flow_state.cpp +188 -47
- {netgraph_core-0.7.2 → netgraph_core-0.9.0}/src/k_shortest_paths.cpp +51 -55
- {netgraph_core-0.7.2 → netgraph_core-0.9.0}/src/max_flow.cpp +133 -21
- {netgraph_core-0.7.2 → netgraph_core-0.9.0}/src/profiling.cpp +1 -0
- netgraph_core-0.9.0/src/shortest_paths.cpp +731 -0
- {netgraph_core-0.7.2 → netgraph_core-0.9.0}/src/strict_multidigraph.cpp +15 -1
- {netgraph_core-0.7.2 → netgraph_core-0.9.0}/tests/cpp/flow_graph_tests.cpp +46 -0
- {netgraph_core-0.7.2 → netgraph_core-0.9.0}/tests/cpp/flow_policy_tests.cpp +156 -0
- {netgraph_core-0.7.2 → netgraph_core-0.9.0}/tests/cpp/flow_state_tests.cpp +122 -0
- {netgraph_core-0.7.2 → netgraph_core-0.9.0}/tests/cpp/k_shortest_paths_tests.cpp +24 -0
- {netgraph_core-0.7.2 → netgraph_core-0.9.0}/tests/cpp/masking_tests.cpp +1 -1
- {netgraph_core-0.7.2 → netgraph_core-0.9.0}/tests/cpp/max_flow_tests.cpp +80 -0
- {netgraph_core-0.7.2 → netgraph_core-0.9.0}/tests/cpp/shortest_paths_tests.cpp +153 -0
- {netgraph_core-0.7.2 → netgraph_core-0.9.0}/tests/cpp/test_utils.hpp +0 -33
- {netgraph_core-0.7.2 → netgraph_core-0.9.0}/tests/py/conftest.py +124 -13
- netgraph_core-0.9.0/tests/py/test_equal_balanced_modes.py +237 -0
- {netgraph_core-0.7.2 → netgraph_core-0.9.0}/tests/py/test_flow_policy.py +0 -5
- netgraph_core-0.9.0/tests/py/test_flow_policy_spf_memo.py +158 -0
- {netgraph_core-0.7.2 → netgraph_core-0.9.0}/tests/py/test_flow_policy_validation.py +0 -15
- {netgraph_core-0.7.2 → netgraph_core-0.9.0}/tests/py/test_graph_from_arrays.py +6 -7
- {netgraph_core-0.7.2 → netgraph_core-0.9.0}/tests/py/test_max_flow.py +6 -3
- {netgraph_core-0.7.2 → netgraph_core-0.9.0}/tests/py/test_max_flow_summary.py +1 -1
- netgraph_core-0.9.0/tests/py/test_maxflow_certificate.py +197 -0
- {netgraph_core-0.7.2 → netgraph_core-0.9.0}/tests/py/test_policy_vs_maxflow_equivalence.py +0 -19
- netgraph_core-0.9.0/tests/py/test_review_regressions.py +590 -0
- netgraph_core-0.9.0/tests/py/test_spf_to.py +114 -0
- netgraph_core-0.9.0/tests/py/test_static_paths.py +392 -0
- {netgraph_core-0.7.2 → netgraph_core-0.9.0}/tests/py/test_thread_safety.py +17 -8
- netgraph_core-0.7.2/.gcovr.cfg +0 -20
- netgraph_core-0.7.2/CHANGELOG.md +0 -177
- netgraph_core-0.7.2/include/netgraph/core/shortest_paths.hpp +0 -64
- netgraph_core-0.7.2/src/flow_policy.cpp +0 -419
- netgraph_core-0.7.2/src/shortest_paths.cpp +0 -411
- {netgraph_core-0.7.2 → netgraph_core-0.9.0}/.coveragerc +0 -0
- {netgraph_core-0.7.2 → netgraph_core-0.9.0}/.github/workflows/tests.yml +0 -0
- {netgraph_core-0.7.2 → netgraph_core-0.9.0}/.pre-commit-config.yaml +0 -0
- {netgraph_core-0.7.2 → netgraph_core-0.9.0}/LICENSE +0 -0
- {netgraph_core-0.7.2 → netgraph_core-0.9.0}/dev/benchmark_profiling_overhead.py +0 -0
- {netgraph_core-0.7.2 → netgraph_core-0.9.0}/dev/coverage_summary.py +0 -0
- {netgraph_core-0.7.2 → netgraph_core-0.9.0}/dev/run-checks.sh +0 -0
- {netgraph_core-0.7.2 → netgraph_core-0.9.0}/include/netgraph/core/constants.hpp +0 -0
- {netgraph_core-0.7.2 → netgraph_core-0.9.0}/include/netgraph/core/flow.hpp +0 -0
- {netgraph_core-0.7.2 → netgraph_core-0.9.0}/include/netgraph/core/k_shortest_paths.hpp +0 -0
- {netgraph_core-0.7.2 → netgraph_core-0.9.0}/python/netgraph_core/py.typed +0 -0
- {netgraph_core-0.7.2 → netgraph_core-0.9.0}/tests/cpp/smoke_test.cpp +0 -0
- {netgraph_core-0.7.2 → netgraph_core-0.9.0}/tests/cpp/strict_multidigraph_tests.cpp +0 -0
- {netgraph_core-0.7.2 → netgraph_core-0.9.0}/tests/py/test_api_validation.py +0 -0
- {netgraph_core-0.7.2 → netgraph_core-0.9.0}/tests/py/test_batch_max_flow_consistency.py +0 -0
- {netgraph_core-0.7.2 → netgraph_core-0.9.0}/tests/py/test_batch_max_flow_masks.py +0 -0
- {netgraph_core-0.7.2 → netgraph_core-0.9.0}/tests/py/test_cross_validation.py +0 -0
- {netgraph_core-0.7.2 → netgraph_core-0.9.0}/tests/py/test_edge_cases.py +0 -0
- {netgraph_core-0.7.2 → netgraph_core-0.9.0}/tests/py/test_ext_edge_ids.py +0 -0
- {netgraph_core-0.7.2 → netgraph_core-0.9.0}/tests/py/test_flow_policy_configurations.py +0 -0
- {netgraph_core-0.7.2 → netgraph_core-0.9.0}/tests/py/test_flow_policy_edge_cases.py +0 -0
- {netgraph_core-0.7.2 → netgraph_core-0.9.0}/tests/py/test_flow_policy_lifecycle.py +0 -0
- {netgraph_core-0.7.2 → netgraph_core-0.9.0}/tests/py/test_flow_policy_masks.py +0 -0
- {netgraph_core-0.7.2 → netgraph_core-0.9.0}/tests/py/test_flow_policy_path_distribution.py +0 -0
- {netgraph_core-0.7.2 → netgraph_core-0.9.0}/tests/py/test_graph_structure.py +0 -0
- {netgraph_core-0.7.2 → netgraph_core-0.9.0}/tests/py/test_imports.py +0 -0
- {netgraph_core-0.7.2 → netgraph_core-0.9.0}/tests/py/test_ksp.py +0 -0
- {netgraph_core-0.7.2 → netgraph_core-0.9.0}/tests/py/test_ksp_variants.py +0 -0
- {netgraph_core-0.7.2 → netgraph_core-0.9.0}/tests/py/test_lifetime_safety.py +0 -0
- {netgraph_core-0.7.2 → netgraph_core-0.9.0}/tests/py/test_mask_semantics.py +0 -0
- {netgraph_core-0.7.2 → netgraph_core-0.9.0}/tests/py/test_mask_validation.py +0 -0
- {netgraph_core-0.7.2 → netgraph_core-0.9.0}/tests/py/test_masking_comprehensive.py +0 -0
- {netgraph_core-0.7.2 → netgraph_core-0.9.0}/tests/py/test_max_flow_cost_distribution.py +0 -0
- {netgraph_core-0.7.2 → netgraph_core-0.9.0}/tests/py/test_paths_resolve.py +0 -0
- {netgraph_core-0.7.2 → netgraph_core-0.9.0}/tests/py/test_profiling.py +0 -0
- {netgraph_core-0.7.2 → netgraph_core-0.9.0}/tests/py/test_sensitivity.py +0 -0
- {netgraph_core-0.7.2 → netgraph_core-0.9.0}/tests/py/test_spf_basic.py +0 -0
- {netgraph_core-0.7.2 → netgraph_core-0.9.0}/tests/py/test_spf_edge_select.py +0 -0
- {netgraph_core-0.7.2 → netgraph_core-0.9.0}/tests/py/test_spf_masks.py +0 -0
|
@@ -10,6 +10,25 @@ on:
|
|
|
10
10
|
workflow_dispatch:
|
|
11
11
|
|
|
12
12
|
jobs:
|
|
13
|
+
# A tag push publishes whatever version pyproject.toml declares, so a mistyped
|
|
14
|
+
# tag would ship the wrong version silently. Fail fast instead.
|
|
15
|
+
check_version:
|
|
16
|
+
name: Tag matches project version
|
|
17
|
+
runs-on: ubuntu-latest
|
|
18
|
+
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')
|
|
19
|
+
steps:
|
|
20
|
+
- uses: actions/checkout@v4
|
|
21
|
+
- name: Compare tag with pyproject version
|
|
22
|
+
run: |
|
|
23
|
+
python -c "
|
|
24
|
+
import tomllib, os, sys
|
|
25
|
+
version = tomllib.load(open('pyproject.toml','rb'))['project']['version']
|
|
26
|
+
tag = os.environ['GITHUB_REF_NAME'].removeprefix('v')
|
|
27
|
+
if tag != version:
|
|
28
|
+
sys.exit(f'tag {tag!r} != pyproject version {version!r}')
|
|
29
|
+
print(f'tag matches project version: {version}')
|
|
30
|
+
"
|
|
31
|
+
|
|
13
32
|
build_wheels:
|
|
14
33
|
name: Wheel (${{ matrix.os }}, ${{ matrix.cibw_archs }})
|
|
15
34
|
runs-on: ${{ matrix.os }}
|
|
@@ -95,7 +114,7 @@ jobs:
|
|
|
95
114
|
|
|
96
115
|
publish_pypi:
|
|
97
116
|
name: Publish to PyPI
|
|
98
|
-
needs: [build_wheels, build_sdist, test_wheels_centos_stream_9]
|
|
117
|
+
needs: [check_version, build_wheels, build_sdist, test_wheels_centos_stream_9]
|
|
99
118
|
runs-on: ubuntu-latest
|
|
100
119
|
environment: pypi
|
|
101
120
|
permissions:
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
cd "$(dirname "${BASH_SOURCE[0]}")/.."
|
|
4
|
+
mode=${1:-help}
|
|
5
|
+
case "$mode" in
|
|
6
|
+
setup|check) ;;
|
|
7
|
+
*) echo 'Usage: bash .superset/workspace.sh {setup|check}'; exit 2 ;;
|
|
8
|
+
esac
|
|
9
|
+
if [[ "$mode" == setup && ! -x venv/bin/python ]]; then
|
|
10
|
+
make venv
|
|
11
|
+
fi
|
|
12
|
+
[[ -x venv/bin/python ]] || { echo 'Run bash .superset/workspace.sh setup first.' >&2; exit 1; }
|
|
13
|
+
export VIRTUAL_ENV="$PWD/venv"
|
|
14
|
+
export PATH="$VIRTUAL_ENV/bin:$PATH"
|
|
15
|
+
unset PYTHONPATH PYTHONHOME
|
|
16
|
+
export CMAKE_BUILD_PARALLEL_LEVEL="${CMAKE_BUILD_PARALLEL_LEVEL:-4}"
|
|
17
|
+
if [[ "$(uname -s)" == Darwin ]]; then
|
|
18
|
+
export CC="$(xcrun --find clang)"
|
|
19
|
+
export CXX="$(xcrun --find clang++)"
|
|
20
|
+
export MACOSX_DEPLOYMENT_TARGET="${MACOSX_DEPLOYMENT_TARGET:-15.0}"
|
|
21
|
+
fi
|
|
22
|
+
if [[ "$mode" == setup ]]; then
|
|
23
|
+
python -m pip install -e '.[dev]'
|
|
24
|
+
python -m pip check
|
|
25
|
+
python -c 'import netgraph_core, _netgraph_core; print("Workspace environment ready")'
|
|
26
|
+
else
|
|
27
|
+
make install
|
|
28
|
+
make check-ci
|
|
29
|
+
fi
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Development
|
|
2
|
+
|
|
3
|
+
- Setup: `bash .superset/workspace.sh setup`. Each worktree gets its own venv;
|
|
4
|
+
setup does not replace the Git hooks shared by worktrees.
|
|
5
|
+
- Superset **Run**: `bash .superset/workspace.sh check` rebuilds the Python
|
|
6
|
+
extension, then runs lint, types, C++ and Python tests. Editable Python imports
|
|
7
|
+
alone do not rebuild changed C++.
|
|
8
|
+
- For C++ changes, also run `make sanitize-test` (ASan/UBSan; errors must fail).
|
|
9
|
+
- For downstream changes, run NetGraph's `dev/check_core_integration.sh` with
|
|
10
|
+
this worktree's absolute path. Test the chosen build, not the published wheel.
|
|
11
|
+
|
|
12
|
+
Confirm defects with a reproducer. Max-flow tests need feasibility, conservation
|
|
13
|
+
and optimality/cut checks with nonuniform costs, masks and cost-factor cases.
|
|
14
|
+
Keep bindings and `_docs.py` consistent. Performance claims need correctness
|
|
15
|
+
checks and A/B/A on one quiet machine. Local checks do not replace CI.
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [0.9.0] - 2026-09-13
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- **Shortest Paths**: `shortest_paths_to(dst, ...)` / `Algorithms.spf_to` runs Dijkstra over the in-adjacency and returns distances *to* one destination plus a forward-oriented `PredDAG` valid for placement from any node toward it (the union of the per-source shortest-path DAGs). `fanout_edges` adds edges regardless of cost, for an origin whose first hop is a traffic split rather than a routing decision, such as a pseudo source whose demand originates evenly at every attached real source; a fan-out edge must leave a node with no incoming DAG entry, which keeps the result acyclic.
|
|
13
|
+
- **Flow Placement**: two equal-balanced modes for hop-by-hop ECMP with a load-blind forwarding table. `EQUAL_BALANCED_FIXED` takes the split set from the DAG edges with *capacity* rather than residual and keeps the single global admission scale, so a member filled since the DAG was computed drives the scale to 0 (lossless hash-ECMP admission; the existing `EQUAL_BALANCED` drops such members from the split, which is progressive/TE behaviour). `EQUAL_BALANCED_LOSSY` splits over the same set without a scale: each edge carries `min(share, residual)`, the excess is dropped, deficits propagate downstream, and the placed amount is what reaches the destination. `FlowGraph.place_with_drops` returns the dropped volume per edge; `FlowState::place_on_dag` gained an optional `drops` collector. On a 100/10 parallel pair offered 100 units, `EQUAL_BALANCED` and `EQUAL_BALANCED_FIXED` admit 20 and `EQUAL_BALANCED_LOSSY` delivers 60. `FlowPolicy` treats the new modes as equal-balanced except that the lossy mode skips the equalizing rebalance, so with pinned routes each LSP carries what fits and `placed` is the delivered total.
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
|
|
17
|
+
- **Flow Policy**: a cost-only policy (`require_capacity=false`) no longer passes the residual to SPF when an equal-balanced per-flow target is set. A residual forces capacity-aware edge selection, so such policies silently routed around saturated edges instead of following costs; they now route on cost alone, and the per-flow target still bounds how much each flow requests.
|
|
18
|
+
|
|
19
|
+
- **Flow Policy**: `get_path_bundle` now memoizes raw SPF results keyed by the exact inputs that can vary per call (src, dst, residual content, residual-awareness). EqualBalanced placement and rebalance rounds re-request bundles against residual state that repeats -- 94% of SPF calls in a measured place/rebalance cycle were exact input repeats, largely remove+place round-trips restoring identical bytes -- and those calls are now elided. Matching is exact (FlowGraph state stamp fast path, full residual `memcmp` content path), so all outputs are bit-identical; a corpus hash over max-flow, SPF, KSP and policy outputs is unchanged. Measured on place/rebalance churn: 31-70% faster; single EqualBalanced placement: 6-26% faster; Proportional mode skips the memo (it measured 0% repeat inputs) and is unaffected. `FlowGraph` gained an internal monotonic state stamp to support this; no public API change.
|
|
20
|
+
|
|
21
|
+
## [0.8.0] - 2026-08-24
|
|
22
|
+
|
|
23
|
+
### Added
|
|
24
|
+
|
|
25
|
+
- **Flow Policy**: `set_static_paths(src, dst, paths)` pins a demand to explicit path bundles (MPLS-style routing), finishing a feature that was ported but never bound and whose port bound every flow to the first bundle. One flow per usable bundle, each bound to its own bundle in supply order; bundles are validated against the graph and pruned against the policy's masks, and a bundle with no surviving `src->dst` walk is down (no reroute). `EqualBalanced` spreads over the up bundles only. With static paths the policy neither grows its flow set nor reoptimizes; `max_path_cost`/`-factor`, `min_flow_count` and `reoptimize_flows_on_each_placement` are inert.
|
|
26
|
+
- **Shortest Paths**: `PredDAG.from_edges(graph, edges)` builds a single-path `PredDAG` from a contiguous edge-id sequence (`make_path_dag` in C++) -- the missing constructor for operator-defined explicit paths. Extracting it also deduplicates the two identical path-to-DAG conversion blocks in `k_shortest_paths` (outputs unchanged).
|
|
27
|
+
|
|
28
|
+
### Fixed
|
|
29
|
+
|
|
30
|
+
- **Max-Flow**: `calc_max_flow` could return less than the true maximum, since the tier loop augments only along forward SPF DAGs and never cancels an earlier placement (the reported `min_cut` then contradicted `total_flow`). Added a residual completion phase with reverse arcs. **This is the default configuration** of `max_flow`/`batch_max_flow` (`Proportional` + `require_capacity=True` + `shortest_path=False`), so recorded expected values, regression baselines and stored results must be regenerated: `total_flow` increases where the previous value was suboptimal, and `min_cut`, `edge_flows`, `residual_capacity` and `reachable_nodes` change with it. `sensitivity_analysis`, which is built on `calc_max_flow`, is affected too.
|
|
31
|
+
- **Shortest Paths**: Zero-cost edges produced a cyclic predecessor DAG, making `EqualBalanced` placement return 0 flow and `resolve_to_paths` hang. Equal-cost predecessors are now recorded only while a node is unsettled, and `resolve_to_paths` guards against cycles in caller-supplied DAGs. Side effect on graphs containing zero-cost edges: among nodes mutually reachable at equal distance the settle order now decides which alternatives are kept, so a `PredDAG` can hold fewer equal-cost predecessors than in 0.7.2 and ECMP fan-out narrows accordingly. Graphs with strictly positive costs are unaffected (verified bit-identical).
|
|
32
|
+
- **K-Shortest Paths**: Spur enumeration materialized every equal-cost path, exponential in ECMP fan-out (a 67-node ECMP ladder needed ~4 s and ~3.5 GiB for `k=3`, and a 73-node one ~19 s; both are now instant in ~34 MB). Enumeration is now bounded by the number of candidates still acceptable; tie-breaking among equal-cost paths may differ, but path counts and costs are unchanged.
|
|
33
|
+
- **Flow Policy**: `place_demand` silently routed a second `(src, dst)` pair over the first pair's paths, and neither `place_demand` nor `rebalance_demand` checked that the supplied `FlowGraph` wrapped the policy's own graph -- with equal edge counts that selected paths on one topology and placed flow on another. Both now raise `invalid_argument` (`ValueError` from Python); use one policy per demand, or call `remove_demand()` first to retarget it.
|
|
34
|
+
- **Graph Construction**: `from_arrays` now rejects a total edge cost at or above 2^62, which overflows the int64 path arithmetic in SPF and silently corrupts results. The bound is on the sum of all edge costs -- a conservative upper bound on any path -- so it can reject a graph whose individual paths could not actually overflow.
|
|
35
|
+
- **Flow State**: `compute_min_cut` and max-flow reachability derived placed flow from `capacity - residual`, overstating it for a `FlowState` built with a custom `residual_init`. Both now use `edge_flow`.
|
|
36
|
+
- **K-Shortest Paths**: `k_shortest_paths` read `paths.back()` on an empty vector when `max_cost_factor < 1.0` put the cost ceiling below the shortest path, crashing for `k > 1` (the `k == 1` early return masked it). All `k` now return no paths for such a factor.
|
|
37
|
+
- **Python Bindings**: `batch_max_flow` rejected a valid int32 `pairs` array on Windows, because the dtype check compared buffer format strings and NumPy spells int32 as `NPY_LONG` on LLP64 but `NPY_INT` on LP64. It now compares dtype equivalence, and rejects non-contiguous `pairs` rather than reading them as if packed.
|
|
38
|
+
- **Build**: `NETGRAPH_CORE_SANITIZE` passed its flags as one quoted string, so sanitizer builds never compiled, and the test target was missing them entirely. `make sanitize-test` also no longer sets `detect_leaks=1` on macOS, where it aborts.
|
|
39
|
+
- **Build**: Coverage builds now use `-fprofile-update=atomic` on GCC; the default non-atomic counters corrupt under the threads used by `batch_max_flow` and `sensitivity_analysis`.
|
|
40
|
+
|
|
41
|
+
### Changed
|
|
42
|
+
|
|
43
|
+
- **Max-Flow**: `FlowSummary.costs`/`flows` is now a cost-weighted breakdown of the total flow rather than a list of path costs. Entries added by the new completion phase are *marginal* costs -- the augmenting path's forward edge costs minus the cost of the flow it cancels -- so an entry need not correspond to any traversable `src->dst` path. Consumers that treat these as path costs (for example a cost-distribution report) should be reviewed.
|
|
44
|
+
- **Flow Policy**: `max_path_cost` no longer implies a relative factor of 1.0. Previously `max_path_cost_factor` defaulted to 1.0 whenever either limit was set, so setting `max_path_cost` alone also rejected every path costing more than the best path found -- making the absolute limit unreachable. The two limits are now independent, and a policy with only `max_path_cost` set admits every path up to that cost. (Found while fixing undefined behavior in the same expression: with an unreachable destination, `best_path_cost_` is the `INT64_MAX` sentinel and multiplying it by the factor overflowed the cast back to `int64`.)
|
|
45
|
+
- **Shortest Paths**: Replaced nested per-node predecessor vectors with flat intrusive lists, removing the allocation churn that dominated the hot path (~3-4x faster; output unchanged).
|
|
46
|
+
- **Flow Placement**: `place_on_dag` now rebuilds its `(parent, child)` edge groups into a reused arena instead of nested per-node vectors, which was ~70% of its runtime (~2-2.4x faster; output unchanged).
|
|
47
|
+
- **Max-Flow**: Parallelized `batch_max_flow` across source/destination pairs using `std::async`; workers claim pairs from a shared counter, so a batch whose costs are unevenly distributed still parallelizes. Thread count from `NGRAPH_CORE_BATCH_THREADS` env or hardware concurrency; set it to `1` when calling from your own worker pool to avoid oversubscription.
|
|
48
|
+
- **BREAKING** **C++ API**: `FlowPolicy::set_static_paths` changed signature from `(std::vector<std::tuple<NodeId, NodeId, PredDAG, Cost>>)` to `(NodeId src, NodeId dst, std::vector<PredDAG> bundles)`, and its behavior changed: the previous implementation handed every flow the first matching bundle instead of one bundle per flow. It was never bound to Python, so only C++ callers are affected.
|
|
49
|
+
- **Flow Policy**: `place_demand` computes one SPF when seeding initial flows instead of repeating an identical one per flow; seeding cost no longer scales with `min_flow_count`.
|
|
50
|
+
- **Flow Policy**: The `EqualBalanced` rebalance recursion (`place_demand` -> `rebalance_demand` -> `place_demand`) is now an iterative loop with numerically identical results (returned leftover may differ by ~1 ulp from floating-point summation order); with many pinned bundles of heterogeneous capacity the recursion depth grew like `U * ln(imbalance/kMinFlow)`, a stack-overflow risk on worker threads.
|
|
51
|
+
- **Python Bindings**: Three input errors now raise where they previously did not, or raise a different type -- adjust `except` clauses accordingly. `Algorithms.spf` raises `TypeError` (was `ValueError`) for a wrong `residual` length, matching every other length check; `batch_max_flow` raises `ValueError` for out-of-range node ids that previously produced a zero-flow summary absorbed silently into the batch; and `Algorithms.ksp` validates `dtype` up front, where an invalid value previously went unnoticed whenever the query returned no paths.
|
|
52
|
+
- **Python Bindings**: A wrong-typed `graph`/`algorithms` argument now raises `TypeError` instead of an opaque `RuntimeError: Unable to cast ... to C++ type '?'`.
|
|
53
|
+
- **Python Typing**: Corrected `_docs.py` (pybind11 enums and classes are not `enum.Enum`/`dataclass`, `MinCut.edges` is an `int32` array, removed a `Path` class that never existed, and added the constructors for `FlowIndex`, `FlowPolicyConfig` and the three enums) and widened the type-checking re-exports from 6 names to all 16, so `Algorithms`, `FlowPolicy`, `StrictMultiDiGraph` and friends are no longer `Unknown` to type checkers despite the shipped `py.typed`. A missing stub constructor is now worse than no stub, so a test asserts every stub constructor matches its binding.
|
|
54
|
+
- **Tests**: Several assertions could never fail and now do: a self-loop assertion sat inside `except Exception: pass`, a thread-safety test ended in `assert True` while its workers called `pytest.fail()` from a thread where it cannot fail the test, and two tautologies (`assert size >= 0`) were replaced with the properties actually under test.
|
|
55
|
+
- **Docs**: Corrected README/CONTRIBUTING (required CMake is 3.23 not 3.15, `make py-test` does not exist, `make test` does not collect coverage), the `sensitivity_analysis` description in README and `backend.hpp` (it measures flow *lost* on edge removal, not gain from relaxing capacity), the zero-copy and GIL claims, and header contracts for `from_arrays`, `PredDAG`, `FlowSummary.costs`, `calc_max_flow` and `FlowPolicy`.
|
|
56
|
+
|
|
57
|
+
### Removed
|
|
58
|
+
|
|
59
|
+
- **Dead code**: `.gcovr.cfg` (never read -- gcovr's default config name has no leading dot, and `make cov` passes every flag explicitly), the unreachable `FlowGraph` include and `expect_flow_conservation()` helper in the C++ test utilities, unused test helpers/fixtures, `_USE_MATH_DEFINES` (no `M_*` macro is used), and unused `<optional>`/`<stack>`/`<unordered_map>`/`<unordered_set>`/`<cmath>` includes across `src/` and `include/`.
|
|
60
|
+
- **BREAKING** **Python Bindings**: `Algorithms.build_graph_from_arrays`. Nothing used it (no test, and not NetGraph), and the `Graph` handle it returned could not be passed to `FlowGraph` or `FlowPolicy`, so it only ever served the stateless algorithms while looking like a peer of `build_graph`. Build the graph with `StrictMultiDiGraph.from_arrays()` and pass it to `Algorithms.build_graph()`.
|
|
61
|
+
- **BREAKING** **C++ API**: The `build_graph(std::shared_ptr<const StrictMultiDiGraph>)` overload on `Algorithms` and `Backend`, which existed solely to serve that binding. It was a pure virtual, so an out-of-tree `Backend` implementation must delete its now-dangling `override` to compile. Callers wanting the handle to own the graph construct it directly: `GraphHandle{my_shared_ptr}`.
|
|
62
|
+
- **BREAKING** **Python Bindings**: The `Flow` class (bound from `FlowRecord`). It was reachable only as `_netgraph_core.Flow`, never exported from `netgraph_core`, and no binding constructed or returned one, so nothing could obtain an instance; the name also collided with the C++ alias `using Flow = double`.
|
|
63
|
+
|
|
64
|
+
## [0.7.2] - 2026-03-26
|
|
65
|
+
|
|
66
|
+
### Fixed
|
|
67
|
+
|
|
68
|
+
- Added missing `<stdexcept>` include for MSVC compatibility
|
|
69
|
+
|
|
70
|
+
## [0.7.1] - 2026-03-26
|
|
71
|
+
|
|
72
|
+
### Added
|
|
73
|
+
|
|
74
|
+
- Windows AMD64 wheel builds in release workflow
|
|
75
|
+
- Windows test coverage in CI
|
|
76
|
+
|
|
77
|
+
## [0.7.0] - 2026-03-26
|
|
78
|
+
|
|
79
|
+
### Changed
|
|
80
|
+
|
|
81
|
+
- Relicensed from BSD-3-Clause to MIT
|
|
82
|
+
- Parallelized sensitivity analysis across candidate edges using `std::async`; thread count controlled by `NGRAPH_CORE_SENSITIVITY_THREADS` env or hardware concurrency
|
|
83
|
+
- Thread-local profiling stats to remove global mutex from the hot `record()` path; public API unchanged
|
|
84
|
+
|
|
85
|
+
## [0.6.0] - 2026-02-26
|
|
86
|
+
|
|
87
|
+
### Changed
|
|
88
|
+
|
|
89
|
+
- **License**: Relicensed from GPL-3.0-or-later to BSD-3-Clause.
|
|
90
|
+
|
|
91
|
+
## [0.5.0] - 2026-02-18
|
|
92
|
+
|
|
93
|
+
### Changed
|
|
94
|
+
|
|
95
|
+
- **License**: Relicensed from AGPL-3.0 to GPL-3.0.
|
|
96
|
+
|
|
97
|
+
## [0.4.1] - 2026-02-17
|
|
98
|
+
|
|
99
|
+
### Added
|
|
100
|
+
|
|
101
|
+
- **Build**: Add Linux ARM64 (aarch64) wheel builds using native GitHub ARM runners in the release workflow.
|
|
102
|
+
|
|
103
|
+
## [0.4.0] - 2026-02-17
|
|
104
|
+
|
|
105
|
+
### Added
|
|
106
|
+
|
|
107
|
+
- **Python 3.14**: Add support for Python 3.14 in CI tests, wheel builds, and packaging.
|
|
108
|
+
- **Free Threading**: Declare free-threading compatibility (`py::mod_gil_not_used`) for Python 3.13t/3.14t builds. Build and publish free-threaded (`cp314t`) wheels.
|
|
109
|
+
|
|
110
|
+
### Changed
|
|
111
|
+
|
|
112
|
+
- **CI**: Upgrade cibuildwheel from v2 to v3.3.1 for Python 3.14 wheel builds.
|
|
113
|
+
|
|
114
|
+
### Fixed
|
|
115
|
+
|
|
116
|
+
- **Python Bindings**: Fix object leak on Python 3.14 caused by pybind11 not clearing managed dicts during deallocation. Replace `dynamic_attr` + `_graph_ref` pattern with `py::keep_alive` for FlowState, FlowGraph, and FlowPolicy.
|
|
117
|
+
|
|
118
|
+
## [0.3.6] - 2026-02-17
|
|
119
|
+
|
|
120
|
+
### Fixed
|
|
121
|
+
|
|
122
|
+
- **Python Bindings**: Fix build failure with pybind11 3.x typed tuples by adding explicit `-> py::tuple` return type on the `spf` lambda.
|
|
123
|
+
|
|
124
|
+
### Changed
|
|
125
|
+
|
|
126
|
+
- **CI**: Add CentOS Stream 9 build and test jobs.
|
|
127
|
+
|
|
128
|
+
## [0.3.5] - 2026-02-08
|
|
129
|
+
|
|
130
|
+
### Fixed
|
|
131
|
+
|
|
132
|
+
- **K-Shortest Paths**: Fix off-by-one in Yen's prefix comparison and PredDAG CSR fill order. Paths visiting nodes out of numerical order (e.g., `[0,3,2]`) previously produced malformed predecessor DAGs.
|
|
133
|
+
|
|
134
|
+
## [0.3.4] - 2026-02-02
|
|
135
|
+
|
|
136
|
+
### Changed
|
|
137
|
+
|
|
138
|
+
- **Build**: Simplified version handling.
|
|
139
|
+
|
|
140
|
+
## [0.3.3] - 2026-02-02
|
|
141
|
+
|
|
142
|
+
### Fixed
|
|
143
|
+
|
|
144
|
+
- **License**: Replaced incomplete AGPLv3 LICENSE file with complete official text from GNU.
|
|
145
|
+
|
|
146
|
+
## [0.3.2] - 2025-12-12
|
|
147
|
+
|
|
148
|
+
### Fixed
|
|
149
|
+
|
|
150
|
+
- **Profiling**: Fix ODR violation that caused empty stats when profiling was enabled. Moved `profiling_enabled()` and `ProfilingStats::instance()` definitions from inline header to `profiling.cpp` to ensure a single instance when static library is linked into the Python module.
|
|
151
|
+
|
|
152
|
+
## [0.3.1] - 2025-12-08
|
|
153
|
+
|
|
154
|
+
### Added
|
|
155
|
+
|
|
156
|
+
- **Profiling**: Runtime profiling infrastructure for C++ hot paths (`shortest_paths_core`, `place_demand`, `place_on_dag`).
|
|
157
|
+
- Enable via `NGRAPH_CORE_PROFILE=1` environment variable.
|
|
158
|
+
- Python API: `profiling_enabled()`, `profiling_dump()`, `profiling_reset()`.
|
|
159
|
+
- Minimal overhead when disabled (single static bool check per instrumented scope).
|
|
160
|
+
- ~2% overhead when enabled.
|
|
161
|
+
|
|
162
|
+
### Changed
|
|
163
|
+
|
|
164
|
+
- **Build**: Default optimizations: LTO, loop unrolling, `-fno-math-errno`. Add `make install-native` for CPU-specific builds.
|
|
165
|
+
|
|
166
|
+
## [0.3.0] - 2025-12-06
|
|
167
|
+
|
|
168
|
+
### Changed
|
|
169
|
+
|
|
170
|
+
- **BREAKING**: Minimum Python version raised to 3.11 (was 3.9)
|
|
171
|
+
|
|
172
|
+
## [0.2.3] - 2025-12-06
|
|
173
|
+
|
|
174
|
+
### Changed
|
|
175
|
+
|
|
176
|
+
- **Python bindings**: `StrictMultiDiGraph.from_arrays` now requires `ext_edge_ids` so callers always supply stable external edge identifiers.
|
|
177
|
+
- **FlowPolicy**: construction is now config-only (via `FlowPolicyConfig`), dropping the parameter-heavy constructor.
|
|
178
|
+
|
|
179
|
+
### Fixed
|
|
180
|
+
|
|
181
|
+
- **FlowGraph**: `get_flow_path` now filters only below-`kEpsilon` noise so paths are reconstructed even when per-edge allocations are smaller than `kMinFlow`.
|
|
182
|
+
|
|
183
|
+
## [0.2.2] - 2025-12-05
|
|
184
|
+
|
|
185
|
+
### Fixed
|
|
186
|
+
|
|
187
|
+
- **Flow Placement**: EqualBalanced placement now correctly returns 0 when the shortest path has no capacity with `require_capacity=False`. Previously, flow could be incorrectly reported on partial paths that didn't reach the destination.
|
|
188
|
+
|
|
189
|
+
## [0.2.1] - 2025-12-01
|
|
190
|
+
|
|
191
|
+
### Fixed
|
|
192
|
+
|
|
193
|
+
- **Shortest Paths**: In single-path mode, ties between equal-cost paths are now broken by preferring higher bottleneck capacity. Improves flow placement when multiple equal-cost paths exist with different capacities.
|
|
194
|
+
- **Flow Placement**: Use epsilon threshold in `place_on_dag()` to fix placement of very small flow fractions in large fanout networks.
|
|
195
|
+
|
|
196
|
+
### Changed
|
|
197
|
+
|
|
198
|
+
- **Build**: Use `uv` build frontend for wheel builds.
|
|
199
|
+
- **Build**: Drop 32-bit Linux (i686) wheels.
|
|
200
|
+
|
|
201
|
+
## [0.2.0] - 2025-11-25
|
|
202
|
+
|
|
203
|
+
### Added
|
|
204
|
+
|
|
205
|
+
- **Sensitivity Analysis**: Added `shortest_path` parameter to `sensitivity_analysis()`.
|
|
206
|
+
- `shortest_path=False` (default): Uses full max-flow; reports all saturated edges across all cost tiers.
|
|
207
|
+
- `shortest_path=True`: Uses single-tier shortest-path flow; reports only edges used under ECMP routing.
|
|
208
|
+
- Python type stub documentation for `Algorithms.sensitivity_analysis()`.
|
|
209
|
+
|
|
210
|
+
## [0.1.0] - 2025-11-23
|
|
211
|
+
|
|
212
|
+
### Added
|
|
213
|
+
|
|
214
|
+
- **Core Library**: Initial release of C++ implementation for graph algorithms and flow tracking.
|
|
215
|
+
- **Graph Structures**:
|
|
216
|
+
- `StrictMultiDiGraph`: Immutable directed multigraph using CSR (Compressed Sparse Row) adjacency.
|
|
217
|
+
- `FlowGraph`: Manages flow state, per-flow edge allocations, and residual capacities.
|
|
218
|
+
- **Algorithms**:
|
|
219
|
+
- Shortest paths (Dijkstra variant returning a DAG for ECMP; supports node/edge masking and residual-aware tie-breaking).
|
|
220
|
+
- K-Shortest paths (Yen's algorithm).
|
|
221
|
+
- Max-flow (Successive Shortest Path with ECMP/WCMP placement; supports capacity-aware (TE) and cost-only (IP) routing modes).
|
|
222
|
+
- Sensitivity analysis (identifies bottlenecks).
|
|
223
|
+
- **Flow Policy**:
|
|
224
|
+
- **Modeling**: Unified configuration for IP routing (cost-based ECMP) and Traffic Engineering (capacity-aware TE).
|
|
225
|
+
- **Placement**: `Proportional` (WCMP) and `EqualBalanced` (ECMP) strategies.
|
|
226
|
+
- **Lifecycle**: Manages demand placement, static/dynamic path selection, and re-optimization.
|
|
227
|
+
- **Constraints**: Enforces limits on path cost, stretch factor, and flow counts.
|
|
228
|
+
- **Python Bindings**:
|
|
229
|
+
- Python 3.9+ support via pybind11.
|
|
230
|
+
- NumPy integration using zero-copy views where applicable.
|
|
231
|
+
- Releases GIL during long-running graph algorithms.
|
|
232
|
+
- **Testing**:
|
|
233
|
+
- Python and C++ test suites.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
@AGENTS.md
|
|
@@ -27,7 +27,11 @@ add_executable(netgraph_core_tests
|
|
|
27
27
|
target_link_libraries(netgraph_core_tests PRIVATE netgraph_core GTest::gtest_main)
|
|
28
28
|
target_include_directories(netgraph_core_tests PRIVATE tests/cpp)
|
|
29
29
|
if(NETGRAPH_CORE_COVERAGE AND (CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang"))
|
|
30
|
+
# Atomic counters: see the NETGRAPH_CORE_COVERAGE block below.
|
|
30
31
|
target_compile_options(netgraph_core_tests PRIVATE -O0 -g --coverage)
|
|
32
|
+
if(CMAKE_CXX_COMPILER_ID MATCHES "GNU")
|
|
33
|
+
target_compile_options(netgraph_core_tests PRIVATE -fprofile-update=atomic)
|
|
34
|
+
endif()
|
|
31
35
|
target_link_options(netgraph_core_tests PRIVATE --coverage)
|
|
32
36
|
endif()
|
|
33
37
|
include(GoogleTest)
|
|
@@ -85,7 +89,6 @@ target_include_directories(netgraph_core PUBLIC
|
|
|
85
89
|
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
|
|
86
90
|
$<INSTALL_INTERFACE:include>
|
|
87
91
|
)
|
|
88
|
-
target_compile_definitions(netgraph_core PRIVATE _USE_MATH_DEFINES)
|
|
89
92
|
## C++ standard is set globally above.
|
|
90
93
|
target_compile_features(netgraph_core PUBLIC cxx_std_20)
|
|
91
94
|
|
|
@@ -206,8 +209,15 @@ endif()
|
|
|
206
209
|
if(NETGRAPH_CORE_COVERAGE)
|
|
207
210
|
message(STATUS "Enabling coverage instrumentation (disables optimizations)")
|
|
208
211
|
if(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang")
|
|
212
|
+
# batch_max_flow and sensitivity_analysis run worker threads, and the default
|
|
213
|
+
# non-atomic profile counters corrupt under concurrent updates (gcov then
|
|
214
|
+
# emits impossible values such as "branch 4 taken -1").
|
|
215
|
+
set(COV_FLAGS -O0 -g --coverage)
|
|
216
|
+
if(CMAKE_CXX_COMPILER_ID MATCHES "GNU")
|
|
217
|
+
list(APPEND COV_FLAGS -fprofile-update=atomic)
|
|
218
|
+
endif()
|
|
209
219
|
foreach(tgt netgraph_core _netgraph_core)
|
|
210
|
-
target_compile_options(${tgt} PRIVATE
|
|
220
|
+
target_compile_options(${tgt} PRIVATE ${COV_FLAGS})
|
|
211
221
|
target_link_options(${tgt} PRIVATE --coverage)
|
|
212
222
|
endforeach()
|
|
213
223
|
else()
|
|
@@ -218,8 +228,22 @@ endif()
|
|
|
218
228
|
if(NETGRAPH_CORE_SANITIZE)
|
|
219
229
|
message(STATUS "Enabling sanitizers (disables optimizations)")
|
|
220
230
|
if(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang")
|
|
221
|
-
|
|
222
|
-
|
|
231
|
+
# Must be a CMake list: a single quoted string reaches the compiler as one
|
|
232
|
+
# argument and clang rejects "-fsanitize=address,undefined -fno-omit-frame-pointer".
|
|
233
|
+
#
|
|
234
|
+
# -fno-sanitize-recover=undefined makes UBSan findings FATAL. Without it UBSan
|
|
235
|
+
# prints a "runtime error:" diagnostic and lets the program continue, so the test
|
|
236
|
+
# still exits 0 and ctest reports success -- undefined behaviour would be detected
|
|
237
|
+
# and then silently ignored, which is indistinguishable from not checking at all.
|
|
238
|
+
set(SAN_FLAGS -fsanitize=address,undefined -fno-omit-frame-pointer
|
|
239
|
+
-fno-sanitize-recover=undefined)
|
|
240
|
+
set(SAN_TARGETS netgraph_core _netgraph_core)
|
|
241
|
+
if(TARGET netgraph_core_tests)
|
|
242
|
+
# The test executable links the sanitized static library, so it must be
|
|
243
|
+
# built and linked with the same flags to pull in the sanitizer runtime.
|
|
244
|
+
list(APPEND SAN_TARGETS netgraph_core_tests)
|
|
245
|
+
endif()
|
|
246
|
+
foreach(tgt ${SAN_TARGETS})
|
|
223
247
|
target_compile_options(${tgt} PRIVATE ${SAN_FLAGS})
|
|
224
248
|
target_link_options(${tgt} PRIVATE ${SAN_FLAGS})
|
|
225
249
|
endforeach()
|
|
@@ -47,7 +47,7 @@ Run specific test suites:
|
|
|
47
47
|
|
|
48
48
|
```bash
|
|
49
49
|
make cpp-test # C++ tests (GoogleTest)
|
|
50
|
-
make
|
|
50
|
+
make test # Python tests (pytest)
|
|
51
51
|
```
|
|
52
52
|
|
|
53
53
|
### Code Style
|
|
@@ -65,13 +65,19 @@ make format
|
|
|
65
65
|
|
|
66
66
|
Releases are automated via GitHub Actions when a new tag is pushed.
|
|
67
67
|
|
|
68
|
-
1.
|
|
69
|
-
|
|
70
|
-
|
|
68
|
+
1. Move the pending notes into a new `## [X.Y.Z] - YYYY-MM-DD` section in `CHANGELOG.md`,
|
|
69
|
+
dated the day you tag. Mark anything that breaks consumers as **BREAKING**.
|
|
70
|
+
2. Bump `version` in `pyproject.toml` to the same `X.Y.Z`. CI does not cross-check the
|
|
71
|
+
tag against this value, so a mismatch publishes the wrong version silently.
|
|
72
|
+
3. Commit and push.
|
|
73
|
+
4. Create and push a matching tag:
|
|
71
74
|
|
|
72
75
|
```bash
|
|
73
|
-
git tag v0.
|
|
74
|
-
git push origin v0.
|
|
76
|
+
git tag v0.8.0
|
|
77
|
+
git push origin v0.8.0
|
|
75
78
|
```
|
|
76
79
|
|
|
77
|
-
|
|
80
|
+
5. The CI pipeline builds wheels and the sdist, tests them, and publishes to PyPI via
|
|
81
|
+
Trusted Publishing. `make publish` / `make publish-test` upload whatever is in your
|
|
82
|
+
local `dist/` and are for Test PyPI or emergencies only -- real releases go through
|
|
83
|
+
the tag.
|
|
@@ -52,7 +52,7 @@ help:
|
|
|
52
52
|
@echo " make check-ci - Run non-mutating lint + tests (CI entrypoint)"
|
|
53
53
|
@echo " make lint - Run only linting (non-mutating: ruff + pyright)"
|
|
54
54
|
@echo " make format - Auto-format code with ruff"
|
|
55
|
-
@echo " make test - Run tests
|
|
55
|
+
@echo " make test - Run Python tests (pytest)"
|
|
56
56
|
@echo " make qt - Run quick tests only (exclude slow/benchmark)"
|
|
57
57
|
@echo " make cpp-test - Build and run C++ tests"
|
|
58
58
|
@echo " make cov - Coverage summary + XML + single-page combined HTML"
|
|
@@ -244,21 +244,7 @@ hooks:
|
|
|
244
244
|
@$(PRECOMMIT) run --all-files || (echo "Some pre-commit hooks failed. Fix and re-run." && exit 1)
|
|
245
245
|
|
|
246
246
|
cpp-test:
|
|
247
|
-
@
|
|
248
|
-
@BUILD_DIR="build/cpp-tests"; \
|
|
249
|
-
mkdir -p "$$BUILD_DIR"; \
|
|
250
|
-
GEN_ARGS=""; \
|
|
251
|
-
if command -v ninja >/dev/null 2>&1; then GEN_ARGS="-G Ninja"; fi; \
|
|
252
|
-
cmake -S . -B "$$BUILD_DIR" -DNETGRAPH_CORE_BUILD_TESTS=ON -DCMAKE_BUILD_TYPE=Release $$GEN_ARGS; \
|
|
253
|
-
cmake --build "$$BUILD_DIR" --config Release -j; \
|
|
254
|
-
if command -v sysctl >/dev/null 2>&1; then \
|
|
255
|
-
NPROC=$$(sysctl -n hw.ncpu 2>/dev/null || echo 2); \
|
|
256
|
-
elif command -v nproc >/dev/null 2>&1; then \
|
|
257
|
-
NPROC=$$(nproc); \
|
|
258
|
-
else \
|
|
259
|
-
NPROC=2; \
|
|
260
|
-
fi; \
|
|
261
|
-
ctest --test-dir "$$BUILD_DIR" --output-on-failure -j "$$NPROC" --timeout 120
|
|
247
|
+
@PATH="$(VENV_BIN):$$PATH" bash dev/cpp-tests.sh release
|
|
262
248
|
|
|
263
249
|
cov:
|
|
264
250
|
@echo "📦 Reinstalling with C++ coverage instrumentation..."
|
|
@@ -281,6 +267,7 @@ cov:
|
|
|
281
267
|
--object-directory build/cpp-tests-cov \
|
|
282
268
|
--filter 'include/netgraph' --filter 'src' --exclude 'tests' --exclude 'bindings/.*' --exclude '.*pybind11.*' --exclude '_deps/pybind11-src/.*' \
|
|
283
269
|
--gcov-ignore-errors=all \
|
|
270
|
+
--gcov-ignore-parse-errors=negative_hits.warn_once_per_file \
|
|
284
271
|
--xml-pretty -o build/coverage/coverage-cpp.xml
|
|
285
272
|
@echo ""
|
|
286
273
|
@echo "================ Python + C++ coverage (summary) ================"
|
|
@@ -289,14 +276,7 @@ cov:
|
|
|
289
276
|
@echo "✅ Coverage ready in build/coverage/: coverage-python.xml, coverage-cpp.xml, coverage-combined.html"
|
|
290
277
|
|
|
291
278
|
sanitize-test:
|
|
292
|
-
@
|
|
293
|
-
@BUILD_DIR="build/cpp-sanitize"; \
|
|
294
|
-
mkdir -p "$$BUILD_DIR"; \
|
|
295
|
-
GEN_ARGS=""; \
|
|
296
|
-
if command -v ninja >/dev/null 2>&1; then GEN_ARGS="-G Ninja"; fi; \
|
|
297
|
-
cmake -S . -B "$$BUILD_DIR" -DNETGRAPH_CORE_BUILD_TESTS=ON -DNETGRAPH_CORE_SANITIZE=ON -DCMAKE_BUILD_TYPE=Debug $$GEN_ARGS; \
|
|
298
|
-
cmake --build "$$BUILD_DIR" --config Debug -j; \
|
|
299
|
-
ASAN_OPTIONS=detect_leaks=1 ctest --test-dir "$$BUILD_DIR" --output-on-failure || echo "⚠️ Some sanitizer tests failed"
|
|
279
|
+
@PATH="$(VENV_BIN):$$PATH" bash dev/cpp-tests.sh sanitize
|
|
300
280
|
|
|
301
281
|
# Clean + reinstall in dev mode (respects CMAKE_ARGS and MACOSX_DEPLOYMENT_TARGET)
|
|
302
282
|
# Uses active PYTHON (venv or PATH) to avoid environment mismatches
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: netgraph-core
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.9.0
|
|
4
4
|
Summary: C++ implementation of graph algorithms for network flow analysis and traffic engineering with Python bindings
|
|
5
5
|
Author: Project Contributors
|
|
6
6
|
License-Expression: MIT
|
|
@@ -20,7 +20,7 @@ Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
|
20
20
|
Classifier: Operating System :: OS Independent
|
|
21
21
|
Project-URL: Homepage, https://github.com/networmix/NetGraph-Core
|
|
22
22
|
Requires-Python: >=3.11
|
|
23
|
-
Requires-Dist: numpy>=1.
|
|
23
|
+
Requires-Dist: numpy>=1.23.2
|
|
24
24
|
Provides-Extra: dev
|
|
25
25
|
Requires-Dist: pytest>=8; extra == "dev"
|
|
26
26
|
Requires-Dist: pytest-cov; extra == "dev"
|
|
@@ -45,7 +45,7 @@ NetGraph-Core provides a specialized graph implementation for networking problem
|
|
|
45
45
|
|
|
46
46
|
- **Determinism**: Guaranteed reproducible edge ordering by (cost, src, dst).
|
|
47
47
|
- **Flow Modeling**: Native support for multi-commodity flow state, residual tracking, and ECMP/WCMP placement.
|
|
48
|
-
- **Performance**: Immutable CSR (Compressed Sparse Row) adjacency and zero-copy NumPy views.
|
|
48
|
+
- **Performance**: Immutable CSR (Compressed Sparse Row) adjacency, and zero-copy NumPy views over mutable flow state.
|
|
49
49
|
|
|
50
50
|
## Core Features
|
|
51
51
|
|
|
@@ -65,13 +65,17 @@ NetGraph-Core provides a specialized graph implementation for networking problem
|
|
|
65
65
|
- Yen's algorithm returning DAG-wrapped paths.
|
|
66
66
|
- Configurable constraints on cost factors (e.g., paths within 1.5x of optimal).
|
|
67
67
|
|
|
68
|
+
- **Explicit Paths**:
|
|
69
|
+
- `PredDAG.from_edges(graph, edges)` builds a path bundle from an operator-supplied
|
|
70
|
+
edge sequence, usable anywhere a `PredDAG` is accepted.
|
|
71
|
+
|
|
68
72
|
- **Max-Flow**:
|
|
69
|
-
- **Algorithm**: Iterative augmentation using Successive Shortest Path on residual graphs, pushing flow across full ECMP/WCMP DAGs at each step.
|
|
73
|
+
- **Algorithm**: Iterative augmentation using Successive Shortest Path on residual graphs, pushing flow across full ECMP/WCMP DAGs at each step. For `Proportional` placement with `require_capacity=True`, a final residual completion phase augments with reverse arcs so the result is a true maximum flow; the other modes are placement models and may report less.
|
|
70
74
|
- **Traffic Engineering (TE) Mode**: Routing adapts to residual capacity (progressive fill).
|
|
71
75
|
- **IP Routing Mode**: Cost-only routing (ECMP/WCMP) ignoring capacity constraints.
|
|
72
76
|
|
|
73
77
|
- **Analysis**:
|
|
74
|
-
- **Sensitivity Analysis**: Identifies
|
|
78
|
+
- **Sensitivity Analysis**: Identifies critical edges by removing each saturated edge and measuring how much total flow is *lost*. Supports `shortest_path` mode to analyze only edges used under ECMP routing (IP/IGP networks) vs. full max-flow (SDN/TE networks).
|
|
75
79
|
- **Min-Cut**: Computes minimum cuts on residual graphs.
|
|
76
80
|
|
|
77
81
|
### 3. Flow Policy Engine
|
|
@@ -83,11 +87,36 @@ Unified configuration object (`FlowPolicy`) that models diverse routing behavior
|
|
|
83
87
|
- `EqualBalanced`: **ECMP** (equal splitting) - equal distribution across next-hops and parallel edges.
|
|
84
88
|
- `Proportional`: **WCMP** (weighted splitting) - distribution proportional to residual capacity.
|
|
85
89
|
- **Lifecycle Management**: Handles demand placement, re-optimization of existing flows, and constraints (path cost, stretch factor, flow counts).
|
|
90
|
+
- **Static (Pinned) Paths**: `FlowPolicy.set_static_paths()` pins a demand to explicit path bundles (MPLS-style). One flow per usable bundle; a bundle with no path surviving the failure masks is *down* and carries nothing, since a pinned path does not reroute.
|
|
86
91
|
|
|
87
92
|
### 4. Python Integration
|
|
88
93
|
|
|
89
|
-
- **Zero-Copy**:
|
|
90
|
-
- **Concurrency**: Releases the Python GIL during
|
|
94
|
+
- **Zero-Copy**: `FlowState` and `FlowGraph` `*_view()` methods expose C++ buffers as read-only NumPy arrays that track mutation in place. `StrictMultiDiGraph.*_view()` returns copies instead, so the graph's immutability cannot be violated.
|
|
95
|
+
- **Concurrency**: Releases the Python GIL during the long-running algorithms (SPF, KSP, max-flow, placement) to enable threading. `PredDAG.resolve_to_paths` holds the GIL. `batch_max_flow` and `sensitivity_analysis` also use internal worker threads, sized by `NGRAPH_CORE_BATCH_THREADS` / `NGRAPH_CORE_SENSITIVITY_THREADS`.
|
|
96
|
+
|
|
97
|
+
## Quick Start
|
|
98
|
+
|
|
99
|
+
```python
|
|
100
|
+
import numpy as np
|
|
101
|
+
import netgraph_core as ngc
|
|
102
|
+
|
|
103
|
+
# Two parallel paths from node 0 to node 3.
|
|
104
|
+
graph = ngc.StrictMultiDiGraph.from_arrays(
|
|
105
|
+
num_nodes=4,
|
|
106
|
+
src=np.array([0, 1, 0, 2], dtype=np.int32),
|
|
107
|
+
dst=np.array([1, 3, 2, 3], dtype=np.int32),
|
|
108
|
+
capacity=np.array([10.0, 10.0, 5.0, 5.0], dtype=np.float64),
|
|
109
|
+
cost=np.array([1, 1, 1, 1], dtype=np.int64),
|
|
110
|
+
ext_edge_ids=np.arange(4, dtype=np.int64), # your own stable edge ids
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
algs = ngc.Algorithms(ngc.Backend.cpu())
|
|
114
|
+
handle = algs.build_graph(graph)
|
|
115
|
+
|
|
116
|
+
total, summary = algs.max_flow(handle, 0, 3)
|
|
117
|
+
print(total) # 15.0
|
|
118
|
+
print(summary.min_cut.edges.tolist()) # [0, 1] -- bottleneck edge ids
|
|
119
|
+
```
|
|
91
120
|
|
|
92
121
|
## Installation
|
|
93
122
|
|
|
@@ -126,16 +155,24 @@ tests/py/ # Python tests (pytest)
|
|
|
126
155
|
make dev # Setup: venv, dependencies, pre-commit hooks
|
|
127
156
|
make check # Run all tests and linting (auto-fix formatting)
|
|
128
157
|
make check-ci # Strict checks without auto-fix (for CI)
|
|
129
|
-
make test # Python tests
|
|
158
|
+
make test # Python tests
|
|
130
159
|
make cpp-test # C++ tests only
|
|
131
160
|
make cov # Combined coverage report (C++ + Python)
|
|
132
161
|
```
|
|
133
162
|
|
|
163
|
+
## Environment Variables
|
|
164
|
+
|
|
165
|
+
| Variable | Effect |
|
|
166
|
+
| --- | --- |
|
|
167
|
+
| `NGRAPH_CORE_PROFILE=1` | Enable profiling of C++ hot paths (`profiling_dump()` / `profiling_reset()`). |
|
|
168
|
+
| `NGRAPH_CORE_BATCH_THREADS` | Worker threads for `batch_max_flow` (default: hardware concurrency). Set to `1` when calling from your own worker pool. |
|
|
169
|
+
| `NGRAPH_CORE_SENSITIVITY_THREADS` | Worker threads for `sensitivity_analysis` (default: hardware concurrency). |
|
|
170
|
+
|
|
134
171
|
## Requirements
|
|
135
172
|
|
|
136
173
|
- **C++:** C++20 compiler (GCC 10+, Clang 12+, MSVC 2019+)
|
|
137
174
|
- **Python:** 3.11+
|
|
138
|
-
- **Build:** CMake 3.
|
|
175
|
+
- **Build:** CMake 3.23+, scikit-build-core
|
|
139
176
|
- **Dependencies:** pybind11, NumPy
|
|
140
177
|
|
|
141
178
|
## License
|