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.
Files changed (103) hide show
  1. {netgraph_core-0.7.2 → netgraph_core-0.9.0}/.github/workflows/release.yml +20 -1
  2. {netgraph_core-0.7.2 → netgraph_core-0.9.0}/.gitignore +3 -0
  3. netgraph_core-0.9.0/.superset/config.json +4 -0
  4. netgraph_core-0.9.0/.superset/workspace.sh +29 -0
  5. netgraph_core-0.9.0/AGENTS.md +15 -0
  6. netgraph_core-0.9.0/CHANGELOG.md +233 -0
  7. netgraph_core-0.9.0/CLAUDE.md +1 -0
  8. {netgraph_core-0.7.2 → netgraph_core-0.9.0}/CMakeLists.txt +28 -4
  9. {netgraph_core-0.7.2 → netgraph_core-0.9.0}/CONTRIBUTING.md +13 -7
  10. {netgraph_core-0.7.2 → netgraph_core-0.9.0}/Makefile +4 -24
  11. {netgraph_core-0.7.2 → netgraph_core-0.9.0}/PKG-INFO +46 -9
  12. {netgraph_core-0.7.2 → netgraph_core-0.9.0}/README.md +44 -7
  13. {netgraph_core-0.7.2 → netgraph_core-0.9.0}/bindings/python/module.cpp +121 -31
  14. netgraph_core-0.9.0/dev/cpp-tests.sh +27 -0
  15. {netgraph_core-0.7.2 → netgraph_core-0.9.0}/include/netgraph/core/algorithms.hpp +5 -4
  16. {netgraph_core-0.7.2 → netgraph_core-0.9.0}/include/netgraph/core/backend.hpp +12 -14
  17. {netgraph_core-0.7.2 → netgraph_core-0.9.0}/include/netgraph/core/flow_graph.hpp +34 -2
  18. {netgraph_core-0.7.2 → netgraph_core-0.9.0}/include/netgraph/core/flow_policy.hpp +101 -7
  19. {netgraph_core-0.7.2 → netgraph_core-0.9.0}/include/netgraph/core/flow_state.hpp +17 -2
  20. {netgraph_core-0.7.2 → netgraph_core-0.9.0}/include/netgraph/core/max_flow.hpp +19 -3
  21. {netgraph_core-0.7.2 → netgraph_core-0.9.0}/include/netgraph/core/options.hpp +11 -0
  22. {netgraph_core-0.7.2 → netgraph_core-0.9.0}/include/netgraph/core/profiling.hpp +3 -5
  23. netgraph_core-0.9.0/include/netgraph/core/shortest_paths.hpp +131 -0
  24. {netgraph_core-0.7.2 → netgraph_core-0.9.0}/include/netgraph/core/strict_multidigraph.hpp +12 -0
  25. {netgraph_core-0.7.2 → netgraph_core-0.9.0}/include/netgraph/core/types.hpp +21 -2
  26. {netgraph_core-0.7.2 → netgraph_core-0.9.0}/pyproject.toml +3 -3
  27. {netgraph_core-0.7.2 → netgraph_core-0.9.0}/python/netgraph_core/__init__.py +27 -15
  28. {netgraph_core-0.7.2 → netgraph_core-0.9.0}/python/netgraph_core/_docs.py +368 -59
  29. {netgraph_core-0.7.2 → netgraph_core-0.9.0}/src/cpu_backend.cpp +18 -6
  30. {netgraph_core-0.7.2 → netgraph_core-0.9.0}/src/flow_graph.cpp +52 -3
  31. netgraph_core-0.9.0/src/flow_policy.cpp +841 -0
  32. {netgraph_core-0.7.2 → netgraph_core-0.9.0}/src/flow_state.cpp +188 -47
  33. {netgraph_core-0.7.2 → netgraph_core-0.9.0}/src/k_shortest_paths.cpp +51 -55
  34. {netgraph_core-0.7.2 → netgraph_core-0.9.0}/src/max_flow.cpp +133 -21
  35. {netgraph_core-0.7.2 → netgraph_core-0.9.0}/src/profiling.cpp +1 -0
  36. netgraph_core-0.9.0/src/shortest_paths.cpp +731 -0
  37. {netgraph_core-0.7.2 → netgraph_core-0.9.0}/src/strict_multidigraph.cpp +15 -1
  38. {netgraph_core-0.7.2 → netgraph_core-0.9.0}/tests/cpp/flow_graph_tests.cpp +46 -0
  39. {netgraph_core-0.7.2 → netgraph_core-0.9.0}/tests/cpp/flow_policy_tests.cpp +156 -0
  40. {netgraph_core-0.7.2 → netgraph_core-0.9.0}/tests/cpp/flow_state_tests.cpp +122 -0
  41. {netgraph_core-0.7.2 → netgraph_core-0.9.0}/tests/cpp/k_shortest_paths_tests.cpp +24 -0
  42. {netgraph_core-0.7.2 → netgraph_core-0.9.0}/tests/cpp/masking_tests.cpp +1 -1
  43. {netgraph_core-0.7.2 → netgraph_core-0.9.0}/tests/cpp/max_flow_tests.cpp +80 -0
  44. {netgraph_core-0.7.2 → netgraph_core-0.9.0}/tests/cpp/shortest_paths_tests.cpp +153 -0
  45. {netgraph_core-0.7.2 → netgraph_core-0.9.0}/tests/cpp/test_utils.hpp +0 -33
  46. {netgraph_core-0.7.2 → netgraph_core-0.9.0}/tests/py/conftest.py +124 -13
  47. netgraph_core-0.9.0/tests/py/test_equal_balanced_modes.py +237 -0
  48. {netgraph_core-0.7.2 → netgraph_core-0.9.0}/tests/py/test_flow_policy.py +0 -5
  49. netgraph_core-0.9.0/tests/py/test_flow_policy_spf_memo.py +158 -0
  50. {netgraph_core-0.7.2 → netgraph_core-0.9.0}/tests/py/test_flow_policy_validation.py +0 -15
  51. {netgraph_core-0.7.2 → netgraph_core-0.9.0}/tests/py/test_graph_from_arrays.py +6 -7
  52. {netgraph_core-0.7.2 → netgraph_core-0.9.0}/tests/py/test_max_flow.py +6 -3
  53. {netgraph_core-0.7.2 → netgraph_core-0.9.0}/tests/py/test_max_flow_summary.py +1 -1
  54. netgraph_core-0.9.0/tests/py/test_maxflow_certificate.py +197 -0
  55. {netgraph_core-0.7.2 → netgraph_core-0.9.0}/tests/py/test_policy_vs_maxflow_equivalence.py +0 -19
  56. netgraph_core-0.9.0/tests/py/test_review_regressions.py +590 -0
  57. netgraph_core-0.9.0/tests/py/test_spf_to.py +114 -0
  58. netgraph_core-0.9.0/tests/py/test_static_paths.py +392 -0
  59. {netgraph_core-0.7.2 → netgraph_core-0.9.0}/tests/py/test_thread_safety.py +17 -8
  60. netgraph_core-0.7.2/.gcovr.cfg +0 -20
  61. netgraph_core-0.7.2/CHANGELOG.md +0 -177
  62. netgraph_core-0.7.2/include/netgraph/core/shortest_paths.hpp +0 -64
  63. netgraph_core-0.7.2/src/flow_policy.cpp +0 -419
  64. netgraph_core-0.7.2/src/shortest_paths.cpp +0 -411
  65. {netgraph_core-0.7.2 → netgraph_core-0.9.0}/.coveragerc +0 -0
  66. {netgraph_core-0.7.2 → netgraph_core-0.9.0}/.github/workflows/tests.yml +0 -0
  67. {netgraph_core-0.7.2 → netgraph_core-0.9.0}/.pre-commit-config.yaml +0 -0
  68. {netgraph_core-0.7.2 → netgraph_core-0.9.0}/LICENSE +0 -0
  69. {netgraph_core-0.7.2 → netgraph_core-0.9.0}/dev/benchmark_profiling_overhead.py +0 -0
  70. {netgraph_core-0.7.2 → netgraph_core-0.9.0}/dev/coverage_summary.py +0 -0
  71. {netgraph_core-0.7.2 → netgraph_core-0.9.0}/dev/run-checks.sh +0 -0
  72. {netgraph_core-0.7.2 → netgraph_core-0.9.0}/include/netgraph/core/constants.hpp +0 -0
  73. {netgraph_core-0.7.2 → netgraph_core-0.9.0}/include/netgraph/core/flow.hpp +0 -0
  74. {netgraph_core-0.7.2 → netgraph_core-0.9.0}/include/netgraph/core/k_shortest_paths.hpp +0 -0
  75. {netgraph_core-0.7.2 → netgraph_core-0.9.0}/python/netgraph_core/py.typed +0 -0
  76. {netgraph_core-0.7.2 → netgraph_core-0.9.0}/tests/cpp/smoke_test.cpp +0 -0
  77. {netgraph_core-0.7.2 → netgraph_core-0.9.0}/tests/cpp/strict_multidigraph_tests.cpp +0 -0
  78. {netgraph_core-0.7.2 → netgraph_core-0.9.0}/tests/py/test_api_validation.py +0 -0
  79. {netgraph_core-0.7.2 → netgraph_core-0.9.0}/tests/py/test_batch_max_flow_consistency.py +0 -0
  80. {netgraph_core-0.7.2 → netgraph_core-0.9.0}/tests/py/test_batch_max_flow_masks.py +0 -0
  81. {netgraph_core-0.7.2 → netgraph_core-0.9.0}/tests/py/test_cross_validation.py +0 -0
  82. {netgraph_core-0.7.2 → netgraph_core-0.9.0}/tests/py/test_edge_cases.py +0 -0
  83. {netgraph_core-0.7.2 → netgraph_core-0.9.0}/tests/py/test_ext_edge_ids.py +0 -0
  84. {netgraph_core-0.7.2 → netgraph_core-0.9.0}/tests/py/test_flow_policy_configurations.py +0 -0
  85. {netgraph_core-0.7.2 → netgraph_core-0.9.0}/tests/py/test_flow_policy_edge_cases.py +0 -0
  86. {netgraph_core-0.7.2 → netgraph_core-0.9.0}/tests/py/test_flow_policy_lifecycle.py +0 -0
  87. {netgraph_core-0.7.2 → netgraph_core-0.9.0}/tests/py/test_flow_policy_masks.py +0 -0
  88. {netgraph_core-0.7.2 → netgraph_core-0.9.0}/tests/py/test_flow_policy_path_distribution.py +0 -0
  89. {netgraph_core-0.7.2 → netgraph_core-0.9.0}/tests/py/test_graph_structure.py +0 -0
  90. {netgraph_core-0.7.2 → netgraph_core-0.9.0}/tests/py/test_imports.py +0 -0
  91. {netgraph_core-0.7.2 → netgraph_core-0.9.0}/tests/py/test_ksp.py +0 -0
  92. {netgraph_core-0.7.2 → netgraph_core-0.9.0}/tests/py/test_ksp_variants.py +0 -0
  93. {netgraph_core-0.7.2 → netgraph_core-0.9.0}/tests/py/test_lifetime_safety.py +0 -0
  94. {netgraph_core-0.7.2 → netgraph_core-0.9.0}/tests/py/test_mask_semantics.py +0 -0
  95. {netgraph_core-0.7.2 → netgraph_core-0.9.0}/tests/py/test_mask_validation.py +0 -0
  96. {netgraph_core-0.7.2 → netgraph_core-0.9.0}/tests/py/test_masking_comprehensive.py +0 -0
  97. {netgraph_core-0.7.2 → netgraph_core-0.9.0}/tests/py/test_max_flow_cost_distribution.py +0 -0
  98. {netgraph_core-0.7.2 → netgraph_core-0.9.0}/tests/py/test_paths_resolve.py +0 -0
  99. {netgraph_core-0.7.2 → netgraph_core-0.9.0}/tests/py/test_profiling.py +0 -0
  100. {netgraph_core-0.7.2 → netgraph_core-0.9.0}/tests/py/test_sensitivity.py +0 -0
  101. {netgraph_core-0.7.2 → netgraph_core-0.9.0}/tests/py/test_spf_basic.py +0 -0
  102. {netgraph_core-0.7.2 → netgraph_core-0.9.0}/tests/py/test_spf_edge_select.py +0 -0
  103. {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:
@@ -221,3 +221,6 @@ marimo/_static/
221
221
  marimo/_lsp/
222
222
  __marimo__/
223
223
  .pgo-profile/
224
+
225
+ # Personal Superset lifecycle overrides
226
+ .superset/config.local.json
@@ -0,0 +1,4 @@
1
+ {
2
+ "setup": ["bash .superset/workspace.sh setup"],
3
+ "run": ["bash .superset/workspace.sh check"]
4
+ }
@@ -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 -O0 -g --coverage)
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
- set(SAN_FLAGS "-fsanitize=address,undefined -fno-omit-frame-pointer")
222
- foreach(tgt netgraph_core _netgraph_core)
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 py-test # Python tests (pytest)
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. Bump version in `pyproject.toml`.
69
- 2. Commit and push.
70
- 3. Create and push a tag:
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.1.0
74
- git push origin v0.1.0
76
+ git tag v0.8.0
77
+ git push origin v0.8.0
75
78
  ```
76
79
 
77
- 4. The CI pipeline will build wheels, sdist, and publish to PyPI.
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 with coverage"
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
- @echo "🧪 Building and running C++ tests..."
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
- @echo "🧪 Building and running C++ tests with sanitizers..."
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.7.2
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.22
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 bottleneck edges where capacity relaxation increases total flow. Supports `shortest_path` mode to analyze only edges used under ECMP routing (IP/IGP networks) vs. full max-flow (SDN/TE networks).
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**: Exposes C++ internal buffers to Python as read-only NumPy arrays (float64/int64).
90
- - **Concurrency**: Releases the Python GIL during graph algorithms to enable threading.
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 with coverage
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.15+, scikit-build-core
175
+ - **Build:** CMake 3.23+, scikit-build-core
139
176
  - **Dependencies:** pybind11, NumPy
140
177
 
141
178
  ## License