Skip to main content

rhai_sci/matrix/
mod.rs

1#[cfg(feature = "nalgebra")]
2use nalgebralib::{DMatrix, DVector};
3#[cfg(feature = "nalgebra")]
4use rhai::FLOAT;
5use rhai::{Array, Dynamic, EvalAltResult, Position};
6
7/// Inspect every row rather than inferring a rectangular shape from the first row.
8pub(crate) fn matrix_dimensions(values: &Array) -> Option<(usize, usize)> {
9    let first = values.first()?.clone().into_array().ok()?;
10    let cols = first.len();
11    if cols == 0 {
12        return None;
13    }
14    for value in values {
15        let row = value.clone().into_array().ok()?;
16        if row.len() != cols || row.iter().any(Dynamic::is_array) {
17            return None;
18        }
19    }
20    Some((values.len(), cols))
21}
22
23/// Normalize a nonempty numeric list, row, or column without changing its values.
24pub(crate) fn numeric_vector_data(values: &Array) -> Result<Array, Box<EvalAltResult>> {
25    let numeric = |value: &Dynamic| value.is_int() || value.is_float();
26    let data = if values.iter().all(|value| !value.is_array()) {
27        values.clone()
28    } else {
29        match matrix_dimensions(values) {
30            Some((1, _)) => values[0].clone().into_array().unwrap(),
31            Some((_, 1)) => values
32                .iter()
33                .map(|value| value.clone().into_array().unwrap().remove(0))
34                .collect(),
35            _ => {
36                return Err(EvalAltResult::ErrorArithmetic(
37                    "Expected a numeric list, row vector, or column vector".into(),
38                    Position::NONE,
39                )
40                .into())
41            }
42        }
43    };
44    if data.is_empty() {
45        return Err(EvalAltResult::ErrorArithmetic(
46            "Numeric vectors must contain at least one value".into(),
47            Position::NONE,
48        )
49        .into());
50    }
51    if !data.iter().all(numeric) {
52        return Err(EvalAltResult::ErrorArithmetic(
53            "Vector elements must be INT or FLOAT".into(),
54            Position::NONE,
55        )
56        .into());
57    }
58    Ok(data)
59}
60
61/// Wrapper around [`rhai::Array`] representing a matrix.
62///
63/// This type provides conversions between Rhai arrays and
64/// `nalgebra::DMatrix`.
65///
66/// # Examples
67/// ```
68/// use rhai::{Array, Dynamic};
69/// use rhai_sci::matrix::RhaiMatrix;
70/// let raw: Array = vec![
71///     Dynamic::from_array(vec![Dynamic::from_float(1.0), Dynamic::from_float(2.0)]),
72///     Dynamic::from_array(vec![Dynamic::from_float(3.0), Dynamic::from_float(4.0)]),
73/// ];
74/// let matrix = RhaiMatrix::from_array(raw.clone());
75/// assert_eq!(matrix.to_array().len(), raw.len());
76/// ```
77#[derive(Clone, Debug)]
78pub struct RhaiMatrix(Array);
79
80impl RhaiMatrix {
81    /// Construct a [`RhaiMatrix`] from a [`rhai::Array`].
82    #[must_use]
83    pub fn from_array(arr: Array) -> Self {
84        Self(arr)
85    }
86
87    /// Construct a [`RhaiMatrix`] representing a row vector (`1×N`).
88    ///
89    /// # Examples
90    /// ```
91    /// use rhai::{Array, Dynamic};
92    /// use rhai_sci::matrix::RhaiMatrix;
93    /// let data: Array = vec![Dynamic::from_int(1), Dynamic::from_int(2)];
94    /// let row = RhaiMatrix::row_vector(data.clone());
95    /// assert!(row.as_row().is_some());
96    /// ```
97    #[must_use]
98    pub fn row_vector(data: Array) -> Self {
99        Self(vec![Dynamic::from_array(data)])
100    }
101
102    /// Construct a [`RhaiMatrix`] representing a column vector (`N×1`).
103    ///
104    /// # Examples
105    /// ```
106    /// use rhai::{Array, Dynamic};
107    /// use rhai_sci::matrix::RhaiMatrix;
108    /// let data: Array = vec![Dynamic::from_int(1), Dynamic::from_int(2)];
109    /// let column = RhaiMatrix::column_vector(data.clone());
110    /// assert!(column.as_column().is_some());
111    /// ```
112    #[must_use]
113    pub fn column_vector(data: Array) -> Self {
114        let rows = data
115            .into_iter()
116            .map(|v| Dynamic::from_array(vec![v]))
117            .collect();
118        Self(rows)
119    }
120
121    /// Return the matrix as a row vector (`1×N`), reshaping a column vector if necessary.
122    ///
123    /// Returns `None` if the matrix is not `1×N` or `N×1`.
124    ///
125    /// # Examples
126    /// ```
127    /// use rhai::{Array, Dynamic};
128    /// use rhai_sci::matrix::RhaiMatrix;
129    /// let data: Array = vec![Dynamic::from_int(1), Dynamic::from_int(2)];
130    /// let column = RhaiMatrix::column_vector(data.clone());
131    /// let row = column.as_row().unwrap();
132    /// assert!(row.as_row().is_some());
133    /// ```
134    #[must_use]
135    pub fn as_row(&self) -> Option<Self> {
136        match matrix_dimensions(&self.0) {
137            Some((1, _)) => Some(self.clone()),
138            Some((_, 1)) => Some(Self::row_vector(crate::matrix_functions::flatten(
139                &mut self.0.clone(),
140            ))),
141            _ => None,
142        }
143    }
144
145    /// Return the matrix as a column vector (`N×1`), reshaping a row vector if necessary.
146    ///
147    /// Returns `None` if the matrix is not `1×N` or `N×1`.
148    ///
149    /// # Examples
150    /// ```
151    /// use rhai::{Array, Dynamic};
152    /// use rhai_sci::matrix::RhaiMatrix;
153    /// let data: Array = vec![Dynamic::from_int(1), Dynamic::from_int(2)];
154    /// let row = RhaiMatrix::row_vector(data.clone());
155    /// let column = row.as_column().unwrap();
156    /// assert!(column.as_column().is_some());
157    /// ```
158    #[must_use]
159    pub fn as_column(&self) -> Option<Self> {
160        match matrix_dimensions(&self.0) {
161            Some((_, 1)) => Some(self.clone()),
162            Some((1, _)) => Some(Self::column_vector(self.0[0].clone().into_array().unwrap())),
163            _ => None,
164        }
165    }
166
167    /// Convert the matrix back into a [`rhai::Array`].
168    #[must_use]
169    pub fn to_array(self) -> Array {
170        self.0
171    }
172
173    /// Convert the matrix into a `nalgebra::DMatrix`.
174    ///
175    /// # Errors
176    /// Returns an error if any element is non-numeric or rows have differing lengths.
177    ///
178    /// # Panics
179    /// Panics if an integer value cannot be represented as `FLOAT`.
180    #[cfg(feature = "nalgebra")]
181    #[allow(clippy::cast_precision_loss)]
182    pub fn to_dmatrix(&self) -> Result<DMatrix<FLOAT>, Box<EvalAltResult>> {
183        if self.0.is_empty() {
184            return Ok(DMatrix::from_element(0, 0, 0.0));
185        }
186        let rows = self.0.len();
187        let first_row = self.0[0].clone().into_array().map_err(|_| {
188            EvalAltResult::ErrorArithmetic(
189                "Matrix must contain row arrays".to_string(),
190                Position::NONE,
191            )
192        })?;
193        let cols = first_row.len();
194        let mut dm = DMatrix::zeros(rows, cols);
195        for (i, row_dyn) in self.0.iter().enumerate() {
196            let row = row_dyn.clone().into_array().map_err(|_| {
197                EvalAltResult::ErrorArithmetic(
198                    "Matrix must contain row arrays".to_string(),
199                    Position::NONE,
200                )
201            })?;
202            if row.len() != cols {
203                return Err(EvalAltResult::ErrorArithmetic(
204                    "Matrix rows must have equal length".to_string(),
205                    Position::NONE,
206                )
207                .into());
208            }
209            for (j, val) in row.iter().enumerate() {
210                dm[(i, j)] = if val.is_float() {
211                    val.as_float().unwrap()
212                } else if val.is_int() {
213                    val.as_int().unwrap() as FLOAT
214                } else {
215                    return Err(EvalAltResult::ErrorArithmetic(
216                        "Matrix elements must be INT or FLOAT".to_string(),
217                        Position::NONE,
218                    )
219                    .into());
220                };
221            }
222        }
223        Ok(dm)
224    }
225
226    /// Create a [`RhaiMatrix`] from a `nalgebra::DMatrix`.
227    #[cfg(feature = "nalgebra")]
228    #[must_use]
229    pub fn from_dmatrix(mat: &DMatrix<FLOAT>) -> Self {
230        let mut rows = Vec::with_capacity(mat.nrows());
231        for i in 0..mat.nrows() {
232            let mut row = Vec::with_capacity(mat.ncols());
233            for j in 0..mat.ncols() {
234                row.push(Dynamic::from_float(mat[(i, j)]));
235            }
236            rows.push(Dynamic::from_array(row));
237        }
238        Self(rows)
239    }
240
241    /// Transpose the matrix.
242    ///
243    /// # Errors
244    /// Returns an error if the matrix contains non-numeric values or rows of
245    /// unequal length.
246    #[cfg(feature = "nalgebra")]
247    pub fn transpose(&self) -> Result<Self, Box<EvalAltResult>> {
248        let dm = self.to_dmatrix()?;
249        Ok(Self::from_dmatrix(&dm.transpose()))
250    }
251
252    /// Horizontally concatenate two matrices.
253    ///
254    /// # Examples
255    /// ```
256    /// use rhai::{Array, Dynamic};
257    /// use rhai_sci::{matrix::RhaiMatrix, validation_functions::is_row_vector};
258    /// let left = RhaiMatrix::row_vector(vec![Dynamic::from_int(1), Dynamic::from_int(2)]);
259    /// let right = RhaiMatrix::row_vector(vec![Dynamic::from_int(3), Dynamic::from_int(4)]);
260    /// let combined = left.concat_h(&right).unwrap();
261    /// let mut arr = combined.to_array();
262    /// assert!(is_row_vector(&mut arr));
263    /// ```
264    /// # Errors
265    /// Returns an error if the matrices have differing row counts or contain
266    /// non-numeric values.
267    #[cfg(feature = "nalgebra")]
268    pub fn concat_h(&self, other: &Self) -> Result<Self, Box<EvalAltResult>> {
269        let left = self.to_dmatrix()?;
270        let right = other.to_dmatrix()?;
271        if left.nrows() != right.nrows() {
272            return Err(EvalAltResult::ErrorArithmetic(
273                "Matrices must have the same number of rows".to_string(),
274                Position::NONE,
275            )
276            .into());
277        }
278        let cols = left.ncols() + right.ncols();
279        let rows = left.nrows();
280        let mat = DMatrix::from_fn(rows, cols, |i, j| {
281            if j < left.ncols() {
282                left[(i, j)]
283            } else {
284                right[(i, j - left.ncols())]
285            }
286        });
287        Ok(Self::from_dmatrix(&mat))
288    }
289
290    /// Vertically concatenate two matrices.
291    ///
292    /// # Examples
293    /// ```
294    /// use rhai::{Array, Dynamic};
295    /// use rhai_sci::{matrix::RhaiMatrix, validation_functions::is_column_vector};
296    /// let top = RhaiMatrix::column_vector(vec![Dynamic::from_int(1), Dynamic::from_int(2)]);
297    /// let bottom = RhaiMatrix::column_vector(vec![Dynamic::from_int(3), Dynamic::from_int(4)]);
298    /// let combined = top.concat_v(&bottom).unwrap();
299    /// let mut arr = combined.to_array();
300    /// assert!(is_column_vector(&mut arr));
301    /// ```
302    /// # Errors
303    /// Returns an error if the matrices have differing column counts or contain
304    /// non-numeric values.
305    #[cfg(feature = "nalgebra")]
306    pub fn concat_v(&self, other: &Self) -> Result<Self, Box<EvalAltResult>> {
307        let top = self.to_dmatrix()?;
308        let bottom = other.to_dmatrix()?;
309        if top.ncols() != bottom.ncols() {
310            return Err(EvalAltResult::ErrorArithmetic(
311                "Matrices must have the same number of columns".to_string(),
312                Position::NONE,
313            )
314            .into());
315        }
316        let rows = top.nrows() + bottom.nrows();
317        let cols = top.ncols();
318        let mat = DMatrix::from_fn(rows, cols, |i, j| {
319            if i < top.nrows() {
320                top[(i, j)]
321            } else {
322                bottom[(i - top.nrows(), j)]
323            }
324        });
325        Ok(Self::from_dmatrix(&mat))
326    }
327}
328
329/// Wrapper around [`rhai::Array`] representing a vector.
330///
331/// # Examples
332/// ```
333/// use rhai::{Array, Dynamic};
334/// use rhai_sci::matrix::RhaiVector;
335/// let raw: Array = vec![Dynamic::from_float(1.0), Dynamic::from_float(2.0)];
336/// let vector = RhaiVector::from_array(raw.clone());
337/// assert_eq!(vector.to_array().len(), raw.len());
338/// ```
339#[derive(Clone, Debug)]
340pub struct RhaiVector(Array);
341
342impl RhaiVector {
343    /// Construct a [`RhaiVector`] from a [`rhai::Array`].
344    #[must_use]
345    pub fn from_array(arr: Array) -> Self {
346        Self(arr)
347    }
348
349    /// Convert the vector back into a [`rhai::Array`].
350    #[must_use]
351    pub fn to_array(self) -> Array {
352        self.0
353    }
354
355    /// Convert the vector into a `nalgebra::DVector`.
356    ///
357    /// # Errors
358    /// Returns an error if any element is non-numeric.
359    ///
360    /// # Panics
361    /// Panics if an integer value cannot be represented as `FLOAT`.
362    #[cfg(feature = "nalgebra")]
363    #[allow(clippy::cast_precision_loss)]
364    pub fn to_dvector(&self) -> Result<DVector<FLOAT>, Box<EvalAltResult>> {
365        let mut dv = DVector::zeros(self.0.len());
366        for (i, val) in self.0.iter().enumerate() {
367            dv[i] = if val.is_float() {
368                val.as_float().unwrap()
369            } else if val.is_int() {
370                val.as_int().unwrap() as FLOAT
371            } else {
372                return Err(EvalAltResult::ErrorArithmetic(
373                    "Vector elements must be INT or FLOAT".to_string(),
374                    Position::NONE,
375                )
376                .into());
377            };
378        }
379        Ok(dv)
380    }
381
382    /// Create a [`RhaiVector`] from a `nalgebra::DVector`.
383    #[cfg(feature = "nalgebra")]
384    #[must_use]
385    pub fn from_dvector(vec: &DVector<FLOAT>) -> Self {
386        let mut data = Vec::with_capacity(vec.len());
387        for i in 0..vec.len() {
388            data.push(Dynamic::from_float(vec[i]));
389        }
390        Self(data)
391    }
392}