Pypp.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 function pypp::call<R,Args...>
6 
7 #pragma once
8 
9 #include <Python.h>
10 #include <boost/range/combine.hpp>
11 #include <stdexcept>
12 #include <string>
13 
14 #include "Utilities/TMPL.hpp"
15 #include "Utilities/TypeTraits.hpp"
17 
18 /// \ingroup TestingFrameworkGroup
19 /// Contains all functions for calling python from C++
20 namespace pypp {
21 namespace detail {
22 
23 template <typename R, typename = std::nullptr_t>
24 struct CallImpl {
25  template <typename... Args>
26  static R call(const std::string& module_name,
27  const std::string& function_name, const Args&... t) {
28  PyObject* module = PyImport_ImportModule(module_name.c_str());
29  if (module == nullptr) {
30  PyErr_Print();
31  throw std::runtime_error{std::string("Could not find python module.\n") +
32  module_name};
33  }
34  PyObject* func = PyObject_GetAttrString(module, function_name.c_str());
35  if (func == nullptr or not PyCallable_Check(func)) {
36  if (PyErr_Occurred()) {
37  PyErr_Print();
38  }
39  throw std::runtime_error{"Could not find python function in module.\n"};
40  }
41  PyObject* args = pypp::make_py_tuple(t...);
42  PyObject* value = PyObject_CallObject(func, args);
43  Py_DECREF(args); // NOLINT
44  if (value == nullptr) {
45  Py_DECREF(func); // NOLINT
46  Py_DECREF(module); // NOLINT
47  PyErr_Print();
48  throw std::runtime_error{"Function returned null"};
49  }
50  auto ret = from_py_object<R>(value);
51  Py_DECREF(value); // NOLINT
52  Py_DECREF(func); // NOLINT
53  Py_DECREF(module); // NOLINT
54  return ret;
55  }
56 };
57 
58 template <typename T>
59 struct convert_to_container_of_doubles {};
60 
61 template <template <class, class...> class Container, class... Ts>
62 struct convert_to_container_of_doubles<Container<DataVector, Ts...>> {
63  using type = Container<double, Ts...>;
64 };
65 
66 template <size_t Dim>
67 struct convert_to_container_of_doubles<std::array<DataVector, Dim>> {
68  using type = std::array<double, Dim>;
69 };
70 
71 template <typename T>
72 using convert_to_container_of_doubles_t =
73  typename convert_to_container_of_doubles<T>::type;
74 
75 template <typename T>
76 struct SliceContainerImpl {
77  static auto apply(const T& container_dv, const size_t slice_idx) noexcept {
78  convert_to_container_of_doubles_t<std::decay_t<decltype(container_dv)>>
79  container_double{};
80  ASSERT(slice_idx < container_dv.begin()->size(),
81  "Trying to slice DataVector of size " << container_dv.begin()->size()
82  << "with slice_idx "
83  << slice_idx);
84  for (decltype(auto) double_and_datavector_components :
85  boost::combine(container_double, container_dv)) {
86  boost::get<0>(double_and_datavector_components) =
87  boost::get<1>(double_and_datavector_components)[slice_idx];
88  }
89  return container_double;
90  }
91 };
92 
93 template <>
94 struct SliceContainerImpl<double> {
95  static double apply(const double& t, const size_t /*slice_index*/) noexcept {
96  return t;
97  }
98 };
99 
100 template <typename T>
101 decltype(auto) slice_container_of_datavectors_to_container_of_doubles(
102  const T& in, const size_t slice_index) noexcept {
103  return SliceContainerImpl<T>::apply(in, slice_index);
104 }
105 
106 template <typename R>
107 struct CallImpl<
108  R, Requires<(tt::is_a_v<Tensor, R> or tt::is_std_array_v<R>) and
109  cpp17::is_same_v<typename R::value_type, DataVector>>> {
110  template <typename... Args>
111  static R call(const std::string& module_name,
112  const std::string& function_name, const Args&... t) {
113 
114  static_assert(sizeof...(Args) > 0,
115  "Call to python which returns a Tensor of DataVectors must "
116  "pass at least one argument");
117 
118  static_assert(
119  cpp17::is_same_v<typename std::decay_t<decltype(
120  get_first_argument(t...))>::value_type,
121  DataVector>,
122  "Call to python which returns a Tensor of DataVectors must "
123  "pass a Tensor or std::array of DataVectors as its first argument.");
124 
125  PyObject* module = PyImport_ImportModule(module_name.c_str());
126  if (module == nullptr) {
127  PyErr_Print();
128  throw std::runtime_error{std::string("Could not find python module.\n") +
129  module_name};
130  }
131  PyObject* func = PyObject_GetAttrString(module, function_name.c_str());
132  if (func == nullptr or not PyCallable_Check(func)) {
133  if (PyErr_Occurred()) {
134  PyErr_Print();
135  }
136  throw std::runtime_error{"Could not find python function in module.\n"};
137  }
138 
139  const auto put_container_of_doubles_into_container_of_datavector = [](
140  auto& container_dv, const auto& container_double,
141  const size_t slice_idx) noexcept {
142  ASSERT(slice_idx < container_dv.begin()->size(),
143  "Trying to slice DataVector of size "
144  << container_dv.begin()->size() << "with slice_idx "
145  << slice_idx);
146  for (decltype(auto) datavector_and_double_components :
147  boost::combine(container_dv, container_double)) {
148  boost::get<0>(datavector_and_double_components)[slice_idx] =
149  boost::get<1>(datavector_and_double_components);
150  }
151  };
152 
153  const size_t npts = get_first_argument(t...).begin()->size();
154  auto return_container = make_with_value<R>(
156 
157  for (size_t s = 0; s < npts; ++s) {
158  PyObject* args = pypp::make_py_tuple(
159  slice_container_of_datavectors_to_container_of_doubles(t, s)...);
160  PyObject* value = PyObject_CallObject(func, args);
161  Py_DECREF(args); // NOLINT
162  if (value == nullptr) {
163  Py_DECREF(func); // NOLINT
164  Py_DECREF(module); // NOLINT
165  PyErr_Print();
166  throw std::runtime_error{"Function returned null"};
167  }
168 
169  const auto ret =
170  from_py_object<convert_to_container_of_doubles_t<R>>(value);
171  Py_DECREF(value); // NOLINT
172  put_container_of_doubles_into_container_of_datavector(return_container,
173  ret, s);
174  }
175  Py_DECREF(func); // NOLINT
176  Py_DECREF(module); // NOLINT
177  return return_container;
178  }
179 };
180 } // namespace detail
181 
182 /// Calls a Python function from a module/file with given parameters
183 ///
184 /// \param module_name name of module the function is in
185 /// \param function_name name of Python function in module
186 /// \param t the arguments to be passed to the Python function
187 /// \return the object returned by the Python function converted to a C++ type
188 ///
189 /// Custom classes can be converted between Python and C++ by overloading the
190 /// `pypp::ToPythonObject<T>` and `pypp::FromPythonObject<T>` structs for your
191 /// own types. This tells C++ how to deconstruct the Python object into
192 /// fundamental types and reconstruct the C++ object and vice-versa.
193 ///
194 /// \note In order to setup the python interpreter and add the local directories
195 /// to the path, a SetupLocalPythonEnvironment object needs to be constructed
196 /// in the local scope.
197 ///
198 /// \example
199 /// The following example calls the function `test_numeric` from the module
200 /// `pypp_py_tests` which multiplies two integers.
201 /// \snippet Test_Pypp.cpp pypp_int_test
202 /// Alternatively, this examples calls `test_vector` from `pypp_py_tests` which
203 /// converts two vectors to python lists and multiplies them pointwise.
204 /// \snippet Test_Pypp.cpp pypp_vector_test
205 ///
206 /// Pypp can also be used to take a function that performs manipulations of
207 /// NumPy arrays and apply it to either a Tensor of doubles or a Tensor of
208 /// DataVectors. This is useful for testing functions which act on Tensors
209 /// pointwise. For example, let's say we wanted to call the NumPy function which
210 /// performs \f$ v_i = A B^a C_{ia} + D^{ab} E_{iab} \f$, which is implemented
211 /// in python as
212 ///
213 /// \code{.py} def test_einsum(scalar, t_A, t_ia, t_AA, t_iaa):
214 /// return scalar * np.einsum("a,ia->i", t_A, t_ia) +
215 /// np.einsum("ab, iab->i", t_AA, t_iaa)
216 /// \endcode
217 ///
218 /// where \f$ v_i \f$ is the return tensor and
219 /// \f$ A, B^a, C_{ia},D^{ab}, E_{iab} \f$ are the input tensors respectively.
220 /// We call this function through C++ as:
221 /// \snippet Test_Pypp.cpp einsum_example
222 /// for type `T` either a `double` or `DataVector`.
223 ///
224 /// Pypp will also support testing of functions which return and operate on
225 /// `std::array`s of `DataVectors`s. To return a `std::array` of DataVectors,
226 /// the python function should return a python list of doubles.
227 ///
228 /// \note In order to return a
229 /// `Tensor<DataVector...>` from `pypp::call`, at least one
230 /// `Tensor<DataVector...>` must be taken as an argument, as the size of the
231 /// returned tensor needs to be deduced.
232 template <typename R, typename... Args>
233 R call(const std::string& module_name, const std::string& function_name,
234  const Args&... t) {
235  return detail::CallImpl<R>::call(module_name, function_name, t...);
236 }
237 } // namespace pypp
Contains all functions for calling python from C++.
Definition: CheckWithRandomValues.hpp:59
T signaling_NaN(T... args)
decltype(auto) constexpr get_first_argument(T &&t, Ts &&...) noexcept
Returns the first argument of a parameter pack.
Definition: TMPL.hpp:569
#define ASSERT(a, m)
Assert that an expression should be true.
Definition: Assert.hpp:49
constexpr bool is_std_array_v
Definition: TypeTraits.hpp:451
constexpr auto apply(F &&f, const DataBox< BoxTags > &box, Args &&... args)
Apply the function f with argument Tags TagsList from DataBox box
Definition: DataBox.hpp:1595
A collection of useful type traits.
Definition: TensorExpression.hpp:115
Definition: Determinant.hpp:11
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 converting to and from Python objects.
Stores a collection of function values.
Definition: DataVector.hpp:46
Wraps the template metaprogramming library used (brigand)
R call(const std::string &module_name, const std::string &function_name, const Args &... t)
Calls a Python function from a module/file with given parameters.
Definition: Pypp.hpp:233
typename Requires_detail::requires_impl< B >::template_error_type_failed_to_meet_requirements_on_template_parameters Requires
Express requirements on the template parameters of a function or class, replaces std::enable_if_t ...
Definition: Requires.hpp:67
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.