1use rayon::prelude::*;
16
17use crate::core::expression::column_sums;
18use crate::errors::EdgeErrors;
19use crate::numeric::stats::{quantile_type7, rank_average};
20use crate::utils::traits::EdgeFloat;
21
22const TMM_NULL_TOLERANCE: f64 = 1e-6;
31
32const TMMWSP_ZERO_EPS: f64 = 1e-14;
37
38const TMMWSP_WEIGHT_RIDGE: f64 = 1e-6;
43
44const F75_DEGENERATE: f64 = 1e-20;
50
51const REF_COLUMN_QUANTILE: f64 = 0.75;
56
57const NORM_METHOD_NAMES: &str = "TMM, TMMwsp, RLE, upperquartile, none";
59
60#[derive(Clone, Copy, Debug, PartialEq, Eq)]
66pub enum NormMethod {
67 Tmm,
69 TmmwSp,
71 Rle,
73 UpperQuartile,
75 None,
77}
78
79pub fn parse_norm_method(s: &str) -> Option<NormMethod> {
92 match s.to_ascii_lowercase().as_str() {
93 "tmm" => Some(NormMethod::Tmm),
94 "tmmwsp" | "tmmwzp" => Some(NormMethod::TmmwSp),
95 "rle" => Some(NormMethod::Rle),
96 "upperquartile" => Some(NormMethod::UpperQuartile),
97 "none" => Some(NormMethod::None),
98 _ => None,
99 }
100}
101
102impl TryFrom<&str> for NormMethod {
103 type Error = EdgeErrors;
104
105 fn try_from(s: &str) -> Result<Self, Self::Error> {
115 parse_norm_method(s).ok_or_else(|| EdgeErrors::UnknownMethod {
116 kind: "normalisation",
117 name: s.to_string(),
118 expected: NORM_METHOD_NAMES,
119 })
120 }
121}
122
123#[derive(Clone, Copy, Debug)]
133pub struct NormParams {
134 pub logratio_trim: f64,
136 pub sum_trim: f64,
138 pub do_weighting: bool,
141 pub a_cutoff: f64,
144 pub quantile: f64,
146}
147
148impl NormParams {
149 pub fn new(
165 logratio_trim: f64,
166 sum_trim: f64,
167 do_weighting: bool,
168 a_cutoff: f64,
169 quantile: f64,
170 ) -> Self {
171 Self {
172 logratio_trim,
173 sum_trim,
174 do_weighting,
175 a_cutoff,
176 quantile,
177 }
178 }
179}
180
181impl Default for NormParams {
182 fn default() -> Self {
184 Self {
185 logratio_trim: 0.3,
186 sum_trim: 0.05,
187 do_weighting: true,
188 a_cutoff: -1e10,
189 quantile: 0.75,
190 }
191 }
192}
193
194fn validate_params(params: &NormParams) -> Result<(), EdgeErrors> {
204 for (name, trim) in [
205 ("logratio_trim", params.logratio_trim),
206 ("sum_trim", params.sum_trim),
207 ] {
208 if !(0.0..1.0).contains(&trim) {
209 return Err(EdgeErrors::InvalidArgument(format!(
210 "{name} must lie in [0, 1); got {trim}."
211 )));
212 }
213 }
214 if !(0.0..=1.0).contains(¶ms.quantile) {
215 return Err(EdgeErrors::InvalidArgument(format!(
216 "quantile must lie in [0, 1]; got {}.",
217 params.quantile
218 )));
219 }
220 if params.a_cutoff.is_nan() {
221 return Err(EdgeErrors::InvalidArgument(
222 "a_cutoff must not be NaN.".to_string(),
223 ));
224 }
225 Ok(())
226}
227
228fn resolve_lib_size<T: EdgeFloat>(
247 counts: &[T],
248 n_samples: usize,
249 lib_size: Option<&[f64]>,
250) -> Result<Vec<f64>, EdgeErrors> {
251 let lib = match lib_size {
252 Some(ls) => {
253 if ls.len() != n_samples {
254 return Err(EdgeErrors::LengthMismatch {
255 name: "lib_size",
256 expected: n_samples,
257 got: ls.len(),
258 });
259 }
260 ls.to_vec()
261 }
262 None => column_sums(counts, n_samples),
263 };
264
265 if let Some(j) = lib.iter().position(|v| !v.is_finite() || *v <= 0.0) {
267 return Err(EdgeErrors::InvalidArgument(format!(
268 "library size for sample {j} must be finite and positive; got {}.",
269 lib[j]
270 )));
271 }
272 Ok(lib)
273}
274
275fn to_column_major_nonzero<T: EdgeFloat>(
292 counts: &[T],
293 n_genes: usize,
294 n_samples: usize,
295) -> Result<(Vec<f64>, usize), EdgeErrors> {
296 let mut keep = vec![false; n_genes];
297 let mut n_kept = 0_usize;
298 for (g, row) in counts.chunks_exact(n_samples).enumerate() {
299 let mut any_positive = false;
300 for v in row {
301 if !v.is_finite() || *v < T::zero() {
302 return Err(EdgeErrors::InvalidArgument(format!(
303 "counts must be non-negative and finite, found {v} for gene {g}"
304 )));
305 }
306 any_positive |= *v > T::zero();
307 }
308 keep[g] = any_positive;
309 n_kept += usize::from(any_positive);
310 }
311
312 let mut x = vec![0.0_f64; n_kept * n_samples];
313 let mut k = 0_usize;
314 for (g, row) in counts.chunks_exact(n_samples).enumerate() {
315 if !keep[g] {
316 continue;
317 }
318 for (j, v) in row.iter().enumerate() {
319 x[j * n_kept + k] = v.to_f64().unwrap_or(f64::NAN);
320 }
321 k += 1;
322 }
323 Ok((x, n_kept))
324}
325
326fn normalise_to_unit_product(f: &mut [f64]) -> Result<(), EdgeErrors> {
339 if let Some(j) = f.iter().position(|v| !v.is_finite() || *v <= 0.0) {
340 return Err(EdgeErrors::InvalidArgument(format!(
341 "normalisation factor for sample {j} is not finite and positive ({}); \
342 the counts are too sparse for this method",
343 f[j]
344 )));
345 }
346 let log_mean = f.iter().map(|v| v.ln()).sum::<f64>() / f.len() as f64;
347 let scale = log_mean.exp();
348 for v in f.iter_mut() {
349 *v /= scale;
350 }
351 Ok(())
352}
353
354fn resolve_ref_column(
376 x: &[f64],
377 n_kept: usize,
378 n_samples: usize,
379 lib_size: &[f64],
380 method: NormMethod,
381) -> Result<usize, EdgeErrors> {
382 if method == NormMethod::TmmwSp {
383 return Ok(arg_max_sqrt_colsum(x, n_kept, n_samples));
384 }
385
386 let mut f75 = vec![0.0_f64; n_samples];
387 for (j, f) in f75.iter_mut().enumerate() {
388 *f = quantile_type7(&x[j * n_kept..(j + 1) * n_kept], REF_COLUMN_QUANTILE)? / lib_size[j];
389 }
390
391 let median = quantile_type7(&f75, 0.5)?;
392 if median < F75_DEGENERATE {
393 return Ok(arg_max_sqrt_colsum(x, n_kept, n_samples));
394 }
395
396 let mean = f75.iter().sum::<f64>() / n_samples as f64;
397 let mut best = 0_usize;
399 let mut best_gap = f64::INFINITY;
400 for (j, f) in f75.iter().enumerate() {
401 let gap = (f - mean).abs();
402 if gap < best_gap {
403 best_gap = gap;
404 best = j;
405 }
406 }
407 if !best_gap.is_finite() {
408 return Err(EdgeErrors::InvalidReferenceColumn(
409 "upper-quartile factors are not finite for any sample".to_string(),
410 ));
411 }
412 Ok(best)
413}
414
415fn arg_max_sqrt_colsum(x: &[f64], n_kept: usize, n_samples: usize) -> usize {
430 let mut best = 0_usize;
431 let mut best_sum = f64::NEG_INFINITY;
432 for j in 0..n_samples {
433 let s: f64 = x[j * n_kept..(j + 1) * n_kept]
434 .iter()
435 .map(|v| v.sqrt())
436 .sum();
437 if s > best_sum {
438 best_sum = s;
439 best = j;
440 }
441 }
442 best
443}
444
445fn calc_factor_quantile(
466 x: &[f64],
467 n_kept: usize,
468 n_samples: usize,
469 lib_size: &[f64],
470 p: f64,
471) -> Result<Vec<f64>, EdgeErrors> {
472 let mut f = vec![0.0_f64; n_samples];
473 for (j, out) in f.iter_mut().enumerate() {
474 let q = quantile_type7(&x[j * n_kept..(j + 1) * n_kept], p)?;
475 if q <= 0.0 {
476 return Err(EdgeErrors::InvalidArgument(format!(
477 "the {p} quantile of sample {j} is zero; upper-quartile \
478 normalisation is undefined for counts this sparse"
479 )));
480 }
481 *out = q / lib_size[j];
482 }
483 Ok(f)
484}
485
486fn calc_factor_rle(
510 x: &[f64],
511 n_kept: usize,
512 n_samples: usize,
513 lib_size: &[f64],
514) -> Result<Vec<f64>, EdgeErrors> {
515 let mut log_gm = vec![0.0_f64; n_kept];
516 for j in 0..n_samples {
517 for (acc, v) in log_gm.iter_mut().zip(&x[j * n_kept..(j + 1) * n_kept]) {
518 *acc += v.ln();
519 }
520 }
521 let positive: Vec<usize> = log_gm
522 .iter()
523 .enumerate()
524 .filter(|(_, v)| (*v / n_samples as f64).exp() > 0.0)
525 .map(|(g, _)| g)
526 .collect();
527 if positive.is_empty() {
528 return Err(EdgeErrors::NoGenesAfterFiltering { n_genes: n_kept });
529 }
530
531 let gm: Vec<f64> = positive
532 .iter()
533 .map(|&g| (log_gm[g] / n_samples as f64).exp())
534 .collect();
535
536 let mut f = vec![0.0_f64; n_samples];
537 let mut ratios = vec![0.0_f64; positive.len()];
538 for (j, out) in f.iter_mut().enumerate() {
539 let col = &x[j * n_kept..(j + 1) * n_kept];
540 for (r, (&g, &m)) in ratios.iter_mut().zip(positive.iter().zip(gm.iter())) {
541 *r = col[g] / m;
542 }
543 *out = quantile_type7(&ratios, 0.5)? / lib_size[j];
544 }
545 Ok(f)
546}
547
548fn calc_factor_tmm(
582 obs: &[f64],
583 reference: &[f64],
584 lib_obs: f64,
585 lib_ref: f64,
586 params: &NormParams,
587) -> f64 {
588 let n_all = obs.len();
589 let mut log_r = Vec::with_capacity(n_all);
590 let mut abs_e = Vec::with_capacity(n_all);
591 let mut var = Vec::with_capacity(n_all);
592
593 for (&o, &r) in obs.iter().zip(reference.iter()) {
594 let p_obs = o / lib_obs;
595 let p_ref = r / lib_ref;
596 let lr = (p_obs / p_ref).log2();
597 let ae = (p_obs.log2() + p_ref.log2()) / 2.0;
598 if lr.is_finite() && ae.is_finite() && ae > params.a_cutoff {
599 log_r.push(lr);
600 abs_e.push(ae);
601 var.push((lib_obs - o) / lib_obs / o + (lib_ref - r) / lib_ref / r);
602 }
603 }
604
605 let n = log_r.len();
606 if n == 0 || log_r.iter().fold(0.0_f64, |m, v| m.max(v.abs())) < TMM_NULL_TOLERANCE {
607 return 1.0;
608 }
609
610 let lo_l = (n as f64 * params.logratio_trim).floor() + 1.0;
611 let hi_l = n as f64 + 1.0 - lo_l;
612 let lo_s = (n as f64 * params.sum_trim).floor() + 1.0;
613 let hi_s = n as f64 + 1.0 - lo_s;
614
615 let rank_r = rank_average(&log_r);
616 let rank_e = rank_average(&abs_e);
617
618 let mut num = 0.0_f64;
619 let mut den = 0.0_f64;
620 let mut kept = 0_usize;
621 for i in 0..n {
622 let keep = rank_r[i] >= lo_l && rank_r[i] <= hi_l && rank_e[i] >= lo_s && rank_e[i] <= hi_s;
623 if !keep {
624 continue;
625 }
626 kept += 1;
627 if params.do_weighting {
628 num += log_r[i] / var[i];
629 den += 1.0 / var[i];
630 } else {
631 num += log_r[i];
632 }
633 }
634
635 let f = if params.do_weighting {
636 num / den
637 } else if kept == 0 {
638 f64::NAN
639 } else {
640 num / kept as f64
641 };
642
643 let f = if f.is_nan() { 0.0 } else { f };
645
646 f.exp2()
647}
648
649fn calc_factor_tmmwsp(
687 obs: &[f64],
688 reference: &[f64],
689 lib_obs: f64,
690 lib_ref: f64,
691 params: &NormParams,
692) -> f64 {
693 let n_all = obs.len();
694 let mut pair_obs = Vec::with_capacity(n_all);
695 let mut pair_ref = Vec::with_capacity(n_all);
696 let mut single_ref = Vec::new();
698 let mut single_obs = Vec::new();
699
700 for (&o, &r) in obs.iter().zip(reference.iter()) {
701 match (o > TMMWSP_ZERO_EPS, r > TMMWSP_ZERO_EPS) {
702 (true, true) => {
703 pair_obs.push(o);
704 pair_ref.push(r);
705 }
706 (true, false) => single_obs.push(o),
707 (false, true) => single_ref.push(r),
708 (false, false) => {}
709 }
710 }
711
712 let n_eligible = single_obs.len().min(single_ref.len());
713 if n_eligible > 0 {
714 single_obs.sort_unstable_by(|a, b| b.total_cmp(a));
715 single_ref.sort_unstable_by(|a, b| b.total_cmp(a));
716 pair_obs.extend_from_slice(&single_obs[..n_eligible]);
717 pair_ref.extend_from_slice(&single_ref[..n_eligible]);
718 }
719
720 let n = pair_obs.len();
721 if n == 0 {
722 return 1.0;
723 }
724
725 let p_obs: Vec<f64> = pair_obs.iter().map(|o| o / lib_obs).collect();
726 let p_ref: Vec<f64> = pair_ref.iter().map(|r| r / lib_ref).collect();
727 let m: Vec<f64> = p_obs
728 .iter()
729 .zip(p_ref.iter())
730 .map(|(o, r)| (o / r).log2())
731 .collect();
732 let a: Vec<f64> = p_obs
733 .iter()
734 .zip(p_ref.iter())
735 .map(|(o, r)| 0.5 * (o * r).log2())
736 .collect();
737
738 if m.iter().fold(0.0_f64, |acc, v| acc.max(v.abs())) < TMM_NULL_TOLERANCE {
739 return 1.0;
740 }
741
742 let m_shrunk: Vec<f64> = pair_obs
743 .iter()
744 .zip(pair_ref.iter())
745 .map(|(o, r)| (((o + 0.5) / (lib_obs + 0.5)) / ((r + 0.5) / (lib_ref + 0.5))).log2())
746 .collect();
747
748 let mut order_m: Vec<usize> = (0..n).collect();
749 order_m.sort_by(|&i, &j| {
751 m[i].total_cmp(&m[j])
752 .then(m_shrunk[i].total_cmp(&m_shrunk[j]))
753 });
754 let mut order_a: Vec<usize> = (0..n).collect();
755 order_a.sort_by(|&i, &j| a[i].total_cmp(&a[j]));
756
757 let keep_m = trim_window(&order_m, n, params.logratio_trim);
758 let keep_a = trim_window(&order_a, n, params.sum_trim);
759
760 let mut num = 0.0_f64;
761 let mut den = 0.0_f64;
762 let mut kept = 0_usize;
763 for i in 0..n {
764 if !(keep_m[i] && keep_a[i]) {
765 continue;
766 }
767 kept += 1;
768 if params.do_weighting {
769 let v = (1.0 - p_obs[i]) / p_obs[i] / lib_obs + (1.0 - p_ref[i]) / p_ref[i] / lib_ref;
770 let w = (1.0 + TMMWSP_WEIGHT_RIDGE) / (v + TMMWSP_WEIGHT_RIDGE);
771 num += w * m[i];
772 den += w;
773 } else {
774 num += m[i];
775 }
776 }
777
778 if kept == 0 {
779 return 1.0;
780 }
781 let tmm = if params.do_weighting {
782 num / den
783 } else {
784 num / kept as f64
785 };
786 if tmm.is_nan() { 1.0 } else { tmm.exp2() }
787}
788
789fn trim_window(order: &[usize], n: usize, trim: f64) -> Vec<bool> {
806 let mut keep = vec![false; n];
807 let lo = (n as f64 * trim) as usize + 1;
808 let hi = n + 1 - lo.min(n + 1);
809 if lo > hi {
810 return keep;
811 }
812 for &idx in &order[lo - 1..hi] {
813 keep[idx] = true;
814 }
815 keep
816}
817
818pub fn calc_norm_factors<T: EdgeFloat>(
856 counts: &[T],
857 n_genes: usize,
858 n_samples: usize,
859 lib_size: Option<&[f64]>,
860 method: NormMethod,
861 ref_column: Option<usize>,
862 params: Option<NormParams>,
863) -> Result<Vec<f64>, EdgeErrors> {
864 let params = params.unwrap_or_default();
865 validate_params(¶ms)?;
866
867 if n_genes == 0 || n_samples == 0 {
868 return Err(EdgeErrors::EmptyCounts { n_genes, n_samples });
869 }
870 if counts.len() != n_genes * n_samples {
871 return Err(EdgeErrors::LengthMismatch {
872 name: "counts",
873 expected: n_genes * n_samples,
874 got: counts.len(),
875 });
876 }
877 if let Some(r) = ref_column
878 && r >= n_samples
879 {
880 return Err(EdgeErrors::InvalidReferenceColumn(format!(
881 "column {r} requested but there are only {n_samples} samples"
882 )));
883 }
884
885 let lib = resolve_lib_size(counts, n_samples, lib_size)?;
886 let (x, n_kept) = to_column_major_nonzero(counts, n_genes, n_samples)?;
887
888 let method = if n_kept == 0 || n_samples == 1 {
890 NormMethod::None
891 } else {
892 method
893 };
894
895 let mut f = match method {
896 NormMethod::Tmm | NormMethod::TmmwSp => {
897 let reference = match ref_column {
898 Some(r) => r,
899 None => resolve_ref_column(&x, n_kept, n_samples, &lib, method)?,
900 };
901 let kernel = if method == NormMethod::Tmm {
902 calc_factor_tmm
903 } else {
904 calc_factor_tmmwsp
905 };
906 let ref_col = &x[reference * n_kept..(reference + 1) * n_kept];
907 (0..n_samples)
908 .into_par_iter()
909 .map(|j| {
910 kernel(
911 &x[j * n_kept..(j + 1) * n_kept],
912 ref_col,
913 lib[j],
914 lib[reference],
915 ¶ms,
916 )
917 })
918 .collect()
919 }
920 NormMethod::Rle => calc_factor_rle(&x, n_kept, n_samples, &lib)?,
921 NormMethod::UpperQuartile => {
922 calc_factor_quantile(&x, n_kept, n_samples, &lib, params.quantile)?
923 }
924 NormMethod::None => vec![1.0; n_samples],
925 };
926
927 normalise_to_unit_product(&mut f)?;
928 Ok(f)
929}
930
931#[cfg(test)]
936mod tests {
937 use super::*;
938 use approx::assert_relative_eq;
939 use std::path::PathBuf;
940
941 const TOL: f64 = 1e-14;
943
944 fn read_csv(name: &str) -> (Vec<f64>, usize, usize) {
957 let path: PathBuf = [env!("CARGO_MANIFEST_DIR"), "tests", "data", name]
958 .iter()
959 .collect();
960 let text = std::fs::read_to_string(&path)
961 .unwrap_or_else(|e| panic!("cannot read {}: {e}", path.display()));
962
963 let mut lines = text.lines().filter(|l| !l.trim().is_empty());
964 let header = lines.next().expect("fixture has no header");
965 let n_cols = header.split(',').count();
966
967 let mut values = Vec::new();
968 let mut n_rows = 0_usize;
969 for line in lines {
970 for field in line.split(',') {
971 values.push(
972 field
973 .trim()
974 .trim_matches('"')
975 .parse::<f64>()
976 .unwrap_or_else(|e| panic!("bad field {field:?} in {name}: {e}")),
977 );
978 }
979 n_rows += 1;
980 }
981 assert_eq!(values.len(), n_rows * n_cols, "ragged fixture {name}");
982 (values, n_rows, n_cols)
983 }
984
985 #[allow(clippy::too_many_arguments)]
998 fn check(
999 counts: &[f64],
1000 n_genes: usize,
1001 n_samples: usize,
1002 lib_size: Option<&[f64]>,
1003 method: NormMethod,
1004 ref_column: Option<usize>,
1005 params: Option<NormParams>,
1006 expected: &[f64],
1007 ) {
1008 let got = calc_norm_factors(
1009 counts, n_genes, n_samples, lib_size, method, ref_column, params,
1010 )
1011 .expect("calc_norm_factors failed");
1012 assert_eq!(got.len(), expected.len());
1013 for (g, e) in got.iter().zip(expected.iter()) {
1014 assert_relative_eq!(g, e, max_relative = TOL, epsilon = 0.0);
1015 }
1016 }
1017
1018 #[test]
1023 fn test_parse_norm_method_accepts_every_edger_spelling() {
1024 assert_eq!(parse_norm_method("TMM"), Some(NormMethod::Tmm));
1025 assert_eq!(parse_norm_method("tmm"), Some(NormMethod::Tmm));
1026 assert_eq!(parse_norm_method("TMMwsp"), Some(NormMethod::TmmwSp));
1027 assert_eq!(parse_norm_method("TMMwzp"), Some(NormMethod::TmmwSp));
1028 assert_eq!(parse_norm_method("RLE"), Some(NormMethod::Rle));
1029 assert_eq!(
1030 parse_norm_method("upperquartile"),
1031 Some(NormMethod::UpperQuartile)
1032 );
1033 assert_eq!(parse_norm_method("none"), Some(NormMethod::None));
1034 assert_eq!(parse_norm_method("quantile"), None);
1035 }
1036
1037 #[test]
1038 fn test_try_from_reports_the_unknown_method() {
1039 let err = NormMethod::try_from("wibble").unwrap_err();
1040 assert!(matches!(err, EdgeErrors::UnknownMethod { .. }));
1041 }
1042
1043 #[test]
1044 fn test_default_params_are_edgers() {
1045 let p = NormParams::default();
1046 assert_eq!(p.logratio_trim, 0.3);
1047 assert_eq!(p.sum_trim, 0.05);
1048 assert!(p.do_weighting);
1049 assert_eq!(p.a_cutoff, -1e10);
1050 assert_eq!(p.quantile, 0.75);
1051 let q = NormParams::new(0.2, 0.1, false, -5.0, 0.5);
1052 assert_eq!(q.logratio_trim, 0.2);
1053 assert!(!q.do_weighting);
1054 }
1055
1056 #[test]
1064 fn test_part1_tmm_matches_r() {
1065 let (x, g, s) = read_csv("test_data_part1.csv");
1066 check(
1067 &x,
1068 g,
1069 s,
1070 None,
1071 NormMethod::Tmm,
1072 None,
1073 None,
1074 &[
1075 1.09214845928324,
1076 0.881664551382546,
1077 1.03839386653584,
1078 1.0001216498394,
1079 ],
1080 );
1081 }
1082
1083 #[test]
1084 fn test_part1_tmmwsp_matches_r() {
1085 let (x, g, s) = read_csv("test_data_part1.csv");
1086 check(
1087 &x,
1088 g,
1089 s,
1090 None,
1091 NormMethod::TmmwSp,
1092 None,
1093 None,
1094 &[
1095 1.11707573778036,
1096 0.875057108000525,
1097 1.03061167368031,
1098 0.992626639707476,
1099 ],
1100 );
1101 }
1102
1103 #[test]
1104 fn test_part1_rle_matches_r() {
1105 let (x, g, s) = read_csv("test_data_part1.csv");
1106 check(
1107 &x,
1108 g,
1109 s,
1110 None,
1111 NormMethod::Rle,
1112 None,
1113 None,
1114 &[
1115 1.08551277222965,
1116 0.956217074652334,
1117 0.91748548770939,
1118 1.05004851312078,
1119 ],
1120 );
1121 }
1122
1123 #[test]
1124 fn test_part1_upper_quartile_matches_r() {
1125 let (x, g, s) = read_csv("test_data_part1.csv");
1126 check(
1127 &x,
1128 g,
1129 s,
1130 None,
1131 NormMethod::UpperQuartile,
1132 None,
1133 None,
1134 &[
1135 0.917795511372727,
1136 1.20110064867884,
1137 0.913136650908906,
1138 0.993433714094184,
1139 ],
1140 );
1141 }
1142
1143 #[test]
1144 fn test_part1_upper_quartile_at_the_median_matches_r() {
1145 let (x, g, s) = read_csv("test_data_part1.csv");
1146 let params = NormParams {
1147 quantile: 0.5,
1148 ..Default::default()
1149 };
1150 check(
1151 &x,
1152 g,
1153 s,
1154 None,
1155 NormMethod::UpperQuartile,
1156 None,
1157 Some(params),
1158 &[
1159 1.04104095814556,
1160 0.864593338120892,
1161 1.0357564862768,
1162 1.07266136107989,
1163 ],
1164 );
1165 }
1166
1167 #[test]
1168 fn test_part1_none_is_all_ones() {
1169 let (x, g, s) = read_csv("test_data_part1.csv");
1170 let f = calc_norm_factors(&x, g, s, None, NormMethod::None, None, None).unwrap();
1171 assert_eq!(f, vec![1.0; 4]);
1172 }
1173
1174 #[test]
1175 fn test_part1_tmm_with_an_explicit_reference_matches_r() {
1176 let (x, g, s) = read_csv("test_data_part1.csv");
1177 check(
1179 &x,
1180 g,
1181 s,
1182 None,
1183 NormMethod::Tmm,
1184 Some(0),
1185 None,
1186 &[
1187 1.12488124746577,
1188 0.866533389465521,
1189 0.995933161741123,
1190 1.03009630195075,
1191 ],
1192 );
1193 check(
1194 &x,
1195 g,
1196 s,
1197 None,
1198 NormMethod::Tmm,
1199 Some(2),
1200 None,
1201 &[
1202 1.04654677428291,
1203 1.15554356186379,
1204 0.926578374534831,
1205 0.892427355851779,
1206 ],
1207 );
1208 }
1209
1210 #[test]
1211 fn test_part1_tmm_with_custom_trims_matches_r() {
1212 let (x, g, s) = read_csv("test_data_part1.csv");
1213 let params = NormParams {
1214 logratio_trim: 0.2,
1215 sum_trim: 0.1,
1216 ..Default::default()
1217 };
1218 check(
1219 &x,
1220 g,
1221 s,
1222 None,
1223 NormMethod::Tmm,
1224 None,
1225 Some(params),
1226 &[
1227 1.13220983123328,
1228 0.969765698086251,
1229 0.916554640595877,
1230 0.993683089958524,
1231 ],
1232 );
1233 }
1234
1235 #[test]
1236 fn test_part1_tmm_without_weighting_matches_r() {
1237 let (x, g, s) = read_csv("test_data_part1.csv");
1238 let params = NormParams {
1239 do_weighting: false,
1240 ..Default::default()
1241 };
1242 check(
1243 &x,
1244 g,
1245 s,
1246 None,
1247 NormMethod::Tmm,
1248 None,
1249 Some(params),
1250 &[
1251 1.11187880471527,
1252 0.87234553380242,
1253 0.996922506288526,
1254 1.03417159694611,
1255 ],
1256 );
1257 }
1258
1259 #[test]
1260 fn test_part1_tmm_with_supplied_library_sizes_matches_r() {
1261 let (x, g, s) = read_csv("test_data_part1.csv");
1262 check(
1263 &x,
1264 g,
1265 s,
1266 Some(&[1000.0, 2000.0, 1500.0, 1200.0]),
1267 NormMethod::Tmm,
1268 None,
1269 None,
1270 &[
1271 1.44466458873241,
1272 0.720343929088734,
1273 0.855991617053877,
1274 1.12259618110301,
1275 ],
1276 );
1277 }
1278
1279 #[test]
1280 fn test_part1_tmmwsp_variants_match_r() {
1281 let (x, g, s) = read_csv("test_data_part1.csv");
1282 let params = NormParams {
1283 do_weighting: false,
1284 ..Default::default()
1285 };
1286 check(
1287 &x,
1288 g,
1289 s,
1290 None,
1291 NormMethod::TmmwSp,
1292 None,
1293 Some(params),
1294 &[
1295 1.14857890712681,
1296 0.862953537941521,
1297 0.986189268494682,
1298 1.02303732161413,
1299 ],
1300 );
1301 check(
1302 &x,
1303 g,
1304 s,
1305 None,
1306 NormMethod::TmmwSp,
1307 Some(1),
1308 None,
1309 &[
1310 1.24531629866136,
1311 0.95930860926261,
1312 0.769226281876988,
1313 1.08819787022886,
1314 ],
1315 );
1316 }
1317
1318 #[test]
1323 fn test_part2_all_methods_match_r() {
1324 let (x, g, s) = read_csv("test_data_part2.csv");
1325 check(
1326 &x,
1327 g,
1328 s,
1329 None,
1330 NormMethod::Tmm,
1331 None,
1332 None,
1333 &[1.00087299037568, 1.00141716151291, 0.997713849403509],
1334 );
1335 check(
1336 &x,
1337 g,
1338 s,
1339 None,
1340 NormMethod::TmmwSp,
1341 None,
1342 None,
1343 &[0.999243678236565, 1.00770066012757, 0.993109297052868],
1344 );
1345 check(
1346 &x,
1347 g,
1348 s,
1349 None,
1350 NormMethod::Rle,
1351 None,
1352 None,
1353 &[1.00777567277904, 0.990633895481675, 1.00166603054436],
1354 );
1355 check(
1356 &x,
1357 g,
1358 s,
1359 None,
1360 NormMethod::UpperQuartile,
1361 None,
1362 None,
1363 &[0.952168937473405, 1.09472066633077, 0.959362356794534],
1364 );
1365 }
1366
1367 #[test]
1368 fn test_part2_trim_and_weight_variants_match_r() {
1369 let (x, g, s) = read_csv("test_data_part2.csv");
1370 check(
1371 &x,
1372 g,
1373 s,
1374 None,
1375 NormMethod::Tmm,
1376 None,
1377 Some(NormParams {
1378 do_weighting: false,
1379 ..Default::default()
1380 }),
1381 &[0.998440905548369, 1.00708634636271, 0.99451405794568],
1382 );
1383 check(
1384 &x,
1385 g,
1386 s,
1387 None,
1388 NormMethod::TmmwSp,
1389 None,
1390 Some(NormParams {
1391 logratio_trim: 0.2,
1392 sum_trim: 0.1,
1393 ..Default::default()
1394 }),
1395 &[1.0147225769176, 0.998674566234331, 0.986798969146451],
1396 );
1397 }
1398
1399 fn tiny() -> Vec<f64> {
1405 vec![1.0, 4.0, 7.0, 2.0, 5.0, 8.0, 3.0, 6.0, 900.0]
1407 }
1408
1409 #[test]
1410 fn test_tiny_matrix_matches_r() {
1411 let x = tiny();
1412 check(
1413 &x,
1414 3,
1415 3,
1416 None,
1417 NormMethod::Tmm,
1418 None,
1419 None,
1420 &[1.31487541222201, 1.24518272726712, 0.610776484663282],
1421 );
1422 check(
1423 &x,
1424 3,
1425 3,
1426 None,
1427 NormMethod::TmmwSp,
1428 None,
1429 None,
1430 &[1.21548697491053, 1.45714699120165, 0.56460708913077],
1431 );
1432 check(
1433 &x,
1434 3,
1435 3,
1436 None,
1437 NormMethod::Rle,
1438 None,
1439 None,
1440 &[2.49099606900441, 3.51095868773469, 0.114340803167416],
1441 );
1442 check(
1443 &x,
1444 3,
1445 3,
1446 None,
1447 NormMethod::UpperQuartile,
1448 None,
1449 None,
1450 &[0.984518260707848, 0.866376069422906, 1.17238371242325],
1451 );
1452 }
1453
1454 fn sparse() -> Vec<f64> {
1459 vec![
1460 3.0, 1.0, 1.0, 0.0, 0.0, 0.0, 0.0, 4.0, 1.0, 0.0, 2.0, 0.0, 0.0, 3.0, 2.0, 1.0, 0.0,
1461 0.0, 0.0, 1.0, 0.0, 2.0, 0.0, 1.0, 0.0, 2.0, 2.0, 0.0, 0.0, 2.0, 0.0, 1.0, 1.0, 0.0,
1462 0.0, 1.0, 1.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 1.0, 0.0, 1.0, 0.0, 0.0,
1463 0.0, 1.0, 0.0, 3.0, 1.0, 0.0, 0.0, 0.0, 0.0,
1464 ]
1465 }
1466
1467 #[test]
1468 fn test_sparse_matrix_matches_r() {
1469 let x = sparse();
1470 check(
1471 &x,
1472 20,
1473 3,
1474 None,
1475 NormMethod::Tmm,
1476 None,
1477 None,
1478 &[1.86860983013238, 0.718566958550494, 0.744756200069697],
1479 );
1480 check(
1481 &x,
1482 20,
1483 3,
1484 None,
1485 NormMethod::TmmwSp,
1486 None,
1487 None,
1488 &[1.01633695449891, 0.900162048144607, 1.09305391560417],
1489 );
1490 check(
1491 &x,
1492 20,
1493 3,
1494 None,
1495 NormMethod::TmmwSp,
1496 None,
1497 Some(NormParams {
1498 do_weighting: false,
1499 ..Default::default()
1500 }),
1501 &[1.06398557538257, 0.879775142410609, 1.06829838721288],
1502 );
1503 check(
1504 &x,
1505 20,
1506 3,
1507 None,
1508 NormMethod::Rle,
1509 None,
1510 None,
1511 &[2.60622175336887, 0.562126260530541, 0.682581887787085],
1512 );
1513 check(
1514 &x,
1515 20,
1516 3,
1517 None,
1518 NormMethod::UpperQuartile,
1519 None,
1520 None,
1521 &[1.25294073464069, 0.810726357708683, 0.984453434360544],
1522 );
1523 }
1524
1525 #[test]
1530 fn test_single_sample_degrades_to_no_normalisation() {
1531 let x = vec![1.0, 2.0, 3.0, 4.0, 5.0];
1532 let f = calc_norm_factors(&x, 5, 1, None, NormMethod::Tmm, None, None).unwrap();
1533 assert_eq!(f, vec![1.0]);
1534 }
1535
1536 #[test]
1537 fn test_all_zero_matrix_degrades_to_no_normalisation() {
1538 let x = vec![0.0_f64; 12];
1539 for method in [
1540 NormMethod::Tmm,
1541 NormMethod::TmmwSp,
1542 NormMethod::Rle,
1543 NormMethod::UpperQuartile,
1544 ] {
1545 let f = calc_norm_factors(&x, 4, 3, None, method, None, None);
1546 assert!(f.is_err());
1550 }
1551 let f = calc_norm_factors(
1552 &x,
1553 4,
1554 3,
1555 Some(&[10.0, 10.0, 10.0]),
1556 NormMethod::Tmm,
1557 None,
1558 None,
1559 )
1560 .unwrap();
1561 assert_eq!(f, vec![1.0; 3]);
1562 }
1563
1564 #[test]
1565 fn test_proportional_columns_give_exactly_one() {
1566 let mut x = Vec::new();
1568 for i in 1..=10 {
1569 x.push(10.0 * i as f64);
1570 x.push(20.0 * i as f64);
1571 }
1572 for method in [NormMethod::Tmm, NormMethod::TmmwSp, NormMethod::Rle] {
1573 let f = calc_norm_factors(&x, 10, 2, None, method, None, None).unwrap();
1574 assert_relative_eq!(f[0], 1.0, max_relative = 1e-14);
1575 assert_relative_eq!(f[1], 1.0, max_relative = 1e-14);
1576 }
1577 }
1578
1579 #[test]
1580 fn test_identical_columns_give_exactly_one() {
1581 let mut x = Vec::new();
1582 for i in 1..=10 {
1583 x.push(i as f64);
1584 x.push(i as f64);
1585 }
1586 for method in [NormMethod::Tmm, NormMethod::TmmwSp] {
1587 let f = calc_norm_factors(&x, 10, 2, None, method, None, None).unwrap();
1588 assert_eq!(f, vec![1.0, 1.0]);
1589 }
1590 }
1591
1592 #[test]
1593 fn test_factors_have_geometric_mean_one() {
1594 let (x, g, s) = read_csv("test_data_part1.csv");
1595 for method in [
1596 NormMethod::Tmm,
1597 NormMethod::TmmwSp,
1598 NormMethod::Rle,
1599 NormMethod::UpperQuartile,
1600 NormMethod::None,
1601 ] {
1602 let f = calc_norm_factors(&x, g, s, None, method, None, None).unwrap();
1603 let product: f64 = f.iter().product();
1604 assert_relative_eq!(product, 1.0, max_relative = 1e-12);
1605 }
1606 }
1607
1608 #[test]
1613 fn test_reference_column_is_the_one_nearest_the_mean_f75() {
1614 let (x, g, s) = read_csv("test_data_part1.csv");
1615 let lib = resolve_lib_size(&x, s, None).unwrap();
1616 let (cm, n_kept) = to_column_major_nonzero(&x, g, s).unwrap();
1617 let chosen = resolve_ref_column(&cm, n_kept, s, &lib, NormMethod::Tmm).unwrap();
1619 assert_eq!(chosen, 3);
1620 let wsp = resolve_ref_column(&cm, n_kept, s, &lib, NormMethod::TmmwSp).unwrap();
1622 assert_eq!(wsp, arg_max_sqrt_colsum(&cm, n_kept, s));
1623 }
1624
1625 #[test]
1626 fn test_explicit_reference_column_overrides_the_heuristic() {
1627 let (x, g, s) = read_csv("test_data_part1.csv");
1628 let auto = calc_norm_factors(&x, g, s, None, NormMethod::Tmm, None, None).unwrap();
1629 let pinned = calc_norm_factors(&x, g, s, None, NormMethod::Tmm, Some(3), None).unwrap();
1630 assert_eq!(auto, pinned);
1631 }
1632
1633 #[test]
1638 fn test_f32_counts_agree_with_f64() {
1639 let (x, g, s) = read_csv("test_data_part1.csv");
1640 let x32: Vec<f32> = x.iter().map(|v| *v as f32).collect();
1641 for method in [
1642 NormMethod::Tmm,
1643 NormMethod::TmmwSp,
1644 NormMethod::Rle,
1645 NormMethod::UpperQuartile,
1646 ] {
1647 let a = calc_norm_factors(&x, g, s, None, method, None, None).unwrap();
1648 let b = calc_norm_factors(&x32, g, s, None, method, None, None).unwrap();
1649 for (p, q) in a.iter().zip(b.iter()) {
1650 assert_relative_eq!(p, q, max_relative = 1e-12);
1651 }
1652 }
1653 }
1654
1655 #[test]
1656 fn test_transpose_drops_only_all_zero_genes() {
1657 let x = vec![1.0, 2.0, 0.0, 0.0, 3.0, 4.0];
1659 let (cm, n_kept) = to_column_major_nonzero(&x, 3, 2).unwrap();
1660 assert_eq!(n_kept, 2);
1661 assert_eq!(cm, vec![1.0, 3.0, 2.0, 4.0]);
1662 }
1663
1664 #[test]
1669 fn test_rejects_an_out_of_range_reference_column() {
1670 let x = vec![1.0_f64; 6];
1671 let err = calc_norm_factors(&x, 3, 2, None, NormMethod::Tmm, Some(2), None).unwrap_err();
1672 assert!(matches!(err, EdgeErrors::InvalidReferenceColumn(_)));
1673 }
1674
1675 #[test]
1676 fn test_rejects_a_library_size_of_the_wrong_length() {
1677 let x = vec![1.0_f64; 6];
1678 let err =
1679 calc_norm_factors(&x, 3, 2, Some(&[10.0]), NormMethod::Tmm, None, None).unwrap_err();
1680 assert!(matches!(
1681 err,
1682 EdgeErrors::LengthMismatch {
1683 name: "lib_size",
1684 ..
1685 }
1686 ));
1687 }
1688
1689 #[test]
1690 fn test_rejects_a_dead_sample() {
1691 let x = vec![5.0, 0.0, 4.0, 7.0, 0.0, 6.0, 3.0, 0.0, 2.0, 9.0, 0.0, 8.0];
1693 let err = calc_norm_factors(&x, 4, 3, None, NormMethod::Tmm, None, None).unwrap_err();
1694 assert!(matches!(err, EdgeErrors::InvalidArgument(_)));
1695 }
1696
1697 #[test]
1698 fn test_rejects_a_shape_mismatch_and_empty_counts() {
1699 let err =
1700 calc_norm_factors(&[1.0_f64; 5], 3, 2, None, NormMethod::Tmm, None, None).unwrap_err();
1701 assert!(matches!(
1702 err,
1703 EdgeErrors::LengthMismatch { name: "counts", .. }
1704 ));
1705 let err =
1706 calc_norm_factors::<f64>(&[], 0, 2, None, NormMethod::Tmm, None, None).unwrap_err();
1707 assert!(matches!(err, EdgeErrors::EmptyCounts { .. }));
1708 }
1709
1710 #[test]
1711 fn test_rejects_negative_counts() {
1712 let x = vec![1.0, 2.0, -3.0, 4.0];
1713 let err = calc_norm_factors(&x, 2, 2, None, NormMethod::Tmm, None, None).unwrap_err();
1714 assert!(matches!(err, EdgeErrors::InvalidArgument(_)));
1715 }
1716
1717 #[test]
1718 fn test_rejects_parameters_outside_their_domain() {
1719 let x = vec![1.0_f64; 6];
1720 for params in [
1721 NormParams {
1722 logratio_trim: 1.0,
1723 ..Default::default()
1724 },
1725 NormParams {
1726 sum_trim: -0.1,
1727 ..Default::default()
1728 },
1729 NormParams {
1730 quantile: 1.5,
1731 ..Default::default()
1732 },
1733 ] {
1734 let err =
1735 calc_norm_factors(&x, 3, 2, None, NormMethod::Tmm, None, Some(params)).unwrap_err();
1736 assert!(matches!(err, EdgeErrors::InvalidArgument(_)));
1737 }
1738 }
1739
1740 #[test]
1741 fn test_upper_quartile_rejects_a_zero_quantile() {
1742 let mut x = vec![0.0_f64; 16];
1745 for g in 0..8 {
1746 x[g * 2 + 1] = 1.0;
1747 }
1748 x[7 * 2] = 10.0;
1749 let err =
1750 calc_norm_factors(&x, 8, 2, None, NormMethod::UpperQuartile, None, None).unwrap_err();
1751 assert!(matches!(err, EdgeErrors::InvalidArgument(_)));
1752 }
1753
1754 #[test]
1755 fn test_rle_rejects_a_matrix_with_no_gene_positive_everywhere() {
1756 let x = vec![1.0, 0.0, 0.0, 2.0, 3.0, 0.0, 0.0, 4.0];
1758 let err = calc_norm_factors(&x, 4, 2, None, NormMethod::Rle, None, None).unwrap_err();
1759 assert!(matches!(err, EdgeErrors::NoGenesAfterFiltering { .. }));
1760 }
1761}