stack-of-tasks / eigenpy

Efficient bindings between Numpy and Eigen using Boost.Python

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Exposing Eigen with std::complex

ofloveandhate opened this issue · comments

I'm working on exposing Eigen with custom types. My other thread is about Boost.Multiprecision numeric types, but for this thread, I'm trying to expose Eigen matrices and vectors with std::complex. I'm doing this as a precursor to exposing the variable precision types from Boost.

The main question in this thread is "what are the necessary steps to using EipenPy to expose Eigen matrices or vectors with a custom type?", and I'm using this basic type as an example that gets away from interactions with other libraries.

So far, I have a header:

// this is "eigenpy_interactions.hpp"...

#include <eigenpy/user-type.hpp>
#include <eigenpy/ufunc.hpp>


// this code derived from 
// https://github.com/stack-of-tasks/eigenpy/issues/365
// where I asked about using custom types, and @jcarpent responded with a discussion
// of an application of this in Pinnochio, a library for rigid body dynamics.
namespace eigenpy
{
  namespace internal
  {

  	// // a template specialization for std::complex numbers
  	  template <>
  	  struct getitem<bertini::dbl>
  	  {
  	   using NumT = bertini::dbl;

  	    static PyObject* run(void* data, void* /* arr */) {
  	      NumT & scalar = *static_cast<NumT*>(data);

  	      boost::python::object m(boost::ref(scalar));
  	      Py_INCREF(m.ptr());
  	      return m.ptr();
  	    }
  	  };

} // internal namespace

	template <typename Scalar>
	void registerUfunct_without_comparitors(){
		  const int type_code = Register::getTypeCode<Scalar>();

		  PyObject *numpy_str;
		#if PY_MAJOR_VERSION >= 3
		  numpy_str = PyUnicode_FromString("numpy");
		#else
		  numpy_str = PyString_FromString("numpy");
		#endif
		  PyObject *numpy;
		  numpy = PyImport_Import(numpy_str);
		  Py_DECREF(numpy_str);

		  import_ufunc();

		  // Matrix multiply
		  {
		    int types[3] = {type_code, type_code, type_code};

		    std::stringstream ss;
		    ss << "return result of multiplying two matrices of ";
		    ss << bp::type_info(typeid(Scalar)).name();
		    PyUFuncObject *ufunc =
		        (PyUFuncObject *)PyObject_GetAttrString(numpy, "matmul");
		    if (!ufunc) {
		      std::stringstream ss;
		      ss << "Impossible to define matrix_multiply for given type "
		         << bp::type_info(typeid(Scalar)).name() << std::endl;
		      eigenpy::Exception(ss.str());
		    }
		    if (PyUFunc_RegisterLoopForType((PyUFuncObject *)ufunc, type_code,
		                                    &internal::gufunc_matrix_multiply<Scalar>,
		                                    types, 0) < 0) {
		      std::stringstream ss;
		      ss << "Impossible to register matrix_multiply for given type "
		         << bp::type_info(typeid(Scalar)).name() << std::endl;
		      eigenpy::Exception(ss.str());
		    }

		    Py_DECREF(ufunc);
		  }

		  // Binary operators
		  EIGENPY_REGISTER_BINARY_UFUNC(add, type_code, Scalar, Scalar, Scalar);
		  EIGENPY_REGISTER_BINARY_UFUNC(subtract, type_code, Scalar, Scalar, Scalar);
		  EIGENPY_REGISTER_BINARY_UFUNC(multiply, type_code, Scalar, Scalar, Scalar);
		  EIGENPY_REGISTER_BINARY_UFUNC(divide, type_code, Scalar, Scalar, Scalar);

		  // Comparison operators
		  EIGENPY_REGISTER_BINARY_UFUNC(equal, type_code, Scalar, Scalar, bool);
		  EIGENPY_REGISTER_BINARY_UFUNC(not_equal, type_code, Scalar, Scalar, bool);
		  // EIGENPY_REGISTER_BINARY_UFUNC(greater, type_code, Scalar, Scalar, bool);
		  // EIGENPY_REGISTER_BINARY_UFUNC(less, type_code, Scalar, Scalar, bool);
		  // EIGENPY_REGISTER_BINARY_UFUNC(greater_equal, type_code, Scalar, Scalar, bool);
		  // EIGENPY_REGISTER_BINARY_UFUNC(less_equal, type_code, Scalar, Scalar, bool);

		  // Unary operators
		  EIGENPY_REGISTER_UNARY_UFUNC(negative, type_code, Scalar, Scalar);

		  Py_DECREF(numpy);
	}


} // eigenpy namespace

