Skip to content

Commit dd5b98b

Browse files
committed
Release v0.2.3: Update Python bindings to require ext_edge_ids, refactor FlowPolicy construction to be config-only, and enhance FlowGraph path filtering for noise.
1 parent 4758249 commit dd5b98b

27 files changed

+399
-346
lines changed

CHANGELOG.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,17 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [0.2.3] - 2025-12-06
9+
10+
### Changed
11+
12+
- **Python bindings**: `StrictMultiDiGraph.from_arrays` now requires `ext_edge_ids` so callers always supply stable external edge identifiers.
13+
- **FlowPolicy**: construction is now config-only (via `FlowPolicyConfig`), dropping the parameter-heavy constructor.
14+
15+
### Fixed
16+
17+
- **FlowGraph**: `get_flow_path` now filters only below-`kEpsilon` noise so paths are reconstructed even when per-edge allocations are smaller than `kMinFlow`.
18+
819
## [0.2.2] - 2025-12-05
920

1021
### Fixed

Makefile

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,6 @@ install:
105105

106106
check:
107107
@PYTHON=$(PYTHON) bash dev/run-checks.sh
108-
@$(MAKE) lint
109108

110109
check-ci:
111110
@$(MAKE) lint

bindings/python/module.cpp

Lines changed: 7 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -116,27 +116,23 @@ PYBIND11_MODULE(_netgraph_core, m) {
116116
[](std::int32_t num_nodes,
117117
py::array src, py::array dst,
118118
py::array capacity, py::array cost,
119-
py::object ext_edge_ids_obj) {
119+
py::array ext_edge_ids) {
120120
// public API: src/dst are int32; pass through as int32 to core
121121
auto src_s = as_span<std::int32_t>(src, "src");
122122
auto dst_s = as_span<std::int32_t>(dst, "dst");
123123
if (src_s.size() != dst_s.size()) throw py::type_error("src and dst must have the same length");
124124
auto cap_s = as_span<double>(capacity, "capacity");
125125
// Cost dtype must be int64 to match internal Cost type
126126
auto cost_s = as_span<std::int64_t>(cost, "cost");
127-
// Optional ext_edge_ids
128-
std::span<const std::int64_t> ext_s;
129-
if (!ext_edge_ids_obj.is_none()) {
130-
py::array ext_arr = ext_edge_ids_obj.cast<py::array>();
131-
ext_s = as_span<std::int64_t>(ext_arr, "ext_edge_ids");
132-
}
127+
// ext_edge_ids required: maps internal edge indices to caller's stable IDs
128+
auto ext_s = as_span<std::int64_t>(ext_edge_ids, "ext_edge_ids");
133129
return StrictMultiDiGraph::from_arrays(num_nodes,
134130
src_s,
135131
dst_s,
136132
cap_s, cost_s, ext_s);
137133
},
138134
py::arg("num_nodes"), py::arg("src"), py::arg("dst"), py::arg("capacity"), py::arg("cost"),
139-
py::kw_only(), py::arg("ext_edge_ids") = py::none())
135+
py::arg("ext_edge_ids"))
140136
.def("num_nodes", &StrictMultiDiGraph::num_nodes)
141137
.def("num_edges", &StrictMultiDiGraph::num_edges)
142138
.def("capacity_view", [](const StrictMultiDiGraph& g){ return copy_to_numpy<double>(g.capacity_view()); })
@@ -175,13 +171,9 @@ PYBIND11_MODULE(_netgraph_core, m) {
175171
std::int32_t num_nodes,
176172
py::array src, py::array dst,
177173
py::array capacity, py::array cost,
178-
py::object ext_edge_ids_obj){
174+
py::array ext_edge_ids){
179175
// Build graph and construct shared ownership directly
180-
std::span<const std::int64_t> ext_s;
181-
if (!ext_edge_ids_obj.is_none()) {
182-
py::array ext_arr = ext_edge_ids_obj.cast<py::array>();
183-
ext_s = as_span<std::int64_t>(ext_arr, "ext_edge_ids");
184-
}
176+
auto ext_s = as_span<std::int64_t>(ext_edge_ids, "ext_edge_ids");
185177
auto sp = std::make_shared<StrictMultiDiGraph>(
186178
StrictMultiDiGraph::from_arrays(
187179
num_nodes,
@@ -193,7 +185,7 @@ PYBIND11_MODULE(_netgraph_core, m) {
193185
auto gh = algs.build_graph(std::static_pointer_cast<const StrictMultiDiGraph>(sp));
194186
// GraphHandle holds shared ownership; no additional Python-side owner needed
195187
return PyGraph{ gh, py::none(), sp->num_nodes(), sp->num_edges() };
196-
}, py::arg("num_nodes"), py::arg("src"), py::arg("dst"), py::arg("capacity"), py::arg("cost"), py::kw_only(), py::arg("ext_edge_ids") = py::none())
188+
}, py::arg("num_nodes"), py::arg("src"), py::arg("dst"), py::arg("capacity"), py::arg("cost"), py::arg("ext_edge_ids"))
197189
.def("spf", [](const Algorithms& algs, const PyGraph& pg, std::int32_t src,
198190
py::object dst, py::object selection_obj, py::object residual_obj,
199191
py::object node_mask, py::object edge_mask, bool multipath, std::string dtype){

dev/run-checks.sh

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -81,14 +81,6 @@ if [ "${SKIP_CPP_TESTS:-0}" = "1" ]; then
8181
else
8282
echo "🔧 Checking for CMake to run C++ tests..."
8383
if command -v cmake >/dev/null 2>&1; then
84-
# Quick connectivity check to GitHub for googletest download; skip if offline
85-
if command -v curl >/dev/null 2>&1; then
86-
if ! curl -Is --connect-timeout 5 https://github.com >/dev/null 2>&1; then
87-
echo "⚠️ Network appears unavailable (github.com unreachable). Skipping C++ tests."
88-
SKIP_CPP_TESTS=1
89-
fi
90-
fi
91-
9284
if [ "${SKIP_CPP_TESTS:-0}" != "1" ]; then
9385
echo "🧪 Running C++ tests (ctest)..."
9486
BUILD_DIR="build/cpp-tests"

include/netgraph/core/flow_policy.hpp

Lines changed: 0 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -92,31 +92,6 @@ class FlowPolicy {
9292
}
9393
}
9494

95-
FlowPolicy(const ExecutionContext& ctx,
96-
PathAlg path_alg,
97-
FlowPlacement flow_placement,
98-
EdgeSelection selection,
99-
bool require_capacity = true,
100-
bool multipath = true,
101-
int min_flow_count = 1,
102-
std::optional<int> max_flow_count = std::nullopt,
103-
std::optional<Cost> max_path_cost = std::nullopt,
104-
std::optional<double> max_path_cost_factor = std::nullopt,
105-
bool shortest_path = false,
106-
bool reoptimize_flows_on_each_placement = false,
107-
int max_no_progress_iterations = 100,
108-
int max_total_iterations = 10000,
109-
bool diminishing_returns_enabled = true,
110-
int diminishing_returns_window = 8,
111-
double diminishing_returns_epsilon_frac = 1e-3)
112-
: ctx_(ctx),
113-
path_alg_(path_alg), flow_placement_(flow_placement), selection_(selection),
114-
require_capacity_(require_capacity), multipath_(multipath), shortest_path_(shortest_path),
115-
min_flow_count_(min_flow_count), max_flow_count_(max_flow_count), max_path_cost_(max_path_cost),
116-
max_path_cost_factor_(max_path_cost_factor), reoptimize_flows_on_each_placement_(reoptimize_flows_on_each_placement),
117-
max_no_progress_iterations_(max_no_progress_iterations), max_total_iterations_(max_total_iterations),
118-
diminishing_returns_enabled_(diminishing_returns_enabled), diminishing_returns_window_(diminishing_returns_window),
119-
diminishing_returns_epsilon_frac_(diminishing_returns_epsilon_frac) {}
12095
~FlowPolicy() noexcept = default;
12196

12297
[[nodiscard]] int flow_count() const noexcept { return static_cast<int>(flows_.size()); }

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "scikit_build_core.build"
44

55
[project]
66
name = "netgraph-core"
7-
version = "0.2.2"
7+
version = "0.2.3"
88
description = "C++ implementation of graph algorithms for network flow analysis and traffic engineering with Python bindings"
99
readme = "README.md"
1010
requires-python = ">=3.9"

python/netgraph_core/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@
4141
MinCut as MinCut,
4242
PredDAG as PredDAG,
4343
)
44-
except Exception:
44+
except ImportError:
4545
# Safe fallback if _docs.py changes; runtime bindings above remain authoritative.
4646
pass
4747

