Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] Support Eigen::Map<Eigen::SparseMatrix> #782

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions docs/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@ Version TBD (unreleased)
documentation for more details, important caveats, and an example policy.
(PR `#767 <https://github.com/wjakob/nanobind/pull/767`__)

- Added casters for `Eigen::Map<Eigen::SparseMatrix<...>` types from the `Eigen library
<https://eigen.tuxfamily.org/index.php?title=Main_Page>`__. (PR `#782
<https://github.com/wjakob/nanobind/pull/782>`_).

Version 2.2.0 (October 3, 2024)
-------------------------------

Expand Down
8 changes: 5 additions & 3 deletions docs/eigen.rst
Original file line number Diff line number Diff line change
Expand Up @@ -112,9 +112,11 @@ Eigen types:

#include <nanobind/eigen/sparse.h>

The ``Eigen::SparseMatrix<..>`` type maps to either ``scipy.sparse.csr_matrix``
or ``scipy.sparse.csc_matrix`` depending on whether row- or column-major
storage is used.
The ``Eigen::SparseMatrix<..>`` and ``Eigen::Map<Eigen::SparseMatrix<..>>``
types map to either ``scipy.sparse.csr_matrix`` or ``scipy.sparse.csc_matrix``
depending on whether row- or column-major storage is used.

There is no support for Eigen sparse vectors because an equivalent type does
not exist as part of ``scipy.sparse``.


118 changes: 115 additions & 3 deletions include/nanobind/eigen/sparse.h
Original file line number Diff line number Diff line change
Expand Up @@ -146,15 +146,127 @@ template <typename T> struct type_caster<T, enable_if_t<is_eigen_sparse_matrix_v
/// Caster for Eigen::Map<Eigen::SparseMatrix>, still needs to be implemented.
template <typename T>
struct type_caster<Eigen::Map<T>, enable_if_t<is_eigen_sparse_matrix_v<T>>> {
using Scalar = typename T::Scalar;
using StorageIndex = typename T::StorageIndex;
using Index = typename T::Index;
using SparseMap = Eigen::Map<T>;
using Map = Eigen::Map<T>;
using SparseMatrixCaster = type_caster<T>;
static constexpr auto Name = SparseMatrixCaster::Name;
static constexpr bool RowMajor = T::IsRowMajor;

using ScalarNDArray = ndarray<numpy, Scalar, shape<-1>>;
using StorageIndexNDArray = ndarray<numpy, StorageIndex, shape<-1>>;

using ScalarCaster = make_caster<ScalarNDArray>;
using StorageIndexCaster = make_caster<StorageIndexNDArray>;

static constexpr auto Name = const_name<RowMajor>("scipy.sparse.csr_matrix[",
"scipy.sparse.csc_matrix[")
+ make_caster<Scalar>::Name + const_name("]");

template <typename T_> using Cast = Map;
template <typename T_> static constexpr bool can_cast() { return true; }

bool from_python(handle src, uint8_t flags, cleanup_list *cleanup) noexcept = delete;
ScalarCaster data_caster;
StorageIndexCaster indices_caster, indptr_caster;
Index rows, cols, nnz;

bool from_python(handle src, uint8_t flags, cleanup_list *cleanup) noexcept {
// Disable implicit conversions
//
// I'm not convinced this manipulation of the flags works. It seems in
// ndarray caster the flag is just reset to true.
return from_python_(src, flags & ~(uint8_t)cast_flags::convert, cleanup);
}

bool from_python_(handle src, uint8_t flags, cleanup_list *cleanup) noexcept
{
object obj = borrow(src);
try {
object matrix_type = module_::import_("scipy.sparse").attr(RowMajor ? "csr_matrix" : "csc_matrix");
if (!obj.type().is(matrix_type))
obj = matrix_type(obj);
} catch (const python_error &) {
Comment on lines +186 to +189
Copy link

@bilmes bilmes Nov 27, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a comment on lines 178-180. I might not be understanding this precisely, but doesn't this bit of code attempt to convert (line 180) obj to a sparse matrix if it is not already one? If so, this might cause issues where an overloaded routine that could take either a dense or sparse matrix, if the sparse matrix routine is considered first, anything that can be converted to a sparse matrix via csr_matrix (such as an int, float, or dense matrix) would then be converted to a sparse matrix and that's probaby not desirable. A simple fix would be to just fail (return false) if obj.type().is(matrix_type) is not true.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, another minor comment on the above. If it is the case that you do end up returning false if !obj.type().is(matrix_type) then the catch block of the exception I'd guess should probably re-throw the exception since it probably means something is wrong other than that the object is not a sparse matrix (e.g., an import or some other error). I.e., the return of true/false should probably (unless I don't understand things) only happen if there are no errors. Similarly, in the below, when you are returning false, this means that it passes the above type check but there is still a problem getting, say, indices, and I'd guess that would mean something is quite wrong (not well indicated by a false return) with the object and/or setup.

return false;
}

// I thought this would not allow conversions, but it does
// I think conversion is ok if std::is_const_v<T> is true, so long as
// each *_caster is the owner.
// For non-const, conversion seems to be a bad idea. I think the dense
// version will throw a RunTime error rather than convert
if (object data_o = obj.attr("data"); !data_caster.from_python(data_o, flags, cleanup))
return false;

if (object indices_o = obj.attr("indices"); !indices_caster.from_python(indices_o, flags, cleanup))
return false;

if (object indptr_o = obj.attr("indptr"); !indptr_caster.from_python(indptr_o, flags, cleanup))
return false;

static handle from_cpp(const Map &v, rv_policy policy, cleanup_list *cleanup) noexcept = delete;
object shape_o = obj.attr("shape"), nnz_o = obj.attr("nnz");
try {
if (len(shape_o) != 2)
return false;
rows = cast<Index>(shape_o[0]);
cols = cast<Index>(shape_o[1]);
nnz = cast<Index>(nnz_o);
} catch (const python_error &) {
return false;
}
return true;
}

