1#![allow(clippy::needless_range_loop)]
39
40use crate::errors::EdgeErrors;
41use crate::sc::zeros::ZeroCells;
42
43const DEFAULT_MAX_ITER: usize = 50;
51
52const DEFAULT_MAX_BACKTRACK: usize = 10;
56
57const DEFAULT_EPS: f64 = 1e-6;
60
61const GRADIENT_TOLERANCE: f64 = 0.01;
66
67const STEP_CUTOFF: f64 = 40.0;
73
74const EIGENVALUE_CUTOFF: f64 = 1e-8;
79
80const JACOBI_SWEEPS: usize = 30;
85
86pub const CONV_SUCCESS: i32 = 1;
88
89pub const CONV_CRITICAL_POINT: i32 = -10;
92
93pub const CONV_MAX_ITER: i32 = -20;
95
96pub const CONV_SINGULAR: i32 = -25;
98
99pub const CONV_NOT_FINITE: i32 = -30;
101
102pub const CONV_STALLED: i32 = -40;
104
105pub const CONV_AT_BOUND: i32 = -60;
110
111#[derive(Clone, Copy, Debug)]
121pub struct PmlData<'a> {
122 pub design: &'a [f64],
124 pub offset: &'a [f64],
126 pub counts: &'a [f64],
128 pub cell_index: &'a [usize],
130 pub subject_start: &'a [usize],
132 pub subject_total: &'a [f64],
134}
135
136impl PmlData<'_> {
137 pub fn n_cells(&self) -> usize {
143 self.offset.len()
144 }
145
146 pub fn n_subjects(&self) -> usize {
152 self.subject_start.len().saturating_sub(1)
153 }
154
155 pub fn n_beta(&self) -> usize {
162 let n = self.n_cells();
163 self.design.len().checked_div(n).unwrap_or(0)
164 }
165
166 fn validate(&self) -> Result<(), EdgeErrors> {
172 let n_cells = self.n_cells();
173 if n_cells == 0 {
174 return Err(EdgeErrors::MustBePositive("n_cells".to_string()));
175 }
176 let k = self.n_subjects();
177 if k == 0 {
178 return Err(EdgeErrors::MustBePositive("n_subjects".to_string()));
179 }
180 let n_beta = self.n_beta();
181 if n_beta == 0 || n_beta * n_cells != self.design.len() {
182 return Err(EdgeErrors::LengthMismatch {
183 name: "design",
184 expected: n_cells * n_beta.max(1),
185 got: self.design.len(),
186 });
187 }
188 if self.counts.len() != self.cell_index.len() {
189 return Err(EdgeErrors::LengthMismatch {
190 name: "cell_index",
191 expected: self.counts.len(),
192 got: self.cell_index.len(),
193 });
194 }
195 if self.subject_total.len() != k {
196 return Err(EdgeErrors::LengthMismatch {
197 name: "subject_total",
198 expected: k,
199 got: self.subject_total.len(),
200 });
201 }
202 if self.subject_start[0] != 0 || self.subject_start[k] != n_cells {
203 return Err(EdgeErrors::InvalidArgument(format!(
204 "subject_start must run from 0 to {n_cells}; got {} to {}.",
205 self.subject_start[0], self.subject_start[k]
206 )));
207 }
208 for s in 0..k {
209 if self.subject_start[s] >= self.subject_start[s + 1] {
210 return Err(EdgeErrors::SubjectsNotContiguous { subject: s });
211 }
212 }
213 let mut previous: Option<usize> = None;
214 for &c in self.cell_index {
215 if c >= n_cells {
216 return Err(EdgeErrors::InvalidArgument(format!(
217 "cell_index {c} is out of range for {n_cells} cells."
218 )));
219 }
220 if previous.is_some_and(|p| c <= p) {
221 return Err(EdgeErrors::InvalidArgument(
222 "cell_index must be strictly increasing.".to_string(),
223 ));
224 }
225 previous = Some(c);
226 }
227 Ok(())
228 }
229}
230
231#[derive(Clone, Copy, Debug)]
240pub struct PmlVariance {
241 pub subject: f64,
249 pub cell: f64,
252}
253
254#[derive(Clone, Copy, Debug)]
260pub struct PmlParams {
261 pub reml: bool,
266 pub eps: f64,
269 pub ord: u32,
275 pub max_iter: usize,
277 pub max_backtrack: usize,
279}
280
281impl Default for PmlParams {
282 fn default() -> Self {
284 Self {
285 reml: false,
286 eps: DEFAULT_EPS,
287 ord: 1,
288 max_iter: DEFAULT_MAX_ITER,
289 max_backtrack: DEFAULT_MAX_BACKTRACK,
290 }
291 }
292}
293
294#[derive(Clone, Debug)]
300pub struct PmlResult {
301 pub beta: Vec<f64>,
303 pub log_w: Vec<f64>,
305 pub information: Vec<f64>,
311 pub log_likelihood: f64,
313 pub log_likelihood_prev: f64,
317 pub log_det: f64,
322 pub second_order: f64,
327 pub convergence: i32,
333 pub iterations: usize,
335 pub backtracks: usize,
338}
339
340#[derive(Clone, Debug)]
345pub struct PmlObjective {
346 pub objective: f64,
348 pub gradient: Vec<f64>,
351}
352
353#[derive(Clone, Copy, Debug)]
359enum Penalty {
360 Gamma {
362 alpha: f64,
364 lambda: f64,
366 },
367 LogNormal {
369 alpha: f64,
371 },
372}
373
374impl Penalty {
375 fn accumulate(&self, log_likelihood: f64, log_w: &[f64], w: &[f64]) -> f64 {
387 match *self {
388 Penalty::Gamma { alpha, lambda } => {
389 let mut sum_log_w = 0.0;
390 for &l in log_w {
391 sum_log_w += l;
392 }
393 let mut sum_w = 0.0;
394 for &x in w {
395 sum_w += x;
396 }
397 log_likelihood + (alpha * sum_log_w - lambda * sum_w)
398 }
399 Penalty::LogNormal { alpha } => {
400 let mut quad = 0.0;
401 for &l in log_w {
402 quad += l * l;
403 }
404 log_likelihood - quad / alpha / 2.0 - (log_w.len() as f64) / 2.0 * alpha.ln()
405 }
406 }
407 }
408
409 fn gradient(&self, log_w_s: f64, w_s: f64) -> f64 {
420 match *self {
421 Penalty::Gamma { alpha, lambda } => alpha - lambda * w_s,
422 Penalty::LogNormal { alpha } => -log_w_s / alpha,
423 }
424 }
425
426 fn curvature(&self, w_s: f64) -> f64 {
436 match *self {
437 Penalty::Gamma { lambda, .. } => lambda * w_s,
438 Penalty::LogNormal { alpha } => 1.0 / alpha,
439 }
440 }
441}
442
443pub fn opt_pml(
473 data: &PmlData<'_>,
474 beta_init: &[f64],
475 variance: &PmlVariance,
476 params: Option<PmlParams>,
477) -> Result<PmlResult, EdgeErrors> {
478 opt_pml_tabled(data, None, beta_init, variance, params)
479}
480
481pub(crate) fn opt_pml_tabled(
496 data: &PmlData<'_>,
497 zeros: Option<&ZeroCells>,
498 beta_init: &[f64],
499 variance: &PmlVariance,
500 params: Option<PmlParams>,
501) -> Result<PmlResult, EdgeErrors> {
502 let exps = variance.subject.exp();
503 if !(exps.is_finite() && exps > 1.0) {
504 return Err(EdgeErrors::InvalidArgument(format!(
505 "PmlVariance::subject must be finite and strictly positive for opt_pml; got {}.",
506 variance.subject
507 )));
508 }
509 let penalty = Penalty::Gamma {
510 alpha: 1.0 / (exps - 1.0),
511 lambda: 1.0 / (exps.sqrt() * (exps - 1.0)),
512 };
513 optimise(
514 data,
515 zeros,
516 beta_init,
517 None,
518 penalty,
519 variance.cell,
520 params.unwrap_or_default(),
521 )
522}
523
524pub fn opt_pml_nbm(
543 data: &PmlData<'_>,
544 beta_init: &[f64],
545 variance: &PmlVariance,
546 params: Option<PmlParams>,
547) -> Result<PmlResult, EdgeErrors> {
548 if !(variance.subject.is_finite() && variance.subject > 0.0) {
549 return Err(EdgeErrors::InvalidArgument(format!(
550 "PmlVariance::subject must be finite and strictly positive for opt_pml_nbm; got {}.",
551 variance.subject
552 )));
553 }
554 let penalty = Penalty::LogNormal {
555 alpha: variance.subject,
556 };
557 optimise(
558 data,
559 None,
560 beta_init,
561 None,
562 penalty,
563 variance.cell,
564 params.unwrap_or_default(),
565 )
566}
567
568pub fn pml_log_likelihood_gradient(
588 data: &PmlData<'_>,
589 beta: &[f64],
590 log_w: &[f64],
591 variance: &PmlVariance,
592) -> Result<PmlObjective, EdgeErrors> {
593 data.validate()?;
594 let n_beta = data.n_beta();
595 let k = data.n_subjects();
596 if beta.len() != n_beta {
597 return Err(EdgeErrors::LengthMismatch {
598 name: "beta",
599 expected: n_beta,
600 got: beta.len(),
601 });
602 }
603 if log_w.len() != k {
604 return Err(EdgeErrors::LengthMismatch {
605 name: "log_w",
606 expected: k,
607 got: log_w.len(),
608 });
609 }
610
611 let alpha = variance.subject;
612 let gamma = variance.cell;
613 let penalty = Penalty::Gamma {
615 alpha,
616 lambda: alpha,
617 };
618
619 let mut work = Workspace::new(data, None);
620 work.sweep_current(data, beta, log_w, gamma, penalty);
621
622 let mut gradient = Vec::with_capacity(n_beta + k);
623 gradient.extend(work.current.db.iter().map(|v| -v));
624 gradient.extend(work.current.dw.iter().map(|v| -v));
625 Ok(PmlObjective {
626 objective: -work.current.log_likelihood,
627 gradient,
628 })
629}
630
631#[cfg(feature = "gpu")]
651pub(crate) fn opt_pml_from(
652 data: &PmlData<'_>,
653 beta_init: &[f64],
654 log_w_init: &[f64],
655 variance: &PmlVariance,
656 params: Option<PmlParams>,
657) -> Result<PmlResult, EdgeErrors> {
658 let exps = variance.subject.exp();
659 if !(exps.is_finite() && exps > 1.0) {
660 return Err(EdgeErrors::InvalidArgument(format!(
661 "PmlVariance::subject must be finite and strictly positive for opt_pml; got {}.",
662 variance.subject
663 )));
664 }
665 let penalty = Penalty::Gamma {
666 alpha: 1.0 / (exps - 1.0),
667 lambda: 1.0 / (exps.sqrt() * (exps - 1.0)),
668 };
669 optimise(
670 data,
671 None,
672 beta_init,
673 Some(log_w_init),
674 penalty,
675 variance.cell,
676 params.unwrap_or_default(),
677 )
678}
679
680#[cfg(feature = "gpu")]
687const EXP_SERIES_MAX: f64 = 1e-3;
688
689#[cfg(feature = "gpu")]
698const LIKDIF_ROUNDING: f64 = 1e-12;
699
700#[cfg(feature = "gpu")]
710#[inline(always)]
711fn exp_series(z: f64) -> f64 {
712 1.0 + z * (1.0 + z * (0.5 + z * (1.0 / 6.0 + z * (1.0 / 24.0 + z * (1.0 / 120.0)))))
713}
714
715#[cfg(feature = "gpu")]
721const LOG_PRODUCT_GROUP: u32 = 8;
722
723#[cfg(feature = "gpu")]
730struct LogSum {
731 sum: f64,
733 product: f64,
735 count: u32,
737}
738
739#[cfg(feature = "gpu")]
740impl LogSum {
741 fn new() -> Self {
743 Self {
744 sum: 0.0,
745 product: 1.0,
746 count: 0,
747 }
748 }
749
750 #[inline(always)]
756 fn push(&mut self, t: f64) {
757 self.product *= t;
758 self.count += 1;
759 if self.count == LOG_PRODUCT_GROUP {
760 self.sum += self.product.ln();
761 self.product = 1.0;
762 self.count = 0;
763 }
764 }
765
766 fn finish(self) -> f64 {
772 self.sum + self.product.ln()
773 }
774}
775
776#[cfg(feature = "gpu")]
779#[derive(Clone, Copy, Debug)]
780pub(crate) struct DeviceCurvature<'a> {
781 pub(crate) subject: &'a [f64],
783 pub(crate) cross: &'a [f64],
785 pub(crate) schur: &'a [f64],
787}
788
789#[cfg(feature = "gpu")]
792#[derive(Clone, Copy, Debug)]
793pub(crate) struct NewtonFinish {
794 pub(crate) log_likelihood: f64,
796 pub(crate) log_likelihood_prev: f64,
798 pub(crate) log_det: f64,
800 pub(crate) iterations: usize,
802}
803
804#[cfg(feature = "gpu")]
847#[allow(clippy::too_many_arguments)]
848pub(crate) fn newton_finish(
849 data: &PmlData<'_>,
850 beta: &[f64],
851 log_w: &[f64],
852 variance: &PmlVariance,
853 eps: f64,
854 max_iter: usize,
855 curvature: Option<DeviceCurvature<'_>>,
856 stored: &mut Vec<f64>,
857) -> Option<NewtonFinish> {
858 match data.n_beta() {
859 1 => newton_finish_width::<1>(
860 data, beta, log_w, variance, eps, max_iter, curvature, stored,
861 ),
862 2 => newton_finish_width::<2>(
863 data, beta, log_w, variance, eps, max_iter, curvature, stored,
864 ),
865 3 => newton_finish_width::<3>(
866 data, beta, log_w, variance, eps, max_iter, curvature, stored,
867 ),
868 4 => newton_finish_width::<4>(
869 data, beta, log_w, variance, eps, max_iter, curvature, stored,
870 ),
871 5 => newton_finish_width::<5>(
872 data, beta, log_w, variance, eps, max_iter, curvature, stored,
873 ),
874 6 => newton_finish_width::<6>(
875 data, beta, log_w, variance, eps, max_iter, curvature, stored,
876 ),
877 7 => newton_finish_width::<7>(
878 data, beta, log_w, variance, eps, max_iter, curvature, stored,
879 ),
880 8 => newton_finish_width::<8>(
881 data, beta, log_w, variance, eps, max_iter, curvature, stored,
882 ),
883 _ => None,
885 }
886}
887
888#[cfg(feature = "gpu")]
899#[allow(clippy::too_many_arguments)]
900fn newton_finish_width<const NB: usize>(
901 data: &PmlData<'_>,
902 beta: &[f64],
903 log_w: &[f64],
904 variance: &PmlVariance,
905 eps: f64,
906 max_iter: usize,
907 curvature: Option<DeviceCurvature<'_>>,
908 stored: &mut Vec<f64>,
909) -> Option<NewtonFinish> {
910 let nb = NB;
911 let k = data.n_subjects();
912 let gamma = variance.cell;
913 let exps = variance.subject.exp();
914 if beta.len() != nb || log_w.len() != k || !(exps.is_finite() && exps > 1.0) {
915 return None;
916 }
917 if curvature.is_some_and(|c| {
918 c.subject.len() != k || c.cross.len() != k * nb || c.schur.len() != nb * nb
919 }) {
920 return None;
921 }
922 let alpha = 1.0 / (exps - 1.0);
923 let lambda = 1.0 / (exps.sqrt() * (exps - 1.0));
924
925 let run = positive_runs(data.cell_index, data.subject_start);
926
927 let mut beta = beta.to_vec();
928 let mut log_w = log_w.to_vec();
929 let mut new_beta = vec![0.0; nb];
930 let mut new_log_w = vec![0.0; k];
931 let mut db = vec![0.0; nb];
932 let mut dw = vec![0.0; k];
933 let mut vw = vec![0.0; k];
934 let mut vwb = vec![0.0; k * nb];
935 let mut vb = vec![0.0; nb * nb];
936 let mut factor = vec![0.0; nb * nb];
937 let mut step_beta = vec![0.0; nb];
938 let mut perm = vec![0usize; nb];
939 let mut tmp = vec![0.0; nb];
940 stored.resize(data.n_cells(), 0.0);
942
943 for iterations in 1..=max_iter {
944 let borrowed = curvature.filter(|_| iterations == 1);
945 let assemble = borrowed.is_none();
946 db.fill(0.0);
947 vb.fill(0.0);
948 let mut log_likelihood_prev = 0.0;
949 for s in 0..k {
950 let w_s = log_w[s].exp();
951 let mut resid = 0.0;
952 let mut weight = 0.0;
953 let mut sum_log = LogSum::new();
954 let mut linear = 0.0;
955 let mut weighted_log = 0.0;
956 let mut score = [0.0; NB];
957 let mut first = [0.0; NB];
958 let mut second = [[0.0; NB]; NB];
959
960 let mut fold = |row: &[f64], d: f64, p: f64| {
961 resid += d;
962 for a in 0..nb {
963 score[a] += row[a] * d;
964 }
965 if assemble {
966 weight += p;
967 for a in 0..nb {
968 let pa = p * row[a];
969 first[a] += pa;
970 for b in a..nb {
971 second[a][b] += pa * row[b];
972 }
973 }
974 }
975 };
976
977 for r in data.subject_start[s]..data.subject_start[s + 1] {
978 let row = &data.design[r * nb..(r + 1) * nb];
979 let mut eta = data.offset[r];
980 for j in 0..nb {
981 eta += row[j] * beta[j];
982 }
983 stored[r] = (eta + log_w[s]).exp();
984 }
985 for r in data.subject_start[s]..data.subject_start[s + 1] {
986 let row = &data.design[r * nb..(r + 1) * nb];
987 let extb = stored[r];
988 let t = extb + gamma;
989 sum_log.push(t);
990 let inv = 1.0 / t;
991 let u = extb * inv;
992 fold(row, -gamma * u, gamma * u * inv);
993 }
994 for i in run[s]..run[s + 1] {
995 let c = data.cell_index[i];
996 let y = data.counts[i];
997 let row = &data.design[c * nb..(c + 1) * nb];
998 let mut eta = data.offset[c];
999 for j in 0..nb {
1000 eta += row[j] * beta[j];
1001 }
1002 let extb = stored[c];
1003 let t = extb + gamma;
1004 linear += eta * y;
1005 weighted_log += y * t.ln();
1006 let inv = 1.0 / t;
1007 let u = extb * inv;
1008 fold(row, y * (1.0 - u), y * u * inv);
1009 }
1010
1011 log_likelihood_prev +=
1012 linear + log_w[s] * data.subject_total[s] - gamma * sum_log.finish() - weighted_log
1013 + (alpha * log_w[s] - lambda * w_s);
1014 dw[s] = resid + (alpha - lambda * w_s);
1015 for a in 0..nb {
1016 db[a] += score[a];
1017 }
1018 if assemble {
1019 vw[s] = gamma * weight + lambda * w_s;
1020 for a in 0..nb {
1021 vwb[s * nb + a] = gamma * first[a];
1022 for b in a..nb {
1023 vb[a * nb + b] += gamma * second[a][b];
1024 }
1025 }
1026 }
1027 }
1028
1029 if let Some(device) = borrowed {
1031 vw.copy_from_slice(device.subject);
1032 vwb.copy_from_slice(device.cross);
1033 factor.copy_from_slice(device.schur);
1034 } else {
1035 for a in 0..nb {
1036 for b in a..nb {
1037 let mut acc = 0.0;
1038 for s in 0..k {
1039 acc += vwb[s * nb + a] * vwb[s * nb + b] / vw[s];
1040 }
1041 let v = vb[a * nb + b] - acc;
1042 factor[a * nb + b] = v;
1043 factor[b * nb + a] = v;
1044 }
1045 }
1046 }
1047 for j in 0..nb {
1048 let mut acc = 0.0;
1049 for s in 0..k {
1050 acc += vwb[s * nb + j] * dw[s] / vw[s];
1051 }
1052 step_beta[j] = db[j] - acc;
1053 }
1054 ldlt_solve(&mut factor, &mut step_beta, nb, &mut perm, &mut tmp);
1055
1056 for j in 0..nb {
1057 new_beta[j] = beta[j] + step_beta[j];
1058 }
1059 let mut log_likelihood = 0.0;
1060 let mut log_det = 0.0;
1061 for s in 0..k {
1062 let mut acc = 0.0;
1063 for j in 0..nb {
1064 acc += vwb[s * nb + j] * step_beta[j];
1065 }
1066 let new_log_w_s = log_w[s] + (dw[s] - acc) / vw[s];
1067 new_log_w[s] = new_log_w_s;
1068 let new_w_s = new_log_w_s.exp();
1069 let step_log_w_s = new_log_w_s - log_w[s];
1070
1071 let mut sum_log = LogSum::new();
1072 let mut linear = 0.0;
1073 let mut weighted_log = 0.0;
1074 let mut weight = 0.0;
1075 for r in data.subject_start[s]..data.subject_start[s + 1] {
1076 let row = &data.design[r * nb..(r + 1) * nb];
1077 let mut z = step_log_w_s;
1078 for j in 0..nb {
1079 z += row[j] * step_beta[j];
1080 }
1081 let extb = if z.abs() < EXP_SERIES_MAX {
1082 stored[r] * exp_series(z)
1083 } else {
1084 let mut eta = data.offset[r];
1085 for j in 0..nb {
1086 eta += row[j] * new_beta[j];
1087 }
1088 (eta + new_log_w_s).exp()
1089 };
1090 let t = extb + gamma;
1091 sum_log.push(t);
1092 let inv = 1.0 / t;
1093 weight += gamma * extb * inv * inv;
1094 }
1095 for i in run[s]..run[s + 1] {
1096 let c = data.cell_index[i];
1097 let y = data.counts[i];
1098 let row = &data.design[c * nb..(c + 1) * nb];
1099 let mut eta = data.offset[c];
1100 for j in 0..nb {
1101 eta += row[j] * new_beta[j];
1102 }
1103 let extb = (eta + new_log_w_s).exp();
1104 let t = extb + gamma;
1105 linear += eta * y;
1106 weighted_log += y * t.ln();
1107 let inv = 1.0 / t;
1108 weight += y * extb * inv * inv;
1109 }
1110 log_likelihood += linear + new_log_w_s * data.subject_total[s]
1111 - gamma * sum_log.finish()
1112 - weighted_log
1113 + (alpha * new_log_w_s - lambda * new_w_s);
1114 log_det += (gamma * weight + lambda * new_w_s).abs().ln();
1115 }
1116
1117 let likdif = log_likelihood - log_likelihood_prev;
1118 if likdif.is_nan() || likdif < -LIKDIF_ROUNDING * log_likelihood.abs() {
1119 return None;
1120 }
1121 if likdif <= eps {
1122 return Some(NewtonFinish {
1123 log_likelihood,
1124 log_likelihood_prev,
1125 log_det,
1126 iterations,
1127 });
1128 }
1129 beta.copy_from_slice(&new_beta);
1130 log_w.copy_from_slice(&new_log_w);
1131 }
1132 None
1133}
1134
1135pub fn check_convergence(
1155 result: &PmlResult,
1156 params: &PmlParams,
1157 variance_at_bound: bool,
1158 initial: i32,
1159) -> i32 {
1160 let mut code = initial;
1161 if code == CONV_SUCCESS {
1162 code = if variance_at_bound {
1163 CONV_AT_BOUND
1164 } else if result.log_likelihood.is_nan() {
1165 CONV_NOT_FINITE
1166 } else if result.iterations == params.max_iter {
1167 CONV_MAX_ITER
1168 } else if result.backtracks == params.max_backtrack + 1 {
1169 CONV_CRITICAL_POINT
1170 } else if result.backtracks == params.max_backtrack + 2 {
1171 CONV_STALLED
1172 } else {
1173 CONV_SUCCESS
1174 };
1175 }
1176
1177 let n_beta = result.beta.len();
1178 if n_beta > 1 && min_eigenvalue(&result.information, n_beta) < EIGENVALUE_CUTOFF {
1179 code = CONV_SINGULAR;
1180 }
1181 code
1182}
1183
1184pub(crate) fn positive_runs(cells: &[usize], starts: &[usize]) -> Vec<usize> {
1199 starts
1200 .iter()
1201 .map(|&b| cells.partition_point(|&c| c < b))
1202 .collect()
1203}
1204
1205const MAX_UNROLLED_WIDTH: usize = 8;
1209
1210struct Sweep {
1215 log_likelihood: f64,
1217 w: Vec<f64>,
1219 db: Vec<f64>,
1221 dw: Vec<f64>,
1223 vw: Vec<f64>,
1225 vwb: Vec<f64>,
1227 vb: Vec<f64>,
1229}
1230
1231impl Sweep {
1232 fn new(nb: usize, k: usize) -> Self {
1243 Self {
1244 log_likelihood: 0.0,
1245 w: vec![0.0; k],
1246 db: vec![0.0; nb],
1247 dw: vec![0.0; k],
1248 vw: vec![0.0; k],
1249 vwb: vec![0.0; k * nb],
1250 vb: vec![0.0; nb * nb],
1251 }
1252 }
1253}
1254
1255struct Workspace<'z> {
1257 zeros: Option<&'z ZeroCells>,
1259 stored: Vec<f64>,
1262 run: Vec<usize>,
1264 wide: Vec<f64>,
1267 current: Sweep,
1269 trial: Sweep,
1271 vw_step: Vec<f64>,
1273 vb2: Vec<f64>,
1275 factor: Vec<f64>,
1277 perm: Vec<usize>,
1279 tmp: Vec<f64>,
1281 step_beta: Vec<f64>,
1283 step_log_w: Vec<f64>,
1285 dwvw: Vec<f64>,
1287 damp_beta: Vec<f64>,
1289 damp_log_w: Vec<f64>,
1291 new_beta: Vec<f64>,
1293 new_log_w: Vec<f64>,
1295}
1296
1297impl<'z> Workspace<'z> {
1298 fn new(data: &PmlData<'_>, zeros: Option<&'z ZeroCells>) -> Self {
1309 let n_cells = data.n_cells();
1310 let k = data.n_subjects();
1311 let nb = data.n_beta();
1312 let run = positive_runs(data.cell_index, data.subject_start);
1313
1314 Self {
1315 zeros,
1316 stored: vec![0.0; n_cells],
1317 run,
1318 wide: if nb > MAX_UNROLLED_WIDTH {
1319 vec![0.0; 2 * nb + nb * nb]
1320 } else {
1321 Vec::new()
1322 },
1323 current: Sweep::new(nb, k),
1324 trial: Sweep::new(nb, k),
1325 vw_step: vec![0.0; k],
1326 vb2: vec![0.0; nb * nb],
1327 factor: vec![0.0; nb * nb],
1328 perm: vec![0; nb],
1329 tmp: vec![0.0; nb],
1330 step_beta: vec![0.0; nb],
1331 step_log_w: vec![0.0; k],
1332 dwvw: vec![0.0; k],
1333 damp_beta: vec![0.0; nb],
1334 damp_log_w: vec![0.0; k],
1335 new_beta: vec![0.0; nb],
1336 new_log_w: vec![0.0; k],
1337 }
1338 }
1339
1340 fn sweep_current(
1350 &mut self,
1351 data: &PmlData<'_>,
1352 beta: &[f64],
1353 log_w: &[f64],
1354 gamma: f64,
1355 penalty: Penalty,
1356 ) {
1357 sweep(
1358 data,
1359 beta,
1360 log_w,
1361 gamma,
1362 penalty,
1363 self.zeros,
1364 &mut self.stored,
1365 &self.run,
1366 &mut self.wide,
1367 &mut self.current,
1368 );
1369 }
1370
1371 fn sweep_trial(&mut self, data: &PmlData<'_>, gamma: f64, penalty: Penalty) -> f64 {
1383 sweep(
1384 data,
1385 &self.new_beta,
1386 &self.new_log_w,
1387 gamma,
1388 penalty,
1389 self.zeros,
1390 &mut self.stored,
1391 &self.run,
1392 &mut self.wide,
1393 &mut self.trial,
1394 );
1395 self.trial.log_likelihood
1396 }
1397}
1398
1399#[allow(clippy::too_many_arguments)]
1423fn sweep(
1424 data: &PmlData<'_>,
1425 beta: &[f64],
1426 log_w: &[f64],
1427 gamma: f64,
1428 penalty: Penalty,
1429 zeros: Option<&ZeroCells>,
1430 stored: &mut [f64],
1431 run: &[usize],
1432 wide: &mut [f64],
1433 out: &mut Sweep,
1434) {
1435 macro_rules! width {
1436 ($nb:literal) => {
1437 sweep_width::<$nb>(
1438 data, beta, log_w, gamma, penalty, zeros, stored, run, wide, out,
1439 )
1440 };
1441 }
1442 match data.n_beta() {
1443 1 => width!(1),
1444 2 => width!(2),
1445 3 => width!(3),
1446 4 => width!(4),
1447 5 => width!(5),
1448 6 => width!(6),
1449 7 => width!(7),
1450 8 => width!(8),
1451 _ => width!(0),
1452 }
1453}
1454
1455#[allow(clippy::too_many_arguments)]
1462fn sweep_width<const NB: usize>(
1463 data: &PmlData<'_>,
1464 beta: &[f64],
1465 log_w: &[f64],
1466 gamma: f64,
1467 penalty: Penalty,
1468 zeros: Option<&ZeroCells>,
1469 stored: &mut [f64],
1470 run: &[usize],
1471 wide: &mut [f64],
1472 out: &mut Sweep,
1473) {
1474 let nb = if NB == 0 { data.n_beta() } else { NB };
1475 let k = data.n_subjects();
1476 let mut local = [0.0; 2 * MAX_UNROLLED_WIDTH + MAX_UNROLLED_WIDTH * MAX_UNROLLED_WIDTH];
1477 let sums: &mut [f64] = if NB == 0 {
1478 wide
1479 } else {
1480 &mut local[..2 * nb + nb * nb]
1481 };
1482
1483 out.db.fill(0.0);
1484 out.vb.fill(0.0);
1485 let ln_gamma = gamma.ln();
1486 let mut log_likelihood = 0.0;
1487 for s in 0..k {
1488 let cells = data.subject_start[s]..data.subject_start[s + 1];
1489 let log_w_s = log_w[s];
1490 let w_s = log_w_s.exp();
1491 sums.fill(0.0);
1492 let (score, rest) = sums.split_at_mut(nb);
1493 let (first, second) = rest.split_at_mut(nb);
1494 let mut resid = 0.0;
1495 let mut weight = 0.0;
1496 let mut sum_log = 0.0;
1497 let mut linear = 0.0;
1498 let mut weighted_log = 0.0;
1499
1500 let mut fold = |row: &[f64], d: f64, p: f64| {
1503 resid += d;
1504 weight += p;
1505 for a in 0..nb {
1506 score[a] += row[a] * d;
1507 let pa = p * row[a];
1508 first[a] += pa;
1509 for b in a..nb {
1510 second[a * nb + b] += pa * row[b];
1511 }
1512 }
1513 };
1514
1515 match zeros {
1516 None => {
1517 for r in cells.clone() {
1520 let row = &data.design[r * nb..(r + 1) * nb];
1521 let mut eta = data.offset[r];
1522 for j in 0..nb {
1523 eta += row[j] * beta[j];
1524 }
1525 stored[r] = (eta + log_w_s).exp();
1526 }
1527 for r in cells {
1528 let row = &data.design[r * nb..(r + 1) * nb];
1529 let extb = stored[r];
1530 let t = extb + gamma;
1531 sum_log += t.ln();
1532 let inv = 1.0 / t;
1533 let u = extb * inv;
1534 fold(row, -gamma * u, gamma * u * inv);
1535 }
1536 }
1537 Some(z) => {
1538 for g in &z.groups[z.subject_groups[s]..z.subject_groups[s + 1]] {
1541 let row = &data.design[g.row * nb..(g.row + 1) * nb];
1542 let mut log_tau = log_w_s - ln_gamma;
1543 for j in 0..nb {
1544 log_tau += row[j] * beta[j];
1545 }
1546 let [l, a, b] = g.eval::<0, 3>(log_tau);
1547 sum_log += g.n * ln_gamma + l;
1548 fold(row, -gamma * a, b);
1549 }
1550 for &r in &z.loose[z.subject_loose[s]..z.subject_loose[s + 1]] {
1551 let row = &data.design[r * nb..(r + 1) * nb];
1552 let mut eta = data.offset[r];
1553 for j in 0..nb {
1554 eta += row[j] * beta[j];
1555 }
1556 let extb = (eta + log_w_s).exp();
1557 stored[r] = extb;
1558 let t = extb + gamma;
1559 sum_log += t.ln();
1560 let inv = 1.0 / t;
1561 let u = extb * inv;
1562 fold(row, -gamma * u, gamma * u * inv);
1563 }
1564 }
1565 }
1566 for i in run[s]..run[s + 1] {
1567 let c = data.cell_index[i];
1568 let y = data.counts[i];
1569 let row = &data.design[c * nb..(c + 1) * nb];
1570 let mut eta = data.offset[c];
1571 for j in 0..nb {
1572 eta += row[j] * beta[j];
1573 }
1574 let extb = if zeros.is_some() {
1575 let e = (eta + log_w_s).exp();
1576 stored[c] = e;
1577 e
1578 } else {
1579 stored[c]
1580 };
1581 let t = extb + gamma;
1582 linear += eta * y;
1583 weighted_log += y * t.ln();
1584 let inv = 1.0 / t;
1585 let u = extb * inv;
1586 fold(row, y * (1.0 - u), y * u * inv);
1587 }
1588
1589 log_likelihood += linear + log_w_s * data.subject_total[s] - gamma * sum_log - weighted_log;
1590 out.w[s] = w_s;
1591 out.dw[s] = resid + penalty.gradient(log_w_s, w_s);
1592 out.vw[s] = gamma * weight + penalty.curvature(w_s);
1593 for a in 0..nb {
1594 out.db[a] += score[a];
1595 out.vwb[s * nb + a] = gamma * first[a];
1596 for b in a..nb {
1597 out.vb[a * nb + b] += gamma * second[a * nb + b];
1598 }
1599 }
1600 }
1601 for a in 0..nb {
1602 for b in (a + 1)..nb {
1603 out.vb[b * nb + a] = out.vb[a * nb + b];
1604 }
1605 }
1606 out.log_likelihood = penalty.accumulate(log_likelihood, log_w, &out.w);
1607}
1608
1609fn optimise(
1632 data: &PmlData<'_>,
1633 zeros: Option<&ZeroCells>,
1634 beta_init: &[f64],
1635 log_w_init: Option<&[f64]>,
1636 penalty: Penalty,
1637 gamma: f64,
1638 params: PmlParams,
1639) -> Result<PmlResult, EdgeErrors> {
1640 data.validate()?;
1641 let nb = data.n_beta();
1642 let k = data.n_subjects();
1643 if beta_init.len() != nb {
1644 return Err(EdgeErrors::LengthMismatch {
1645 name: "beta_init",
1646 expected: nb,
1647 got: beta_init.len(),
1648 });
1649 }
1650 if !(gamma.is_finite() && gamma > 0.0) {
1651 return Err(EdgeErrors::InvalidDispersion(gamma));
1652 }
1653
1654 let mut beta = beta_init.to_vec();
1655 let mut log_w = match log_w_init {
1656 Some(init) if init.len() == k => init.to_vec(),
1657 Some(init) => {
1658 return Err(EdgeErrors::LengthMismatch {
1659 name: "log_w_init",
1660 expected: k,
1661 got: init.len(),
1662 });
1663 }
1664 None => vec![0.0; k],
1665 };
1666 let mut work = Workspace::new(data, zeros);
1667 work.sweep_current(data, &beta, &log_w, gamma, penalty);
1668
1669 let mut log_likelihood = work.current.log_likelihood;
1670 let mut log_likelihood_prev = 0.0;
1671 let mut likdif = 0.0;
1672 let mut step = 0usize;
1673 let mut backtracks = 0usize;
1674
1675 while step == 0 || (likdif > params.eps && step < params.max_iter) {
1676 step += 1;
1677 work.damp_beta.fill(1.0);
1678 work.damp_log_w.fill(1.0);
1679
1680 let cur = &work.current;
1681 work.vw_step.copy_from_slice(&cur.vw);
1682 for s in 0..k {
1683 work.dwvw[s] = cur.dw[s] / cur.vw[s];
1684 }
1685 for a in 0..nb {
1688 for b in a..nb {
1689 let mut acc = 0.0;
1690 for s in 0..k {
1691 acc += cur.vwb[s * nb + a] * cur.vwb[s * nb + b] / cur.vw[s];
1692 }
1693 let v = cur.vb[a * nb + b] - acc;
1694 work.vb2[a * nb + b] = v;
1695 work.vb2[b * nb + a] = v;
1696 }
1697 }
1698 for j in 0..nb {
1700 let mut acc = 0.0;
1701 for s in 0..k {
1702 acc += cur.vwb[s * nb + j] * work.dwvw[s];
1703 }
1704 work.step_beta[j] = cur.db[j] - acc;
1705 }
1706 work.factor.copy_from_slice(&work.vb2);
1707 ldlt_solve(
1708 &mut work.factor,
1709 &mut work.step_beta,
1710 nb,
1711 &mut work.perm,
1712 &mut work.tmp,
1713 );
1714 for s in 0..k {
1715 let mut acc = 0.0;
1716 for j in 0..nb {
1717 acc += cur.vwb[s * nb + j] * work.step_beta[j];
1718 }
1719 work.step_log_w[s] = work.dwvw[s] - acc / cur.vw[s];
1720 }
1721
1722 for j in 0..nb {
1723 work.new_beta[j] = beta[j] + work.step_beta[j];
1724 }
1725 for s in 0..k {
1726 work.new_log_w[s] = log_w[s] + work.step_log_w[s];
1727 }
1728
1729 log_likelihood_prev = log_likelihood;
1730 log_likelihood = work.sweep_trial(data, gamma, penalty);
1731 likdif = log_likelihood - log_likelihood_prev;
1732
1733 backtracks = 0;
1734 let mut min_step = STEP_CUTOFF;
1735 while likdif < 0.0 || log_likelihood.is_infinite() {
1736 backtracks += 1;
1737 min_step /= 2.0;
1738
1739 if backtracks > params.max_backtrack {
1740 likdif = 0.0;
1741 log_likelihood = log_likelihood_prev;
1742 let grad_beta = work.current.db.iter().fold(0.0f64, |m, v| m.max(v.abs()));
1743 let grad_log_w = work.current.dw.iter().fold(0.0f64, |m, v| m.max(v.abs()));
1744 if grad_beta > GRADIENT_TOLERANCE || grad_log_w > GRADIENT_TOLERANCE {
1745 backtracks += 1;
1746 }
1747 break;
1748 }
1749
1750 for j in 0..nb {
1753 let d = work.step_beta[j];
1754 if d < STEP_CUTOFF && d > -STEP_CUTOFF {
1755 work.damp_beta[j] /= 2.0;
1756 work.new_beta[j] = beta[j] + d * work.damp_beta[j];
1757 } else if d > 0.0 {
1758 work.new_beta[j] = beta[j] + min_step;
1759 } else {
1760 work.new_beta[j] = beta[j] - min_step;
1761 }
1762 }
1763 for s in 0..k {
1764 let d = work.step_log_w[s];
1765 if d < STEP_CUTOFF && d > -STEP_CUTOFF {
1766 work.damp_log_w[s] /= 2.0;
1767 work.new_log_w[s] = log_w[s] + d * work.damp_log_w[s];
1768 } else if d > 0.0 {
1769 work.new_log_w[s] = log_w[s] + min_step;
1770 } else {
1771 work.new_log_w[s] = log_w[s] - min_step;
1772 }
1773 }
1774
1775 log_likelihood = work.sweep_trial(data, gamma, penalty);
1776 likdif = log_likelihood - log_likelihood_prev;
1777 }
1778
1779 beta.copy_from_slice(&work.new_beta);
1780 log_w.copy_from_slice(&work.new_log_w);
1781 std::mem::swap(&mut work.current, &mut work.trial);
1782 }
1783
1784 let mut log_det = 0.0;
1787 for s in 0..k {
1788 log_det += work.vw_step[s].abs().ln();
1789 }
1790 if params.reml {
1791 work.factor.copy_from_slice(&work.vb2);
1792 let det = determinant(&mut work.factor, nb, &mut work.perm);
1793 log_det += det.abs().ln();
1794 }
1795
1796 let second_order = match penalty {
1797 Penalty::Gamma { lambda, .. } if params.ord > 1 => {
1798 laplace_correction(&work, data, &beta, &log_w, gamma, lambda, params.ord)
1799 }
1800 _ => 0.0,
1801 };
1802
1803 let mut result = PmlResult {
1804 beta,
1805 log_w,
1806 information: work.vb2.clone(),
1807 log_likelihood,
1808 log_likelihood_prev,
1809 log_det,
1810 second_order,
1811 convergence: CONV_SUCCESS,
1812 iterations: step,
1813 backtracks,
1814 };
1815 result.convergence = check_convergence(&result, ¶ms, false, CONV_SUCCESS);
1816 Ok(result)
1817}
1818
1819fn laplace_correction(
1846 work: &Workspace<'_>,
1847 data: &PmlData<'_>,
1848 beta: &[f64],
1849 log_w: &[f64],
1850 gamma: f64,
1851 lambda: f64,
1852 ord: u32,
1853) -> f64 {
1854 let k = data.n_subjects();
1855 let nb = data.n_beta();
1856 let ln_gamma = gamma.ln();
1857 let high = ord > 2;
1858 let terms = |e: f64| {
1860 let t = e + gamma;
1861 let p2 = e / (t * t);
1862 let p3 = p2 / t;
1863 let p4 = p3 / t;
1864 let p5 = p4 / t;
1865 let e2 = e * e;
1866 [
1867 p2,
1868 p3 * (gamma - e),
1869 p4 * (gamma * gamma + e2 - (4.0 * gamma) * e),
1870 p5 * (gamma * gamma * gamma - (11.0 * gamma * gamma) * e + (11.0 * gamma) * e2
1871 - e2 * e),
1872 ]
1873 };
1874
1875 let mut third_acc = 0.0;
1876 let mut fourth_acc = 0.0;
1877 let mut fifth_acc = 0.0;
1878 for s in 0..k {
1879 let mut sums = [0.0; 4];
1880 let mut dense = |e: f64| {
1881 let f = terms(e);
1882 for j in 0..4 {
1883 sums[j] += gamma * f[j];
1884 }
1885 };
1886 match work.zeros {
1887 None => {
1888 for &e in &work.stored[data.subject_start[s]..data.subject_start[s + 1]] {
1889 dense(e);
1890 }
1891 }
1892 Some(z) => {
1893 for &r in &z.loose[z.subject_loose[s]..z.subject_loose[s + 1]] {
1894 dense(work.stored[r]);
1895 }
1896 for g in &z.groups[z.subject_groups[s]..z.subject_groups[s + 1]] {
1898 let row = &data.design[g.row * nb..(g.row + 1) * nb];
1899 let mut log_tau = log_w[s] - ln_gamma;
1900 for j in 0..nb {
1901 log_tau += row[j] * beta[j];
1902 }
1903 let f = g.eval::<2, 4>(log_tau);
1904 for j in 0..4 {
1905 sums[j] += f[j];
1906 }
1907 }
1908 }
1909 }
1910 for i in work.run[s]..work.run[s + 1] {
1911 let y = data.counts[i];
1912 let f = terms(work.stored[data.cell_index[i]]);
1913 for j in 0..4 {
1914 sums[j] += y * f[j];
1915 }
1916 }
1917 let prior = lambda * work.current.w[s];
1918 let vw = gamma * sums[0] + prior;
1919 let third = gamma * sums[1] + prior;
1920 third_acc += third * third / (vw * vw * vw);
1921 if high {
1922 let fourth = gamma * sums[2] + prior;
1923 let fifth = gamma * sums[3] + prior;
1924 let vs = vw * vw;
1925 fourth_acc += fourth / vs;
1926 fifth_acc += fifth * third / (vs * vs);
1927 }
1928 }
1929
1930 let mut second = 5.0 * third_acc / 24.0;
1931 if high {
1932 second -= fourth_acc / 8.0;
1933 second += 7.0 * fifth_acc / 48.0;
1934 }
1935 second
1936}
1937
1938fn ldlt_solve(a: &mut [f64], b: &mut [f64], n: usize, perm: &mut [usize], tmp: &mut [f64]) {
1958 for step in 0..n {
1959 let mut pivot = step;
1960 let mut best = a[step * n + step].abs();
1961 for i in (step + 1)..n {
1962 let v = a[i * n + i].abs();
1963 if v > best {
1964 best = v;
1965 pivot = i;
1966 }
1967 }
1968 perm[step] = pivot;
1969 if pivot != step {
1970 for j in 0..step {
1971 a.swap(step * n + j, pivot * n + j);
1972 }
1973 for i in (pivot + 1)..n {
1974 a.swap(i * n + step, i * n + pivot);
1975 }
1976 a.swap(step * n + step, pivot * n + pivot);
1977 for i in (step + 1)..pivot {
1978 a.swap(i * n + step, pivot * n + i);
1979 }
1980 }
1981
1982 if step > 0 {
1983 for j in 0..step {
1984 tmp[j] = a[j * n + j] * a[step * n + j];
1985 }
1986 let mut acc = 0.0;
1987 for j in 0..step {
1988 acc += a[step * n + j] * tmp[j];
1989 }
1990 a[step * n + step] -= acc;
1991 for i in (step + 1)..n {
1992 let mut acc = 0.0;
1993 for j in 0..step {
1994 acc += a[i * n + j] * tmp[j];
1995 }
1996 a[i * n + step] -= acc;
1997 }
1998 }
1999
2000 let d = a[step * n + step];
2001 if d != 0.0 {
2002 for i in (step + 1)..n {
2003 a[i * n + step] /= d;
2004 }
2005 }
2006 }
2007
2008 for step in 0..n {
2009 b.swap(step, perm[step]);
2010 }
2011 for i in 0..n {
2012 let mut acc = b[i];
2013 for j in 0..i {
2014 acc -= a[i * n + j] * b[j];
2015 }
2016 b[i] = acc;
2017 }
2018 for i in 0..n {
2019 let d = a[i * n + i];
2020 b[i] = if d.abs() > f64::MIN_POSITIVE {
2023 b[i] / d
2024 } else {
2025 0.0
2026 };
2027 }
2028 for i in (0..n).rev() {
2029 let mut acc = b[i];
2030 for j in (i + 1)..n {
2031 acc -= a[j * n + i] * b[j];
2032 }
2033 b[i] = acc;
2034 }
2035 for step in (0..n).rev() {
2036 b.swap(step, perm[step]);
2037 }
2038}
2039
2040fn determinant(a: &mut [f64], n: usize, perm: &mut [usize]) -> f64 {
2054 let mut det = 1.0;
2055 perm.fill(0);
2056 for step in 0..n {
2057 let mut pivot = step;
2058 let mut best = a[step * n + step].abs();
2059 for i in (step + 1)..n {
2060 let v = a[i * n + step].abs();
2061 if v > best {
2062 best = v;
2063 pivot = i;
2064 }
2065 }
2066 if pivot != step {
2067 for j in 0..n {
2068 a.swap(step * n + j, pivot * n + j);
2069 }
2070 det = -det;
2071 }
2072 let d = a[step * n + step];
2073 det *= d;
2074 if d == 0.0 {
2075 return 0.0;
2076 }
2077 for i in (step + 1)..n {
2078 let factor = a[i * n + step] / d;
2079 a[i * n + step] = factor;
2080 for j in (step + 1)..n {
2081 a[i * n + j] -= factor * a[step * n + j];
2082 }
2083 }
2084 }
2085 det
2086}
2087
2088fn min_eigenvalue(matrix: &[f64], n: usize) -> f64 {
2102 if n == 0 {
2103 return f64::NAN;
2104 }
2105 if n == 1 {
2106 return matrix[0];
2107 }
2108 let mut a = matrix.to_vec();
2109 let mut frobenius = 0.0;
2110 for &v in &a {
2111 frobenius += v * v;
2112 }
2113 let tolerance = (frobenius * f64::EPSILON * f64::EPSILON).max(f64::MIN_POSITIVE);
2114
2115 for _ in 0..JACOBI_SWEEPS {
2116 let mut off = 0.0;
2117 for p in 0..n {
2118 for q in (p + 1)..n {
2119 off += a[p * n + q] * a[p * n + q];
2120 }
2121 }
2122 if off <= tolerance {
2123 break;
2124 }
2125 for p in 0..n {
2126 for q in (p + 1)..n {
2127 let apq = a[p * n + q];
2128 if apq == 0.0 {
2129 continue;
2130 }
2131 let theta = (a[q * n + q] - a[p * n + p]) / (2.0 * apq);
2132 let t = theta.signum() / (theta.abs() + (theta * theta + 1.0).sqrt());
2133 let c = 1.0 / (t * t + 1.0).sqrt();
2134 let s = t * c;
2135 for r in 0..n {
2136 let arp = a[r * n + p];
2137 let arq = a[r * n + q];
2138 a[r * n + p] = c * arp - s * arq;
2139 a[r * n + q] = s * arp + c * arq;
2140 }
2141 for r in 0..n {
2142 let apr = a[p * n + r];
2143 let aqr = a[q * n + r];
2144 a[p * n + r] = c * apr - s * aqr;
2145 a[q * n + r] = s * apr + c * aqr;
2146 }
2147 }
2148 }
2149 }
2150 (0..n).map(|i| a[i * n + i]).fold(f64::INFINITY, f64::min)
2151}
2152
2153#[cfg(test)]
2158#[allow(clippy::excessive_precision)]
2160#[allow(clippy::type_complexity)]
2162mod tests {
2163 use super::*;
2164 use approx::assert_relative_eq;
2165
2166 const DESIGN: [f64; 24] = [
2169 1.0, 0.0, 1.0, 1.0, 1.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0, 1.0, 1.0, 1.0, 0.0, 1.0, 1.0,
2170 1.0, 0.0, 1.0, 1.0, 1.0, 1.0,
2171 ];
2172
2173 const OFFSET: [f64; 12] = [
2175 0.25, -0.5, 0.75, 0.0, 0.5, -0.25, 0.125, 0.375, -0.125, 0.25, 0.0, -0.375,
2176 ];
2177
2178 const SUBJECT_START: [usize; 4] = [0, 4, 9, 12];
2180
2181 const COUNTS: [f64; 8] = [3.0, 5.0, 2.0, 4.0, 1.0, 7.0, 2.0, 6.0];
2183
2184 const CELL_INDEX: [usize; 8] = [0, 2, 3, 5, 6, 7, 9, 10];
2186
2187 const SUBJECT_TOTAL: [f64; 3] = [10.0, 12.0, 8.0];
2189
2190 const COUNTS_B: [f64; 5] = [3.0, 5.0, 2.0, 2.0, 6.0];
2192
2193 const CELL_INDEX_B: [usize; 5] = [0, 2, 3, 9, 10];
2195
2196 const SUBJECT_TOTAL_B: [f64; 3] = [10.0, 0.0, 8.0];
2198
2199 const TOL: f64 = 1e-11;
2206
2207 fn fixture_a() -> PmlData<'static> {
2209 PmlData {
2210 design: &DESIGN,
2211 offset: &OFFSET,
2212 counts: &COUNTS,
2213 cell_index: &CELL_INDEX,
2214 subject_start: &SUBJECT_START,
2215 subject_total: &SUBJECT_TOTAL,
2216 }
2217 }
2218
2219 fn fixture_b() -> PmlData<'static> {
2221 PmlData {
2222 design: &DESIGN,
2223 offset: &OFFSET,
2224 counts: &COUNTS_B,
2225 cell_index: &CELL_INDEX_B,
2226 subject_start: &SUBJECT_START,
2227 subject_total: &SUBJECT_TOTAL_B,
2228 }
2229 }
2230
2231 fn assert_slice(got: &[f64], want: &[f64]) {
2238 assert_eq!(got.len(), want.len());
2239 for (g, w) in got.iter().zip(want) {
2240 assert_relative_eq!(*g, *w, max_relative = TOL, epsilon = 1e-12);
2241 }
2242 }
2243
2244 #[test]
2250 fn test_opt_pml_well_behaved_matches_nebula() {
2251 let data = fixture_a();
2252 let variance = PmlVariance {
2253 subject: 0.25,
2254 cell: 2.0,
2255 };
2256 let fit = opt_pml(&data, &[0.5, -0.25], &variance, None).expect("fit");
2257
2258 assert_slice(&fit.beta, &[1.1055943907637618, -0.98348970166026206]);
2259 assert_slice(
2260 &fit.log_w,
2261 &[
2262 -0.025429869309964156,
2263 0.022419629678247836,
2264 0.33781545877640862,
2265 ],
2266 );
2267 assert_slice(
2268 &fit.information,
2269 &[
2270 5.4708370512237092,
2271 2.6193993778249318,
2272 2.6193993778249318,
2273 4.0223986651682804,
2274 ],
2275 );
2276 assert_relative_eq!(fit.log_likelihood, -60.378642466146786, max_relative = TOL);
2277 assert_relative_eq!(fit.log_det, 6.0421923783970968, max_relative = TOL);
2278 assert_eq!(fit.second_order, 0.0);
2279 assert_eq!(fit.iterations, 4);
2280 assert_eq!(fit.backtracks, 0);
2281 assert_eq!(fit.convergence, CONV_SUCCESS);
2282 }
2283
2284 #[test]
2285 fn test_opt_pml_third_order_laplace_correction() {
2286 let data = fixture_a();
2287 let variance = PmlVariance {
2288 subject: 0.25,
2289 cell: 2.0,
2290 };
2291 let params = PmlParams {
2292 ord: 3,
2293 ..Default::default()
2294 };
2295 let fit = opt_pml(&data, &[0.5, -0.25], &variance, Some(params)).expect("fit");
2296
2297 assert_slice(&fit.beta, &[1.1055943907637618, -0.98348970166026206]);
2299 assert_relative_eq!(fit.second_order, 0.0028301625845031939, max_relative = TOL);
2300 }
2301
2302 #[test]
2303 fn test_opt_pml_reml_log_determinant() {
2304 let data = fixture_a();
2305 let variance = PmlVariance {
2306 subject: 0.25,
2307 cell: 2.0,
2308 };
2309 let params = PmlParams {
2310 reml: true,
2311 ..Default::default()
2312 };
2313 let fit = opt_pml(&data, &[0.5, -0.25], &variance, Some(params)).expect("fit");
2314
2315 assert_relative_eq!(fit.log_det, 8.759838692617901, max_relative = TOL);
2316 assert_slice(&fit.beta, &[1.1055943907637618, -0.98348970166026206]);
2317 }
2318
2319 #[test]
2320 fn test_opt_pml_subject_with_all_zero_counts() {
2321 let data = fixture_b();
2322 let variance = PmlVariance {
2323 subject: 0.25,
2324 cell: 2.0,
2325 };
2326 let fit = opt_pml(&data, &[0.5, -0.25], &variance, None).expect("fit");
2327
2328 assert_slice(&fit.beta, &[0.15781792039265752, -0.098178037869394941]);
2329 assert_slice(
2330 &fit.log_w,
2331 &[
2332 0.31781101972092152,
2333 -0.83477809302857631,
2334 0.46458776408134972,
2335 ],
2336 );
2337 assert_slice(
2338 &fit.information,
2339 &[
2340 4.9151627959404793,
2341 2.59325292186585,
2342 2.59325292186585,
2343 3.5781777249175555,
2344 ],
2345 );
2346 assert_relative_eq!(fit.log_likelihood, -50.769290856943599, max_relative = TOL);
2347 assert_relative_eq!(fit.log_det, 5.3802869340559738, max_relative = TOL);
2348 assert_eq!(fit.convergence, CONV_SUCCESS);
2349 }
2350
2351 #[test]
2352 fn test_opt_pml_zero_subject_third_order() {
2353 let data = fixture_b();
2354 let variance = PmlVariance {
2355 subject: 0.25,
2356 cell: 2.0,
2357 };
2358 let params = PmlParams {
2359 ord: 3,
2360 ..Default::default()
2361 };
2362 let fit = opt_pml(&data, &[0.5, -0.25], &variance, Some(params)).expect("fit");
2363 assert_relative_eq!(fit.second_order, 0.029026901671004347, max_relative = TOL);
2364 }
2365
2366 #[test]
2367 fn test_opt_pml_start_near_bound_backtracks() {
2368 let data = fixture_a();
2371 let variance = PmlVariance {
2372 subject: 0.25,
2373 cell: 2.0,
2374 };
2375 let fit = opt_pml(&data, &[90.0, 0.0], &variance, None).expect("fit");
2376
2377 assert_slice(&fit.beta, &[1.1055943759446161, -0.98348968952057669]);
2378 assert_slice(
2379 &fit.log_w,
2380 &[
2381 -0.025429869876909816,
2382 0.022419639320108459,
2383 0.3378154594265026,
2384 ],
2385 );
2386 assert_slice(
2387 &fit.information,
2388 &[
2389 5.4711020927179739,
2390 2.6193969725187061,
2391 2.6193969725187061,
2392 4.0224446400576319,
2393 ],
2394 );
2395 assert_eq!(fit.iterations, 9);
2396 assert_eq!(fit.convergence, CONV_SUCCESS);
2397 }
2398
2399 #[test]
2400 fn test_opt_pml_far_outside_bounds_uses_fixed_displacement() {
2401 let data = fixture_a();
2404 let variance = PmlVariance {
2405 subject: 0.25,
2406 cell: 2.0,
2407 };
2408 let fit = opt_pml(&data, &[-90.0, 60.0], &variance, None).expect("fit");
2409
2410 assert_slice(&fit.beta, &[1.1055943725487707, -0.98348968650982715]);
2411 assert_relative_eq!(fit.log_det, 6.0423215051590002, max_relative = TOL);
2412 assert_eq!(fit.iterations, 12);
2413 }
2414
2415 #[test]
2416 fn test_opt_pml_critical_point_reports_minus_ten() {
2417 let data = fixture_a();
2421 let variance = PmlVariance {
2422 subject: 8.0,
2423 cell: 0.001,
2424 };
2425 let fit = opt_pml(&data, &[15.0, 5.0], &variance, None).expect("fit");
2426
2427 assert_slice(&fit.beta, &[4.9609375, -4.9609375]);
2428 assert_slice(&fit.log_w, &[0.0390625, 0.0390625, 4.5234136840078936]);
2429 assert_slice(
2430 &fit.information,
2431 &[
2432 7.0733722800021132e-05,
2433 7.0105348391562988e-05,
2434 7.0105348391562988e-05,
2435 0.00016310178559274507,
2436 ],
2437 );
2438 assert_relative_eq!(fit.log_det, -20.515549220945573, max_relative = TOL);
2439 assert_eq!(fit.iterations, 3);
2440 assert_eq!(fit.backtracks, 11);
2441 assert_eq!(fit.convergence, CONV_CRITICAL_POINT);
2442 }
2443
2444 #[test]
2445 fn test_opt_pml_stalled_reports_minus_forty() {
2446 let data = fixture_a();
2447 let variance = PmlVariance {
2448 subject: 8.0,
2449 cell: 0.001,
2450 };
2451 let fit = opt_pml(&data, &[20.0, 0.0], &variance, None).expect("fit");
2452
2453 assert_slice(&fit.beta, &[14.9609375, -4.9609375]);
2454 assert_slice(&fit.log_w, &[-4.9609375, -4.9609375, 4.9962571608576489]);
2455 assert_eq!(fit.iterations, 2);
2456 assert_eq!(fit.backtracks, 12);
2457 assert_eq!(fit.convergence, CONV_STALLED);
2458 }
2459
2460 #[test]
2461 fn test_opt_pml_iteration_budget_reports_minus_twenty() {
2462 let data = fixture_a();
2464 let variance = PmlVariance {
2465 subject: 0.25,
2466 cell: 2.0,
2467 };
2468 let params = PmlParams {
2469 eps: -1e-300,
2470 ..Default::default()
2471 };
2472 let fit = opt_pml(&data, &[0.5, -0.25], &variance, Some(params)).expect("fit");
2473
2474 assert_eq!(fit.iterations, 50);
2475 assert_eq!(fit.convergence, CONV_MAX_ITER);
2476 assert_slice(&fit.beta, &[1.1055943907637629, -0.98348970166026151]);
2477 assert_slice(
2478 &fit.information,
2479 &[
2480 5.4708368635191373,
2481 2.619399280826618,
2482 2.619399280826618,
2483 4.0223986123865263,
2484 ],
2485 );
2486 }
2487
2488 #[test]
2489 fn test_opt_pml_collinear_design_reports_minus_twentyfive() {
2490 let design: Vec<f64> = DESIGN
2492 .chunks_exact(2)
2493 .flat_map(|r| [r[0], r[1], r[1]])
2494 .collect();
2495 let data = PmlData {
2496 design: &design,
2497 offset: &OFFSET,
2498 counts: &COUNTS,
2499 cell_index: &CELL_INDEX,
2500 subject_start: &SUBJECT_START,
2501 subject_total: &SUBJECT_TOTAL,
2502 };
2503 let variance = PmlVariance {
2504 subject: 0.25,
2505 cell: 2.0,
2506 };
2507 let fit = opt_pml(&data, &[0.5, -0.25, 0.1], &variance, None).expect("fit");
2508
2509 assert_eq!(fit.convergence, CONV_SINGULAR);
2510 assert_slice(
2511 &fit.beta,
2512 &[1.1055943330380458, -1.0834897078722583, 0.10000000000000001],
2513 );
2514 assert_relative_eq!(fit.log_det, 6.042689573294485, max_relative = TOL);
2515 }
2516
2517 #[test]
2518 fn test_opt_pml_single_coefficient() {
2519 let design = [1.0; 12];
2520 let data = PmlData {
2521 design: &design,
2522 offset: &OFFSET,
2523 counts: &COUNTS,
2524 cell_index: &CELL_INDEX,
2525 subject_start: &SUBJECT_START,
2526 subject_total: &SUBJECT_TOTAL,
2527 };
2528 let variance = PmlVariance {
2529 subject: 0.25,
2530 cell: 2.0,
2531 };
2532 let params = PmlParams {
2533 reml: true,
2534 ord: 3,
2535 ..Default::default()
2536 };
2537 let fit = opt_pml(&data, &[0.0], &variance, Some(params)).expect("fit");
2538
2539 assert_slice(&fit.beta, &[0.64526354365336647]);
2540 assert_slice(
2541 &fit.log_w,
2542 &[
2543 0.040002950937536336,
2544 0.10805651074009694,
2545 0.21875064209166895,
2546 ],
2547 );
2548 assert_slice(&fit.information, &[5.7533150000267472]);
2549 assert_relative_eq!(fit.log_det, 7.9235375511695514, max_relative = TOL);
2550 assert_relative_eq!(fit.second_order, 0.0044134970888637676, max_relative = TOL);
2551 }
2552
2553 #[test]
2554 fn test_opt_pml_nbm_matches_nebula() {
2555 let data = fixture_a();
2556 let variance = PmlVariance {
2557 subject: 0.3,
2558 cell: 2.0,
2559 };
2560 let params = PmlParams {
2561 reml: true,
2562 ..Default::default()
2563 };
2564 let fit = opt_pml_nbm(&data, &[0.5, -0.25], &variance, Some(params)).expect("fit");
2565
2566 assert_slice(&fit.beta, &[1.2213403122060453, -0.98985988766009869]);
2567 assert_slice(
2568 &fit.log_w,
2569 &[
2570 -0.14301073703498665,
2571 -0.095894342874540239,
2572 0.23890507990952689,
2573 ],
2574 );
2575 assert_slice(
2576 &fit.information,
2577 &[
2578 5.4025646031315233,
2579 2.5076353884976541,
2580 2.5076353884976541,
2581 3.9141513741254204,
2582 ],
2583 );
2584 assert_relative_eq!(fit.log_likelihood, -49.318101708775217, max_relative = TOL);
2585 assert_relative_eq!(fit.log_det, 8.6579114426780528, max_relative = TOL);
2586 assert_eq!(fit.second_order, 0.0);
2587 assert_eq!(fit.iterations, 3);
2588 assert_eq!(fit.convergence, CONV_SUCCESS);
2589 }
2590
2591 #[test]
2592 fn test_pml_log_likelihood_gradient_matches_nebula() {
2593 let data = fixture_a();
2597 let variance = PmlVariance {
2598 subject: 0.25,
2599 cell: 2.0,
2600 };
2601 let out = pml_log_likelihood_gradient(&data, &[0.5, -0.25], &[0.1, -0.2, 0.05], &variance)
2602 .expect("gradient");
2603
2604 assert_relative_eq!(out.objective, 53.926203132942852, max_relative = TOL);
2605 assert_slice(
2606 &out.gradient,
2607 &[
2608 -5.2597994101747254,
2609 -0.19846989257393055,
2610 -0.40624529144311583,
2611 -2.8367693486273806,
2612 -2.0229915782218155,
2613 ],
2614 );
2615 }
2616
2617 fn fixture_large() -> (Vec<f64>, Vec<f64>, Vec<f64>, Vec<usize>, Vec<f64>) {
2626 let n_cells = 64usize;
2627 let mut design = Vec::with_capacity(n_cells * 3);
2628 let mut offset = Vec::with_capacity(n_cells);
2629 let mut counts = Vec::new();
2630 let mut cell_index = Vec::new();
2631 let mut subject_total = vec![0.0; 4];
2632 let starts = [0usize, 12, 32, 48, 64];
2633 for i in 0..n_cells {
2634 design.push(1.0);
2635 design.push(if i % 3 == 0 { 1.0 } else { 0.0 });
2636 design.push((i % 7) as f64 / 8.0);
2637 offset.push(((i % 9) as f64 - 4.0) / 8.0);
2638 let y = ((i * 7 + 3) % 11) as f64;
2639 if y > 0.0 {
2640 counts.push(y);
2641 cell_index.push(i);
2642 }
2643 let s = starts.iter().rposition(|&b| b <= i).unwrap_or(0).min(3);
2644 subject_total[s] += y;
2645 }
2646 (design, offset, counts, cell_index, subject_total)
2647 }
2648
2649 #[test]
2650 fn test_opt_pml_wider_design_and_more_cells() {
2651 let (design, offset, counts, cell_index, subject_total) = fixture_large();
2652 assert_eq!(subject_total, vec![58.0, 100.0, 83.0, 82.0]);
2653 let starts = [0usize, 12, 32, 48, 64];
2654 let data = PmlData {
2655 design: &design,
2656 offset: &offset,
2657 counts: &counts,
2658 cell_index: &cell_index,
2659 subject_start: &starts,
2660 subject_total: &subject_total,
2661 };
2662 let variance = PmlVariance {
2663 subject: 0.35,
2664 cell: 1.5,
2665 };
2666 let params = PmlParams {
2667 reml: true,
2668 ord: 3,
2669 ..Default::default()
2670 };
2671 let fit = opt_pml(&data, &[0.5, -0.25, 0.75], &variance, Some(params)).expect("fit");
2672
2673 assert_slice(
2674 &fit.beta,
2675 &[
2676 1.4272053191863423,
2677 0.20743588590645803,
2678 -0.017982095723719935,
2679 ],
2680 );
2681 assert_slice(
2682 &fit.log_w,
2683 &[
2684 0.21944093016930771,
2685 0.12415265795703513,
2686 0.20913336580790309,
2687 0.14393280971073011,
2688 ],
2689 );
2690 assert_slice(
2691 &fit.information,
2692 &[
2693 8.398261243665857,
2694 2.7062153540842324,
2695 3.0637064599168475,
2696 2.7062153540842324,
2697 16.226178650135836,
2698 1.2821611476475319,
2699 3.0637064599168475,
2700 1.2821611476475319,
2701 5.5864643706193053,
2702 ],
2703 );
2704 assert_relative_eq!(fit.log_likelihood, -276.5244116459354, max_relative = TOL);
2705 assert_relative_eq!(fit.log_det, 18.407453390744379, max_relative = TOL);
2706 assert_relative_eq!(fit.second_order, 0.0029993806316514779, max_relative = TOL);
2707 assert_eq!(fit.iterations, 4);
2708 assert_eq!(fit.convergence, CONV_SUCCESS);
2709
2710 let plain = opt_pml(&data, &[0.5, -0.25, 0.75], &variance, None).expect("fit");
2712 assert_relative_eq!(plain.log_det, 12.052188343414912, max_relative = TOL);
2713 assert_eq!(plain.second_order, 0.0);
2714 assert_slice(&plain.beta, &fit.beta);
2715 }
2716
2717 #[test]
2718 fn test_check_convergence_bound_and_nan_branches() {
2719 let base = PmlResult {
2720 beta: vec![0.0, 0.0],
2721 log_w: vec![0.0],
2722 information: vec![4.0, 0.0, 0.0, 4.0],
2723 log_likelihood: -1.0,
2724 log_likelihood_prev: -1.0,
2725 log_det: 0.0,
2726 second_order: 0.0,
2727 convergence: CONV_SUCCESS,
2728 iterations: 3,
2729 backtracks: 0,
2730 };
2731 let params = PmlParams::default();
2732
2733 assert_eq!(
2734 check_convergence(&base, ¶ms, false, CONV_SUCCESS),
2735 CONV_SUCCESS
2736 );
2737 assert_eq!(
2738 check_convergence(&base, ¶ms, true, CONV_SUCCESS),
2739 CONV_AT_BOUND
2740 );
2741
2742 let nan = PmlResult {
2743 log_likelihood: f64::NAN,
2744 ..base.clone()
2745 };
2746 assert_eq!(
2747 check_convergence(&nan, ¶ms, false, CONV_SUCCESS),
2748 CONV_NOT_FINITE
2749 );
2750
2751 assert_eq!(check_convergence(&base, ¶ms, false, -50), -50);
2753
2754 let singular = PmlResult {
2756 information: vec![1.0, 1.0, 1.0, 1.0],
2757 ..base.clone()
2758 };
2759 assert_eq!(
2760 check_convergence(&singular, ¶ms, true, CONV_SUCCESS),
2761 CONV_SINGULAR
2762 );
2763 }
2764
2765 #[test]
2766 fn test_min_eigenvalue_known_matrix() {
2767 let a = [2.0, 1.0, 1.0, 2.0];
2769 assert_relative_eq!(min_eigenvalue(&a, 2), 1.0, max_relative = 1e-13);
2770 assert_relative_eq!(min_eigenvalue(&[7.0], 1), 7.0, max_relative = 1e-15);
2771 }
2772
2773 #[test]
2774 fn test_ldlt_solve_indefinite_system() {
2775 let mut a = [1.0, 2.0, 2.0, 1.0];
2777 let mut b = [3.0, 3.0];
2778 let mut perm = [0usize; 2];
2779 let mut tmp = [0.0; 2];
2780 ldlt_solve(&mut a, &mut b, 2, &mut perm, &mut tmp);
2781 assert_relative_eq!(b[0], 1.0, max_relative = 1e-13);
2782 assert_relative_eq!(b[1], 1.0, max_relative = 1e-13);
2783 }
2784
2785 #[test]
2786 fn test_determinant_matches_closed_form() {
2787 let mut a = [4.0, 1.0, 2.0, 1.0, 5.0, 3.0, 2.0, 3.0, 6.0];
2788 let mut perm = [0usize; 3];
2789 assert_relative_eq!(
2791 determinant(&mut a, 3, &mut perm),
2792 70.0,
2793 max_relative = 1e-13
2794 );
2795 }
2796
2797 #[test]
2798 fn test_rejects_malformed_input() {
2799 let data = fixture_a();
2800 let variance = PmlVariance {
2801 subject: 0.25,
2802 cell: 2.0,
2803 };
2804 assert!(opt_pml(&data, &[0.5], &variance, None).is_err());
2805
2806 let bad_gamma = PmlVariance {
2807 subject: 0.25,
2808 cell: 0.0,
2809 };
2810 assert!(opt_pml(&data, &[0.5, -0.25], &bad_gamma, None).is_err());
2811
2812 let bad_subject = PmlVariance {
2813 subject: 0.0,
2814 cell: 2.0,
2815 };
2816 assert!(opt_pml(&data, &[0.5, -0.25], &bad_subject, None).is_err());
2817
2818 let short = PmlData {
2819 subject_total: &SUBJECT_TOTAL[..2],
2820 ..data
2821 };
2822 assert!(opt_pml(&short, &[0.5, -0.25], &variance, None).is_err());
2823 }
2824
2825 fn categorical_gene() -> (
2829 Vec<f64>,
2830 Vec<f64>,
2831 Vec<f64>,
2832 Vec<usize>,
2833 Vec<f64>,
2834 Vec<usize>,
2835 ) {
2836 use rand::prelude::*;
2837 use rand::rngs::SmallRng;
2838 let mut rng = SmallRng::seed_from_u64(11);
2839 let starts = vec![0usize, 400, 800, 1200];
2840 let mut design = Vec::new();
2841 let mut offset = Vec::new();
2842 let mut counts = Vec::new();
2843 let mut cell_index = Vec::new();
2844 let mut subject_total = vec![0.0; 3];
2845 for c in 0..1200 {
2846 let s = c / 400;
2847 let kind = f64::from(rng.random_bool(0.4));
2848 design.extend([1.0, (s % 2) as f64, kind]);
2849 let o: f64 = rng.random_range(-2.3..2.3);
2850 offset.push(o);
2851 if rng.random_bool(0.1 + 0.05 * kind) {
2852 let y = rng.random_range(1..6) as f64;
2853 counts.push(y);
2854 cell_index.push(c);
2855 subject_total[s] += y;
2856 }
2857 }
2858 (design, offset, counts, cell_index, subject_total, starts)
2859 }
2860
2861 #[test]
2862 fn test_opt_pml_tables_match_the_dense_sweep() {
2863 let (design, offset, counts, cell_index, subject_total, starts) = categorical_gene();
2864 let data = PmlData {
2865 design: &design,
2866 offset: &offset,
2867 counts: &counts,
2868 cell_index: &cell_index,
2869 subject_start: &starts,
2870 subject_total: &subject_total,
2871 };
2872 let zeros = ZeroCells::build(&design, &offset, &starts, 3).expect("categorical");
2873 for (subject, cell) in [(0.3, 0.05), (0.8, 2.0), (0.05, 40.0)] {
2874 let variance = PmlVariance { subject, cell };
2875 let params = PmlParams {
2876 reml: true,
2877 ord: 3,
2878 ..Default::default()
2879 };
2880 let start = [-1.5, 0.2, 0.1];
2881 let dense = opt_pml(&data, &start, &variance, Some(params)).expect("dense");
2882 let tabled = opt_pml_tabled(&data, Some(&zeros), &start, &variance, Some(params))
2883 .expect("tabled");
2884 assert_eq!(dense.iterations, tabled.iterations);
2885 for (a, b) in dense.beta.iter().zip(&tabled.beta) {
2886 assert_relative_eq!(*a, *b, max_relative = 1e-11);
2887 }
2888 for (a, b) in dense.information.iter().zip(&tabled.information) {
2889 assert_relative_eq!(*a, *b, max_relative = 1e-11);
2890 }
2891 assert_relative_eq!(
2892 dense.log_likelihood,
2893 tabled.log_likelihood,
2894 max_relative = 1e-13
2895 );
2896 assert_relative_eq!(dense.log_det, tabled.log_det, max_relative = 1e-12);
2897 assert_relative_eq!(
2898 dense.second_order,
2899 tabled.second_order,
2900 max_relative = 1e-10
2901 );
2902 }
2903 }
2904}