Skip to main content

sim_lib_discrete_algebra/
sparse.rs

1//! Coordinate-list sparse matrix over a [`Semiring`].
2//!
3//! v1 uses the simple, codec-friendly coordinate-list (COO) form. Duplicate
4//! coordinates are merged by semiring `add` during `canonicalize`, which also
5//! drops `zero` entries and sorts by `(row, col)`. CSR/CSC may be added later as
6//! internal acceleration without changing this public contract.
7
8use crate::error::AlgebraError;
9use crate::matrix::Matrix;
10use crate::semiring::Semiring;
11
12/// A single non-default entry of a sparse matrix.
13#[derive(Debug, Clone, PartialEq, Eq)]
14pub struct SparseEntry<S> {
15    /// Row index.
16    pub row: usize,
17    /// Column index.
18    pub col: usize,
19    /// Stored value.
20    pub value: S,
21}
22
23/// A coordinate-list sparse matrix.
24#[derive(Debug, Clone, PartialEq)]
25pub struct SparseMatrix<S: Semiring> {
26    /// Number of rows.
27    pub rows: usize,
28    /// Number of columns.
29    pub cols: usize,
30    /// Stored entries (canonical after [`SparseMatrix::canonicalize`]).
31    pub entries: Vec<SparseEntry<S>>,
32}
33
34impl<S: Semiring> SparseMatrix<S> {
35    /// An empty `rows x cols` sparse matrix.
36    pub fn new(rows: usize, cols: usize) -> Self {
37        SparseMatrix {
38            rows,
39            cols,
40            entries: Vec::new(),
41        }
42    }
43
44    /// Checked empty `rows x cols` sparse matrix.
45    pub fn try_new(rows: usize, cols: usize) -> Result<Self, AlgebraError> {
46        let m = SparseMatrix {
47            rows,
48            cols,
49            entries: Vec::new(),
50        };
51        m.validate()?;
52        Ok(m)
53    }
54
55    /// Number of sparse matrix rows.
56    pub fn row_count(&self) -> usize {
57        self.rows
58    }
59
60    /// Number of sparse matrix columns.
61    pub fn col_count(&self) -> usize {
62        self.cols
63    }
64
65    /// Read-only sparse entries.
66    pub fn entries(&self) -> &[SparseEntry<S>] {
67        &self.entries
68    }
69
70    /// Build from entries, rejecting any whose coordinates are out of range.
71    pub fn from_entries(
72        rows: usize,
73        cols: usize,
74        entries: Vec<SparseEntry<S>>,
75    ) -> Result<Self, AlgebraError> {
76        let mut m = SparseMatrix {
77            rows,
78            cols,
79            entries,
80        };
81        m.validate()?;
82        m.canonicalize();
83        Ok(m)
84    }
85
86    /// Validate public sparse dimensions and entry coordinates.
87    pub fn validate(&self) -> Result<(), AlgebraError> {
88        let len = self
89            .rows
90            .checked_mul(self.cols)
91            .ok_or(AlgebraError::DimensionOverflow {
92                rows: self.rows,
93                cols: self.cols,
94            })?;
95        for e in &self.entries {
96            if e.row >= self.rows || e.col >= self.cols {
97                return Err(AlgebraError::IndexOutOfBounds {
98                    index: e.row.saturating_mul(self.cols).saturating_add(e.col),
99                    len,
100                });
101            }
102        }
103        Ok(())
104    }
105
106    /// Sort entries by `(row, col)`, merge duplicates by semiring `add`, and
107    /// drop entries equal to `zero`.
108    pub fn canonicalize(&mut self) {
109        self.entries.sort_by_key(|e| (e.row, e.col));
110        let mut merged: Vec<SparseEntry<S>> = Vec::with_capacity(self.entries.len());
111        for e in self.entries.drain(..) {
112            match merged.last_mut() {
113                Some(last) if last.row == e.row && last.col == e.col => {
114                    last.value = last.value.add(&e.value);
115                }
116                _ => merged.push(e),
117            }
118        }
119        merged.retain(|e| !e.value.is_zero());
120        self.entries = merged;
121    }
122
123    /// Densify, filling absent positions with the semiring `zero`.
124    pub fn to_dense(&self) -> Result<Matrix<S>, AlgebraError> {
125        self.validate()?;
126        let mut m = Matrix::try_new(self.rows, self.cols)?;
127        for e in &self.entries {
128            m.set(e.row, e.col, e.value.clone())?;
129        }
130        Ok(m)
131    }
132
133    /// Build a canonical sparse matrix from a dense one, keeping non-`zero`
134    /// entries only.
135    pub fn from_dense_nonzero(dense: &Matrix<S>) -> Result<Self, AlgebraError> {
136        dense.validate()?;
137        let mut entries = Vec::new();
138        for r in 0..dense.rows {
139            for c in 0..dense.cols {
140                let v = dense.get(r, c)?;
141                if !v.is_zero() {
142                    entries.push(SparseEntry {
143                        row: r,
144                        col: c,
145                        value: v.clone(),
146                    });
147                }
148            }
149        }
150        Ok(SparseMatrix {
151            rows: dense.rows,
152            cols: dense.cols,
153            entries,
154        })
155    }
156}
157
158#[cfg(test)]
159mod tests {
160    use super::*;
161    use crate::Counting;
162
163    fn e(row: usize, col: usize, v: u64) -> SparseEntry<Counting> {
164        SparseEntry {
165            row,
166            col,
167            value: Counting::from_u64(v),
168        }
169    }
170
171    #[test]
172    fn from_entries_rejects_out_of_range() {
173        let r = SparseMatrix::from_entries(2, 2, vec![e(0, 0, 1), e(2, 0, 1)]);
174        assert!(matches!(r, Err(AlgebraError::IndexOutOfBounds { .. })));
175    }
176
177    #[test]
178    fn canonicalize_merges_duplicates_and_sorts() {
179        let m = SparseMatrix::from_entries(2, 2, vec![e(1, 1, 2), e(0, 0, 3), e(1, 1, 5)]).unwrap();
180        assert_eq!(m.entries, vec![e(0, 0, 3), e(1, 1, 7)]);
181    }
182
183    #[test]
184    fn canonicalize_drops_zero() {
185        // A merge that produces zero in the counting semiring only happens when
186        // both are zero; an explicit zero entry is dropped.
187        let m = SparseMatrix::from_entries(1, 2, vec![e(0, 0, 0), e(0, 1, 4)]).unwrap();
188        assert_eq!(m.entries, vec![e(0, 1, 4)]);
189    }
190
191    #[test]
192    fn dense_sparse_round_trip() {
193        let m = SparseMatrix::from_entries(2, 2, vec![e(0, 1, 7), e(1, 0, 9)]).unwrap();
194        let dense = m.to_dense().unwrap();
195        let back = SparseMatrix::from_dense_nonzero(&dense).unwrap();
196        assert_eq!(back, m);
197    }
198
199    #[test]
200    fn invalid_public_sparse_matrix_fails_before_densifying() {
201        let bad = SparseMatrix {
202            rows: 1,
203            cols: 1,
204            entries: vec![e(1, 0, 4)],
205        };
206
207        assert!(matches!(
208            bad.validate(),
209            Err(AlgebraError::IndexOutOfBounds { .. })
210        ));
211        assert!(matches!(
212            bad.to_dense(),
213            Err(AlgebraError::IndexOutOfBounds { .. })
214        ));
215    }
216
217    #[test]
218    fn checked_sparse_constructor_rejects_dimension_overflow() {
219        assert!(matches!(
220            SparseMatrix::<Counting>::try_new(usize::MAX, 2),
221            Err(AlgebraError::DimensionOverflow { .. })
222        ));
223    }
224}