Skip to main content

hurray_core/layout/addressing/
csc.rs

1//! CSC (Compressed Sparse Column) sparse layout element lookup.
2//!
3//! The column analog of CSR (`docs/spec/layouts/csc.md`): within a column, the stored row
4//! indices are strictly increasing, so a non-zero is found by binary-searching the
5//! column's slice of `row_indices`. Mirrors the standalone-function API validated by
6//! [`super::csf::element_offset`].
7
8use crate::{Error, Result};
9
10/// Looks up the storage offset (`values` / `row_indices` buffer index) of logical index
11/// `(row, col)` in a CSC matrix, or returns `None` if the element is a structural zero.
12///
13/// # Arguments
14///
15/// - `query` — logical index `[row, col]`; MUST have length 2 (CSC is rank-2).
16/// - `row_indices` — the `row_indices` buffer (buffer 1) as `uint64`: the row of each
17///   non-zero, in column-major storage order (`nnz` entries).
18/// - `col_ptr` — the `col_ptr` buffer (buffer 2) as `uint64`: `ncols + 1` entries, where
19///   `col_ptr[j]` is the first storage index of column `j` and `col_ptr[ncols] = nnz`.
20///
21/// # Errors
22///
23/// - [`Error::IndexRankMismatch`] — `query.len() != 2`.
24/// - [`Error::IndexOutOfRange`] — `col` is not a valid column (`col >= ncols`).
25/// - [`Error::InvalidLayout`] — `col_ptr` is empty, or a `col_ptr` entry points outside
26///   `row_indices` / is non-monotone for the queried column.
27///
28/// The caller is responsible for validating `row` against `shape[0]`; an out-of-range row
29/// simply reports a structural zero (`None`).
30///
31/// # Examples
32///
33/// ```
34/// use hurray_core::layout::addressing::csc::element_offset;
35///
36/// // 3×3 matrix, stored column-major:
37/// //   col 0: (0,0)
38/// //   col 1: (2,1)
39/// //   col 2: (0,2)
40/// let row_indices: &[u64] = &[0, 2, 0];
41/// let col_ptr: &[u64] = &[0, 1, 2, 3];
42///
43/// assert_eq!(element_offset(&[2, 1], row_indices, col_ptr).unwrap(), Some(1));
44/// assert_eq!(element_offset(&[0, 2], row_indices, col_ptr).unwrap(), Some(2));
45/// assert_eq!(element_offset(&[1, 1], row_indices, col_ptr).unwrap(), None); // structural zero
46/// ```
47pub fn element_offset(query: &[u64], row_indices: &[u64], col_ptr: &[u64]) -> Result<Option<u64>> {
48    if query.len() != 2 {
49        return Err(Error::IndexRankMismatch {
50            index_rank: query.len(),
51            shape_rank: 2,
52        });
53    }
54    let (row, col) = (query[0], query[1]);
55
56    if col_ptr.is_empty() {
57        return Err(Error::InvalidLayout(
58            "csc: col_ptr must have at least ncols+1 = 1 entries".into(),
59        ));
60    }
61    let ncols = (col_ptr.len() - 1) as u64;
62    if col >= ncols {
63        return Err(Error::IndexOutOfRange {
64            dim: 1,
65            index: col,
66            size: ncols,
67        });
68    }
69
70    let start = col_ptr[col as usize] as usize;
71    let end = col_ptr[col as usize + 1] as usize;
72    if start > end || end > row_indices.len() {
73        return Err(Error::InvalidLayout(format!(
74            "csc: col_ptr[{col}..{}] = [{start}..{end}] out of bounds for row_indices.len()={}",
75            col + 1,
76            row_indices.len()
77        )));
78    }
79
80    match row_indices[start..end].binary_search(&row) {
81        Ok(k) => Ok(Some((start + k) as u64)),
82        Err(_) => Ok(None),
83    }
84}
85
86#[cfg(test)]
87mod tests {
88    use super::*;
89
90    // 4×4 matrix, stored column-major:
91    //   col 0: (0,0)=a, (3,0)=b
92    //   col 1: (1,1)=c
93    //   col 2: (3,2)=d
94    //   col 3: (0,3)=e, (3,3)=f
95    // values order: a,b,c,d,e,f
96    const ROW_INDICES: &[u64] = &[0, 3, 1, 3, 0, 3];
97    const COL_PTR: &[u64] = &[0, 2, 3, 4, 6];
98
99    #[test]
100    fn lookup_hits() {
101        assert_eq!(
102            element_offset(&[0, 0], ROW_INDICES, COL_PTR).unwrap(),
103            Some(0)
104        );
105        assert_eq!(
106            element_offset(&[3, 0], ROW_INDICES, COL_PTR).unwrap(),
107            Some(1)
108        );
109        assert_eq!(
110            element_offset(&[1, 1], ROW_INDICES, COL_PTR).unwrap(),
111            Some(2)
112        );
113        assert_eq!(
114            element_offset(&[3, 2], ROW_INDICES, COL_PTR).unwrap(),
115            Some(3)
116        );
117        assert_eq!(
118            element_offset(&[0, 3], ROW_INDICES, COL_PTR).unwrap(),
119            Some(4)
120        );
121        assert_eq!(
122            element_offset(&[3, 3], ROW_INDICES, COL_PTR).unwrap(),
123            Some(5)
124        );
125    }
126
127    #[test]
128    fn structural_zeros() {
129        assert_eq!(element_offset(&[1, 0], ROW_INDICES, COL_PTR).unwrap(), None);
130        assert_eq!(element_offset(&[0, 1], ROW_INDICES, COL_PTR).unwrap(), None);
131        assert_eq!(element_offset(&[0, 2], ROW_INDICES, COL_PTR).unwrap(), None); // col 2 has only row 3
132        assert_eq!(element_offset(&[2, 3], ROW_INDICES, COL_PTR).unwrap(), None);
133    }
134
135    #[test]
136    fn wrong_rank_rejected() {
137        assert!(matches!(
138            element_offset(&[0], ROW_INDICES, COL_PTR),
139            Err(Error::IndexRankMismatch { .. })
140        ));
141    }
142
143    #[test]
144    fn col_out_of_range_rejected() {
145        assert!(matches!(
146            element_offset(&[0, 4], ROW_INDICES, COL_PTR),
147            Err(Error::IndexOutOfRange { dim: 1, .. })
148        ));
149    }
150
151    #[test]
152    fn empty_col_ptr_rejected() {
153        assert!(matches!(
154            element_offset(&[0, 0], ROW_INDICES, &[]),
155            Err(Error::InvalidLayout(_))
156        ));
157    }
158
159    #[test]
160    fn empty_matrix() {
161        assert_eq!(element_offset(&[0, 0], &[], &[0, 0, 0]).unwrap(), None);
162    }
163}