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}