Skip to content

Commit 1185dac

Browse files
committed
added ksp
1 parent 53e230a commit 1185dac

22 files changed

Lines changed: 1244 additions & 450 deletions

bindings/python/module.cpp

Lines changed: 58 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,11 @@ PYBIND11_MODULE(_netgraph_core, m) {
3131
py::enum_<EdgeSelect>(m, "EdgeSelect")
3232
.value("ALL_MIN_COST", EdgeSelect::AllMinCost)
3333
.value("SINGLE_MIN_COST", EdgeSelect::SingleMinCost)
34-
.value("ALL_MIN_COST_WITH_CAP_REMAINING", EdgeSelect::AllMinCostWithCapRemaining);
34+
.value("ALL_MIN_COST_WITH_CAP_REMAINING", EdgeSelect::AllMinCostWithCapRemaining)
35+
.value("ALL_ANY_COST_WITH_CAP_REMAINING", EdgeSelect::AllAnyCostWithCapRemaining)
36+
.value("SINGLE_MIN_COST_WITH_CAP_REMAINING", EdgeSelect::SingleMinCostWithCapRemaining)
37+
.value("SINGLE_MIN_COST_WITH_CAP_REMAINING_LOAD_FACTORED", EdgeSelect::SingleMinCostWithCapRemainingLoadFactored)
38+
.value("USER_DEFINED", EdgeSelect::UserDefined);
3539

3640
py::enum_<FlowPlacement>(m, "FlowPlacement")
3741
.value("PROPORTIONAL", FlowPlacement::Proportional)
@@ -176,19 +180,6 @@ PYBIND11_MODULE(_netgraph_core, m) {
176180
return py::make_tuple(std::move(dist_arr), res.second);
177181
}, py::arg("g"), py::arg("src"), py::arg("dst") = py::none(), py::kw_only(), py::arg("edge_select") = EdgeSelect::AllMinCost, py::arg("multipath") = true, py::arg("node_mask") = py::none(), py::arg("edge_mask") = py::none(), py::arg("eps") = 1e-10);
178182

179-
py::class_<Path>(m, "Path")
180-
.def_property_readonly("nodes", [](const Path& p){
181-
py::array_t<std::int32_t> arr(p.nodes.size());
182-
std::memcpy(arr.mutable_data(), p.nodes.data(), p.nodes.size()*sizeof(std::int32_t));
183-
return arr;
184-
})
185-
.def_property_readonly("edges", [](const Path& p){
186-
py::array_t<std::int32_t> arr(p.edges.size());
187-
std::memcpy(arr.mutable_data(), p.edges.data(), p.edges.size()*sizeof(std::int32_t));
188-
return arr;
189-
})
190-
.def_readonly("cost", &Path::cost);
191-
192183
m.def("ksp",
193184
[](const StrictMultiDiGraph& g, std::int32_t src, std::int32_t dst,
194185
int k, py::object max_cost_factor, bool unique, py::object node_mask, py::object edge_mask, double eps) {
@@ -209,9 +200,17 @@ PYBIND11_MODULE(_netgraph_core, m) {
209200
if (static_cast<std::size_t>(buf.shape[0]) != static_cast<std::size_t>(g.num_edges())) throw py::type_error("edge_mask length must equal num_edges");
210201
}
211202
py::gil_scoped_release release;
212-
auto paths = k_shortest_paths(g, src, dst, k, mcf, unique, eps);
203+
auto items = k_shortest_paths(g, src, dst, k, mcf, unique, eps);
213204
py::gil_scoped_acquire acquire;
214-
return paths;
205+
// Convert to list of (dist ndarray, PredDAG)
206+
py::list out;
207+
for (auto& pr : items) {
208+
auto& dist = pr.first;
209+
py::array_t<double> dist_arr(dist.size());
210+
std::memcpy(dist_arr.mutable_data(), dist.data(), dist.size()*sizeof(double));
211+
out.append(py::make_tuple(std::move(dist_arr), pr.second));
212+
}
213+
return out;
215214
}, py::arg("g"), py::arg("src"), py::arg("dst"), py::kw_only(), py::arg("k"), py::arg("max_cost_factor") = py::none(), py::arg("unique") = true, py::arg("node_mask") = py::none(), py::arg("edge_mask") = py::none(), py::arg("eps") = 1e-10);
216215

