Skip to content
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
64184f6
Start working on Eigen::Tensor binding
ManifoldFR Mar 27, 2026
06a267e
Working type_caster for plain tensor (dynamic)
ManifoldFR Mar 30, 2026
b2e5b78
eigen/tensor.h: update copyright owner
ManifoldFR Mar 30, 2026
02ca82b
eigen/tensor.h : define shortcuts for ndarray types from tensor
ManifoldFR Mar 30, 2026
393d5a2
Clarify why is_eigen_tensor type trait works
ManifoldFR Mar 31, 2026
e56fce3
eigen/tensor.h : add type traits related to checking for TensorRef an…
ManifoldFR Mar 31, 2026
2dde294
eigen/tensor.h : check scalar type for plain tensor caster
ManifoldFR Mar 31, 2026
4a2c972
eigen/tensor.h : working TensorMap converter
ManifoldFR Mar 31, 2026
b7f930a
tests: add python test file
ManifoldFR Mar 31, 2026
0765b91
eigen/tensor.h : add caster for TensorRef
ManifoldFR Mar 31, 2026
1513a0a
tests : add more tensor tests (python)
ManifoldFR Mar 31, 2026
209c0f6
tests: add tensor ref update test
ManifoldFR Mar 31, 2026
b932bac
tensor plain : tune return from_cpp
ManifoldFR Mar 31, 2026
1c03704
tests/tensor : add class with tensor members test
ManifoldFR Mar 31, 2026
37b1fd3
tests/tensor : add Buffer test akin to main eigen test :)
ManifoldFR Mar 31, 2026
0a315fb
eigen/tensor : add support for Aligned TensorMap
ManifoldFR Apr 10, 2026
9bf81f5
tests : test Tensor cast, test 0d Tensor and having 0 in one of the s…
ManifoldFR Apr 10, 2026
f6cbdec
docs : add line to table for Eigen tensors
ManifoldFR Apr 10, 2026
cfd2b71
eigen/tensor : add type_caster for (non-plain) Tensor expressions
ManifoldFR Apr 10, 2026
7b9a452
remove newlines, remove casts
ManifoldFR Apr 11, 2026
7a3ca8b
eigen/tensor : infer rv policy
ManifoldFR Apr 11, 2026
0582270
tests/eigen : remove gc.collect() calls on advice of wjakob
ManifoldFR Apr 12, 2026
9c28676
docs : add mention for Eigen Tensor types
ManifoldFR Apr 12, 2026
8906c24
tests/eigen-tensor : test addition of non-contiguous views
ManifoldFR Apr 12, 2026
904af4b
tests : remove unused imports
ManifoldFR Apr 12, 2026
c59a6ac
Test that TensorMap/TensorRef don't handle non-contiguous arrays
ManifoldFR Apr 13, 2026
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
1 change: 1 addition & 0 deletions cmake/nanobind-config.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,7 @@ function (nanobind_build_library TARGET_NAME)
${NB_DIR}/include/nanobind/stl/vector.h
${NB_DIR}/include/nanobind/eigen/dense.h
${NB_DIR}/include/nanobind/eigen/sparse.h
${NB_DIR}/include/nanobind/eigen/tensor.h

${NB_DIR}/src/buffer.h
${NB_DIR}/src/hash.h
Expand Down
2 changes: 2 additions & 0 deletions docs/exchanging.rst
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,8 @@ to external projects that provide further casters:
- ``#include <nanobind/eigen/dense.h>``
* - ``Eigen::SparseMatrix<..>``
- ``#include <nanobind/eigen/sparse.h>``
* - ``Eigen::Tensor<..>``, ``Eigen::TensorMap<..>``, ``Eigen::TensorRef<..>``
- ``#include <nanobind/eigen/tensor.h>``
* - Apache Arrow types
- `https://github.com/maximiliank/nanobind_pyarrow <https://github.com/maximiliank/nanobind_pyarrow>`__
* - ...
Expand Down
324 changes: 324 additions & 0 deletions include/nanobind/eigen/tensor.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,324 @@
/*
nanobind/eigen/tensor.h: type casters for Eigen tensors

Copyright (c) 2026 INRIA

Author(s): Wilson Jallet

All rights reserved. Use of this source code is governed by a
BSD-style license that can be found in the LICENSE file.
*/

