Skip to content

Ref<const MatrixXd> silently reads garbage for (1, N) numpy arrays due to false F-contiguous detection #638

@thanhndv212

Description

@thanhndv212

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!

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions