@@ -74,7 +74,21 @@ PYBIND11_MODULE(_netgraph_core, m) {
7474 auto dst_s = as_span<std::int32_t >(dst, " dst" );
7575 if (src_s.size () != dst_s.size ()) throw py::type_error (" src and dst must have the same length" );
7676 auto cap_s = as_span<double >(capacity, " capacity" );
77- // Accept any numeric dtype for cost; force-cast to int64
77+ // Accept any numeric dtype for cost; if float, require finite values, then force-cast to int64
78+ if (py::isinstance<py::array_t <double >>(cost)) {
79+ auto carr = py::cast<py::array_t <double >>(cost);
80+ if (!(carr.flags () & py::array::c_style)) {
81+ throw py::type_error (" cost: array must be C-contiguous (use np.ascontiguousarray)" );
82+ }
83+ auto cbuf_d = carr.request ();
84+ if (cbuf_d.ndim != 1 ) throw py::type_error (" cost must be a 1-D array" );
85+ const double * cd = static_cast <const double *>(cbuf_d.ptr );
86+ for (ssize_t i = 0 ; i < cbuf_d.shape [0 ]; ++i) {
87+ if (!std::isfinite (cd[i])) {
88+ throw py::value_error (" cost values must be finite" );
89+ }
90+ }
91+ }
7892 py::array_t <std::int64_t , py::array::c_style | py::array::forcecast> cost_i64 (cost);
7993 auto cbuf = cost_i64.request ();
8094 std::span<const Cost> cost_s (static_cast <const Cost*>(cbuf.ptr ), static_cast <std::size_t >(cbuf.size ));
@@ -102,6 +116,34 @@ PYBIND11_MODULE(_netgraph_core, m) {
102116 self_obj
103117 );
104118 })
119+ .def (" edge_src_view" , [](py::object self_obj, const StrictMultiDiGraph& g){
120+ auto s = g.edge_src_view ();
121+ return py::array (
122+ py::buffer_info (
123+ const_cast <std::int32_t *>(s.data ()),
124+ sizeof (std::int32_t ),
125+ py::format_descriptor<std::int32_t >::format (),
126+ 1 ,
127+ { s.size () },
128+ { sizeof (std::int32_t ) }
129+ ),
130+ self_obj
131+ );
132+ })
133+ .def (" edge_dst_view" , [](py::object self_obj, const StrictMultiDiGraph& g){
134+ auto s = g.edge_dst_view ();
135+ return py::array (
136+ py::buffer_info (
137+ const_cast <std::int32_t *>(s.data ()),
138+ sizeof (std::int32_t ),
139+ py::format_descriptor<std::int32_t >::format (),
140+ 1 ,
141+ { s.size () },
142+ { sizeof (std::int32_t ) }
143+ ),
144+ self_obj
145+ );
146+ })
105147 .def (" cost_view" , [](py::object self_obj, const StrictMultiDiGraph& g){
106148 auto s = g.cost_view ();
107149 return py::array (
@@ -167,8 +209,18 @@ PYBIND11_MODULE(_netgraph_core, m) {
167209 m.def (" spf" ,
168210 [](const StrictMultiDiGraph& g, std::int32_t src, py::object dst,
169211 py::object selection_obj, py::object residual_obj, py::object node_mask, py::object edge_mask) {
212+ // Basic index validation
213+ if (src < 0 || src >= g.num_nodes ()) {
214+ throw py::value_error (" src out of range" );
215+ }
170216 std::optional<NodeId> dst_opt;
171- if (!dst.is_none ()) dst_opt = static_cast <NodeId>(py::cast<std::int32_t >(dst));
217+ if (!dst.is_none ()) {
218+ auto dval = static_cast <NodeId>(py::cast<std::int32_t >(dst));
219+ if (dval < 0 || dval >= g.num_nodes ()) {
220+ throw py::value_error (" dst out of range" );
221+ }
222+ dst_opt = dval;
223+ }
172224 // Parse selection
173225 EdgeSelection selection;
174226 if (!selection_obj.is_none ()) {
@@ -224,6 +276,9 @@ PYBIND11_MODULE(_netgraph_core, m) {
224276 m.def (" ksp" ,
225277 [](const StrictMultiDiGraph& g, std::int32_t src, std::int32_t dst,
226278 int k, py::object max_cost_factor, bool unique, py::object node_mask, py::object edge_mask) {
279+ if (src < 0 || src >= g.num_nodes ()) throw py::value_error (" src out of range" );
280+ if (dst < 0 || dst >= g.num_nodes ()) throw py::value_error (" dst out of range" );
281+ if (k <= 0 ) throw py::value_error (" k must be >= 1" );
227282 std::optional<double > mcf;
228283 if (!max_cost_factor.is_none ()) mcf = py::cast<double >(max_cost_factor);
229284 const bool * node_ptr = nullptr ;
@@ -264,24 +319,71 @@ PYBIND11_MODULE(_netgraph_core, m) {
264319 return out;
265320 }, 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 ());
266321
322+ // resolve_to_paths: return list of paths; each path is list of (node, tuple(edge_ids...)) ending with (dst, ())
323+ m.def (" resolve_to_paths" ,
324+ [](const PredDAG& dag, std::int32_t src, std::int32_t dst, bool split_parallel_edges, py::object max_paths){
325+ std::optional<std::int64_t > mp;
326+ if (!max_paths.is_none ()) mp = py::cast<std::int64_t >(max_paths);
327+ auto out = netgraph::core::resolve_to_paths (dag, src, dst, split_parallel_edges, mp);
328+ py::list paths;
329+ for (auto & path : out) {
330+ py::list py_elems;
331+ for (auto & pr : path) {
332+ py::tuple edge_tuple (pr.second .size ());
333+ for (std::size_t i=0 ;i<pr.second .size ();++i) edge_tuple[i] = py::int_ (pr.second [i]);
334+ py_elems.append (py::make_tuple (py::int_ (pr.first ), edge_tuple));
335+ }
336+ paths.append (py::tuple (py_elems));
337+ }
338+ return paths;
339+ }, py::arg (" dag" ), py::arg (" src" ), py::arg (" dst" ), py::kw_only (), py::arg (" split_parallel_edges" ) = false , py::arg (" max_paths" ) = py::none ());
340+
267341 py::class_<MinCut>(m, " MinCut" )
268- .def_readonly (" edges" , &MinCut::edges);
269- py::class_<CostBucket>(m, " CostBucket" )
270- .def_readonly (" cost" , &CostBucket::cost)
271- .def_readonly (" share" , &CostBucket::share);
272- py::class_<CostDistribution>(m, " CostDistribution" )
273- .def_readonly (" buckets" , &CostDistribution::buckets);
342+ .def_property_readonly (" edges" , [](const MinCut& mc){
343+ py::array_t <std::int32_t > arr (mc.edges .size ());
344+ if (!mc.edges .empty ()) {
345+ std::memcpy (arr.mutable_data (), mc.edges .data (), mc.edges .size ()*sizeof (std::int32_t ));
346+ }
347+ return arr;
348+ });
349+ // CostBucket/CostDistribution removed in favor of parallel arrays on FlowSummary
274350 py::class_<FlowSummary>(m, " FlowSummary" )
275351 .def_readonly (" total_flow" , &FlowSummary::total_flow)
276352 .def_readonly (" min_cut" , &FlowSummary::min_cut)
277- .def_readonly (" cost_distribution" , &FlowSummary::cost_distribution)
278- .def_readonly (" edge_flows" , &FlowSummary::edge_flows);
353+ .def_property_readonly (" costs" , [](const FlowSummary& s){
354+ py::array_t <std::int64_t > arr (s.costs .size ());
355+ if (!s.costs .empty ()) std::memcpy (arr.mutable_data (), s.costs .data (), s.costs .size ()*sizeof (std::int64_t ));
356+ return arr;
357+ })
358+ .def_property_readonly (" flows" , [](const FlowSummary& s){
359+ py::array_t <double > arr (s.flows .size ());
360+ if (!s.flows .empty ()) std::memcpy (arr.mutable_data (), s.flows .data (), s.flows .size ()*sizeof (double ));
361+ return arr;
362+ })
363+ .def_property_readonly (" edge_flows" , [](const FlowSummary& s){
364+ py::array_t <double > arr (s.edge_flows .size ());
365+ if (!s.edge_flows .empty ()) std::memcpy (arr.mutable_data (), s.edge_flows .data (), s.edge_flows .size ()*sizeof (double ));
366+ return arr;
367+ })
368+ .def_property_readonly (" residual_capacity" , [](const FlowSummary& s){
369+ py::array_t <double > arr (s.residual_capacity .size ());
370+ if (!s.residual_capacity .empty ()) std::memcpy (arr.mutable_data (), s.residual_capacity .data (), s.residual_capacity .size ()*sizeof (double ));
371+ return arr;
372+ })
373+ .def_property_readonly (" reachable_nodes" , [](const FlowSummary& s){
374+ // Store as bool array for Python; cast bytes to bool
375+ py::array_t <bool > arr (s.reachable_nodes .size ());
376+ auto * out = arr.mutable_data ();
377+ for (std::size_t i=0 ;i<s.reachable_nodes .size ();++i) out[i] = static_cast <bool >(s.reachable_nodes [i]);
378+ return arr;
379+ });
279380
280381 // spf_residual removed; unified spf accepts optional residual and EdgeSelection
281382
282383 m.def (" max_flow" ,
283384 [](const StrictMultiDiGraph& g, std::int32_t src, std::int32_t dst,
284385 FlowPlacement placement, bool shortest_path, bool with_edge_flows,
386+ bool with_reachable, bool with_residuals,
285387 py::object node_mask, py::object edge_mask) {
286388 const bool * node_ptr = nullptr ;
287389 const bool * edge_ptr = nullptr ;
@@ -305,22 +407,24 @@ PYBIND11_MODULE(_netgraph_core, m) {
305407 edge_ptr = static_cast <const bool *>(buf.ptr );
306408 }
307409 py::gil_scoped_release release;
308- auto res = calc_max_flow (g, src, dst, placement, shortest_path, with_edge_flows, node_ptr, edge_ptr);
410+ auto res = calc_max_flow (g, src, dst, placement, shortest_path, with_edge_flows, with_reachable, with_residuals, node_ptr, edge_ptr);
309411 py::gil_scoped_acquire acquire;
310412 return res;
311- }, 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 (" with_edge_flows" ) = false , py::arg (" node_mask" ) = py::none (), py::arg (" edge_mask" ) = py::none ());
413+ }, 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 (" with_edge_flows" ) = false , py::arg (" with_reachable " ) = false , py::arg ( " with_residuals " ) = false , py::arg ( " node_mask" ) = py::none (), py::arg (" edge_mask" ) = py::none ());
312414
313415 m.def (" batch_max_flow" ,
314416 [](const StrictMultiDiGraph& g, py::array pairs,
315417 py::object node_masks, py::object edge_masks,
316- FlowPlacement placement, bool shortest_path, bool with_edge_flows) {
418+ FlowPlacement placement, bool shortest_path, bool with_edge_flows,
419+ bool with_reachable, bool with_residuals) {
317420 auto buf = pairs.request ();
318421 if (buf.ndim != 2 || buf.shape [1 ] != 2 ) throw py::type_error (" pairs must be shape [B,2]" );
319422 // accept int32 for pairs
320423 if (buf.format != py::format_descriptor<std::int32_t >::format ()) throw py::type_error (" pairs dtype must be int32" );
424+ const std::size_t batch_size = static_cast <std::size_t >(buf.shape [0 ]);
321425 std::vector<std::pair<NodeId,NodeId>> pp;
322426 auto * ptr = static_cast <const std::int32_t *>(buf.ptr );
323- pp.reserve (static_cast <std:: size_t >(buf. shape [ 0 ]) );
427+ pp.reserve (batch_size );
324428 for (ssize_t i=0 ;i<buf.shape [0 ];++i) {
325429 if (ptr[2 *i] < 0 || ptr[2 *i+1 ] < 0 ) throw py::type_error (" pairs must be non-negative indices" );
326430 pp.emplace_back (static_cast <std::int32_t >(ptr[2 *i]), static_cast <std::int32_t >(ptr[2 *i+1 ]));
@@ -329,6 +433,7 @@ PYBIND11_MODULE(_netgraph_core, m) {
329433 auto parse_mask_list = [&](py::object list_obj, std::size_t expected_len, const char * what, std::vector<const bool *>& out_ptrs, std::vector<py::array>& keep){
330434 if (list_obj.is_none ()) return ;
331435 auto seq = py::cast<py::sequence>(list_obj);
436+ if (static_cast <std::size_t >(py::len (seq)) != batch_size) throw py::type_error (std::string (what) + " length must equal number of pairs" );
332437 for (auto item: seq) {
333438 auto arr = py::cast<py::array>(item);
334439 if (!(arr.flags () & py::array::c_style)) throw py::type_error (std::string (what) + " arrays must be C-contiguous (np.ascontiguousarray)" );
@@ -347,10 +452,10 @@ PYBIND11_MODULE(_netgraph_core, m) {
347452 parse_mask_list (node_masks, static_cast <std::size_t >(g.num_nodes ()), " node_masks" , node_ptrs, node_keep);
348453 parse_mask_list (edge_masks, static_cast <std::size_t >(g.num_edges ()), " edge_masks" , edge_ptrs, edge_keep);
349454 py::gil_scoped_release release;
350- auto out = netgraph::core::batch_max_flow (g, pp, placement, shortest_path, with_edge_flows, node_ptrs, edge_ptrs);
455+ auto out = netgraph::core::batch_max_flow (g, pp, placement, shortest_path, with_edge_flows, with_reachable, with_residuals, node_ptrs, edge_ptrs);
351456 py::gil_scoped_acquire acquire;
352457 return out;
353- }, py::arg (" g" ), py::arg (" pairs" ), py::kw_only (), py::arg (" node_masks" ) = py::none (), py::arg (" edge_masks" ) = py::none (), py::arg (" flow_placement" ) = FlowPlacement::Proportional, py::arg (" shortest_path" ) = false , py::arg (" with_edge_flows" ) = false );
458+ }, py::arg (" g" ), py::arg (" pairs" ), py::kw_only (), py::arg (" node_masks" ) = py::none (), py::arg (" edge_masks" ) = py::none (), py::arg (" flow_placement" ) = FlowPlacement::Proportional, py::arg (" shortest_path" ) = false , py::arg (" with_edge_flows" ) = false , py::arg ( " with_reachable " ) = false , py::arg ( " with_residuals " ) = false );
354459
355460 // FlowState bindings
356461 py::class_<FlowState>(m, " FlowState" )
@@ -468,7 +573,8 @@ PYBIND11_MODULE(_netgraph_core, m) {
468573 .def (" flow_count" , &FlowPolicy::flow_count)
469574 .def (" placed_demand" , &FlowPolicy::placed_demand)
470575 .def (" place_demand" , [](FlowPolicy& p, FlowGraph& fg, std::int32_t src, std::int32_t dst, std::int32_t flowClass, double volume, py::object target_per_flow, py::object min_flow){ std::optional<double > tpf; if (!target_per_flow.is_none ()) tpf = py::cast<double >(target_per_flow); std::optional<double > mfl; if (!min_flow.is_none ()) mfl = py::cast<double >(min_flow); py::gil_scoped_release rel; auto pr = p.place_demand (fg, src, dst, flowClass, volume, tpf, mfl); py::gil_scoped_acquire acq; return py::make_tuple (pr.first , pr.second ); }, py::arg (" flow_graph" ), py::arg (" src" ), py::arg (" dst" ), py::arg (" flowClass" ), py::arg (" volume" ), py::arg (" target_per_flow" ) = py::none (), py::arg (" min_flow" ) = py::none ())
471- .def (" rebalance_demand" , [](FlowPolicy& p, FlowGraph& fg, std::int32_t src, std::int32_t dst, std::int32_t flowClass, double target){ py::gil_scoped_release rel; auto pr = p.rebalance_demand (fg, src, dst, flowClass, target); py::gil_scoped_acquire acq; return py::make_tuple (pr.first , pr.second ); })
576+ .def (" rebalance_demand" , [](FlowPolicy& p, FlowGraph& fg, std::int32_t src, std::int32_t dst, std::int32_t flowClass, double target){ py::gil_scoped_release rel; auto pr = p.rebalance_demand (fg, src, dst, flowClass, target); py::gil_scoped_acquire acq; return py::make_tuple (pr.first , pr.second ); },
577+ py::arg (" flow_graph" ), py::arg (" src" ), py::arg (" dst" ), py::arg (" flowClass" ), py::arg (" target" ))
472578 .def (" remove_demand" , [](FlowPolicy& p, FlowGraph& fg){ py::gil_scoped_release rel; p.remove_demand (fg); py::gil_scoped_acquire acq; })
473579 .def_property_readonly (" flows" , [](const FlowPolicy& p){ py::dict out; for (auto const & kv : p.flows ()) { const auto & idx = kv.first ; const auto & f = kv.second ; out[py::make_tuple (idx.src , idx.dst , idx.flowClass , idx.flowId )] = py::make_tuple (f.src , f.dst , f.cost , f.placed_flow ); } return out; });
474580}
0 commit comments