#pragma once

#include <nanobind/ndarray.h>

Comment thread
ManifoldFR marked this conversation as resolved.
Outdated
#include <unsupported/Eigen/CXX11/Tensor>


NAMESPACE_BEGIN(NB_NAMESPACE)

Comment thread
ManifoldFR marked this conversation as resolved.
Outdated
NAMESPACE_BEGIN(detail)

/// As of April 2026, Eigen::Tensor types support 16-byte alignment or no alignment.
inline bool is_tensor_aligned(const void *data, std::size_t align = Eigen::Aligned) {
return (reinterpret_cast<std::size_t>(data) % align) == 0;
}

/// Type trait for inheriting from Eigen::TensorBase.
/// All TensorBase specializations inherit from TensorBase<T, ReadOnlyAccessors>.
template<typename T> constexpr bool is_eigen_tensor_v =
std::is_base_of_v<Eigen::TensorBase<T, Eigen::ReadOnlyAccessors>, T>;

template<typename T> constexpr bool eigen_tensor_is_row_major_v = T::Layout == Eigen::RowMajor;
template<typename T> constexpr bool eigen_tensor_is_col_major_v = T::Layout == Eigen::ColMajor;

template<typename T>
constexpr bool is_eigen_tensor_map_v = false;

// Covers const case
template<typename T, int Options, template<class> class MakePointer>
constexpr bool is_eigen_tensor_map_v<Eigen::TensorMap<T, Options, MakePointer>> = true;

template<typename T>
constexpr bool is_eigen_tensor_ref_v = false;

// Covers const case
template<typename T>
constexpr bool is_eigen_tensor_ref_v<Eigen::TensorRef<T>> = true;

template<typename T>
constexpr bool is_eigen_tensor_plain_v = false;

template<typename Scalar, int NumIndices, int Options, typename IndexType>
constexpr bool is_eigen_tensor_plain_v<Eigen::Tensor<Scalar, NumIndices, Options, IndexType>>
= true;

template<typename Scalar, std::ptrdiff_t... Indices, int Options, typename IndexType>
constexpr bool is_eigen_tensor_plain_v<Eigen::TensorFixedSize<Scalar, Eigen::Sizes<Indices...>, Options, IndexType>> = true;

template<typename T>
constexpr bool is_eigen_tensor_xpr_v =
is_eigen_tensor_v<T> &&
!is_eigen_tensor_plain_v<T> &&
!is_eigen_tensor_map_v<T> &&
!is_eigen_tensor_ref_v<T>;

template<typename T, typename Scalar = typename T::Scalar>
using ndarray_for_eigen_tensor_t = ndarray<
Scalar,
numpy,
ndim<T::NumDimensions>,
std::conditional_t<
eigen_tensor_is_row_major_v<T>,
c_contig,
f_contig>>;

/** \brief Type caster for ``Eigen::TensorMap<T>``
*/
template<typename T, int MapOptions, template<class> class MakePointer>
struct type_caster<
Eigen::TensorMap<T, MapOptions, MakePointer>,
enable_if_t<is_ndarray_scalar_v<typename T::Scalar>>> {

using Scalar = typename T::Scalar;
using IndexType = typename T::Index;
static constexpr int NumIndices = T::NumIndices;
static constexpr int Options = T::Options;
using PlainTensor = Eigen::Tensor<Scalar, NumIndices, Options, IndexType>;
using Dimensions = typename T::Dimensions;
using MapType = Eigen::TensorMap<T, MapOptions, MakePointer>;
static constexpr bool IsAligned = MapType::IsAligned;

// Only partial specification. Dimensions not known at compile time...
using NDArray =
ndarray_for_eigen_tensor_t<T, std::conditional_t<std::is_const_v<T>,
const Scalar,
Scalar>>;
using NDArrayCaster = make_caster<NDArray>;
static constexpr auto Name = NDArrayCaster::Name;
template<typename T_> using Cast = MapType;
template<typename T_> static constexpr bool can_cast() { return true; };

NDArrayCaster caster;

bool from_python(handle src, uint8_t flags, cleanup_list *cleanup) noexcept {
// Disable implicit conversions
flags &= ~(uint8_t)cast_flags::convert;
// Do not accept None
flags &= ~(uint8_t)cast_flags::accepts_none;

if (!caster.from_python(src, flags, cleanup))
return false;
if(IsAligned && !is_tensor_aligned(caster.value.data()))
return false;

return true;
}

static handle from_cpp(const MapType &v, rv_policy policy, cleanup_list *cleanup) noexcept {
size_t shape[NumIndices];
for (size_t i = 0 ; i < NumIndices; i++) {
shape[i] = static_cast<size_t>(v.dimension(i));
Comment thread
ManifoldFR marked this conversation as resolved.
Outdated
}

void* ptr = (void *)v.data();
if (policy == rv_policy::automatic || policy == rv_policy::automatic_reference)
policy = rv_policy::reference;
return NDArrayCaster::from_cpp(
NDArray {ptr, NumIndices, shape, handle()},
policy,
cleanup);
}

operator MapType() {
NDArray &t = caster.value;
std::array<long, NumIndices> shape;
for (size_t i = 0 ; i < NumIndices; i++) {
shape[i] = t.shape(i);
}
return MapType(t.data(), shape);
}
};


