Skip to content

Commit cb58069

Browse files
committed
added a bunch more exercises to the custom type, + pytest
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)
1 parent c6048d3 commit cb58069

File tree

8 files changed

+457
-40
lines changed

8 files changed

+457
-40
lines changed

CMakeLists.txt

+1-1
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ string(REPLACE "-pedantic" "" CMAKE_CXX_FLAGS ${CMAKE_CXX_FLAGS})
9898

9999
# If needed, fix CMake policy for APPLE systems
100100
apply_default_apple_configuration()
101-
check_minimal_cxx_standard(11 ENFORCE)
101+
check_minimal_cxx_standard(17 ENFORCE)
102102

103103
if(WIN32)
104104
set(LINK copy_if_different)

examples/custom_numeric_type/CMakeLists.txt

+3-3
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ string(REPLACE "-pedantic" "" CMAKE_CXX_FLAGS ${CMAKE_CXX_FLAGS})
6161

6262
# If needed, fix CMake policy for APPLE systems
6363
apply_default_apple_configuration()
64-
check_minimal_cxx_standard(11 ENFORCE)
64+
check_minimal_cxx_standard(14 ENFORCE)
6565

6666
if(WIN32)
6767
set(LINK copy_if_different)
@@ -113,9 +113,9 @@ include_directories(${MPC_INCLUDES})
113113
# ----------------------------------------------------
114114
# --- INCLUDE ----------------------------------------
115115
# ----------------------------------------------------
116-
set(${PROJECT_NAME}_HEADERS include/header.hpp)
116+
set(${PROJECT_NAME}_HEADERS include/header.hpp include/a_class.hpp)
117117

118-
set(${PROJECT_NAME}_SOURCES src/src.cpp)
118+
set(${PROJECT_NAME}_SOURCES src/src.cpp src/a_class.cpp)
119119

120120
add_library(${PROJECT_NAME} SHARED ${${PROJECT_NAME}_SOURCES}
121121
${${PROJECT_NAME}_HEADERS})
+148-19
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,173 @@
1+
# some code that exercises C++-exposed code using custom numeric type via EigenPy.
2+
13
import sys
24

35
sys.path.append('./')
46

7+
import numpy as np
58
import eigenpy_example_custom_numeric_type as example
69

710

11+
def make_empty_with_conversion(num_type):
12+
return np.array( np.empty( (3)).astype(np.int64),dtype=num_type)
13+
14+
15+
def make_zeros_with_conversion(num_type):
16+
return np.array( np.zeros( (3)).astype(np.int32),dtype=num_type) # make an array of the custom numeric type
17+
18+
19+
20+
21+
22+
23+
24+
25+
def make_in_numpy_then_modify_in_cpp(num_type):
26+
A = make_zeros_with_conversion(num_type)
27+
example.set_to_ones(A)
28+
29+
assert(A[0] == num_type(1))
30+
31+
def make_in_cpp_then_modify_in_cpp_once(num_type):
32+
33+
A = example.make_a_vector_in_cpp(4,num_type(1)) # the second argument is used only for type dispatch
34+
example.set_to_ones(A)
35+
36+
for a in A:
37+
assert(a == num_type(1))
38+
39+
40+
def make_in_cpp_then_modify_in_cpp_list(num_type):
41+
42+
my_list = []
43+
44+
for ii in range(10):
45+
A = example.make_a_vector_in_cpp(4,num_type(1)) # the second argument is used only for type dispatch
46+
my_list.append( A )
47+
48+
for A in my_list:
49+
example.set_to_ones(A)
50+
for a in A:
51+
assert(a == num_type(1))
52+
53+
example.set_to_ones(A)
54+
55+
56+
def make_then_call_function_taking_scalar_and_vector(num_type):
57+
A = make_zeros_with_conversion(num_type)
58+
s = num_type(3)
59+
60+
result = example.a_function_taking_both_a_scalar_and_a_vector(s, A)
61+
62+
63+
64+
def set_entire_array_to_one_value(num_type):
65+
A = example.make_a_vector_in_cpp(10, num_type(0)) # again, type dispatch on the second
66+
67+
cst = num_type("13") / num_type("7") # 13/7 seems like a good number. why not.
68+
69+
example.set_all_entries_to_constant(A,cst) # all entries should become the constant, in this case 13
70+
71+
72+
73+
74+
def class_function_with_both_arguments():
75+
num_type = example.MpfrFloat
76+
77+
c = example.JustSomeClass();
78+
79+
A = example.make_a_vector_in_cpp(10, num_type(0)) # again, type dispatch on the second
80+
81+
cst = num_type("13") / num_type("7") # 13/7 seems like a good number. why not.
82+
83+
c.foo(cst,A) # all entries should become the constant, in this case 13
84+
example.qwfp(cst,A)
85+
86+
87+
88+
def numpy_norm(num_type):
89+
A = make_zeros_with_conversion(num_type)
90+
example.set_to_ones(A)
91+
92+
# assert np.abs(np.linalg.norm(A) - np.sqrt(3)) < 1e-10
93+
94+
95+
def numpy_manual_norm(num_type):
96+
A = make_zeros_with_conversion(num_type)
97+
example.set_to_ones(A)
98+
assert np.sqrt(np.sum((A)**2)) < 1e-10
99+
print('arst')
100+
101+
102+
103+
def expected_to_succeed(num_type):
104+
105+
print(f'testing {num_type} at precision {num_type.default_precision()}')
106+
107+
make_empty_with_conversion(num_type)
108+
make_zeros_with_conversion(num_type)
109+
110+
make_in_numpy_then_modify_in_cpp(num_type)
111+
make_in_cpp_then_modify_in_cpp_once(num_type)
112+
make_in_cpp_then_modify_in_cpp_list(num_type)
113+
114+
set_entire_array_to_one_value(num_type)
115+
116+
make_then_call_function_taking_scalar_and_vector(num_type)
117+
118+
class_function_with_both_arguments()
119+
120+
numpy_norm(num_type)
121+
numpy_manual_norm(num_type)
122+
123+
124+
125+
126+
127+
128+
129+
130+
131+
132+
133+
134+
135+
def make_empty_without_conversion(num_type):
136+
return np.empty( (3),dtype=num_type)
137+
138+
def make_zeros_without_conversion(num_type):
139+
140+
A = np.zeros( (3),dtype=num_type) # make an array of the custom numeric type
141+
assert(A[0] == num_type(0))
142+
143+
return A
8144

