diff --git a/aidge_core/unit_tests/test_tensor_scalar.py b/aidge_core/unit_tests/test_tensor_scalar.py new file mode 100644 index 0000000000000000000000000000000000000000..c054d3b877877c01b84b20983699758087bf05d8 --- /dev/null +++ b/aidge_core/unit_tests/test_tensor_scalar.py @@ -0,0 +1,136 @@ +""" +Copyright (c) 2023 CEA-List + +This program and the accompanying materials are made available under the +terms of the Eclipse Public License 2.0 which is available at +http://www.eclipse.org/legal/epl-2.0. + +SPDX-License-Identifier: EPL-2.0 +""" + +import unittest +import numpy as np + +import aidge_core + +class test_tensor_scalar(unittest.TestCase): + """Test tensor binding for scalar (0-rank) tensors + """ + def setUp(self): + pass + def tearDown(self): + pass + + def _scalar_np_array(self, dtype=None): + return np.array(1, dtype=dtype) + + def _scalar_np(self, dtype=None): + return np.int32(1).astype(dtype) + + def test_np_array_int_to_tensor(self): + np_array = self._scalar_np_array(dtype="int8") + t = aidge_core.Tensor(np_array) + self.assertEqual(t.dtype(), aidge_core.dtype.int8) + + np_array = self._scalar_np_array(dtype="int16") + t = aidge_core.Tensor(np_array) + self.assertEqual(t.dtype(), aidge_core.dtype.int16) + + np_array = self._scalar_np_array(dtype="int32") + t = aidge_core.Tensor(np_array) + self.assertEqual(t.dtype(), aidge_core.dtype.int32) + + np_array = self._scalar_np_array(dtype="int64") + t = aidge_core.Tensor(np_array) + self.assertEqual(t.dtype(), aidge_core.dtype.int64) + + def test_np_array_uint_to_tensor(self): + np_array = self._scalar_np_array(dtype="uint8") + t = aidge_core.Tensor(np_array) + self.assertEqual(t.dtype(), aidge_core.dtype.uint8) + + np_array = self._scalar_np_array(dtype="uint16") + t = aidge_core.Tensor(np_array) + self.assertEqual(t.dtype(), aidge_core.dtype.uint16) + + np_array = self._scalar_np_array(dtype="uint32") + t = aidge_core.Tensor(np_array) + self.assertEqual(t.dtype(), aidge_core.dtype.uint32) + + np_array = self._scalar_np_array(dtype="uint64") + t = aidge_core.Tensor(np_array) + self.assertEqual(t.dtype(), aidge_core.dtype.uint64) + + def test_np_scalar_int_to_tensor(self): + np_array = self._scalar_np(dtype="int8") + t = aidge_core.Tensor(np_array) + self.assertEqual(t.dtype(), aidge_core.dtype.int8) + + np_array = self._scalar_np(dtype="int16") + t = aidge_core.Tensor(np_array) + self.assertEqual(t.dtype(), aidge_core.dtype.int16) + + np_array = self._scalar_np(dtype="int32") + t = aidge_core.Tensor(np_array) + self.assertEqual(t.dtype(), aidge_core.dtype.int32) + + np_array = self._scalar_np(dtype="int64") + t = aidge_core.Tensor(np_array) + self.assertEqual(t.dtype(), aidge_core.dtype.int64) + + def test_np_scalar_uint_to_tensor(self): + np_array = self._scalar_np(dtype="uint8") + t = aidge_core.Tensor(np_array) + self.assertEqual(t.dtype(), aidge_core.dtype.uint8) + + np_array = self._scalar_np(dtype="uint16") + t = aidge_core.Tensor(np_array) + self.assertEqual(t.dtype(), aidge_core.dtype.uint16) + + np_array = self._scalar_np(dtype="uint32") + t = aidge_core.Tensor(np_array) + self.assertEqual(t.dtype(), aidge_core.dtype.uint32) + + np_array = self._scalar_np(dtype="uint64") + t = aidge_core.Tensor(np_array) + self.assertEqual(t.dtype(), aidge_core.dtype.uint64) + + def test_np_array_float_to_tensor(self): + np_array = self._scalar_np_array(dtype="float32") + t = aidge_core.Tensor(np_array) + self.assertEqual(t.dtype(), aidge_core.dtype.float32) + np_array = self._scalar_np_array(dtype="float64") + t = aidge_core.Tensor(np_array) + self.assertEqual(t.dtype(), aidge_core.dtype.float64) + + def test_np_scalar_float_to_tensor(self): + np_array = self._scalar_np(dtype="float32") + t = aidge_core.Tensor(np_array) + self.assertEqual(t.dtype(), aidge_core.dtype.float32) + np_array = self._scalar_np(dtype="float64") + t = aidge_core.Tensor(np_array) + self.assertEqual(t.dtype(), aidge_core.dtype.float64) + + def test_getcoord_getidx_scalar(self): + np_array = self._scalar_np_array() + t = aidge_core.Tensor(np_array) + coord = t.get_coord(0) + self.assertEqual(tuple(coord), ()) + idx = t.get_idx(coord) + self.assertEqual(idx, 0) + + def test_indexing_scalar(self): + np_array = self._scalar_np_array() + t = aidge_core.Tensor(np_array) + val = t[0] + self.assertEqual(val, np_array[()]) + + def test_coord_indexing_scalar(self): + np_array = self._scalar_np_array() + t = aidge_core.Tensor(np_array) + val = t[()] + self.assertEqual(val, np_array[()]) + + +if __name__ == '__main__': + unittest.main() diff --git a/python_binding/data/pybind_Tensor.cpp b/python_binding/data/pybind_Tensor.cpp index 1d0f02a507514153621fac3dcc9681989b6f94ff..d4d6edc9ca4d51eabe0665352997f5d5469bff74 100644 --- a/python_binding/data/pybind_Tensor.cpp +++ b/python_binding/data/pybind_Tensor.cpp @@ -23,17 +23,267 @@ namespace py = pybind11; namespace Aidge { +using registrableTensor = Registrable<Tensor, + std::tuple<std::string, DataType>, + std::shared_ptr<TensorImpl>(DeviceIdx_t device, std::vector<DimSize_t> dims)>; + +using pyTensorClass = py::class_<Tensor, + std::shared_ptr<Tensor>, + Data, + registrableTensor>; + +using pyTensorRegistrableClass = py::class_<registrableTensor, + std::shared_ptr<registrableTensor>>; + +using NumpyDType = py::detail::npy_api::constants; + +// Map Numpy dtype ids to aidge datatypes. +// If a numpy dtype is not present, np array of this type is rejected. +static const std::map<NumpyDType, DataType> NumpyTypeNameAsNativeType = { + { NumpyDType::NPY_INT8_, NativeType<std::int8_t>::type }, + { NumpyDType::NPY_INT16_, NativeType<std::int16_t>::type }, + { NumpyDType::NPY_INT32_, NativeType<std::int32_t>::type }, + { NumpyDType::NPY_INT64_, NativeType<std::int64_t>::type }, + { NumpyDType::NPY_UINT8_, NativeType<std::uint8_t>::type }, + { NumpyDType::NPY_UINT16_, NativeType<std::uint16_t>::type }, + { NumpyDType::NPY_UINT32_, NativeType<std::uint32_t>::type }, + { NumpyDType::NPY_UINT64_, NativeType<std::uint64_t>::type }, + { NumpyDType::NPY_FLOAT_, NativeType<float>::type }, + { NumpyDType::NPY_DOUBLE_, NativeType<double>::type }, +}; + +// The Numpy API indexes that we need to convert bare numpy scalars +// They are not provided by the pybind API, hence we have to redo +// the API mapping for these. +// Ref for instance to the merge request proposal to add support +// for numpy scalars: https://github.com/pybind/pybind11/pull/3544/ +// If merged upstream, we will be able to remove this code. +enum NUMPY_API_Slots { + PyArray_GetNDArrayCFeatureVersion = 211, + PyArray_TypeObjectFromType = 46, + PyArray_ScalarAsCtype = 62, +}; + +// Get the Numpy API ptr, we can't reuse the implementation of pybind +// as it is private. We use the same scheme and return the pointer to the +// Numpy API array. +static void **NumpyAPIPtr() { + static void **api_ptr = []() { + py::module_ m = py::module_::import("numpy.core.multiarray"); + auto c = m.attr("_ARRAY_API"); + void **api_ptr = (void **) PyCapsule_GetPointer(c.ptr(), nullptr); + if (api_ptr == nullptr) { + AIDGE_THROW_OR_ABORT(py::import_error, "numpy binding: unable to get numpy _ARRAY_API pointer."); + } + using ftype = unsigned int (*)(); + auto version = ftype(api_ptr[NUMPY_API_Slots::PyArray_GetNDArrayCFeatureVersion])(); + if (version < 0x7) { + AIDGE_THROW_OR_ABORT(py::import_error, "numpy binding: requires numpy >= 1.7.0"); + } + return api_ptr; + }(); + return api_ptr; +} + +// Wrapper for the Numpy API PyArray_ScalarAsCtype +static void NumpyScalarAsCtype(const py::object val, void *dst_ptr) { + using ftype = void (*)(PyObject *, void *); + void **api_ptr = NumpyAPIPtr(); + ((ftype)api_ptr[NUMPY_API_Slots::PyArray_ScalarAsCtype])(val.ptr(), dst_ptr); +} + +// Wrapper for the Numpy API PyArray_TypeObjectFromType +static PyObject *NumpyTypeObjectFromType(const NumpyDType npy_dtype) { + using ftype = PyObject *(*)(int); + void **api_ptr = NumpyAPIPtr(); + auto obj = ((ftype)api_ptr[NUMPY_API_Slots::PyArray_TypeObjectFromType])(npy_dtype); + return obj; +} + +// Detects and convert (without cast) a numpy scalar of npy_dtype or returns false. +// If matches, fills the value and aidge dtype in the provided pointers. +static bool NPScalarGetValue(const py::object val_obj, const NumpyDType npy_dtype, void* dst_ptr, DataType* aidge_dtype_ptr) { + auto search_datatype = NumpyTypeNameAsNativeType.find(npy_dtype); + if (search_datatype == NumpyTypeNameAsNativeType.end()) { + return false; + } + auto pyobj_dtype = NumpyTypeObjectFromType(npy_dtype); + if (!isinstance(val_obj, pyobj_dtype)) { + return false; + } + *aidge_dtype_ptr = search_datatype->second; + NumpyScalarAsCtype(val_obj, dst_ptr); + return true; +} + +using NativeValue = union { + std::int8_t i8; std::int16_t i16; std::int32_t i32; std::int64_t i64; + std::uint8_t u8; std::uint16_t u16; std::uint32_t u32; std::uint64_t u64; + float f32; double f64; +}; + +static bool getNPScalarNativeVal(const py::object obj, NativeValue* val_ptr, DataType* aidge_dtype_ptr) { + NativeValue native_val; + DataType native_dtype; + bool found = (NPScalarGetValue(obj, NumpyDType::NPY_INT32_, &native_val.i32, &native_dtype) || + NPScalarGetValue(obj, NumpyDType::NPY_FLOAT_, &native_val.f32, &native_dtype) || + NPScalarGetValue(obj, NumpyDType::NPY_INT8_, &native_val.i8, &native_dtype) || + NPScalarGetValue(obj, NumpyDType::NPY_INT16_, &native_val.i16, &native_dtype) || + NPScalarGetValue(obj, NumpyDType::NPY_INT64_, &native_val.i64, &native_dtype) || + NPScalarGetValue(obj, NumpyDType::NPY_UINT8_, &native_val.u8, &native_dtype) || + NPScalarGetValue(obj, NumpyDType::NPY_UINT16_, &native_val.u16, &native_dtype) || + NPScalarGetValue(obj, NumpyDType::NPY_UINT32_, &native_val.u32, &native_dtype) || + NPScalarGetValue(obj, NumpyDType::NPY_UINT64_, &native_val.u64, &native_dtype) || + NPScalarGetValue(obj, NumpyDType::NPY_DOUBLE_, &native_val.f64, &native_dtype)); + if (found) { + *val_ptr = native_val; + *aidge_dtype_ptr = native_dtype; + } + return found; +} + +static bool getScalarNativeVal(const py::object obj, NativeValue* val_ptr, DataType* aidge_dtype_ptr) { + NativeValue native_val; + DataType native_dtype; + bool found; + // Try to match actual numpy scalars first in order to avoid unexpected casting + // when matching native python types as numpy does some automatic conversions + // behind the scene. + found = getNPScalarNativeVal(obj, &native_val, &native_dtype); + if (!found) { + // Then try to match int and float python scalar objects + if (py::isinstance<py::int_>(obj)) { + // Note that we use the following strategy for casting native python int: + // in order, either: int32, int64 or float32, the first that does not overflows + using caster_i32 = py::detail::type_caster<std::int32_t>; + using caster_i64 = py::detail::type_caster<std::int64_t>; + using caster_f32 = py::detail::type_caster<float>; + if (caster_i32().load(obj, false)) { + native_dtype = NativeType<std::int32_t>::type; + native_val.i32 = py::cast<std::int32_t>(obj); + } else if (caster_i64().load(obj, false)) { + native_dtype = NativeType<std::int64_t>::type; + native_val.i64 = py::cast<std::int64_t>(obj); + } else { + native_dtype = NativeType<float>::type; + native_val.f32 = py::cast<float>(obj); + } + found = true; + } else if (py::isinstance<py::float_>(obj)) { + // Note that for native python float, we cast to float32 which may loss + // precision as python floats are of type float64. + native_dtype = NativeType<float>::type; + native_val.f32 = py::cast<float>(obj); + found = true; + } + } + if (found) { + *val_ptr = native_val; + *aidge_dtype_ptr = native_dtype; + } + return found; +} + +static void getConservativeNativeVal(const py::object obj, NativeValue *val_ptr, DataType * aidge_dtype_ptr) { + NativeValue native_val; + DataType native_dtype; + bool found; + found = getNPScalarNativeVal(obj, &native_val, &native_dtype); + if (!found) { + if (py::isinstance<py::int_>(obj)) { + // Note that for conservative cast we use our largests int types in order + // and otherwise fallback to double, i.e.: int64, then uint64, then double + using caster_i64 = py::detail::type_caster<std::int64_t>; + using caster_u64 = py::detail::type_caster<std::uint64_t>; + if (caster_i64().load(obj, false)) { + native_dtype = NativeType<std::int64_t>::type; + native_val.i64 = py::cast<std::int64_t>(obj); + } else if (caster_u64().load(obj, false)) { + native_dtype = NativeType<std::uint64_t>::type; + native_val.u64 = py::cast<std::uint64_t>(obj); + } else { + native_dtype = NativeType<double>::type; + native_val.f64 = py::cast<double>(obj); + } + found = true; + } else if (py::isinstance<py::float_>(obj)) { + // Note that for conservative cast we use double which is our larger float + native_dtype = NativeType<double>::type; + native_val.f64 = py::cast<double>(obj); + found = true; + } + } + if (!found) { + AIDGE_THROW_OR_ABORT(py::value_error, "Unsupported python type passed as scalar"); + } + *val_ptr = native_val; + *aidge_dtype_ptr = native_dtype; +} template<typename T> -void addCtor(py::class_<Tensor, - std::shared_ptr<Tensor>, - Data, - Registrable<Tensor, - std::tuple<std::string, DataType>, - std::shared_ptr<TensorImpl>(DeviceIdx_t device, std::vector<DimSize_t> dims)>>& mTensor){ - mTensor.def(py::init([]( - py::array_t<T, py::array::c_style | py::array::forcecast> b, - std::string backend = "cpu") { +static T castToNativeType(const py::object val_obj) { + NativeValue val; + DataType dtype; + getConservativeNativeVal(val_obj, &val, &dtype); + switch (dtype) { + case DataType::Int8: + return (T)val.i8; + case DataType::Int16: + return (T)val.i16; + case DataType::Int32: + return (T)val.i32; + case DataType::Int64: + return (T)val.i64; + case DataType::UInt8: + return (T)val.u8; + case DataType::UInt16: + return (T)val.u16; + case DataType::UInt32: + return (T)val.u32; + case DataType::UInt64: + return (T)val.u64; + case DataType::Float32: + return (T)val.f32; + case DataType::Float64: + return (T)val.f64; + } + AIDGE_THROW_OR_ABORT(py::cast_error, "Unexpected ly missing conversion to scalar type"); +} + +static void addScalarCtor(pyTensorClass& mTensor) { + // Contructor based on bare py::object in order to match either + // python scalars (int, float) or numpy scalars (np.int32, np.int64, ...). + // There is a merge request to support numpy scalars in pybind, through py::numpy_scalar<T> + // though it is not merged: https://github.com/pybind/pybind11/pull/3544/. + // Hence we use some helper functions defined above to try matching the different numpy scalar types. + mTensor.def(py::init([](py::object obj, + const std::string backend="cpu") { + NativeValue native_val; + DataType native_dtype; + bool found = getScalarNativeVal(obj, &native_val, &native_dtype); + if (!found) { + AIDGE_THROW_OR_ABORT(py::value_error, "Unsupported python type passed to Tensor constructor"); + } + Tensor* newTensor = new Tensor(); + newTensor->setDataType(native_dtype); + const std::vector<DimSize_t> input_dims(0); + newTensor->resize(input_dims); + std::set<std::string> availableBackends = Tensor::getAvailableBackends(); + if (availableBackends.find(backend) != availableBackends.end()){ + newTensor->setBackend(backend); + newTensor->getImpl()->copyFromHost(static_cast<void *>(&native_val), newTensor->size()); + }else{ + AIDGE_THROW_OR_ABORT(py::value_error, "Could not find backend {}, verify you have `import aidge_backend_{}`.\n", backend, backend); + } + + return newTensor; + }), py::arg("val"), py::arg("backend")="cpu", py::kw_only()); +} + +template<typename T> +void addArrayCtor(pyTensorClass& mTensor) { + mTensor.def(py::init([](const py::array_t<T, py::array::c_style|py::array::forcecast> b, + const std::string backend = "cpu") { /* Request a buffer descriptor from Python */ py::buffer_info info = b.request(); Tensor* newTensor = new Tensor(); @@ -44,37 +294,23 @@ void addCtor(py::class_<Tensor, std::set<std::string> availableBackends = Tensor::getAvailableBackends(); if (availableBackends.find(backend) != availableBackends.end()){ newTensor->setBackend(backend); - newTensor->getImpl()->copyFromHost(static_cast<T*>(info.ptr), newTensor->size()); + newTensor->getImpl()->copyFromHost(static_cast<const T*>(info.ptr), newTensor->size()); }else{ AIDGE_THROW_OR_ABORT(py::value_error, "Could not find backend {}, verify you have `import aidge_backend_{}`.\n", backend, backend); } return newTensor; - }), py::arg("array"), py::arg("backend")="cpu") - .def(py::init<T>(), py::arg("val")) - .def("__setitem__", (void (Tensor::*)(std::size_t, T)) &Tensor::set) - .def("__setitem__", (void (Tensor::*)(std::vector<std::size_t>, T)) &Tensor::set) - ; + }), py::arg("array"), py::arg("backend")="cpu", py::kw_only()); } void init_Tensor(py::module& m){ - py::class_<Registrable<Tensor, - std::tuple<std::string, DataType>, - std::shared_ptr<TensorImpl>(DeviceIdx_t device, std::vector<DimSize_t> dims)>, - std::shared_ptr<Registrable<Tensor, - std::tuple<std::string, DataType>, - std::shared_ptr<TensorImpl>(DeviceIdx_t device, std::vector<DimSize_t> dims)>>>(m,"TensorRegistrable"); - - py::class_<Tensor, std::shared_ptr<Tensor>, - Data, - Registrable<Tensor, - std::tuple<std::string, DataType>, - std::shared_ptr<TensorImpl>(DeviceIdx_t device, std::vector<DimSize_t> dims)>> pyClassTensor + pyTensorRegistrableClass(m,"TensorRegistrable"); + + pyTensorClass pyClassTensor (m,"Tensor", py::multiple_inheritance(), py::buffer_protocol()); - pyClassTensor.def(py::init<>()) - .def(py::init<const std::vector<std::size_t>&>(), py::arg("dims")) + pyClassTensor .def(py::self + py::self) .def(py::self - py::self) .def(py::self * py::self) @@ -107,7 +343,7 @@ void init_Tensor(py::module& m){ .def("__len__", [](Tensor& b) -> size_t{ return b.size(); }) - .def("__getitem__", [](Tensor& b, size_t idx)-> py::object { + .def("__getitem__", [](const Tensor& b, const size_t idx)-> py::object { if (idx >= b.size()) throw py::index_error(); switch(b.dataType()){ case DataType::Float64: @@ -126,11 +362,15 @@ void init_Tensor(py::module& m){ return py::cast(b.get<std::uint8_t>(idx)); case DataType::UInt16: return py::cast(b.get<std::uint16_t>(idx)); + case DataType::UInt32: + return py::cast(b.get<std::uint32_t>(idx)); + case DataType::UInt64: + return py::cast(b.get<std::uint64_t>(idx)); default: return py::none(); } }) - .def("__getitem__", [](Tensor& b, std::vector<size_t> coordIdx)-> py::object { + .def("__getitem__", [](const Tensor& b, const std::vector<size_t>& coordIdx)-> py::object { if (b.getIdx(coordIdx) >= b.size()) throw py::index_error(); switch(b.dataType()){ case DataType::Float64: @@ -149,10 +389,90 @@ void init_Tensor(py::module& m){ return py::cast(b.get<std::uint8_t>(coordIdx)); case DataType::UInt16: return py::cast(b.get<std::uint16_t>(coordIdx)); + case DataType::UInt32: + return py::cast(b.get<std::uint32_t>(coordIdx)); + case DataType::UInt64: + return py::cast(b.get<std::uint64_t>(coordIdx)); default: return py::none(); } }) + .def("__setitem__", [](Tensor& b, const std::size_t idx, const py::object val) { + if (idx >= b.size()) throw py::index_error(); + switch(b.dataType()){ + case DataType::Float64: + b.set(idx, castToNativeType<double>(val)); + break; + case DataType::Float32: + b.set(idx, castToNativeType<float>(val)); + break; + case DataType::Int8: + b.set(idx, castToNativeType<std::int8_t>(val)); + break; + case DataType::Int16: + b.set(idx, castToNativeType<std::int16_t>(val)); + break; + case DataType::Int32: + b.set(idx, castToNativeType<std::int32_t>(val)); + break; + case DataType::Int64: + b.set(idx, castToNativeType<std::int64_t>(val)); + break; + case DataType::UInt8: + b.set(idx, castToNativeType<std::uint8_t>(val)); + break; + case DataType::UInt16: + b.set(idx, castToNativeType<std::uint16_t>(val)); + break; + case DataType::UInt32: + b.set(idx, castToNativeType<std::uint32_t>(val)); + break; + case DataType::UInt64: + b.set(idx, castToNativeType<std::uint64_t>(val)); + break; + default: + break; + + } + }) + .def("__setitem__", [](Tensor& b, const std::vector<size_t>& coordIdx, const py::object val) { + if (b.getIdx(coordIdx) >= b.size()) throw py::index_error(); + switch(b.dataType()){ + case DataType::Float64: + b.set(coordIdx, castToNativeType<double>(val)); + break; + case DataType::Float32: + b.set(coordIdx, castToNativeType<float>(val)); + break; + case DataType::Int8: + b.set(coordIdx, castToNativeType<std::int8_t>(val)); + break; + case DataType::Int16: + b.set(coordIdx, castToNativeType<std::int16_t>(val)); + break; + case DataType::Int32: + b.set(coordIdx, castToNativeType<std::int32_t>(val)); + break; + case DataType::Int64: + b.set(coordIdx, castToNativeType<std::int64_t>(val)); + break; + case DataType::UInt8: + b.set(coordIdx, castToNativeType<std::uint8_t>(val)); + break; + case DataType::UInt16: + b.set(coordIdx, castToNativeType<std::uint16_t>(val)); + break; + case DataType::UInt32: + b.set(coordIdx, castToNativeType<std::uint32_t>(val)); + break; + case DataType::UInt64: + b.set(coordIdx, castToNativeType<std::uint64_t>(val)); + break; + default: + break; + + } + }) .def_buffer([](Tensor& b) -> py::buffer_info { const std::shared_ptr<TensorImpl>& tensorImpl = b.getImpl(); @@ -194,6 +514,12 @@ void init_Tensor(py::module& m){ case DataType::UInt16: dataFormatDescriptor = py::format_descriptor<std::uint16_t>::format(); break; + case DataType::UInt32: + dataFormatDescriptor = py::format_descriptor<std::uint32_t>::format(); + break; + case DataType::UInt64: + dataFormatDescriptor = py::format_descriptor<std::uint64_t>::format(); + break; default: throw py::value_error("Unsupported data format"); } @@ -208,14 +534,55 @@ void init_Tensor(py::module& m){ ); }); - // TODO : If the ctor with the right data type does not exist, pybind will always convert the data to INT ! - // Need to find a way to avoid this ! - addCtor<std::int32_t>(pyClassTensor); - addCtor<std::int64_t>(pyClassTensor); - addCtor<float>(pyClassTensor); -// #if SIZE_MAX != 0xFFFFFFFF - addCtor<double>(pyClassTensor); -// #endif + // + // Constructors overloads follow + // The implemented python constructor interface is: + // __init__(self, val: float|int|nd.ndarray = None, backend: str = "cpu", *, dims: list|tuple = None): + // + // Where: + // - if no arg is specified we get an undefined Tensor (no dims, no val); + // - if only dims is specified, will create an uninitialized tensor of the given dims and dtype float32; + // - otherwise if val is specified, dims is ignored and if val is a: + // - scalar: it will create a 0-rank scalar tensor of dtype: + // - if val is float: float32 + // - if val is int: in this order: int32, int64 or float32 (the firsts which doe not overflows) + // - np.ndarray of a given np.dtype: it will create an equivalent tensor of dtype == np.dtype when supported + // - np.dtype scalar: it will create an equivalent scalar tensor of dtype == np.dtype when supported + // + // In order to implement this, we provide several overloads which are carefully ordered in order to fullfil + // the above requirements. + // + + // Undefined Tensor + pyClassTensor.def(py::init<>()); + + // Uninitialized tensor of given dims and dtype float32 + // Note that we force dims to be a keyword only argument + pyClassTensor.def(py::init<const std::vector<std::size_t>&>(), py::kw_only(), py::arg("dims")); + + // N-D array tensors (including 0-D for from numpy 0-rank arrays) + // Note that in this case we have to define all supported Tensor dtypes + // otherwise the dtypes will be promoted by pybind unexpectedly. + // Note that these overloads must appear before the scalars overloads below + // otherwise pybind will try to demote 0-D arrays to scalar without preserving the + // np array dtype. + // TODO: Note that the list of supported numpy dtype is possibly incomplete there + // TODO: need to add some conversion functions to target dtypes not supported by numpy + // such as int4, int7, bfloat, ... + addArrayCtor<std::int8_t>(pyClassTensor); + addArrayCtor<std::int16_t>(pyClassTensor); + addArrayCtor<std::int32_t>(pyClassTensor); + addArrayCtor<std::int64_t>(pyClassTensor); + addArrayCtor<std::uint8_t>(pyClassTensor); + addArrayCtor<std::uint16_t>(pyClassTensor); + addArrayCtor<std::uint32_t>(pyClassTensor); + addArrayCtor<std::uint64_t>(pyClassTensor); + addArrayCtor<float>(pyClassTensor); + addArrayCtor<double>(pyClassTensor); + + // Numpy Scalar argument + // Handles python scalars and numpy scalars with a single overload + addScalarCtor(pyClassTensor); } }