python/netgraph_core/_docs.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22
33
This module is intentionally light; runtime implementations live in the
44
compiled extension. The goal is to provide type hints and help text.
5+
6+
TODO: replace this hand-written stub with generated `.pyi` (e.g. via
7+
pybind11-stubgen) to prevent drift from the runtime bindings.
58
"""
69

710
from __future__ import annotations
@@ -405,8 +408,7 @@ def build_graph_from_arrays(
405408
dst: "np.ndarray",
406409
capacity: "np.ndarray",
407410
cost: "np.ndarray",
408-
*,
409-
ext_edge_ids: "Optional[np.ndarray]" = None,
411+
ext_edge_ids: "np.ndarray",
410412
) -> "Graph":
411413
"""Build graph directly from arrays (graph is owned by the handle)."""
412414
...

python/netgraph_core/_version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@
22

33
__all__ = ["__version__"]
44

5-
__version__ = "0.2.2"
5+
__version__ = "0.2.3"

src/cpu_backend.cpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ class CpuBackend final : public Backend {
2424
const GraphHandle& gh, NodeId src, const SpfOptions& opts) override {
2525
const StrictMultiDiGraph& g = *gh.graph;
2626
// Validate mask lengths strictly; mismatches are user errors.
27+
// NOTE: Keep this as the single public boundary check; deeper layers
28+
// (shortest_paths) should rely on this to avoid redundant validation.
2729
if (!opts.node_mask.empty() && opts.node_mask.size() != static_cast<std::size_t>(g.num_nodes())) {
2830
throw std::invalid_argument("CpuBackend::spf: node_mask length mismatch");
2931
}

0 commit comments

Comments
 (0)