View on GitHub

pygen-converter

pygen-converter est un outils permettant une conversion implicite entre la librairie C++ Eigen et la librairie Python Numpy. C’est une lib avec uniquement des headers qui offre tous les bindings pour les types standards de Eigen (e.g. MatrixXd, Vector3i, ArrayXXf, …) et non-standard ou définie par l’utilisateur.

De quoi c’est composé?

Il n’y a qu’un seul header <pygen/converters.h> et il est uniquement composé de fonctions et structures templatées.

Les structures

Les structures parlent d’elles-mêmes. python_list_to_eigen_vector, python_list_to_eigen_matrix permet la conversion de liste Python vers des vecteurs/matrices Eigen. numpy_array_to_eigen_vector, numpy_array_to_eigen_matrix permet la conversion de Numpy ndarray vers des vecteurs/matrices Eigen. eigen_vector_to_numpy_array, eigen_matrix_to_numpy_array permet la conversion des vecteurs/matrices Eigen vers des Numpy ndarray. Toutes ces structures attendent comme typename un type Eigen Matrix ou Array c’est-à-dire des Eigen::Matrix<Scalar, _rows, _cols> et Eigen::Array<Scalar, _rows, _cols>Scalar est un type de base (int, std::complex<float>, etc..), _rows vaut le nombre de lignes ou Eigen::Dynamic pour les matrices à lignes dynamiques et _cols vaut le nombre de colonnes ou Eigen::Dynamic pour matrices à colonnes dynamiques.

Les fonctions

Les fonctions parlent aussi d’elles-mêmes. convertAllVector, convertAllRowVector, convertAllMatrix convertissent les types globaux des matrices, des vecteurs et des vecteurs lignes de Eigen. convertAllRowArray, convertAllColumnArray, convertAllArray convertissent la même chose mais pour les Eigen Array. Les fonctions attendent un Scalar comme typename (int, std::complex<float>, etc..).

convertVector et convertMatrix sont deux autres fonctions qui permettent de gérer les types utilisateurs (non-standard à Eigen) Il faut donner comme typename Eigen::Matrix<Scalar, _rows, _cols> ou Eigen::Array<Scalar, _rows, _cols>.

Conversion simplifiée

Il y a une enum struct appelée Converters qui peut être utilisé avec la fonction convert pour gérer plusieurs types de conversions. La structure est composée de bitflag qui sont :

  • None (Pas de conversion)
  • Matrix (Conversion de tous les types matrices)
  • Vector (Conversion de tous les types vecteurs-colonne)
  • RowVector (Conversion de tous les types vecteurs-ligne)
  • Array (Conversion de tous les types de tableau)
  • ColumnArray (Conversion de tous les types de tableau-colonne)
  • RowArray (Conversion de tous les types de tableau-ligne)
  • NoStandard (Conversion de types non standard Vector6, Matrix6, Array6)
  • NoRowMatrixConversion (Matrix | Vector)
  • AllMatrixConversion (Matrix | Vector | RowVector)
  • NoRowArrayConversion (Array | ColumnArray)
  • AllArrayConversion (Array | ColumnArray | RowArray)
  • All (AllMatrixConversion | AllArrayConversion | NoStandard)

Par exemple, pour obtenir les conversions de toutes les matrices et vecteurs de type double, il faut juste appeler

pygen::convert<double>(pygen::Converters::NoRowMatrixConversion);

Dans le cas où la conversion des liste Python vers un type Eigen n’est pas souhaitée, il faut appeler

pygen::convert<double>(pygen::Converters::NoRowMatrixConversion, false);

Utilisation

Le code minimal pour binder une librairie C++ est le suivant

#include <boost/python.hpp>
#include <boost/python/numpy.hpp>
#include <pygen/converters.h>

namespace py = boost::python;
namespace np = boost::python::numpy;

BOOST_PYTHON_MODULE(libName)
{
    Py_Initialize();
    np::initialize();

    // Code pour le binding
}

La façon la plus simple d’ajouter des conversion et d’utiliser directement la méthode convert comme ci-dessus. Il y a cependant quelques difficultés qu’il faut connaitre.

Conversions spécifiques

Pour ajouter des conversions spécifiques, il suffit d’appeler

pygen::convertMatrix<Eigen::Matrix<double, 5, 4> >();
pygen::convertVector<Eigen::Matrix<double, 5, 1> >(false);

Il est possible de passer un booléen comme argument pour activer ou non la conversion des listes Python vers des type Eigen (true par défaut).

Binding des structures

Admettons qu’il faille binder une structure C++ contenant des types Eigen comme celle-ci

struct StructToBind {
    double val;
    Eigen::MatrixXd mat;
    Eigen::Vector3f vec;
};

Pour créer les bindings :

pygen::convertMatrix(Eigen::MatrixXd);
pygen::convertVector(Eigen::Vector3f);

py::class_<StructToBind>("StructToBind")
    .def_readwrite("val", &ToBind::val)
    .add_property("mat", 
        py::make_getter(&ToBind::mat, 
            py::return_value_policy<py::copy_non_const_reference>()),
        py::make_setter(&ToBind::mat))
    .add_property("vec", 
        py::make_getter(&ToBind::vec, 
            py::return_value_policy<py::copy_non_const_reference>()),
        py::make_setter(&ToBind::vec));