9-
def try_things(num_type):
10145

11-
print(f'testing {num_type}')
146+
def expected_to_crash(num_type):
147+
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")
148+
make_empty_without_conversion(num_type)
149+
make_zeros_without_conversion(num_type)
12150

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

15-
import numpy as np
16152

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

20-
print(A)
21154

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

25-
print(M)
26156

27-
assert(M[0,0] == num_type(0))
28157

29158

30-
example.set_to_ones(M)
31159

32160

33-
assert(M[0,0] == num_type(1))
161+
for prec in [20, 50, 100]:
162+
example.MpfrFloat.default_precision(prec)
163+
expected_to_succeed(example.MpfrFloat)
34164

165+
example.MpfrComplex.default_precision(prec)
166+
expected_to_succeed(example.MpfrComplex)
35167

36-
print(M)
37168

38-
print('making zeros without conversion')
39-
B = np.zeros( (4,5), dtype=num_type)
40-
print(B)
41169

42170

43-
try_things(example.MpfrFloat)
44-
try_things(example.MpfrComplex)
171+
# these really shouldn't crash!!!! but they do, and it's a problem. 2024.12.18
172+
expected_to_crash(example.MpfrFloat)
173+
expected_to_crash(example.MpfrComplex)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
#pragma once
2+
3+
#ifndef EXAMPLE_A_CLASS
4+
#define EXAMPLE_A_CLASS
5+
6+
#include <eigenpy/eigenpy.hpp>
7+
#include <eigenpy/user-type.hpp>
8+
#include <eigenpy/ufunc.hpp>
9+
10+
#include <boost/multiprecision/mpc.hpp>
11+
12+
#include <boost/multiprecision/eigen.hpp>
13+
14+
15+
16+
namespace bmp = boost::multiprecision;
17+
18+
using mpfr_float =
19+
boost::multiprecision::number<boost::multiprecision::mpfr_float_backend<0>,
20+
boost::multiprecision::et_off>;
21+
22+
using bmp::backends::mpc_complex_backend;
23+
using mpfr_complex =
24+
bmp::number<mpc_complex_backend<0>,
25+
bmp::et_off>; // T is a variable-precision complex number with
26+
// expression templates turned on.
27+
28+
29+
class Whatevs : public boost::python::def_visitor<Whatevs>{
30+
31+
public:
32+
static
33+
void qwfp(mpfr_float const& c, Eigen::Matrix<mpfr_float,Eigen::Dynamic, Eigen::Dynamic> const& M){}
34+
};
35+
36+
class JustSomeClass
37+
{
38+
public:
39+
JustSomeClass(){};
40+
~JustSomeClass() = default;
41+
42+
void foo(mpfr_float const& the_constant, Eigen::Matrix<mpfr_float, Eigen::Dynamic, Eigen::Dynamic> const& M){};
43+
44+
static int bar(JustSomeClass const& self, mpfr_float const& c, Eigen::Matrix<mpfr_float,Eigen::Dynamic, Eigen::Dynamic> const& M){return 42;}
45+
};
46+
47+
48+
49+
50+
51+
void ExposeAClass();
52+
53+
54+
#endif

