├── .editorconfig ├── .github └── workflows │ └── rust.yml ├── .gitignore ├── CHANGELOG.md ├── Cargo.toml ├── LICENSE ├── README.md ├── README.tpl ├── rustfmt.toml ├── src ├── from_numpy.rs ├── lib.rs └── to_numpy.rs └── tests ├── errors.rs ├── from_numpy.rs └── to_numpy.rs /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = tab 5 | indent_size = 4 6 | end_of_line = lf 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | -------------------------------------------------------------------------------- /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | on: 3 | push: 4 | branches: [master] 5 | pull_request: 6 | jobs: 7 | build: 8 | runs-on: ubuntu-latest 9 | strategy: 10 | matrix: 11 | channel: [nightly] 12 | steps: 13 | - name: Install Rust 14 | run: rustup install ${{matrix.channel}} 15 | - uses: actions/checkout@v1 16 | - name: Build 17 | run: cargo +${{matrix.channel}} build --color=always --all-targets 18 | - name: Install Python packages 19 | run: | 20 | pip3 install -U numpy 21 | - name: Run tests 22 | run: cargo +${{matrix.channel}} test --color=always 23 | 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /Cargo.lock 3 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Version 0.3.0 2 | * Update to `numpy` 0.11 and `pyo3` 0.11. 3 | * Take `&PyAny` instead of `impl AsRef` in `*_from_numpy` functions. 4 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "nalgebra-numpy" 3 | description = "conversions between nalgebra and numpy" 4 | version = "0.3.0" 5 | authors = ["Maarten de Vries "] 6 | edition = "2018" 7 | license = "BSD-2-Clause" 8 | repository = "https://github.com/fusion-engineering/nalgebra-numpy" 9 | documentation = "https://docs.rs/nalgebra-numpy/" 10 | 11 | readme = "README.md" 12 | keywords = ["python", "numpy", "nalgebra", "matrix", "conversion"] 13 | categories = ["science"] 14 | 15 | [dependencies] 16 | nalgebra = "0.24.1" 17 | numpy = "0.11" 18 | pyo3 = "0.11" 19 | 20 | [dev-dependencies] 21 | inline-python = "0.6.0" 22 | assert2 = "0.3.4" 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2019 Fusion Engineering B.V. 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 2. Redistributions in binary form must reproduce the above copyright notice, 10 | this list of conditions and the following disclaimer in the documentation 11 | and/or other materials provided with the distribution. 12 | 13 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 14 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 15 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 16 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 17 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 18 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 19 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 20 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 21 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 22 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # nalgebra-numpy 2 | 3 | This crate provides conversion between [`nalgebra`] and [`numpy`](https://numpy.org/). 4 | It is intended to be used when you want to share nalgebra matrices between Python and Rust code, 5 | for example with [`inline-python`](https://docs.rs/inline-python). 6 | 7 | ## Conversion from numpy to nalgebra. 8 | 9 | It is possible to create either a view or a copy of a numpy array. 10 | You can use [`matrix_from_numpy`] to copy the data into a new matrix, 11 | or one of [`matrix_slice_from_numpy`] or [`matrix_slice_mut_from_numpy`] to create a view. 12 | If a numpy array is not compatible with the requested matrix type, 13 | an error is returned. 14 | 15 | Keep in mind though that the borrow checker can not enforce rules on data managed by a Python object. 16 | You could potentially keep an immutable view around in Rust, and then modify the data from Python. 17 | For this reason, creating any view -- even an immutable one -- is unsafe. 18 | 19 | ## Conversion from nalgebra to numpy. 20 | 21 | A nalgebra matrix can also be converted to a numpy array, using [`matrix_to_numpy`]. 22 | This function always creates a copy. 23 | Since all nalgebra arrays can be represented as a numpy array, 24 | this directly returns a [`pyo3::PyObject`] rather than a `Result`. 25 | 26 | ## Examples. 27 | 28 | Copy a numpy array to a new fixed size matrix: 29 | 30 | ```rust 31 | use inline_python::{Context, python}; 32 | use nalgebra_numpy::{matrix_from_numpy}; 33 | 34 | let gil = pyo3::Python::acquire_gil(); 35 | let context = Context::new_with_gil(gil.python()); 36 | context.run(python! { 37 | import numpy as np 38 | matrix = np.array([ 39 | [1.0, 2.0, 3.0], 40 | [4.0, 5.0, 6.0], 41 | [7.0, 8.0, 9.0], 42 | ]) 43 | }); 44 | 45 | let matrix = context.globals(gil.python()).get_item("matrix").unwrap(); 46 | let matrix : nalgebra::Matrix3 = matrix_from_numpy(gil.python(), matrix)?; 47 | 48 | assert_eq!(matrix, nalgebra::Matrix3::new( 49 | 1.0, 2.0, 3.0, 50 | 4.0, 5.0, 6.0, 51 | 7.0, 8.0, 9.0, 52 | )); 53 | ``` 54 | 55 | Dynamic matrices are also supported: 56 | 57 | ```rust 58 | use nalgebra::DMatrix; 59 | # 60 | 61 | let matrix : DMatrix = matrix_from_numpy(gil.python(), matrix)?; 62 | assert_eq!(matrix, DMatrix::from_row_slice(3, 3, &[ 63 | 1.0, 2.0, 3.0, 64 | 4.0, 5.0, 6.0, 65 | 7.0, 8.0, 9.0, 66 | ])); 67 | ``` 68 | 69 | And so are partially dynamic matrices: 70 | 71 | ```rust 72 | use nalgebra::{MatrixMN, Dynamic, U3}; 73 | 74 | let matrix : MatrixMN = matrix_from_numpy(gil.python(), matrix)?; 75 | assert_eq!(matrix, MatrixMN::::from_row_slice(&[ 76 | 1.0, 2.0, 3.0, 77 | 4.0, 5.0, 6.0, 78 | 7.0, 8.0, 9.0, 79 | ])); 80 | ``` 81 | 82 | A conversion to python object looks as follows: 83 | ```rust 84 | use nalgebra_numpy::matrix_to_numpy; 85 | use nalgebra::Matrix3; 86 | use inline_python::python; 87 | 88 | let gil = pyo3::Python::acquire_gil(); 89 | let matrix = matrix_to_numpy(gil.python(), &Matrix3::::new( 90 | 0, 1, 2, 91 | 3, 4, 5, 92 | 6, 7, 8, 93 | )); 94 | 95 | python! { 96 | from numpy import array_equal 97 | assert array_equal('matrix, [ 98 | [0, 1, 2], 99 | [3, 4, 5], 100 | [6, 7, 8], 101 | ]) 102 | } 103 | ``` 104 | 105 | [`nalgebra`]: https://docs.rs/nalgebra 106 | [`matrix_from_numpy`]: https://docs.rs/nalgebra-numpy/latest/nalgebra_numpy/fn.matrix_from_numpy.html 107 | [`matrix_slice_from_numpy`]: https://docs.rs/nalgebra-numpy/latest/nalgebra_numpy/fn.matrix_slice_from_numpy.html 108 | [`matrix_slice_mut_from_numpy`]: https://docs.rs/nalgebra-numpy/latest/nalgebra_numpy/fn.matrix_slice_mut_from_numpy.html 109 | [`matrix_to_numpy`]: https://docs.rs/nalgebra-numpy/latest/nalgebra_numpy/fn.matrix_to_numpy.html 110 | [`pyo3::PyObject`]: https://docs.rs/pyo3/latest/pyo3/type.PyObject.html 111 | -------------------------------------------------------------------------------- /README.tpl: -------------------------------------------------------------------------------- 1 | # {{crate}} 2 | 3 | {{readme}} 4 | 5 | [`nalgebra`]: https://docs.rs/nalgebra 6 | [`matrix_from_numpy`]: https://docs.rs/nalgebra-numpy/latest/nalgebra_numpy/fn.matrix_from_numpy.html 7 | [`matrix_slice_from_numpy`]: https://docs.rs/nalgebra-numpy/latest/nalgebra_numpy/fn.matrix_slice_from_numpy.html 8 | [`matrix_slice_mut_from_numpy`]: https://docs.rs/nalgebra-numpy/latest/nalgebra_numpy/fn.matrix_slice_mut_from_numpy.html 9 | [`matrix_to_numpy`]: https://docs.rs/nalgebra-numpy/latest/nalgebra_numpy/fn.matrix_to_numpy.html 10 | [`pyo3::PyObject`]: https://docs.rs/pyo3/latest/pyo3/type.PyObject.html 11 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | hard_tabs = true 2 | tab_spaces = 4 3 | max_width = 140 4 | imports_layout = "HorizontalVertical" 5 | match_block_trailing_comma = true 6 | overflow_delimited_expr = true 7 | reorder_impl_items = true 8 | unstable_features = true 9 | use_field_init_shorthand = true 10 | -------------------------------------------------------------------------------- /src/from_numpy.rs: -------------------------------------------------------------------------------- 1 | use nalgebra::base::{SliceStorage, SliceStorageMut}; 2 | use nalgebra::{Dynamic, Matrix}; 3 | use numpy::npyffi; 4 | use numpy::npyffi::objects::PyArrayObject; 5 | use pyo3::{types::PyAny, AsPyPointer}; 6 | 7 | /// Compile-time matrix dimension used in errors. 8 | #[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Debug)] 9 | pub enum Dimension { 10 | Static(usize), 11 | Dynamic, 12 | } 13 | 14 | /// Compile-time shape of a matrix used in errors. 15 | #[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Debug)] 16 | pub struct Shape(Dimension, Dimension); 17 | 18 | /// Error that can occur when converting from Python to a nalgebra matrix. 19 | #[derive(Clone, Eq, PartialEq, Debug)] 20 | pub enum Error { 21 | /// The Python object is not a [`numpy.ndarray`](https://numpy.org/devdocs/reference/arrays.ndarray.html). 22 | WrongObjectType(WrongObjectTypeError), 23 | 24 | /// The input array is not compatible with the requested nalgebra matrix. 25 | IncompatibleArray(IncompatibleArrayError), 26 | 27 | /// The input array is not properly aligned. 28 | UnalignedArray(UnalignedArrayError), 29 | } 30 | 31 | /// Error indicating that the Python object is not a [`numpy.ndarray`](https://numpy.org/devdocs/reference/arrays.ndarray.html). 32 | #[derive(Clone, Eq, PartialEq, Debug)] 33 | pub struct WrongObjectTypeError { 34 | pub actual: String, 35 | } 36 | 37 | /// Error indicating that the input array is not compatible with the requested nalgebra matrix. 38 | #[derive(Clone, Eq, PartialEq, Debug)] 39 | pub struct IncompatibleArrayError { 40 | pub expected_shape: Shape, 41 | pub actual_shape: Vec, 42 | pub expected_dtype: numpy::DataType, 43 | pub actual_dtype: String, 44 | } 45 | 46 | /// Error indicating that the input array is not properly aligned. 47 | #[derive(Clone, Eq, PartialEq, Debug)] 48 | pub struct UnalignedArrayError; 49 | 50 | /// Create a nalgebra view from a numpy array. 51 | /// 52 | /// The array dtype must match the output type exactly. 53 | /// If desired, you can convert the array to the desired type in Python 54 | /// using [`numpy.ndarray.astype`](https://numpy.org/devdocs/reference/generated/numpy.ndarray.astype.html). 55 | /// 56 | /// # Safety 57 | /// This function creates a const slice that references data owned by Python. 58 | /// The user must ensure that the data is not modified through other pointers or references. 59 | #[allow(clippy::needless_lifetimes)] 60 | pub unsafe fn matrix_slice_from_numpy<'a, N, R, C>( 61 | _py: pyo3::Python, 62 | input: &'a PyAny, 63 | ) -> Result, Error> 64 | where 65 | N: nalgebra::Scalar + numpy::Element, 66 | R: nalgebra::Dim, 67 | C: nalgebra::Dim, 68 | { 69 | matrix_slice_from_numpy_ptr(input.as_ptr()) 70 | } 71 | 72 | /// Create a mutable nalgebra view from a numpy array. 73 | /// 74 | /// The array dtype must match the output type exactly. 75 | /// If desired, you can convert the array to the desired type in Python 76 | /// using [`numpy.ndarray.astype`](https://numpy.org/devdocs/reference/generated/numpy.ndarray.astype.html). 77 | /// 78 | /// # Safety 79 | /// This function creates a mutable slice that references data owned by Python. 80 | /// The user must ensure that no other Rust references to the same data exist. 81 | #[allow(clippy::needless_lifetimes)] 82 | pub unsafe fn matrix_slice_mut_from_numpy<'a, N, R, C>( 83 | _py: pyo3::Python, 84 | input: &'a PyAny, 85 | ) -> Result, Error> 86 | where 87 | N: nalgebra::Scalar + numpy::Element, 88 | R: nalgebra::Dim, 89 | C: nalgebra::Dim, 90 | { 91 | matrix_slice_mut_from_numpy_ptr(input.as_ptr()) 92 | } 93 | 94 | /// Create an owning nalgebra matrix from a numpy array. 95 | /// 96 | /// The data is copied into the matrix. 97 | /// 98 | /// The array dtype must match the output type exactly. 99 | /// If desired, you can convert the array to the desired type in Python 100 | /// using [`numpy.ndarray.astype`](https://numpy.org/devdocs/reference/generated/numpy.ndarray.astype.html). 101 | pub fn matrix_from_numpy(py: pyo3::Python, input: &PyAny) -> Result, Error> 102 | where 103 | N: nalgebra::Scalar + numpy::Element, 104 | R: nalgebra::Dim, 105 | C: nalgebra::Dim, 106 | nalgebra::base::default_allocator::DefaultAllocator: nalgebra::base::allocator::Allocator, 107 | { 108 | Ok(unsafe { matrix_slice_from_numpy::(py, input) }?.into_owned()) 109 | } 110 | 111 | /// Same as [`matrix_slice_from_numpy`], but takes a raw [`PyObject`](pyo3::ffi::PyObject) pointer. 112 | #[allow(clippy::missing_safety_doc)] 113 | pub unsafe fn matrix_slice_from_numpy_ptr<'a, N, R, C>( 114 | array: *mut pyo3::ffi::PyObject, 115 | ) -> Result, Error> 116 | where 117 | N: nalgebra::Scalar + numpy::Element, 118 | R: nalgebra::Dim, 119 | C: nalgebra::Dim, 120 | { 121 | let array = cast_to_py_array(array)?; 122 | let shape = check_array_compatible::(array)?; 123 | check_array_alignment(array)?; 124 | 125 | let row_stride = Dynamic::new(*(*array).strides.add(0) as usize / std::mem::size_of::()); 126 | let col_stride = Dynamic::new(*(*array).strides.add(1) as usize / std::mem::size_of::()); 127 | let storage = SliceStorage::::from_raw_parts((*array).data as *const N, shape, (row_stride, col_stride)); 128 | 129 | Ok(Matrix::from_data(storage)) 130 | } 131 | 132 | /// Same as [`matrix_slice_mut_from_numpy`], but takes a raw [`PyObject`](pyo3::ffi::PyObject) pointer. 133 | #[allow(clippy::missing_safety_doc)] 134 | pub unsafe fn matrix_slice_mut_from_numpy_ptr<'a, N, R, C>( 135 | array: *mut pyo3::ffi::PyObject, 136 | ) -> Result, Error> 137 | where 138 | N: nalgebra::Scalar + numpy::Element, 139 | R: nalgebra::Dim, 140 | C: nalgebra::Dim, 141 | { 142 | let array = cast_to_py_array(array)?; 143 | let shape = check_array_compatible::(array)?; 144 | check_array_alignment(array)?; 145 | 146 | let row_stride = Dynamic::new(*(*array).strides.add(0) as usize / std::mem::size_of::()); 147 | let col_stride = Dynamic::new(*(*array).strides.add(1) as usize / std::mem::size_of::()); 148 | let storage = SliceStorageMut::::from_raw_parts((*array).data as *mut N, shape, (row_stride, col_stride)); 149 | 150 | Ok(Matrix::from_data(storage)) 151 | } 152 | 153 | /// Check if an object is numpy array and cast the pointer. 154 | unsafe fn cast_to_py_array(object: *mut pyo3::ffi::PyObject) -> Result<*mut PyArrayObject, WrongObjectTypeError> { 155 | if npyffi::array::PyArray_Check(object) == 1 { 156 | Ok(&mut *(object as *mut npyffi::objects::PyArrayObject)) 157 | } else { 158 | Err(WrongObjectTypeError { 159 | actual: object_type_string(object), 160 | }) 161 | } 162 | } 163 | 164 | /// Check if a numpy array is compatible and return the runtime shape. 165 | unsafe fn check_array_compatible(array: *mut PyArrayObject) -> Result<(R, C), IncompatibleArrayError> 166 | where 167 | N: numpy::Element, 168 | R: nalgebra::Dim, 169 | C: nalgebra::Dim, 170 | { 171 | // Delay semi-expensive construction of error object using a lambda. 172 | let make_error = || { 173 | let expected_shape = Shape( 174 | R::try_to_usize().map(Dimension::Static).unwrap_or(Dimension::Dynamic), 175 | C::try_to_usize().map(Dimension::Static).unwrap_or(Dimension::Dynamic), 176 | ); 177 | IncompatibleArrayError { 178 | expected_shape, 179 | actual_shape: shape(array), 180 | expected_dtype: N::DATA_TYPE, 181 | actual_dtype: data_type_string(array), 182 | } 183 | }; 184 | 185 | // Input array must have two dimensions. 186 | if (*array).nd != 2 { 187 | return Err(make_error()); 188 | } 189 | 190 | let input_rows = *(*array).dimensions.add(0) as usize; 191 | let input_cols = *(*array).dimensions.add(1) as usize; 192 | 193 | // Check number of rows in input array. 194 | if R::try_to_usize().map(|expected| input_rows == expected) == Some(false) { 195 | return Err(make_error()); 196 | } 197 | 198 | // Check number of columns in input array. 199 | if C::try_to_usize().map(|expected| input_cols == expected) == Some(false) { 200 | return Err(make_error()); 201 | } 202 | 203 | // Check the data type of the input array. 204 | if npyffi::array::PY_ARRAY_API.PyArray_EquivTypenums((*(*array).descr).type_num, N::ffi_dtype() as u32 as i32) != 1 { 205 | return Err(make_error()); 206 | } 207 | 208 | // All good. 209 | Ok((R::from_usize(input_rows), C::from_usize(input_cols))) 210 | } 211 | 212 | unsafe fn check_array_alignment(array: *mut PyArrayObject) -> Result<(), UnalignedArrayError> { 213 | if (*array).flags & npyffi::flags::NPY_ARRAY_ALIGNED != 0 { 214 | Ok(()) 215 | } else { 216 | Err(UnalignedArrayError) 217 | } 218 | } 219 | 220 | /// Get a string representing the type of a Python object. 221 | unsafe fn object_type_string(object: *mut pyo3::ffi::PyObject) -> String { 222 | let py_type = (*object).ob_type; 223 | let name = (*py_type).tp_name; 224 | let name = std::ffi::CStr::from_ptr(name).to_bytes(); 225 | String::from_utf8_lossy(name).into_owned() 226 | } 227 | 228 | /// Get a string representing the data type of a numpy array. 229 | unsafe fn data_type_string(array: *mut PyArrayObject) -> String { 230 | // Convert the dtype to string. 231 | // Don't forget to call Py_DecRef in all paths if py_name isn't null. 232 | let py_name = pyo3::ffi::PyObject_Str((*array).descr as *mut pyo3::ffi::PyObject); 233 | if py_name.is_null() { 234 | return String::from(""); 235 | } 236 | 237 | let mut size = 0isize; 238 | let data = pyo3::ffi::PyUnicode_AsUTF8AndSize(py_name, &mut size as *mut isize); 239 | if data.is_null() { 240 | pyo3::ffi::Py_DecRef(py_name); 241 | return String::from(""); 242 | } 243 | 244 | let name = std::slice::from_raw_parts(data as *mut u8, size as usize); 245 | let name = String::from_utf8_unchecked(name.to_vec()); 246 | pyo3::ffi::Py_DecRef(py_name); 247 | name 248 | } 249 | 250 | /// Get the shape of a numpy array as [`Vec`]. 251 | unsafe fn shape(object: *mut numpy::npyffi::objects::PyArrayObject) -> Vec { 252 | let num_dims = (*object).nd; 253 | let dimensions = std::slice::from_raw_parts((*object).dimensions as *const usize, num_dims as usize); 254 | dimensions.to_vec() 255 | } 256 | 257 | impl From for Error { 258 | fn from(other: WrongObjectTypeError) -> Self { 259 | Self::WrongObjectType(other) 260 | } 261 | } 262 | 263 | impl From for Error { 264 | fn from(other: IncompatibleArrayError) -> Self { 265 | Self::IncompatibleArray(other) 266 | } 267 | } 268 | 269 | impl From for Error { 270 | fn from(other: UnalignedArrayError) -> Self { 271 | Self::UnalignedArray(other) 272 | } 273 | } 274 | 275 | impl std::fmt::Display for Dimension { 276 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 277 | match self { 278 | Self::Dynamic => write!(f, "Dynamic"), 279 | Self::Static(x) => write!(f, "{}", x), 280 | } 281 | } 282 | } 283 | 284 | impl std::fmt::Display for Shape { 285 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 286 | let Self(rows, cols) = self; 287 | write!(f, "[{}, {}]", rows, cols) 288 | } 289 | } 290 | 291 | impl std::fmt::Display for Error { 292 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 293 | match self { 294 | Self::WrongObjectType(e) => write!(f, "{}", e), 295 | Self::IncompatibleArray(e) => write!(f, "{}", e), 296 | Self::UnalignedArray(e) => write!(f, "{}", e), 297 | } 298 | } 299 | } 300 | 301 | impl std::fmt::Display for WrongObjectTypeError { 302 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 303 | write!(f, "wrong object type: expected a numpy.ndarray, found {}", self.actual) 304 | } 305 | } 306 | 307 | impl std::fmt::Display for IncompatibleArrayError { 308 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 309 | write!( 310 | f, 311 | "incompatible array: expected ndarray(shape={}, dtype='{}'), found ndarray(shape={:?}, dtype={:?})", 312 | self.expected_shape, 313 | FormatDataType(&self.expected_dtype), 314 | self.actual_shape, 315 | self.actual_dtype, 316 | ) 317 | } 318 | } 319 | 320 | impl std::fmt::Display for UnalignedArrayError { 321 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 322 | write!(f, "the input array is not properly aligned for this platform") 323 | } 324 | } 325 | 326 | /// Helper to format [`numpy::DataType`] more consistently. 327 | struct FormatDataType<'a>(&'a numpy::DataType); 328 | 329 | impl std::fmt::Display for FormatDataType<'_> { 330 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 331 | let Self(dtype) = self; 332 | match dtype { 333 | numpy::DataType::Bool => write!(f, "bool"), 334 | numpy::DataType::Complex32 => write!(f, "complex32"), 335 | numpy::DataType::Complex64 => write!(f, "complex64"), 336 | numpy::DataType::Float32 => write!(f, "float32"), 337 | numpy::DataType::Float64 => write!(f, "float64"), 338 | numpy::DataType::Int8 => write!(f, "int8"), 339 | numpy::DataType::Int16 => write!(f, "int16"), 340 | numpy::DataType::Int32 => write!(f, "int32"), 341 | numpy::DataType::Int64 => write!(f, "int64"), 342 | numpy::DataType::Object => write!(f, "object"), 343 | numpy::DataType::Uint8 => write!(f, "uint8"), 344 | numpy::DataType::Uint16 => write!(f, "uint16"), 345 | numpy::DataType::Uint32 => write!(f, "uint32"), 346 | numpy::DataType::Uint64 => write!(f, "uint64"), 347 | } 348 | } 349 | } 350 | 351 | impl std::error::Error for Error {} 352 | impl std::error::Error for WrongObjectTypeError {} 353 | impl std::error::Error for IncompatibleArrayError {} 354 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! This crate provides conversion between [`nalgebra`] and [`numpy`](https://numpy.org/). 2 | //! It is intended to be used when you want to share nalgebra matrices between Python and Rust code, 3 | //! for example with [`inline-python`](https://docs.rs/inline-python). 4 | //! 5 | //! # Conversion from numpy to nalgebra. 6 | //! 7 | //! It is possible to create either a view or a copy of a numpy array. 8 | //! You can use [`matrix_from_numpy`] to copy the data into a new matrix, 9 | //! or one of [`matrix_slice_from_numpy`] or [`matrix_slice_mut_from_numpy`] to create a view. 10 | //! If a numpy array is not compatible with the requested matrix type, 11 | //! an error is returned. 12 | //! 13 | //! Keep in mind though that the borrow checker can not enforce rules on data managed by a Python object. 14 | //! You could potentially keep an immutable view around in Rust, and then modify the data from Python. 15 | //! For this reason, creating any view -- even an immutable one -- is unsafe. 16 | //! 17 | //! # Conversion from nalgebra to numpy. 18 | //! 19 | //! A nalgebra matrix can also be converted to a numpy array, using [`matrix_to_numpy`]. 20 | //! This function always creates a copy. 21 | //! Since all nalgebra arrays can be represented as a numpy array, 22 | //! this directly returns a [`pyo3::PyObject`] rather than a `Result`. 23 | //! 24 | //! # Examples. 25 | //! 26 | //! Copy a numpy array to a new fixed size matrix: 27 | //! 28 | //! ``` 29 | //! use inline_python::{Context, python}; 30 | //! use nalgebra_numpy::{matrix_from_numpy}; 31 | //! 32 | //! # fn main() -> Result<(), nalgebra_numpy::Error> { 33 | //! let gil = pyo3::Python::acquire_gil(); 34 | //! let context = Context::new_with_gil(gil.python()); 35 | //! context.run(python! { 36 | //! import numpy as np 37 | //! matrix = np.array([ 38 | //! [1.0, 2.0, 3.0], 39 | //! [4.0, 5.0, 6.0], 40 | //! [7.0, 8.0, 9.0], 41 | //! ]) 42 | //! }); 43 | //! 44 | //! let matrix = context.globals(gil.python()).get_item("matrix").unwrap(); 45 | //! let matrix : nalgebra::Matrix3 = matrix_from_numpy(gil.python(), matrix)?; 46 | //! 47 | //! assert_eq!(matrix, nalgebra::Matrix3::new( 48 | //! 1.0, 2.0, 3.0, 49 | //! 4.0, 5.0, 6.0, 50 | //! 7.0, 8.0, 9.0, 51 | //! )); 52 | //! # Ok(()) 53 | //! # } 54 | //! ``` 55 | //! 56 | //! Dynamic matrices are also supported: 57 | //! 58 | //! ``` 59 | //! # use inline_python::{Context, python}; 60 | //! # use nalgebra_numpy::{matrix_from_numpy}; 61 | //! use nalgebra::DMatrix; 62 | //! # fn main() -> Result<(), nalgebra_numpy::Error> { 63 | //! # let gil = pyo3::Python::acquire_gil(); 64 | //! # let context = Context::new_with_gil(gil.python()); 65 | //! # context.run(python! { 66 | //! # import numpy as np 67 | //! # matrix = np.array([ 68 | //! # [1.0, 2.0, 3.0], 69 | //! # [4.0, 5.0, 6.0], 70 | //! # [7.0, 8.0, 9.0], 71 | //! # ]) 72 | //! # }); 73 | //! # 74 | //! # let matrix = context.globals(gil.python()).get_item("matrix").unwrap(); 75 | //! 76 | //! let matrix : DMatrix = matrix_from_numpy(gil.python(), matrix)?; 77 | //! assert_eq!(matrix, DMatrix::from_row_slice(3, 3, &[ 78 | //! 1.0, 2.0, 3.0, 79 | //! 4.0, 5.0, 6.0, 80 | //! 7.0, 8.0, 9.0, 81 | //! ])); 82 | //! # Ok(()) 83 | //! # } 84 | //! ``` 85 | //! 86 | //! And so are partially dynamic matrices: 87 | //! 88 | //! ``` 89 | //! # use inline_python::{Context, python}; 90 | //! # use nalgebra_numpy::{matrix_from_numpy}; 91 | //! use nalgebra::{MatrixMN, Dynamic, U3}; 92 | //! # fn main() -> Result<(), nalgebra_numpy::Error> { 93 | //! # let gil = pyo3::Python::acquire_gil(); 94 | //! # let context = Context::new_with_gil(gil.python()); 95 | //! # context.run(python! { 96 | //! # import numpy as np 97 | //! # matrix = np.array([ 98 | //! # [1.0, 2.0, 3.0], 99 | //! # [4.0, 5.0, 6.0], 100 | //! # [7.0, 8.0, 9.0], 101 | //! # ]) 102 | //! # }); 103 | //! # let matrix = context.globals(gil.python()).get_item("matrix").unwrap(); 104 | //! 105 | //! let matrix : MatrixMN = matrix_from_numpy(gil.python(), matrix)?; 106 | //! assert_eq!(matrix, MatrixMN::::from_row_slice(&[ 107 | //! 1.0, 2.0, 3.0, 108 | //! 4.0, 5.0, 6.0, 109 | //! 7.0, 8.0, 9.0, 110 | //! ])); 111 | //! # Ok(()) 112 | //! # } 113 | //! ``` 114 | //! 115 | //! A conversion to python object looks as follows: 116 | //! ``` 117 | //! use nalgebra_numpy::matrix_to_numpy; 118 | //! use nalgebra::Matrix3; 119 | //! use inline_python::python; 120 | //! 121 | //! let gil = pyo3::Python::acquire_gil(); 122 | //! let matrix = matrix_to_numpy(gil.python(), &Matrix3::::new( 123 | //! 0, 1, 2, 124 | //! 3, 4, 5, 125 | //! 6, 7, 8, 126 | //! )); 127 | //! 128 | //! python! { 129 | //! from numpy import array_equal 130 | //! assert array_equal('matrix, [ 131 | //! [0, 1, 2], 132 | //! [3, 4, 5], 133 | //! [6, 7, 8], 134 | //! ]) 135 | //! } 136 | //! ``` 137 | 138 | mod from_numpy; 139 | mod to_numpy; 140 | 141 | pub use from_numpy::*; 142 | pub use to_numpy::*; 143 | -------------------------------------------------------------------------------- /src/to_numpy.rs: -------------------------------------------------------------------------------- 1 | use nalgebra::Matrix; 2 | use numpy::PyArray; 3 | use pyo3::IntoPy; 4 | 5 | /// Copy a nalgebra matrix to a numpy ndarray. 6 | /// 7 | /// This does not create a view of the nalgebra matrix. 8 | /// As such, the matrix can be dropped without problem. 9 | pub fn matrix_to_numpy<'py, N, R, C, S>(py: pyo3::Python<'py>, matrix: &Matrix) -> pyo3::PyObject 10 | where 11 | N: nalgebra::Scalar + numpy::Element, 12 | R: nalgebra::Dim, 13 | C: nalgebra::Dim, 14 | S: nalgebra::storage::Storage, 15 | { 16 | let array = PyArray::new(py, (matrix.nrows(), matrix.ncols()), false); 17 | for r in 0..matrix.nrows() { 18 | for c in 0..matrix.ncols() { 19 | unsafe { 20 | *array.uget_mut((r, c)) = matrix[(r, c)].inlined_clone(); 21 | } 22 | } 23 | } 24 | 25 | array.into_py(py) 26 | } 27 | -------------------------------------------------------------------------------- /tests/errors.rs: -------------------------------------------------------------------------------- 1 | use assert2::assert; 2 | use inline_python::{python, Context}; 3 | use nalgebra::{U1, U2, U3}; 4 | use nalgebra_numpy::{matrix_from_numpy, Error}; 5 | 6 | #[test] 7 | fn wrong_type() { 8 | let gil = pyo3::Python::acquire_gil(); 9 | let py = gil.python(); 10 | let context = Context::new_with_gil(py); 11 | 12 | context.run(python! { 13 | float = 3.4 14 | int = 8 15 | list = [1.0, 2.0, 3.0] 16 | }); 17 | 18 | let get_global = |name| context.globals(py).get_item(name).unwrap(); 19 | 20 | assert!(let Err(Error::WrongObjectType(_)) = matrix_from_numpy::(py, get_global("float"))); 21 | assert!(let Err(Error::WrongObjectType(_)) = matrix_from_numpy::(py, get_global("int"))); 22 | assert!(let Err(Error::WrongObjectType(_)) = matrix_from_numpy::(py, get_global("list"))); 23 | } 24 | 25 | #[test] 26 | fn wrong_shape() { 27 | let gil = pyo3::Python::acquire_gil(); 28 | let py = gil.python(); 29 | let context = Context::new_with_gil(py); 30 | 31 | context.run(python! { 32 | import numpy as np 33 | matrix = np.array([ 34 | [1.0, 2.0, 3.0], 35 | [4.0, 5.0, 6.0], 36 | ]); 37 | }); 38 | 39 | let get_global = |name| context.globals(py).get_item(name).unwrap(); 40 | 41 | assert!(let Ok(_) = matrix_from_numpy::(py, get_global("matrix"))); 42 | assert!(let Err(Error::IncompatibleArray(_)) = matrix_from_numpy::(py, get_global("matrix"))); 43 | assert!(let Err(Error::IncompatibleArray(_)) = matrix_from_numpy::(py, get_global("matrix"))); 44 | } 45 | 46 | #[test] 47 | fn wrong_data_type() { 48 | let gil = pyo3::Python::acquire_gil(); 49 | let py = gil.python(); 50 | let context = Context::new_with_gil(py); 51 | 52 | context.run(python! { 53 | import numpy as np 54 | matrix_f32 = np.array([[1.0]]).astype(np.float32); 55 | matrix_f64 = np.array([[1.0]]).astype(np.float64); 56 | matrix_i32 = np.array([[1]]).astype(np.int32); 57 | matrix_i64 = np.array([[1]]).astype(np.int64); 58 | }); 59 | 60 | let get_global = |name| context.globals(py).get_item(name).expect(name); 61 | 62 | assert!(let Ok(_) = matrix_from_numpy::(py, get_global("matrix_f32"))); 63 | assert!(let Ok(_) = matrix_from_numpy::(py, get_global("matrix_f64"))); 64 | assert!(let Ok(_) = matrix_from_numpy::(py, get_global("matrix_i32"))); 65 | assert!(let Ok(_) = matrix_from_numpy::(py, get_global("matrix_i64"))); 66 | 67 | assert!(let Err(Error::IncompatibleArray(_)) = matrix_from_numpy::(py, get_global("matrix_f64"))); 68 | assert!(let Err(Error::IncompatibleArray(_)) = matrix_from_numpy::(py, get_global("matrix_i32"))); 69 | assert!(let Err(Error::IncompatibleArray(_)) = matrix_from_numpy::(py, get_global("matrix_i64"))); 70 | 71 | assert!(let Err(Error::IncompatibleArray(_)) = matrix_from_numpy::(py, get_global("matrix_f32"))); 72 | assert!(let Err(Error::IncompatibleArray(_)) = matrix_from_numpy::(py, get_global("matrix_i32"))); 73 | assert!(let Err(Error::IncompatibleArray(_)) = matrix_from_numpy::(py, get_global("matrix_i64"))); 74 | 75 | assert!(let Err(Error::IncompatibleArray(_)) = matrix_from_numpy::(py, get_global("matrix_f32"))); 76 | assert!(let Err(Error::IncompatibleArray(_)) = matrix_from_numpy::(py, get_global("matrix_f64"))); 77 | assert!(let Err(Error::IncompatibleArray(_)) = matrix_from_numpy::(py, get_global("matrix_i64"))); 78 | 79 | assert!(let Err(Error::IncompatibleArray(_)) = matrix_from_numpy::(py, get_global("matrix_f32"))); 80 | assert!(let Err(Error::IncompatibleArray(_)) = matrix_from_numpy::(py, get_global("matrix_f64"))); 81 | assert!(let Err(Error::IncompatibleArray(_)) = matrix_from_numpy::(py, get_global("matrix_i32"))); 82 | } 83 | 84 | #[test] 85 | fn unaligned_data() { 86 | let gil = pyo3::Python::acquire_gil(); 87 | let py = gil.python(); 88 | let context = Context::new_with_gil(py); 89 | 90 | context.run(python! { 91 | import numpy as np 92 | unaligned = np.array([range(8)], dtype=np.uint8)[:, 1:7].view(np.uint16) 93 | }); 94 | 95 | let get_global = |name| context.globals(py).get_item(name).unwrap(); 96 | 97 | assert!(let Err(Error::UnalignedArray(_)) = matrix_from_numpy::(py, get_global("unaligned"))); 98 | } 99 | -------------------------------------------------------------------------------- /tests/from_numpy.rs: -------------------------------------------------------------------------------- 1 | use assert2::{assert, let_assert}; 2 | use inline_python::{python, Context}; 3 | use nalgebra::{Dynamic, U2, U3}; 4 | use nalgebra_numpy::matrix_from_numpy; 5 | use nalgebra_numpy::matrix_slice_mut_from_numpy; 6 | 7 | /// Test conversion of a numpy array to a Matrix3. 8 | #[test] 9 | #[rustfmt::skip] 10 | fn matrix3_f64() { 11 | let gil = pyo3::Python::acquire_gil(); 12 | let py = gil.python(); 13 | let context = Context::new_with_gil(py); 14 | 15 | context.run(python! { 16 | import numpy as np 17 | matrix = np.array([ 18 | [1.0, 2.0, 3.0], 19 | [4.0, 5.0, 6.0], 20 | [7.0, 8.0, 9.0], 21 | ]) 22 | }); 23 | 24 | let matrix = context.globals(py).get_item("matrix").unwrap(); 25 | let_assert!(Ok(matrix) = matrix_from_numpy::(py, matrix)); 26 | 27 | assert!(matrix == nalgebra::Matrix3::new( 28 | 1.0, 2.0, 3.0, 29 | 4.0, 5.0, 6.0, 30 | 7.0, 8.0, 9.0, 31 | )); 32 | } 33 | 34 | /// Test conversion of a numpy array to a Matrix3. 35 | #[test] 36 | #[rustfmt::skip] 37 | fn matrix3_f32() { 38 | let gil = pyo3::Python::acquire_gil(); 39 | let py = gil.python(); 40 | let context = Context::new_with_gil(py); 41 | context.run(python! { 42 | import numpy as np 43 | matrix = np.array([ 44 | [1.0, 2.0, 3.0], 45 | [4.0, 5.0, 6.0], 46 | [7.0, 8.0, 9.0], 47 | ]).astype(np.float32) 48 | }); 49 | 50 | let matrix = context.globals(py).get_item("matrix").unwrap(); 51 | let_assert!(Ok(matrix) = matrix_from_numpy::(py, matrix)); 52 | 53 | assert!(matrix == nalgebra::Matrix3::new( 54 | 1.0, 2.0, 3.0, 55 | 4.0, 5.0, 6.0, 56 | 7.0, 8.0, 9.0, 57 | )); 58 | } 59 | 60 | /// Test conversion of a numpy array to a DMatrix3. 61 | #[test] 62 | #[rustfmt::skip] 63 | fn matrixd() { 64 | let gil = pyo3::Python::acquire_gil(); 65 | let py = gil.python(); 66 | let context = Context::new_with_gil(py); 67 | context.run(python! { 68 | import numpy as np 69 | matrix = np.array([ 70 | [1.0, 2.0, 3.0], 71 | [4.0, 5.0, 6.0], 72 | [7.0, 8.0, 9.0], 73 | ]) 74 | }); 75 | 76 | let matrix = context.globals(py).get_item("matrix").unwrap(); 77 | 78 | let_assert!(Ok(matrix) = matrix_from_numpy::(py, matrix)); 79 | assert!(matrix == nalgebra::DMatrix::from_row_slice(3, 3, &[ 80 | 1.0, 2.0, 3.0, 81 | 4.0, 5.0, 6.0, 82 | 7.0, 8.0, 9.0, 83 | ])); 84 | } 85 | 86 | /// Test conversion of a numpy array to a MatrixMN. 87 | #[test] 88 | #[rustfmt::skip] 89 | fn matrix3d() { 90 | let gil = pyo3::Python::acquire_gil(); 91 | let py = gil.python(); 92 | let context = Context::new_with_gil(py); 93 | context.run(python! { 94 | import numpy as np 95 | matrix = np.array([ 96 | [1.0, 2.0, 3.0], 97 | [4.0, 5.0, 6.0], 98 | [7.0, 8.0, 9.0], 99 | ]) 100 | }); 101 | 102 | let matrix = context.globals(py).get_item("matrix").unwrap(); 103 | 104 | let_assert!(Ok(matrix) = matrix_from_numpy::(py, matrix)); 105 | assert!(matrix == nalgebra::MatrixMN::::from_row_slice(&[ 106 | 1.0, 2.0, 3.0, 107 | 4.0, 5.0, 6.0, 108 | 7.0, 8.0, 9.0, 109 | ])); 110 | } 111 | 112 | /// Test conversion of a numpy array to a MatrixMN. 113 | #[test] 114 | #[rustfmt::skip] 115 | fn matrixd3() { 116 | let gil = pyo3::Python::acquire_gil(); 117 | let py = gil.python(); 118 | let context = Context::new_with_gil(py); 119 | context.run(python! { 120 | import numpy as np 121 | matrix = np.array([ 122 | [1.0, 2.0, 3.0], 123 | [4.0, 5.0, 6.0], 124 | [7.0, 8.0, 9.0], 125 | ]) 126 | }); 127 | 128 | let matrix = context.globals(py).get_item("matrix").unwrap(); 129 | 130 | let_assert!(Ok(matrix) = matrix_from_numpy::(py, matrix)); 131 | assert!(matrix == nalgebra::MatrixMN::::from_row_slice(&[ 132 | 1.0, 2.0, 3.0, 133 | 4.0, 5.0, 6.0, 134 | 7.0, 8.0, 9.0, 135 | ])); 136 | } 137 | 138 | /// Test conversion of a non-coniguous numpy array. 139 | #[test] 140 | #[rustfmt::skip] 141 | fn non_contiguous() { 142 | let gil = pyo3::Python::acquire_gil(); 143 | let py = gil.python(); 144 | let context = Context::new_with_gil(py); 145 | context.run(python! { 146 | import numpy as np 147 | matrix = np.array([ 148 | [1.0, 2.0, 3.0], 149 | [4.0, 5.0, 6.0], 150 | [7.0, 8.0, 9.0], 151 | ])[0:2, 0:2]; 152 | }); 153 | 154 | let matrix = context.globals(py).get_item("matrix").unwrap(); 155 | 156 | let_assert!(Ok(matrix) = matrix_from_numpy::(py, matrix)); 157 | assert!(matrix == nalgebra::MatrixN::::new( 158 | 1.0, 2.0, 159 | 4.0, 5.0, 160 | )); 161 | } 162 | 163 | /// Test conversion of a column-major numpy array. 164 | #[test] 165 | #[rustfmt::skip] 166 | fn column_major() { 167 | let gil = pyo3::Python::acquire_gil(); 168 | let py = gil.python(); 169 | let context = Context::new_with_gil(py); 170 | context.run(python! { 171 | import numpy as np 172 | matrix = np.array([ 173 | [1.0, 2.0, 3.0], 174 | [4.0, 5.0, 6.0], 175 | [7.0, 8.0, 9.0], 176 | ], order='F'); 177 | }); 178 | 179 | let matrix = context.globals(py).get_item("matrix").unwrap(); 180 | 181 | let_assert!(Ok(matrix) = matrix_from_numpy::(py, matrix)); 182 | assert!(matrix == nalgebra::Matrix3::new( 183 | 1.0, 2.0, 3.0, 184 | 4.0, 5.0, 6.0, 185 | 7.0, 8.0, 9.0, 186 | )); 187 | } 188 | 189 | /// Test conversion of a column-major numpy array. 190 | #[test] 191 | #[rustfmt::skip] 192 | fn mutable_view() { 193 | let gil = pyo3::Python::acquire_gil(); 194 | let py = gil.python(); 195 | let context = Context::new_with_gil(py); 196 | context.run(python! { 197 | import numpy as np 198 | matrix = np.array([ 199 | [1.0, 2.0, 3.0], 200 | [4.0, 5.0, 6.0], 201 | [7.0, 8.0, 9.0], 202 | ]); 203 | 204 | assert matrix[1, 2] == 6.0 205 | }); 206 | 207 | let matrix = context.globals(py).get_item("matrix").unwrap(); 208 | 209 | let_assert!(Ok(mut matrix) = unsafe { matrix_slice_mut_from_numpy::(py, matrix) }); 210 | 211 | matrix[(1, 2)] = 1337.0; 212 | 213 | // TODO: Do we need to drop the matrix view here to avoid UB? 214 | 215 | context.run(python! { 216 | assert matrix[1, 2] == 1337 217 | }); 218 | } 219 | -------------------------------------------------------------------------------- /tests/to_numpy.rs: -------------------------------------------------------------------------------- 1 | use inline_python::python; 2 | use nalgebra::{DMatrix, Matrix3}; 3 | use nalgebra_numpy::matrix_to_numpy; 4 | 5 | #[test] 6 | #[rustfmt::skip] 7 | fn fixed_size() { 8 | let gil = pyo3::Python::acquire_gil(); 9 | 10 | let matrix = matrix_to_numpy(gil.python(), &Matrix3::::new( 11 | 0, 1, 2, 12 | 3, 4, 5, 13 | 6, 7, 8, 14 | )); 15 | 16 | python! { 17 | from numpy import array_equal 18 | assert array_equal('matrix, [ 19 | [0, 1, 2], 20 | [3, 4, 5], 21 | [6, 7, 8], 22 | ]) 23 | } 24 | } 25 | 26 | #[test] 27 | #[rustfmt::skip] 28 | fn dynamic_size() { 29 | let gil = pyo3::Python::acquire_gil(); 30 | 31 | let matrix = matrix_to_numpy(gil.python(), &DMatrix::::from_row_slice(3, 4, &[ 32 | 0.0, 1.0, 2.0, 3.0, 33 | 4.0, 5.0, 6.0, 7.0, 34 | 8.0, 9.0, 10.0, 11.0, 35 | ])); 36 | 37 | python! { 38 | from numpy import array_equal 39 | assert array_equal('matrix, [ 40 | [0, 1, 2, 3], 41 | [4, 5, 6, 7], 42 | [8, 9, 10, 11], 43 | ]) 44 | } 45 | } 46 | --------------------------------------------------------------------------------