data_beans/aux/
cell_annotations.rs1use rustc_hash::FxHashMap as HashMap;
2
3use anyhow::{bail, Result};
4use log::info;
5use nalgebra::DMatrix;
6
7use legume_numeric::matrix::common_io::read_lines_of_words_delim;
8use legume_numeric::matrix::traits::IoOps;
9
10#[derive(Debug, Clone)]
12pub struct CellAnnotations {
13 pub cell_to_individual: HashMap<Box<str>, usize>,
15 pub individual_ids: Vec<Box<str>>,
17}
18
19impl CellAnnotations {
20 pub fn to_column_aligned_vec(&self, column_names: &[Box<str>], missing: &str) -> Vec<Box<str>> {
25 column_names
26 .iter()
27 .map(|name| {
28 self.cell_to_individual
29 .get(name)
30 .map(|&idx| self.individual_ids[idx].clone())
31 .unwrap_or_else(|| Box::from(missing))
32 })
33 .collect()
34 }
35}
36
37pub struct CellTypeMembership {
42 pub matrix: DMatrix<f32>,
44 pub cell_type_names: Vec<Box<str>>,
46}
47
48pub fn read_cell_annotations(path: &str) -> Result<CellAnnotations> {
57 info!("Reading cell annotations from {}", path);
58
59 let parsed = read_lines_of_words_delim(path, &['\t', ',', ' '], 0)?;
60
61 let mut individual_to_idx: HashMap<Box<str>, usize> = Default::default();
62 let mut individual_ids: Vec<Box<str>> = Vec::new();
63 let mut cell_to_individual: HashMap<Box<str>, usize> = Default::default();
64
65 for words in &parsed.lines {
66 if words.len() < 2 {
67 continue;
68 }
69
70 let cell_id = words[0].clone();
71 let ind_id = words[1].clone();
72
73 let ind_idx = *individual_to_idx.entry(ind_id.clone()).or_insert_with(|| {
74 let idx = individual_ids.len();
75 individual_ids.push(ind_id);
76 idx
77 });
78
79 cell_to_individual.insert(cell_id, ind_idx);
80 }
81
82 info!(
83 "Loaded {} cell annotations: {} individuals",
84 cell_to_individual.len(),
85 individual_ids.len(),
86 );
87
88 Ok(CellAnnotations {
89 cell_to_individual,
90 individual_ids,
91 })
92}
93
94pub fn infer_cell_annotations(column_names: &[Box<str>]) -> CellAnnotations {
99 info!("Inferring individuals from cell names (barcode@indiv)");
100
101 let mut individual_to_idx: HashMap<Box<str>, usize> = Default::default();
102 let mut individual_ids: Vec<Box<str>> = Vec::new();
103 let mut cell_to_individual: HashMap<Box<str>, usize> = Default::default();
104
105 for cell_name in column_names {
106 let indiv: Box<str> = if let Some(pos) = cell_name.rfind('@') {
107 Box::from(&cell_name[pos + 1..])
108 } else {
109 Box::from("all")
110 };
111 let idx = *individual_to_idx.entry(indiv.clone()).or_insert_with(|| {
112 let i = individual_ids.len();
113 individual_ids.push(indiv);
114 i
115 });
116 cell_to_individual.insert(cell_name.clone(), idx);
117 }
118
119 info!(
120 "Inferred {} individuals from cell names",
121 individual_ids.len()
122 );
123
124 CellAnnotations {
125 cell_to_individual,
126 individual_ids,
127 }
128}
129
130pub fn build_onehot_membership(
135 path: &str,
136 column_names: &[Box<str>],
137) -> Result<CellTypeMembership> {
138 info!("Building one-hot membership from {}", path);
139
140 let parsed = read_lines_of_words_delim(path, &['\t', ',', ' '], 0)?;
141
142 let mut celltype_to_idx: HashMap<Box<str>, usize> = Default::default();
143 let mut cell_type_names: Vec<Box<str>> = Vec::new();
144 let mut cell_to_ct: HashMap<Box<str>, usize> = Default::default();
145
146 for words in &parsed.lines {
147 if words.len() < 3 {
148 continue;
149 }
150
151 let cell_id = words[0].clone();
152 let ct_name = words[2].clone();
153
154 let ct_idx = *celltype_to_idx.entry(ct_name.clone()).or_insert_with(|| {
155 let idx = cell_type_names.len();
156 cell_type_names.push(ct_name);
157 idx
158 });
159
160 cell_to_ct.insert(cell_id, ct_idx);
161 }
162
163 let n_cells = column_names.len();
164 let n_ct = cell_type_names.len();
165 let mut matrix = DMatrix::<f32>::zeros(n_cells, n_ct);
166 let mut matched = 0usize;
167
168 for (cell_idx, cell_name) in column_names.iter().enumerate() {
169 if let Some(&ct_idx) = cell_to_ct.get(cell_name) {
170 matrix[(cell_idx, ct_idx)] = 1.0;
171 matched += 1;
172 }
173 }
174
175 info!(
176 "One-hot membership: {}/{} cells matched, {} cell types",
177 matched, n_cells, n_ct
178 );
179
180 if matched == 0 {
181 bail!("No cells matched between SC backend and annotation file");
182 }
183
184 Ok(CellTypeMembership {
185 matrix,
186 cell_type_names,
187 })
188}
189
190pub fn read_membership_proportions(
196 file_path: &str,
197 column_names: &[Box<str>],
198) -> Result<CellTypeMembership> {
199 info!("Reading membership proportions from {}", file_path);
200
201 let mat_with_names = DMatrix::<f32>::from_parquet(file_path)?;
202 let cell_type_names = mat_with_names.cols;
203 let n_cell_types = cell_type_names.len();
204 let n_cells = column_names.len();
205
206 let parquet_cell_lookup: HashMap<&str, usize> = mat_with_names
208 .rows
209 .iter()
210 .enumerate()
211 .map(|(i, name)| (name.as_ref(), i))
212 .collect();
213
214 let mut matrix = DMatrix::<f32>::zeros(n_cells, n_cell_types);
216 let mut matched = 0usize;
217
218 for (cell_idx, cell_name) in column_names.iter().enumerate() {
219 if let Some(&pq_row) = parquet_cell_lookup.get(cell_name.as_ref()) {
220 for k in 0..n_cell_types {
221 matrix[(cell_idx, k)] = mat_with_names.mat[(pq_row, k)];
222 }
223 matched += 1;
224 }
225 }
226
227 info!(
228 "Matched {}/{} cells to membership parquet ({} cell types)",
229 matched, n_cells, n_cell_types
230 );
231
232 if matched == 0 {
233 bail!("No cells matched between SC backend and membership parquet");
234 }
235
236 Ok(CellTypeMembership {
237 matrix,
238 cell_type_names,
239 })
240}
241
242#[cfg(test)]
243mod tests {
244 use super::*;
245
246 #[test]
247 fn test_read_cell_annotations_roundtrip() -> Result<()> {
248 use std::io::Write;
249
250 let dir = tempfile::tempdir()?;
251 let path = dir.path().join("cells.tsv");
252 let path_str = path.to_str().unwrap();
253
254 let mut f = std::fs::File::create(&path)?;
255 writeln!(f, "cell_id\tindividual_id\tcell_type")?;
256 writeln!(f, "cell_0\tIND_A\tT_cell")?;
257 writeln!(f, "cell_1\tIND_A\tB_cell")?;
258 writeln!(f, "cell_2\tIND_B\tT_cell")?;
259 writeln!(f, "cell_3\tIND_B\tT_cell")?;
260 f.flush()?;
261
262 let anno = read_cell_annotations(path_str)?;
263 assert_eq!(anno.individual_ids.len(), 2);
264 assert_eq!(anno.cell_to_individual.len(), 4);
265
266 let &ind_idx = anno.cell_to_individual.get(&Box::from("cell_0")).unwrap();
267 assert_eq!(anno.individual_ids[ind_idx].as_ref(), "IND_A");
268
269 Ok(())
270 }
271
272 #[test]
273 fn test_infer_cell_annotations() {
274 let names: Vec<Box<str>> = vec![
275 "ACGT@IND_A".into(),
276 "TGCA@IND_A".into(),
277 "GGCC@IND_B".into(),
278 ];
279 let anno = infer_cell_annotations(&names);
280 assert_eq!(anno.individual_ids.len(), 2);
281 assert_eq!(anno.cell_to_individual.len(), 3);
282 }
283
284 #[test]
285 fn test_to_column_aligned_vec() {
286 let names: Vec<Box<str>> = vec!["ACGT@IND_A".into(), "TGCA@IND_B".into(), "XXXX".into()];
287 let anno = infer_cell_annotations(&names);
288 let aligned = anno.to_column_aligned_vec(&names, "missing");
289 assert_eq!(aligned[0].as_ref(), "IND_A");
290 assert_eq!(aligned[1].as_ref(), "IND_B");
291 assert_eq!(aligned[2].as_ref(), "all");
292 }
293}