1use anyhow::{bail, Result};
15use ndarray::{Array1, Array2, Axis};
16
17use crate::arrayweights::array_weights_gene_by_gene;
18use crate::fit::{lmfit, lmfit_weighted, MArrayLM};
19use crate::lowess::LowessInterpolator;
20
21pub fn choose_lowess_span(n: usize, small_n: f64, min_span: f64, power: f64) -> f64 {
24 (min_span + (1.0 - min_span) * (small_n / n as f64).powf(power)).min(1.0)
25}
26
27#[derive(Clone, Debug)]
30pub struct VoomOutput {
31 pub e: Array2<f64>, pub weights: Array2<f64>, pub design: Array2<f64>,
34 pub lib_size: Array1<f64>,
35 pub span: f64,
36}
37
38#[derive(Clone, Debug)]
41pub struct VoomaOutput {
42 pub weights: Array2<f64>, pub design: Array2<f64>,
44 pub span: f64,
45}
46
47pub fn voom(
58 counts: &Array2<f64>,
59 design: Option<&Array2<f64>>,
60 lib_size: Option<&Array1<f64>>,
61 span: f64,
62 adaptive_span: bool,
63) -> Result<VoomOutput> {
64 voom_weighted(counts, design, lib_size, span, adaptive_span, None)
65}
66
67pub(crate) fn voom_weighted(
74 counts: &Array2<f64>,
75 design: Option<&Array2<f64>>,
76 lib_size: Option<&Array1<f64>>,
77 span: f64,
78 adaptive_span: bool,
79 array_weights: Option<&Array1<f64>>,
80) -> Result<VoomOutput> {
81 let ngenes = counts.nrows();
82 let nsamples = counts.ncols();
83 if ngenes < 2 {
84 bail!("Need at least two genes to fit a mean-variance trend");
85 }
86 let mut min_count = f64::INFINITY;
87 for &c in counts.iter() {
88 if c.is_nan() {
89 bail!("NA counts not allowed");
90 }
91 min_count = min_count.min(c);
92 }
93 if min_count < 0.0 {
94 bail!("Negative counts not allowed");
95 }
96
97 let design_owned;
98 let design = match design {
99 Some(d) => {
100 if d.nrows() != nsamples {
101 bail!(
102 "design rows ({}) does not match number of samples ({})",
103 d.nrows(),
104 nsamples
105 );
106 }
107 d
108 }
109 None => {
110 design_owned = Array2::<f64>::ones((nsamples, 1));
111 &design_owned
112 }
113 };
114 let p = design.ncols();
115
116 let lib_size: Array1<f64> = match lib_size {
117 Some(l) => {
118 if l.len() != nsamples {
119 bail!(
120 "lib_size length ({}) does not match number of samples ({})",
121 l.len(),
122 nsamples
123 );
124 }
125 l.clone()
126 }
127 None => counts.sum_axis(Axis(0)),
128 };
129
130 let span = if adaptive_span {
131 choose_lowess_span(ngenes, 50.0, 0.3, 1.0 / 3.0)
132 } else {
133 span
134 };
135
136 let mut y = Array2::<f64>::zeros((ngenes, nsamples));
138 for g in 0..ngenes {
139 for s in 0..nsamples {
140 y[[g, s]] = ((counts[[g, s]] + 0.5) / (lib_size[s] + 1.0) * 1e6).log2();
141 }
142 }
143
144 let gene_names: Vec<String> = (0..ngenes).map(|i| i.to_string()).collect();
145 let coef_names: Vec<String> = (0..p).map(|j| j.to_string()).collect();
146 let fit = match array_weights {
147 Some(aw) => {
148 let mut wmat = Array2::<f64>::zeros((ngenes, nsamples));
149 for g in 0..ngenes {
150 for s in 0..nsamples {
151 wmat[[g, s]] = aw[s];
152 }
153 }
154 lmfit_weighted(&y, design, &wmat, gene_names, coef_names)?
155 }
156 None => lmfit(&y, design, gene_names, coef_names)?,
157 };
158
159 let n_with_reps = fit.df_residual.iter().filter(|&&d| d > 0.0).count();
161 if n_with_reps < 2 {
162 return Ok(VoomOutput {
163 e: y,
164 weights: Array2::ones((ngenes, nsamples)),
165 design: design.clone(),
166 lib_size,
167 span,
168 });
169 }
170
171 let mean_log_lib = lib_size.iter().map(|&l| (l + 1.0).log2()).sum::<f64>() / nsamples as f64;
173 let log2_1e6 = 1e6_f64.log2();
174 let sx_all: Vec<f64> = (0..ngenes)
175 .map(|g| fit.amean[g] + mean_log_lib - log2_1e6)
176 .collect();
177 let sy_all: Vec<f64> = (0..ngenes).map(|g| fit.sigma[g].sqrt()).collect();
178
179 let (sx, sy): (Vec<f64>, Vec<f64>) = (0..ngenes)
182 .filter(|&g| counts.row(g).sum() != 0.0)
183 .map(|g| (sx_all[g], sy_all[g]))
184 .unzip();
185 let trend = LowessInterpolator::fit(&sx, &sy, span, 3);
186
187 let fitted = fit.coefficients.dot(&design.t()); let mut weights = Array2::<f64>::zeros((ngenes, nsamples));
190 for g in 0..ngenes {
191 for s in 0..nsamples {
192 let fitted_cpm = 2f64.powf(fitted[[g, s]]);
193 let fitted_count = 1e-6 * fitted_cpm * (lib_size[s] + 1.0);
194 let pred = trend.eval(fitted_count.log2());
195 weights[[g, s]] = 1.0 / pred.powi(4);
196 }
197 }
198
199 Ok(VoomOutput {
200 e: y,
201 weights,
202 design: design.clone(),
203 lib_size,
204 span,
205 })
206}
207
208#[derive(Clone, Debug)]
211pub struct VoomQualityWeights {
212 pub e: Array2<f64>, pub weights: Array2<f64>, pub design: Array2<f64>,
215 pub lib_size: Array1<f64>,
216 pub span: f64,
217 pub sample_weights: Array1<f64>, }
219
220pub fn voom_with_quality_weights(
235 counts: &Array2<f64>,
236 design: Option<&Array2<f64>>,
237 lib_size: Option<&Array1<f64>>,
238 span: f64,
239 adaptive_span: bool,
240 var_design: Option<&Array2<f64>>,
241 prior_n: f64,
242) -> Result<VoomQualityWeights> {
243 let v1 = voom_weighted(counts, design, lib_size, span, adaptive_span, None)?;
245
246 let aw1 = array_weights_gene_by_gene(&v1.e, &v1.design, Some(&v1.weights), var_design, prior_n);
250
251 let v2 = voom_weighted(counts, design, lib_size, span, adaptive_span, Some(&aw1))?;
253
254 let aw2 = array_weights_gene_by_gene(&v2.e, &v2.design, Some(&v2.weights), var_design, prior_n);
256
257 let mut weights = v2.weights;
259 let (ng, ns) = weights.dim();
260 for g in 0..ng {
261 for s in 0..ns {
262 weights[[g, s]] *= aw2[s];
263 }
264 }
265
266 Ok(VoomQualityWeights {
267 e: v2.e,
268 weights,
269 design: v2.design,
270 lib_size: v2.lib_size,
271 span: v2.span,
272 sample_weights: aw2,
273 })
274}
275
276pub fn vooma(
287 y: &Array2<f64>,
288 design: Option<&Array2<f64>>,
289 span: Option<f64>,
290 legacy_span: bool,
291) -> Result<VoomaOutput> {
292 let ngenes = y.nrows();
293 let nsamples = y.ncols();
294 if ngenes < 1 {
295 bail!("y has no rows");
296 }
297
298 let design_owned;
299 let design = match design {
300 Some(d) => {
301 if d.nrows() != nsamples {
302 bail!(
303 "design rows ({}) does not match number of samples ({})",
304 d.nrows(),
305 nsamples
306 );
307 }
308 d
309 }
310 None => {
311 design_owned = Array2::<f64>::ones((nsamples, 1));
312 &design_owned
313 }
314 };
315 let p = design.ncols();
316
317 let gene_names: Vec<String> = (0..ngenes).map(|i| i.to_string()).collect();
318 let coef_names: Vec<String> = (0..p).map(|j| j.to_string()).collect();
319 let fit = lmfit(y, design, gene_names, coef_names)?;
320
321 if fit.amean.iter().any(|a| a.is_nan()) {
322 bail!("y contains entirely NA rows");
323 }
324
325 let sx: Vec<f64> = fit.amean.to_vec();
326 let sy: Vec<f64> = (0..ngenes).map(|g| fit.sigma[g].sqrt()).collect();
327
328 let span = match span {
329 Some(s) => s,
330 None => {
331 if legacy_span {
332 choose_lowess_span(ngenes, 10.0, 0.3, 0.5)
333 } else {
334 choose_lowess_span(ngenes, 50.0, 0.3, 1.0 / 3.0)
335 }
336 }
337 };
338 let trend = LowessInterpolator::fit(&sx, &sy, span, 3);
339
340 let mu = fit.coefficients.dot(&design.t()); let mut weights = Array2::<f64>::zeros((ngenes, nsamples));
342 for g in 0..ngenes {
343 for s in 0..nsamples {
344 weights[[g, s]] = 1.0 / trend.eval(mu[[g, s]]).powi(4);
345 }
346 }
347
348 Ok(VoomaOutput {
349 weights,
350 design: design.clone(),
351 span,
352 })
353}
354
355#[derive(Clone, Debug)]
357pub struct VoomaByGroupOutput {
358 pub weights: Array2<f64>,
361 pub design: Array2<f64>,
363 pub span: f64,
366}
367
368fn drop_zero_columns(m: &Array2<f64>) -> Array2<f64> {
373 let keep: Vec<usize> = (0..m.ncols())
374 .filter(|&j| m.column(j).iter().any(|&v| v != 0.0))
375 .collect();
376 m.select(Axis(1), &keep)
377}
378
379pub fn vooma_by_group(
397 y: &Array2<f64>,
398 group: &[usize],
399 design: Option<&Array2<f64>>,
400 span: Option<f64>,
401 legacy_span: bool,
402) -> Result<VoomaByGroupOutput> {
403 let ngenes = y.nrows();
404 let narrays = y.ncols();
405 if group.len() != narrays {
406 bail!(
407 "length of group ({}) must equal number of samples ({})",
408 group.len(),
409 narrays
410 );
411 }
412
413 let mut levels: Vec<usize> = group.to_vec();
415 levels.sort_unstable();
416 levels.dedup();
417 let ngroups = levels.len();
418 let intgroup: Vec<usize> = group
419 .iter()
420 .map(|g| levels.iter().position(|l| l == g).unwrap())
421 .collect();
422
423 let design: Array2<f64> = match design {
425 Some(d) => {
426 if d.nrows() != narrays {
427 bail!(
428 "design rows ({}) does not match number of samples ({})",
429 d.nrows(),
430 narrays
431 );
432 }
433 d.clone()
434 }
435 None => {
436 let mut ind = Array2::<f64>::zeros((narrays, ngroups));
437 for (s, &gi) in intgroup.iter().enumerate() {
438 ind[[s, gi]] = 1.0;
439 }
440 ind
441 }
442 };
443
444 let mut cols_of: Vec<Vec<usize>> = vec![Vec::new(); ngroups];
446 for (s, &gi) in intgroup.iter().enumerate() {
447 cols_of[gi].push(s);
448 }
449 let has_singleton = cols_of.iter().any(|c| c.len() == 1);
450
451 let voomall = if has_singleton {
453 Some(vooma(y, Some(&design), span, legacy_span)?)
454 } else {
455 None
456 };
457
458 let mut weights = Array2::<f64>::zeros((ngenes, narrays));
459 let mut last_span: Option<f64> = None;
460 for cols in &cols_of {
461 if cols.len() == 1 {
462 let va = voomall.as_ref().unwrap();
463 let s = cols[0];
464 for g in 0..ngenes {
465 weights[[g, s]] = va.weights[[g, s]];
466 }
467 } else {
468 let yi = y.select(Axis(1), cols);
469 let di = drop_zero_columns(&design.select(Axis(0), cols));
470 let voomi = vooma(&yi, Some(&di), span, legacy_span)?;
471 for (k, &s) in cols.iter().enumerate() {
472 for g in 0..ngenes {
473 weights[[g, s]] = voomi.weights[[g, k]];
474 }
475 }
476 last_span = Some(voomi.span);
477 }
478 }
479
480 let out_span = last_span
481 .or_else(|| voomall.as_ref().map(|v| v.span))
482 .unwrap_or(0.0);
483
484 Ok(VoomaByGroupOutput {
485 weights,
486 design,
487 span: out_span,
488 })
489}
490
491#[derive(Clone, Debug)]
493pub struct VoomaLmFit {
494 pub fit: MArrayLM,
496 pub weights: Array2<f64>,
498 pub span: f64,
500}
501
502pub fn vooma_lm_fit(
512 y: &Array2<f64>,
513 design: Option<&Array2<f64>>,
514 prior_weights: Option<&Array2<f64>>,
515 span: Option<f64>,
516 legacy_span: bool,
517 gene_names: Vec<String>,
518 coef_names: Vec<String>,
519) -> Result<VoomaLmFit> {
520 let ngenes = y.nrows();
521 let nsamples = y.ncols();
522 if nsamples < 2 {
523 bail!("Too few samples");
524 }
525 if ngenes < 2 {
526 bail!("Need multiple rows");
527 }
528
529 let design_owned;
530 let design = match design {
531 Some(d) => {
532 if d.nrows() != nsamples {
533 bail!(
534 "design rows ({}) does not match number of samples ({})",
535 d.nrows(),
536 nsamples
537 );
538 }
539 d
540 }
541 None => {
542 design_owned = Array2::<f64>::ones((nsamples, 1));
543 &design_owned
544 }
545 };
546
547 if let Some(pw) = prior_weights {
548 if pw.nrows() != ngenes || pw.ncols() != nsamples {
549 bail!(
550 "prior_weights dimensions ({}x{}) must match y ({}x{})",
551 pw.nrows(),
552 pw.ncols(),
553 ngenes,
554 nsamples
555 );
556 }
557 }
558
559 let fit0 = match prior_weights {
561 Some(pw) => lmfit_weighted(y, design, pw, gene_names.clone(), coef_names.clone())?,
562 None => lmfit(y, design, gene_names.clone(), coef_names.clone())?,
563 };
564 if fit0.amean.iter().any(|a| a.is_nan()) {
565 bail!("y contains entirely NA rows");
566 }
567
568 let sx: Vec<f64> = fit0.amean.to_vec();
570 let sy: Vec<f64> = (0..ngenes).map(|g| fit0.sigma[g].sqrt()).collect();
571 let span = match span {
572 Some(s) => s,
573 None => {
574 if legacy_span {
575 choose_lowess_span(ngenes, 10.0, 0.3, 0.5)
576 } else {
577 choose_lowess_span(ngenes, 50.0, 0.3, 1.0 / 3.0)
578 }
579 }
580 };
581 let trend = LowessInterpolator::fit(&sx, &sy, span, 3);
582
583 let mu = fit0.coefficients.dot(&design.t());
585 let mut weights = Array2::<f64>::zeros((ngenes, nsamples));
586 for g in 0..ngenes {
587 for s in 0..nsamples {
588 weights[[g, s]] = 1.0 / trend.eval(mu[[g, s]]).powi(4);
589 }
590 }
591 if let Some(pw) = prior_weights {
592 weights = &weights * pw;
593 }
594
595 let fit = lmfit_weighted(y, design, &weights, gene_names, coef_names)?;
596 Ok(VoomaLmFit { fit, weights, span })
597}
598
599#[cfg(test)]
600#[allow(clippy::excessive_precision)]
601mod tests {
602 use super::*;
603 use ndarray::array;
604
605 fn counts_fixture() -> Array2<f64> {
606 array![
607 [10., 12., 8., 11.],
608 [100., 110., 95., 105.],
609 [5., 7., 6., 4.],
610 [200., 190., 210., 205.],
611 [50., 45., 55., 60.],
612 [1., 2., 0., 3.],
613 [1000., 1100., 900., 950.],
614 [30., 25., 35., 40.],
615 ]
616 }
617
618 fn design_fixture() -> Array2<f64> {
619 array![[1., 0.], [1., 0.], [1., 1.], [1., 1.]]
621 }
622
623 #[test]
624 fn choose_span_matches_r() {
625 assert_eq!(choose_lowess_span(8, 50.0, 0.3, 1.0 / 3.0), 1.0);
627 let s = choose_lowess_span(1000, 50.0, 0.3, 1.0 / 3.0);
629 let expect = 0.3 + 0.7 * (50.0_f64 / 1000.0).powf(1.0 / 3.0);
630 assert!((s - expect).abs() < 1e-12 && s < 1.0);
631 }
632
633 #[test]
634 fn voom_matches_r() {
635 let counts = counts_fixture();
636 let design = design_fixture();
637 let v = voom(&counts, Some(&design), None, 0.5, false).unwrap();
638
639 let lib = array![1396.0, 1491.0, 1309.0, 1378.0];
641 for (a, b) in v.lib_size.iter().zip(lib.iter()) {
642 assert!((a - b).abs() < 1e-9);
643 }
644
645 for g in 0..counts.nrows() {
647 for s in 0..counts.ncols() {
648 let expect = ((counts[[g, s]] + 0.5) / (lib[s] + 1.0) * 1e6).log2();
649 assert!((v.e[[g, s]] - expect).abs() < 1e-9);
650 }
651 }
652
653 let want = array![
655 [
656 28.935889399719,
657 28.935889399719,
658 10.326303259626,
659 17.013134202459
660 ],
661 [
662 78.797663894764,
663 80.528056879469,
664 77.731587915647,
665 79.054873411272
666 ],
667 [
668 0.641934244773,
669 0.877223560596,
670 0.507411168766,
671 0.507411168766
672 ],
673 [
674 97.087653321213,
675 99.514156468690,
676 100.012320428675,
677 107.314600669557
678 ],
679 [
680 50.936936306788,
681 57.095789543676,
682 65.164440442125,
683 66.225455485966
684 ],
685 [
686 0.507411168766,
687 0.507411168766,
688 0.507411168766,
689 0.507411168766
690 ],
691 [
692 2193.696131719829,
693 2193.696131719829,
694 1703.605796348448,
695 1968.434108844519
696 ],
697 [
698 28.935889399719,
699 28.935889399719,
700 35.153539352782,
701 38.113189099620
702 ],
703 ];
704 for (a, b) in v.weights.iter().zip(want.iter()) {
705 let rel = (a - b).abs() / b.abs();
706 assert!(rel < 1e-9, "weight {a} vs {b} (rel {rel:e})");
707 }
708 }
709
710 #[test]
711 fn vooma_matches_r() {
712 let y = array![
714 [5.10, 4.80, 6.20, 5.50],
715 [2.30, 3.10, 2.80, 3.50],
716 [7.70, 7.20, 8.10, 6.90],
717 [1.10, 0.90, 1.40, 1.20],
718 [9.30, 9.10, 8.80, 9.50],
719 [4.40, 4.90, 5.20, 4.10],
720 [6.60, 6.20, 6.90, 6.40],
721 [3.30, 3.80, 3.10, 2.90],
722 [8.10, 7.60, 8.40, 8.00],
723 [0.50, 1.20, 0.80, 0.30],
724 ];
725 let design = design_fixture();
726 let v = vooma(&y, Some(&design), None, false).unwrap();
727 assert_eq!(v.span, 1.0); let want = array![
730 [
731 5.769009938122,
732 5.769009938122,
733 5.710681439260,
734 5.710681439260
735 ],
736 [
737 7.764204399417,
738 7.764204399417,
739 7.299187981925,
740 7.299187981925
741 ],
742 [
743 6.131523915696,
744 6.131523915696,
745 6.146966664045,
746 6.146966664045
747 ],
748 [
749 10.027067353155,
750 10.027067353155,
751 9.549770567334,
752 9.549770567334
753 ],
754 [
755 6.732012491201,
756 6.732012491201,
757 6.722541890163,
758 6.722541890163
759 ],
760 [
761 5.873692933054,
762 5.873692933054,
763 5.873692933054,
764 5.873692933054
765 ],
766 [
767 5.828720439884,
768 5.828720439884,
769 5.892290676457,
770 5.892290676457
771 ],
772 [
773 6.892732120849,
774 6.892732120849,
775 7.443835424811,
776 7.443835424811
777 ],
778 [
779 6.257090085442,
780 6.257090085442,
781 6.374681498763,
782 6.374681498763
783 ],
784 [
785 10.292360128274,
786 10.292360128274,
787 10.566485009140,
788 10.566485009140
789 ],
790 ];
791 for (a, b) in v.weights.iter().zip(want.iter()) {
792 let rel = (a - b).abs() / b.abs();
793 assert!(rel < 1e-9, "weight {a} vs {b} (rel {rel:e})");
794 }
795 }
796
797 fn rclose(a: f64, b: f64) -> bool {
798 (a - b).abs() <= 1e-7 * (1.0 + b.abs())
799 }
800
801 fn vlf_fixture() -> (Array2<f64>, Array2<f64>, Array2<f64>) {
804 let ngenes = 50usize;
805 let narrays = 6usize;
806 let group = [0.0, 0.0, 0.0, 1.0, 1.0, 1.0];
807 let mut y = Array2::<f64>::zeros((ngenes, narrays));
808 let mut pw = Array2::<f64>::zeros((ngenes, narrays));
809 for g in 0..ngenes {
810 let gi = g as i64;
811 let base = ((gi % 7) - 3) as f64;
812 let extra = (((gi * 5) % 11) - 5) as f64;
813 let lvl = 5.0 + (gi % 10) as f64 * 0.2;
814 let eff = base * 0.25;
815 for k in 0..narrays {
816 let ki = k as i64;
817 let noise = (((gi * 13 + ki * 17) % 19) - 9) as f64 * 0.04;
818 y[[g, k]] = lvl + eff * group[k] + extra * 0.05 + noise;
819 pw[[g, k]] = 0.6 + (((gi * 3 + ki * 7) % 9) as f64) * 0.1;
820 }
821 }
822 let mut design = Array2::<f64>::zeros((narrays, 2));
823 for k in 0..narrays {
824 design[[k, 0]] = 1.0;
825 design[[k, 1]] = group[k];
826 }
827 (y, design, pw)
828 }
829
830 fn vwqw_counts_fixture() -> (Array2<f64>, Array2<f64>) {
833 let ngenes = 10usize;
834 let narrays = 6usize;
835 let grp = [0i64, 0, 0, 1, 1, 1];
836 let mut counts = Array2::<f64>::zeros((ngenes, narrays));
837 for g in 0..ngenes {
838 let gi = g as i64;
839 for j in 0..narrays {
840 let ji = j as i64;
841 let c = 20 + gi * 15 + ji * 3 + ((gi * 5 + ji * 7) % 13) * 4 + grp[j] * gi * 2;
842 counts[[g, j]] = c as f64;
843 }
844 }
845 let mut design = Array2::<f64>::zeros((narrays, 2));
846 for j in 0..narrays {
847 design[[j, 0]] = 1.0;
848 design[[j, 1]] = grp[j] as f64;
849 }
850 (counts, design)
851 }
852
853 #[test]
854 fn voom_with_quality_weights_matches_r() {
855 let (counts, design) = vwqw_counts_fixture();
856 let v = voom_with_quality_weights(&counts, Some(&design), None, 0.5, false, None, 10.0)
857 .unwrap();
858
859 let aw_want = [
861 1.1428522224351862,
862 0.69948202996388553,
863 1.1916899290606626,
864 1.1563249275179759,
865 0.74938331550111592,
866 1.2113961366163049,
867 ];
868 for (g, x) in v.sample_weights.iter().zip(aw_want.iter()) {
869 assert!(rclose(*g, *x), "sample weight: got {g}, want {x}");
870 }
871
872 assert!(rclose(v.weights[[0, 0]], 4.6250384357077206));
874 assert!(rclose(v.weights[[0, 3]], 9.0644002874417744));
875 assert!(rclose(v.weights[[0, 5]], 10.34938541502423));
876 assert!(rclose(v.weights[[4, 2]], 28.109033884186356));
877 assert!(rclose(v.weights[[5, 1]], 16.86267872361735));
878 assert!(rclose(v.weights[[9, 0]], 75.782464040995038));
879 assert!(rclose(v.weights[[9, 5]], 117.79877846147826));
880
881 assert!(rclose(v.e[[0, 0]], 14.185832765530236));
883 assert!(rclose(v.e[[9, 5]], 17.166249779108728));
884 }
885
886 #[test]
887 fn vooma_lm_fit_matches_r() {
888 let (y, design, pw) = vlf_fixture();
889 let gn: Vec<String> = (0..50).map(|i| format!("g{i}")).collect();
890 let cn = vec!["Intercept".to_string(), "group".to_string()];
891
892 let r = vooma_lm_fit(&y, Some(&design), None, None, false, gn.clone(), cn.clone()).unwrap();
894 assert_eq!(r.span, 1.0); assert!(rclose(r.fit.coefficients[[0, 0]], 4.8166666666666673));
896 assert!(rclose(r.fit.coefficients[[0, 1]], -0.73666666666666669));
897 assert!(rclose(r.fit.coefficients[[2, 1]], 0.016666666666666802));
898 assert!(rclose(r.fit.sigma[0], 2.0001523415288656));
899 assert!(rclose(r.fit.sigma[4], 0.50739386270805098));
900 assert!(rclose(r.fit.stdev_unscaled[[0, 0]], 0.07765105731999547));
901 assert!(rclose(r.fit.stdev_unscaled[[0, 1]], 0.10897521719515758));
902 assert_eq!(r.fit.df_residual[0], 4.0);
903 assert!(rclose(r.weights[[0, 0]], 55.282032012091854));
905 assert!(rclose(r.weights[[0, 3]], 57.019909902580309));
906 assert!(rclose(r.weights[[24, 0]], 43.694024586743168));
907 assert!(rclose(r.weights[[49, 0]], 33.652368280339203));
908 assert!(rclose(r.weights[[49, 3]], 48.718872300466991));
909
910 let r2 = vooma_lm_fit(&y, Some(&design), Some(&pw), None, false, gn, cn).unwrap();
912 assert_eq!(r2.span, 1.0);
913 assert!(rclose(r2.fit.coefficients[[0, 0]], 4.9046666666666683));
914 assert!(rclose(r2.fit.coefficients[[0, 1]], -0.83800000000000119));
915 assert!(rclose(r2.fit.sigma[0], 1.8170323609512815));
916 assert!(rclose(r2.fit.stdev_unscaled[[0, 0]], 0.07413980745014162));
917 assert!(rclose(r2.weights[[0, 0]], 36.385394507083909));
918 assert!(rclose(r2.weights[[0, 5]], 91.56829234031899));
919 assert!(rclose(r2.weights[[49, 0]], 30.459503135870523));
920 }
921
922 #[test]
923 fn vooma_by_group_matches_r() {
924 let ngenes = 12usize;
926 let mky = |narrays: usize| -> Array2<f64> {
927 Array2::from_shape_fn((ngenes, narrays), |(g0, s0)| {
928 5.0 + 0.25 * g0 as f64 + 0.1 * s0 as f64 + 0.15 * ((g0 * (s0 + 1)) % 5) as f64
929 })
930 };
931 let rclose = |a: f64, b: f64| (a - b).abs() <= 1e-7 * (1.0 + b.abs());
932
933 let gw = array![
936 [128.57974235629533, 201.43265373150086, 169.52805974815644],
937 [51.72862401222929, 18.636960776554076, 37.75223112586292],
938 [37.559193159543049, 20.439481407168092, 13.083239709395334],
939 [37.614806241386034, 146.28421635333993, 8.9183551719300596],
940 [106.95854312349614, 172.92065689564916, 12.144674253781014],
941 [69.276054650291854, 206.54103286812787, 10.92966941384552],
942 [63.332917660356472, 18.636960776554002, 15.654008874403512],
943 [32.388566989657896, 20.439481407168017, 12.99773820977671],
944 [37.212711780045019, 146.28421635334118, 9.8481582955355105],
945 [100.30030326001432, 172.92065689564888, 15.212910649940969],
946 [70.172943221746678, 206.54103286812716, 14.112001483913859],
947 [124.95596005142109, 32.391260048340605, 24.542006411260687],
948 ];
949
950 let ya = mky(6);
952 let group_a = [0usize, 0, 1, 1, 2, 2];
953 let va = vooma_by_group(&ya, &group_a, None, Some(0.5), false).unwrap();
954 assert!((va.span - 0.5).abs() < 1e-12);
955 for ((g, s), _) in va.weights.indexed_iter() {
956 let e = gw[[g, group_a[s]]];
957 assert!(rclose(va.weights[[g, s]], e), "A[{g},{s}]");
958 }
959
960 let single_b = [
962 92.423854171536973,
963 46.676048745081182,
964 36.257599360043535,
965 52.633157173375849,
966 97.417756778627549,
967 67.100924018777832,
968 43.554031111104514,
969 36.052167162893426,
970 52.218302840608807,
971 99.089796796342043,
972 71.514137307408575,
973 49.250767908710742,
974 ];
975 let yb = mky(5);
976 let group_b = [0usize, 0, 1, 1, 2];
977 let vb = vooma_by_group(&yb, &group_b, None, Some(0.5), false).unwrap();
978 assert!((vb.span - 0.5).abs() < 1e-12);
979 for g in 0..ngenes {
980 assert!(rclose(vb.weights[[g, 0]], gw[[g, 0]]), "B[{g},0]");
982 assert!(rclose(vb.weights[[g, 1]], gw[[g, 0]]), "B[{g},1]");
983 assert!(rclose(vb.weights[[g, 2]], gw[[g, 1]]), "B[{g},2]");
984 assert!(rclose(vb.weights[[g, 3]], gw[[g, 1]]), "B[{g},3]");
985 assert!(rclose(vb.weights[[g, 4]], single_b[g]), "B[{g},4]");
986 }
987 }
988}