static handle from_cpp(const Map &v, rv_policy policy, cleanup_list *) noexcept
{
if (!v.isCompressed()) {
PyErr_SetString(PyExc_ValueError,
"nanobind: unable to return an Eigen sparse matrix that is not in a compressed format. "
"Please call `.makeCompressed()` before returning the value on the C++ end.");
return handle();
}

object matrix_type;
try {
matrix_type = module_::import_("scipy.sparse").attr(RowMajor ? "csr_matrix" : "csc_matrix");
} catch (python_error &e) {
e.restore();
return handle();
}

const Index rows = v.rows(), cols = v.cols();
const size_t data_shape[] = { (size_t) v.nonZeros() };
const size_t outer_indices_shape[] = { (size_t) ((RowMajor ? rows : cols) + 1) };

T *src = std::addressof(const_cast<T &>(v));
object owner;
if (policy == rv_policy::move) {
src = new T(std::move(v));
owner = capsule(src, [](void *p) noexcept { delete (T *) p; });
}

ScalarNDArray data(src->valuePtr(), 1, data_shape, owner);
StorageIndexNDArray outer_indices(src->outerIndexPtr(), 1, outer_indices_shape, owner);
StorageIndexNDArray inner_indices(src->innerIndexPtr(), 1, data_shape, owner);

try {
return matrix_type(nanobind::make_tuple(
std::move(data), std::move(inner_indices), std::move(outer_indices)),
nanobind::make_tuple(rows, cols))
.release();
} catch (python_error &e) {
e.restore();
return handle();
}
};

operator Map()
{
ScalarNDArray& values = data_caster.value;
StorageIndexNDArray& inner_indices = indices_caster.value;
StorageIndexNDArray& outer_indices = indptr_caster.value;
return SparseMap(rows, cols, nnz, outer_indices.data(), inner_indices.data(), values.data());
}
};


Expand Down
13 changes: 13 additions & 0 deletions tests/test_eigen.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
#include <nanobind/eigen/dense.h>
#include <nanobind/eigen/sparse.h>
#include <nanobind/trampoline.h>
#include <iostream>

namespace nb = nanobind;

Expand Down Expand Up @@ -166,8 +167,20 @@ NB_MODULE(test_eigen_ext, m) {
assert(!m.isCompressed());
return m.markAsRValue();
});
// This function doesn't appear to be called in tests/test_eigen.py
m.def("sparse_complex", []() -> Eigen::SparseMatrix<std::complex<double>> { return {}; });

m.def("sparse_map_c", [](const Eigen::Map<const SparseMatrixC> &) { });
m.def("sparse_map_r", [](const Eigen::Map<const SparseMatrixR> &) { });
m.def("sparse_update_map_to_zero_c", [](nb::object obj) {
Eigen::Map<SparseMatrixC> c = nb::cast<Eigen::Map<SparseMatrixC>>(obj);
for (int i = 0; i < c.nonZeros(); ++i) { c.valuePtr()[i] = 0; }
});
m.def("sparse_update_map_to_zero_r", [](nb::object obj) {
Eigen::Map<SparseMatrixR> r = nb::cast<Eigen::Map<SparseMatrixR>>(obj);
for (int i = 0; i < r.nonZeros(); ++i) { r.valuePtr()[i] = 0; }
});

/// issue #166
using Matrix1d = Eigen::Matrix<double,1,1>;
try {
Expand Down
26 changes: 26 additions & 0 deletions tests/test_eigen.py
Original file line number Diff line number Diff line change
Expand Up @@ -374,3 +374,29 @@ def test14_single_element():
a = np.array([[1]], dtype=np.uint32)
assert a.ndim == 2 and a.shape == (1, 1)
t.addMXuCC(a, a)

@needs_numpy_and_eigen
def test15_sparse_map():
pytest.importorskip("scipy")
import scipy
c = scipy.sparse.csc_matrix([[1, 0], [0, 1]], dtype=np.float32)
# These should be copy-less
t.sparse_map_c(c)
r = scipy.sparse.csr_matrix([[1, 0], [0, 1]], dtype=np.float32)
t.sparse_map_r(r)
# These should be ok, but will copy(?)
t.sparse_map_c(r)
t.sparse_map_r(c)

t.sparse_update_map_to_zero_c(c);
assert c.sum() == 0
t.sparse_update_map_to_zero_r(r);
assert r.sum() == 0

c = scipy.sparse.csc_matrix([[1, 0], [0, 1]], dtype=np.float32)
# Shouldn't this fail list t.castToMapVXi above?
t.sparse_update_map_to_zero_r(c);