extendr_api/optional/
ndarray.rs

1/*!
2Defines conversions between R objects and the [`ndarray`](https://docs.rs/ndarray/latest/ndarray/) crate, which offers native Rust array types and numerical computation routines.
3
4To enable these conversions, you must first enable the `ndarray` feature for extendr:
5```toml
6[dependencies]
7extendr-api = { version = "0.7", features = ["ndarray"] }
8```
9
10Specifically, extendr supports the following conversions:
11* [`Robj` → `ArrayView1`](FromRobj#impl-FromRobj<%27a>-for-ArrayView1<%27a%2C%20T>), for when you have an R vector that you want to analyse in Rust:
12    ```rust
13    use extendr_api::prelude::*;
14    use ndarray::ArrayView1;
15
16    #[extendr]
17    fn describe_vector(vector: ArrayView1<f64>){
18        println!("This R vector has length {:?}", vector.len())
19    }
20    ```
21* [`Robj` → `ArrayView2`](FromRobj#impl-FromRobj<%27a>-for-ArrayView2<%27a%2C%20f64>), for when you have an R matrix that you want to analyse in Rust.
22    ```rust
23    use extendr_api::prelude::*;
24    use ndarray::ArrayView2;
25
26    #[extendr]
27    fn describe_matrix(matrix: ArrayView2<f64>){
28        println!("This R matrix has shape {:?}", matrix.dim())
29    }
30    ```
31* [`ArrayBase` → `Robj`](Robj#impl-TryFrom<ArrayBase<S%2C%20D>>-for-Robj), for when you want to return a reference to an [`ndarray`] Array from Rust back to R.
32    ```rust
33    use extendr_api::prelude::*;
34    use ndarray::Array2;
35
36    #[extendr]
37    fn return_matrix() -> Robj {
38        Array2::<f64>::zeros((4, 4)).try_into().unwrap()
39    }
40    ```
41
42The item type (ie the `T` in [`Array2<T>`]) can be a variety of Rust types that can represent scalars: [`u32`], [`i32`], [`f64`] and, if you have the `num_complex` compiled feature
43enabled, `Complex<f64>`. Items can also be extendr's wrapper types: [`Rbool`], [`Rint`], [`Rfloat`] and [`Rcplx`].
44
45Note that the extendr-ndarray integration only supports accessing R arrays as [`ArrayView`], which are immutable.
46Therefore, instead of directly editing the input array, it is recommended that you instead return a new array from your `#[extendr]`-annotated function, which you allocate in Rust.
47It will then be copied into a new block of memory managed by R.
48This is made easier by the fact that [ndarray allocates a new array automatically when performing operations on array references](ArrayBase#binary-operators-with-array-and-scalar):
49```rust
50use extendr_api::prelude::*;
51use ndarray::ArrayView2;
52
53#[extendr]
54fn scalar_multiplication(matrix: ArrayView2<f64>, scalar: f64) -> Robj {
55    (&matrix * scalar).try_into().unwrap()
56}
57```
58
59For all array uses in Rust, refer to the [`ndarray::ArrayBase`] documentation, which explains the usage for all of the above types.
60*/
61#[doc(hidden)]
62use ndarray::prelude::*;
63use ndarray::{Data, ShapeBuilder};
64
65use crate::prelude::{c64, dim_symbol, Rcplx, Rfloat, Rint};
66use crate::*;
67
68macro_rules! make_array_view_1 {
69    ($type: ty, $error_fn: expr) => {
70        impl<'a> TryFrom<&'_ Robj> for ArrayView1<'a, $type> {
71            type Error = crate::Error;
72
73            fn try_from(robj: &Robj) -> Result<Self> {
74                if let Some(v) = robj.as_typed_slice() {
75                    Ok(ArrayView1::<'a, $type>::from(v))
76                } else {
77                    Err($error_fn(robj.clone()))
78                }
79            }
80        }
81
82        impl<'a> TryFrom<Robj> for ArrayView1<'a, $type> {
83            type Error = crate::Error;
84
85            fn try_from(robj: Robj) -> Result<Self> {
86                Self::try_from(&robj)
87            }
88        }
89    };
90}
91
92macro_rules! make_array_view_2 {
93    ($type: ty, $error_str: expr, $error_fn: expr) => {
94        impl<'a> TryFrom<&'_ Robj> for ArrayView2<'a, $type> {
95            type Error = crate::Error;
96            fn try_from(robj: &Robj) -> Result<Self> {
97                if robj.is_matrix() {
98                    let nrows = robj.nrows();
99                    let ncols = robj.ncols();
100                    if let Some(v) = robj.as_typed_slice() {
101                        // use fortran order.
102                        let shape = (nrows, ncols).into_shape().f();
103                        return ArrayView2::from_shape(shape, v)
104                            .map_err(|err| Error::NDArrayShapeError(err));
105                    } else {
106                        return Err($error_fn(robj.clone()));
107                    }
108                }
109                return Err(Error::ExpectedMatrix(robj.clone()));
110            }
111        }
112
113        impl<'a> TryFrom<Robj> for ArrayView2<'a, $type> {
114            type Error = crate::Error;
115            fn try_from(robj: Robj) -> Result<Self> {
116                Self::try_from(&robj)
117            }
118        }
119    };
120}
121make_array_view_1!(Rbool, Error::ExpectedLogical);
122make_array_view_1!(Rint, Error::ExpectedInteger);
123make_array_view_1!(i32, Error::ExpectedInteger);
124make_array_view_1!(Rfloat, Error::ExpectedReal);
125make_array_view_1!(f64, Error::ExpectedReal);
126make_array_view_1!(Rcplx, Error::ExpectedComplex);
127make_array_view_1!(c64, Error::ExpectedComplex);
128make_array_view_1!(Rstr, Error::ExpectedString);
129
130make_array_view_2!(Rbool, "Not a logical matrix.", Error::ExpectedLogical);
131make_array_view_2!(Rint, "Not an integer matrix.", Error::ExpectedInteger);
132make_array_view_2!(i32, "Not an integer matrix.", Error::ExpectedInteger);
133make_array_view_2!(Rfloat, "Not a floating point matrix.", Error::ExpectedReal);
134make_array_view_2!(f64, "Not a floating point matrix.", Error::ExpectedReal);
135make_array_view_2!(
136    Rcplx,
137    "Not a complex number matrix.",
138    Error::ExpectedComplex
139);
140make_array_view_2!(c64, "Not a complex number matrix.", Error::ExpectedComplex);
141make_array_view_2!(Rstr, "Not a string matrix.", Error::ExpectedString);
142
143impl<A, S, D> TryFrom<&ArrayBase<S, D>> for Robj
144where
145    S: Data<Elem = A>,
146    A: Copy + ToVectorValue,
147    D: Dimension,
148{
149    type Error = Error;
150
151    /// Converts a reference to an ndarray Array into an equivalent R array.
152    /// The data itself is copied.
153    fn try_from(value: &ArrayBase<S, D>) -> Result<Self> {
154        // Refer to https://github.com/rust-ndarray/ndarray/issues/1060 for an excellent discussion
155        // on how to convert from `ndarray` types to R/fortran arrays
156        // This thread has informed the design decisions made here.
157
158        // In general, transposing and then iterating an ndarray in C-order (`iter()`) is exactly
159        // equivalent to iterating that same array in Fortan-order (which `ndarray` doesn't currently support)
160        let mut result = value
161            .t()
162            .iter()
163            // Since we only have a reference, we have to copy all elements so that we can own the entire R array
164            .copied()
165            .collect_robj();
166        result.set_attrib(
167            dim_symbol(),
168            value
169                .shape()
170                .iter()
171                .map(|x| i32::try_from(*x))
172                .collect::<std::result::Result<Vec<i32>, <i32 as TryFrom<usize>>::Error>>()
173                .map_err(|_err| {
174                    Error::Other(String::from(
175                        "One or more array dimensions were too large to be handled by R.",
176                    ))
177                })?,
178        )?;
179        Ok(result)
180    }
181}
182
183impl<A, S, D> TryFrom<ArrayBase<S, D>> for Robj
184where
185    S: Data<Elem = A>,
186    A: Copy + ToVectorValue,
187    D: Dimension,
188{
189    type Error = Error;
190
191    /// Converts an ndarray Array into an equivalent R array.
192    /// The data itself is copied.
193    fn try_from(value: ArrayBase<S, D>) -> Result<Self> {
194        Robj::try_from(&value)
195    }
196}
197
198#[cfg(test)]
199mod test {
200    use super::*;
201    use crate as extendr_api;
202    use ndarray::array;
203    use rstest::rstest;
204
205    #[rstest]
206    // Scalars
207    #[case(
208        "1.0",
209        ArrayView1::<f64>::from(&[1.][..])
210    )]
211    #[case(
212        "1L",
213        ArrayView1::<i32>::from(&[1][..])
214    )]
215    #[case(
216        "TRUE",
217        ArrayView1::<Rbool>::from(&[TRUE][..])
218    )]
219    // Matrices
220    #[case(
221       "matrix(c(1, 2, 3, 4, 5, 6, 7, 8), ncol=2, nrow=4)",
222        <Array2<f64>>::from_shape_vec((4, 2).f(), vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0]).unwrap()
223    )]
224    #[case(
225        // Testing the memory layout is Fortran
226        "matrix(c(1, 2, 3, 4, 5, 6, 7, 8), ncol=2, nrow=4)[, 1]",
227        <Array2<f64>>::from_shape_vec((4, 2).f(), vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0]).unwrap().column(0).to_owned()
228    )]
229    #[case(
230        "matrix(c(1L, 2L, 3L, 4L, 5L, 6L, 7L, 8L), ncol=2, nrow=4)",
231        <Array2<i32>>::from_shape_vec((4, 2).f(), vec![1, 2, 3, 4, 5, 6, 7, 8]).unwrap()
232    )]
233    #[case(
234        "matrix(c(T, T, T, T, F, F, F, F), ncol=2, nrow=4)",
235        <Array2<Rbool>>::from_shape_vec((4, 2).f(), vec![true.into(), true.into(), true.into(), true.into(), false.into(), false.into(), false.into(), false.into()]).unwrap()
236    )]
237    fn test_from_robj<DataType, DimType, Error>(
238        #[case] left: &'static str,
239        #[case] right: ArrayBase<DataType, DimType>,
240    ) where
241        DataType: Data,
242        Error: std::fmt::Debug,
243        for<'a> ArrayView<'a, <DataType as ndarray::RawData>::Elem, DimType>:
244            TryFrom<&'a Robj, Error = Error>,
245        DimType: Dimension,
246        <DataType as ndarray::RawData>::Elem: PartialEq + std::fmt::Debug,
247        Error: std::fmt::Debug,
248    {
249        // Tests for the R → Rust conversion
250        test! {
251            let left_robj = eval_string(left).unwrap();
252            let left_array = <ArrayView<DataType::Elem, DimType>>::try_from(&left_robj).unwrap();
253            assert_eq!( left_array, right );
254        }
255    }
256
257    #[rstest]
258    #[case(
259        // An empty array should still convert to an empty R array with the same shape
260        Array4::<i32>::zeros((0, 1, 2, 3).f()),
261        "array(integer(), c(0, 1, 2, 3))"
262    )]
263    #[case(
264        array![1., 2., 3.],
265        "array(c(1, 2, 3))"
266    )]
267    #[case(
268        // We give both R and Rust the same 1d vector and tell them both to read it as a matrix in C order.
269        // Therefore these arrays should be the same.
270        Array::from_shape_vec((2, 3), vec![1., 2., 3., 4., 5., 6.]).unwrap(),
271        "matrix(c(1, 2, 3, 4, 5, 6), nrow=2, byrow=TRUE)"
272    )]
273    #[case(
274        // We give both R and Rust the same 1d vector and tell them both to read it as a matrix
275        // in fortran order. Therefore these arrays should be the same.
276        Array::from_shape_vec((2, 3).f(), vec![1., 2., 3., 4., 5., 6.]).unwrap(),
277        "matrix(c(1, 2, 3, 4, 5, 6), nrow=2, byrow=FALSE)"
278    )]
279    #[case(
280        // We give both R and Rust the same 1d vector and tell them both to read it as a 3d array
281        // in fortran order. Therefore these arrays should be the same.
282        Array::from_shape_vec((1, 2, 3).f(), vec![1, 2, 3, 4, 5, 6]).unwrap(),
283        "array(1:6, c(1, 2, 3))"
284    )]
285    #[case(
286        // We give R a 1d vector and tell it to read it as a 3d vector
287        // Then we give Rust the equivalent vector manually split out.
288        array![[[1, 5], [3, 7]], [[2, 6], [4, 8]]],
289        "array(1:8, dim=c(2, 2, 2))"
290    )]
291    fn test_to_robj<ElementType, DimType>(
292        #[case] array: Array<ElementType, DimType>,
293        #[case] r_expr: &str,
294    ) where
295        Robj: TryFrom<Array<ElementType, DimType>>,
296        for<'a> Robj: TryFrom<&'a Array<ElementType, DimType>>,
297        <robj::Robj as TryFrom<Array<ElementType, DimType>>>::Error: std::fmt::Debug,
298        for<'a> <robj::Robj as TryFrom<&'a Array<ElementType, DimType>>>::Error: std::fmt::Debug,
299    {
300        // Tests for the Rust → R conversion, so we therefore perform the
301        // comparison in R
302        test! {
303            // Test for borrowed array
304            assert_eq!(
305                &(Robj::try_from(&array).unwrap()),
306                &eval_string(r_expr).unwrap()
307            );
308            // Test for owned array
309            assert_eq!(
310                &(Robj::try_from(array).unwrap()),
311                &eval_string(r_expr).unwrap()
312            );
313        }
314    }
315
316    #[test]
317    fn test_round_trip() {
318        test! {
319            let rvals = [
320                R!("matrix(c(1L, 2L, 3L, 4L, 5L, 6L), nrow=2)"),
321                R!("array(1:8, c(4, 2))")
322            ];
323            for rval in rvals {
324                let rval = rval.unwrap();
325                let rust_arr= <ArrayView2<i32>>::try_from(&rval).unwrap();
326                let r_arr: Robj = (&rust_arr).try_into().unwrap();
327                assert_eq!(
328                    rval,
329                    r_arr
330                );
331            }
332        }
333    }
334}