And I have a source file:

#include "eigenpy_interactions.hpp"

void ExportEigenPy(){

	ExportDoubleComplex();


}


void ExportDoubleComplex()
{
	namespace bp = boost::python;
	using T = std::complex<double>;


	class_<T>("DoubleComplex")
	.def(bp::init<>("Default constructor.",bp::arg("self")))
	.def(bp::init<T>("Copy constructor.",bp::args("self","value")))
	//        .def(bp::init<bool>("Copy constructor.",bp::args("self","value")))
	//        .def(bp::init<float>("Copy constructor.",bp::args("self","value")))
	//        .def(bp::init<double>("Copy constructor.",bp::args("self","value")))
	//        .def(bp::init<int>("Copy constructor.",bp::args("self","value")))
	//        .def(bp::init<long int>("Copy constructor.",bp::args("self","value")))
	//        .def(bp::init<unsigned int>("Copy constructor.",bp::args("self","value")))
	//        .def(bp::init<unsigned long int>("Copy constructor.",bp::args("self","value")))
	// .def(bp::init<std::string>("Constructor from a string.",bp::args("self","str_value")))
	    

	.def(bp::self +  bp::self)
	.def(bp::self += bp::self)
	.def(bp::self -  bp::self)
	.def(bp::self -= bp::self)
	.def(bp::self *  bp::self)
	.def(bp::self *= bp::self)
	.def(bp::self /  bp::self)
	.def(bp::self /= bp::self)

	// .def(bp::self <  bp::self)
	// .def(bp::self <= bp::self)
	// .def(bp::self >  bp::self)
	// .def(bp::self >= bp::self)
	.def(bp::self == bp::self)
	.def(bp::self != bp::self)
	.def(bp::self_ns::pow(bp::self_ns::self,long()));



	eigenpy::registerNewType<T>();
	eigenpy::registerUfunct_without_comparitors<T>();


	// eigenpy::registerCast<T,double>(false);
	eigenpy::registerCast<double,T>(true);
	// eigenpy::registerCast<T,float>(false);
	eigenpy::registerCast<float,T>(true);
}


The code compiles but gives me warnings:

