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