/** \brief Type caster for plain ``Eigen::Tensor<T>`` types.
*/
template<typename Scalar, int NumIndices, int Options, typename IndexType>
struct type_caster<
Eigen::Tensor<Scalar, NumIndices, Options, IndexType>,
enable_if_t<is_ndarray_scalar_v<Scalar>>> {

using PlainTensor = Eigen::Tensor<Scalar, NumIndices, Options, IndexType>;
using Dimensions = typename PlainTensor::Dimensions;
using Coeffs = typename PlainTensor::CoeffReturnType;
static constexpr bool IsRowMajor = bool(Options & Eigen::RowMajorBit);
using NDArray = ndarray_for_eigen_tensor_t<PlainTensor>;
using NDArrayCaster = make_caster<NDArray>;

// PlainTensor value;
NB_TYPE_CASTER(PlainTensor, NDArrayCaster::Name);

bool from_python(handle src, uint8_t flags, cleanup_list *cleanup) noexcept {
using NDArrayConst = ndarray_for_eigen_tensor_t<PlainTensor, const Scalar>;
make_caster<NDArrayConst> caster;
// Do not accept None
if (!caster.from_python(src, flags & ~(uint8_t)cast_flags::accepts_none, cleanup))
return false;

const NDArrayConst &array = caster.value;
// copy tensor dims
std::array<long, NumIndices> out_dims;
for(size_t i = 0; i < NumIndices; i++) {
out_dims[i] = array.shape(i);
}
value.resize(out_dims);

memcpy(value.data(), array.data(), array.size() * sizeof(Scalar));

return true;
}

static handle from_cpp(const PlainTensor &v, rv_policy policy, cleanup_list *cleanup) noexcept {
size_t shape[NumIndices];

for (size_t i = 0 ; i < NumIndices; i++) {
shape[i] = static_cast<size_t>(v.dimension(i));
}

void *ptr = (void *)v.data();

object owner;
if (policy == rv_policy::move) {
PlainTensor *tmp = new PlainTensor((PlainTensor&&)v);
owner = capsule(tmp, [](void* p) noexcept {
delete (PlainTensor*) p;
});
ptr = tmp->data();
policy = rv_policy::reference;
} else if (policy == rv_policy::reference_internal && cleanup->self()) {
owner = borrow(cleanup->self());
policy = rv_policy::reference;
}
return NDArrayCaster::from_cpp(
NDArray {ptr, NumIndices, shape, owner},
policy, cleanup);
}
};

/** \brief Type caster for Tensor expressions. From-cpp conversion just converts the expression to a plain Tensor object.
*/
template<typename T>
struct type_caster<T, enable_if_t<is_eigen_tensor_xpr_v<T> && is_ndarray_scalar_v<typename T::Scalar>>> {
static constexpr int NumDimensions = T::NumDimensions;
static constexpr int Options = T::Options;
using IndexType = typename T::Index;
using XprTraits = typename Eigen::internal::traits<T>;
static constexpr int Layout = XprTraits::Layout;
using PlainTensor = Eigen::Tensor<typename T::Scalar, NumDimensions, Layout, IndexType>;
using Caster = make_caster<PlainTensor>;
static constexpr auto Name = Caster::Name;
template<typename T_> using Cast = T;
template<typename T_> static constexpr bool can_cast() { return true; }

/// Generating an expression template from a Python object is impossible.
bool from_python(handle src, uint8_t flags, cleanup_list *cleanup) noexcept = delete;

template <typename T2>
static handle from_cpp(T2 &&v, rv_policy policy, cleanup_list *cleanup) noexcept {
return Caster::from_cpp(std::forward<T2>(v), policy, cleanup);
}
};

