diff --git a/bindings/pyroot_experimental/PyROOT/CMakeLists.txt b/bindings/pyroot_experimental/PyROOT/CMakeLists.txt index e72ff0b49273e368dd99fe05d5df1f22d0d73895..5b6abea394885f82bdca533e602e6caf26508dc3 100644 --- a/bindings/pyroot_experimental/PyROOT/CMakeLists.txt +++ b/bindings/pyroot_experimental/PyROOT/CMakeLists.txt @@ -9,7 +9,9 @@ ############################################################################ if(dataframe) - list(APPEND PYROOT_EXTRA_PYSOURCE ROOT/pythonization/_rdataframe.py) + list(APPEND PYROOT_EXTRA_PYSOURCE + ROOT/pythonization/_rdataframe.py + ROOT/pythonization/_rtensor.py) list(APPEND PYROOT_EXTRA_SOURCE src/RDataFramePyz.cxx) endif() diff --git a/bindings/pyroot_experimental/PyROOT/python/ROOT/pythonization/_rtensor.py b/bindings/pyroot_experimental/PyROOT/python/ROOT/pythonization/_rtensor.py new file mode 100644 index 0000000000000000000000000000000000000000..fdc7e11b15b9df9c35f53afe3d4ac1f4155e07d2 --- /dev/null +++ b/bindings/pyroot_experimental/PyROOT/python/ROOT/pythonization/_rtensor.py @@ -0,0 +1,56 @@ +# Author: Stefan Wunsch CERN 02/2019 + +################################################################################ +# Copyright (C) 1995-2018, Rene Brun and Fons Rademakers. # +# All rights reserved. # +# # +# For the licensing terms see $ROOTSYS/LICENSE. # +# For the list of contributors see $ROOTSYS/README/CREDITS. # +################################################################################ + +from ROOT import pythonization +from libROOTPython import GetEndianess, GetTensorDataPointer, GetSizeOfType, AsRVec +from ROOT.pythonization._rvec import _array_interface_dtype_map + + +def get_array_interface(self): + cppname = type(self).__cppname__ + for dtype in _array_interface_dtype_map: + if not cppname.find("RTensor<{},".format(dtype)) is -1: + dtype_numpy = _array_interface_dtype_map[dtype] + dtype_size = GetSizeOfType(dtype) + endianess = GetEndianess() + shape = self.GetShape() + strides = self.GetStrides() + # Numpy breaks for data pointer of 0 even though the array is empty. + # We set the pointer to 1 but the value itself is arbitrary and never accessed. + pointer = GetTensorDataPointer(self, cppname) + if pointer == 0: + pointer == 1 + return { + "shape": tuple(s for s in shape), + "strides": tuple(s * dtype_size for s in strides), + "typestr": "{}{}{}".format(endianess, dtype_numpy, dtype_size), + "version": 3, + "data": (pointer, False) + } + + +def add_array_interface_property(klass, name): + if True in [ + not name.find("RTensor<{},".format(dtype)) is -1 for dtype in _array_interface_dtype_map + ]: + klass.__array_interface__ = property(get_array_interface) + + +@pythonization() +def pythonize_rtensor(klass, name): + # Parameters: + # klass: class to be pythonized + # name: string containing the name of the class + + if name.startswith("TMVA::Experimental::RTensor<"): + # Add numpy array interface + add_array_interface_property(klass, name) + + return True diff --git a/bindings/pyroot_experimental/PyROOT/src/PyROOTModule.cxx b/bindings/pyroot_experimental/PyROOT/src/PyROOTModule.cxx index 3e574cb48a3df5e83fb4d5a687144705b5541243..508843f9c15ce96e05d8e91c5c2e4c9f7647128c 100644 --- a/bindings/pyroot_experimental/PyROOT/src/PyROOTModule.cxx +++ b/bindings/pyroot_experimental/PyROOT/src/PyROOTModule.cxx @@ -64,6 +64,8 @@ static PyMethodDef gPyROOTMethods[] = {{(char *)"AddDirectoryWritePyz", (PyCFunc (char *)"Get endianess of the system"}, {(char *)"GetVectorDataPointer", (PyCFunction)PyROOT::GetVectorDataPointer, METH_VARARGS, (char *)"Get pointer to data of vector"}, + {(char *)"GetTensorDataPointer", (PyCFunction)PyROOT::GetTensorDataPointer, METH_VARARGS, + (char *)"Get pointer to data of RTensor"}, {(char *)"GetSizeOfType", (PyCFunction)PyROOT::GetSizeOfType, METH_VARARGS, (char *)"Get size of data-type"}, {(char *)"GetCppCallableClass", (PyCFunction)PyROOT::GetCppCallableClass, METH_VARARGS, diff --git a/bindings/pyroot_experimental/PyROOT/src/PyROOTPythonize.h b/bindings/pyroot_experimental/PyROOT/src/PyROOTPythonize.h index 26cb345735f117a3179073f619e8798719b60c5b..7ccc586e054c61b4a74d07dcbda4938d7d39fa86 100644 --- a/bindings/pyroot_experimental/PyROOT/src/PyROOTPythonize.h +++ b/bindings/pyroot_experimental/PyROOT/src/PyROOTPythonize.h @@ -42,6 +42,7 @@ PyObject *GetCppCallableClass(PyObject *self, PyObject *args); PyObject *GetEndianess(PyObject *self, PyObject *args); PyObject *GetVectorDataPointer(PyObject *self, PyObject *args); +PyObject *GetTensorDataPointer(PyObject *self, PyObject *args); PyObject *GetSizeOfType(PyObject *self, PyObject *args); PyObject *MakeNumpyDataFrame(PyObject *self, PyObject *obj); diff --git a/bindings/pyroot_experimental/PyROOT/src/PyzPythonHelpers.cxx b/bindings/pyroot_experimental/PyROOT/src/PyzPythonHelpers.cxx index 11f26e82535e2f8944c8837a73b3690011ec1f9f..eabfb8514e6b2e14e0a604644609ba1731ada65c 100644 --- a/bindings/pyroot_experimental/PyROOT/src/PyzPythonHelpers.cxx +++ b/bindings/pyroot_experimental/PyROOT/src/PyzPythonHelpers.cxx @@ -53,14 +53,13 @@ PyObject *PyROOT::GetSizeOfType(PyObject * /*self*/, PyObject *args) } //////////////////////////////////////////////////////////////////////////// -/// \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. +/// \brief Helper to get pointer to the data of an object +/// \param[in] args Arguments with Python object and data-type +/// \param[in] method Name of the method returning the pointer to the data /// /// This function returns the pointer to the data of a vector as an Python -/// integer. -PyObject *PyROOT::GetVectorDataPointer(PyObject * /*self*/, PyObject *args) +/// integer retrieved by the given method. +PyObject* GetDataPointerHelper(PyObject* args, const std::string& method) { // Get pointer of C++ object PyObject *pyobj = PyTuple_GetItem(args, 0); @@ -75,7 +74,7 @@ PyObject *PyROOT::GetVectorDataPointer(PyObject * /*self*/, PyObject *args) unsigned long long pointer = 0; std::stringstream code; code << "*((long*)" << &pointer << ") = reinterpret_cast<long>(reinterpret_cast<" << cppname << "*>(" << cppobj - << ")->data())"; + << ")->" << method << "())"; gInterpreter->Calc(code.str().c_str()); // Return pointer as integer @@ -83,6 +82,32 @@ PyObject *PyROOT::GetVectorDataPointer(PyObject * /*self*/, PyObject *args) return pypointer; } +//////////////////////////////////////////////////////////////////////////// +/// \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) +{ + return GetDataPointerHelper(args, "data"); +} + +//////////////////////////////////////////////////////////////////////////// +/// \brief Get pointer to the data of a RTensor +/// \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 RTensor as an Python +/// integer. +PyObject *PyROOT::GetTensorDataPointer(PyObject * /*self*/, PyObject *args) +{ + return GetDataPointerHelper(args, "GetData"); +} + //////////////////////////////////////////////////////////////////////////// /// \brief Get endianess of the system /// \param[in] self Always null, since this is a module function. diff --git a/bindings/pyroot_experimental/PyROOT/test/CMakeLists.txt b/bindings/pyroot_experimental/PyROOT/test/CMakeLists.txt index e886c5bbb5cd3f3b9e52d4a7648c0c751a66657a..7727e056e5e7ebf03e6043707af5cecd51d7d049 100644 --- a/bindings/pyroot_experimental/PyROOT/test/CMakeLists.txt +++ b/bindings/pyroot_experimental/PyROOT/test/CMakeLists.txt @@ -89,6 +89,9 @@ if (dataframe) ROOT_ADD_PYUNITTEST(pyroot_pyz_rdataframe_makenumpy rdataframe_makenumpy.py) endif() +# RTensor pythonizations +ROOT_ADD_PYUNITTEST(pyroot_pyz_rtensor rtensor.py) + # Passing Python callables to ROOT.TF ROOT_ADD_PYUNITTEST(pyroot_pyz_tf_pycallables tf_pycallables.py) diff --git a/bindings/pyroot_experimental/PyROOT/test/rtensor.py b/bindings/pyroot_experimental/PyROOT/test/rtensor.py new file mode 100644 index 0000000000000000000000000000000000000000..9f96b33e5c665258060fa10102172280c9e72554 --- /dev/null +++ b/bindings/pyroot_experimental/PyROOT/test/rtensor.py @@ -0,0 +1,84 @@ +import unittest +import ROOT +RTensor = ROOT.TMVA.Experimental.RTensor +import numpy as np + + +class ArrayInterface(unittest.TestCase): + """ + Test memory adoption of RTensor array interface. + """ + + # Helpers + dtypes = [ + "int", "unsigned int", "long", "long long", "Long64_t", "unsigned long", + "unsigned long long", "ULong64_t", "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): + # TODO + pass + """ + np_obj[0,0] = self.get_maximum_for_dtype(np_obj.dtype) + np_obj[0,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(len(expected_shape), len(np_obj.shape)) + for a, b in zip(expected_shape, np_obj.shape): + self.assertEqual(a, b) + + + # Tests + def test_memoryAdoption(self): + """ + Test correct adoption of different datatypes + """ + shape = ROOT.std.vector("size_t")((2, 2)) + for dtype in self.dtypes: + root_obj = RTensor(dtype)(shape) + np_obj = np.asarray(root_obj) + self.check_memory_adoption(root_obj, np_obj) + self.check_shape((2, 2), np_obj) + + def test_memoryLayout(self): + """ + Test adoption of the memory layout + """ + shape = ROOT.std.vector("size_t")((2, 2)) + x = RTensor("float")(shape) + y = np.asarray(x) + self.assertTrue(y.flags.c_contiguous) + + x = x.Transpose() + y = np.asarray(x) + self.assertTrue(y.flags.f_contiguous) + + def test_ownData(self): + """ + Test ownership of adopted numpy array + """ + shape = ROOT.std.vector("size_t")((2, 2)) + x = RTensor("float")(shape) + y = np.asarray(x) + self.assertFalse(y.flags.owndata) + + y = np.transpose(y) + self.assertFalse(y.flags.owndata) + + y = np.copy(y) + self.assertTrue(y.flags.owndata)