Hello,
I came across a bug in eigenpy that affected my code described here. I hope it could be fixed at the root.
When a (1, N) numpy array is passed to a Boost.Python binding that accepts Eigen::Ref<const MatrixXd>, eigenpy creates a direct in-place Map instead of copying — but the resulting Ref has Stride<0,0> with compile-time-zero values that override the actual numpy strides. As a result, only element (0, 0) maps correctly; all elements (0, c) for c > 0 read from wrong memory locations and contain garbage values. No error or warning is raised.
To reproduce
Environment
- eigenpy version: 3.12.0
- Python: 3.12
- NumPy: any version (the
(1, N) dual-contiguous flag behavior is standard)
- Eigen: 3.x
- OS: Ubuntu
// binding (Boost.Python)
#include <eigenpy/eigenpy.hpp>
#include <Eigen/Core>
void print_matrix(Eigen::Ref<const Eigen::MatrixXd> m) {
std::cout << "rows=" << m.rows() << " cols=" << m.cols() << std::endl;
std::cout << m << std::endl;
}
BOOST_PYTHON_MODULE(test_mod) {
eigenpy::enableEigenPy();
boost::python::def("print_matrix", print_matrix);
}
import numpy as np
import test_mod
# (1, 5) — shape that is both C- and F-contiguous in numpy
a = np.array([[1.0, 2.0, 3.0, 4.0, 5.0]])
print("numpy value:", a)
print("C_CONTIGUOUS=%s F_CONTIGUOUS=%s" % (a.flags['C_CONTIGUOUS'], a.flags['F_CONTIGUOUS']))
print("--- C++ Ref<const MatrixXd> receives (1,5): ---")
test_mod.print_matrix(a)
# Expected: 1 2 3 4 5
# Actual: 1 <garbage> <garbage> <garbage> <garbage>
# (2, 3) — shape where C- and F-contiguous flags differ: works correctly
b = np.array([[1.0, 2.0, 3.0],
[4.0, 5.0, 6.0]])
print("\n--- C++ Ref<const MatrixXd> receives (2,3) [works correctly]: ---")
test_mod.print_matrix(b)
Result
numpy value: [[1. 2. 3. 4. 5.]]
C_CONTIGUOUS=True F_CONTIGUOUS=True
--- C++ Ref<const MatrixXd> receives (1,5): ---
rows=1 cols=5
1 4.90607e-321 0 0 3.70442e-315
--- C++ Ref<const MatrixXd> receives (2,3) [works correctly]: ---
rows=2 cols=3
1 2 3
4 5 6
(1, N) numpy arrays are simultaneously C-contiguous and F-contiguous — this is a standard numpy property: with a single row, the stride ordering is irrelevant. NumPy sets both NPY_ARRAY_C_CONTIGUOUS and NPY_ARRAY_F_CONTIGUOUS flags to True.
eigenpy's layout compatibility check in numpy-eigen.hpp (or equivalent) is:
template <typename MatType>
bool is_arr_layout_compatible_with_mat_type(PyArrayObject* arr) {
bool is_array_F_cont = PyArray_IS_F_CONTIGUOUS(arr);
bool is_array_C_cont = PyArray_IS_C_CONTIGUOUS(arr);
// For ColMajor (MatrixXd): compatible if F-contiguous
return (!IsRowMajor && is_array_F_cont) || (IsRowMajor && is_array_C_cont);
}
For (1, N) C-order arrays and MatrixXd (ColMajor / !IsRowMajor):
is_array_F_cont = true (numpy sets this for single-row arrays)
- check returns
true → eigenpy concludes no copy needed, creates direct Map
The direct Map is created with NumpyMapStride = Stride<0, 0>:
typedef Eigen::Map<MatType, 0, NumpyMapStride> NumpyMap;
Stride<0, 0> stores compile-time zero outer and inner stride values. These compile-time constants override whatever runtime stride values would have been passed. For a (1, 5) C-order array with actual numpy strides (40, 8) (bytes), the Eigen Map ignores the outer stride entirely and uses stride 0 — so (0, c) for c > 0 maps to the wrong memory.
The correct numpy strides in elements are outer=5, inner=1 (C-order). A ColMajor Map of a (1, N) C-order array would actually need a copy since the layouts are genuinely incompatible for accessing a row vector as a column-major matrix — but (1, N) triggers the false positive because the F-contiguous flag is set.
Affected shapes: any (1, N) array passed to Ref<const MatrixXd> (ColMajor). Analogously, (N, 1) passed to Ref<const MatrixXf, 0, RowMajor> would trigger the symmetric case.
Not affected: (M, N) arrays with M > 1 — these have distinct C- and F-contiguous flags, so eigenpy correctly detects incompatibility and allocates a copy.
Expected behavior
eigenpy should detect that a (1, N) C-order array is not genuinely F-contiguous in the ColMajor sense (i.e., its inner stride in elements is not 1 along the column axis) and allocate a copy, the same way it does for (M, N) with M > 1.
A possible fix: tighten the compatibility check to also verify element-level strides are consistent with the Eigen storage order, not just the numpy contiguity flags:
// Instead of relying solely on PyArray_IS_F_CONTIGUOUS, verify actual strides:
npy_intp inner_stride = PyArray_STRIDE(arr, IsRowMajor ? 1 : 0) / sizeof(Scalar);
bool truly_compatible = (inner_stride == 1);
return truly_compatible && ((!IsRowMajor && is_array_F_cont) || (IsRowMajor && is_array_C_cont));
Thanks!
Hello,
I came across a bug in eigenpy that affected my code described here. I hope it could be fixed at the root.
When a
(1, N)numpy array is passed to a Boost.Python binding that acceptsEigen::Ref<const MatrixXd>, eigenpy creates a direct in-placeMapinstead of copying — but the resultingRefhasStride<0,0>with compile-time-zero values that override the actual numpy strides. As a result, only element(0, 0)maps correctly; all elements(0, c)forc > 0read from wrong memory locations and contain garbage values. No error or warning is raised.To reproduce
Environment
(1, N)dual-contiguous flag behavior is standard)Result
(1, N)numpy arrays are simultaneously C-contiguous and F-contiguous — this is a standard numpy property: with a single row, the stride ordering is irrelevant. NumPy sets bothNPY_ARRAY_C_CONTIGUOUSandNPY_ARRAY_F_CONTIGUOUSflags toTrue.eigenpy's layout compatibility check in
numpy-eigen.hpp(or equivalent) is:For
(1, N)C-order arrays andMatrixXd(ColMajor /!IsRowMajor):is_array_F_cont = true(numpy sets this for single-row arrays)true→ eigenpy concludes no copy needed, creates directMapThe direct
Mapis created withNumpyMapStride = Stride<0, 0>:Stride<0, 0>stores compile-time zero outer and inner stride values. These compile-time constants override whatever runtime stride values would have been passed. For a(1, 5)C-order array with actual numpy strides(40, 8)(bytes), the EigenMapignores the outer stride entirely and uses stride0— so(0, c)forc > 0maps to the wrong memory.The correct numpy strides in elements are
outer=5, inner=1(C-order). A ColMajorMapof a(1, N)C-order array would actually need a copy since the layouts are genuinely incompatible for accessing a row vector as a column-major matrix — but(1, N)triggers the false positive because the F-contiguous flag is set.Affected shapes: any
(1, N)array passed toRef<const MatrixXd>(ColMajor). Analogously,(N, 1)passed toRef<const MatrixXf, 0, RowMajor>would trigger the symmetric case.Not affected:
(M, N)arrays withM > 1— these have distinct C- and F-contiguous flags, so eigenpy correctly detects incompatibility and allocates a copy.Expected behavior
eigenpy should detect that a
(1, N)C-order array is not genuinely F-contiguous in the ColMajor sense (i.e., its inner stride in elements is not 1 along the column axis) and allocate a copy, the same way it does for(M, N)withM > 1.A possible fix: tighten the compatibility check to also verify element-level strides are consistent with the Eigen storage order, not just the numpy contiguity flags:
Thanks!