Skip to main content

sim_lib_discrete_algebra/
matrix.rs

1//! Generic dense matrix over a [`Semiring`], with semiring matrix multiply.
2//!
3//! Row-major storage: element `(r, c)` lives at `data[r * cols + c]`. The
4//! `data.len() == rows * cols` invariant is maintained by every checked
5//! constructor. The fields remain public for wire compatibility, so every
6//! fallible reader and operator validates public values before indexing.
7
8use crate::error::AlgebraError;
9use crate::semiring::Semiring;
10
11/// Explicit limits for potentially expensive matrix operations.
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub struct AlgebraLimits {
14    /// Maximum allowed dimension `n` for `power` / `closure` / `materialize`.
15    pub max_dim: usize,
16}
17
18impl Default for AlgebraLimits {
19    fn default() -> Self {
20        AlgebraLimits { max_dim: 1024 }
21    }
22}
23
24impl AlgebraLimits {
25    /// Effectively unlimited; intended for tests.
26    pub fn unlimited() -> Self {
27        AlgebraLimits {
28            max_dim: usize::MAX,
29        }
30    }
31
32    fn check_matrix_dims(&self, rows: usize, cols: usize, op: &str) -> Result<(), AlgebraError> {
33        let max_dim = rows.max(cols);
34        if max_dim > self.max_dim {
35            return Err(AlgebraError::LimitExceeded(format!(
36                "{op}: dimension {max_dim} exceeds max_dim {}",
37                self.max_dim
38            )));
39        }
40        Ok(())
41    }
42}
43
44/// A dense, row-major matrix over the semiring `S`.
45///
46/// # Examples
47///
48/// Build matrices over the counting semiring and multiply them; the identity
49/// acts as a multiplicative unit:
50///
51/// ```
52/// use sim_lib_discrete_algebra::{Counting, Matrix};
53///
54/// let a = Matrix::from_rows(vec![
55///     vec![Counting::from_u64(1), Counting::from_u64(2)],
56///     vec![Counting::from_u64(3), Counting::from_u64(4)],
57/// ])
58/// .unwrap();
59/// let id = Matrix::identity(2);
60///
61/// assert_eq!(a.matmul(&id).unwrap(), a);
62/// assert_eq!(a.get(1, 0).unwrap(), &Counting::from_u64(3));
63/// ```
64#[derive(Debug, Clone, PartialEq)]
65pub struct Matrix<S: Semiring> {
66    /// Number of rows.
67    pub rows: usize,
68    /// Number of columns.
69    pub cols: usize,
70    /// Row-major entries; `data.len() == rows * cols`.
71    pub data: Vec<S>,
72}
73
74impl<S: Semiring> Matrix<S> {
75    /// A `rows x cols` matrix filled with the semiring `zero`.
76    pub fn new(rows: usize, cols: usize) -> Self {
77        Self::filled(rows, cols, S::zero())
78    }
79
80    /// Checked `rows x cols` matrix filled with the semiring `zero`.
81    pub fn try_new(rows: usize, cols: usize) -> Result<Self, AlgebraError> {
82        Self::try_filled(rows, cols, S::zero())
83    }
84
85    /// Checked `rows x cols` matrix filled with the semiring `zero`, guarded by
86    /// an explicit dimension limit.
87    pub fn try_new_with_limits(
88        rows: usize,
89        cols: usize,
90        limits: AlgebraLimits,
91    ) -> Result<Self, AlgebraError> {
92        Self::try_filled_with_limits(rows, cols, S::zero(), limits)
93    }
94
95    /// A `rows x cols` matrix filled with `value`.
96    pub fn filled(rows: usize, cols: usize, value: S) -> Self {
97        let len = checked_len(rows, cols).expect("matrix dimensions must fit in usize");
98        Matrix {
99            rows,
100            cols,
101            data: vec![value; len],
102        }
103    }
104
105    /// Checked `rows x cols` matrix filled with `value`.
106    pub fn try_filled(rows: usize, cols: usize, value: S) -> Result<Self, AlgebraError> {
107        let len = checked_len(rows, cols)?;
108        Ok(Matrix {
109            rows,
110            cols,
111            data: vec![value; len],
112        })
113    }
114
115    /// Checked `rows x cols` matrix filled with `value`, guarded by an explicit
116    /// dimension limit.
117    pub fn try_filled_with_limits(
118        rows: usize,
119        cols: usize,
120        value: S,
121        limits: AlgebraLimits,
122    ) -> Result<Self, AlgebraError> {
123        limits.check_matrix_dims(rows, cols, "matrix construction")?;
124        Self::try_filled(rows, cols, value)
125    }
126
127    /// The `n x n` identity: `one` on the diagonal, `zero` elsewhere.
128    pub fn identity(n: usize) -> Self {
129        let mut m = Self::new(n, n);
130        for i in 0..n {
131            m.data[i * n + i] = S::one();
132        }
133        m
134    }
135
136    /// Checked `n x n` identity matrix.
137    pub fn try_identity(n: usize) -> Result<Self, AlgebraError> {
138        let mut m = Self::try_new(n, n)?;
139        for i in 0..n {
140            m.data[i * n + i] = S::one();
141        }
142        Ok(m)
143    }
144
145    /// Checked `n x n` identity matrix guarded by an explicit dimension limit.
146    pub fn try_identity_with_limits(n: usize, limits: AlgebraLimits) -> Result<Self, AlgebraError> {
147        limits.check_matrix_dims(n, n, "identity construction")?;
148        Self::try_identity(n)
149    }
150
151    /// Build from a vector of rows, rejecting ragged input.
152    pub fn from_rows(rows: Vec<Vec<S>>) -> Result<Self, AlgebraError> {
153        let nrows = rows.len();
154        let ncols = rows.first().map_or(0, Vec::len);
155        let expected = checked_len(nrows, ncols)?;
156        let mut data = Vec::with_capacity(expected);
157        for row in rows {
158            if row.len() != ncols {
159                return Err(AlgebraError::Ragged);
160            }
161            data.extend(row);
162        }
163        Ok(Matrix {
164            rows: nrows,
165            cols: ncols,
166            data,
167        })
168    }
169
170    /// Whether the matrix is square.
171    pub fn is_square(&self) -> bool {
172        self.rows == self.cols
173    }
174
175    /// Number of matrix rows.
176    pub fn row_count(&self) -> usize {
177        self.rows
178    }
179
180    /// Number of matrix columns.
181    pub fn col_count(&self) -> usize {
182        self.cols
183    }
184
185    /// Read-only row-major data slice.
186    pub fn data(&self) -> &[S] {
187        &self.data
188    }
189
190    /// Validate the public structural invariant before indexing by shape.
191    pub fn validate(&self) -> Result<(), AlgebraError> {
192        let expected = checked_len(self.rows, self.cols)?;
193        if self.data.len() != expected {
194            return Err(AlgebraError::InvalidMatrix {
195                rows: self.rows,
196                cols: self.cols,
197                expected,
198                actual: self.data.len(),
199            });
200        }
201        Ok(())
202    }
203
204    /// Bounds-checked read of entry `(r, c)`.
205    pub fn get(&self, r: usize, c: usize) -> Result<&S, AlgebraError> {
206        self.validate()?;
207        if r >= self.rows || c >= self.cols {
208            return Err(AlgebraError::IndexOutOfBounds {
209                index: r.saturating_mul(self.cols).saturating_add(c),
210                len: self.data.len(),
211            });
212        }
213        Ok(&self.data[offset(self.cols, r, c)?])
214    }
215
216    /// Bounds-checked write of entry `(r, c)`.
217    pub fn set(&mut self, r: usize, c: usize, value: S) -> Result<(), AlgebraError> {
218        self.validate()?;
219        if r >= self.rows || c >= self.cols {
220            return Err(AlgebraError::IndexOutOfBounds {
221                index: r.saturating_mul(self.cols).saturating_add(c),
222                len: self.data.len(),
223            });
224        }
225        let index = offset(self.cols, r, c)?;
226        self.data[index] = value;
227        Ok(())
228    }
229
230    /// Immutable slice of row `r`, or an error if out of range.
231    pub fn row(&self, r: usize) -> Result<&[S], AlgebraError> {
232        self.validate()?;
233        if r >= self.rows {
234            return Err(AlgebraError::IndexOutOfBounds {
235                index: r,
236                len: self.rows,
237            });
238        }
239        let start = r
240            .checked_mul(self.cols)
241            .ok_or(AlgebraError::DimensionOverflow {
242                rows: r,
243                cols: self.cols,
244            })?;
245        let end = start
246            .checked_add(self.cols)
247            .ok_or(AlgebraError::DimensionOverflow {
248                rows: r + 1,
249                cols: self.cols,
250            })?;
251        Ok(&self.data[start..end])
252    }
253
254    /// The transpose (a fresh `cols x rows` matrix).
255    pub fn transpose(&self) -> Result<Self, AlgebraError> {
256        self.validate()?;
257        let len = checked_len(self.cols, self.rows)?;
258        let mut data = Vec::with_capacity(len);
259        for c in 0..self.cols {
260            for r in 0..self.rows {
261                data.push(self.data[offset(self.cols, r, c)?].clone());
262            }
263        }
264        Ok(Matrix {
265            rows: self.cols,
266            cols: self.rows,
267            data,
268        })
269    }
270
271    /// Semiring matrix multiply: `self` (`m x p`) by `other` (`p x q`).
272    pub fn matmul(&self, other: &Self) -> Result<Self, AlgebraError> {
273        self.validate()?;
274        other.validate()?;
275        if self.cols != other.rows {
276            return Err(AlgebraError::ShapeMismatch(format!(
277                "matmul: {}x{} by {}x{}",
278                self.rows, self.cols, other.rows, other.cols
279            )));
280        }
281        let len = checked_len(self.rows, other.cols)?;
282        let mut data = Vec::with_capacity(len);
283        for i in 0..self.rows {
284            for j in 0..other.cols {
285                let mut acc = S::zero();
286                for k in 0..self.cols {
287                    let term = self.data[offset(self.cols, i, k)?]
288                        .mul(&other.data[offset(other.cols, k, j)?]);
289                    acc = acc.add(&term);
290                }
291                data.push(acc);
292            }
293        }
294        Ok(Matrix {
295            rows: self.rows,
296            cols: other.cols,
297            data,
298        })
299    }
300}
301
302fn checked_len(rows: usize, cols: usize) -> Result<usize, AlgebraError> {
303    rows.checked_mul(cols)
304        .ok_or(AlgebraError::DimensionOverflow { rows, cols })
305}
306
307fn offset(cols: usize, row: usize, col: usize) -> Result<usize, AlgebraError> {
308    row.checked_mul(cols)
309        .and_then(|base| base.checked_add(col))
310        .ok_or(AlgebraError::DimensionOverflow { rows: row, cols })
311}
312
313#[cfg(test)]
314mod tests {
315    use super::*;
316    use crate::Counting;
317    use crate::tropical_min::MinPlus;
318
319    #[test]
320    fn from_rows_rejects_ragged() {
321        let r = Matrix::from_rows(vec![
322            vec![Counting::from_u64(1)],
323            vec![Counting::from_u64(1), Counting::from_u64(2)],
324        ]);
325        assert_eq!(r.unwrap_err(), AlgebraError::Ragged);
326    }
327
328    #[test]
329    fn matmul_known_result_over_counting() {
330        // [[1,2],[3,4]] * [[5,6],[7,8]] = [[19,22],[43,50]] over the integers.
331        let a = Matrix::from_rows(vec![
332            vec![Counting::from_u64(1), Counting::from_u64(2)],
333            vec![Counting::from_u64(3), Counting::from_u64(4)],
334        ])
335        .unwrap();
336        let b = Matrix::from_rows(vec![
337            vec![Counting::from_u64(5), Counting::from_u64(6)],
338            vec![Counting::from_u64(7), Counting::from_u64(8)],
339        ])
340        .unwrap();
341        let c = a.matmul(&b).unwrap();
342        assert_eq!(c.data[0], Counting::from_u64(19));
343        assert_eq!(c.data[1], Counting::from_u64(22));
344        assert_eq!(c.data[2], Counting::from_u64(43));
345        assert_eq!(c.data[3], Counting::from_u64(50));
346    }
347
348    #[test]
349    fn matmul_shape_mismatch() {
350        let a: Matrix<MinPlus> = Matrix::new(2, 3);
351        let b: Matrix<MinPlus> = Matrix::new(2, 2);
352        assert!(matches!(a.matmul(&b), Err(AlgebraError::ShapeMismatch(_))));
353    }
354
355    #[test]
356    fn invalid_public_matrix_fails_before_indexing() {
357        let bad = Matrix {
358            rows: 2,
359            cols: 2,
360            data: vec![Counting::from_u64(1), Counting::from_u64(2)],
361        };
362
363        assert!(matches!(
364            bad.validate(),
365            Err(AlgebraError::InvalidMatrix { .. })
366        ));
367        assert!(matches!(
368            bad.get(0, 0),
369            Err(AlgebraError::InvalidMatrix { .. })
370        ));
371        assert!(matches!(
372            bad.row(0),
373            Err(AlgebraError::InvalidMatrix { .. })
374        ));
375        assert!(matches!(
376            bad.transpose(),
377            Err(AlgebraError::InvalidMatrix { .. })
378        ));
379        assert!(matches!(
380            bad.matmul(&Matrix::identity(2)),
381            Err(AlgebraError::InvalidMatrix { .. })
382        ));
383    }
384
385    #[test]
386    fn checked_constructors_reject_dimension_overflow() {
387        assert!(matches!(
388            Matrix::<Counting>::try_new(usize::MAX, 2),
389            Err(AlgebraError::DimensionOverflow { .. })
390        ));
391        assert!(matches!(
392            Matrix::<Counting>::try_filled(2, usize::MAX, Counting::from_u64(1)),
393            Err(AlgebraError::DimensionOverflow { .. })
394        ));
395    }
396
397    #[test]
398    fn identity_is_multiplicative_unit() {
399        let a = Matrix::from_rows(vec![
400            vec![Counting::from_u64(1), Counting::from_u64(2)],
401            vec![Counting::from_u64(3), Counting::from_u64(4)],
402        ])
403        .unwrap();
404        let id = Matrix::identity(2);
405        assert_eq!(a.matmul(&id).unwrap(), a);
406        assert_eq!(id.matmul(&a).unwrap(), a);
407    }
408}