217216
py::class_<MinCut>(m, "MinCut")
@@ -256,6 +255,49 @@ PYBIND11_MODULE(_netgraph_core, m) {
256255
return res;
257256
}, py::arg("g"), py::arg("src"), py::arg("dst"), py::kw_only(), py::arg("flow_placement") = FlowPlacement::Proportional, py::arg("shortest_path") = false, py::arg("eps") = 1e-10, py::arg("with_edge_flows") = false, py::arg("node_mask") = py::none(), py::arg("edge_mask") = py::none());
258257

258+
// Residual-aware SPF (exposes selection variants requiring capacity/flow)
259+
m.def("spf_residual",
260+
[](const StrictMultiDiGraph& g, std::int32_t src, py::object dst,
261+
EdgeSelect policy, bool multipath, double eps,
262+
py::array residual, py::object node_mask, py::object edge_mask) {
263+
std::optional<std::int32_t> dst_opt;
264+
if (!dst.is_none()) dst_opt = py::cast<std::int32_t>(dst);
265+
// residual must be 1-D float64 of length num_edges
266+
if (!(py::isinstance<py::array_t<double>>(residual))) {
267+
throw py::type_error("residual must be a numpy float64 array");
268+
}
269+
if (!(residual.flags() & py::array::c_style)) {
270+
throw py::type_error("residual must be C-contiguous (np.ascontiguousarray)");
271+
}
272+
auto rbuf = residual.request();
273+
if (rbuf.ndim != 1 || static_cast<std::size_t>(rbuf.shape[0]) != static_cast<std::size_t>(g.num_edges())) {
274+
throw py::type_error("residual length must equal num_edges");
275+
}
276+
std::vector<double> residual_vec(static_cast<std::size_t>(rbuf.shape[0]));
277+
std::memcpy(residual_vec.data(), rbuf.ptr, residual_vec.size()*sizeof(double));
278+
const bool* node_ptr = nullptr; const bool* edge_ptr = nullptr; py::array node_arr, edge_arr;
279+
if (!node_mask.is_none()) {
280+
node_arr = py::cast<py::array>(node_mask);
281+
auto b = node_arr.request();
282+
if (b.ndim != 1 || b.format != py::format_descriptor<bool>::format() || static_cast<std::size_t>(b.shape[0]) != static_cast<std::size_t>(g.num_nodes())) {
283+
throw py::type_error("node_mask must be 1-D bool and length=num_nodes");
284+
}
285+
node_ptr = static_cast<const bool*>(b.ptr);
286+
}
287+
if (!edge_mask.is_none()) {
288+
edge_arr = py::cast<py::array>(edge_mask);
289+
auto b = edge_arr.request();
290+
if (b.ndim != 1 || b.format != py::format_descriptor<bool>::format() || static_cast<std::size_t>(b.shape[0]) != static_cast<std::size_t>(g.num_edges())) {
291+
throw py::type_error("edge_mask must be 1-D bool and length=num_edges");
292+
}
293+
edge_ptr = static_cast<const bool*>(b.ptr);
294+
}
295+
py::gil_scoped_release rel;
296+
auto res = shortest_paths_with_residual(g, src, dst_opt, policy, multipath, eps, residual_vec, node_ptr, edge_ptr);
297+
py::gil_scoped_acquire acq;
298+
return res;
299+
}, py::arg("g"), py::arg("src"), py::arg("dst") = py::none(), py::arg("edge_select") = EdgeSelect::AllMinCostWithCapRemaining, py::arg("multipath") = true, py::arg("eps") = 1e-12, py::arg("residual"), py::arg("node_mask") = py::none(), py::arg("edge_mask") = py::none());
300+
259301
m.def("max_flow",
260302
[](const StrictMultiDiGraph& g, std::int32_t src, std::int32_t dst,
261303
FlowPlacement placement, bool shortest_path, double eps, bool with_edge_flows,

dev/run-checks.sh

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,9 @@ echo ""
108108

109109
# Run Python tests (with coverage if available)
110110
echo "🧪 Running Python tests..."
111+
# Ensure the extension is built for the active Python and importable
112+
echo "🔧 Installing project in editable mode for current Python..."
113+
"$PYTHON" -m pip install -e . >/dev/null
111114
if "$PYTHON" -c "import pytest_cov" >/dev/null 2>&1; then
112115
"$PYTHON" -m pytest --cov=netgraph_core --cov-report=term-missing
113116
else

include/netgraph/core/k_shortest_paths.hpp

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,17 @@
44
#include <vector>
55

66
#include "netgraph/core/strict_multidigraph.hpp"
7+
#include "netgraph/core/shortest_paths.hpp"
78
#include "netgraph/core/types.hpp"
89

910
namespace netgraph::core {
1011

11-
struct Path {
12-
std::vector<NodeId> nodes;
13-
std::vector<EdgeId> edges;
14-
double cost {0.0};
15-
};
16-
17-
std::vector<Path> k_shortest_paths(
12+
// Compute up to k shortest paths from s to t (Yen-like) and return
13+
// SPF-compatible outputs per path: (distances, predecessor DAG).
14+
// Distances are float64[N], PredDAG encodes one concrete path with single parent
15+
// per node along the path; other nodes have no parents and dist=inf.
16+
// Deterministic tie-breaking across equal-cost edges uses compacted edge order.
17+
std::vector<std::pair<std::vector<double>, PredDAG>> k_shortest_paths(
1818
const StrictMultiDiGraph& g, NodeId s, NodeId t,
1919
int k, std::optional<double> max_cost_factor,
2020
bool unique, double eps);

include/netgraph/core/shortest_paths.hpp

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,4 +31,16 @@ shortest_paths(const StrictMultiDiGraph& g, NodeId src,
3131
const bool* node_mask = nullptr,
3232
const bool* edge_mask = nullptr);
3333

34+
// Residual-aware shortest paths: like shortest_paths but considers per-edge
35+
// residual capacities (residual[e] = remaining capacity). Some EdgeSelect
36+
// policies require residual/capacity/load (e.g., load-factored). When residual
37+
// is provided, edges with residual < MIN_CAP are excluded.
38+
std::pair<std::vector<double>, PredDAG>
39+
shortest_paths_with_residual(const StrictMultiDiGraph& g, NodeId src,
40+
std::optional<NodeId> dst,
41+
EdgeSelect policy, bool multipath, double eps,
42+
const std::vector<double>& residual,
43+
const bool* node_mask = nullptr,
44+
const bool* edge_mask = nullptr);
45+
3446
} // namespace netgraph::core

include/netgraph/core/types.hpp

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,11 @@ using Cap = double;
1313
enum class EdgeSelect {
1414
AllMinCost = 1,
1515
SingleMinCost = 2,
16-
AllMinCostWithCapRemaining = 3
16+
AllMinCostWithCapRemaining = 3,
17+
AllAnyCostWithCapRemaining = 4,
18+
SingleMinCostWithCapRemaining = 5,
19+
SingleMinCostWithCapRemainingLoadFactored = 6,
20+
UserDefined = 99
1721
};
1822

1923
enum class FlowPlacement {

pyproject.toml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,14 @@ version = "0.1.0"
1414
description = "Native C++ engine for NetGraph with Python bindings (CPU today, GPU-ready)"
1515
readme = "README.md"
1616
requires-python = ">=3.9"
17-
license = { file = "LICENSE" }
17+
license = "AGPL-3.0-or-later"
1818
authors = [{ name = "NetGraph Contributors" }]
1919
classifiers = [
2020
"Programming Language :: Python :: 3",
2121
"Programming Language :: Python :: 3 :: Only",
2222
"Programming Language :: C++",
2323
"Programming Language :: Python :: Implementation :: CPython",
2424
"Operating System :: OS Independent",
25-
"License :: OSI Approved :: GNU Affero General Public License v3 or later (AGPLv3+)",
2625
]
2726
dependencies = ["numpy>=1.22"]
2827

python/netgraph_core/__init__.py

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,30 +7,48 @@
77
FlowPlacement,
88
FlowSummary,
99
MinCut,
10-
Path,
1110
PredDAG,
1211
StrictMultiDiGraph,
1312
batch_max_flow,
1413
calc_max_flow,
1514
ksp,
1615
max_flow,
1716
spf,
17+
spf_residual,
1818
)
1919

2020
from ._version import __version__
2121

22+
# Provide richer type information for editors/type-checkers without affecting runtime.
23+
try: # pragma: no cover - typing-only import
24+
from typing import TYPE_CHECKING as _TYPE_CHECKING
25+
26+
if _TYPE_CHECKING: # noqa: SIM108
27+
from ._docs import ( # noqa: I001
28+
CostBucket as CostBucket,
29+
CostDistribution as CostDistribution,
30+
EdgeSelect as EdgeSelect,
31+
FlowPlacement as FlowPlacement,
32+
FlowSummary as FlowSummary,
33+
MinCut as MinCut,
34+
PredDAG as PredDAG,
35+
)
36+
except Exception:
37+
# Safe fallback if _docs.py changes; runtime bindings above remain authoritative.
38+
pass
39+
2240
__all__ = [
2341
"__version__",
2442
"StrictMultiDiGraph",
2543
"spf",
2644
"ksp",
2745
"calc_max_flow",
2846
"max_flow",
47+
"spf_residual",
2948
"batch_max_flow",
3049
"EdgeSelect",
3150
"FlowPlacement",
3251
"PredDAG",
33-
"Path",
3452
"MinCut",
3553
"CostBucket",
3654
"CostDistribution",

python/netgraph_core/_docs.py

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,17 +16,33 @@
1616

1717

1818
class EdgeSelect(Enum):
19+
"""Edge selection policies for shortest-path traversal.
20+
21+
Mirrors netgraph.core.types.EdgeSelect (C++ enum).
22+
"""
23+
1924
ALL_MIN_COST = 1
2025
SINGLE_MIN_COST = 2
2126
ALL_MIN_COST_WITH_CAP_REMAINING = 3
27+
ALL_ANY_COST_WITH_CAP_REMAINING = 4
28+
SINGLE_MIN_COST_WITH_CAP_REMAINING = 5
29+
SINGLE_MIN_COST_WITH_CAP_REMAINING_LOAD_FACTORED = 6
30+
USER_DEFINED = 99
2231

2332

2433
class FlowPlacement(Enum):
34+
"""How to place flow across equal-cost predecessors during augmentation."""
35+
2536
PROPORTIONAL = 1
2637
EQUAL_BALANCED = 2
2738

2839

2940
class PredDAG:
41+
"""Compact predecessor DAG representation.
42+
43+
Arrays are int32; offsets has length N+1.
44+
"""
45+
3046
parent_offsets: np.ndarray
3147
parents: np.ndarray
3248
via_edges: np.ndarray
@@ -57,6 +73,26 @@ def spf(
5773
...
5874

5975

76+
def spf_residual(
77+
g: "StrictMultiDiGraph",
78+
src: int,
79+
dst: Optional[int] = None,
80+
*,
81+
edge_select: EdgeSelect = EdgeSelect.ALL_MIN_COST_WITH_CAP_REMAINING,
82+
multipath: bool = True,
83+
eps: float = 1e-12,
84+
residual: np.ndarray | list[float] = ...,
85+
node_mask: Optional[np.ndarray] = None,
86+
edge_mask: Optional[np.ndarray] = None,
87+
):
88+
"""Residual-aware shortest paths wrapper.
89+
90+
Uses per-edge residual capacities to filter/select edges.
91+
Returns: (dist: float64[N], pred: PredDAG)
92+
"""
93+
...
94+
95+
6096
def ksp(
6197
g: "StrictMultiDiGraph",
6298
src: int,
@@ -83,3 +119,42 @@ def calc_max_flow(
83119
node_mask: Optional[np.ndarray] = None,
84120
edge_mask: Optional[np.ndarray] = None,
85121
): ...
122+
123+
124+
@dataclass(frozen=True)
125+
class CostBucket:
126+
cost: float
127+
share: float
128+
129+
130+
@dataclass(frozen=True)
131+
class CostDistribution:
132+
buckets: list[CostBucket]
133+
134+
135+
@dataclass(frozen=True)
136+
class MinCut:
137+
edges: list[int]
138+
139+
140+
@dataclass(frozen=True)
141+
class FlowSummary:
142+
total_flow: float
143+
min_cut: MinCut
144+
cost_distribution: CostDistribution
145+
edge_flows: list[float]
146+
147+
148+
def batch_max_flow(
149+
g: "StrictMultiDiGraph",
150+
pairs: np.ndarray,
151+
*,
152+
flow_placement: FlowPlacement = FlowPlacement.PROPORTIONAL,
153+
shortest_path: bool = False,
154+
eps: float = 1e-10,
155+
with_edge_flows: bool = False,
156+
threads: Optional[int] = None,
157+
seed: Optional[int] = None,
158+
node_masks: Optional[list[np.ndarray]] = None,
159+
edge_masks: Optional[list[np.ndarray]] = None,
160+
) -> list[FlowSummary]: ...

0 commit comments

Comments
 (0)