use crate::{
error::DiffsolRtError,
scalar_type::{Scalar, ScalarType},
};
use diffsol::{
error::DiffsolError, Context, DenseMatrix, FaerScalar, Matrix, MatrixCommon, NalgebraScalar,
Vector, VectorCommon,
};
use num_traits::FromPrimitive;
use std::any::Any;
use std::marker::PhantomData;
use std::sync::Arc;
pub const MAX_NDIM: usize = 3;
pub(crate) trait ToTensor {
fn to_tensor(self) -> Tensor;
}
pub(crate) trait FromTensor: Sized {
type Context;
fn from_tensor(tensor: TensorView<'_>, ctx: &Self::Context) -> Result<Self, DiffsolRtError>;
}
pub(crate) fn column_major_values<T: FromPrimitive>(
tensor: TensorView<'_>,
) -> Result<Vec<T>, DiffsolRtError> {
Ok(match tensor.dtype() {
ScalarType::F32 => tensor
.columns::<f32>()?
.flatten()
.map(|&x| T::from_f32(x).unwrap())
.collect(),
ScalarType::F64 => tensor
.columns::<f64>()?
.flatten()
.map(|&x| T::from_f64(x).unwrap())
.collect(),
})
}
fn lane_values<T: FromPrimitive + Clone>(
tensor: TensorView<'_>,
nbatch: usize,
) -> Result<Vec<T>, DiffsolRtError> {
let nlanes = *tensor.shape().last().unwrap();
if nlanes != nbatch && nlanes != 1 {
return Err(
DiffsolError::Other(format!("Expected 1 or {nbatch} lanes, got {nlanes}")).into(),
);
}
let values: Vec<T> = column_major_values(tensor)?;
if nlanes == nbatch {
return Ok(values);
}
let mut lanes = Vec::with_capacity(values.len() * nbatch);
for _ in 0..nbatch {
lanes.extend_from_slice(&values);
}
Ok(lanes)
}
fn vector_from<V: Vector<T: Scalar>>(
tensor: TensorView<'_>,
ctx: &V::C,
) -> Result<V, DiffsolRtError> {
tensor.expect_ndim(2)?;
Ok(V::from_vec(lane_values(tensor, ctx.nbatch())?, ctx.clone()))
}
fn dense_matrix_from<M: DenseMatrix<T: Scalar>>(
tensor: TensorView<'_>,
ctx: &M::C,
) -> Result<M, DiffsolRtError> {
tensor.expect_ndim(3)?;
let (nrows, ncols) = (tensor.shape()[0], tensor.shape()[1]);
let data = lane_values(tensor, ctx.nbatch())?;
Ok(M::from_vec(nrows, ncols, data, ctx.clone()))
}
impl<T: Scalar + FaerScalar> FromTensor for diffsol::FaerVec<T> {
type Context = diffsol::FaerContext;
fn from_tensor(tensor: TensorView<'_>, ctx: &Self::Context) -> Result<Self, DiffsolRtError> {
vector_from(tensor, ctx)
}
}
impl<T: Scalar + NalgebraScalar> FromTensor for diffsol::NalgebraVec<T> {
type Context = diffsol::NalgebraContext;
fn from_tensor(tensor: TensorView<'_>, ctx: &Self::Context) -> Result<Self, DiffsolRtError> {
vector_from(tensor, ctx)
}
}
impl<T: Scalar + FaerScalar> FromTensor for diffsol::FaerMat<T> {
type Context = diffsol::FaerContext;
fn from_tensor(tensor: TensorView<'_>, ctx: &Self::Context) -> Result<Self, DiffsolRtError> {
dense_matrix_from(tensor, ctx)
}
}
impl<T: Scalar + NalgebraScalar> FromTensor for diffsol::NalgebraMat<T> {
type Context = diffsol::NalgebraContext;
fn from_tensor(tensor: TensorView<'_>, ctx: &Self::Context) -> Result<Self, DiffsolRtError> {
dense_matrix_from(tensor, ctx)
}
}
fn faer_layout<T: Scalar + FaerScalar>(m: &faer::Mat<T>, ptr: *const T) -> RawLayout {
let size = std::mem::size_of::<T>();
let strides = [
m.row_stride() as usize * size,
m.col_stride() as usize * size,
];
let shape = [m.nrows(), m.ncols()];
RawLayout::valid(ptr as *const u8, &shape, &strides, T::scalar_type())
}
fn nalgebra_layout<T: Scalar + NalgebraScalar>(
m: &nalgebra::DMatrix<T>,
ptr: *const T,
) -> RawLayout {
let size = std::mem::size_of::<T>();
let (rs, cs) = m.strides();
let shape = [m.nrows(), m.ncols()];
RawLayout::valid(
ptr as *const u8,
&shape,
&[rs * size, cs * size],
T::scalar_type(),
)
}
fn matrix_lanes(storage: RawLayout, nbatch: usize) -> RawLayout {
let (nrows, ncols) = (storage.shape[0], storage.shape[1] / nbatch);
let (row_stride, col_stride) = (storage.strides[0], storage.strides[1]);
storage.with_layout(
&[nrows, ncols, nbatch],
&[row_stride, col_stride, ncols * col_stride],
)
}
pub unsafe trait TensorLayout {
fn tensor_layout(&self) -> RawLayout;
}
pub unsafe trait TensorLayoutMut: TensorLayout {
fn tensor_layout_mut(&mut self) -> RawLayout;
}
unsafe impl<T: Scalar + FaerScalar> TensorLayout for diffsol::FaerVec<T> {
fn tensor_layout(&self) -> RawLayout {
faer_layout(self.inner(), self.inner().as_ptr())
}
}
unsafe impl<T: Scalar + FaerScalar> TensorLayoutMut for diffsol::FaerVec<T> {
fn tensor_layout_mut(&mut self) -> RawLayout {
let ptr = self.inner_mut().as_ptr_mut();
faer_layout(self.inner(), ptr)
}
}
unsafe impl<T: Scalar + NalgebraScalar> TensorLayout for diffsol::NalgebraVec<T> {
fn tensor_layout(&self) -> RawLayout {
nalgebra_layout(self.inner(), self.inner().as_ptr())
}
}
unsafe impl<T: Scalar + NalgebraScalar> TensorLayoutMut for diffsol::NalgebraVec<T> {
fn tensor_layout_mut(&mut self) -> RawLayout {
let ptr = self.inner_mut().as_mut_ptr();
nalgebra_layout(self.inner(), ptr)
}
}
unsafe impl<T: Scalar + FaerScalar> TensorLayout for diffsol::FaerMat<T> {
fn tensor_layout(&self) -> RawLayout {
let layout = faer_layout(self.inner(), self.inner().as_ptr());
matrix_lanes(layout, self.context().nbatch())
}
}
unsafe impl<T: Scalar + NalgebraScalar> TensorLayout for diffsol::NalgebraMat<T> {
fn tensor_layout(&self) -> RawLayout {
let layout = nalgebra_layout(self.inner(), self.inner().as_ptr());
matrix_lanes(layout, self.context().nbatch())
}
}
unsafe impl<T: Scalar> TensorLayout for Vec<T> {
fn tensor_layout(&self) -> RawLayout {
RawLayout::valid_col_major(self.as_ptr() as *const u8, &[self.len()], T::scalar_type())
}
}
impl<V: TensorLayout + 'static> ToTensor for V {
fn to_tensor(self) -> Tensor {
let layout = self.tensor_layout();
unsafe { Tensor::from_layout(self, layout) }
}
}
pub(crate) fn shared_tensor<O, V>(owner: Arc<O>, part: impl FnOnce(&O) -> &V) -> Tensor
where
O: ?Sized + 'static,
V: TensorLayout,
{
let layout = part(&owner).tensor_layout();
unsafe { Tensor::from_layout(owner, layout) }
}
fn scalar_size(dtype: ScalarType) -> usize {
match dtype {
ScalarType::F32 => std::mem::size_of::<f32>(),
ScalarType::F64 => std::mem::size_of::<f64>(),
}
}
fn layout_error(message: String) -> DiffsolRtError {
DiffsolError::Other(message).into()
}
fn check_layout(
ptr: *const u8,
shape: &[usize],
strides: &[usize],
dtype: ScalarType,
) -> Result<(), DiffsolRtError> {
let ndim = shape.len();
if ndim == 0 || ndim > MAX_NDIM {
return Err(layout_error(format!(
"A tensor has 1 to {MAX_NDIM} axes, got {ndim}"
)));
}
if strides.len() != ndim {
return Err(layout_error(format!(
"Expected {ndim} strides, got {}",
strides.len()
)));
}
let size = scalar_size(dtype);
if shape[0] > 1 && strides[0] != size {
return Err(layout_error(format!(
"The values of a column are not adjacent: strides[0] is {} bytes, expected {size}",
strides[0]
)));
}
let strides_aligned = shape
.iter()
.zip(strides)
.all(|(&n, &stride)| n <= 1 || stride.is_multiple_of(size));
if !(ptr as usize).is_multiple_of(size) || !strides_aligned {
return Err(layout_error("The data is not aligned".to_string()));
}
Ok(())
}
fn check_dtype<T: Scalar>(dtype: ScalarType) -> Result<(), DiffsolRtError> {
if dtype != T::scalar_type() {
return Err(layout_error("Data type mismatch".to_string()));
}
Ok(())
}
fn byte_offset(shape: &[usize], strides: &[usize], index: &[usize]) -> usize {
assert_eq!(index.len(), shape.len(), "wrong number of indices");
index
.iter()
.zip(shape)
.zip(strides)
.map(|((&i, &n), &stride)| {
assert!(i < n, "index {i} is out of range for an axis of length {n}");
i * stride
})
.sum()
}
#[repr(C)]
#[derive(Clone, Copy, Debug)]
pub struct RawLayout {
ptr: *const u8,
dtype: ScalarType,
ndim: usize,
shape: [usize; MAX_NDIM],
strides: [usize; MAX_NDIM],
}
impl RawLayout {
fn new(
ptr: *const u8,
shape: &[usize],
strides: &[usize],
dtype: ScalarType,
) -> Result<Self, DiffsolRtError> {
check_layout(ptr, shape, strides, dtype)?;
let ndim = shape.len();
let mut layout = Self {
ptr,
dtype,
ndim,
shape: [0; MAX_NDIM],
strides: [0; MAX_NDIM],
};
layout.shape[..ndim].copy_from_slice(shape);
layout.strides[..ndim].copy_from_slice(strides);
Ok(layout)
}
fn col_major(
ptr: *const u8,
shape: &[usize],
dtype: ScalarType,
) -> Result<Self, DiffsolRtError> {
let mut strides = [0; MAX_NDIM];
let mut stride = scalar_size(dtype);
for (s, &n) in strides.iter_mut().zip(shape) {
*s = stride;
stride *= n;
}
Self::new(ptr, shape, &strides[..shape.len().min(MAX_NDIM)], dtype)
}
fn valid(ptr: *const u8, shape: &[usize], strides: &[usize], dtype: ScalarType) -> Self {
Self::new(ptr, shape, strides, dtype)
.unwrap_or_else(|err| panic!("invalid tensor layout: {err}"))
}
fn valid_col_major(ptr: *const u8, shape: &[usize], dtype: ScalarType) -> Self {
Self::col_major(ptr, shape, dtype)
.unwrap_or_else(|err| panic!("invalid tensor layout: {err}"))
}
fn shape(&self) -> &[usize] {
&self.shape[..self.ndim]
}
fn strides(&self) -> &[usize] {
&self.strides[..self.ndim]
}
fn with_layout(self, shape: &[usize], strides: &[usize]) -> Self {
assert_eq!(
shape.iter().product::<usize>(),
self.shape().iter().product::<usize>()
);
let last_byte = |shape: &[usize], strides: &[usize]| -> usize {
shape
.iter()
.zip(strides)
.map(|(n, s)| n.saturating_sub(1) * s)
.sum()
};
assert!(last_byte(shape, strides) <= last_byte(self.shape(), self.strides()));
Self::valid(self.ptr, shape, strides, self.dtype)
}
pub(crate) fn batch_lanes(self, ninner: usize, nouter: usize) -> Self {
assert_eq!(self.ndim, 2, "a vector layout has 2 axes");
let (n, nlanes) = (self.shape[0], self.shape[1]);
let (s, c) = (self.strides[0], self.strides[1]);
let strides = if nlanes == ninner * nouter {
[s, c, ninner * c]
} else if nlanes == nouter {
[s, 0, c]
} else if nlanes == 1 {
[s, 0, 0]
} else {
panic!("cannot broadcast {nlanes} lanes into {ninner} x {nouter} lanes");
};
Self::valid(self.ptr, &[n, ninner, nouter], &strides, self.dtype)
}
fn check_no_overlap(&self) -> Result<(), DiffsolRtError> {
let mut extent = scalar_size(self.dtype);
for (&n, &stride) in self.shape().iter().zip(self.strides()) {
if n > 1 {
if stride < extent {
return Err(layout_error(
"The values of a mutable array overlap".to_string(),
));
}
extent += (n - 1) * stride;
}
}
Ok(())
}
fn truncate_axis(mut self, axis: usize, len: usize) -> Self {
let n = self.shape()[axis];
assert!(
len <= n,
"cannot keep {len} indices of an axis of length {n}"
);
self.shape[axis] = len;
self
}
fn index_ptr<T: Scalar>(&self, index: &[usize]) -> Result<*const T, DiffsolRtError> {
check_dtype::<T>(self.dtype)?;
let offset = byte_offset(self.shape(), self.strides(), index);
Ok(self.ptr.wrapping_add(offset) as *const T)
}
fn column_ptr<T: Scalar>(&self, index: &[usize]) -> Result<(*const T, usize), DiffsolRtError> {
check_dtype::<T>(self.dtype)?;
let offset = byte_offset(&self.shape()[1..], &self.strides()[1..], index);
Ok((self.ptr.wrapping_add(offset) as *const T, self.shape[0]))
}
unsafe fn index<'a, T: Scalar>(&self, index: &[usize]) -> Result<&'a T, DiffsolRtError> {
let ptr = self.index_ptr::<T>(index)?;
Ok(unsafe { &*ptr })
}
unsafe fn column_of_type<'a, T: Scalar>(&self, index: &[usize]) -> &'a [T] {
let offset = byte_offset(&self.shape()[1..], &self.strides()[1..], index);
let len = self.shape[0];
if len == 0 {
return &[];
}
unsafe { std::slice::from_raw_parts(self.ptr.add(offset) as *const T, len) }
}
unsafe fn column<'a, T: Scalar>(&self, index: &[usize]) -> Result<&'a [T], DiffsolRtError> {
check_dtype::<T>(self.dtype)?;
Ok(unsafe { self.column_of_type(index) })
}
unsafe fn columns<'a, T: Scalar>(
&self,
) -> Result<impl Iterator<Item = &'a [T]> + 'a, DiffsolRtError> {
check_dtype::<T>(self.dtype)?;
let layout = *self;
let ncolumns: usize = layout.shape()[1..].iter().product();
Ok((0..ncolumns).map(move |mut c| {
let mut index = [0; MAX_NDIM];
for (i, &n) in index.iter_mut().zip(&layout.shape()[1..]) {
*i = c % n;
c /= n;
}
unsafe { layout.column_of_type(&index[..layout.ndim - 1]) }
}))
}
}
pub struct Tensor {
layout: RawLayout,
_owner: Box<dyn Any>,
}
impl Tensor {
pub unsafe fn new<O: 'static>(
owner: O,
ptr: *const u8,
shape: &[usize],
strides: &[usize],
dtype: ScalarType,
) -> Self {
let layout = RawLayout::valid(ptr, shape, strides, dtype);
unsafe { Self::from_layout(owner, layout) }
}
unsafe fn from_layout<O: 'static>(owner: O, layout: RawLayout) -> Self {
Self {
layout,
_owner: Box::new(owner),
}
}
pub fn from_vec<T: Scalar + 'static>(values: Vec<T>, shape: &[usize]) -> Self {
assert_eq!(
values.len(),
shape.iter().product::<usize>(),
"the number of values must be the product of the shape"
);
let layout =
RawLayout::valid_col_major(values.as_ptr() as *const u8, shape, T::scalar_type());
unsafe { Self::from_layout(values, layout) }
}
pub fn view(&self) -> TensorView<'_> {
TensorView {
layout: self.layout,
_data: PhantomData,
}
}
pub fn shape(&self) -> &[usize] {
self.layout.shape()
}
pub fn strides(&self) -> &[usize] {
self.layout.strides()
}
pub fn data_ptr(&self) -> *const u8 {
self.layout.ptr
}
pub fn dtype(&self) -> ScalarType {
self.layout.dtype
}
pub(crate) fn with_layout(mut self, shape: &[usize], strides: &[usize]) -> Self {
self.layout = self.layout.with_layout(shape, strides);
self
}
pub(crate) fn truncate_axis(mut self, axis: usize, len: usize) -> Self {
self.layout = self.layout.truncate_axis(axis, len);
self
}
pub fn index<T: Scalar>(&self, index: impl AsRef<[usize]>) -> Result<&T, DiffsolRtError> {
self.view().index(index)
}
pub fn column<T: Scalar>(&self, index: &[usize]) -> Result<&[T], DiffsolRtError> {
self.view().column(index)
}
pub fn columns<T: Scalar>(&self) -> Result<impl Iterator<Item = &[T]> + '_, DiffsolRtError> {
self.view().columns()
}
}
impl<'a> From<&'a Tensor> for TensorView<'a> {
fn from(tensor: &'a Tensor) -> Self {
tensor.view()
}
}
#[repr(transparent)]
#[derive(Clone, Copy, Debug)]
pub struct TensorView<'a> {
layout: RawLayout,
_data: PhantomData<&'a [u8]>,
}
impl<'a> TensorView<'a> {
pub unsafe fn new(
ptr: *const u8,
shape: &[usize],
strides: &[usize],
dtype: ScalarType,
) -> Result<Self, DiffsolRtError> {
Ok(Self {
layout: RawLayout::new(ptr, shape, strides, dtype)?,
_data: PhantomData,
})
}
pub unsafe fn new_col_major(ptr: *const u8, shape: &[usize], dtype: ScalarType) -> Self {
Self {
layout: RawLayout::valid_col_major(ptr, shape, dtype),
_data: PhantomData,
}
}
pub fn from_slice<T: Scalar>(values: &'a [T], shape: &[usize]) -> Self {
assert_eq!(
values.len(),
shape.iter().product::<usize>(),
"the number of values must be the product of the shape"
);
unsafe { Self::new_col_major(values.as_ptr() as *const u8, shape, T::scalar_type()) }
}
pub fn shape(&self) -> &[usize] {
self.layout.shape()
}
pub fn strides(&self) -> &[usize] {
self.layout.strides()
}
pub fn data_ptr(&self) -> *const u8 {
self.layout.ptr
}
pub fn dtype(&self) -> ScalarType {
self.layout.dtype
}
pub(crate) fn expect_ndim(&self, expected: usize) -> Result<(), DiffsolRtError> {
let ndim = self.layout.ndim;
if ndim != expected {
return Err(layout_error(format!(
"Expected a {expected}D array, got a {ndim}D array"
)));
}
Ok(())
}
pub fn index<T: Scalar>(&self, index: impl AsRef<[usize]>) -> Result<&'a T, DiffsolRtError> {
unsafe { self.layout.index(index.as_ref()) }
}
pub fn column<T: Scalar>(&self, index: &[usize]) -> Result<&'a [T], DiffsolRtError> {
unsafe { self.layout.column(index) }
}
pub fn columns<T: Scalar>(&self) -> Result<impl Iterator<Item = &'a [T]> + 'a, DiffsolRtError> {
unsafe { self.layout.columns() }
}
}
impl TensorView<'_> {
pub(crate) unsafe fn from_layout(layout: RawLayout) -> Self {
Self {
layout,
_data: PhantomData,
}
}
}
#[repr(transparent)]
#[derive(Debug)]
pub struct TensorViewMut<'a> {
layout: RawLayout,
_data: PhantomData<&'a mut [u8]>,
}
impl<'a> TensorViewMut<'a> {
pub unsafe fn new(
ptr: *mut u8,
shape: &[usize],
strides: &[usize],
dtype: ScalarType,
) -> Result<Self, DiffsolRtError> {
let layout = RawLayout::new(ptr, shape, strides, dtype)?;
layout.check_no_overlap()?;
Ok(Self {
layout,
_data: PhantomData,
})
}
pub(crate) unsafe fn from_layout(layout: RawLayout) -> Self {
layout
.check_no_overlap()
.unwrap_or_else(|err| panic!("invalid mutable tensor layout: {err}"));
Self {
layout,
_data: PhantomData,
}
}
pub fn from_slice_mut<T: Scalar>(values: &'a mut [T], shape: &[usize]) -> Self {
assert_eq!(
values.len(),
shape.iter().product::<usize>(),
"the number of values must be the product of the shape"
);
let layout =
RawLayout::valid_col_major(values.as_mut_ptr() as *const u8, shape, T::scalar_type());
unsafe { Self::from_layout(layout) }
}
pub fn as_view(&self) -> TensorView<'_> {
TensorView {
layout: self.layout,
_data: PhantomData,
}
}
pub fn shape(&self) -> &[usize] {
self.layout.shape()
}
pub fn strides(&self) -> &[usize] {
self.layout.strides()
}
pub fn data_ptr(&mut self) -> *mut u8 {
self.layout.ptr as *mut u8
}
pub fn dtype(&self) -> ScalarType {
self.layout.dtype
}
pub fn index<T: Scalar>(&self, index: impl AsRef<[usize]>) -> Result<&T, DiffsolRtError> {
unsafe { self.layout.index(index.as_ref()) }
}
pub fn column<T: Scalar>(&self, index: &[usize]) -> Result<&[T], DiffsolRtError> {
unsafe { self.layout.column(index) }
}
pub fn index_mut<T: Scalar>(
&mut self,
index: impl AsRef<[usize]>,
) -> Result<&mut T, DiffsolRtError> {
let ptr = self.layout.index_ptr::<T>(index.as_ref())? as *mut T;
Ok(unsafe { &mut *ptr })
}
pub fn column_mut<T: Scalar>(&mut self, index: &[usize]) -> Result<&mut [T], DiffsolRtError> {
let (ptr, len) = self.layout.column_ptr::<T>(index)?;
if len == 0 {
return Ok(&mut []);
}
Ok(unsafe { std::slice::from_raw_parts_mut(ptr as *mut T, len) })
}
}
#[cfg(test)]
mod tests {
use super::{FromTensor, Tensor, TensorLayout, TensorView, TensorViewMut, ToTensor};
use diffsol::{
Context, DenseMatrix, FaerContext, FaerMat, FaerVec, NalgebraContext, NalgebraMat,
NalgebraVec, Vector,
};
fn vector_value(b: usize, i: usize) -> f64 {
10.0 * b as f64 + i as f64
}
fn at(tensor: &Tensor, index: &[usize]) -> f64 {
*tensor.index::<f64>(index).unwrap()
}
fn all_values(tensor: &Tensor) -> Vec<f64> {
let mut indices = vec![vec![]];
for &n in tensor.shape().iter().rev() {
indices = indices
.into_iter()
.flat_map(|index: Vec<usize>| {
(0..n).map(move |i| [vec![i], index.clone()].concat())
})
.collect();
}
indices.iter().map(|index| at(tensor, index)).collect()
}
fn matrix_value(b: usize, i: usize, j: usize) -> f64 {
100.0 * b as f64 + 10.0 * i as f64 + j as f64
}
fn check_vector_round_trip<V>(ctx: V::C, nbatch: usize)
where
V: Vector<T = f64> + ToTensor + FromTensor<Context = V::C>,
{
let n = 3;
let ctx = ctx.clone_with_nbatch(nbatch).unwrap();
let values = (0..nbatch)
.flat_map(|b| (0..n).map(move |i| vector_value(b, i)))
.collect();
let v = V::from_vec(values, ctx.clone());
let tensor = v.clone().to_tensor();
assert_eq!(tensor.shape(), &[n, nbatch]);
for b in 0..nbatch {
let lane: Vec<f64> = (0..n).map(|i| vector_value(b, i)).collect();
assert_eq!(
(0..n).map(|i| at(&tensor, &[i, b])).collect::<Vec<_>>(),
lane
);
}
let back = V::from_tensor(tensor.view(), &ctx).unwrap();
assert_eq!(back.context().nbatch(), nbatch);
assert_eq!(back.clone_as_vec(), v.clone_as_vec());
}
fn check_matrix_round_trip<M>(ctx: M::C, nbatch: usize)
where
M: DenseMatrix<T = f64> + ToTensor + FromTensor<Context = M::C>,
{
let (nrows, ncols) = (2, 3);
let ctx = ctx.clone_with_nbatch(nbatch).unwrap();
let mut values = vec![0.0; nbatch * nrows * ncols];
for b in 0..nbatch {
for i in 0..nrows {
for j in 0..ncols {
values[(b * ncols + j) * nrows + i] = matrix_value(b, i, j);
}
}
}
let m = M::from_vec(nrows, ncols, values, ctx.clone());
let tensor = m.to_tensor();
assert_eq!(tensor.shape(), &[nrows, ncols, nbatch]);
for b in 0..nbatch {
for i in 0..nrows {
for j in 0..ncols {
assert_eq!(at(&tensor, &[i, j, b]), matrix_value(b, i, j));
}
}
}
let back = M::from_tensor(tensor.view(), &ctx).unwrap();
let again = back.to_tensor();
assert_eq!(all_values(&again), all_values(&tensor));
}
#[test]
fn vectors_round_trip_with_a_lane_axis() {
for nbatch in [1, 3] {
check_vector_round_trip::<NalgebraVec<f64>>(NalgebraContext::default(), nbatch);
check_vector_round_trip::<FaerVec<f64>>(FaerContext::default(), nbatch);
}
}
#[test]
fn dense_matrices_round_trip_with_a_lane_axis() {
for nbatch in [1, 3] {
check_matrix_round_trip::<NalgebraMat<f64>>(NalgebraContext::default(), nbatch);
check_matrix_round_trip::<FaerMat<f64>>(FaerContext::default(), nbatch);
}
}
#[test]
fn one_lane_broadcasts_and_other_lane_counts_are_rejected() {
let ctx = NalgebraContext::default().clone_with_nbatch(3).unwrap();
let one_lane = Tensor::from_vec(vec![1.0, 2.0], &[2, 1]);
let v = NalgebraVec::<f64>::from_tensor(one_lane.view(), &ctx).unwrap();
assert_eq!(v.context().nbatch(), 3);
assert_eq!(v.clone_as_vec(), vec![1.0, 2.0, 1.0, 2.0, 1.0, 2.0]);
let one_lane = Tensor::from_vec(vec![1.0, 2.0, 3.0, 4.0], &[2, 2, 1]);
let m = NalgebraMat::<f64>::from_tensor(one_lane.view(), &ctx).unwrap();
assert_eq!(diffsol::Matrix::context(&m).nbatch(), 3);
let all = all_values(&m.to_tensor());
assert_eq!(all, [1.0, 2.0, 3.0, 4.0].repeat(3));
let two_lanes = Tensor::from_vec(vec![1.0, 2.0, 3.0, 4.0], &[2, 2]);
let err = NalgebraVec::<f64>::from_tensor(two_lanes.view(), &ctx)
.unwrap_err()
.to_string();
assert!(err.contains("Expected 1 or 3 lanes, got 2"), "{err}");
let no_lane = TensorView::from_slice(&[1.0, 2.0], &[2]);
let err = NalgebraVec::<f64>::from_tensor(no_lane, &ctx)
.unwrap_err()
.to_string();
assert!(err.contains("Expected a 2D array, got a 1D array"), "{err}");
}
#[test]
fn shared_tensors_keep_the_owner_alive() {
struct Owner {
values: Vec<f64>,
}
let owner = std::sync::Arc::new(Owner {
values: vec![1.0, 2.0, 3.0],
});
let tensor = super::shared_tensor(owner.clone(), |o| &o.values);
assert_eq!(tensor.data_ptr(), owner.values.as_ptr() as *const u8);
assert_eq!(std::sync::Arc::strong_count(&owner), 2);
drop(owner);
assert_eq!(all_values(&tensor), vec![1.0, 2.0, 3.0]);
}
#[test]
fn truncate_axis_keeps_the_first_indices() {
let values: Vec<f64> = (0..12).map(f64::from).collect();
let tensor = Tensor::from_vec(values, &[2, 3, 2]).truncate_axis(1, 2);
assert_eq!(tensor.shape(), &[2, 2, 2]);
assert_eq!(
all_values(&tensor),
vec![0.0, 1.0, 2.0, 3.0, 6.0, 7.0, 8.0, 9.0]
);
}
#[test]
#[should_panic(expected = "cannot keep 4 indices of an axis of length 3")]
fn truncate_axis_cannot_make_an_axis_longer() {
Tensor::from_vec(vec![0.0f64; 3], &[3]).truncate_axis(0, 4);
}
#[test]
fn with_layout_keeps_the_values() {
let values: Vec<f64> = (0..12).map(f64::from).collect();
let s = std::mem::size_of::<f64>();
let tensor =
Tensor::from_vec(values.clone(), &[6, 2]).with_layout(&[3, 2, 2], &[s, 3 * s, 6 * s]);
assert_eq!(tensor.shape(), &[3, 2, 2]);
assert_eq!(all_values(&tensor), values);
}
#[test]
fn column_gives_contiguous_columns_and_index_uses_the_strides() {
let data = [1.0f64, 2.0, -1.0, 3.0, 4.0, -1.0];
let s = std::mem::size_of::<f64>();
let view = unsafe {
TensorView::new(
data.as_ptr() as *const u8,
&[2, 2],
&[s, 3 * s],
super::ScalarType::F64,
)
}
.unwrap();
assert_eq!(view.column::<f64>(&[0]).unwrap(), &[1.0, 2.0]);
assert_eq!(view.column::<f64>(&[1]).unwrap(), &[3.0, 4.0]);
let columns: Vec<&[f64]> = view.columns::<f64>().unwrap().collect();
assert_eq!(columns, vec![&[1.0, 2.0][..], &[3.0, 4.0][..]]);
assert_eq!(*view.index::<f64>([1, 1]).unwrap(), 4.0);
let error = view.column::<f32>(&[0]).unwrap_err().to_string();
assert!(error.contains("Data type mismatch"), "{error}");
assert!(view.columns::<f32>().is_err());
assert!(view.index::<f32>([0, 0]).is_err());
}
#[test]
fn views_reject_layouts_without_contiguous_columns() {
let data = [1.0f64, 2.0, 3.0, 4.0];
let ptr = data.as_ptr() as *const u8;
let s = std::mem::size_of::<f64>();
let f64_type = super::ScalarType::F64;
let error = |shape: &[usize], strides: &[usize], ptr: *const u8| {
unsafe { TensorView::new(ptr, shape, strides, f64_type) }
.unwrap_err()
.to_string()
};
let err = error(&[2, 2], &[2 * s, s], ptr);
assert!(
err.contains("The values of a column are not adjacent"),
"{err}"
);
let err = error(&[2], &[s], ptr.wrapping_add(1));
assert!(err.contains("The data is not aligned"), "{err}");
let err = error(&[1, 2], &[s, s + 1], ptr);
assert!(err.contains("The data is not aligned"), "{err}");
let err = error(&[1, 1, 1, 1], &[s; 4], ptr);
assert!(err.contains("A tensor has 1 to 3 axes, got 4"), "{err}");
let err = error(&[2, 2], &[s], ptr);
assert!(err.contains("Expected 2 strides, got 1"), "{err}");
let view = unsafe { TensorView::new(ptr, &[1, 2], &[3, 2 * s], f64_type) }.unwrap();
assert_eq!(view.column::<f64>(&[1]).unwrap(), &[3.0]);
}
#[test]
fn views_from_slices_have_1_to_3_axes() {
let values: Vec<f64> = (0..12).map(f64::from).collect();
let s = std::mem::size_of::<f64>();
let v1 = TensorView::from_slice(&values, &[12]);
assert_eq!(v1.shape(), &[12]);
assert_eq!(v1.strides(), &[s]);
let v2 = TensorView::from_slice(&values, &[3, 4]);
assert_eq!(v2.shape(), &[3, 4]);
assert_eq!(v2.strides(), &[s, 3 * s]);
assert_eq!(*v2.index::<f64>([2, 1]).unwrap(), 5.0);
let v3 = TensorView::from_slice(&values, &[2, 3, 2]);
assert_eq!(v3.shape(), &[2, 3, 2]);
assert_eq!(v3.strides(), &[s, 2 * s, 6 * s]);
assert_eq!(*v3.index::<f64>([1, 2, 1]).unwrap(), 11.0);
v3.expect_ndim(3).unwrap();
assert!(v3.expect_ndim(2).is_err());
}
#[test]
#[should_panic(expected = "A tensor has 1 to 3 axes, got 4")]
fn views_from_slices_with_4_axes_panic() {
let values = [0.0f64; 16];
TensorView::from_slice(&values, &[2, 2, 2, 2]);
}
#[test]
fn view_of_an_array_has_the_same_layout_and_values() {
let ctx = FaerContext::default().clone_with_nbatch(2).unwrap();
let m = FaerMat::<f64>::from_vec(2, 3, (0..12).map(f64::from).collect(), ctx);
let tensor = m.to_tensor();
let view = tensor.view();
assert_eq!(view.shape(), tensor.shape());
assert_eq!(view.strides(), tensor.strides());
assert_eq!(view.dtype(), tensor.dtype());
assert_eq!(view.data_ptr(), tensor.data_ptr());
let view_columns: Vec<&[f64]> = view.columns().unwrap().collect();
let tensor_columns: Vec<&[f64]> = tensor.columns().unwrap().collect();
assert_eq!(view_columns, tensor_columns);
assert_eq!(
all_values(&tensor),
(0..12).map(f64::from).collect::<Vec<_>>()
);
let from: TensorView<'_> = (&tensor).into();
assert_eq!(from.shape(), tensor.shape());
}
#[test]
fn vectors_convert_from_views_with_padded_lanes() {
let data = [1.0f64, 2.0, 0.0, 0.0, 10.0, 20.0, 0.0, 0.0, 100.0, 200.0];
let s = std::mem::size_of::<f64>();
let view = unsafe {
TensorView::new(
data.as_ptr() as *const u8,
&[2, 3],
&[s, 4 * s],
super::ScalarType::F64,
)
}
.unwrap();
let ctx = NalgebraContext::default().clone_with_nbatch(3).unwrap();
let v = NalgebraVec::<f64>::from_tensor(view, &ctx).unwrap();
assert_eq!(v.clone_as_vec(), vec![1.0, 2.0, 10.0, 20.0, 100.0, 200.0]);
}
#[test]
fn f32_views_convert_to_f64_vectors() {
let data = [1.0f32, 2.0];
let view = TensorView::from_slice(&data, &[2, 1]);
let v = NalgebraVec::<f64>::from_tensor(view, &NalgebraContext::default()).unwrap();
assert_eq!(v.clone_as_vec(), vec![1.0, 2.0]);
}
#[test]
fn columns_skip_the_padding_of_faer_storage() {
let ctx = FaerContext::default().clone_with_nbatch(2).unwrap();
let v = FaerVec::<f64>::from_vec(vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0], ctx);
let tensor = v.to_tensor();
assert!(tensor.strides()[1] >= 3 * std::mem::size_of::<f64>());
let columns: Vec<&[f64]> = tensor.columns().unwrap().collect();
assert_eq!(columns, vec![&[1.0, 2.0, 3.0][..], &[4.0, 5.0, 6.0][..]]);
assert_eq!(tensor.column::<f64>(&[1]).unwrap(), &[4.0, 5.0, 6.0]);
let m = FaerMat::<f64>::from_vec(2, 3, (0..12).map(f64::from).collect(), ctx);
let tensor = m.to_tensor();
let columns: Vec<Vec<f64>> = tensor.columns().unwrap().map(<[f64]>::to_vec).collect();
let expected: Vec<Vec<f64>> = (0..6)
.map(|c| vec![2.0 * c as f64, 2.0 * c as f64 + 1.0])
.collect();
assert_eq!(columns, expected);
assert_eq!(tensor.column::<f64>(&[2, 1]).unwrap(), &[10.0, 11.0]);
}
#[test]
fn empty_axes_give_empty_columns_or_no_columns() {
let no_rows = Tensor::from_vec(Vec::<f64>::new(), &[0, 2]);
let columns: Vec<&[f64]> = no_rows.columns().unwrap().collect();
assert_eq!(columns, vec![&[] as &[f64]; 2]);
assert_eq!(no_rows.column::<f64>(&[1]).unwrap(), &[] as &[f64]);
let no_lanes = Tensor::from_vec(Vec::<f64>::new(), &[3, 0]);
assert_eq!(no_lanes.columns::<f64>().unwrap().count(), 0);
}
#[test]
fn metadata_gives_the_layout() {
let tensor = Tensor::from_vec((0..6).map(f64::from).collect(), &[2, 3]);
let s = std::mem::size_of::<f64>();
assert_eq!(tensor.shape(), &[2, 3]);
assert_eq!(tensor.strides(), &[s, 2 * s]);
assert_eq!(tensor.dtype(), super::ScalarType::F64);
let value = unsafe { *(tensor.data_ptr().add(s + 4 * s) as *const f64) };
assert_eq!(value, 5.0);
}
#[test]
fn batch_lanes_broadcast_with_strides_of_zero() {
let s = std::mem::size_of::<f64>();
let ctx = |nbatch| FaerContext::default().clone_with_nbatch(nbatch).unwrap();
let lanes = |nbatch: usize| -> Vec<f64> {
(0..nbatch)
.flat_map(|b| (0..2).map(move |i| vector_value(b, i)))
.collect()
};
let (ninner, nouter) = (3, 2);
let full = FaerVec::<f64>::from_vec(lanes(6), ctx(6));
let outer = FaerVec::<f64>::from_vec(lanes(2), ctx(2));
let one = FaerVec::<f64>::from_vec(lanes(1), ctx(1));
for (v, expected_lane) in [
(
&full,
Box::new(|k, b| b * ninner + k) as Box<dyn Fn(usize, usize) -> usize>,
),
(&outer, Box::new(|_, b| b)),
(&one, Box::new(|_, _| 0)),
] {
let layout = v.tensor_layout().batch_lanes(ninner, nouter);
let view = unsafe { TensorView::from_layout(layout) };
assert_eq!(view.shape(), &[2, ninner, nouter]);
assert_eq!(view.strides()[0], s);
for b in 0..nouter {
for k in 0..ninner {
let lane = expected_lane(k, b);
let expected = [vector_value(lane, 0), vector_value(lane, 1)];
assert_eq!(view.column::<f64>(&[k, b]).unwrap(), &expected);
}
}
}
let c = full.tensor_layout().strides()[1];
let strides = |v: &FaerVec<f64>| v.tensor_layout().batch_lanes(ninner, nouter).strides;
assert_eq!(strides(&full), [s, c, ninner * c]);
assert_eq!(
strides(&outer)[1..],
[0, outer.tensor_layout().strides()[1]]
);
assert_eq!(strides(&one)[1..], [0, 0]);
}
#[test]
#[should_panic(expected = "cannot broadcast 4 lanes into 3 x 2 lanes")]
fn batch_lanes_reject_other_lane_counts() {
let ctx = NalgebraContext::default().clone_with_nbatch(4).unwrap();
let v = NalgebraVec::<f64>::zeros(2, ctx);
v.tensor_layout().batch_lanes(3, 2);
}
#[test]
fn mutable_views_write_each_lane_of_a_padded_vector() {
use super::TensorLayoutMut;
let ctx = FaerContext::default().clone_with_nbatch(4).unwrap();
let mut v = FaerVec::<f64>::zeros(3, ctx);
let layout = v.tensor_layout_mut().batch_lanes(2, 2);
let mut view = unsafe { TensorViewMut::from_layout(layout) };
for b in 0..2 {
for k in 0..2 {
let lane = (b * 2 + k) as f64;
view.column_mut::<f64>(&[k, b]).unwrap().copy_from_slice(&[
lane,
lane + 0.5,
lane + 0.25,
]);
}
}
*view.index_mut::<f64>([2, 1, 1]).unwrap() = -1.0;
assert_eq!(*view.as_view().index::<f64>([2, 1, 1]).unwrap(), -1.0);
let expected: Vec<f64> = (0..4)
.flat_map(|l| [l as f64, l as f64 + 0.5, l as f64 + 0.25])
.collect::<Vec<_>>()
.into_iter()
.enumerate()
.map(|(i, x)| if i == 11 { -1.0 } else { x })
.collect();
assert_eq!(v.clone_as_vec(), expected);
}
#[test]
fn mutable_views_reject_overlapping_layouts() {
let mut data = [0.0f64; 4];
let s = std::mem::size_of::<f64>();
let ptr = data.as_mut_ptr() as *mut u8;
let f64_type = super::ScalarType::F64;
let err = unsafe { TensorViewMut::new(ptr, &[2, 2], &[s, 0], f64_type) }.unwrap_err();
assert!(err.to_string().contains("overlap"), "{err}");
let err = unsafe { TensorViewMut::new(ptr, &[2, 2], &[s, s], f64_type) }.unwrap_err();
assert!(err.to_string().contains("overlap"), "{err}");
let mut view =
unsafe { TensorViewMut::new(ptr, &[2, 1, 2], &[s, 0, 2 * s], f64_type) }.unwrap();
view.column_mut::<f64>(&[0, 1]).unwrap().fill(3.0);
let view = TensorViewMut::from_slice_mut(&mut data, &[4]);
assert_eq!(view.column::<f64>(&[]).unwrap(), &[0.0, 0.0, 3.0, 3.0]);
}
#[test]
fn vec_converts_to_a_1d_array() {
let tensor = vec![1.0f64, 2.0, 3.0].to_tensor();
assert_eq!(tensor.shape(), &[3]);
assert_eq!(tensor.column::<f64>(&[]).unwrap(), &[1.0, 2.0, 3.0]);
assert_eq!(tensor.columns::<f64>().unwrap().count(), 1);
}
}