diff --git a/bindings/pyroot_experimental/PyROOT/CMakeLists.txt b/bindings/pyroot_experimental/PyROOT/CMakeLists.txt index b63f0f3b77178efb2ed5dd5aadd51d16d71de700..e2329f764a5df34bac173ee947e8277eaf9e42b9 100644 --- a/bindings/pyroot_experimental/PyROOT/CMakeLists.txt +++ b/bindings/pyroot_experimental/PyROOT/CMakeLists.txt @@ -11,6 +11,7 @@ set(sources src/PyROOTWrapper.cxx src/TTreePyz.cxx src/PrettyPrintingPyz.cxx + src/ArrayInterfacePyz.cxx ) file(COPY python/ROOT DESTINATION ${localruntimedir}) diff --git a/bindings/pyroot_experimental/PyROOT/python/ROOT/pythonization/_rvec.py b/bindings/pyroot_experimental/PyROOT/python/ROOT/pythonization/_rvec.py new file mode 100644 index 0000000000000000000000000000000000000000..c302381a8992d4fae68c450a5a91f83fc575c977 --- /dev/null +++ b/bindings/pyroot_experimental/PyROOT/python/ROOT/pythonization/_rvec.py @@ -0,0 +1,51 @@ +from ROOT import pythonization +from libROOTPython import GetEndianess, GetVectorDataPointer, GetSizeOfType + +_array_interface_dtypes = [ + "float", "double", "int", "long", "unsigned int", "unsigned long" +] + +_array_interface_dtype_map = { + "float": "f", + "double": "f", + "int": "i", + "long": "i", + "unsigned int": "u", + "unsigned long": "u" +} + + +def get_array_interface(self): + cppname = type(self).__cppname__ + for dtype in _array_interface_dtypes: + if cppname.endswith("<{}>".format(dtype)): + dtype_numpy = _array_interface_dtype_map[dtype] + dtype_size = GetSizeOfType(dtype) + endianess = GetEndianess() + size = self.size() + pointer = GetVectorDataPointer(self, cppname) + return { + "shape": (size, ), + "typestr": "{}{}{}".format(endianess, dtype_numpy, dtype_size), + "version": 3, + "data": (pointer, False) + } + + +def add_array_interface_property(klass, name): + if True in [ + "<{}>".format(dtype) in name for dtype in _array_interface_dtypes + ]: + klass.__array_interface__ = property(get_array_interface) + + +@pythonization +def pythonize_rvec(klass, name): + # Parameters: + # klass: class to be pythonized + # name: string containing the name of the class + + # Add numpy array interface + add_array_interface_property(klass, name) + + return True diff --git a/bindings/pyroot_experimental/PyROOT/python/ROOT/pythonization/_stl_vector.py b/bindings/pyroot_experimental/PyROOT/python/ROOT/pythonization/_stl_vector.py new file mode 100644 index 0000000000000000000000000000000000000000..7cb7cef7b3da6eba94b87cb7e7e3973710c92690 --- /dev/null +++ b/bindings/pyroot_experimental/PyROOT/python/ROOT/pythonization/_stl_vector.py @@ -0,0 +1,15 @@ +from ROOT import pythonization +from ROOT.pythonization._rvec import add_array_interface_property + + +@pythonization +def pythonize_stl_vector(klass, name): + # Parameters: + # klass: class to be pythonized + # name: string containing the name of the class + + # Add numpy array interface + # NOTE: The pythonization is reused from ROOT::VecOps::RVec + add_array_interface_property(klass, name) + + return True diff --git a/bindings/pyroot_experimental/PyROOT/src/ArrayInterfacePyz.cxx b/bindings/pyroot_experimental/PyROOT/src/ArrayInterfacePyz.cxx new file mode 100644 index 0000000000000000000000000000000000000000..4ce721edac302ce7b5f7584ab59c08425ec1cd20 --- /dev/null +++ b/bindings/pyroot_experimental/PyROOT/src/ArrayInterfacePyz.cxx @@ -0,0 +1,78 @@ +#include "CPyCppyy.h" +#include "CPPInstance.h" +#include "PyROOTPythonize.h" +#include "RConfig.h" +#include "TInterpreter.h" + +#include <sstream> + +//////////////////////////////////////////////////////////////////////////// +/// \brief Get size of C++ data-type +/// \param[in] self Always null, since this is a module function. +/// \param[in] args C++ data-type as Python string +/// +/// This function returns the length of a C++ data-type in bytes +/// as a Python integer. +PyObject *PyROOT::GetSizeOfType(PyObject * /*self*/, PyObject *args) +{ + // Get name of data-type + PyObject *pydtype = PyTuple_GetItem(args, 0); + std::string dtype = CPyCppyy_PyUnicode_AsString(pydtype); + + // Call interpreter to get size of data-type using `sizeof` + long size; + std::stringstream code; + code << "*((long*)" << &size << ") = (long)sizeof(" << dtype << ")"; + gInterpreter->Calc(code.str().c_str()); + + // Return size of data-type as integer + PyObject *pysize = PyInt_FromLong(size); + return pysize; +} + +//////////////////////////////////////////////////////////////////////////// +/// \brief Get pointer to the data of a vector +/// \param[in] self Always null, since this is a module function. +/// \param[in] args[0] Data-type of the C++ object as Python string +/// \param[in] args[1] Python representation of the C++ object. +/// +/// This function returns the pointer to the data of a vector as an Python +/// integer. +PyObject *PyROOT::GetVectorDataPointer(PyObject * /*self*/, PyObject *args) +{ + // Get pointer of C++ object + PyObject *pyobj = PyTuple_GetItem(args, 0); + auto instance = (CPyCppyy::CPPInstance *)(pyobj); + auto cppobj = instance->GetObject(); + + // Get name of C++ object as string + PyObject *pycppname = PyTuple_GetItem(args, 1); + std::string cppname = CPyCppyy_PyUnicode_AsString(pycppname); + + // Call interpreter to get pointer to data (using `data` method) + long pointer; + std::stringstream code; + code << "*((long*)" << &pointer << ") = reinterpret_cast<long>(reinterpret_cast<" << cppname << "*>(" << cppobj + << ")->data())"; + gInterpreter->Calc(code.str().c_str()); + + // Return pointer as integer + PyObject *pypointer = PyInt_FromLong(pointer); + return pypointer; +} + +//////////////////////////////////////////////////////////////////////////// +/// \brief Get endianess of the system +/// \param[in] self Always null, since this is a module function. +/// \param[out] Endianess as Python string +/// +/// This function returns endianess of the system as a Python integer. The +/// return value is either '<' or '>' for little or big endian, respectively. +PyObject *PyROOT::GetEndianess(PyObject * /* self */) +{ +#ifdef R__BYTESWAP + return CPyCppyy_PyUnicode_FromString("<"); +#else + return CPyCppyy_PyUnicode_FromString(">"); +#endif +} diff --git a/bindings/pyroot_experimental/PyROOT/src/PyROOTModule.cxx b/bindings/pyroot_experimental/PyROOT/src/PyROOTModule.cxx index 5c6db005adebeff31f09413e28c112fed8592827..a2cbbfa509e4ae6eb897f55dc2ee2becf83878c5 100644 --- a/bindings/pyroot_experimental/PyROOT/src/PyROOTModule.cxx +++ b/bindings/pyroot_experimental/PyROOT/src/PyROOTModule.cxx @@ -31,6 +31,12 @@ static PyMethodDef gPyROOTMethods[] = {{(char *)"PythonizeTTree", (PyCFunction)P (char *)"Pythonizations for class TTree"}, {(char *)"AddPrettyPrintingPyz", (PyCFunction)PyROOT::AddPrettyPrintingPyz, METH_VARARGS, (char *)"Add pretty printing pythonization"}, + {(char *)"GetEndianess", (PyCFunction)PyROOT::GetEndianess, METH_NOARGS, + (char *)"Get endianess of the system"}, + {(char *)"GetVectorDataPointer", (PyCFunction)PyROOT::GetVectorDataPointer, METH_VARARGS, + (char *)"Get pointer to data of vector"}, + {(char *)"GetSizeOfType", (PyCFunction)PyROOT::GetSizeOfType, METH_VARARGS, + (char *)"Get size of data-type"}, {NULL, NULL, 0, NULL}}; #if PY_VERSION_HEX >= 0x03000000 diff --git a/bindings/pyroot_experimental/PyROOT/src/PyROOTPythonize.h b/bindings/pyroot_experimental/PyROOT/src/PyROOTPythonize.h index c0473053efa94a0362fca162e17c174ca5ec1c8b..668418c0032d78718692876772bf0f042ee1355c 100644 --- a/bindings/pyroot_experimental/PyROOT/src/PyROOTPythonize.h +++ b/bindings/pyroot_experimental/PyROOT/src/PyROOTPythonize.h @@ -8,6 +8,9 @@ namespace PyROOT { PyObject *AddPrettyPrintingPyz(PyObject *self, PyObject *args); PyObject *PythonizeTTree(PyObject *self, PyObject *args); +PyObject *GetEndianess(PyObject *self); +PyObject *GetVectorDataPointer(PyObject *self, PyObject *args); +PyObject *GetSizeOfType(PyObject *self, PyObject *args); } // namespace PyROOT diff --git a/bindings/pyroot_experimental/PyROOT/test/CMakeLists.txt b/bindings/pyroot_experimental/PyROOT/test/CMakeLists.txt index 0a59db7d6596b8ced162bd8344466c58e671c6a7..9a3d8dac57649ce4f687a0f01e0dd0dd1ba8653a 100644 --- a/bindings/pyroot_experimental/PyROOT/test/CMakeLists.txt +++ b/bindings/pyroot_experimental/PyROOT/test/CMakeLists.txt @@ -1,5 +1,6 @@ # General pythonizations ROOT_ADD_PYUNITTEST(pyroot_pretty_printing pretty_printing.py) +ROOT_ADD_PYUNITTEST(pyroot_array_interface array_interface.py) # TTree pythonizations ROOT_ADD_PYUNITTEST(pyroot_pyz_ttree_branch_attr ttree_branch_attr.py diff --git a/bindings/pyroot_experimental/PyROOT/test/array_interface.py b/bindings/pyroot_experimental/PyROOT/test/array_interface.py new file mode 100644 index 0000000000000000000000000000000000000000..544682795feb08ac0b5694b79339324db104c9c2 --- /dev/null +++ b/bindings/pyroot_experimental/PyROOT/test/array_interface.py @@ -0,0 +1,50 @@ +import unittest +import ROOT +import numpy as np + + +class ArrayInterface(unittest.TestCase): + # Helpers + dtypes = [ + "int", "unsigned int", "long", "unsigned long", "float", "double" + ] + + def get_maximum_for_dtype(self, dtype): + if np.issubdtype(dtype, np.integer): + return np.iinfo(dtype).max + if np.issubdtype(dtype, np.floating): + return np.finfo(dtype).max + + def get_minimum_for_dtype(self, dtype): + if np.issubdtype(dtype, np.integer): + return np.iinfo(dtype).min + if np.issubdtype(dtype, np.floating): + return np.finfo(dtype).min + + def check_memory_adoption(self, root_obj, np_obj): + root_obj[0] = self.get_maximum_for_dtype(np_obj.dtype) + root_obj[1] = self.get_minimum_for_dtype(np_obj.dtype) + self.assertEqual(root_obj[0], np_obj[0]) + self.assertEqual(root_obj[1], np_obj[1]) + + def check_shape(self, expected_shape, np_obj): + self.assertEqual(expected_shape, np_obj.shape) + + # Tests + def test_RVec(self): + for dtype in self.dtypes: + root_obj = ROOT.ROOT.VecOps.RVec(dtype)(2) + np_obj = np.asarray(root_obj) + self.check_memory_adoption(root_obj, np_obj) + self.check_shape((2, ), np_obj) + + def test_STLVector(self): + for dtype in self.dtypes: + root_obj = ROOT.std.vector(dtype)(2) + np_obj = np.asarray(root_obj) + self.check_memory_adoption(root_obj, np_obj) + self.check_shape((2, ), np_obj) + + +if __name__ == '__main__': + unittest.main()