Skip to content

Commit

Permalink
added a bunch more exercises to the custom type, + pytest
Browse files Browse the repository at this point in the history
much of the functions I added were me trying to get Python to crash in a specific way.  I failed, which is a good thing, because it means that EigenPy doesn't have the bug I thought it does.

BUT.  EigenPy *does* have two issues exercised in the unit tests for the custom type, issues stack-of-tasks#519  and stack-of-tasks#520 .  Additionally, this code exercises issue stack-of-tasks#521 , where I try to compute vector norms in two different ways and fail.

Additionally, I bumped the C++ standard to C++14, since Boost 1.87 didn't work correctly with only C++11, and 1.87 is now distributed by homebrew (I develop on a Mac)
  • Loading branch information
ofloveandhate committed Dec 18, 2024
1 parent c6048d3 commit cb58069
Show file tree
Hide file tree
Showing 8 changed files with 457 additions and 40 deletions.
2 changes: 1 addition & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ string(REPLACE "-pedantic" "" CMAKE_CXX_FLAGS ${CMAKE_CXX_FLAGS})

# If needed, fix CMake policy for APPLE systems
apply_default_apple_configuration()
check_minimal_cxx_standard(11 ENFORCE)
check_minimal_cxx_standard(17 ENFORCE)

if(WIN32)
set(LINK copy_if_different)
Expand Down
6 changes: 3 additions & 3 deletions examples/custom_numeric_type/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ string(REPLACE "-pedantic" "" CMAKE_CXX_FLAGS ${CMAKE_CXX_FLAGS})

# If needed, fix CMake policy for APPLE systems
apply_default_apple_configuration()
check_minimal_cxx_standard(11 ENFORCE)
check_minimal_cxx_standard(14 ENFORCE)

if(WIN32)
set(LINK copy_if_different)
Expand Down Expand Up @@ -113,9 +113,9 @@ include_directories(${MPC_INCLUDES})
# ----------------------------------------------------
# --- INCLUDE ----------------------------------------
# ----------------------------------------------------
set(${PROJECT_NAME}_HEADERS include/header.hpp)
set(${PROJECT_NAME}_HEADERS include/header.hpp include/a_class.hpp)

set(${PROJECT_NAME}_SOURCES src/src.cpp)
set(${PROJECT_NAME}_SOURCES src/src.cpp src/a_class.cpp)

add_library(${PROJECT_NAME} SHARED ${${PROJECT_NAME}_SOURCES}
${${PROJECT_NAME}_HEADERS})
Expand Down
167 changes: 148 additions & 19 deletions examples/custom_numeric_type/examplescript.py
Original file line number Diff line number Diff line change
@@ -1,44 +1,173 @@
# some code that exercises C++-exposed code using custom numeric type via EigenPy.

import sys

sys.path.append('./')

import numpy as np
import eigenpy_example_custom_numeric_type as example


def make_empty_with_conversion(num_type):
return np.array( np.empty( (3)).astype(np.int64),dtype=num_type)


def make_zeros_with_conversion(num_type):
return np.array( np.zeros( (3)).astype(np.int32),dtype=num_type) # make an array of the custom numeric type








def make_in_numpy_then_modify_in_cpp(num_type):
A = make_zeros_with_conversion(num_type)
example.set_to_ones(A)

assert(A[0] == num_type(1))

def make_in_cpp_then_modify_in_cpp_once(num_type):

A = example.make_a_vector_in_cpp(4,num_type(1)) # the second argument is used only for type dispatch
example.set_to_ones(A)

for a in A:
assert(a == num_type(1))


def make_in_cpp_then_modify_in_cpp_list(num_type):

my_list = []

for ii in range(10):
A = example.make_a_vector_in_cpp(4,num_type(1)) # the second argument is used only for type dispatch
my_list.append( A )

for A in my_list:
example.set_to_ones(A)
for a in A:
assert(a == num_type(1))

example.set_to_ones(A)


def make_then_call_function_taking_scalar_and_vector(num_type):
A = make_zeros_with_conversion(num_type)
s = num_type(3)

result = example.a_function_taking_both_a_scalar_and_a_vector(s, A)



def set_entire_array_to_one_value(num_type):
A = example.make_a_vector_in_cpp(10, num_type(0)) # again, type dispatch on the second

cst = num_type("13") / num_type("7") # 13/7 seems like a good number. why not.

