Performance improvements, better readme and complete python bindings
🧪 Test / test (push) Failing after 28m30s
🧪 Test / test (push) Failing after 28m30s
This commit is contained in:
@@ -0,0 +1,311 @@
|
||||
#pragma once
|
||||
// Auto-binding helpers for KPN++ Python bindings.
|
||||
//
|
||||
// Usage in your binding .cpp:
|
||||
//
|
||||
// #define KPN_BUILD_PYTHON
|
||||
// #include <kpn/python/auto_bind.hpp>
|
||||
//
|
||||
// int produce() { return 42; }
|
||||
// int double_it(int x) { return x * 2; }
|
||||
// void print_it(int x) { std::cout << x << '\n'; }
|
||||
//
|
||||
// using MyNodes = kpn::python::NodeRegistry<
|
||||
// kpn::python::Entry<produce, "produce">,
|
||||
// kpn::python::Entry<double_it, "double_it">,
|
||||
// kpn::python::Entry<print_it, "print_it">
|
||||
// >;
|
||||
//
|
||||
// NB_MODULE(my_kpn, m) {
|
||||
// kpn::python::bind_network<MyNodes>(m); // KPN_BIND_PYTHON behaviour
|
||||
// kpn::python::bind_debug<MyNodes>(m); // KPN_PYTHON_DEBUG behaviour
|
||||
// }
|
||||
//
|
||||
// bind_network registers:
|
||||
// - Network class (PyNetwork<auto-deduced-variant>) with auto-registered converters
|
||||
// - make_<name>(capacity=5) factory for each entry
|
||||
// - <Name>Node class for each entry
|
||||
//
|
||||
// bind_debug additionally registers each raw C++ function as a free Python
|
||||
// callable (e.g. double_it(5) → 10) so node logic can be tested without a network.
|
||||
//
|
||||
// To support a custom type T, specialise kpn::PythonConverter<T> before calling
|
||||
// bind_network:
|
||||
//
|
||||
// namespace kpn {
|
||||
// template<> struct PythonConverter<MyVec3> {
|
||||
// static constexpr const char* type_name = "vec3"; // optional friendly name
|
||||
// static nb::object to_python(const MyVec3& v) { ... }
|
||||
// static MyVec3 from_python(nb::object o) { ... }
|
||||
// };
|
||||
// } // namespace kpn
|
||||
|
||||
#include "../variant_node.hpp"
|
||||
#include "../traits.hpp"
|
||||
#include "bindings.hpp"
|
||||
|
||||
#ifdef KPN_BUILD_PYTHON
|
||||
#include <nanobind/nanobind.h>
|
||||
#include <nanobind/stl/shared_ptr.h>
|
||||
#include <nanobind/stl/string.h>
|
||||
#include <nanobind/stl/vector.h>
|
||||
|
||||
#include <cctype>
|
||||
#include <string>
|
||||
#include <tuple>
|
||||
#include <type_traits>
|
||||
|
||||
// ── PythonConverter specialisations for built-in nanobind-castable types ─────
|
||||
// These live in kpn:: to match the primary template in variant_node.hpp.
|
||||
|
||||
namespace kpn {
|
||||
|
||||
template<> struct PythonConverter<int> {
|
||||
static constexpr const char* type_name = "int";
|
||||
static nanobind::object to_python(const int& v) { return nanobind::cast(v); }
|
||||
static int from_python(nanobind::object o) { return nanobind::cast<int>(std::move(o)); }
|
||||
};
|
||||
|
||||
template<> struct PythonConverter<float> {
|
||||
static constexpr const char* type_name = "float";
|
||||
static nanobind::object to_python(const float& v) { return nanobind::cast(v); }
|
||||
static float from_python(nanobind::object o) { return nanobind::cast<float>(std::move(o)); }
|
||||
};
|
||||
|
||||
template<> struct PythonConverter<double> {
|
||||
static constexpr const char* type_name = "double";
|
||||
static nanobind::object to_python(const double& v) { return nanobind::cast(v); }
|
||||
static double from_python(nanobind::object o) { return nanobind::cast<double>(std::move(o)); }
|
||||
};
|
||||
|
||||
template<> struct PythonConverter<bool> {
|
||||
static constexpr const char* type_name = "bool";
|
||||
static nanobind::object to_python(const bool& v) { return nanobind::cast(v); }
|
||||
static bool from_python(nanobind::object o) { return nanobind::cast<bool>(std::move(o)); }
|
||||
};
|
||||
|
||||
template<> struct PythonConverter<std::string> {
|
||||
static constexpr const char* type_name = "str";
|
||||
static nanobind::object to_python(const std::string& v) { return nanobind::cast(v); }
|
||||
static std::string from_python(nanobind::object o) {
|
||||
return nanobind::cast<std::string>(std::move(o));
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace kpn
|
||||
|
||||
namespace kpn::python {
|
||||
namespace nb = nanobind;
|
||||
|
||||
// ── Entry<Func, Name> ─────────────────────────────────────────────────────────
|
||||
// Compile-time descriptor for one bindable node function.
|
||||
|
||||
template<auto Func, fixed_string Name>
|
||||
struct Entry {
|
||||
static constexpr auto func = Func;
|
||||
static constexpr auto name = Name;
|
||||
};
|
||||
|
||||
// ── NodeRegistry<Es...> ───────────────────────────────────────────────────────
|
||||
|
||||
template<typename... Es>
|
||||
struct NodeRegistry {
|
||||
using entries_tuple = std::tuple<Es...>;
|
||||
static constexpr std::size_t size = sizeof...(Es);
|
||||
};
|
||||
|
||||
// ── Internal TMP ──────────────────────────────────────────────────────────────
|
||||
|
||||
namespace detail {
|
||||
|
||||
// All non-void port types for a single function (args + normalised returns).
|
||||
template<auto Func>
|
||||
struct entry_port_types {
|
||||
using args = args_t<decltype(Func)>;
|
||||
using ret = normalised_return_t<return_t<decltype(Func)>>;
|
||||
using type = decltype(std::tuple_cat(std::declval<args>(), std::declval<ret>()));
|
||||
};
|
||||
|
||||
// Flat tuple of all types across all entries (may contain duplicates).
|
||||
template<typename... Es>
|
||||
struct all_types_flat {
|
||||
using type = decltype(std::tuple_cat(
|
||||
std::declval<typename entry_port_types<Es::func>::type>()...));
|
||||
};
|
||||
|
||||
template<typename Registry>
|
||||
struct registry_flat_types;
|
||||
|
||||
template<typename... Es>
|
||||
struct registry_flat_types<NodeRegistry<Es...>> {
|
||||
using type = typename all_types_flat<Es...>::type;
|
||||
};
|
||||
|
||||
// Unpack a tuple into unique_types_t (which takes a pack, not a tuple).
|
||||
// unique_types_t<T> takes Ts... not std::tuple<Ts...>, so we need this bridge.
|
||||
template<typename Tuple>
|
||||
struct unpack_unique;
|
||||
|
||||
template<typename... Ts>
|
||||
struct unpack_unique<std::tuple<Ts...>> {
|
||||
using type = kpn::detail::unique_types_t<Ts...>;
|
||||
};
|
||||
|
||||
// SFINAE: does PythonConverter<T> have a 'type_name' member?
|
||||
template<typename Conv, typename = void>
|
||||
struct has_type_name : std::false_type {};
|
||||
|
||||
template<typename Conv>
|
||||
struct has_type_name<Conv, std::void_t<decltype(Conv::type_name)>> : std::true_type {};
|
||||
|
||||
inline std::string make_class_name(std::string_view snake) {
|
||||
std::string result(snake);
|
||||
if (!result.empty()) result[0] = static_cast<char>(std::toupper(result[0]));
|
||||
result += "Node";
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace detail
|
||||
|
||||
// ── registry_variant_t<Registry> ─────────────────────────────────────────────
|
||||
// Deduces std::variant<UniqueTypes...> from all port types across the registry.
|
||||
|
||||
template<typename Registry>
|
||||
using registry_variant_t = typename kpn::detail::tuple_to_variant<
|
||||
typename detail::unpack_unique<
|
||||
typename detail::registry_flat_types<Registry>::type>::type
|
||||
>::type;
|
||||
|
||||
// ── Converter registration ────────────────────────────────────────────────────
|
||||
|
||||
template<typename T, typename Variant>
|
||||
void register_one_type(PyNetwork<Variant>& net) {
|
||||
const char* friendly = nullptr;
|
||||
if constexpr (detail::has_type_name<PythonConverter<T>>::value)
|
||||
friendly = PythonConverter<T>::type_name;
|
||||
|
||||
net.template register_full_type<T>(
|
||||
[](const T& v) -> nb::object { return PythonConverter<T>::to_python(v); },
|
||||
[](nb::object o) -> T { return PythonConverter<T>::from_python(std::move(o)); },
|
||||
friendly);
|
||||
}
|
||||
|
||||
template<typename Variant, typename... Ts>
|
||||
void register_types_impl(PyNetwork<Variant>& net, std::tuple<Ts...>*) {
|
||||
(register_one_type<Ts>(net), ...);
|
||||
}
|
||||
|
||||
template<typename Registry, typename Variant>
|
||||
void register_all_converters(PyNetwork<Variant>& net) {
|
||||
using Flat = typename detail::registry_flat_types<Registry>::type;
|
||||
using Unique = typename detail::unpack_unique<Flat>::type;
|
||||
register_types_impl(net, static_cast<Unique*>(nullptr));
|
||||
}
|
||||
|
||||
// ── Per-entry class + factory registration ───────────────────────────────────
|
||||
|
||||
namespace detail {
|
||||
|
||||
template<typename E, typename Variant>
|
||||
void register_one_entry(nb::module_& m) {
|
||||
using Wrapper = VariantNodeWrapper<E::func, Variant>;
|
||||
|
||||
auto class_name = make_class_name(E::name.view());
|
||||
auto make_name = "make_" + std::string(E::name.view());
|
||||
|
||||
nb::class_<Wrapper, IVariantNode<Variant>>(m, class_name.c_str())
|
||||
.def("__init__", [](Wrapper* self, std::size_t cap) {
|
||||
new (self) Wrapper(cap);
|
||||
}, nb::arg("capacity") = 5);
|
||||
|
||||
m.def(make_name.c_str(),
|
||||
[](std::size_t cap) -> std::shared_ptr<IVariantNode<Variant>> {
|
||||
return std::make_shared<Wrapper>(cap);
|
||||
},
|
||||
nb::arg("capacity") = 5);
|
||||
}
|
||||
|
||||
template<typename Variant, typename... Es>
|
||||
void register_entries_impl(nb::module_& m, std::tuple<Es...>*) {
|
||||
(register_one_entry<Es, Variant>(m), ...);
|
||||
}
|
||||
|
||||
} // namespace detail
|
||||
|
||||
// ── bind_network<Registry> ────────────────────────────────────────────────────
|
||||
// Registers:
|
||||
// - INode — base class (opaque Python handle)
|
||||
// - Network — PyNetwork with auto-registered converters
|
||||
// - <Name>Node — VariantNodeWrapper for each entry
|
||||
// - make_<name>() — factory returning shared_ptr<INode>
|
||||
|
||||
template<typename Registry>
|
||||
void bind_network(nb::module_& m) {
|
||||
using Variant = registry_variant_t<Registry>;
|
||||
using Net = PyNetwork<Variant>;
|
||||
using Entries = typename Registry::entries_tuple;
|
||||
|
||||
nb::class_<IVariantNode<Variant>>(m, "INode");
|
||||
|
||||
nb::class_<Net>(m, "Network")
|
||||
.def("__init__", [](Net* self) {
|
||||
new (self) Net();
|
||||
register_all_converters<Registry>(*self);
|
||||
})
|
||||
// add(name, c++_node)
|
||||
.def("add", [](Net& self, std::string name,
|
||||
std::shared_ptr<IVariantNode<Variant>> node) {
|
||||
self.add(std::move(name), std::move(node));
|
||||
}, nb::arg("name"), nb::arg("node"))
|
||||
// add_node(name, callable, inputs=[...], outputs=[...], capacity=5)
|
||||
.def("add_node", &Net::add_node_python,
|
||||
nb::arg("name"),
|
||||
nb::arg("callable"),
|
||||
nb::arg("inputs") = std::vector<std::string>{},
|
||||
nb::arg("outputs") = std::vector<std::string>{},
|
||||
nb::arg("capacity") = std::size_t(5))
|
||||
.def("connect", &Net::connect,
|
||||
nb::arg("src"), nb::arg("out_idx"),
|
||||
nb::arg("dst"), nb::arg("in_idx"))
|
||||
.def("build", &Net::build)
|
||||
.def("start", &Net::start)
|
||||
.def("stop", &Net::stop)
|
||||
.def("read", &Net::read,
|
||||
nb::arg("node"), nb::arg("out_idx") = std::size_t(0))
|
||||
.def("write", &Net::write,
|
||||
nb::arg("node"), nb::arg("in_idx"), nb::arg("value"))
|
||||
;
|
||||
|
||||
detail::register_entries_impl<Variant>(m, static_cast<Entries*>(nullptr));
|
||||
}
|
||||
|
||||
// ── bind_debug<Registry> ─────────────────────────────────────────────────────
|
||||
// Exposes each node's raw C++ function as a free Python callable so node logic
|
||||
// can be unit-tested without constructing a network.
|
||||
//
|
||||
// Example: assert kpn.double_it(5) == 10
|
||||
|
||||
namespace detail {
|
||||
|
||||
template<typename E>
|
||||
void bind_one_debug(nb::module_& m) {
|
||||
auto name_str = std::string(E::name.view());
|
||||
m.def(name_str.c_str(), E::func);
|
||||
}
|
||||
|
||||
template<typename... Es>
|
||||
void bind_debug_impl(nb::module_& m, std::tuple<Es...>*) {
|
||||
(bind_one_debug<Es>(m), ...);
|
||||
}
|
||||
|
||||
} // namespace detail
|
||||
|
||||
template<typename Registry>
|
||||
void bind_debug(nb::module_& m) {
|
||||
using Entries = typename Registry::entries_tuple;
|
||||
detail::bind_debug_impl(m, static_cast<Entries*>(nullptr));
|
||||
}
|
||||
|
||||
} // namespace kpn::python
|
||||
|
||||
#endif // KPN_BUILD_PYTHON
|
||||
@@ -26,6 +26,9 @@ namespace nb = nanobind;
|
||||
// them via IVariantChannel adapters. The variant only lives at the boundary;
|
||||
// each node's internal Channel<T> stores raw T values.
|
||||
|
||||
template<typename Variant>
|
||||
class PyNode; // forward declaration
|
||||
|
||||
template<typename Variant>
|
||||
class PyNetwork {
|
||||
public:
|
||||
@@ -43,8 +46,6 @@ public:
|
||||
}
|
||||
|
||||
// connect(src_name, out_idx, dst_name, in_idx)
|
||||
// Wires src's output port out_idx to dst's input port in_idx.
|
||||
// Type check: both sides must carry the same T.
|
||||
void connect(const std::string& src_name, std::size_t out_idx,
|
||||
const std::string& dst_name, std::size_t in_idx)
|
||||
{
|
||||
@@ -65,7 +66,6 @@ public:
|
||||
dst_name + ".input[" + std::to_string(in_idx) +
|
||||
"] (" + dst.input_type(in_idx).name() + ")");
|
||||
|
||||
// The destination node owns the input channel — get it, then tell src to use it.
|
||||
auto ch = dst.input_channel(in_idx);
|
||||
src.set_output_channel(out_idx, std::move(ch));
|
||||
adj_[src_name].push_back(dst_name);
|
||||
@@ -92,16 +92,12 @@ public:
|
||||
|
||||
// ── Python tap/inject ─────────────────────────────────────────────────────
|
||||
|
||||
// Read one value from node's output port. Releases GIL while blocking.
|
||||
nb::object read(const std::string& node_name, std::size_t out_idx) {
|
||||
// We need a channel that sits on the output of this node.
|
||||
// read() installs a tap channel if not already present.
|
||||
auto key = tap_key(node_name, out_idx);
|
||||
if (!taps_.count(key)) {
|
||||
auto& src = node_at(node_name);
|
||||
if (out_idx >= src.output_count())
|
||||
throw std::out_of_range(node_name + ": output index out of range");
|
||||
// Create a tap channel matching the output type and wire it
|
||||
auto tap = make_tap_channel(src.output_type(out_idx));
|
||||
src.set_output_channel(out_idx, tap);
|
||||
taps_[key] = std::move(tap);
|
||||
@@ -114,7 +110,6 @@ public:
|
||||
return variant_to_python(std::move(v));
|
||||
}
|
||||
|
||||
// Write a Python value into node's input port. Releases GIL while blocking.
|
||||
void write(const std::string& node_name, std::size_t in_idx, nb::object value) {
|
||||
auto& dst = node_at(node_name);
|
||||
if (in_idx >= dst.input_count())
|
||||
@@ -128,8 +123,31 @@ public:
|
||||
}
|
||||
}
|
||||
|
||||
// ── Converter registration ────────────────────────────────────────────────
|
||||
// Called once per type at module init time to register to/from Python converters.
|
||||
// ── Python-callable node creation ─────────────────────────────────────────
|
||||
// Creates a PyNode wrapping a Python callable and adds it to the graph.
|
||||
// Type names must have been registered via register_full_type<T>().
|
||||
|
||||
void add_node_python(std::string name, nb::object callable,
|
||||
std::vector<std::string> in_names,
|
||||
std::vector<std::string> out_names,
|
||||
std::size_t capacity = 5)
|
||||
{
|
||||
std::vector<std::type_index> in_types, out_types;
|
||||
for (auto& s : in_names) in_types.push_back(resolve_type_name(s));
|
||||
for (auto& s : out_names) out_types.push_back(resolve_type_name(s));
|
||||
|
||||
add(std::move(name),
|
||||
std::make_shared<PyNode<Variant>>(
|
||||
std::move(callable),
|
||||
std::move(in_types),
|
||||
std::move(out_types),
|
||||
to_python_,
|
||||
from_python_,
|
||||
ch_factories_,
|
||||
capacity));
|
||||
}
|
||||
|
||||
// ── Type converter registration ───────────────────────────────────────────
|
||||
|
||||
template<typename T>
|
||||
void register_type(
|
||||
@@ -143,6 +161,52 @@ public:
|
||||
};
|
||||
}
|
||||
|
||||
// register_channel_factory<T>: registers factory for creating input channels.
|
||||
template<typename T>
|
||||
void register_channel_factory() {
|
||||
ch_factories_[std::type_index(typeid(T))] =
|
||||
[](std::size_t cap) -> std::shared_ptr<VChannel> {
|
||||
return std::make_shared<VariantChannel<T, Variant>>(
|
||||
std::make_shared<Channel<T>>(cap));
|
||||
};
|
||||
}
|
||||
|
||||
// Backward-compatible alias.
|
||||
template<typename T>
|
||||
void register_tap_factory(std::size_t = 5) {
|
||||
register_channel_factory<T>();
|
||||
}
|
||||
|
||||
// register_full_type<T>: registers converters + channel factory + type name.
|
||||
// This is what auto_bind.hpp calls; manual bindings can call register_type +
|
||||
// register_tap_factory separately for backward compatibility.
|
||||
template<typename T>
|
||||
void register_full_type(
|
||||
std::function<nb::object(const T&)> to_py,
|
||||
std::function<T(nb::object)> from_py,
|
||||
const char* friendly_name = nullptr)
|
||||
{
|
||||
register_type<T>(std::move(to_py), std::move(from_py));
|
||||
register_channel_factory<T>();
|
||||
auto idx = std::type_index(typeid(T));
|
||||
type_names_.insert_or_assign(typeid(T).name(), idx);
|
||||
if (friendly_name) type_names_.insert_or_assign(friendly_name, idx);
|
||||
}
|
||||
|
||||
// ── Type name lookup ──────────────────────────────────────────────────────
|
||||
|
||||
void register_type_name(const std::string& name, std::type_index idx) {
|
||||
type_names_.insert_or_assign(name, idx);
|
||||
}
|
||||
|
||||
std::type_index resolve_type_name(const std::string& name) const {
|
||||
auto it = type_names_.find(name);
|
||||
if (it == type_names_.end())
|
||||
throw std::runtime_error(
|
||||
"type '" + name + "' not registered — call register_full_type<T>() first");
|
||||
return it->second;
|
||||
}
|
||||
|
||||
private:
|
||||
VNode& node_at(const std::string& name) {
|
||||
auto it = nodes_.find(name);
|
||||
@@ -166,15 +230,14 @@ private:
|
||||
return node + ":" + std::to_string(idx);
|
||||
}
|
||||
|
||||
std::shared_ptr<VChannel> make_tap_channel(std::type_index type) {
|
||||
// Create the right VariantChannel<T> based on the registered type index.
|
||||
// We need a factory registered per type — stored in tap_factories_.
|
||||
auto it = tap_factories_.find(type);
|
||||
if (it == tap_factories_.end())
|
||||
std::shared_ptr<VChannel> make_tap_channel(std::type_index type,
|
||||
std::size_t cap = 5) {
|
||||
auto it = ch_factories_.find(type);
|
||||
if (it == ch_factories_.end())
|
||||
throw std::runtime_error(
|
||||
"no tap factory for type: " + std::string(type.name()) +
|
||||
" — was register_type() called for this type?");
|
||||
return it->second();
|
||||
"no channel factory for type: " + std::string(type.name()) +
|
||||
" — call register_full_type<T>() or register_tap_factory<T>()");
|
||||
return it->second(cap);
|
||||
}
|
||||
|
||||
nb::object variant_to_python(Variant v) {
|
||||
@@ -194,18 +257,6 @@ private:
|
||||
return it->second(std::move(obj));
|
||||
}
|
||||
|
||||
public:
|
||||
// Called by register_type to also register a tap channel factory.
|
||||
template<typename T>
|
||||
void register_tap_factory(std::size_t capacity = 5) {
|
||||
auto idx = std::type_index(typeid(T));
|
||||
tap_factories_[idx] = [capacity]() -> std::shared_ptr<VChannel> {
|
||||
auto ch = std::make_shared<Channel<T>>(capacity);
|
||||
return std::make_shared<VariantChannel<T, Variant>>(std::move(ch));
|
||||
};
|
||||
}
|
||||
|
||||
private:
|
||||
std::map<std::string, std::shared_ptr<VNode>> nodes_;
|
||||
std::map<std::string, std::vector<std::string>> adj_;
|
||||
std::vector<std::string> topo_;
|
||||
@@ -213,19 +264,27 @@ private:
|
||||
|
||||
std::map<std::type_index, std::function<nb::object(const Variant&)>> to_python_;
|
||||
std::map<std::type_index, std::function<Variant(nb::object)>> from_python_;
|
||||
std::map<std::type_index, std::function<std::shared_ptr<VChannel>()>> tap_factories_;
|
||||
|
||||
// Channel factory: type → function(capacity) → VChannel.
|
||||
// Used both for tap channels (read()) and PyNode input channel creation.
|
||||
std::map<std::type_index,
|
||||
std::function<std::shared_ptr<VChannel>(std::size_t)>> ch_factories_;
|
||||
|
||||
// Friendly name → type_index (e.g. "int" → typeid(int)).
|
||||
std::map<std::string, std::type_index> type_names_;
|
||||
};
|
||||
|
||||
// ── PyNode<Variant> ───────────────────────────────────────────────────────────
|
||||
// A pure-Python processing node. Holds a nanobind callable.
|
||||
// run_loop: pop inputs (release GIL), call Python (acquire GIL), push outputs (release GIL).
|
||||
// run_loop: pop inputs (release GIL), call Python (acquire GIL), push outputs.
|
||||
|
||||
template<typename Variant>
|
||||
class PyNode : public IVariantNode<Variant> {
|
||||
public:
|
||||
using VChannel = IVariantChannel<Variant>;
|
||||
|
||||
using ChannelFactory = std::function<std::shared_ptr<VChannel>(std::size_t capacity)>;
|
||||
using ChannelFactory =
|
||||
std::function<std::shared_ptr<VChannel>(std::size_t capacity)>;
|
||||
|
||||
PyNode(nb::object callable,
|
||||
std::vector<std::type_index> in_types,
|
||||
@@ -264,7 +323,6 @@ public:
|
||||
for (auto& ch : in_channels_) ch->disable();
|
||||
if (thread_.joinable()) {
|
||||
thread_.request_stop();
|
||||
// Release GIL while joining — run_loop may be waiting to acquire it.
|
||||
nb::gil_scoped_release release;
|
||||
thread_.join();
|
||||
}
|
||||
@@ -311,20 +369,17 @@ public:
|
||||
|
||||
private:
|
||||
void run_loop() {
|
||||
// This thread does not hold the GIL. It acquires it only for Python calls.
|
||||
while (!stop_flag_.load(std::memory_order_relaxed)) {
|
||||
try {
|
||||
auto t0 = clock_t::now();
|
||||
|
||||
// Pop all inputs — no GIL needed, these are pure C++ channel ops
|
||||
std::vector<Variant> inputs(in_channels_.size());
|
||||
for (std::size_t i = 0; i < in_channels_.size(); ++i)
|
||||
inputs[i] = in_channels_[i]->pop();
|
||||
|
||||
auto t1 = clock_t::now();
|
||||
auto t1 = clock_t::now();
|
||||
auto cpu0 = NodeStats::cpu_now();
|
||||
|
||||
// Acquire GIL only for the Python call and type conversion
|
||||
std::vector<Variant> outputs;
|
||||
{
|
||||
nb::gil_scoped_acquire acquire;
|
||||
@@ -344,10 +399,9 @@ private:
|
||||
}
|
||||
|
||||
auto cpu1 = NodeStats::cpu_now();
|
||||
auto t2 = clock_t::now();
|
||||
auto t2 = clock_t::now();
|
||||
stats_.record_exec(duration_t(t2 - t1), duration_t(t1 - t0), cpu0, cpu1);
|
||||
|
||||
// Push outputs — no GIL needed
|
||||
for (std::size_t i = 0; i < out_channels_.size(); ++i) {
|
||||
if (out_channels_[i])
|
||||
out_channels_[i]->push(std::move(outputs[i]));
|
||||
@@ -385,9 +439,9 @@ private:
|
||||
NodeStats stats_;
|
||||
};
|
||||
|
||||
// ── register_py_network ───────────────────────────────────────────────────────
|
||||
// Registers PyNetwork<Variant> and PyNode<Variant> with the given nanobind module.
|
||||
// Call once per module, passing the Variant type derived from your registered node types.
|
||||
// ── register_py_network (legacy helper) ───────────────────────────────────────
|
||||
// Registers PyNetwork<Variant> with the given nanobind module.
|
||||
// Prefer bind_network<Registry> from auto_bind.hpp for new code.
|
||||
|
||||
template<typename Variant>
|
||||
void register_py_network(nb::module_& m, const char* class_name = "Network") {
|
||||
@@ -402,7 +456,7 @@ void register_py_network(nb::module_& m, const char* class_name = "Network") {
|
||||
.def("start", &Net::start)
|
||||
.def("stop", &Net::stop)
|
||||
.def("read", &Net::read,
|
||||
nb::arg("node"), nb::arg("out_idx") = 0)
|
||||
nb::arg("node"), nb::arg("out_idx") = std::size_t(0))
|
||||
.def("write", &Net::write,
|
||||
nb::arg("node"), nb::arg("in_idx"), nb::arg("value"));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user