/** \brief Type caster for ``Eigen::TensorRef<T>``
*/
template<typename T>
struct type_caster<
Eigen::TensorRef<T>,
enable_if_t<is_ndarray_scalar_v<typename T::Scalar>>> {

using Scalar = typename T::Scalar;
using IndexType = typename T::Index;
static constexpr int NumIndices = T::NumIndices;
static constexpr int Options = T::Options;
using PlainTensor = Eigen::Tensor<Scalar, NumIndices, Options, IndexType>;
using Dimensions = typename T::Dimensions;

// Only partial specification. Dimensions not known at compile time...
using NDArray =
ndarray_for_eigen_tensor_t<T, std::conditional_t<std::is_const_v<T>,
const Scalar,
Scalar>>;
using NDArrayCaster = make_caster<NDArray>;

using MapType = Eigen::TensorMap<T>;
using MapCaster = make_caster<MapType>;

using RefType = Eigen::TensorRef<T>;


static constexpr bool MaybeConvert = std::is_const_v<T>;
using PlainCaster = make_caster<PlainTensor>;

static constexpr auto Name = const_name<MaybeConvert>(PlainCaster::Name, MapCaster::Name);

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

MapCaster caster;
struct Empty {};
std::conditional_t<MaybeConvert, PlainCaster, Empty> plain_caster;

bool from_python(handle src, uint8_t flags, cleanup_list *cleanup) noexcept {
// no conversion for mutable Ref
if constexpr (!std::is_const_v<T>)
flags &= ~(uint8_t) cast_flags::convert;

// Try direct cast
if (caster.from_python(src, flags, cleanup))
return true;

// if const T, attempt leveraging PlainTensor conversion
if constexpr (MaybeConvert) {
// we create a new temporary tensor object, and
// its lifetime is that of plain_caster.
// for manual conversion, disable conversion.
if ((flags & (uint8_t) cast_flags::manual))
flags &= ~(uint8_t) cast_flags::convert;
if (plain_caster.from_python(src, flags, cleanup))
return true;
}

return false;
}

static handle from_cpp(const RefType &v, rv_policy policy, cleanup_list *cleanup) noexcept {
size_t shape[NumIndices];

for (size_t i = 0; i < NumIndices; i++) {
shape[i] = static_cast<size_t>(v.dimension(i));
}

return NDArrayCaster::from_cpp(
NDArray((void *) v.data(), NumIndices, shape, handle()),
(policy == rv_policy::automatic ||
policy == rv_policy::automatic_reference)
? rv_policy::reference
: policy,
cleanup);
}

operator RefType() {
if constexpr (MaybeConvert) {
// if there's a value, return it
if (plain_caster.caster.value.is_valid())
return RefType(plain_caster.operator PlainTensor&());
}
return RefType(caster.operator MapType());
}
};


NAMESPACE_END(detail)

NAMESPACE_END(NB_NAMESPACE)
3 changes: 3 additions & 0 deletions tests/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,8 @@ find_package (Eigen3 3.3.1 NO_MODULE)
if (TARGET Eigen3::Eigen)
nanobind_add_module(test_eigen_ext test_eigen.cpp ${NB_EXTRA_ARGS})
target_link_libraries(test_eigen_ext PRIVATE Eigen3::Eigen)
nanobind_add_module(test_eigen_tensor_ext test_eigen_tensor.cpp ${NB_EXTRA_ARGS})
target_link_libraries(test_eigen_tensor_ext PRIVATE Eigen3::Eigen)
endif()

add_library(
Expand All @@ -159,6 +161,7 @@ set(TEST_FILES
test_callbacks.py
test_classes.py
test_eigen.py
test_eigen_tensor.py
test_enum.py
test_eval.py
test_exception.py
Expand Down
Loading
Loading