ChimeraTK-DeviceAccess 03.31.00
Loading...
Searching...
No Matches
PyConvert.h
Go to the documentation of this file.
1// SPDX-FileCopyrightText: Deutsches Elektronen-Synchrotron DESY, MSK, ChimeraTK Project <chimeratk-support@desy.de>
2// SPDX-License-Identifier: LGPL-3.0-or-later
3#pragma once
4
28#include <ChimeraTK/Exception.h>
29#include <ChimeraTK/SupportedUserTypes.h>
30
31#include <pybind11/numpy.h>
32
33namespace ChimeraTK {
34
35 namespace py = pybind11;
36
37 /********************************************************************************************************************/
38
44 template<typename expectedUserType>
45 expectedUserType convertPyScalar(const py::object& input) {
46 // check for numpy scalars (e.g., np.int32, np.float64)
47 // NumPy scalars have an `.item()` method that returns standard Python primitives
48 py::object clean_input = input;
49 if(py::hasattr(input, "item")) {
50 // This throws if it's not a numpy scalar or array with single element
51 clean_input = input.attr("item")(); // Convert np.int32(5) -> Python int(5)
52 }
53
54 // Python strings - str or numpy str
55 if(py::isinstance<py::str>(clean_input)) {
56 return userTypeToUserType<expectedUserType>(clean_input.cast<std::string>());
57 }
58
59 // Python float is float64
60 if(py::isinstance<py::float_>(clean_input)) {
61 return userTypeToUserType<expectedUserType>(clean_input.cast<double>());
62 }
63 // input is Python bool or int, but bool is a subclass of int in Python
64 if(py::isinstance<py::int_>(clean_input)) {
65 // in order not to lose information, cast negative integers to int64 and positive to uint64,
66 // before userTypeToUserType does any clamping
67 // Determine the sign via an object-level comparison with 0 (portable; _PyLong_Sign would be faster but is a
68 // private CPython API).
69 if(clean_input.attr("__lt__")(py::int_(0)).cast<bool>()) {
70 return userTypeToUserType<expectedUserType>(clean_input.cast<int64_t>());
71 }
72 return userTypeToUserType<expectedUserType>(clean_input.cast<uint64_t>());
73 }
74 // input is something unknown but might still define Python number protocol
75 return userTypeToUserType<expectedUserType>(clean_input.cast<double>());
76 }
77
78 /********************************************************************************************************************/
79
81 template<typename expectedUserType, typename FROM>
82 std::vector<expectedUserType> convertTypedPyArray(const py::array& arr) {
83 std::vector<expectedUserType> converted(arr.size());
84 // Convert reference handle to typed wrapper (Zero-copy, O(1) operation)
85 auto typed_arr = arr.cast<py::array_t<FROM>>();
86
87 // Request an unchecked proxy view (NDIM=1 for 1D, 2 for 2D, etc.)
88 // Disables GIL acquisition and runtime bounds-checking overhead.
89 auto r = typed_arr.template unchecked<1>();
90
91 if constexpr(std::is_same_v<expectedUserType, FROM>) {
92 // memcpy shortcut when in/out types are same
93 std::memcpy(converted.data(), r.data(0), r.shape(0) * r.itemsize());
94 }
95 else {
96 // Directly access elements
97 for(ssize_t i = 0; i < r.shape(0); ++i) {
98 // Zero-copy direct read
99 // std::cout << "received value: " << in << " of size " << sizeof(FROM) << " bytes.\n";
100 converted[i] = userTypeToUserType<expectedUserType>(r(i));
101 }
102 }
103 return converted;
104 }
105
106 /********************************************************************************************************************/
107
114 template<typename expectedUserType>
115 std::vector<expectedUserType> convertPyArray(const py::array& arr) {
116 py::dtype dt = arr.dtype();
117
118 if(dt.is(py::dtype::of<int8_t>())) {
119 return convertTypedPyArray<expectedUserType, int8_t>(arr);
120 }
121 if(dt.is(py::dtype::of<int16_t>())) {
122 return convertTypedPyArray<expectedUserType, int16_t>(arr);
123 }
124 if(dt.is(py::dtype::of<int32_t>())) {
125 return convertTypedPyArray<expectedUserType, int32_t>(arr);
126 }
127 if(dt.is(py::dtype::of<int64_t>())) {
128 return convertTypedPyArray<expectedUserType, int64_t>(arr);
129 }
130 if(dt.is(py::dtype::of<uint8_t>())) {
131 return convertTypedPyArray<expectedUserType, uint8_t>(arr);
132 }
133 if(dt.is(py::dtype::of<uint16_t>())) {
134 return convertTypedPyArray<expectedUserType, uint16_t>(arr);
135 }
136 if(dt.is(py::dtype::of<uint32_t>())) {
137 return convertTypedPyArray<expectedUserType, uint32_t>(arr);
138 }
139 if(dt.is(py::dtype::of<uint64_t>())) {
140 return convertTypedPyArray<expectedUserType, uint64_t>(arr);
141 }
142 if(dt.is(py::dtype::of<float>())) {
143 return convertTypedPyArray<expectedUserType, float>(arr);
144 }
145 if(dt.is(py::dtype::of<double>())) {
146 return convertTypedPyArray<expectedUserType, double>(arr);
147 }
148 if(dt.is(py::dtype::of<bool>())) {
149 return convertTypedPyArray<expectedUserType, bool>(arr);
150 }
151
152 // note, we exclude numpy arrays of strings since they are fixed-width of unknown width
153 throw ChimeraTK::logic_error("Unsupported NumPy array dtype!");
154 }
155
156 /********************************************************************************************************************/
157
160 template<typename expectedUserType>
161 std::vector<expectedUserType> convertPyList(const py::object& input) {
162 // Note, the following does not work because the cast often chooses a wrong type when doing auto conversion:
163 // auto vec = input.cast<std::vector<std::variant<int, double>>();
164 // Instead, we must manually go over the sequence and convert every item
165
166 auto seq = input.cast<py::sequence>();
167 std::vector<expectedUserType> converted(seq.size());
168
169 std::transform(seq.begin(), seq.end(), converted.begin(), [](const auto& itemHandle) {
170 auto v = py::reinterpret_borrow<py::object>(itemHandle);
171 return convertPyScalar<expectedUserType>(v);
172 });
173 return converted;
174 }
175
176 /********************************************************************************************************************/
177
179 template<typename expectedUserType>
180 std::vector<expectedUserType> convertPyObject(const py::object& input, bool allowScalar, bool allowArray) {
181 assert(allowScalar || allowArray);
182 if(allowArray) {
183 // check numpy arrays first (Zero-copy, preserves multi-dim metadata)
184 if(py::isinstance<py::array>(input)) {
185 auto arr = input.cast<py::array>();
186 return convertPyArray<expectedUserType>(arr);
187 }
188
189 // Python lists/tuples must go to convertPyList, NOT to convertPyScalar,
190 // even when a scalar is also allowed (e.g. write1D with allowScalar=true).
191 if(py::isinstance<py::sequence>(input) && !py::isinstance<py::str>(input)) {
192 return convertPyList<expectedUserType>(input);
193 }
194 }
195
196 if(allowScalar) {
197 return {convertPyScalar<expectedUserType>(input)};
198 }
199
200 if(allowArray) {
201 // when input is a scalar string, and allowScalar=false so it's clear it should be treated as array of chars
202
203 // try casting to vector (Handles Python lists/tuples)
204 return convertPyList<expectedUserType>(input);
205 }
206
207 throw ChimeraTK::logic_error("unsupported parameters for convertPyObject");
208 }
209
210 /********************************************************************************************************************/
211
213 template<typename expectedUserType, typename FROM>
214 std::vector<std::vector<expectedUserType>> convertTypedPyArray2D(const py::array& arr) {
215 auto typed = arr.cast<py::array_t<FROM>>();
216 auto r = typed.template unchecked<2>();
217 ssize_t rows = arr.shape(0);
218 ssize_t cols = arr.shape(1);
219 std::vector<std::vector<expectedUserType>> converted(rows, std::vector<expectedUserType>(cols));
220
221 if constexpr(std::is_same_v<expectedUserType, FROM>) {
222 if(arr.flags() & py::array::c_style) {
223 // memcpy shortcut when in/out types are same
224 for(ssize_t i = 0; i < rows; ++i) {
225 std::memcpy(converted[i].data(), r.data(i, 0), cols * r.itemsize());
226 }
227 return converted;
228 }
229 }
230 for(ssize_t i = 0; i < rows; ++i) {
231 for(ssize_t j = 0; j < cols; ++j) {
232 converted[i][j] = userTypeToUserType<expectedUserType>(r(i, j));
233 }
234 }
235 return converted;
236 }
237
238 /********************************************************************************************************************/
239
241 template<typename expectedUserType>
242 std::vector<std::vector<expectedUserType>> convertPyArray2D(const py::array& arr) {
243 py::dtype dt = arr.dtype();
244 if(dt.is(py::dtype::of<int8_t>())) {
245 return convertTypedPyArray2D<expectedUserType, int8_t>(arr);
246 }
247 if(dt.is(py::dtype::of<int16_t>())) {
248 return convertTypedPyArray2D<expectedUserType, int16_t>(arr);
249 }
250 if(dt.is(py::dtype::of<int32_t>())) {
251 return convertTypedPyArray2D<expectedUserType, int32_t>(arr);
252 }
253 if(dt.is(py::dtype::of<int64_t>())) {
254 return convertTypedPyArray2D<expectedUserType, int64_t>(arr);
255 }
256 if(dt.is(py::dtype::of<uint8_t>())) {
257 return convertTypedPyArray2D<expectedUserType, uint8_t>(arr);
258 }
259 if(dt.is(py::dtype::of<uint16_t>())) {
260 return convertTypedPyArray2D<expectedUserType, uint16_t>(arr);
261 }
262 if(dt.is(py::dtype::of<uint32_t>())) {
263 return convertTypedPyArray2D<expectedUserType, uint32_t>(arr);
264 }
265 if(dt.is(py::dtype::of<uint64_t>())) {
266 return convertTypedPyArray2D<expectedUserType, uint64_t>(arr);
267 }
268 if(dt.is(py::dtype::of<float>())) {
269 return convertTypedPyArray2D<expectedUserType, float>(arr);
270 }
271 if(dt.is(py::dtype::of<double>())) {
272 return convertTypedPyArray2D<expectedUserType, double>(arr);
273 }
274 if(dt.is(py::dtype::of<bool>())) {
275 return convertTypedPyArray2D<expectedUserType, bool>(arr);
276 }
277 throw ChimeraTK::logic_error("Unsupported NumPy array dtype!");
278 }
279
280 /********************************************************************************************************************/
281
283 template<typename expectedUserType>
284 std::vector<std::vector<expectedUserType>> convertPyObject2D(const py::object& input) {
285 if(py::isinstance<py::array>(input)) {
286 auto arr = input.cast<py::array>();
287 if(arr.ndim() != 2) {
288 throw ChimeraTK::logic_error("convertPyObject2D: expected a 2D numpy array");
289 }
290 return convertPyArray2D<expectedUserType>(arr);
291 }
292
293 // otherwise a sequence of sequences (e.g. numpy array wrapped as list, or list of lists)
294 auto outer = input.cast<py::sequence>();
295 std::vector<std::vector<expectedUserType>> result;
296 result.reserve(outer.size());
297 for(const auto& rowHandle : outer) {
298 auto row = py::reinterpret_borrow<py::object>(rowHandle);
299 result.push_back(convertPyObject<expectedUserType>(row, false, true));
300 }
301 return result;
302 }
303
304 /********************************************************************************************************************/
305
309 template<typename expectedUserType>
310 expectedUserType convertScalarValue(const py::object& input) {
311 std::vector<expectedUserType> ret = convertPyObject<expectedUserType>(input, true, true);
312 if(ret.size() != 1) {
313 throw ChimeraTK::logic_error("array of size!=1 was supplied where a scalar value was expected");
314 }
315 return ret[0];
316 }
317
319 template<typename expectedUserType>
320 std::optional<expectedUserType> convertScalarOptionalValue(const py::object& input) {
321 std::vector<expectedUserType> ret = convertPyObject<expectedUserType>(input, true, true);
322 if(ret.size() > 1) {
323 throw ChimeraTK::logic_error("array of size>1 was supplied where a scalar value was expected");
324 }
325 if(ret.empty()) {
326 return {};
327 }
328 return ret[0];
329 }
330
331 /********************************************************************************************************************/
332
333} // namespace ChimeraTK
Exception thrown when a logic error has occured.
Definition Exception.h:51
This header collects some conversion routines which should be useful both for ApplicationCore Python ...
std::vector< std::vector< expectedUserType > > convertTypedPyArray2D(const py::array &arr)
helper for 2D numpy array conversion when the data type is known at compile time
Definition PyConvert.h:214
expectedUserType convertPyScalar(const py::object &input)
Convert a python scalar (bool, int, float, str; also numpy scalars) into the expected UserType.
Definition PyConvert.h:45
std::optional< expectedUserType > convertScalarOptionalValue(const py::object &input)
Just like convertScalarValue, but input may be empty.
Definition PyConvert.h:320
expectedUserType convertScalarValue(const py::object &input)
Convert a python object (scalar, 1-element list, or 1-element array) into a UserType scalar value.
Definition PyConvert.h:310
std::vector< expectedUserType > convertTypedPyArray(const py::array &arr)
helper for 1D numpy array conversion when data types are know at compile time
Definition PyConvert.h:82
std::vector< expectedUserType > convertPyList(const py::object &input)
this takes native python lists, content possibly of mixed types convertToPyArr should be preferred wh...
Definition PyConvert.h:161
std::vector< expectedUserType > convertPyArray(const py::array &arr)
This takes 1D numpy arrays of any type.
Definition PyConvert.h:115
std::vector< expectedUserType > convertPyObject(const py::object &input, bool allowScalar, bool allowArray)
input may be numpy arrays, numpy scalars, python primitives, and lists of numpy scalars or python pri...
Definition PyConvert.h:180
std::vector< std::vector< expectedUserType > > convertPyArray2D(const py::array &arr)
Take a 2D numpy array and convert it into a vector of vectors of expectedUserType (one inner vector p...
Definition PyConvert.h:242
std::vector< std::vector< expectedUserType > > convertPyObject2D(const py::object &input)
input may be a nested python list (list of lists) or a 2D numpy array; result is one inner vector per...
Definition PyConvert.h:284