/opt/homebrew/include/eigenpy/user-type.hpp:66:27: warning: inline function 'eigenpy::internal::SpecialMethods<std::complex<double>, 15>::getitem' is not defined [-Wundefined-inline]
  inline static PyObject* getitem(void* /*ip*/,
                          ^
/opt/homebrew/include/eigenpy/user-type.hpp:320:69: note: used here
  PyArray_GetItemFunc* getitem = &internal::SpecialMethods<Scalar>::getitem;
                                                                    ^
/opt/homebrew/include/eigenpy/user-type.hpp:68:21: warning: inline function 'eigenpy::internal::SpecialMethods<std::complex<double>, 15>::setitem' is not defined [-Wundefined-inline]
  inline static int setitem(PyObject* /*op*/, void* /*ov*/,
                    ^
/opt/homebrew/include/eigenpy/user-type.hpp:321:69: note: used here
  PyArray_SetItemFunc* setitem = &internal::SpecialMethods<Scalar>::setitem;
                                                                    ^
/opt/homebrew/include/eigenpy/user-type.hpp:73:26: warning: inline function 'eigenpy::internal::SpecialMethods<std::complex<double>, 15>::nonzero' is not defined [-Wundefined-inline]
  inline static npy_bool nonzero(
                         ^
/opt/homebrew/include/eigenpy/user-type.hpp:322:69: note: used here
  PyArray_NonzeroFunc* nonzero = &internal::SpecialMethods<Scalar>::nonzero;
                                                                    ^
/opt/homebrew/include/eigenpy/user-type.hpp:64:22: warning: inline function 'eigenpy::internal::SpecialMethods<std::complex<double>, 15>::copyswap' is not defined [-Wundefined-inline]
  inline static void copyswap(void* /*dst*/, void* /*src*/, int /*swap*/,
                     ^
/opt/homebrew/include/eigenpy/user-type.hpp:323:71: note: used here
  PyArray_CopySwapFunc* copyswap = &internal::SpecialMethods<Scalar>::copyswap;
                                                                      ^
/opt/homebrew/include/eigenpy/user-type.hpp:70:22: warning: inline function 'eigenpy::internal::SpecialMethods<std::complex<double>, 15>::copyswapn' is not defined [-Wundefined-inline]
  inline static void copyswapn(void* /*dest*/, long /*dstride*/, void* /*src*/,
                     ^
/opt/homebrew/include/eigenpy/user-type.hpp:325:42: note: used here
      &internal::SpecialMethods<Scalar>::copyswapn);
                                         ^
/opt/homebrew/include/eigenpy/user-type.hpp:75:22: warning: inline function 'eigenpy::internal::SpecialMethods<std::complex<double>, 15>::dotfunc' is not defined [-Wundefined-inline]
  inline static void dotfunc(void* /*ip0_*/, npy_intp /*is0*/, void* /*ip1_*/,
                     ^
/opt/homebrew/include/eigenpy/user-type.hpp:326:65: note: used here
  PyArray_DotFunc* dotfunc = &internal::SpecialMethods<Scalar>::dotfunc;
                                                                ^
/opt/homebrew/include/eigenpy/user-type.hpp:78:21: warning: inline function 'eigenpy::internal::SpecialMethods<std::complex<double>, 15>::fill' is not defined [-Wundefined-inline]
  inline static int fill(void* data_, npy_intp length, void* arr);
                    ^
/opt/homebrew/include/eigenpy/user-type.hpp:327:63: note: used here
  PyArray_FillFunc* fill = &internal::SpecialMethods<Scalar>::fill;
                                                              ^
/opt/homebrew/include/eigenpy/user-type.hpp:79:21: warning: inline function 'eigenpy::internal::SpecialMethods<std::complex<double>, 15>::fillwithscalar' is not defined [-Wundefined-inline]
  inline static int fillwithscalar(void* buffer_, npy_intp length, void* value,
                    ^
/opt/homebrew/include/eigenpy/user-type.hpp:329:42: note: used here
      &internal::SpecialMethods<Scalar>::fillwithscalar;

and of course these not-defined inline functions cascade to a runtime error when trying to import my library:

ImportError: dlopen(/usr/local/lib/python3.11/site-packages/_pybertini.so, 0x0002): symbol not found in flat namespace '__ZN7eigenpy8internal14SpecialMethodsINSt3__17complexIdEELi15EE7dotfuncEPvlS6_lS6_lS6_'

Conclusion / prompt:

Something isn't working correctly to provide a lookup for the specialization of getitem I provided in the header, and there are other lookups that aren't happening either.

What's the right way to specialize these SpecialMethods functions? Which ones do I need to specialize? Where can i find documentation on what these functions are supposed to do?


(after successfully exposing std::complex, I'll return to my other task: Boost.Multiprecision variable precision real and complex numbers)

I'll add one more thing before I wait for a reply: If I don't expose std::complex<double> via Eigenpy, I get this error when I try to work with an Eigen type containing complexes of doubles:

No to_python (by-value) converter found for C++ type: Eigen::Matrix<std::__1::complex<double>, -1, 1, 0, -1, 1>

So I need to expose std::complex<double> through EigenPy so that I have variable size Eigen matrices of complexes of doubles in Python. (I previously had this through minieigen, but it lacks slicing and numpy interoperability, so I'm trying to leverage what EigenPy offers 😊)

I was missing a call to eigenpy::enableEigenPy(). I added that before exposing anything through EigenPy, and now my code allows me to work with vectors of complexes of doubles. On to Boost.Multiprecision types!

Very nice you find the issue! Enjoy!