ATLAS Offline Software
Loading...
Searching...
No Matches
NanobindBindings.cxx
Go to the documentation of this file.
1/*
2 Copyright (C) 2002-2025 CERN for the benefit of the ATLAS collaboration
3*/
4
8
11
12#ifdef XAOD_STANDALONE
15#endif
17
18#include <nanobind/nanobind.h>
19#include <nanobind/ndarray.h>
20#include <nanobind/operators.h>
21#include <nanobind/stl/list.h>
22#include <nanobind/stl/string.h>
23#include <nanobind/stl/vector.h>
24#include <nanobind/stl/pair.h>
25#include <nanobind/stl/map.h>
26
27#include <cstdio>
28#include <string>
29
30namespace nb = nanobind;
31
32using namespace nb::literals;
33
34// this function lets us take an object and translate the pointer to a hexadecimal representation
35// 140164452316520 becomes "0x7f7a9463f568"
37 char buffer[32]; // Ensure the buffer is large enough for the hex representation
38 std::snprintf(buffer, sizeof(buffer), "0x%llx", reinterpret_cast<unsigned long long>(&obj));
39 return std::string(buffer);
40}
41
42// this function converts us from type_info to a human-readable name
43std::string get_type_name(const std::type_info& type_info) {
44 if (type_info == typeid(int)) {
45 return "int32";
46 } else if (type_info == typeid(unsigned int)) {
47 return "uint32";
48 } else if (type_info == typeid(short)) {
49 return "int16";
50 } else if (type_info == typeid(unsigned short)) {
51 return "uint16";
52 } else if (type_info == typeid(char)) {
54 return "int8";
55 } else if (type_info == typeid(unsigned char)) {
56 return "uint8";
57 } else if (type_info == typeid(float)) {
58 return "float32";
59 } else if (type_info == typeid(double)) {
60 return "float64";
61 } else if (type_info == typeid(long)) {
62 return "int64";
63 } else if (type_info == typeid(unsigned long)) {
64 return "uint64";
65 } else if (type_info == typeid(bool)) {
66 return "bool";
67 } else {
68 // If the type is unknown, you can return the mangled name or a default message
69 return std::string("unknown ('") + type_info.name() + "')";
70 }
71}
72
73void setProperty(columnar::PythonToolHandle &self, const std::string& key, nb::object value){
74 if (nb::isinstance<nb::str>(value)) {
75 self.setProperty(key, nb::cast<std::string>(value));
76 } else if (nb::isinstance<nb::int_>(value)) {
77 self.setProperty(key, nb::cast<int>(value));
78 } else if (nb::isinstance<nb::float_>(value)) {
79 self.setProperty(key, nb::cast<double>(value));
80 } else {
81 throw std::runtime_error("Unsupported property type. Must be str, int, or float.");
82 }
83}
84
85nb::object getColumnVoid(columnar::PythonToolHandle &self, const std::string& key) {
86 auto [size, ptr, type] = self.getColumnVoid(key);
87 // Wrap the raw pointer in a numpy array without copying. The caller is
88 // responsible for keeping the PythonToolHandle alive while using the result.
89#define MAKE_NDARRAY(T) \
90 nb::ndarray<nb::numpy, T, nb::ro>(static_cast<const T*>(ptr), {size}).cast()
91 if (*type == typeid(float)) return MAKE_NDARRAY(float);
92 if (*type == typeid(double)) return MAKE_NDARRAY(double);
93 if (*type == typeid(char)) return MAKE_NDARRAY(char);
94 if (*type == typeid(int)) return MAKE_NDARRAY(int);
95 if (*type == typeid(std::uint8_t)) return MAKE_NDARRAY(std::uint8_t);
96 if (*type == typeid(std::uint16_t)) return MAKE_NDARRAY(std::uint16_t);
97 if (*type == typeid(std::uint32_t)) return MAKE_NDARRAY(std::uint32_t);
98 if (*type == typeid(std::uint64_t)) return MAKE_NDARRAY(std::uint64_t);
99 if (*type == typeid(std::int16_t)) return MAKE_NDARRAY(std::int16_t);
100 if (*type == typeid(std::int32_t)) return MAKE_NDARRAY(std::int32_t);
101 if (*type == typeid(std::int64_t)) return MAKE_NDARRAY(std::int64_t);
102#undef MAKE_NDARRAY
103 throw std::runtime_error("getColumnVoid: unsupported column type: " + std::string(type->name()));
104}
105
106void setColumnVoid(columnar::PythonToolHandle &self, const std::string& key, nb::ndarray<> column, bool is_const = true) {
107 // TODO: figure out how to get type_info from handle instead...
108 // nb::handle handle = column.handle();
109
110 const std::type_info* type_info = nullptr;
111 const nb::dlpack::dtype dtype = column.dtype();
112 switch ((nb::dlpack::dtype_code) dtype.code) {
113 case nb::dlpack::dtype_code::Int:
114 switch (dtype.bits) {
115 // escape hatch to handle char for now
116 // we should rely on signed/unsigned and nbits, instead of std::type_info
117 // case 8: type_info = &typeid(std::int8_t); break;
118 case 8: type_info = &typeid(char); break;
119 case 16: type_info = &typeid(std::int16_t); break;
120 case 32: type_info = &typeid(std::int32_t); break;
121 case 64: type_info = &typeid(std::int64_t); break;
122 }
123 break;
124
125 case nb::dlpack::dtype_code::UInt:
126 switch (dtype.bits) {
127 case 8: type_info = &typeid(std::uint8_t); break;
128 case 16: type_info = &typeid(std::uint16_t); break;
129 case 32: type_info = &typeid(std::uint32_t); break;
130 case 64: type_info = &typeid(std::uint64_t); break;
131 }
132 break;
133
134 case nb::dlpack::dtype_code::Float:
135 switch (dtype.bits) {
136 case 32: type_info = &typeid(float); break;
137 case 64: type_info = &typeid(double); break;
138 }
139 break;
140
141 default:
142 break;
143 }
144
145 if (type_info == nullptr) throw std::runtime_error ("unsupported column type passed in");
146 self.setColumnVoid(key, column.shape(0), column.data(), *type_info, is_const);
147}
148
149void setImmutableColumnVoid(columnar::PythonToolHandle &self, const std::string& key, nb::ndarray<> column){
150 setColumnVoid(self, key, column, true);
151};
152
153namespace {
154
155#ifdef XAOD_STANDALONE
158struct PyMessagePrinter : public asg::IMessagePrinter {
159 nb::callable m_callback;
160
161 explicit PyMessagePrinter(nb::callable cb)
162 : m_callback(std::move(cb)) {}
163
164 void print(MSG::Level lvl, const std::string& name,
165 const std::string& text) override {
166 // Guard against Python interpreter shutdown
167 if (!Py_IsInitialized())
168 return;
169 nb::gil_scoped_acquire gil;
170 m_callback(static_cast<int>(lvl), name, text);
171 }
172};
173
174// Global state for printer management
175static std::unique_ptr<PyMessagePrinter> g_printer;
176static std::unique_ptr<asg::MessagePrinterOverlay> g_overlay;
177
178void set_printer_from_callable(nb::callable cb) {
179 g_printer = std::make_unique<PyMessagePrinter>(std::move(cb));
180 g_overlay.reset();
181 g_overlay = std::make_unique<asg::MessagePrinterOverlay>(g_printer.get());
182}
183
184void clear_printer() {
185 g_overlay.reset();
186 g_printer.reset();
187}
188#endif // XAOD_STANDALONE
189
190} // anonymous namespace
191
192
193NB_MODULE(python_tool_handle, module) {
194 module.doc() = "Nanobind bindings for PythonToolHandle";
195
197 throw nb::import_error("This module can only be used in columnar access mode. Try setting up a ColumnarAnalysis release instead.");
198
199 module.attr("numberOfEventsName") = &columnar::eventRangeColumnName;
200 module.attr("eventRangeColumnName") = &columnar::eventRangeColumnName;
201
202 // Install a Python callable as the global C++ message printer.
203#ifdef XAOD_STANDALONE
204 // Overload 1: with callback function
205 module.def("set_python_printer", &set_printer_from_callable, nb::arg("callback"),
206 "Install a Python callable(level: int, name: str, text: str) as the global "
207 "C++ message printer.");
208
209 // Overload 2: reset (no argument)
210 module.def("set_python_printer", &clear_printer,
211 "Reset to the default stdout message printer.");
212#else
213 // In Athena/AthAnalysis builds IMessagePrinter does not exist; expose the
214 // function so Python code doesn't get AttributeError, but raise at call time.
215 module.def("set_python_printer", [](nb::args, nb::kwargs) {
216 throw std::runtime_error(
217 "set_python_printer is only available in standalone "
218 "(AnalysisBase/ColumnarAnalysis) builds, not in Athena/AthAnalysis.");
219 }, "Not available in Athena/AthAnalysis builds.");
220#endif // XAOD_STANDALONE
221
223 nb::enum_<columnar::ColumnAccessMode>(module, "ColumnAccessMode")
224 .value("input", columnar::ColumnAccessMode::input)
225 .value("output", columnar::ColumnAccessMode::output)
226 .value("update", columnar::ColumnAccessMode::update)
227 .def("__repr__", [](const columnar::ColumnAccessMode &mode) -> std::string {
228 switch (mode) {
230 return "<ColumnAccessMode input>";
232 return "<ColumnAccessMode output>";
234 return "<ColumnAccessMode update>";
235 default:
236 return "<ColumnAccessMode update value=" + std::to_string(static_cast<int>(mode)) + ">";
237 }
238 })
239 .export_values(); // Makes the enum values accessible without namespace in Python
240
241 nb::enum_<MSG::Level>(module, "MsgLevel", nb::is_arithmetic())
242 .value("NIL", MSG::NIL)
243 .value("VERBOSE", MSG::VERBOSE)
244 .value("DEBUG", MSG::DEBUG)
245 .value("INFO", MSG::INFO)
246 .value("WARNING", MSG::WARNING)
247 .value("ERROR", MSG::ERROR)
248 .value("FATAL", MSG::FATAL)
249 .export_values();
250
251 nb::class_<columnar::ColumnInfo>(module, "ColumnInfo")
252 .def(nb::init<>()) // Default constructor
253 .def_ro("name", &columnar::ColumnInfo::name)
254 .def_ro("index", &columnar::ColumnInfo::index)
255 .def_prop_ro("dtype", [](const columnar::ColumnInfo &self){
256 return get_type_name(*self.type);
257 })
258 .def_ro("access_mode", &columnar::ColumnInfo::accessMode)
259 .def_ro("offset_name", &columnar::ColumnInfo::offsetName)
260 .def_ro("fixed_dimensions", &columnar::ColumnInfo::fixedDimensions)
261 .def_ro("sole_link_target_name", &columnar::ColumnInfo::soleLinkTargetName)
262 .def_ro("is_variant_link", &columnar::ColumnInfo::isVariantLink)
263 .def_ro("variant_link_target_names", &columnar::ColumnInfo::variantLinkTargetNames)
264 .def_ro("key_column_for_variant_link", &columnar::ColumnInfo::keyColumnForVariantLink)
265 .def_ro("is_offset", &columnar::ColumnInfo::isOffset)
266 .def_ro("replaces_column", &columnar::ColumnInfo::replacesColumn)
267 .def_ro("is_optional", &columnar::ColumnInfo::isOptional)
268 .def("__repr__", [](const columnar::ColumnInfo &self) {
269 std::string access_mode;
270 switch (self.accessMode) {
272 access_mode = "input";
273 break;
275 access_mode = "output";
276 break;
278 access_mode = "update";
279 break;
280 default:
281 // For unknown values, return the integer value
282 access_mode = "unknown";
283 }
284 return "<ColumnInfo name='" + self.name + "'" +
285 (self.isOffset ? "" : ", offset='" + self.offsetName + "'") +
286 ", access_mode='" + access_mode + "'" +
287 ", dtype='" + (self.type ? get_type_name(*self.type) : "" ) + "'" +
288 (self.isOptional ? ", optional": "") +
289 ">";
290 })
291 .def("to_dict", [](const columnar::ColumnInfo& self) {
292 nb::dict d;
293 d["name"] = self.name;
294 d["index"] = self.index;
295 d["dtype"] = self.type ? get_type_name(*self.type) : "";
296 d["access_mode"] = static_cast<int>(self.accessMode);
297 d["offset_name"] = self.offsetName;
298 d["fixed_dimensions"] = self.fixedDimensions;
299 d["sole_link_target_name"] = self.soleLinkTargetName;
300 d["is_variant_link"] = self.isVariantLink;
301 d["variant_link_target_names"] = self.variantLinkTargetNames;
302 d["key_column_for_variant_link"] = self.keyColumnForVariantLink;
303 d["is_offset"] = self.isOffset;
304 d["replaces_column"] = self.replacesColumn;
305 d["is_optional"] = self.isOptional;
306 return d;
307 });
308
309 nb::class_<columnar::PythonToolHandle>(module, "PythonToolHandle")
310 .def(nb::init())
311
312 // Properties
313 .def_prop_ro("type", [](const columnar::PythonToolHandle &self) {
314 const asg::AsgToolConfig& config = self.getConfig();
315 const std::string& type = config.type();
316 if (type.empty()) {
317 std::cerr << "Warning: PythonToolHandle.type is empty."
318 << " Set with PythonToolHandle.set_type_and_name." << std::endl;
319 }
320 return type;
321 })
322
323 .def_prop_ro("name", [](const columnar::PythonToolHandle &self) {
324 const asg::AsgToolConfig& config = self.getConfig();
325 const std::string& name = config.name();
326 if (name.empty()) {
327 std::cerr << "Warning: PythonToolHandle.name is empty."
328 << " Set with PythonToolHandle.set_type_and_name." << std::endl;
329 }
330 return name;
331 })
332
333 // Methods
334 .def("set_type_and_name",
335 [](columnar::PythonToolHandle &self, const std::string& type_and_name) {
336 self.setTypeAndName(type_and_name);
337 },
338 "type_and_name"_a,
339 "Set the type and name of the tool.")
340
341 .def("set_property", &setProperty,
342 "key"_a, "value"_a,
343 "Set a property on the tool.")
344
345 .def("__setattr__", &setProperty,
346 "key"_a, "value"_a,
347 "Set a property on the tool.")
348
349 .def("preinitialize",
351 "Preinitialize the tool.")
352
353 // rename_containers([("from", "to"), ...])
354 .def("rename_containers",
356 "renames"_a,
357 "Rename the columns the tool uses.")
358
359 // rename_containers({"from": "to"}, ...})
360 .def("rename_containers",
361 [](columnar::PythonToolHandle &self, const std::map<std::string,std::string>& renames){
362 std::vector<std::pair<std::string, std::string>> vectorized;
363 for (const auto& pair : renames)
364 vectorized.emplace_back(pair);
365
366 return self.renameContainers(vectorized);
367 },
368 "renames"_a,
369 "Rename the columns the tool uses.")
370
371 .def("initialize",
373 "Initialize the tool.")
374
375 .def("apply_systematic_variation",
376 [](columnar::PythonToolHandle &self, const std::string& sys_name) {
377 self.applySystematicVariation(sys_name);
378 },
379 "sys_name"_a,
380 "Apply a systematic variation to the tool.")
381
382 .def("set_column",
383 [](columnar::PythonToolHandle &self, const std::string& key, nb::ndarray<float> column) {
384 self.setColumn<float>(key, column.shape(0), column.data());
385 },
386 "key"_a, "column"_a,
387 "Set a float column pointer.")
388
389 .def("set_column",
390 [](columnar::PythonToolHandle &self, const std::string& key, nb::ndarray<char> column) {
391 self.setColumn<char>(key, column.shape(0), column.data());
392 },
393 "key"_a, "column"_a,
394 "Set a char column pointer.")
395
396 .def("set_column",
397 [](columnar::PythonToolHandle &self, const std::string& key, nb::ndarray<int> column) {
398 self.setColumn<int>(key, column.shape(0), column.data());
399 },
400 "key"_a, "column"_a,
401 "Set an int column pointer.")
402
403 .def("set_column",
404 [](columnar::PythonToolHandle &self, const std::string& key, nb::ndarray<std::uint8_t> column) {
405 self.setColumn<uint8_t>(key, column.shape(0), column.data());
406 },
407 "key"_a, "column"_a,
408 "Set a uint8_t column pointer.")
409
410 .def("set_column",
411 [](columnar::PythonToolHandle &self, const std::string& key, nb::ndarray<std::uint16_t> column) {
412 self.setColumn<uint16_t>(key, column.shape(0), column.data());
413 },
414 "key"_a, "column"_a,
415 "Set a uint16_t column pointer.")
416
417 .def("set_column",
418 [](columnar::PythonToolHandle &self, const std::string& key, nb::ndarray<std::uint32_t> column) {
419 self.setColumn<uint32_t>(key, column.shape(0), column.data());
420 },
421 "key"_a, "column"_a,
422 "Set a uint32_t column pointer.")
423
424 .def("set_column",
425 [](columnar::PythonToolHandle &self, const std::string& key, nb::ndarray<std::uint64_t> column) {
426 self.setColumn<uint64_t>(key, column.shape(0), column.data());
427 },
428 "key"_a, "column"_a,
429 "Set a uint64_t column pointer.")
430
431 .def("set_column_void", &setColumnVoid,
432 // cppcheck-suppress assignBoolToPointer
433 "key"_a, "column"_a, "is_const"_a = true,
434 "Set a void column pointer (nanobind version).")
435
436 .def("__setitem__", &setImmutableColumnVoid,
437 "key"_a, "column"_a,
438 "Set a void immutable column pointer (nanobind version).")
439
440 .def("__getitem__", &getColumnVoid,
441 "key"_a,
442 "Get a column as a numpy array (zero-copy view into the tool's buffer).")
443
444 .def("keys",
446 "Return the column names (enables dict(handle)).")
447
448 .def("call",
450 "Call the tool and reset the columns.")
451
452 .def_prop_ro(
453 "columns",
455 "Get the expected column information."
456 )
457
458 .def("get_recommended_systematics",
460 "Get the recommended systematics.")
461
462 // Make this more fancy in the future
463 // <PythonToolHandle(CP::MuonEfficiencyScaleFactors/unique0) object at 0x7f2943b07568>
464 .def("__repr__", [](const columnar::PythonToolHandle &self) {
465 const asg::AsgToolConfig& config = self.getConfig();
466 return "<PythonToolHandle(" + config.type() + "/" + config.name() + ") object at " + getAddressString(self) + ">";
467 });
468}
Definition of message levels and a helper function.
NB_MODULE(python_tool_handle, module)
nb::object getColumnVoid(columnar::PythonToolHandle &self, const std::string &key)
std::string get_type_name(const std::type_info &type_info)
void setColumnVoid(columnar::PythonToolHandle &self, const std::string &key, nb::ndarray<> column, bool is_const=true)
#define MAKE_NDARRAY(T)
void setProperty(columnar::PythonToolHandle &self, const std::string &key, nb::object value)
void setImmutableColumnVoid(columnar::PythonToolHandle &self, const std::string &key, nb::ndarray<> column)
std::string getAddressString(const columnar::PythonToolHandle &obj)
void print(char *figname, TCanvas *c1)
an object that can create a AsgTool
a handle to a python tool for use via nanobind
void setColumn(const std::string &key, std::size_t size, CT *dataPtr)
set a column pointer (raw pointer version)
void renameContainers(const std::vector< std::pair< std::string, std::string > > &renames)
rename the columns the tool uses
void preinitialize()
preinitialize the tool
void applySystematicVariation(const std::string &sysName)
set the tool to apply the given systematic variation
void setTypeAndName(const std::string &typeAndName)
set the type and name for the tool
void setColumnVoid(const std::string &name, std::size_t size, const void *dataPtr, const std::type_info &type, bool isConst)
set a column pointer
std::vector< std::string > getRecommendedSystematics() const
get the recommended systematics
std::tuple< std::size_t, const void *, const std::type_info * > getColumnVoid(const std::string &name) const
get a column pointer by name; returns {size, ptr, type_info}
const asg::AsgToolConfig & getConfig() const
get the AsgToolConfig
std::vector< ColumnInfo > getColumnInfo() const
get the expected column info
std::vector< std::string > getColumnNames() const
get the expected column names
void setProperty(const std::string &key, T &&value)
set a property on the tool
void call()
call the tool and reset the columns
void initialize()
initialize the tool
STL class.
constexpr unsigned columnarAccessMode
ColumnAccessMode
an enum for the different access modes for a column
Definition ColumnInfo.h:19
@ update
an updateable column
Definition ColumnInfo.h:27
@ output
an output column
Definition ColumnInfo.h:24
@ input
an input column
Definition ColumnInfo.h:21
setWord1 uint16_t
setEventNumber uint32_t
a struct that contains meta-information about each column that's needed to interface the column with ...
Definition ColumnInfo.h:35
std::string offsetName
the name of the offset column used for this column (or empty string for none)
Definition ColumnInfo.h:74
std::string soleLinkTargetName
for simple link columns: the name of the target container
Definition ColumnInfo.h:131
std::string keyColumnForVariantLink
if this is a key column for a variant link, the name of the associated link column
Definition ColumnInfo.h:177
std::vector< unsigned > fixedDimensions
the fixed dimensions this column has (if any)
Definition ColumnInfo.h:82
bool isOptional
whether this column is optional
Definition ColumnInfo.h:121
std::vector< std::string > variantLinkTargetNames
for variant link key columns: the names of the containers we can link to
Definition ColumnInfo.h:162
std::string name
the name of the column
Definition ColumnInfo.h:42
bool isOffset
whether this is an offset column
Definition ColumnInfo.h:92
ColumnAccessMode accessMode
the access mode for the column
Definition ColumnInfo.h:58
std::string replacesColumn
whether this replaces another column
Definition ColumnInfo.h:102
bool isVariantLink
whether this is a variant link column
Definition ColumnInfo.h:139
unsigned index
the index of the column in the data array
Definition ColumnInfo.h:46
const std::type_info * type
the type of the individual entries in the column
Definition ColumnInfo.h:54