Skip to content

Commit 8eadee2

Browse files
authoredMar 3, 2023
Eigen: support sparse matrix types (wjakob#126)
This commit adds support for sparse Eigen matrices along with tests.
1 parent 67150d4 commit 8eadee2

File tree

9 files changed

+314
-18
lines changed

9 files changed

+314
-18
lines changed
 

‎.github/workflows/ci.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ jobs:
5555
- name: Install NumPy
5656
if: matrix.python != 'pypy3.9' && matrix.python != '3.12.0-alpha.5'
5757
run: |
58-
python -m pip install numpy
58+
python -m pip install numpy scipy
5959
6060
- name: Configure
6161
run: >

‎docs/changelog.rst

+3
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,9 @@ Version 0.2.0 (TBA)
6060
* Added casters for dense matrix/array types from the `Eigen library
6161
<https://eigen.tuxfamily.org/index.php?title=Main_Page>`__. (PR `#120
6262
<https://github.com/wjakob/nanobind/pull/120>`__).
63+
* Added casters for sparse matrix/array types from the `Eigen library
64+
<https://eigen.tuxfamily.org/index.php?title=Main_Page>`__. (PR `#126
65+
<https://github.com/wjakob/nanobind/pull/126>`_).
6366
* Implemented `nb::bind_vector\<T\>() <bind_vector>` analogous to similar
6467
functionality in pybind11. (commit `f2df8a
6568
<https://github.com/wjakob/nanobind/commit/f2df8a90fbfb06ee03a79b0dd85fa0e266efeaa9>`__).

‎docs/eigen.rst

+13
Original file line numberDiff line numberDiff line change
@@ -100,3 +100,16 @@ apply:
100100
.. code-block:: cpp
101101
102102
void f4(nb::DRef<Eigen::MatrixXf> x) { x *= 2; }
103+
104+
105+
Sparse matrices
106+
---------------
107+
108+
There is also support for Eigen's sparse matrices which are mapped to
109+
``scipy.sparse.csr_matrix``/``scipy.sparse.csc_matrix``. To use it the extra header
110+
:file:`nanobind/eigen/sparse.h` has to be included.
111+
112+
.. note::
113+
114+
There is no support for Eigen sparse vectors because there exists no
115+
equivalent type in ``scipy.sparse``.

‎docs/index.rst

+1
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@ discourse:
100100
ownership_adv
101101
lowlevel
102102
typeslots
103+
eigen
103104

104105
.. toctree::
105106
:caption: API Reference

‎include/nanobind/eigen/dense.h

+23-16
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@
1313
#include <nanobind/ndarray.h>
1414
#include <Eigen/Core>
1515

16-
static_assert(EIGEN_VERSION_AT_LEAST(3, 2, 7),
17-
"Eigen matrix support in pybind11 requires Eigen >= 3.2.7");
16+
static_assert(EIGEN_VERSION_AT_LEAST(3, 3, 1),
17+
"Eigen matrix support in nanobind requires Eigen >= 3.3.1");
1818

1919
NAMESPACE_BEGIN(NB_NAMESPACE)
2020

@@ -25,20 +25,23 @@ template <typename T> using DMap = Eigen::Map<T, 0, DStride>;
2525

2626
NAMESPACE_BEGIN(detail)
2727

28+
template <typename T>
29+
constexpr int NumDimensions = int(T::MaxSizeAtCompileTime) == 1 ? 0 : bool(T::IsVectorAtCompileTime) ? 1 : 2;
30+
2831
template <typename T>
2932
using array_for_eigen_t = ndarray<
3033
typename T::Scalar,
3134
numpy,
3235
std::conditional_t<
33-
T::NumDimensions == 1,
36+
NumDimensions<T> == 1,
3437
shape<(size_t) T::SizeAtCompileTime>,
3538
shape<(size_t) T::RowsAtCompileTime,
3639
(size_t) T::ColsAtCompileTime>>,
3740
std::conditional_t<
3841
T::InnerStrideAtCompileTime == Eigen::Dynamic,
3942
any_contig,
4043
std::conditional_t<
41-
T::IsRowMajor || T::NumDimensions == 1,
44+
T::IsRowMajor || NumDimensions<T> == 1,
4245
c_contig,
4346
f_contig
4447
>
@@ -53,9 +56,13 @@ is_base_of_template_v<T, Eigen::EigenBase>;
5356
template <typename T> constexpr bool is_eigen_plain_v =
5457
is_base_of_template_v<T, Eigen::PlainObjectBase>;
5558

59+
/// Detect Eigen::SparseMatrix
60+
template <typename T> constexpr bool is_eigen_sparse_v =
61+
is_base_of_template_v<T, Eigen::SparseMatrixBase>;
62+
5663
/// Detects expression templates
5764
template <typename T> constexpr bool is_eigen_xpr_v =
58-
is_eigen_v<T> && !is_eigen_plain_v<T> &&
65+
is_eigen_v<T> && !is_eigen_plain_v<T> && !is_eigen_sparse_v<T> &&
5966
!std::is_base_of_v<Eigen::MapBase<T, Eigen::ReadOnlyAccessors>, T>;
6067

6168
template <typename T> struct type_caster<T, enable_if_t<is_eigen_plain_v<T>>> {
@@ -71,7 +78,7 @@ template <typename T> struct type_caster<T, enable_if_t<is_eigen_plain_v<T>>> {
7178
return false;
7279
const NDArray &array = caster.value;
7380

74-
if constexpr (T::NumDimensions == 1) {
81+
if constexpr (NumDimensions<T> == 1) {
7582
value.resize(array.shape(0));
7683
memcpy(value.data(), array.data(),
7784
array.shape(0) * sizeof(Scalar));
@@ -93,10 +100,10 @@ template <typename T> struct type_caster<T, enable_if_t<is_eigen_plain_v<T>>> {
93100
}
94101

95102
static handle from_cpp(const T &v, rv_policy policy, cleanup_list *cleanup) noexcept {
96-
size_t shape[T::NumDimensions];
97-
int64_t strides[T::NumDimensions];
103+
size_t shape[NumDimensions<T>];
104+
int64_t strides[NumDimensions<T>];
98105

99-
if constexpr (T::NumDimensions == 1) {
106+
if constexpr (NumDimensions<T> == 1) {
100107
shape[0] = v.size();
101108
strides[0] = v.innerStride();
102109
} else {
@@ -139,7 +146,7 @@ template <typename T> struct type_caster<T, enable_if_t<is_eigen_plain_v<T>>> {
139146
policy == rv_policy::move ? rv_policy::reference : policy;
140147

141148
object o = steal(NDArrayCaster::from_cpp(
142-
NDArray(ptr, T::NumDimensions, shape, owner, strides),
149+
NDArray(ptr, NumDimensions<T>, shape, owner, strides),
143150
array_rv_policy, cleanup));
144151

145152
return o.release();
@@ -165,7 +172,7 @@ template <typename T> struct type_caster<T, enable_if_t<is_eigen_xpr_v<T>>> {
165172

166173
/// Caster for Eigen::Map<T>
167174
template <typename T, int Options, typename StrideType>
168-
struct type_caster<Eigen::Map<T, Options, StrideType>> {
175+
struct type_caster<Eigen::Map<T, Options, StrideType>, enable_if_t<is_eigen_plain_v<T>>> {
169176
using Map = Eigen::Map<T, Options, StrideType>;
170177
using NDArray = array_for_eigen_t<Map>;
171178
using NDArrayCaster = type_caster<NDArray>;
@@ -180,10 +187,10 @@ struct type_caster<Eigen::Map<T, Options, StrideType>> {
180187
}
181188

182189
static handle from_cpp(const Map &v, rv_policy, cleanup_list *cleanup) noexcept {
183-
size_t shape[T::NumDimensions];
184-
int64_t strides[T::NumDimensions];
190+
size_t shape[NumDimensions<T>];
191+
int64_t strides[NumDimensions<T>];
185192

186-
if constexpr (T::NumDimensions == 1) {
193+
if constexpr (NumDimensions<T> == 1) {
187194
shape[0] = v.size();
188195
strides[0] = v.innerStride();
189196
} else {
@@ -194,7 +201,7 @@ struct type_caster<Eigen::Map<T, Options, StrideType>> {
194201
}
195202

196203
return NDArrayCaster::from_cpp(
197-
NDArray((void *) v.data(), T::NumDimensions, shape, handle(), strides),
204+
NDArray((void *) v.data(), NumDimensions<T>, shape, handle(), strides),
198205
rv_policy::reference, cleanup);
199206
}
200207

@@ -224,7 +231,7 @@ struct type_caster<Eigen::Map<T, Options, StrideType>> {
224231

225232
/// Caster for Eigen::Ref<T>
226233
template <typename T, int Options, typename StrideType>
227-
struct type_caster<Eigen::Ref<T, Options, StrideType>> {
234+
struct type_caster<Eigen::Ref<T, Options, StrideType>, enable_if_t<is_eigen_plain_v<T>>> {
228235
using Ref = Eigen::Ref<T, Options, StrideType>;
229236
using Map = Eigen::Map<T, Options, StrideType>;
230237
using MapCaster = make_caster<Map>;

‎include/nanobind/eigen/sparse.h

+177
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
/*
2+
nanobind/eigen/sparse.h: type casters for sparse Eigen matrices
3+
4+
Copyright (c) 2023 Henri Menke and Wenzel Jakob
5+
6+
All rights reserved. Use of this source code is governed by a
7+
BSD-style license that can be found in the LICENSE file.
8+
*/
9+
10+
#pragma once
11+
12+
#include <nanobind/ndarray.h>
13+
#include <nanobind/eigen/dense.h>
14+
#include <Eigen/SparseCore>
15+
16+
#include <memory>
17+
#include <type_traits>
18+
#include <utility>
19+
20+
NAMESPACE_BEGIN(NB_NAMESPACE)
21+
22+
NAMESPACE_BEGIN(detail)
23+
24+
/// Detect Eigen::SparseMatrix
25+
template <typename T> constexpr bool is_eigen_sparse_matrix_v =
26+
is_eigen_sparse_v<T> &&
27+
!std::is_base_of_v<Eigen::SparseMapBase<T, Eigen::ReadOnlyAccessors>, T>;
28+
29+
30+
/// Caster for Eigen::SparseMatrix
31+
template <typename T> struct type_caster<T, enable_if_t<is_eigen_sparse_matrix_v<T>>> {
32+
using Scalar = typename T::Scalar;
33+
using StorageIndex = typename T::StorageIndex;
34+
using Index = typename T::Index;
35+
using SparseMap = Eigen::Map<T>;
36+
37+
static_assert(std::is_same_v<T, Eigen::SparseMatrix<Scalar, T::Options, StorageIndex>>,
38+
"nanobind: Eigen sparse caster only implemented for matrices");
39+
40+
static constexpr bool row_major = T::IsRowMajor;
41+
42+
using ScalarNDArray = ndarray<numpy, Scalar, shape<any>>;
43+
using StorageIndexNDArray = ndarray<numpy, StorageIndex, shape<any>>;
44+
45+
using ScalarCaster = make_caster<ScalarNDArray>;
46+
using StorageIndexCaster = make_caster<StorageIndexNDArray>;
47+
48+
NB_TYPE_CASTER(T, const_name<row_major>("scipy.sparse.csr_matrix[",
49+
"scipy.sparse.csc_matrix[")
50+
+ make_caster<Scalar>::Name + const_name("]"));
51+
52+
ScalarCaster data_caster;
53+
StorageIndexCaster indices_caster, indptr_caster;
54+
55+
bool from_python(handle src, uint8_t flags, cleanup_list *cleanup) noexcept {
56+
object obj = borrow(src);
57+
try {
58+
object matrix_type = module_::import_("scipy.sparse").attr(row_major ? "csr_matrix" : "csc_matrix");
59+
if (!obj.type().is(matrix_type))
60+
obj = matrix_type(obj);
61+
} catch (const python_error &) {
62+
return false;
63+
}
64+
65+
if (object data_o = obj.attr("data"); !data_caster.from_python(data_o, flags, cleanup))
66+
return false;
67+
ScalarNDArray& values = data_caster.value;
68+
69+
if (object indices_o = obj.attr("indices"); !indices_caster.from_python(indices_o, flags, cleanup))
70+
return false;
71+
StorageIndexNDArray& inner_indices = indices_caster.value;
72+
73+
if (object indptr_o = obj.attr("indptr"); !indptr_caster.from_python(indptr_o, flags, cleanup))
74+
return false;
75+
StorageIndexNDArray& outer_indices = indptr_caster.value;
76+
77+
object shape_o = obj.attr("shape"), nnz_o = obj.attr("nnz");
78+
Index rows, cols, nnz;
79+
try {
80+
if (len(shape_o) != 2)
81+
return false;
82+
rows = cast<Index>(shape_o[0]);
83+
cols = cast<Index>(shape_o[1]);
84+
nnz = cast<Index>(nnz_o);
85+
} catch (const python_error &) {
86+
return false;
87+
}
88+
89+
value = SparseMap(rows, cols, nnz, outer_indices.data(), inner_indices.data(), values.data());
90+
91+
return true;
92+
}
93+
94+
static handle from_cpp(T &&v, rv_policy policy, cleanup_list *cleanup) noexcept {
95+
if (policy == rv_policy::automatic ||
96+
policy == rv_policy::automatic_reference)
97+
policy = rv_policy::move;
98+
99+
return from_cpp((const T &) v, policy, cleanup);
100+
}
101+
102+
static handle from_cpp(const T &v, rv_policy policy, cleanup_list *) noexcept {
103+
if (!v.isCompressed()) {
104+
PyErr_SetString(PyExc_ValueError,
105+
"nanobind: unable to return an Eigen sparse matrix that is not in a compressed format. "
106+
"Please call `.makeCompressed()` before returning the value on the C++ end.");
107+
return handle();
108+
}
109+
110+
object matrix_type;
111+
try {
112+
matrix_type = module_::import_("scipy.sparse").attr(row_major ? "csr_matrix" : "csc_matrix");
113+
} catch (python_error &e) {
114+
e.restore();
115+
return handle();
116+
}
117+
118+
const Index rows = v.rows();
119+
const Index cols = v.cols();
120+
const size_t data_shape[] = { (size_t)v.nonZeros() };
121+
const size_t outer_indices_shape[] = { (size_t)((row_major ? rows : cols) + 1) };
122+
123+
T *src = std::addressof(const_cast<T &>(v));
124+
object owner;
125+
if (policy == rv_policy::move) {
126+
src = new T(std::move(v));
127+
owner = capsule(src, [](void *p) noexcept { delete (T *) p; });
128+
}
129+
130+
ScalarNDArray data(src->valuePtr(), 1, data_shape, owner);
131+
StorageIndexNDArray outer_indices(src->outerIndexPtr(), 1, outer_indices_shape, owner);
132+
StorageIndexNDArray inner_indices(src->innerIndexPtr(), 1, data_shape, owner);
133+
134+
try {
135+
return matrix_type(make_tuple(
136+
std::move(data), std::move(inner_indices), std::move(outer_indices)),
137+
make_tuple(rows, cols))
138+
.release();
139+
} catch (python_error &e) {
140+
e.restore();
141+
return handle();
142+
}
143+
}
144+
};
145+
146+
147+
/// Caster for Eigen::Map<Eigen::SparseMatrix>
148+
template <typename T>
149+
struct type_caster<Eigen::Map<T>, enable_if_t<is_eigen_sparse_matrix_v<T>>> {
150+
using Map = Eigen::Map<T>;
151+
using SparseMatrixCaster = type_caster<T>;
152+
static constexpr auto Name = SparseMatrixCaster::Name;
153+
template <typename T_> using Cast = Map;
154+
155+
bool from_python(handle src, uint8_t flags, cleanup_list *cleanup) noexcept = delete;
156+
157+
static handle from_cpp(const Map &v, rv_policy policy, cleanup_list *cleanup) noexcept = delete;
158+
};
159+
160+
161+
/// Caster for Eigen::Ref<Eigen::SparseMatrix>
162+
template <typename T, int Options>
163+
struct type_caster<Eigen::Ref<T, Options>, enable_if_t<is_eigen_sparse_matrix_v<T>>> {
164+
using Ref = Eigen::Ref<T, Options>;
165+
using Map = Eigen::Map<T, Options>;
166+
using MapCaster = make_caster<Map>;
167+
static constexpr auto Name = MapCaster::Name;
168+
template <typename T_> using Cast = Ref;
169+
170+
bool from_python(handle src, uint8_t flags, cleanup_list *cleanup) noexcept = delete;
171+
172+
static handle from_cpp(const Ref &v, rv_policy policy, cleanup_list *cleanup) noexcept = delete;
173+
};
174+
175+
NAMESPACE_END(detail)
176+
177+
NAMESPACE_END(NB_NAMESPACE)

‎tests/CMakeLists.txt

+1-1
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ nanobind_add_module(test_intrusive_ext test_intrusive.cpp object.cpp object.h ${
2121
nanobind_add_module(test_exception_ext test_exception.cpp object.cpp object.h ${NB_EXTRA_ARGS})
2222
nanobind_add_module(test_make_iterator_ext test_make_iterator.cpp ${NB_EXTRA_ARGS})
2323

24-
find_package (Eigen3 NO_MODULE)
24+
find_package (Eigen3 3.3.1 NO_MODULE)
2525
if (TARGET Eigen3::Eigen)
2626
nanobind_add_module(test_eigen_ext test_eigen.cpp ${NB_EXTRA_ARGS})
2727
target_link_libraries(test_eigen_ext PRIVATE Eigen3::Eigen)

‎tests/test_eigen.cpp

+24
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
#include <nanobind/eigen/dense.h>
2+
#include <nanobind/eigen/sparse.h>
23

34
namespace nb = nanobind;
45

@@ -93,6 +94,29 @@ NB_MODULE(test_eigen_ext, m) {
9394
m.def("updateV3i", [](Eigen::Ref<Eigen::Vector3i> a) { a[2] = 123; });
9495
m.def("updateVXi", [](Eigen::Ref<Eigen::VectorXi> a) { a[2] = 123; });
9596

97+
using SparseMatrixR = Eigen::SparseMatrix<float, Eigen::RowMajor>;
98+
using SparseMatrixC = Eigen::SparseMatrix<float>;
99+
Eigen::MatrixXf mat(5, 6);
100+
mat <<
101+
0, 3, 0, 0, 0, 11,
102+
22, 0, 0, 0, 17, 11,
103+
7, 5, 0, 1, 0, 11,
104+
0, 0, 0, 0, 0, 11,
105+
0, 0, 14, 0, 8, 11;
106+
m.def("sparse_r", [mat]() -> SparseMatrixR {
107+
return Eigen::SparseView<Eigen::MatrixXf>(mat);
108+
});
109+
m.def("sparse_c", [mat]() -> SparseMatrixC {
110+
return Eigen::SparseView<Eigen::MatrixXf>(mat);
111+
});
112+
m.def("sparse_copy_r", [](const SparseMatrixR &m) -> SparseMatrixR { return m; });
113+
m.def("sparse_copy_c", [](const SparseMatrixC &m) -> SparseMatrixC { return m; });
114+
m.def("sparse_r_uncompressed", []() -> SparseMatrixR {
115+
SparseMatrixR m(2,2);
116+
m.coeffRef(0,0) = 1.0f;
117+
return m;
118+
});
119+
96120
struct Buffer {
97121
uint32_t x[30] { };
98122

‎tests/test_eigen.py

+71
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import pytest
22
import gc
3+
import re
4+
import sys
35

46
try:
57
import numpy as np
@@ -172,3 +174,72 @@ def test07_mutate_arg():
172174
A2 = A.copy()
173175
t.mutate_MXu(A)
174176
assert np.all(A == 2*A2)
177+
178+
179+
@needs_numpy_and_eigen
180+
def test_sparse():
181+
pytest.importorskip("scipy")
182+
import scipy.sparse
183+
184+
# no isinstance here because we want strict type equivalence
185+
assert type(t.sparse_r()) is scipy.sparse.csr_matrix
186+
assert type(t.sparse_c()) is scipy.sparse.csc_matrix
187+
assert type(t.sparse_copy_r(t.sparse_r())) is scipy.sparse.csr_matrix
188+
assert type(t.sparse_copy_c(t.sparse_c())) is scipy.sparse.csc_matrix
189+
assert type(t.sparse_copy_r(t.sparse_c())) is scipy.sparse.csr_matrix
190+
assert type(t.sparse_copy_c(t.sparse_r())) is scipy.sparse.csc_matrix
191+
192+
def assert_sparse_equal_ref(sparse_mat):
193+
ref = np.array(
194+
[
195+
[0.0, 3, 0, 0, 0, 11],
196+
[22, 0, 0, 0, 17, 11],
197+
[7, 5, 0, 1, 0, 11],
198+
[0, 0, 0, 0, 0, 11],
199+
[0, 0, 14, 0, 8, 11],
200+
]
201+
)
202+
np.testing.assert_array_equal(sparse_mat.toarray(), ref)
203+
204+
assert_sparse_equal_ref(t.sparse_r())
205+
assert_sparse_equal_ref(t.sparse_c())
206+
assert_sparse_equal_ref(t.sparse_copy_r(t.sparse_r()))
207+
assert_sparse_equal_ref(t.sparse_copy_c(t.sparse_c()))
208+
assert_sparse_equal_ref(t.sparse_copy_r(t.sparse_c()))
209+
assert_sparse_equal_ref(t.sparse_copy_c(t.sparse_r()))
210+
211+
212+
@needs_numpy_and_eigen
213+
def test_sparse_failures():
214+
pytest.importorskip("scipy")
215+
import scipy
216+
217+
with pytest.raises(
218+
ValueError,
219+
match=re.escape(
220+
"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."
221+
),
222+
):
223+
t.sparse_r_uncompressed()
224+
225+
csr_matrix = scipy.sparse.csr_matrix
226+
scipy.sparse.csr_matrix = None
227+
with pytest.raises(TypeError, match=re.escape("'NoneType' object is not callable")):
228+
t.sparse_r()
229+
230+
del scipy.sparse.csr_matrix
231+
with pytest.raises(
232+
AttributeError,
233+
match=re.escape("module 'scipy.sparse' has no attribute 'csr_matrix'"),
234+
):
235+
t.sparse_r()
236+
237+
sys_path = sys.path
238+
sys.path = []
239+
del sys.modules["scipy"]
240+
with pytest.raises(ModuleNotFoundError, match=re.escape("No module named 'scipy'")):
241+
t.sparse_r()
242+
243+
# undo sabotage of the module
244+
sys.path = sys_path
245+
scipy.sparse.csr_matrix = csr_matrix

0 commit comments

Comments
 (0)
Please sign in to comment.