Binding de fonctions spécifiques

Il y a deux types de fonctions qui posent problème lors du binding. Les fonction qui retourne une const& de type Eigen et celles qui contiennent des références vers des types Eigen.

Gérer les retours par const&

Les fonctions qui retournent un const& de type Eigen ont besoin d’être bindées avec une politique particulière. Voici une class complètement inutile à binder.

class ClassToBind {
public:
    ClassToBind() : mat_(Eigen::MatrixXd::Random(3, 4)) {}

    const Eigen::MatrixXd& getMat() const noexcept {
        return mat_;
    }

    Eigen::MatrixXd& getMatRef() {
        return mat_;
    }

    void setMat(Eigen::MatrixXd& mat) {
        mat = Eigen::MatrixXd::Random(3, 4);
        mat_ = mat;
    }

private:
    Eigen::MatrixXd mat_;
};

Le binding devient

py::class_<ClassToBind>("ClassToBind", py::init<>())
    .def("get_mat", &ClassToBind::getMat, 
        py::return_value_policy<py::copy_const_reference>());

Gérer les fonctions contenant des références de type Eigen

Les fonctions qui retournent une référence est un réel problème. Il faut les wrapper. Pour les fonctions telles que getMatRef, la meilleure façon est de d’ajouter un getter et un setter comme. De cette façon, l’utilisateur peut récupérer la matrice via le getter et la modifier via le setter.

Pour gérer generateRandomMat il faut wrapper la class.

class WrapCTP : public ClassToBind {
public:
    void setMatRef(const Eigen::MatrixXd& mat) {
        Eigen::MatrixXd& inMat = getMatRef();
        inMat = mat;
    }

    Eigen::MatrixXd wrapSetMat(Eigen::MatrixXd mat) {
        setMat(mat);
        return mat;
    }
};

et la binder avec

py::class_<WrapCTP, py::bases<ClassToBind> >("ClassToBind", py::init<>())
    .def("get_mat", &WrapCTP::getMat, 
        py::return_value_policy<py::copy_const_reference>())
    .def("get_mat_ref", &WrapCTP::getMatRef, 
        py::return_value_policy<py::copy_non_const_reference>())
    .def("set_mat_ref", &WrapCTP::setMatRef, 
        py::return_value_policy<py::copy_non_const_reference>())
    .def("set_mat", &WrapCTP::wrapSetMat);

Si generateRandomMat est une méthode statique, alors seule la fonction a besoin d’être wrappée.

static Eigen::MatrixXd wrapGenerateRandomMat(Eigen::MatrixXd mat)
{
    ClassToBind::generateRandomMat(mat);
    return mat;
}

// In the cpp bindings
py::class_<ClassToBind>("ClassToBind")
    .def("generate_random_matrix", py::make_function(wrapGenerateRandomMat));

Erreur commune et slicing

L’utilisateur Python va peut-être essayé de modifier les valeurs internes du vecteur dans StructToBind. Hélas, ceci ne va rien modifier ce qui est contre-intuitif.

In [2]: a.vec[1]
Out[2]: 5.0

In [3]: a.vec[1] = 8

In [4]: a.vec[1]
Out[4]: 5.0

C’est dû au fait que seule des copy passent entre python et c++. Le 3ème appel donne 1) Copie et converti a.vec en numpy ndarray, 2) set la 1ère valeur du vecteur copié à 8.

Pour permettre le slicing, il faut procurer des functions spécifiques

struct  WrapSTB : public StructToBind {
void set_vec(const Eigen::VectorXd& nv) {
    vec = nv;
}

void set_vec_slice(int start, Eigen::VectorXd nv) {
    if (nv.size() > vec.size() - start)
        throw std::runtime_error("Bad dimension !!!!");
    vec.segment(start, nv.size()) = nv;
}

void set_vec_splice_from_val(int index, double val) {
    if (index >= vec.size())
        throw std::domain_error("out of bounds");
    vec(index) = val;
}
};

py::class_<WrapSTB>("StructToBind", py::no_init)
 .def_readonly("vec", &WrapSTB::vec,    
    py::return_value_policy<py::copy_non_const_reference>())
 .def("set_vec", &WrapSTB::set_vec)
 .def("set_vec", &WrapSTB::set_vec_slice)
 .def("set_vec", &WrapSTB::set_vec_splice_from_val);

Pours et contres

La lib gère toutes les conversions Numpy<->Eigen de manière très simple. Du point de bue de l’utilisateur, c’est très pratique d’avoir ce genre de conversions automatiques. Les bindings sont par contre plus durs à mettre en place.

Le plus gros désaventage et qu’elle fait des copies. De ce fait, les performances en prennent un coup et il n’y a pas d’accès direct aux matrices Eigen.

D’un autre côté, elle offre la possibilité aux utilisateurs Python d’utiliser directement la library Numpy.

Pour parer aux problèmes de performances et de gestion des références, il est préférable d’utiliser minieigen. Elle permet l’accès directe aux matrices Eigen dans Python. La contrepartie est la nécessité d’utiliser une nouvelle librairie. Les utilisateurs Python ont généralement une meilleure connaissance de la librairie Numpy.