example.set_all_entries_to_constant(A,cst) # all entries should become the constant, in this case 13




def class_function_with_both_arguments():
num_type = example.MpfrFloat

c = example.JustSomeClass();

A = example.make_a_vector_in_cpp(10, num_type(0)) # again, type dispatch on the second

cst = num_type("13") / num_type("7") # 13/7 seems like a good number. why not.

c.foo(cst,A) # all entries should become the constant, in this case 13
example.qwfp(cst,A)



def numpy_norm(num_type):
A = make_zeros_with_conversion(num_type)
example.set_to_ones(A)

# assert np.abs(np.linalg.norm(A) - np.sqrt(3)) < 1e-10


def numpy_manual_norm(num_type):
A = make_zeros_with_conversion(num_type)
example.set_to_ones(A)
assert np.sqrt(np.sum((A)**2)) < 1e-10
print('arst')



def expected_to_succeed(num_type):

print(f'testing {num_type} at precision {num_type.default_precision()}')

make_empty_with_conversion(num_type)
make_zeros_with_conversion(num_type)

make_in_numpy_then_modify_in_cpp(num_type)
make_in_cpp_then_modify_in_cpp_once(num_type)
make_in_cpp_then_modify_in_cpp_list(num_type)

set_entire_array_to_one_value(num_type)

make_then_call_function_taking_scalar_and_vector(num_type)

class_function_with_both_arguments()

numpy_norm(num_type)
numpy_manual_norm(num_type)













def make_empty_without_conversion(num_type):
return np.empty( (3),dtype=num_type)

def make_zeros_without_conversion(num_type):

A = np.zeros( (3),dtype=num_type) # make an array of the custom numeric type
assert(A[0] == num_type(0))

return A

def try_things(num_type):

print(f'testing {num_type}')
def expected_to_crash(num_type):
print("the following calls are expected to crash, not because they should, but because for whatever reason, eigenpy does not let us directly make numpy arrays WITHOUT converting")
make_empty_without_conversion(num_type)
make_zeros_without_conversion(num_type)

x = num_type(2) # the number 2, in variable precision as a complex number

import numpy as np

print('making array from empty WITH conversion')
A = np.array( np.empty( (3,4)).astype(np.int64),dtype=num_type)

print(A)

print('making array from zeros WITH conversion')
M = np.array( np.zeros( (3,4)).astype(np.int64),dtype=num_type) # make an array of the custom numeric type

print(M)

assert(M[0,0] == num_type(0))


example.set_to_ones(M)


assert(M[0,0] == num_type(1))
for prec in [20, 50, 100]:
example.MpfrFloat.default_precision(prec)
expected_to_succeed(example.MpfrFloat)

example.MpfrComplex.default_precision(prec)
expected_to_succeed(example.MpfrComplex)

print(M)

print('making zeros without conversion')
B = np.zeros( (4,5), dtype=num_type)
print(B)


try_things(example.MpfrFloat)
try_things(example.MpfrComplex)
# these really shouldn't crash!!!! but they do, and it's a problem. 2024.12.18
expected_to_crash(example.MpfrFloat)
expected_to_crash(example.MpfrComplex)
54 changes: 54 additions & 0 deletions examples/custom_numeric_type/include/a_class.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
#pragma once

#ifndef EXAMPLE_A_CLASS
#define EXAMPLE_A_CLASS

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

#include <boost/multiprecision/mpc.hpp>

#include <boost/multiprecision/eigen.hpp>



namespace bmp = boost::multiprecision;

using mpfr_float =
boost::multiprecision::number<boost::multiprecision::mpfr_float_backend<0>,
boost::multiprecision::et_off>;

using bmp::backends::mpc_complex_backend;
using mpfr_complex =
bmp::number<mpc_complex_backend<0>,
bmp::et_off>; // T is a variable-precision complex number with
// expression templates turned on.


class Whatevs : public boost::python::def_visitor<Whatevs>{

public:
static
void qwfp(mpfr_float const& c, Eigen::Matrix<mpfr_float,Eigen::Dynamic, Eigen::Dynamic> const& M){}
};

class JustSomeClass
{
public:
JustSomeClass(){};
~JustSomeClass() = default;

void foo(mpfr_float const& the_constant, Eigen::Matrix<mpfr_float, Eigen::Dynamic, Eigen::Dynamic> const& M){};

static int bar(JustSomeClass const& self, mpfr_float const& c, Eigen::Matrix<mpfr_float,Eigen::Dynamic, Eigen::Dynamic> const& M){return 42;}
};





