Skip to main content

data_beans/
sparse_util.rs

1use clap::ValueEnum;
2
3pub struct ValuesIndicesPointers<'a> {
4    pub values: &'a [f32],
5    pub indices: &'a [u64],
6    pub indptr: &'a [u64],
7}
8
9pub struct CooTripletsShape {
10    pub triplets: Vec<(u64, u64, f32)>,
11    pub shape: TripletsShape,
12}
13
14pub struct TripletsShape {
15    pub nrows: usize,
16    pub ncols: usize,
17    pub nnz: usize,
18}
19
20#[derive(ValueEnum, Clone, Copy, Debug, PartialEq)]
21#[clap(rename_all = "lowercase")]
22pub enum IndexPointerType {
23    Column,
24    Row,
25}
26
27pub trait SparseTripletsTraits {
28    /// convert sparse pointers into (row, column, value) triplets
29    fn to_coo(&self, pointer_type: IndexPointerType) -> anyhow::Result<CooTripletsShape>;
30}
31
32/////////////////////
33// implementations //
34/////////////////////
35
36impl<'a> SparseTripletsTraits for ValuesIndicesPointers<'a> {
37    fn to_coo(&self, pointer_type: IndexPointerType) -> anyhow::Result<CooTripletsShape> {
38        use rayon::prelude::*;
39
40        let indices = self.indices;
41        let indptr = self.indptr;
42        let values = self.values;
43
44        let nelem = values.len();
45        if nelem != indices.len() {
46            return Err(anyhow::anyhow!(
47                "`values` and `indices` have different sizes"
48            ));
49        }
50        if indptr.is_empty() {
51            return Err(anyhow::anyhow!("`indptr` is empty"));
52        }
53        let nvectors = indptr.len() - 1;
54
55        // Each compressed vector's entries, clamped to the arrays.
56        let spans: Vec<(usize, usize)> = indptr
57            .windows(2)
58            .map(|w| {
59                let end = (w[1] as usize).min(nelem);
60                ((w[0] as usize).min(end), end)
61            })
62            .collect();
63
64        // The spans say where every triplet lands, so the output is allocated
65        // once and carved into one slot per vector; workers fill the slots in
66        // place, with no per-task buffers to stitch together afterwards.
67        let total: usize = spans.iter().map(|&(start, end)| end - start).sum();
68        let mut triplets: Vec<(u64, u64, f32)> = vec![(0, 0, 0.0); total];
69        let mut slots: Vec<&mut [(u64, u64, f32)]> = Vec::with_capacity(nvectors);
70        let mut rest = triplets.as_mut_slice();
71        for &(start, end) in &spans {
72            let (slot, tail) = rest.split_at_mut(end - start);
73            slots.push(slot);
74            rest = tail;
75        }
76        slots
77            .into_par_iter()
78            .zip(spans.par_iter())
79            .enumerate()
80            .for_each(|(idx, (slot, &(start, end)))| {
81                let j = idx as u64;
82                let entries = indices[start..end].iter().zip(&values[start..end]);
83                for (out, (&i, &x_ij)) in slot.iter_mut().zip(entries) {
84                    *out = match pointer_type {
85                        IndexPointerType::Column => (i, j, x_ij),
86                        IndexPointerType::Row => (j, i, x_ij),
87                    };
88                }
89            });
90
91        let nnz = triplets.len();
92
93        // Shape is derivable from the compressed layout directly — no need to
94        // scan the whole triplets Vec again.
95        let max_idx_index = indices.par_iter().copied().max().unwrap_or(0) as usize + 1;
96        let (nrows, ncols) = match pointer_type {
97            IndexPointerType::Column => (max_idx_index, nvectors),
98            IndexPointerType::Row => (nvectors, max_idx_index),
99        };
100
101        Ok(CooTripletsShape {
102            triplets,
103            shape: TripletsShape { nrows, ncols, nnz },
104        })
105    }
106}
107
108/// Sort `triplets` by (column, row), the order the backend writer sorts by
109/// first, and sum the values of the entries that share both, leaving one
110/// entry per coordinate. The sort is stable, so a coordinate's values are
111/// summed in the order they arrived, as the dense readers accumulate them:
112/// float sums depend on the order.
113pub(crate) fn sum_duplicate_triplets(triplets: &mut Vec<(u64, u64, f32)>) {
114    use rayon::prelude::*;
115    triplets.par_sort_by_key(|&(r, c, _)| (c, r));
116    triplets.dedup_by(|later, kept| {
117        let same = (later.0, later.1) == (kept.0, kept.1);
118        if same {
119            kept.2 += later.2;
120        }
121        same
122    });
123}
124
125#[cfg(test)]
126mod tests {
127    use super::*;
128
129    /// Three values on each coordinate whose sum depends on the order:
130    /// `(1e8 + 1) - 1e8` is 0 in `f32`, `(1e8 - 1e8) + 1` is 1. Enough
131    /// coordinates that a sort has to move entries past their equals.
132    #[test]
133    fn values_on_one_coordinate_are_summed_in_the_order_they_arrived() {
134        let n = 5_000u64;
135        let mut triplets: Vec<(u64, u64, f32)> = Vec::new();
136        for value in [1e8_f32, 1.0, -1e8] {
137            // Descending, so sorting reverses the coordinates.
138            triplets.extend((0..n).rev().map(|k| (k % 50, k / 50, value)));
139        }
140        sum_duplicate_triplets(&mut triplets);
141        assert_eq!(triplets.len(), n as usize);
142        assert!(triplets
143            .windows(2)
144            .all(|w| (w[0].1, w[0].0) < (w[1].1, w[1].0)));
145        assert!(
146            triplets.iter().all(|&(_, _, x)| x == 0.0),
147            "summed out of arrival order"
148        );
149    }
150
151    /// A coordinate listed once keeps its value; one listed several times
152    /// holds their sum; the result is ordered by column, then row.
153    #[test]
154    fn duplicates_are_summed_and_singletons_kept() {
155        let (a, b, c) = (0.1_f32, 0.2, 0.3);
156        let mut triplets = vec![(1, 0, a), (0, 1, 2.0), (1, 0, b), (0, 0, 5.0), (1, 0, c)];
157        sum_duplicate_triplets(&mut triplets);
158        assert_eq!(triplets, [(0, 0, 5.0), (1, 0, (a + b) + c), (0, 1, 2.0)]);
159    }
160
161    /// The triplets a sequential walk of the compressed vectors gives.
162    fn walk(
163        values: &[f32],
164        indices: &[u64],
165        indptr: &[u64],
166        pointer_type: IndexPointerType,
167    ) -> Vec<(u64, u64, f32)> {
168        let mut out = Vec::new();
169        for (j, w) in indptr.windows(2).enumerate() {
170            let end = (w[1] as usize).min(values.len());
171            for k in (w[0] as usize).min(end)..end {
172                out.push(match pointer_type {
173                    IndexPointerType::Column => (indices[k], j as u64, values[k]),
174                    IndexPointerType::Row => (j as u64, indices[k], values[k]),
175                });
176            }
177        }
178        out
179    }
180
181    #[test]
182    fn to_coo_keeps_the_order_of_the_compressed_vectors() {
183        // 5,000 vectors of 0..=6 entries, some empty.
184        let mut indptr = vec![0u64];
185        let mut indices = Vec::new();
186        let mut values = Vec::new();
187        for j in 0..5_000u64 {
188            for k in 0..(j * 7 + 3) % 7 {
189                indices.push((j + 11 * k) % 97);
190                values.push((j * 31 + k) as f32);
191            }
192            indptr.push(indices.len() as u64);
193        }
194        for pointer_type in [IndexPointerType::Column, IndexPointerType::Row] {
195            let coo = ValuesIndicesPointers {
196                values: &values,
197                indices: &indices,
198                indptr: &indptr,
199            }
200            .to_coo(pointer_type)
201            .unwrap();
202            assert_eq!(coo.triplets, walk(&values, &indices, &indptr, pointer_type));
203            assert_eq!(coo.shape.nnz, values.len());
204            let (major, minor) = match pointer_type {
205                IndexPointerType::Column => (coo.shape.ncols, coo.shape.nrows),
206                IndexPointerType::Row => (coo.shape.nrows, coo.shape.ncols),
207            };
208            assert_eq!((major, minor), (5_000, 97));
209        }
210    }
211
212    #[test]
213    fn to_coo_clamps_pointers_past_the_arrays() {
214        let values = [1.0f32, 2.0, 3.0];
215        let indices = [0u64, 1, 2];
216        // The last vector claims two entries the arrays do not hold.
217        let indptr = [0u64, 2, 5];
218        let coo = ValuesIndicesPointers {
219            values: &values,
220            indices: &indices,
221            indptr: &indptr,
222        }
223        .to_coo(IndexPointerType::Column)
224        .unwrap();
225        assert_eq!(coo.triplets, vec![(0, 0, 1.0), (1, 0, 2.0), (2, 1, 3.0)]);
226    }
227}