data_beans/sparse_io_vector/
scales.rs1use super::*;
17
18#[derive(Clone, Debug)]
20pub enum RowFilter {
21 Mask(Vec<bool>),
23 Names(regex::Regex),
25 Broad { share: f64, groups: Vec<Box<str>> },
29}
30
31#[derive(Clone, Debug)]
33pub struct ColumnScales {
34 pub factor: Vec<f32>,
38 pub ignored_rows: Vec<bool>,
40}
41
42impl SparseIoVec {
43 pub fn depth_scales(
49 &self,
50 ignore: &[RowFilter],
51 block_size: Option<usize>,
52 ) -> anyhow::Result<ColumnScales> {
53 anyhow::ensure!(
54 self.column_scales.is_none(),
55 "depth_scales on a scaled vector: read the depths before set_column_scales"
56 );
57 let (n_rows, n_cols) = (self.num_rows(), self.num_columns());
58 let blocks = generate_minibatch_intervals(n_cols, n_rows, block_size);
59
60 let mut ignored = vec![false; n_rows];
61 for filter in ignore {
62 match filter {
63 RowFilter::Mask(mask) => {
64 anyhow::ensure!(
65 mask.len() == n_rows,
66 "row mask has {} flags for {n_rows} rows",
67 mask.len()
68 );
69 ignored.iter_mut().zip(mask).for_each(|(i, &o)| *i |= o);
70 }
71 RowFilter::Names(re) => {
72 let names = self.row_names()?;
73 ignored
74 .iter_mut()
75 .zip(&names)
76 .for_each(|(i, n)| *i |= re.is_match(n));
77 }
78 RowFilter::Broad { share, groups } => {
79 let broad = self.broad_rows(*share, groups, &blocks)?;
80 ignored.iter_mut().zip(broad).for_each(|(i, o)| *i |= o);
81 }
82 }
83 }
84
85 let kept: Vec<usize> = (0..n_rows).filter(|&r| !ignored[r]).collect();
86 let (_, depth, _, _) =
87 crate::qc::collect_column_stat_across_vec(self, Some(&kept), block_size)?.to_f32_vecs();
88
89 Ok(ColumnScales {
90 factor: factors(&depth),
91 ignored_rows: ignored,
92 })
93 }
94
95 fn broad_rows(
97 &self,
98 share: f64,
99 groups: &[Box<str>],
100 blocks: &[(usize, usize)],
101 ) -> anyhow::Result<Vec<bool>> {
102 let (n_rows, n_cols) = (self.num_rows(), self.num_columns());
103 anyhow::ensure!(
104 share.is_finite() && (0.0..1.0).contains(&share),
105 "broad share {share} is not in [0, 1)"
106 );
107 anyhow::ensure!(
108 groups.len() == n_cols,
109 "{} group labels for {n_cols} columns",
110 groups.len()
111 );
112 let mut index: HashMap<&str, usize> = HashMap::default();
113 let g: Vec<usize> = groups
114 .iter()
115 .map(|l| {
116 let next = index.len();
117 *index.entry(l).or_insert(next)
118 })
119 .collect();
120 let n_groups = index.len();
121 let mut size = vec![0usize; n_groups];
122 g.iter().for_each(|&k| size[k] += 1);
123
124 let zero = || vec![0u32; n_groups * n_rows];
126 let detected = blocks
127 .par_iter()
128 .try_fold(zero, |mut d, &(lb, ub)| -> anyhow::Result<Vec<u32>> {
129 let x = self.read_columns_csc(lb..ub)?;
130 for (k, col) in x.col_iter().enumerate() {
131 let dk = &mut d[g[lb + k] * n_rows..][..n_rows];
132 for (&r, &v) in col.row_indices().iter().zip(col.values()) {
133 if v > 0.0 {
134 dk[r] += 1;
135 }
136 }
137 }
138 Ok(d)
139 })
140 .try_reduce(zero, |mut a, b| {
141 a.iter_mut().zip(b).for_each(|(a, b)| *a += b);
142 Ok(a)
143 })?;
144
145 Ok((0..n_rows)
146 .map(|r| {
147 (0..n_groups).all(|k| detected[k * n_rows + r] as f64 > share * size[k] as f64)
148 })
149 .collect())
150 }
151
152 pub fn set_column_scales(&mut self, factor: Option<Vec<f32>>) -> anyhow::Result<()> {
160 if let Some(f) = &factor {
161 anyhow::ensure!(
162 f.len() == self.num_columns(),
163 "{} column scales for {} columns",
164 f.len(),
165 self.num_columns()
166 );
167 if let Some((c, v)) = f
168 .iter()
169 .enumerate()
170 .find(|(_, v)| !(v.is_finite() && **v > 0.0))
171 {
172 anyhow::bail!("column {c} has scale {v}; scales must be positive and finite");
173 }
174 }
175 self.column_scales = factor;
176 Ok(())
177 }
178
179 pub fn column_scales(&self) -> Option<&[f32]> {
181 self.column_scales.as_deref()
182 }
183}
184
185fn factors(depth: &[f32]) -> Vec<f32> {
188 let positive: Vec<f32> = depth.iter().copied().filter(|&d| d > 0.0).collect();
189 if positive.is_empty() {
190 return vec![1.0; depth.len()];
191 }
192 let median = median(&positive);
193 depth
194 .iter()
195 .map(|&d| if d > 0.0 { d / median } else { 1.0 })
196 .collect()
197}
198
199#[cfg(test)]
200mod tests {
201 use super::*;
202 use crate::sparse_io::{create_sparse_from_triplets, SparseIoBackend};
203
204 #[test]
206 fn a_matched_column_read_divides_by_its_factor() {
207 let dir = tempfile::tempdir().unwrap();
208 let path = dir.path().join("a.zarr");
209 let t = [(0, 0, 4.0), (1, 1, 6.0), (0, 2, 9.0)];
210 let mut b = create_sparse_from_triplets(
211 &t,
212 (2, 3, t.len()),
213 Some(path.to_str().unwrap()),
214 Some(&SparseIoBackend::Zarr),
215 )
216 .unwrap();
217 b.register_row_names_vec(&["R0".into(), "R1".into()]);
218 b.register_column_names_vec(&["c0".into(), "c1".into(), "c2".into()]);
219 let mut data = SparseIoVec::new();
220 data.push(Arc::from(b), None).unwrap();
221 data.set_column_scales(Some(vec![2.0, 3.0, 9.0])).unwrap();
222 let matched = data
223 .read_matched(vec![0, 0], vec![2, 1], vec![0.0, 0.0])
224 .unwrap();
225 assert_eq!(matched.shape, (2, 2));
226 let mut out = matched.triplets;
227 out.sort_by_key(|&(r, c, _)| (c, r));
228 assert_eq!(out, [(0, 0, 1.0), (1, 1, 2.0)]);
229 }
230}