Skip to content

Commit 65451d5

Browse files
committed
adding flow policy
1 parent 1185dac commit 65451d5

40 files changed

Lines changed: 2990 additions & 1206 deletions

CMakeLists.txt

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ if(NETGRAPH_CORE_BUILD_TESTS)
1111
)
1212
FetchContent_MakeAvailable(googletest)
1313
enable_testing()
14-
add_executable(netgraph_core_tests tests/cpp/smoke_test.cpp)
14+
add_executable(netgraph_core_tests tests/cpp/smoke_test.cpp tests/cpp/flow_policy_tests.cpp)
1515
target_link_libraries(netgraph_core_tests PRIVATE netgraph_core GTest::gtest_main)
1616
if(NETGRAPH_CORE_COVERAGE AND (CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang"))
1717
target_compile_options(netgraph_core_tests PRIVATE -O0 -g --coverage)
@@ -27,14 +27,14 @@ set(CMAKE_CXX_EXTENSIONS OFF)
2727
set(CMAKE_POSITION_INDEPENDENT_CODE ON)
2828

2929
# Dependencies
30-
find_package(pybind11 CONFIG QUIET)
30+
find_package(pybind11 3 CONFIG QUIET)
3131
if(NOT pybind11_FOUND)
3232
message(STATUS "pybind11 not found via config; using FetchContent")
3333
include(FetchContent)
3434
FetchContent_Declare(
3535
pybind11
3636
GIT_REPOSITORY https://github.com/pybind/pybind11.git
37-
GIT_TAG v2.12.0
37+
GIT_TAG v3.0.0
3838
DOWNLOAD_EXTRACT_TIMESTAMP TRUE
3939
)
4040
FetchContent_MakeAvailable(pybind11)
@@ -45,6 +45,9 @@ add_library(netgraph_core STATIC
4545
src/shortest_paths.cpp
4646
src/k_shortest_paths.cpp
4747
src/max_flow.cpp
48+
src/flow_state.cpp
49+
src/flow_graph.cpp
50+
src/flow_policy.cpp
4851
src/cpu_backend.cpp
4952
)
5053
target_include_directories(netgraph_core PUBLIC

Makefile

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ install:
5555
@echo "📦 Installing package (editable)"
5656
@$(PIP) install -e .
5757

58+
5859
check:
5960
@PYTHON=$(PYTHON) bash dev/run-checks.sh
6061
@$(MAKE) lint
@@ -118,7 +119,7 @@ cpp-test:
118119

119120
cov:
120121
@echo "📦 Reinstalling with C++ coverage instrumentation..."
121-
@$(PIP) install -U scikit-build-core pybind11
122+
@$(PIP) install -U scikit-build-core "pybind11>=3"
122123
@PIP_NO_BUILD_ISOLATION=1 CMAKE_ARGS="-DNETGRAPH_CORE_COVERAGE=ON" $(PIP) install -e .[dev]
123124
@echo "🧪 Running Python tests with coverage..."
124125
@mkdir -p build/coverage

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
NetGraph-Core
22

3-
High-performance C++ core for NetGraph with Python bindings. CPU-first, GPU-ready.
3+
C++ core with Python bindings.
44

55
- Native C++ kernels for shortest paths, k-shortest paths, and max-flow
66
- Deterministic, thread-parallel, batch execution with GIL released
7-
- Clean Python API compatible with NetGraph’s public surface
7+
- Python API with a stable, well-documented surface
88

99
Development
1010

bindings/python/module.cpp

Lines changed: 217 additions & 116 deletions
Large diffs are not rendered by default.
Lines changed: 246 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,246 @@
1+
from __future__ import annotations
2+
3+
import importlib
4+
import json
5+
import sys
6+
from typing import Dict, Tuple
7+
8+
9+
def main() -> int:
10+
try:
11+
# Optional: import external Python reference implementation if available
12+
importlib.import_module("ngraph")
13+
from ngraph.algorithms.base import EdgeSelect, PathAlg # type: ignore
14+
from ngraph.algorithms.flow_init import init_flow_graph # type: ignore
15+
from ngraph.algorithms.placement import FlowPlacement # type: ignore
16+
from ngraph.flows.policy import FlowPolicy # type: ignore
17+
from ngraph.graph.strict_multidigraph import StrictMultiDiGraph # type: ignore
18+
except Exception as exc:
19+
print(
20+
"INFO: External reference (ngraph) not available; skipping external comparison.\n"
21+
f"Details: {exc}"
22+
)
23+
return 0
24+
25+
# Import expected matrix from this repository's tests
26+
try:
27+
tfp = importlib.import_module("tests.py.test_flow_policy")
28+
except Exception:
29+
# Fallback when not installed as package; try relative path import
30+
sys.path.insert(0, ".")
31+
tfp = importlib.import_module("tests.py.test_flow_policy")
32+
33+
EXPECTED: Dict[str, Tuple[Tuple[int, int], float]] = tfp.EXPECTED # type: ignore
34+
EXPECTED_MATRIX: Dict[str, Dict[str, Tuple[float, float]]] = tfp.EXPECTED_MATRIX # type: ignore
35+
36+
# Builders (mirroring tests/py/conftest.py)
37+
def build_triangle1() -> StrictMultiDiGraph:
38+
g = StrictMultiDiGraph()
39+
for n in (0, 1, 2):
40+
g.add_node(n)
41+
g.add_edge(0, 1, cost=1, capacity=15)
42+
g.add_edge(1, 0, cost=1, capacity=15)
43+
g.add_edge(1, 2, cost=1, capacity=15)
44+
g.add_edge(2, 1, cost=1, capacity=15)
45+
g.add_edge(0, 2, cost=1, capacity=5)
46+
g.add_edge(2, 0, cost=1, capacity=5)
47+
init_flow_graph(g)
48+
return g
49+
50+
def build_square3() -> StrictMultiDiGraph:
51+
g = StrictMultiDiGraph()
52+
for n in (0, 1, 2, 3):
53+
g.add_node(n)
54+
g.add_edge(0, 1, cost=1, capacity=100)
55+
g.add_edge(1, 2, cost=1, capacity=125)
56+
g.add_edge(0, 3, cost=1, capacity=75)
57+
g.add_edge(3, 2, cost=1, capacity=50)
58+
g.add_edge(1, 3, cost=1, capacity=50)
59+
g.add_edge(3, 1, cost=1, capacity=50)
60+
init_flow_graph(g)
61+
return g
62+
63+
def build_graph1() -> StrictMultiDiGraph:
64+
g = StrictMultiDiGraph()
65+
for n in range(5):
66+
g.add_node(n)
67+
g.add_edge(0, 1, cost=1, capacity=1)
68+
g.add_edge(0, 2, cost=1, capacity=1)
69+
g.add_edge(1, 3, cost=1, capacity=1)
70+
g.add_edge(2, 3, cost=1, capacity=1)
71+
g.add_edge(1, 2, cost=1, capacity=1)
72+
g.add_edge(2, 1, cost=1, capacity=1)
73+
g.add_edge(3, 4, cost=1, capacity=1)
74+
init_flow_graph(g)
75+
return g
76+
77+
def build_graph2() -> StrictMultiDiGraph:
78+
g = StrictMultiDiGraph()
79+
for n in range(5):
80+
g.add_node(n)
81+
g.add_edge(0, 1, cost=1, capacity=1)
82+
g.add_edge(1, 2, cost=1, capacity=1)
83+
g.add_edge(1, 3, cost=1, capacity=1)
84+
g.add_edge(2, 3, cost=1, capacity=1)
85+
g.add_edge(3, 2, cost=1, capacity=1)
86+
g.add_edge(2, 4, cost=1, capacity=1)
87+
g.add_edge(3, 4, cost=1, capacity=1)
88+
init_flow_graph(g)
89+
return g
90+
91+
def build_graph4() -> StrictMultiDiGraph:
92+
g = StrictMultiDiGraph()
93+
for n in range(5):
94+
g.add_node(n)
95+
g.add_edge(0, 1, cost=1, capacity=1)
96+
g.add_edge(1, 4, cost=1, capacity=1)
97+
g.add_edge(0, 2, cost=2, capacity=2)
98+
g.add_edge(2, 4, cost=2, capacity=2)
99+
g.add_edge(0, 3, cost=3, capacity=3)
100+
g.add_edge(3, 4, cost=3, capacity=3)
101+
init_flow_graph(g)
102+
return g
103+
104+
def build_square1() -> StrictMultiDiGraph:
105+
g = StrictMultiDiGraph()
106+
for n in range(4):
107+
g.add_node(n)
108+
g.add_edge(0, 1, cost=1, capacity=1)
109+
g.add_edge(1, 2, cost=1, capacity=1)
110+
g.add_edge(0, 3, cost=2, capacity=2)
111+
g.add_edge(3, 2, cost=2, capacity=2)
112+
init_flow_graph(g)
113+
return g
114+
115+
def build_line1() -> StrictMultiDiGraph:
116+
g = StrictMultiDiGraph()
117+
for n in range(3):
118+
g.add_node(n)
119+
g.add_edge(0, 1, cost=1, capacity=5)
120+
g.add_edge(1, 2, cost=1, capacity=1)
121+
g.add_edge(1, 2, cost=1, capacity=3)
122+
g.add_edge(1, 2, cost=2, capacity=7)
123+
init_flow_graph(g)
124+
return g
125+
126+
def build_graph3() -> StrictMultiDiGraph:
127+
g = StrictMultiDiGraph()
128+
for n in range(6):
129+
g.add_node(n)
130+
g.add_edge(0, 1, cost=1, capacity=2)
131+
g.add_edge(0, 1, cost=1, capacity=4)
132+
g.add_edge(0, 1, cost=1, capacity=6)
133+
g.add_edge(1, 2, cost=1, capacity=1)
134+
g.add_edge(1, 2, cost=1, capacity=2)
135+
g.add_edge(1, 2, cost=1, capacity=3)
136+
g.add_edge(2, 3, cost=2, capacity=3)
137+
g.add_edge(0, 4, cost=1, capacity=5)
138+
g.add_edge(4, 2, cost=1, capacity=4)
139+
g.add_edge(0, 3, cost=4, capacity=2)
140+
g.add_edge(2, 5, cost=1, capacity=1)
141+
g.add_edge(5, 3, cost=1, capacity=2)
142+
init_flow_graph(g)
143+
return g
144+
145+
def build_two_disjoint() -> StrictMultiDiGraph:
146+
g = StrictMultiDiGraph()
147+
for n in range(4):
148+
g.add_node(n)
149+
g.add_edge(0, 1, cost=1, capacity=3)
150+
g.add_edge(1, 3, cost=1, capacity=2)
151+
g.add_edge(0, 2, cost=1, capacity=4)
152+
g.add_edge(2, 3, cost=1, capacity=1)
153+
init_flow_graph(g)
154+
return g
155+
156+
def build_square4() -> StrictMultiDiGraph:
157+
g = StrictMultiDiGraph()
158+
for n in range(4):
159+
g.add_node(n)
160+
g.add_edge(0, 1, cost=1, capacity=100)
161+
g.add_edge(1, 2, cost=1, capacity=125)
162+
g.add_edge(0, 3, cost=1, capacity=75)
163+
g.add_edge(3, 2, cost=1, capacity=50)
164+
g.add_edge(1, 3, cost=1, capacity=50)
165+
g.add_edge(3, 1, cost=1, capacity=50)
166+
g.add_edge(0, 1, cost=2, capacity=200)
167+
g.add_edge(1, 3, cost=2, capacity=200)
168+
g.add_edge(3, 2, cost=2, capacity=200)
169+
init_flow_graph(g)
170+
return g
171+
172+
BUILDERS = {
173+
"square1_graph": build_square1,
174+
"line1_graph": build_line1,
175+
"graph3": build_graph3,
176+
"two_disjoint_shortest_graph": build_two_disjoint,
177+
"square4_graph": build_square4,
178+
"triangle1_graph": build_triangle1,
179+
"square3_graph": build_square3,
180+
"graph1_graph": build_graph1,
181+
"graph2_graph": build_graph2,
182+
"graph4_graph": build_graph4,
183+
}
184+
185+
POLICIES = [
186+
(
187+
"PROPORTIONAL_ALL_MIN_COST_mpTrue",
188+
FlowPlacement.PROPORTIONAL,
189+
EdgeSelect.ALL_MIN_COST,
190+
True,
191+
None,
192+
),
193+
(
194+
"PROPORTIONAL_SINGLE_MIN_COST_WITH_CAP_mpFalse",
195+
FlowPlacement.PROPORTIONAL,
196+
EdgeSelect.SINGLE_MIN_COST_WITH_CAP_REMAINING,
197+
False,
198+
None,
199+
),
200+
(
201+
"EQUAL_BALANCED_ALL_MIN_COST_mpTrue",
202+
FlowPlacement.EQUAL_BALANCED,
203+
EdgeSelect.ALL_MIN_COST,
204+
True,
205+
16,
206+
),
207+
(
208+
"EQUAL_BALANCED_SINGLE_MIN_COST_WITH_CAP_mpFalse",
209+
FlowPlacement.EQUAL_BALANCED,
210+
EdgeSelect.SINGLE_MIN_COST_WITH_CAP_REMAINING,
211+
False,
212+
16,
213+
),
214+
]
215+
216+
mismatches = []
217+
for gname, (sd, demand) in EXPECTED.items():
218+
src, dst = sd
219+
builder = BUILDERS[gname]
220+
g = builder()
221+
for key, placement, sel, mp, mfc in POLICIES:
222+
policy = FlowPolicy(
223+
path_alg=PathAlg.SPF,
224+
flow_placement=placement,
225+
edge_select=sel,
226+
multipath=mp,
227+
max_flow_count=mfc,
228+
)
229+
placed, remaining = policy.place_demand(g, src, dst, "cls", demand)
230+
exp_p, exp_r = EXPECTED_MATRIX[gname][key]
231+
if abs(placed - exp_p) > 1e-6 or abs(remaining - exp_r) > 1e-6:
232+
mismatches.append(
233+
{
234+
"graph": gname,
235+
"policy": key,
236+
"expected": (exp_p, exp_r),
237+
"actual": (placed, remaining),
238+
}
239+
)
240+
241+
print(json.dumps({"mismatches": mismatches}, indent=2))
242+
return 0 if not mismatches else 1
243+
244+
245+
if __name__ == "__main__":
246+
raise SystemExit(main())

include/netgraph/core/backend.hpp

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,15 @@
1+
/*
2+
ExecutionBackend interface — abstracts SP/MaxFlow/KSP implementations.
3+
4+
The default CPU backend delegates to in-process algorithm implementations.
5+
*/
16
#pragma once
27

38
#include <memory>
49
#include <optional>
510
#include <utility>
611
#include <vector>
12+
#include <span>
713

814
#include "netgraph/core/max_flow.hpp"
915
#include "netgraph/core/shortest_paths.hpp"
@@ -14,14 +20,24 @@ namespace netgraph::core {
1420
class ExecutionBackend {
1521
public:
1622
virtual ~ExecutionBackend() = default;
17-
virtual std::pair<std::vector<double>, PredDAG> shortest_paths(
23+
virtual std::pair<std::vector<Cost>, PredDAG> shortest_paths(
1824
const StrictMultiDiGraph& g, NodeId src, std::optional<NodeId> dst,
19-
EdgeSelect policy, bool multipath, double eps) = 0;
25+
const EdgeSelection& selection,
26+
std::span<const Cap> residual = {},
27+
const bool* node_mask = nullptr,
28+
const bool* edge_mask = nullptr) = 0;
2029

21-
virtual std::pair<double, FlowSummary> calc_max_flow(
22-
const StrictMultiDiGraph& g, NodeId s, NodeId t,
30+
virtual std::pair<Flow, FlowSummary> calc_max_flow(
31+
const StrictMultiDiGraph& g, NodeId src, NodeId dst,
2332
FlowPlacement placement, bool shortest_path,
24-
double eps, bool with_edge_flows,
33+
bool with_edge_flows,
34+
const bool* node_mask = nullptr,
35+
const bool* edge_mask = nullptr) = 0;
36+
37+
virtual std::vector<std::pair<std::vector<Cost>, PredDAG>> k_shortest_paths(
38+
const StrictMultiDiGraph& g, NodeId s, NodeId t,
39+
int k, std::optional<double> max_cost_factor,
40+
bool unique,
2541
const bool* node_mask = nullptr,
2642
const bool* edge_mask = nullptr) = 0;
2743
};
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
/* Numeric thresholds shared by SPF and flow placement. */
2+
#pragma once
3+
4+
#include "netgraph/core/types.hpp"
5+
6+
namespace netgraph::core {
7+
8+
// Global numeric thresholds used across SPF and flow placement
9+
inline constexpr Cap kMinCap = static_cast<Cap>(1.0 / 4096.0); // minimum effective remaining capacity
10+
inline constexpr Flow kMinFlow = static_cast<Flow>(1.0 / 4096.0); // minimum meaningful flow when augmenting
11+
inline constexpr double kEpsilon = 1e-12; // numeric clamp
12+
13+
} // namespace netgraph::core

include/netgraph/core/error.hpp

Lines changed: 0 additions & 20 deletions
This file was deleted.

0 commit comments

Comments
 (0)