Skip to main content

xingo_sparse/
csc.rs

1//! # Chapter 5.8.1 — Compressed sparse column storage
2//!
3//! ## Purpose
4//! [`CscMatrix`] is the canonical storage of the constraint matrix `A`. It is
5//! physically identical to an Arrow `ListArray` of `(row, val)` and to R's
6//! `Matrix::dgCMatrix` (`PLAN.md` §4.2), so the fields are public for
7//! zero-copy interoperability; [`CscMatrix::check_invariants`] guards them.
8//!
9//! ## Notation
10//! `A ∈ R^{m×n}`; column `j` holds the pairs `(row_idx[p], val[p])` for
11//! `p ∈ [col_ptr[j], col_ptr[j+1])`. A *triplet* `(i, j, v)` is one entry of
12//! the coordinate (COO) format; a triplet list may contain duplicates, which
13//! denote the sum of their values, and zeros.
14//!
15//! ## Algorithm C (Compression of a triplet list)
16//! Given `m`, `n` and triplets `(i_k, j_k, v_k)`, `k = 0, …, K−1`.
17//!
18//! C1. [Validate.] Reject an index `i_k ≥ m` or `j_k ≥ n`, and a non-finite
19//!     `v_k`.
20//! C2. [Bucket by row.] Counting-sort the triplets by row, stably: row `i`
21//!     receives the pairs `(j_k, v_k)` in input order. This is a CSR array
22//!     with unsorted column indices and possible duplicates.
23//! C3. [Transpose.] Apply Algorithm T (Chapter 5.8) to the arrays of C2. In
24//!     the result every column lists its row indices in non-decreasing order,
25//!     and duplicates `(i, j)` are adjacent and still in input order.
26//! C4. [Sum and drop.] Sweep each column once, replacing each run of equal
27//!     row indices by the sum of its values (accumulated in input order) and
28//!     dropping sums that are exactly zero. Reject a sum that overflows to
29//!     `±∞`. Compact the arrays in place and rebuild `col_ptr`.
30//!
31//! The output satisfies (C1)–(C4) of Chapter 5.8. Because every step is a
32//! stable counting sort, the result — including the rounding of duplicate
33//! sums — depends only on the input, never on hashing or allocation order.
34//! Cost: `O(K + m + n)` time and space.
35//!
36//! Only *exact* zeros are dropped: deciding that `1e-17` is noise is a
37//! modelling decision taken by the solver's presolve with its tolerances
38//! (`xingo_core::Tolerances::zero`), not by storage.
39//!
40//! ## Invariants
41//! (C1)–(C4) of Chapter 5.8 with `ptr = col_ptr`, `idx = row_idx`,
42//! `n_major = n`, `n_minor = m`; checked by [`CscMatrix::check_invariants`]
43//! and asserted in debug builds after every construction.
44//!
45//! ## References
46//! Davis, *Direct Methods for Sparse Linear Systems* (2006), §2.2–2.5
47//! (`cs_compress`, `cs_dupl`); Gustavson (1978).
48
49use crate::compressed::{check_compressed, transpose};
50use crate::index::{Col, Row, dim_to_u32};
51use crate::real::{Real, is_exact_zero};
52use crate::{CsrMatrix, SparseError};
53
54/// Sparse matrix in compressed sparse column (CSC) format.
55///
56/// See the module documentation for the invariants. Equality is structural
57/// (same dimensions, same stored entries in the same order), which for
58/// matrices satisfying the invariants is equality of the matrices.
59#[derive(Clone, Debug, PartialEq)]
60pub struct CscMatrix<T: Real = f64> {
61    /// Number of rows `m`.
62    pub n_rows: usize,
63    /// Column pointers, length `n + 1`; column `j` occupies
64    /// `col_ptr[j]..col_ptr[j + 1]`.
65    pub col_ptr: Vec<usize>,
66    /// Row index of each stored entry, strictly increasing within a column.
67    pub row_idx: Vec<u32>,
68    /// Value of each stored entry; finite and nonzero.
69    pub val: Vec<T>,
70}
71
72impl<T: Real> CscMatrix<T> {
73    /// The `m × n` zero matrix (the contract's name; the same as
74    /// [`CscMatrix::zeros`]).
75    ///
76    /// # Panics
77    /// If `m` or `n` exceeds `u32::MAX`.
78    pub fn new(n_rows: usize, n_cols: usize) -> Self {
79        Self::zeros(n_rows, n_cols)
80    }
81
82    /// The `m × n` zero matrix.
83    ///
84    /// # Panics
85    /// If `m` or `n` exceeds `u32::MAX`.
86    pub fn zeros(n_rows: usize, n_cols: usize) -> Self {
87        assert!(
88            dim_to_u32("rows", n_rows).is_ok(),
89            "row count exceeds u32::MAX"
90        );
91        assert!(
92            dim_to_u32("columns", n_cols).is_ok(),
93            "column count exceeds u32::MAX"
94        );
95        Self {
96            n_rows,
97            col_ptr: vec![0; n_cols + 1],
98            row_idx: Vec::new(),
99            val: Vec::new(),
100        }
101    }
102
103    /// Builds a matrix from raw compressed arrays, validating every invariant.
104    pub fn from_raw_parts(
105        n_rows: usize,
106        col_ptr: Vec<usize>,
107        row_idx: Vec<u32>,
108        val: Vec<T>,
109    ) -> Result<Self, SparseError> {
110        let a = Self {
111            n_rows,
112            col_ptr,
113            row_idx,
114            val,
115        };
116        a.check_invariants()?;
117        Ok(a)
118    }
119
120    /// Builds an `m × n` matrix from triplets `(i, j, v)` by Algorithm C:
121    /// duplicates are summed in input order and exact zeros are dropped.
122    pub fn from_triplets(
123        n_rows: usize,
124        n_cols: usize,
125        triplets: &[(Row, Col, T)],
126    ) -> Result<Self, SparseError> {
127        dim_to_u32("rows", n_rows)?;
128        dim_to_u32("columns", n_cols)?;
129
130        // C1. Validate.
131        for (k, &(i, j, v)) in triplets.iter().enumerate() {
132            if i.index() >= n_rows {
133                return Err(SparseError::IndexOutOfRange {
134                    what: "row",
135                    index: i.index(),
136                    bound: n_rows,
137                });
138            }
139            if j.index() >= n_cols {
140                return Err(SparseError::IndexOutOfRange {
141                    what: "column",
142                    index: j.index(),
143                    bound: n_cols,
144                });
145            }
146            if !v.is_finite() {
147                return Err(SparseError::InvalidValue {
148                    what: "triplet value",
149                    index: k,
150                    value: v.to_f64(),
151                });
152            }
153        }
154
155        // C2. Stable counting sort by row: CSR with unsorted columns.
156        let mut rptr = vec![0_usize; n_rows + 1];
157        for &(i, _, _) in triplets {
158            rptr[i.index() + 1] += 1;
159        }
160        for i in 0..n_rows {
161            rptr[i + 1] += rptr[i];
162        }
163        let mut next = rptr[..n_rows].to_vec();
164        let mut cidx = vec![0_u32; triplets.len()];
165        let mut cval = vec![T::ZERO; triplets.len()];
166        for &(i, j, v) in triplets {
167            let q = next[i.index()];
168            next[i.index()] += 1;
169            cidx[q] = j.0;
170            cval[q] = v;
171        }
172
173        // C3. Transpose: rows non-decreasing within each column.
174        let (col_ptr, mut row_idx, mut val) = transpose(n_cols, &rptr, &cidx, &cval);
175
176        // C4. Sum runs of equal rows, drop exact zeros, compact in place.
177        let mut out_ptr = Vec::with_capacity(n_cols + 1);
178        out_ptr.push(0);
179        let mut w = 0;
180        for (j, win) in col_ptr.windows(2).enumerate() {
181            let (mut p, end) = (win[0], win[1]);
182            while p < end {
183                let i = row_idx[p];
184                let mut s = val[p];
185                p += 1;
186                while p < end && row_idx[p] == i {
187                    s += val[p];
188                    p += 1;
189                }
190                if !s.is_finite() {
191                    return Err(SparseError::InvalidValue {
192                        what: "sum of duplicate entries in column",
193                        index: j,
194                        value: s.to_f64(),
195                    });
196                }
197                if !is_exact_zero(s) {
198                    row_idx[w] = i;
199                    val[w] = s;
200                    w += 1;
201                }
202            }
203            out_ptr.push(w);
204        }
205        row_idx.truncate(w);
206        val.truncate(w);
207
208        let a = Self {
209            n_rows,
210            col_ptr: out_ptr,
211            row_idx,
212            val,
213        };
214        debug_assert!(a.check_invariants().is_ok(), "{:?}", a.check_invariants());
215        Ok(a)
216    }
217
218    /// Number of rows `m`.
219    #[inline]
220    pub fn n_rows(&self) -> usize {
221        self.n_rows
222    }
223
224    /// Number of columns `n`.
225    #[inline]
226    pub fn n_cols(&self) -> usize {
227        self.col_ptr.len() - 1
228    }
229
230    /// Number of stored entries.
231    #[inline]
232    pub fn nnz(&self) -> usize {
233        self.val.len()
234    }
235
236    /// Row indices and values of column `j`.
237    #[inline]
238    pub fn col(&self, j: Col) -> (&[u32], &[T]) {
239        let r = self.col_ptr[j.index()]..self.col_ptr[j.index() + 1];
240        (&self.row_idx[r.clone()], &self.val[r])
241    }
242
243    /// Entry `a_ij`, or `0` if not stored; `O(log nnz(column j))`.
244    pub fn get(&self, i: Row, j: Col) -> T {
245        let (rows, vals) = self.col(j);
246        rows.binary_search(&i.0).map_or(T::ZERO, |p| vals[p])
247    }
248
249    /// `y ← y + α A x` with dense `x` (length `n`) and `y` (length `m`),
250    /// column by column: `y_i ← y_i + a_ij (α x_j)` for the stored entries,
251    /// skipping the columns with `x_j = 0`.
252    ///
253    /// # Panics
254    /// If `x.len() != n` or `y.len() != m`.
255    pub fn gaxpy(&self, alpha: T, x: &[T], y: &mut [T]) {
256        assert_eq!(x.len(), self.n_cols(), "gaxpy: x has the wrong length");
257        assert_eq!(y.len(), self.n_rows, "gaxpy: y has the wrong length");
258        for (j, &xj) in x.iter().enumerate() {
259            if is_exact_zero(xj) {
260                continue;
261            }
262            let t = alpha * xj;
263            let (rows, vals) = self.col(Col::from_usize(j));
264            for (&i, &v) in rows.iter().zip(vals) {
265                y[i as usize] += v * t;
266            }
267        }
268    }
269
270    /// The same matrix in row-wise (CSR) storage, by Algorithm T.
271    pub fn to_csr(&self) -> CsrMatrix<T> {
272        let (row_ptr, col_idx, val) =
273            transpose(self.n_rows, &self.col_ptr, &self.row_idx, &self.val);
274        let r = CsrMatrix {
275            n_cols: self.n_cols(),
276            row_ptr,
277            col_idx,
278            val,
279        };
280        debug_assert!(r.check_invariants().is_ok(), "{:?}", r.check_invariants());
281        r
282    }
283
284    /// The transpose `Aᵀ` in CSC storage, by Algorithm T.
285    #[must_use]
286    pub fn transpose(&self) -> CscMatrix<T> {
287        let (col_ptr, row_idx, val) =
288            transpose(self.n_rows, &self.col_ptr, &self.row_idx, &self.val);
289        let t = CscMatrix {
290            n_rows: self.n_cols(),
291            col_ptr,
292            row_idx,
293            val,
294        };
295        debug_assert!(t.check_invariants().is_ok(), "{:?}", t.check_invariants());
296        t
297    }
298
299    /// Checks invariants (C1)–(C4).
300    pub fn check_invariants(&self) -> Result<(), SparseError> {
301        check_compressed(
302            "column",
303            "row",
304            self.n_rows,
305            &self.col_ptr,
306            &self.row_idx,
307            &self.val,
308        )
309    }
310}
311
312#[cfg(test)]
313mod tests {
314    use super::*;
315    use crate::real::approx_eq;
316    use proptest::prelude::*;
317
318    /// Dense reference: accumulate the triplets into an `m × n` array.
319    fn dense_reference(m: usize, n: usize, t: &[(Row, Col, f64)]) -> Vec<Vec<f64>> {
320        let mut d = vec![vec![0.0; n]; m];
321        for &(i, j, v) in t {
322            d[i.index()][j.index()] += v;
323        }
324        d
325    }
326
327    /// Strategy for `(m, n, triplets)`. Values are small integers so that
328    /// duplicate sums are exact regardless of summation order, and exact
329    /// cancellations (explicit zeros after summing) are frequent.
330    fn triplets() -> impl Strategy<Value = (usize, usize, Vec<(Row, Col, f64)>)> {
331        (0_usize..8, 0_usize..8).prop_flat_map(|(m, n)| {
332            let entries = if m == 0 || n == 0 {
333                Just(Vec::new()).boxed()
334            } else {
335                proptest::collection::vec(
336                    (0..m, 0..n, -3_i8..=3).prop_map(|(i, j, v)| {
337                        (Row::from_usize(i), Col::from_usize(j), f64::from(v))
338                    }),
339                    0..40,
340                )
341                .boxed()
342            };
343            (Just(m), Just(n), entries)
344        })
345    }
346
347    #[test]
348    fn ex_1_1_small_example() {
349        // A = [ 1 0 2 ]
350        //     [ 0 0 3 ]   with a duplicate (0,2) split as 0.5 + 1.5 and an
351        //                 explicit zero at (1,0).
352        let t = [
353            (Row(1), Col(2), 3.0),
354            (Row(0), Col(2), 0.5),
355            (Row(0), Col(0), 1.0),
356            (Row(1), Col(0), 0.0),
357            (Row(0), Col(2), 1.5),
358        ];
359        let a = CscMatrix::from_triplets(2, 3, &t).unwrap();
360        assert_eq!(a.col_ptr, vec![0, 1, 1, 3]);
361        assert_eq!(a.row_idx, vec![0, 0, 1]);
362        assert_eq!(a.val, vec![1.0, 2.0, 3.0]);
363        assert!(approx_eq(a.get(Row(0), Col(2)), 2.0, 0.0));
364        assert!(is_exact_zero(a.get(Row(1), Col(1))));
365    }
366
367    #[test]
368    fn rejects_bad_input() {
369        let oob = CscMatrix::from_triplets(1, 1, &[(Row(1), Col(0), 1.0)]);
370        assert!(matches!(
371            oob,
372            Err(SparseError::IndexOutOfRange { what: "row", .. })
373        ));
374        let nan = CscMatrix::from_triplets(1, 1, &[(Row(0), Col(0), f64::NAN)]);
375        assert!(matches!(nan, Err(SparseError::InvalidValue { .. })));
376        let ovf = CscMatrix::from_triplets(
377            1,
378            1,
379            &[(Row(0), Col(0), f64::MAX), (Row(0), Col(0), f64::MAX)],
380        );
381        assert!(matches!(ovf, Err(SparseError::InvalidValue { .. })));
382    }
383
384    #[test]
385    fn raw_parts_are_validated() {
386        assert!(CscMatrix::from_raw_parts(2, vec![0, 2], vec![0, 1], vec![1.0, 2.0]).is_ok());
387        // Unsorted rows.
388        assert!(CscMatrix::from_raw_parts(2, vec![0, 2], vec![1, 0], vec![1.0, 2.0]).is_err());
389        // Duplicate rows.
390        assert!(CscMatrix::from_raw_parts(2, vec![0, 2], vec![0, 0], vec![1.0, 2.0]).is_err());
391        // Explicit zero.
392        assert!(CscMatrix::from_raw_parts(2, vec![0, 1], vec![0], vec![0.0]).is_err());
393        // Row out of range.
394        assert!(CscMatrix::from_raw_parts(1, vec![0, 1], vec![1], vec![1.0]).is_err());
395        // Pointer not starting at zero / decreasing / wrong length.
396        assert!(CscMatrix::<f64>::from_raw_parts(2, vec![1, 1], vec![], vec![]).is_err());
397        assert!(CscMatrix::from_raw_parts(2, vec![0, 1, 0], vec![0], vec![1.0]).is_err());
398        assert!(CscMatrix::from_raw_parts(2, vec![0, 2], vec![0], vec![1.0]).is_err());
399        assert!(CscMatrix::<f64>::from_raw_parts(2, vec![], vec![], vec![]).is_err());
400    }
401
402    #[test]
403    fn gaxpy_matches_the_dense_product() {
404        // A = [[1, 0, 2], [0, 0, 3]]
405        let a = CscMatrix::from_raw_parts(2, vec![0, 1, 1, 3], vec![0, 0, 1], vec![1.0, 2.0, 3.0])
406            .unwrap();
407        let mut y = vec![1.0, -1.0];
408        a.gaxpy(2.0, &[1.0, 5.0, -1.0], &mut y);
409        assert!(approx_eq(y[0], 1.0 + 2.0 * (1.0 - 2.0), 0.0));
410        assert!(approx_eq(y[1], -1.0 + 2.0 * (-3.0), 0.0));
411        assert_eq!(CscMatrix::<f64>::new(2, 3), CscMatrix::zeros(2, 3));
412    }
413
414    #[test]
415    fn zeros_is_valid() {
416        let z = CscMatrix::<f64>::zeros(3, 4);
417        assert!(z.check_invariants().is_ok());
418        assert_eq!((z.n_rows(), z.n_cols(), z.nnz()), (3, 4, 0));
419        assert_eq!(z.to_csr().to_csc(), z);
420    }
421
422    proptest! {
423        /// CSC built from triplets equals the dense reference entry by entry,
424        /// stores no zeros and satisfies (C1)–(C4).
425        #[test]
426        fn triplets_match_dense_reference((m, n, t) in triplets()) {
427            let a = CscMatrix::from_triplets(m, n, &t).unwrap();
428            prop_assert!(a.check_invariants().is_ok());
429            let d = dense_reference(m, n, &t);
430            let mut nnz = 0;
431            for (i, row) in d.iter().enumerate() {
432                for (j, &dij) in row.iter().enumerate() {
433                    let aij = a.get(Row::from_usize(i), Col::from_usize(j));
434                    prop_assert!(approx_eq(aij, dij, 0.0));
435                    nnz += usize::from(!is_exact_zero(dij));
436                }
437            }
438            prop_assert_eq!(a.nnz(), nnz);
439        }
440
441        /// CSC → CSR → CSC is the identity, and the CSR holds the same entries.
442        #[test]
443        fn csc_csr_round_trip((m, n, t) in triplets()) {
444            let a = CscMatrix::from_triplets(m, n, &t).unwrap();
445            let r = a.to_csr();
446            prop_assert!(r.check_invariants().is_ok());
447            prop_assert_eq!((r.n_rows(), r.n_cols(), r.nnz()), (m, n, a.nnz()));
448            for i in 0..m {
449                for j in 0..n {
450                    let (i, j) = (Row::from_usize(i), Col::from_usize(j));
451                    prop_assert!(approx_eq(r.get(i, j), a.get(i, j), 0.0));
452                }
453            }
454            prop_assert_eq!(r.to_csc(), a.clone());
455            prop_assert_eq!(a.transpose().transpose(), a);
456        }
457    }
458}