matten_ndarray/convert.rs
1//! `matten::Tensor` ↔ `ndarray::ArrayD<f64>` conversions (RFC-027 §4).
2//!
3//! Both directions copy: `matten::Tensor` owns a contiguous row-major
4//! `Vec<f64>`, and these functions hand over / materialize owned buffers. No
5//! zero-copy is claimed (RFC-025 §3).
6
7use crate::error::MattenNdarrayError;
8use matten::Tensor;
9use ndarray::{ArrayD, IxDyn};
10
11/// Converts a numeric [`Tensor`] into an [`ndarray::ArrayD<f64>`].
12///
13/// The result is standard (row-major) layout. A dynamic tensor returns
14/// [`MattenNdarrayError::DynamicTensor`] rather than panicking. This guard is
15/// unconditional — it does not depend on the companion `dynamic` feature being
16/// enabled (RFC-031).
17///
18/// ```
19/// use matten::Tensor;
20/// use matten_ndarray::to_arrayd;
21///
22/// let t = Tensor::new(vec![1.0, 2.0, 3.0, 4.0], &[2, 2]);
23/// let arr = to_arrayd(&t).unwrap();
24/// assert_eq!(arr.shape(), &[2, 2]);
25/// assert_eq!(arr[[1, 0]], 3.0);
26/// ```
27pub fn to_arrayd(tensor: &Tensor) -> Result<ArrayD<f64>, MattenNdarrayError> {
28 if tensor.is_dynamic() {
29 return Err(MattenNdarrayError::DynamicTensor);
30 }
31
32 let shape = tensor.shape().to_vec();
33 // `to_vec` is row-major; safe here because dynamic tensors were rejected above.
34 let data = tensor.to_vec();
35 ArrayD::from_shape_vec(IxDyn(&shape), data).map_err(MattenNdarrayError::NdarrayShape)
36}
37
38/// Converts an [`ndarray::ArrayD<f64>`] into a [`Tensor`].
39///
40/// Conversion preserves **logical** element order: an `ArrayD` may be in
41/// non-standard (transposed / sliced / non-standard-stride) layout, so the raw
42/// backing buffer is not read directly — that would silently transpose the
43/// data. A shape with a zero-length axis is accepted (RFC-111): core `matten`
44/// accepts zero-sized dimensions, and this bridge no longer imposes a
45/// narrower rule than the type it converts to.
46///
47/// ```
48/// use matten_ndarray::from_arrayd;
49/// use ndarray::{ArrayD, IxDyn};
50///
51/// // A transposed (non-standard-layout) array still converts by logical order.
52/// let a = ArrayD::from_shape_vec(IxDyn(&[2, 3]), vec![1., 2., 3., 4., 5., 6.]).unwrap();
53/// let t = from_arrayd(a.t().to_owned()).unwrap(); // logical shape [3, 2]
54/// assert_eq!(t.shape(), &[3, 2]);
55/// assert_eq!(t.as_slice(), &[1.0, 4.0, 2.0, 5.0, 3.0, 6.0]);
56///
57/// // A zero-length axis round-trips.
58/// let empty = ArrayD::from_shape_vec(IxDyn(&[0, 3]), vec![]).unwrap();
59/// let t = from_arrayd(empty).unwrap();
60/// assert_eq!(t.shape(), &[0, 3]);
61/// assert!(t.is_empty());
62/// ```
63pub fn from_arrayd(array: ArrayD<f64>) -> Result<Tensor, MattenNdarrayError> {
64 let shape: Vec<usize> = array.shape().to_vec();
65
66 // `as_standard_layout` yields a row-major view (cloning only if the input
67 // was non-standard layout); iterating it gives logical order.
68 let data: Vec<f64> = array.as_standard_layout().iter().copied().collect();
69
70 Tensor::try_new(data, &shape).map_err(MattenNdarrayError::Matten)
71}