bixverse_methods/methods/
cis_target.rs1use faer::MatRef;
5use rayon::prelude::*;
6
7use crate::prelude::*;
8use bixverse_core::core::math::vector_helpers::standard_deviation;
9
10#[derive(Debug, Clone)]
16pub struct MotifEnrichment<T> {
17 pub motif_idx: usize,
19 pub nes: T,
21 pub auc: T,
23 pub rank_at_max: u32,
25 pub n_enriched: usize,
27 pub enriched_gene_indices: Vec<usize>,
29}
30
31#[derive(Clone, Debug, Default)]
33pub enum RccType {
34 #[default]
36 ICisTarget,
37 Approx,
39}
40
41pub fn parse_rcc_type(s: &str) -> Option<RccType> {
51 match s.to_lowercase().as_str() {
52 "icistarget" => Some(RccType::ICisTarget),
53 "approx" => Some(RccType::Approx),
54 _ => None,
55 }
56}
57
58fn calculate_auc_single<T>(gene_ranks: &[i32], auc_threshold: i32) -> T
73where
74 T: BixverseFloat,
75{
76 let mut filtered = gene_ranks
77 .iter()
78 .copied()
79 .filter(|&r| r < auc_threshold)
80 .collect::<Vec<i32>>();
81
82 if filtered.is_empty() {
83 return T::zero();
84 }
85
86 filtered.sort_unstable();
87 filtered.push(auc_threshold);
88
89 let n_genes = T::from_usize(gene_ranks.len()).unwrap();
90 let max_auc = T::from_i32(auc_threshold).unwrap() * n_genes;
91
92 let auc = filtered
93 .windows(2)
94 .enumerate()
95 .map(|(i, w)| T::from_i32(w[1] - w[0]).unwrap() * T::from_usize(i + 1).unwrap())
96 .fold(T::zero(), |acc, x| acc + x);
97
98 auc / max_auc
99}
100
101pub fn calculate_aucs<T>(
113 matrix: MatRef<'_, i32>,
114 gene_indices: &[usize],
115 auc_threshold: i32,
116) -> Vec<T>
117where
118 T: BixverseFloat,
119{
120 let n_motifs = matrix.ncols();
121
122 (0..n_motifs)
123 .into_par_iter()
124 .map(|motif_idx| {
125 let gene_ranks: Vec<i32> = gene_indices
126 .iter()
127 .map(|&row_idx| *matrix.get(row_idx, motif_idx))
128 .collect();
129
130 calculate_auc_single(&gene_ranks, auc_threshold)
131 })
132 .collect()
133}
134
135pub fn calculate_nes<T>(auc_scores: &[T]) -> Vec<T>
145where
146 T: BixverseFloat + std::iter::Sum,
147{
148 let n = T::from_usize(auc_scores.len()).unwrap();
149 let mean = auc_scores.iter().copied().fold(T::zero(), |acc, x| acc + x) / n;
150
151 let std = standard_deviation(auc_scores);
152
153 if std == T::zero() {
154 return vec![T::zero(); auc_scores.len()];
155 }
156
157 auc_scores
158 .par_iter()
159 .map(|&auc| (auc - mean) / std)
160 .collect()
161}
162
163fn rolling_mean<T: BixverseFloat>(values: &[T], window: usize) -> Vec<T> {
178 let n = values.len();
179 let mut result = Vec::with_capacity(n);
180 let half_window = window / 2;
181 let mut sum = T::zero();
182 let mut count = 0;
183
184 let start = 0;
185 let end = (half_window + 1).min(n);
186 for &v in &values[start..end] {
187 sum += v;
188 count += 1;
189 }
190 result.push(sum / T::from_usize(count).unwrap());
191
192 for i in 1..n {
193 let new_start = i.saturating_sub(half_window);
194 let new_end = (i + half_window + 1).min(n);
195 let old_start = (i - 1).saturating_sub(half_window);
196 let old_end = (i - 1 + half_window + 1).min(n);
197
198 for &v in &values[old_start..new_start] {
199 sum -= v;
200 count -= 1;
201 }
202
203 for &v in &values[old_end..new_end] {
204 sum += v;
205 count += 1;
206 }
207
208 result.push(sum / T::from_usize(count).unwrap());
209 }
210
211 result
212}
213
214fn calculate_rcc(gene_ranks: &[i32], max_rank: i32) -> Vec<u32> {
225 let mut rcc = vec![0u32; max_rank as usize];
226 let mut filtered: Vec<i32> = gene_ranks
227 .iter()
228 .copied()
229 .filter(|&r| r > 0 && r < max_rank)
230 .collect();
231
232 if filtered.is_empty() {
233 return rcc;
234 }
235
236 filtered.sort_unstable();
237 filtered.push(max_rank);
238
239 let mut prev_rank = 0;
240 for (gene_count, &rank) in filtered.iter().enumerate() {
241 #[allow(clippy::needless_range_loop)]
242 for pos in prev_rank..(rank as usize).min(max_rank as usize) {
243 rcc[pos] = gene_count as u32;
244 }
245 prev_rank = rank as usize;
246 }
247
248 rcc
249}
250
251fn calculate_rcc_thresholds_aprox<T>(
264 matrix: MatRef<'_, i32>,
265 gene_indices: &[usize],
266 max_rank: i32,
267 n_mean: usize,
268) -> Vec<T>
269where
270 T: BixverseFloat,
271{
272 let n_motifs = matrix.ncols();
273 let n_genes = gene_indices.len();
274 let max_rank_extra = (max_rank as usize) + n_mean;
275
276 let mut global_mat = vec![vec![0usize; max_rank_extra]; n_genes + 1];
277
278 for m_idx in 0..n_motifs {
279 let mut ranks: Vec<i32> = gene_indices
280 .iter()
281 .map(|&idx| *matrix.get(idx, m_idx))
282 .filter(|&r| r > 0 && r <= max_rank_extra as i32)
283 .collect();
284
285 if ranks.is_empty() {
286 continue;
287 }
288
289 ranks.sort_unstable();
290
291 for (gene_count, &rank) in ranks.iter().enumerate() {
292 if (rank as usize) < max_rank_extra {
293 global_mat[gene_count + 1][rank as usize] += 1;
294 }
295 }
296 }
297
298 let mut rcc_mean_raw = vec![T::zero(); max_rank_extra];
299 let mut rcc_sd_raw = vec![T::zero(); max_rank_extra];
300
301 for rank_pos in 0..max_rank_extra {
302 let total_count: usize = global_mat.iter().map(|row| row[rank_pos]).sum();
303
304 if total_count == 0 {
305 if rank_pos > 0 {
306 rcc_mean_raw[rank_pos] = rcc_mean_raw[rank_pos - 1];
307 rcc_sd_raw[rank_pos] = rcc_sd_raw[rank_pos - 1];
308 }
309 continue;
310 }
311
312 let total_count_t = T::from_usize(total_count).unwrap();
313
314 let mut sum = T::zero();
315 let mut sum_sq = T::zero();
316
317 for (num_genes, count) in global_mat.iter().enumerate() {
318 let c = T::from_usize(count[rank_pos]).unwrap();
319 let ng = T::from_usize(num_genes).unwrap();
320 sum += ng * c;
321 sum_sq += ng * ng * c;
322 }
323
324 let mean = sum / total_count_t;
325 let variance = (sum_sq / total_count_t) - (mean * mean);
326
327 rcc_mean_raw[rank_pos] = mean;
328 rcc_sd_raw[rank_pos] = variance.sqrt();
329 }
330
331 let mut rcc_mean = vec![T::zero(); max_rank as usize];
332 let mut rcc_sd = vec![T::zero(); max_rank as usize];
333
334 let smoothed_mean = rolling_mean(&rcc_mean_raw, n_mean);
335 let smoothed_sd = rolling_mean(&rcc_sd_raw, n_mean);
336
337 for i in 0..(max_rank as usize) {
338 if i < 5 {
339 rcc_mean[i] = rcc_mean_raw[i];
340 rcc_sd[i] = rcc_sd_raw[i];
341 } else if i < smoothed_mean.len() {
342 rcc_mean[i] = smoothed_mean[i];
343 rcc_sd[i] = smoothed_sd[i];
344 }
345 }
346
347 rcc_mean
348 .iter()
349 .zip(rcc_sd.iter())
350 .map(|(&m, &s)| m + T::from_f64(2.0).unwrap() * s)
351 .collect()
352}
353
354fn find_leading_edge<T>(
368 matrix: MatRef<'_, i32>,
369 gene_indices: &[usize],
370 motif_idx: usize,
371 rcc_m2sd: &[T],
372 all_rccs: Option<&Vec<Vec<u32>>>,
373) -> (u32, usize, Vec<usize>)
374where
375 T: BixverseFloat,
376{
377 let current_rcc = match all_rccs {
378 Some(rccs) => &rccs[motif_idx],
379 None => &calculate_rcc(
380 &gene_indices
381 .iter()
382 .map(|&idx| *matrix.get(idx, motif_idx))
383 .collect::<Vec<_>>(),
384 rcc_m2sd.len() as i32,
385 ),
386 };
387
388 let mut max_enrichment = T::neg_infinity();
389 let mut best_rank = 0;
390
391 for (rank_pos, &threshold) in rcc_m2sd.iter().enumerate() {
392 if rank_pos < current_rcc.len() {
393 let enrichment = T::from_u32(current_rcc[rank_pos]).unwrap() - threshold;
394 if enrichment > max_enrichment {
395 max_enrichment = enrichment;
396 best_rank = rank_pos;
397 }
398 }
399 }
400
401 let rank_at_max = best_rank as u32;
402
403 let enriched: Vec<usize> = gene_indices
404 .iter()
405 .filter(|&&idx| {
406 let rank = *matrix.get(idx, motif_idx);
407 rank > 0 && rank <= rank_at_max as i32
408 })
409 .copied()
410 .collect();
411
412 let n_enriched = enriched.len();
413
414 (rank_at_max, n_enriched, enriched)
415}
416
417fn calculate_all_rccs(
429 matrix: MatRef<'_, i32>,
430 gene_indices: &[usize],
431 max_rank: i32,
432) -> Vec<Vec<u32>> {
433 let n_motifs = matrix.ncols();
434
435 (0..n_motifs)
436 .into_par_iter()
437 .map(|motif_idx| {
438 let gene_ranks: Vec<i32> = gene_indices
439 .iter()
440 .map(|&idx| *matrix.get(idx, motif_idx))
441 .collect();
442
443 calculate_rcc(&gene_ranks, max_rank)
444 })
445 .collect()
446}
447
448fn calculate_rcc_thresholds<T>(all_rccs: &[Vec<u32>], max_rank: i32) -> Vec<T>
459where
460 T: BixverseFloat,
461{
462 let max_rank_usize = max_rank as usize;
463 let n_motifs = T::from_usize(all_rccs.len()).unwrap();
464
465 let mut sums = vec![T::zero(); max_rank_usize];
467 let mut sums_sq = vec![T::zero(); max_rank_usize];
468 for rcc in all_rccs {
469 for ((sum, sum_sq), &count) in sums.iter_mut().zip(sums_sq.iter_mut()).zip(rcc) {
470 let val = T::from_u32(count).unwrap();
471 *sum += val;
472 *sum_sq += val * val;
473 }
474 }
475
476 sums.into_iter()
477 .zip(sums_sq)
478 .map(|(sum, sum_sq)| {
479 let mean = sum / n_motifs;
480 let variance = (sum_sq / n_motifs) - (mean * mean);
481 mean + T::from_f64(2.0).unwrap() * variance.sqrt()
482 })
483 .collect()
484}
485
486pub fn process_gene_set<T>(
502 matrix: MatRef<'_, i32>,
503 gene_indices: &[usize],
504 auc_threshold: i32,
505 nes_threshold: T,
506 max_rank: i32,
507 method: &RccType,
508 n_mean: usize,
509) -> Vec<MotifEnrichment<T>>
510where
511 T: BixverseFloat + std::iter::Sum,
512{
513 let auc_scores = calculate_aucs(matrix, gene_indices, auc_threshold);
514
515 let nes_scores = calculate_nes(&auc_scores);
516
517 let (rcc_m2sd, all_rccs) = match method {
518 RccType::ICisTarget => {
519 let all_rccs = calculate_all_rccs(matrix, gene_indices, max_rank);
520 let rcc_m2sd: Vec<T> = calculate_rcc_thresholds(&all_rccs, max_rank);
521 (rcc_m2sd, Some(all_rccs))
522 }
523 RccType::Approx => {
524 let rcc_m2sd = calculate_rcc_thresholds_aprox(matrix, gene_indices, max_rank, n_mean);
525 (rcc_m2sd, None)
526 }
527 };
528
529 nes_scores
530 .iter()
531 .enumerate()
532 .filter(|&(_, &nes)| nes >= nes_threshold)
533 .map(|(motif_idx, &nes)| {
534 let (rank_at_max, n_enriched, enriched_gene_indices) = find_leading_edge(
535 matrix,
536 gene_indices,
537 motif_idx,
538 &rcc_m2sd,
539 all_rccs.as_ref(),
540 );
541
542 MotifEnrichment {
543 motif_idx,
544 nes,
545 auc: auc_scores[motif_idx],
546 rank_at_max,
547 n_enriched,
548 enriched_gene_indices,
549 }
550 })
551 .collect()
552}
553
554#[cfg(test)]
559mod tests {
560 use super::*;
561 use approx::assert_relative_eq;
562
563 #[test]
567 fn rolling_mean_is_centred_and_clipped_at_the_edges() {
568 let got: Vec<f64> = rolling_mean(&[1.0, 2.0, 3.0, 4.0, 5.0], 5);
569
570 assert_eq!(got.len(), 5);
571 for (g, w) in got.iter().zip([2.0, 2.5, 3.0, 3.5, 4.0]) {
572 assert_relative_eq!(*g, w, epsilon = 1e-12);
573 }
574 }
575
576 #[test]
578 fn rolling_mean_window_of_one_is_the_identity() {
579 let values = [1.0_f64, 7.0, 3.0];
580 let got: Vec<f64> = rolling_mean(&values, 1);
581
582 for (g, w) in got.iter().zip(values) {
583 assert_relative_eq!(*g, w, epsilon = 1e-12);
584 }
585 }
586
587 #[test]
590 fn auc_single_normalises_by_the_unfiltered_gene_count() {
591 assert_relative_eq!(
592 calculate_auc_single::<f64>(&[1, 3, 7], 5),
593 0.4,
594 epsilon = 1e-12
595 );
596 }
597
598 #[test]
600 fn auc_single_is_zero_when_no_gene_beats_the_threshold() {
601 assert_relative_eq!(
602 calculate_auc_single::<f64>(&[7, 9], 5),
603 0.0,
604 epsilon = 1e-12
605 );
606 }
607
608 #[test]
610 fn rcc_steps_at_each_retained_rank() {
611 assert_eq!(calculate_rcc(&[2, 4, 9], 6), vec![0, 0, 1, 1, 2, 2]);
612 }
613
614 #[test]
617 fn rcc_treats_rank_zero_as_absent() {
618 assert_eq!(calculate_rcc(&[0, 3], 5), vec![0, 0, 0, 1, 1]);
619 }
620
621 #[test]
623 fn nes_z_scores_against_the_sample_sd() {
624 let got: Vec<f64> = calculate_nes(&[1.0, 2.0, 3.0]);
625
626 for (g, w) in got.iter().zip([-1.0, 0.0, 1.0]) {
627 assert_relative_eq!(*g, w, epsilon = 1e-12);
628 }
629 }
630
631 #[test]
634 fn nes_of_a_constant_input_is_all_zero() {
635 assert_eq!(calculate_nes::<f64>(&[2.0; 4]), vec![0.0; 4]);
636 }
637}