Skip to main content

hybit_matrix/
csr32.rs

1use hybit_core::{HybitError, LinearOperator};
2use rayon::prelude::*;
3
4#[derive(Clone, Debug)]
5pub struct Csr32Matrix {
6    nrows: usize,
7    ncols: usize,
8    row_ptr: Vec<u32>,
9    col_idx: Vec<u32>,
10    values: Vec<f64>,
11}
12
13impl Csr32Matrix {
14    pub fn new(
15        nrows: usize,
16        ncols: usize,
17        row_ptr: Vec<u32>,
18        col_idx: Vec<u32>,
19        values: Vec<f64>,
20    ) -> Result<Self, HybitError> {
21        let matrix = Self {
22            nrows,
23            ncols,
24            row_ptr,
25            col_idx,
26            values,
27        };
28        matrix.validate()?;
29        Ok(matrix)
30    }
31
32    pub fn validate(&self) -> Result<(), HybitError> {
33        if self.row_ptr.len() != self.nrows + 1 {
34            return Err(HybitError::InvalidMatrix(
35                "row_ptr length must equal nrows + 1",
36            ));
37        }
38        if self.col_idx.len() != self.values.len() {
39            return Err(HybitError::InvalidMatrix(
40                "col_idx and values lengths differ",
41            ));
42        }
43        if self.row_ptr.first().copied().unwrap_or(1) != 0 {
44            return Err(HybitError::InvalidMatrix("row_ptr[0] must be zero"));
45        }
46        if self.values.len() > u32::MAX as usize {
47            return Err(HybitError::SizeOverflow);
48        }
49        let nnz = self.values.len() as u32;
50        if self.row_ptr.last().copied().unwrap_or(0) != nnz {
51            return Err(HybitError::InvalidMatrix("row_ptr[nrows] must equal nnz"));
52        }
53        for pair in self.row_ptr.windows(2) {
54            if pair[0] > pair[1] {
55                return Err(HybitError::InvalidMatrix(
56                    "row_ptr must be monotonically nondecreasing",
57                ));
58            }
59        }
60        if self.col_idx.iter().any(|&c| c as usize >= self.ncols) {
61            return Err(HybitError::InvalidMatrix("column index out of range"));
62        }
63        if self.values.iter().any(|v| !v.is_finite()) {
64            return Err(HybitError::InvalidMatrix("matrix contains NaN or infinity"));
65        }
66        Ok(())
67    }
68
69    pub fn nrows(&self) -> usize {
70        self.nrows
71    }
72    pub fn ncols(&self) -> usize {
73        self.ncols
74    }
75    pub fn nnz(&self) -> usize {
76        self.values.len()
77    }
78    pub fn row_ptr(&self) -> &[u32] {
79        &self.row_ptr
80    }
81    pub fn col_idx(&self) -> &[u32] {
82        &self.col_idx
83    }
84    pub fn values(&self) -> &[f64] {
85        &self.values
86    }
87
88    pub fn storage_bytes(&self) -> usize {
89        self.row_ptr.len() * std::mem::size_of::<u32>()
90            + self.col_idx.len() * std::mem::size_of::<u32>()
91            + self.values.len() * std::mem::size_of::<f64>()
92    }
93
94    pub fn metadata_bytes(&self) -> usize {
95        self.row_ptr.len() * std::mem::size_of::<u32>()
96            + self.col_idx.len() * std::mem::size_of::<u32>()
97    }
98
99    pub fn diagonal(&self) -> Result<Vec<f64>, HybitError> {
100        if self.nrows != self.ncols {
101            return Err(HybitError::InvalidMatrix(
102                "diagonal requires a square matrix",
103            ));
104        }
105        let mut diagonal = vec![0.0; self.nrows];
106        let mut found = vec![false; self.nrows];
107        for row in 0..self.nrows {
108            let start = self.row_ptr[row] as usize;
109            let end = self.row_ptr[row + 1] as usize;
110            for p in start..end {
111                if self.col_idx[p] as usize == row {
112                    diagonal[row] += self.values[p];
113                    found[row] = true;
114                }
115            }
116        }
117        for (row, &was_found) in found.iter().enumerate() {
118            if !was_found {
119                return Err(HybitError::MissingDiagonal { row });
120            }
121        }
122        Ok(diagonal)
123    }
124
125    pub fn spmv(&self, x: &[f64]) -> Result<Vec<f64>, HybitError> {
126        let mut y = vec![0.0; self.nrows];
127        self.apply(x, &mut y)?;
128        Ok(y)
129    }
130}
131
132impl Csr32Matrix {
133    #[inline(always)]
134    unsafe fn dot_row_unchecked(&self, row: usize, x: &[f64]) -> f64 {
135        // SAFETY contract: Csr32Matrix::new validates row_ptr monotonicity,
136        // row_ptr[nrows] == nnz, col_idx.len() == values.len(), and every
137        // column index is < ncols. LinearOperator::apply validates x length.
138        let start = unsafe { *self.row_ptr.get_unchecked(row) as usize };
139        let end = unsafe { *self.row_ptr.get_unchecked(row + 1) as usize };
140        let mut sum = 0.0;
141        for p in start..end {
142            let value = unsafe { *self.values.get_unchecked(p) };
143            let col = unsafe { *self.col_idx.get_unchecked(p) as usize };
144            let xv = unsafe { *x.get_unchecked(col) };
145            sum += value * xv;
146        }
147        sum
148    }
149
150    /// Apply CSR SpMV using Rayon over independent matrix rows.
151    ///
152    /// This is an explicit opt-in experimental path in HyBIT 0.6.0. The
153    /// ordinary `LinearOperator` implementation remains serial so existing
154    /// callers retain their execution policy. The matrix is immutable after
155    /// construction and its CSR indices are validated, allowing the hot inner
156    /// loop to omit repeated bounds checks safely.
157    pub fn apply_parallel(&self, x: &[f64], y: &mut [f64]) -> Result<(), HybitError> {
158        if x.len() != self.ncols {
159            return Err(HybitError::DimensionMismatch {
160                expected: self.ncols,
161                actual: x.len(),
162            });
163        }
164        if y.len() != self.nrows {
165            return Err(HybitError::DimensionMismatch {
166                expected: self.nrows,
167                actual: y.len(),
168            });
169        }
170        y.par_iter_mut().enumerate().for_each(|(row, out)| {
171            // SAFETY: dimensions are checked above and all CSR indices were
172            // validated by Csr32Matrix::new. Each Rayon worker owns a distinct
173            // output element and reads only immutable matrix/x storage.
174            *out = unsafe { self.dot_row_unchecked(row, x) };
175        });
176        Ok(())
177    }
178}
179
180/// Read-only parallel CSR operator wrapper. It shares the validated CSR
181/// storage and only changes the row execution policy used by `apply`.
182#[derive(Clone, Copy, Debug)]
183pub struct ParallelCsr32Operator<'a> {
184    matrix: &'a Csr32Matrix,
185}
186
187impl<'a> ParallelCsr32Operator<'a> {
188    pub fn new(matrix: &'a Csr32Matrix) -> Self {
189        Self { matrix }
190    }
191    pub fn matrix(&self) -> &'a Csr32Matrix {
192        self.matrix
193    }
194    pub fn rayon_threads(&self) -> usize {
195        rayon::current_num_threads()
196    }
197}
198
199impl LinearOperator for ParallelCsr32Operator<'_> {
200    fn rows(&self) -> usize {
201        self.matrix.nrows
202    }
203    fn cols(&self) -> usize {
204        self.matrix.ncols
205    }
206
207    fn apply(&self, x: &[f64], y: &mut [f64]) -> Result<(), HybitError> {
208        self.matrix.apply_parallel(x, y)
209    }
210}
211
212impl LinearOperator for Csr32Matrix {
213    fn rows(&self) -> usize {
214        self.nrows
215    }
216    fn cols(&self) -> usize {
217        self.ncols
218    }
219
220    fn apply(&self, x: &[f64], y: &mut [f64]) -> Result<(), HybitError> {
221        if x.len() != self.ncols {
222            return Err(HybitError::DimensionMismatch {
223                expected: self.ncols,
224                actual: x.len(),
225            });
226        }
227        if y.len() != self.nrows {
228            return Err(HybitError::DimensionMismatch {
229                expected: self.nrows,
230                actual: y.len(),
231            });
232        }
233        for (row, out) in y.iter_mut().enumerate() {
234            // SAFETY: dimensions are checked above and all CSR indices were
235            // validated by Csr32Matrix::new.
236            *out = unsafe { self.dot_row_unchecked(row, x) };
237        }
238        Ok(())
239    }
240}
241
242#[cfg(test)]
243mod tests {
244    use super::*;
245
246    #[test]
247    fn csr_spmv() {
248        let a = Csr32Matrix::new(
249            3,
250            3,
251            vec![0, 2, 5, 7],
252            vec![0, 1, 0, 1, 2, 1, 2],
253            vec![2.0, -1.0, -1.0, 2.0, -1.0, -1.0, 2.0],
254        )
255        .unwrap();
256        let y = a.spmv(&[1.0, 2.0, 3.0]).unwrap();
257        assert_eq!(y, vec![0.0, 0.0, 4.0]);
258    }
259
260    #[test]
261    fn parallel_csr_matches_serial() {
262        let a = Csr32Matrix::new(
263            3,
264            3,
265            vec![0, 2, 5, 7],
266            vec![0, 1, 0, 1, 2, 1, 2],
267            vec![2.0, -1.0, -1.0, 2.0, -1.0, -1.0, 2.0],
268        )
269        .unwrap();
270        let x = [1.0, 2.0, 3.0];
271        let serial = a.spmv(&x).unwrap();
272        let mut parallel = vec![0.0; 3];
273        a.apply_parallel(&x, &mut parallel).unwrap();
274        assert_eq!(parallel, serial);
275    }
276}