void ExposeAClass();


#endif
60 changes: 45 additions & 15 deletions examples/custom_numeric_type/include/header.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ using mpfr_complex =
bmp::et_off>; // T is a variable-precision complex number with
// expression templates turned on.





void ExposeAll();
void ExposeReal();
void ExposeComplex();
Expand All @@ -39,16 +43,19 @@ namespace internal {

// a template specialization for complex numbers
// derived directly from the example for Pinnochio
template <>
struct getitem<mpfr_float> {
template <class Backend, bmp::expression_template_option ETO>
struct getitem<bmp::number<Backend, ETO>> {

typedef bmp::number<Backend, ETO> Scalar;

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

Scalar &mpfr_scalar = *static_cast<Scalar *>(data);
auto &backend = mpfr_scalar.backend();

if (backend.data()[0]._mpfr_d ==
0) // If the mpfr_scalar is not initialized, we have to init it.
if (backend.data()[0]._mpfr_d == 0) // If the mpfr_scalar is not initialized, we have to init it.
{
mpfr_scalar = mpfr_float(0);
mpfr_scalar = Scalar(0);
}
boost::python::object m(boost::ref(mpfr_scalar));
Py_INCREF(m.ptr());
Expand All @@ -61,12 +68,13 @@ struct getitem<mpfr_float> {
template <>
struct getitem<mpfr_complex> {
static PyObject *run(void *data, void * /* arr */) {
// std::cout << "getitem mpfr_complex" << std::endl;
mpfr_complex &mpfr_scalar = *static_cast<mpfr_complex *>(data);
auto &backend = mpfr_scalar.backend();

if (backend.data()[0].re->_mpfr_d == 0 ||
backend.data()[0].im->_mpfr_d ==
0) // If the mpfr_scalar is not initialized, we have to init it.
if (backend.data()[0].re[0]._mpfr_d == 0 ||
backend.data()[0].im[0]._mpfr_d == 0)
// If the mpfr_scalar is not initialized, we have to init it.
{
mpfr_scalar = mpfr_complex(0);
}
Expand Down Expand Up @@ -158,10 +166,8 @@ struct BoostNumberPythonVisitor : public boost::python::def_visitor<
template <class PyClass>
void visit(PyClass &cl) const {
cl.def(bp::init<>("Default constructor.", bp::arg("self")))
.def(bp::init<BoostNumber>("Copy constructor.",
bp::args("self", "value")))
// .def(bp::init<bool>("Copy
// constructor.",bp::args("self","value")))
.def(bp::init<BoostNumber>("Copy constructor.", bp::args("self", "value")))
.def(bp::init<int>("Copy constructor.",bp::args("self","value")))
// .def(bp::init<float>("Copy
// constructor.",bp::args("self","value")))
// .def(bp::init<double>("Copy
Expand Down Expand Up @@ -235,10 +241,11 @@ struct BoostNumberPythonVisitor : public boost::python::def_visitor<
}

static void expose(const std::string &type_name) {
bp::class_<BoostNumber>(type_name.c_str(), "", bp::no_init)
bp::class_<BoostNumber>(type_name.c_str(), "docstring here?", bp::no_init)
.def(BoostNumberPythonVisitor<BoostNumber>());

eigenpy::registerNewType<BoostNumber>();
auto code = eigenpy::registerNewType<BoostNumber>();

eigenpy::registerCommonUfunc<BoostNumber>();

#define IMPLICITLY_CONVERTIBLE(T1, T2) bp::implicitly_convertible<T1, T2>();
Expand Down Expand Up @@ -382,4 +389,27 @@ struct BoostComplexPythonVisitor
}
};






// showing we can write a function that returns an eigen vector, and then use it in Python via Eigenpy.
template<typename T>
Eigen::Matrix<T, Eigen::Dynamic, 1> make_a_vector_in_cpp(size_t length, T /* for type dispatch*/ )
{
Eigen::Matrix<T, Eigen::Dynamic,1> a;
a.resize(length,1);
for (size_t ii(0); ii<length; ++ii)
{
a(ii) = static_cast<T>(ii);

}

return a;
}


#include "a_class.hpp"

#endif
Loading

0 comments on commit cb58069

Please sign in to comment.