PyppFundamentals.hpp
Go to the documentation of this file.
1 // Distributed under the MIT License.
2 // See LICENSE.txt for details.
3 
4 /// \file
5 /// Defines classes for converting to and from Python objects
6 
7 #pragma once
8 
9 #include <Python.h>
10 #include <array>
11 #include <cstddef>
12 #include <initializer_list>
13 #include <stdexcept>
14 #include <type_traits>
15 #include <vector>
16 
17 // These macros are required so that the NumPy API will work when used in
18 // multiple cpp files.
19 #ifndef PY_ARRAY_UNIQUE_SYMBOL
20 #define PY_ARRAY_UNIQUE_SYMBOL SPECTRE_PY_API
21 #endif
22 #define NO_IMPORT_ARRAY
23 // Disable compiler warnings. NumPy ensures API compatibility among different
24 // 1.x versions, as features become deprecated in Numpy 1.x will still function
25 // but cause a compiler warning
26 #define NPY_NO_DEPRECATED_API NPY_1_7_API_VERSION
27 #include <numpy/arrayobject.h>
28 
29 #include "DataStructures/DataVector.hpp"
32 #include "Utilities/Gsl.hpp"
33 #include "Utilities/MakeArray.hpp"
34 #include "Utilities/Requires.hpp"
35 #include "Utilities/TypeTraits.hpp"
36 
37 namespace pypp {
38 /// \cond
39 using None = void*;
40 
41 template <typename T, typename = std::nullptr_t>
42 struct ToPyObject;
43 
44 template <typename T>
45 PyObject* to_py_object(const T& t) {
46  PyObject* value = ToPyObject<T>::convert(t);
47  if (value == nullptr) {
48  throw std::runtime_error{"Failed to convert argument."};
49  }
50  return value;
51 }
52 
53 template <typename T, typename = std::nullptr_t>
54 struct FromPyObject;
55 
56 template <typename T>
57 T from_py_object(PyObject* t) {
58  return FromPyObject<T>::convert(t);
59 }
60 /// \endcond
61 
62 /// Create a python tuple from Args
63 ///
64 /// \tparam Args the types of the arguments to be put in the tuple (deducible)
65 /// \param t the arguments to put into the tuple
66 /// \return PyObject* containing a Python tuple
67 template <typename... Args>
68 PyObject* make_py_tuple(const Args&... t) {
69  PyObject* py_tuple = PyTuple_New(sizeof...(Args));
70  int entry = 0;
71  const auto add_entry = [&entry, &py_tuple](const auto& arg) {
72  PyObject* value = to_py_object(arg);
73  PyTuple_SetItem(py_tuple, entry, value);
74  entry++;
75  return '0';
76  };
77  (void)add_entry; // GCC warns that add_entry is unused
78  (void)std::initializer_list<char>{add_entry(t)...};
79  return py_tuple;
80 }
81 
82 ///\cond
83 template <>
84 struct ToPyObject<void, std::nullptr_t> {
85  static PyObject* convert() { return Py_None; }
86 };
87 
88 template <typename T>
89 struct ToPyObject<
90  T, Requires<cpp17::is_same_v<typename std::decay<T>::type, std::string>>> {
91  static PyObject* convert(const T& t) {
92 #if PY_MAJOR_VERSION == 2 && PY_MINOR_VERSION == 7
93  return PyString_FromString(t.c_str());
94 #elif PY_MAJOR_VERSION == 3
95  return PyUnicode_FromString(t.c_str());
96 #else
97  static_assert(false, "Only works on Python 2.7 and 3.x")
98 #endif
99  }
100 };
101 
102 template <typename T>
103 struct ToPyObject<
104  T, Requires<cpp17::is_same_v<typename std::decay<T>::type, bool>>> {
105  static PyObject* convert(const T& t) {
106  return PyBool_FromLong(static_cast<long>(t));
107  }
108 };
109 
110 template <typename T>
111 struct ToPyObject<
112  T, Requires<cpp17::is_same_v<typename std::decay<T>::type, int> or
113  cpp17::is_same_v<typename std::decay<T>::type, short>>> {
114  static PyObject* convert(const T& t) {
115 #if PY_MAJOR_VERSION == 2 && PY_MINOR_VERSION == 7
116  return PyInt_FromLong(t);
117 #elif PY_MAJOR_VERSION == 3
118  return PyLong_FromLong(t);
119 #else
120  static_assert(false, "Only works on Python 2.7 and 3.x")
121 #endif
122  }
123 };
124 
125 template <typename T>
126 struct ToPyObject<
127  T, Requires<cpp17::is_same_v<typename std::decay<T>::type, long>>> {
128  static PyObject* convert(const T& t) { return PyLong_FromLong(t); }
129 };
130 
131 template <typename T>
132 struct ToPyObject<
133  T, Requires<cpp17::is_same_v<typename std::decay<T>::type, unsigned long> or
134  cpp17::is_same_v<typename std::decay<T>::type, unsigned int>>> {
135  static PyObject* convert(const T& t) { return PyLong_FromUnsignedLong(t); }
136 };
137 
138 template <typename T>
139 struct ToPyObject<
140  T, Requires<
141  cpp17::is_same_v<typename std::decay<T>::type, size_t> and
142  not cpp17::is_same_v<typename std::decay<T>::type, unsigned long> and
143  not cpp17::is_same_v<typename std::decay<T>::type, unsigned int>>> {
144  static PyObject* convert(const T& t) { return PyLong_FromSize_t(t); }
145 };
146 
147 template <typename T>
148 struct ToPyObject<
149  T, Requires<std::is_floating_point<typename std::decay<T>::type>::value>> {
150  static PyObject* convert(const T& t) { return PyFloat_FromDouble(t); }
151 };
152 
153 template <typename T, typename A>
154 struct ToPyObject<std::vector<T, A>, std::nullptr_t> {
155  static PyObject* convert(const std::vector<T, A>& t) {
156  PyObject* list = PyList_New(static_cast<long>(t.size()));
157  if (list == nullptr) {
158  throw std::runtime_error{"Failed to convert argument."};
159  }
160  for (size_t i = 0; i < t.size(); ++i) {
161  if (-1 ==
162  PyList_SetItem(list, static_cast<long>(i), to_py_object<T>(t[i]))) {
163  throw std::runtime_error{"Failed to add to PyList."};
164  }
165  }
166  return list;
167  }
168 };
169 
170 template <typename T, size_t Size>
171 struct ToPyObject<std::array<T, Size>, std::nullptr_t> {
172  static PyObject* convert(const std::array<T, Size>& t) {
173  PyObject* list = PyList_New(static_cast<long>(t.size()));
174  if (list == nullptr) {
175  throw std::runtime_error{"Failed to convert argument."};
176  }
177  for (size_t i = 0; i < Size; ++i) {
178  PyObject* value = ToPyObject<T>::convert(gsl::at(t, i));
179  if (value == nullptr) {
180  throw std::runtime_error{"Failed to convert argument."};
181  }
182  if (-1 == PyList_SetItem(list, static_cast<long>(i), value)) {
183  throw std::runtime_error{"Failed to add to PyList."};
184  }
185  }
186  return list;
187  }
188 };
189 
190 template <>
191 struct ToPyObject<DataVector, std::nullptr_t> {
192  static PyObject* convert(const DataVector& t) {
193  PyObject* npy_array = PyArray_SimpleNew( // NOLINT
194  1, (std::array<long, 1>{{static_cast<long>(t.size())}}.data()),
195  NPY_DOUBLE);
196 
197  if (npy_array == nullptr) {
198  throw std::runtime_error{"Failed to convert argument."};
199  }
200  for (size_t i = 0; i < t.size(); ++i) {
201  // clang-tidy: Do not use pointer arithmetic
202  // clang-tidy: Do not use reinterpret cast
203  const auto data = static_cast<double*>(PyArray_GETPTR1( // NOLINT
204  reinterpret_cast<PyArrayObject*>(npy_array), // NOLINT
205  static_cast<long>(i)));
206  if (data == nullptr) {
207  throw std::runtime_error{"Failed to access argument of PyArray."};
208  }
209  *data = t[i];
210  }
211  return npy_array;
212  }
213 };
214 
215 template <>
216 struct FromPyObject<long, std::nullptr_t> {
217  static long convert(PyObject* t) {
218  if (t == nullptr) {
219  throw std::runtime_error{"Received null PyObject."};
220 #if PY_MAJOR_VERSION == 2 && PY_MINOR_VERSION == 7
221  } else if (not PyInt_Check(t) and not PyLong_Check(t)) {
222 #elif PY_MAJOR_VERSION == 3
223  // clang-tidy: hicpp-signed-bitwise
224  } else if (not PyLong_Check(t)) { // NOLINT
225 #else
226  } else {
227  static_assert(false, "Only works on Python 2.7 and 3.x")
228 #endif
229  throw std::runtime_error{"Cannot convert non-long/int type to long."};
230  }
231  return PyLong_AsLong(t);
232  }
233 };
234 
235 template <>
236 struct FromPyObject<unsigned long, std::nullptr_t> {
237  static unsigned long convert(PyObject* t) {
238  if (t == nullptr) {
239  throw std::runtime_error{"Received null PyObject."};
240 #if PY_MAJOR_VERSION == 2 && PY_MINOR_VERSION == 7
241  } else if (not PyInt_Check(t) and not PyLong_Check(t)) {
242 #elif PY_MAJOR_VERSION == 3
243  // clang-tidy: hicpp-signed-bitwise
244  } else if (not PyLong_Check(t)) { // NOLINT
245 #else
246  } else {
247  static_assert(false, "Only works on Python 2.7 and 3.x");
248 #endif
249  throw std::runtime_error{"Cannot convert non-long/int type to long."};
250  }
251  return PyLong_AsUnsignedLong(t);
252  }
253 };
254 
255 template <>
256 struct FromPyObject<double, std::nullptr_t> {
257  static double convert(PyObject* t) {
258  if (t == nullptr) {
259  throw std::runtime_error{"Received null PyObject."};
260  } else if (not PyFloat_Check(t)) {
261  throw std::runtime_error{"Cannot convert non-double type to double."};
262  }
263  return PyFloat_AsDouble(t);
264  }
265 };
266 
267 template <>
268 struct FromPyObject<bool, std::nullptr_t> {
269  static bool convert(PyObject* t) {
270  if (t == nullptr) {
271  throw std::runtime_error{"Received null PyObject."};
272  } else if (not PyBool_Check(t)) {
273  throw std::runtime_error{"Cannot convert non-bool type to bool."};
274  }
275  return static_cast<bool>(PyLong_AsLong(t));
276  }
277 };
278 
279 template <>
280 struct FromPyObject<std::string, std::nullptr_t> {
281  static std::string convert(PyObject* t) {
282  if (t == nullptr) {
283  throw std::runtime_error{"Received null PyObject."};
284 #if PY_MAJOR_VERSION == 2 && PY_MINOR_VERSION == 7
285  } else if (not PyString_CheckExact(t)) {
286 #elif PY_MAJOR_VERSION == 3
287  } else if (not PyUnicode_CheckExact(t)) {
288 #else
289  } else {
290  static_assert(false, "Only works on Python 2.7 and 3.x")
291 #endif
292  throw std::runtime_error{"Cannot convert non-string type to string."};
293  }
294 #if PY_MAJOR_VERSION == 2 && PY_MINOR_VERSION == 7
295  return std::string(PyString_AsString(t));
296 #elif PY_MAJOR_VERSION == 3
297  PyObject* tascii = PyUnicode_AsASCIIString(t);
298  if (nullptr == tascii) {
299  throw std::runtime_error{"Cannot convert to ASCII string."};
300  }
301  std::string str = PyBytes_AsString(tascii);
302  Py_DECREF(tascii); // NOLINT
303  return str;
304 #else
305  static_assert(false, "Only works on Python 2.7 and 3.x")
306 #endif
307  }
308 };
309 
310 // This overload handles the case of converting from a python type of None to a
311 // void*
312 template <>
313 struct FromPyObject<void*, std::nullptr_t> {
314  static void* convert(PyObject* t) {
315  if (t == nullptr) {
316  throw std::runtime_error{"Received null PyObject."};
317  } else if (t != Py_None) {
318  throw std::runtime_error{"Cannot convert non-None type to void."};
319  }
320  return nullptr;
321  }
322 };
323 
324 template <typename T>
325 struct FromPyObject<T, Requires<tt::is_a_v<std::vector, T>>> {
326  static T convert(PyObject* p) {
327  if (p == nullptr) {
328  throw std::runtime_error{"Received null PyObject."};
329  } else if (not PyList_CheckExact(p)) {
330  throw std::runtime_error{"Cannot convert non-list type to vector."};
331  }
332  T t(static_cast<size_t>(PyList_Size(p)));
333  for (size_t i = 0; i < t.size(); ++i) {
334  PyObject* value = PyList_GetItem(p, static_cast<long>(i));
335  if (value == nullptr) {
336  throw std::runtime_error{"Failed to get argument from list."};
337  }
338  t[i] = from_py_object<typename T::value_type>(value);
339  }
340  return t;
341  }
342 };
343 
344 template <typename T>
345 struct FromPyObject<T, Requires<tt::is_std_array_v<T>>> {
346  static T convert(PyObject* p) {
347  if (p == nullptr) {
348  throw std::runtime_error{"Received null PyObject."};
349  } else if (not PyList_CheckExact(p)) {
350  throw std::runtime_error{"Cannot convert non-list type to array."};
351  }
352  T t{};
353  // clang-tidy: Do no implicitly decay an array into a pointer
354  assert(PyList_Size(p) == static_cast<long>(t.size())); // NOLINT
355  for (size_t i = 0; i < t.size(); ++i) {
356  PyObject* value = PyList_GetItem(p, static_cast<long>(i));
357  if (value == nullptr) {
358  throw std::runtime_error{"Failed to get argument from list."};
359  }
360  gsl::at(t, i) = from_py_object<typename T::value_type>(value);
361  }
362  return t;
363  }
364 };
365 template <>
366 struct FromPyObject<DataVector, std::nullptr_t> {
367  static DataVector convert(PyObject* p) {
368  if (p == nullptr) {
369  throw std::runtime_error{"Received null PyObject."};
370  }
371  // clang-tidy: c-style casts. (Expanded from macro)
372  if (not PyArray_CheckExact(p)) { // NOLINT
373  throw std::runtime_error{"Cannot convert non-array type to DataVector."};
374  }
375  // clang-tidy: reinterpret_cast
376  const auto npy_array = reinterpret_cast<PyArrayObject*>(p); // NOLINT
377  if (PyArray_TYPE(npy_array) != NPY_DOUBLE) {
378  throw std::runtime_error{
379  "Cannot convert array of non-double type to DataVector."};
380  }
381  if (PyArray_NDIM(npy_array) != 1) {
382  throw std::runtime_error{
383  "Cannot convert array of ndim != 1 to DataVector."};
384  }
385  // clang-tidy: c-style casts, pointer arithmetic. (Expanded from macro)
386  DataVector t(static_cast<size_t>(PyArray_Size(p))); // NOLINT
387  for (size_t i = 0; i < t.size(); ++i) {
388  // clang-tidy: pointer arithmetic. (Expanded from macro)
389  const auto value = static_cast<const double*>(
390  PyArray_GETPTR1(npy_array, static_cast<long>(i))); // NOLINT
391  if (value == nullptr) {
392  throw std::runtime_error{"Failed to get argument from PyArray."};
393  }
394  t[i] = *value;
395  }
396  return t;
397  }
398 };
399 
400 // This function is needed because one cannot cast an array of size_t's (used by
401 // SpECTRE) to an array of longs (used by NumPy).
402 template <size_t Size>
403 std::array<long, Size> convert_array_of_size_t_to_array_of_long(
404  const std::array<size_t, Size>& arr_of_size_t) {
405  std::array<long, Size> arr_of_long{};
406  for (size_t i = 0; i < Size; ++i) {
407  gsl::at(arr_of_long, i) = static_cast<long>(gsl::at(arr_of_size_t, i));
408  }
409  return arr_of_long;
410 }
411 
412 template <typename T, size_t Size, typename PyObj>
413 T* get_ptr_to_elem(PyObj* npy_array, const std::array<size_t, Size>& idx) {
414  auto t = static_cast<T*>(PyArray_GetPtr( // NOLINT
415  reinterpret_cast<PyArrayObject*>(npy_array), // NOLINT
416  convert_array_of_size_t_to_array_of_long(idx).data()));
417  if (t == nullptr) {
418  throw std::runtime_error{"Failed to access element of PyArray."};
419  }
420  return t;
421 }
422 
423 template <typename T>
424 T tensor_conversion_impl(PyObject* p) {
425  if (p == nullptr) {
426  throw std::runtime_error{"Received null PyObject."};
427  }
428  // clang-tidy: c-style casts. (Expanded from macro)
429  if (not PyArray_CheckExact(p)) { // NOLINT
430  throw std::runtime_error{"Cannot convert non-array type to Tensor."};
431  }
432  // clang-tidy: reinterpret_cast
433  const auto npy_array = reinterpret_cast<PyArrayObject*>(p); // NOLINT
434  if (PyArray_TYPE(npy_array) != NPY_DOUBLE) {
435  throw std::runtime_error{
436  "Cannot convert array of non-double type to Tensor."};
437  } else if (PyArray_NDIM(npy_array) != static_cast<long>(T::rank())) {
438  throw std::runtime_error{
439  "Mismatch between ndim of numpy ndarray and rank of Tensor."};
440  }
441 
442  const auto npy_array_dims = PyArray_DIMS(npy_array);
443  constexpr auto t_array_dims = T::index_dims();
444  for (size_t i = 0; i < T::rank(); ++i) {
445  if (npy_array_dims[i] != static_cast<long>(gsl::at(t_array_dims, i))) {
446  throw std::runtime_error{
447  "Mismatch between number of components of ndarray and Tensor in " +
448  std::to_string(i) + "\'th dim"};
449  }
450  }
451  auto t = make_with_value<T>(
452  *get_ptr_to_elem<typename T::type>(npy_array, make_array<T::rank()>(0ul)),
453  0.);
454  for (IndexIterator<T::rank()> index_it((Index<T::rank()>(T::index_dims())));
455  index_it; ++index_it) {
456  const auto tensor_idx = (*index_it).indices();
457  t.get(tensor_idx) =
458  *get_ptr_to_elem<typename T::type>(npy_array, tensor_idx);
459  }
460  return t;
461 }
462 
463 template <>
464 struct FromPyObject<Scalar<double>> {
465  static Scalar<double> convert(PyObject* p) {
466  if (PyFloat_Check(p)) {
467  return Scalar<double>{PyFloat_AsDouble(p)};
468  } else {
469  return tensor_conversion_impl<Scalar<double>>(p);
470  }
471  }
472 };
473 
474 template <typename T>
475 struct FromPyObject<T, Requires<tt::is_a_v<Tensor, T> and T::rank() != 0 and
476  cpp17::is_same_v<typename T::type, double>>> {
477  static T convert(PyObject* p) { return tensor_conversion_impl<T>(p); }
478 };
479 
480 template <typename T>
481 struct ToPyObject<T, Requires<tt::is_a_v<Tensor, T> and
482  cpp17::is_same_v<typename T::type, double>>> {
483  static PyObject* convert(const T& t) {
485  convert_array_of_size_t_to_array_of_long(T::index_dims());
486  // clang-tidy: cstyle casts, pointer arithmetic, implicit decay array to
487  // pointer. (Expanded from macro.)
488  PyObject* npy_array =
489  PyArray_SimpleNew(T::rank(), dims.data(), NPY_DOUBLE); // NOLINT
490  if (npy_array == nullptr) {
491  throw std::runtime_error{"Failed to create PyArray."};
492  }
493 
494  for (IndexIterator<T::rank()> index_it((Index<T::rank()>(T::index_dims())));
495  index_it; ++index_it) {
496  const auto tensor_idx = (*index_it).indices();
497  *get_ptr_to_elem<typename T::type>(npy_array, tensor_idx) =
498  t.get(tensor_idx);
499  }
500  return npy_array;
501  }
502 };
503 
504 ///\endcond
505 
506 } // namespace pypp
IndexIterator iterates over a unique set of Index.
Definition: IndexIterator.hpp:22
Contains all functions for calling python from C++.
Definition: CheckWithRandomValues.hpp:59
Defines function make_array.
constexpr bool is_std_array_v
Definition: TypeTraits.hpp:451
Defines the type alias Requires.
constexpr bool is_same_v
Variable template for is_same.
Definition: TypeTraits.hpp:221
constexpr bool is_a_v
Definition: TypeTraits.hpp:543
Defines classes for Tensor.
An integer multi-index.
Definition: Index.hpp:28
Defines functions and classes from the GSL.
PyObject * make_py_tuple(const Args &... t)
Create a python tuple from Args.
Definition: PyppFundamentals.hpp:68
Defines type traits, some of which are future STL type_traits header.
Tensor< T, Symmetry<>, index_list<> > Scalar
Scalar type.
Definition: TypeAliases.hpp:21
constexpr T & at(std::array< T, N > &arr, Size index)
Retrieve a entry from a container, with checks in Debug mode that the index being retrieved is valid...
Definition: Gsl.hpp:124
Defines IndexIterator.