Python — приведение массива NumPy в / из пользовательского C ++ Matrix-класса с использованием pybind11

Я пытаюсь обернуть мой код C ++, используя pybind11, В C ++ у меня есть класс Matrix3D который действует как трехмерный массив (т.е. с формой [n,m,p]). Он имеет следующую основную подпись:

template <class T> class Matrix3D
{

public:

std::vector<T> data;
std::vector<size_t> shape;
std::vector<size_t> strides;

Matrix3D<T>();
Matrix3D<T>(std::vector<size_t>);
Matrix3D<T>(const Matrix3D<T>&);

T& operator() (int,int,int);

};

Чтобы свести к минимуму код оболочки, я хотел бы привести этот класс непосредственно к массиву NumPy и из него (с копиями проблем не возникает). Например, я хотел бы напрямую обернуть функцию следующей подписи:

Matrix3D<double> func ( const Matrix3D<double>& );

используя код обертки

#include <pybind11/pybind11.h>
#include <pybind11/stl.h>
#include <pybind11/numpy.h>

namespace py = pybind11;

PYBIND11_PLUGIN(example) {
py::module m("example", "Module description");
m.def("func", &func, "Function description" );
return m.ptr();
}

В настоящее время у меня есть другая промежуточная функция, которая принимает и возвращает py::array_t<double>, Но я хотел бы избежать написания функции-обертки для каждой функции, заменив ее некоторым шаблоном.

Это было сделано для Eigen-библиотека (для массивов и (2-D) матриц). Но код слишком сложен для меня, чтобы выводить мой собственный код. Кроме того, мне действительно нужно обернуть только один простой класс.

7

Решение

С помощью @kazemakase и @jagerman (последний через форум pybind11) Я понял это. Сам класс должен иметь конструктор, который может копировать из некоторого ввода, здесь с использованием итератора:

#include <vector>
#include <assert.h>
#include <iterator>template <class T> class Matrix3D
{
public:

std::vector<T>      data;
std::vector<size_t> shape;
std::vector<size_t> strides;

Matrix3D<T>() = default;

template<class Iterator>
Matrix3D<T>(const std::vector<size_t> &shape, Iterator first, Iterator last);
};template <class T>
template<class Iterator>
Matrix3D<T>::Matrix3D(const std::vector<size_t> &shape_, Iterator first, Iterator last)
{
shape = shape_;

assert( shape.size() == 3 );

strides.resize(3);

strides[0] = shape[2]*shape[1];
strides[1] = shape[2];
strides[2] = 1;

int size = shape[0] * shape[1] * shape[2];

assert( last-first == size );

data.resize(size);

std::copy(first, last, data.begin());
}

Чтобы непосредственно обернуть функцию следующей подписи:

Matrix3D<double> func ( const Matrix3D<double>& );

нужен следующий код оболочки

#include <pybind11/pybind11.h>
#include <pybind11/stl.h>
#include <pybind11/numpy.h>

namespace py = pybind11;

namespace pybind11 { namespace detail {
template <typename T> struct type_caster<Matrix3D<T>>
{
public:

PYBIND11_TYPE_CASTER(Matrix3D<T>, _("Matrix3D<T>"));

// Conversion part 1 (Python -> C++)
bool load(py::handle src, bool convert)
{
if ( !convert and !py::array_t<T>::check_(src) )
return false;

auto buf = py::array_t<T, py::array::c_style | py::array::forcecast>::ensure(src);
if ( !buf )
return false;

auto dims = buf.ndim();
if ( dims != 3  )
return false;

std::vector<size_t> shape(3);

for ( int i = 0 ; i < 3 ; ++i )
shape[i] = buf.shape()[i];

value = Matrix3D<T>(shape, buf.data(), buf.data()+buf.size());

return true;
}

//Conversion part 2 (C++ -> Python)
static py::handle cast(const Matrix3D<T>& src, py::return_value_policy policy, py::handle parent)
{

std::vector<size_t> shape  (3);
std::vector<size_t> strides(3);

for ( int i = 0 ; i < 3 ; ++i ) {
shape  [i] = src.shape  [i];
strides[i] = src.strides[i]*sizeof(T);
}

py::array a(std::move(shape), std::move(strides), src.data.data() );

return a.release();

}
};
}} // namespace pybind11::detail

PYBIND11_PLUGIN(example) {
py::module m("example", "Module description");
m.def("func", &func, "Function description" );
return m.ptr();
}

Обратите внимание, что перегрузка функций теперь также возможна. Например, если существует перегруженная функция со следующей подписью:

Matrix3D<int   > func ( const Matrix3D<int   >& );
Matrix3D<double> func ( const Matrix3D<double>& );

Требуется следующее определение функции-оболочки:

m.def("func", py::overload_cast<Matrix3D<int   >&>(&func), "Function description" );
m.def("func", py::overload_cast<Matrix3D<double>&>(&func), "Function description" );
3

Другие решения

Я не знаком с pybind11, но заинтересовался после прочтения этого вопроса. Из документального фильма похоже, что вам придется написать свой собственный Тип заклинателя. Это, очевидно, довольно сложная тема, но кажется выполнимой с некоторыми усилиями.

Вырезано из документации, это оболочка такого конвертера для конвертации в тип C ++ inty:

namespace pybind11 { namespace detail {
template <> struct type_caster<inty> {
public:
PYBIND11_TYPE_CASTER(inty, _("inty"));

// Conversion part 1 (Python->C++)
bool load(handle src, bool);

//Conversion part 2 (C++ -> Python)
static handle cast(inty src, return_value_policy, handle);
};
}} // namespace pybind11::detail

Кажется, все, что вам нужно сделать, это заменить inty с Matrix3D<double> и реализовать load() а также cast(),

Посмотрим, как они это сделали для Эйгена (eigen.h, строка 236 вперед):

bool load(handle src, bool) {
auto buf = array_t<Scalar>::ensure(src);
if (!buf)
return false;

auto dims = buf.ndim();
if (dims < 1 || dims > 2)
return false;

auto fits = props::conformable(buf);
if (!fits)
return false; // Non-comformable vector/matrix types

value = Eigen::Map<const Type, 0, EigenDStride>(buf.data(), fits.rows, fits.cols, fits.stride);

return true;
}

Это не выглядит слишком сложно. Сначала они проверяют, что вход имеет тип array_t<Scalar> (наверное array_t<double> в твоем случае). Затем они проверяют размеры и некоторую совместимость (вы, вероятно, можете пропустить последнее). И, наконец, создать собственную матрицу. Поскольку копирование не является проблемой, на этом этапе просто создайте новый Martix3D<double> экземпляр и заполните его данными из массива numpy.

Существуют разные реализации cast() функция для разных случаев l-значения и константности. Я предполагаю, что достаточно сделать только одну реализацию, которая создает копию данных в новом массиве numpy, если это нормально. Смотрите функцию eigen_array_cast() как вернуть массив как handle тип возврата.

Я не проверял ничего из этого, и в этом процессе может быть больше, чем кажется. Надеюсь, это послужит отправной точкой.

2

По вопросам рекламы [email protected]