examples/custom_numeric_type/include/header.hpp

+45-15
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,10 @@ using mpfr_complex =
2525
bmp::et_off>; // T is a variable-precision complex number with
2626
// expression templates turned on.
2727

28+
29+
30+
31+
2832
void ExposeAll();
2933
void ExposeReal();
3034
void ExposeComplex();
@@ -39,16 +43,19 @@ namespace internal {
3943

4044
// a template specialization for complex numbers
4145
// derived directly from the example for Pinnochio
42-
template <>
43-
struct getitem<mpfr_float> {
46+
template <class Backend, bmp::expression_template_option ETO>
47+
struct getitem<bmp::number<Backend, ETO>> {
48+
49+
typedef bmp::number<Backend, ETO> Scalar;
50+
4451
static PyObject *run(void *data, void * /* arr */) {
45-
mpfr_float &mpfr_scalar = *static_cast<mpfr_float *>(data);
52+
53+
Scalar &mpfr_scalar = *static_cast<Scalar *>(data);
4654
auto &backend = mpfr_scalar.backend();
4755

48-
if (backend.data()[0]._mpfr_d ==
49-
0) // If the mpfr_scalar is not initialized, we have to init it.
56+
if (backend.data()[0]._mpfr_d == 0) // If the mpfr_scalar is not initialized, we have to init it.
5057
{
51-
mpfr_scalar = mpfr_float(0);
58+
mpfr_scalar = Scalar(0);
5259
}
5360
boost::python::object m(boost::ref(mpfr_scalar));
5461
Py_INCREF(m.ptr());
@@ -61,12 +68,13 @@ struct getitem<mpfr_float> {
6168
template <>
6269
struct getitem<mpfr_complex> {
6370
static PyObject *run(void *data, void * /* arr */) {
71+
// std::cout << "getitem mpfr_complex" << std::endl;
6472
mpfr_complex &mpfr_scalar = *static_cast<mpfr_complex *>(data);
6573
auto &backend = mpfr_scalar.backend();
6674

67-
if (backend.data()[0].re->_mpfr_d == 0 ||
68-
backend.data()[0].im->_mpfr_d ==
69-
0) // If the mpfr_scalar is not initialized, we have to init it.
75+
if (backend.data()[0].re[0]._mpfr_d == 0 ||
76+
backend.data()[0].im[0]._mpfr_d == 0)
77+
// If the mpfr_scalar is not initialized, we have to init it.
7078
{
7179
mpfr_scalar = mpfr_complex(0);
7280
}
@@ -158,10 +166,8 @@ struct BoostNumberPythonVisitor : public boost::python::def_visitor<
158166
template <class PyClass>
159167
void visit(PyClass &cl) const {
160168
cl.def(bp::init<>("Default constructor.", bp::arg("self")))
161-
.def(bp::init<BoostNumber>("Copy constructor.",
162-
bp::args("self", "value")))
163-
// .def(bp::init<bool>("Copy
164-
// constructor.",bp::args("self","value")))
169+
.def(bp::init<BoostNumber>("Copy constructor.", bp::args("self", "value")))
170+
.def(bp::init<int>("Copy constructor.",bp::args("self","value")))
165171
// .def(bp::init<float>("Copy
166172
// constructor.",bp::args("self","value")))
167173
// .def(bp::init<double>("Copy
@@ -235,10 +241,11 @@ struct BoostNumberPythonVisitor : public boost::python::def_visitor<
235241
}
236242

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

241-
eigenpy::registerNewType<BoostNumber>();
247+
auto code = eigenpy::registerNewType<BoostNumber>();
248+
242249
eigenpy::registerCommonUfunc<BoostNumber>();
243250

244251
#define IMPLICITLY_CONVERTIBLE(T1, T2) bp::implicitly_convertible<T1, T2>();
@@ -382,4 +389,27 @@ struct BoostComplexPythonVisitor
382389
}
383390
};
384391

392+
393+
394+
395+
396+
397+
// showing we can write a function that returns an eigen vector, and then use it in Python via Eigenpy.
398+
template<typename T>
399+
Eigen::Matrix<T, Eigen::Dynamic, 1> make_a_vector_in_cpp(size_t length, T /* for type dispatch*/ )
400+
{
401+
Eigen::Matrix<T, Eigen::Dynamic,1> a;
402+
a.resize(length,1);
403+
for (size_t ii(0); ii<length; ++ii)
404+
{
405+
a(ii) = static_cast<T>(ii);
406+
407+
}
408+
409+
return a;
410+
}
411+
412+
413+
#include "a_class.hpp"
414+
385415
#endif

0 commit comments

Comments
 (0)