use super::*;
use crate::{
BinomialLink, Family, GroupIds, Grouping, GroupingRelation, ModelSpec, NegBinomialLink,
ReStructure, Sizing, StartValues,
};
pub(super) fn sim_clustered(csv: &str) -> (Vec<f64>, Vec<f64>, Vec<u32>, usize) {
let mut x = Vec::<f64>::new();
let mut y = Vec::<f64>::new();
let mut raw = Vec::<u32>::new();
for line in csv.lines().skip(1).filter(|l| !l.trim().is_empty()) {
let f: Vec<&str> = line.split(',').map(|s| s.trim_matches('"')).collect();
raw.push(f[0].parse().unwrap());
x.extend_from_slice(&[
1.0,
f[1].parse().unwrap(),
f64::from(u32::from(f[2] == "b")),
]);
y.push(f[3].parse().unwrap());
}
let (ids, nc) = dense_ids(&raw);
(x, y, ids, nc)
}
#[test]
#[should_panic(expected = "n elements")]
fn weights_shape_still_asserted() {
let n = 4;
let x = vec![1.0f64; n];
let y = vec![1.0, 2.0, 3.0, 4.0];
let model = ModelSpec {
family: Family::Gaussian,
re: None,
};
let opts = FitOptions {
weights: Some(vec![1.0; n - 1]),
..FitOptions::default()
};
let _ = fit_cold(&x, &y, n, 1, &model, &GroupIds::default(), &opts);
}
#[test]
fn fit_rank_deficient_drops_and_matches_reduced() {
let n = 30;
let p = 3;
let mut st = 7u64;
let mut x = vec![0.0f64; n * p];
let mut y = vec![0.0f64; n];
for i in 0..n {
let x1 = lcg(&mut st);
x[i * p] = 1.0;
x[i * p + 1] = x1;
x[i * p + 2] = 1.0 + x1; y[i] = 0.5 + 0.4 * x1 + 0.3 * lcg(&mut st);
}
let model = ModelSpec {
family: Family::Gaussian,
re: None,
};
let f = fit_cold(
&x,
&y,
n,
p,
&model,
&GroupIds::default(),
&FitOptions {
target_indices: vec![0, 1, 2],
..FitOptions::default()
},
);
assert!(f.converged, "reduced OLS must converge");
assert_eq!(
f.aliased,
vec![false, false, true],
"later collinear column dropped"
);
assert!(f.beta[2].is_nan(), "aliased β = NaN");
assert!(f.se[2].is_nan(), "aliased se = NaN");
let mut xr = vec![0.0f64; n * 2];
for i in 0..n {
xr[i * 2] = x[i * p];
xr[i * 2 + 1] = x[i * p + 1];
}
let fr = fit_cold(
&xr,
&y,
n,
2,
&model,
&GroupIds::default(),
&FitOptions {
target_indices: vec![0, 1],
..FitOptions::default()
},
);
assert!(
(f.beta[0] - fr.beta[0]).abs() < 1e-9,
"β0 {} vs reduced {}",
f.beta[0],
fr.beta[0]
);
assert!(
(f.beta[1] - fr.beta[1]).abs() < 1e-9,
"β1 {} vs reduced {}",
f.beta[1],
fr.beta[1]
);
}
#[derive(serde::Deserialize)]
struct ColEst {
beta: Vec<Option<f64>>,
}
#[derive(serde::Deserialize)]
struct ColGolden {
estimates: ColEst,
}
#[test]
fn fit_sim_collinear_matches_lme4_drop() {
let raw = include_str!("../../parity/goldens/sim_collinear_glm.json");
let gold: ColGolden = serde_json::from_str(raw).expect("golden JSON parses");
let csv = include_str!("../../parity/data_simulated/sim_collinear.csv");
let mut y = Vec::<f64>::new();
let mut cols: Vec<[f64; 3]> = Vec::new();
for line in csv.lines().skip(1).filter(|l| !l.trim().is_empty()) {
let f: Vec<&str> = line.split(',').map(|s| s.trim_matches('"')).collect();
y.push(f[0].parse().unwrap());
cols.push([
f[1].parse().unwrap(),
f[2].parse().unwrap(),
f[3].parse().unwrap(),
]);
}
let n = y.len();
let p = 4; let mut x = vec![0.0f64; n * p];
for i in 0..n {
x[i * p] = 1.0;
x[i * p + 1] = cols[i][0];
x[i * p + 2] = cols[i][1];
x[i * p + 3] = cols[i][2];
}
let model = ModelSpec {
family: Family::Gaussian,
re: None,
};
let f = fit_cold(
&x,
&y,
n,
p,
&model,
&GroupIds::default(),
&FitOptions {
target_indices: vec![0, 1, 2, 3],
..FitOptions::default()
},
);
assert!(f.converged, "reduced fit converges");
let r_aliased: Vec<bool> = gold.estimates.beta.iter().map(|b| b.is_none()).collect();
assert_eq!(
f.aliased, r_aliased,
"glmm must drop the same column R does"
);
for (j, rb) in gold.estimates.beta.iter().enumerate() {
match rb {
Some(v) => assert!(
(f.beta[j] - v).abs() / v.abs().max(1e-6) < 1e-3,
"β{j} {} vs {v}",
f.beta[j]
),
None => assert!(f.beta[j].is_nan(), "β{j} must be NaN (aliased)"),
}
}
}
pub(super) fn lcg(state: &mut u64) -> f64 {
*state = state
.wrapping_mul(6364136223846793005)
.wrapping_add(1442695040888963407);
(((*state >> 11) as f64) / ((1u64 << 53) as f64)) * 2.0 - 1.0
}
pub(super) fn lmm_hand_dataset() -> (Vec<f64>, Vec<f64>, usize, usize) {
let n = 48usize;
let p = 3;
let n_clusters = 6usize;
let mut st = 42u64;
let u_c: Vec<f64> = (0..n_clusters).map(|_| 0.6 * lcg(&mut st)).collect();
let mut x = vec![0.0f64; n * p];
let mut y = vec![0.0f64; n];
for i in 0..n {
let c = i % n_clusters;
let x1 = lcg(&mut st);
let x2 = lcg(&mut st);
x[i * p] = 1.0;
x[i * p + 1] = x1;
x[i * p + 2] = x2;
y[i] = 0.5 + 0.4 * x1 - 0.2 * x2 + u_c[c] + 0.8 * lcg(&mut st);
}
(x, y, n, p)
}
#[test]
fn theta_width_counts_vech_blocks() {
let re = ReStructure {
sizing: Sizing::FixedClusters { n_clusters: 4 },
slopes: vec![1],
extra_groupings: vec![Grouping {
relation: GroupingRelation::Crossed { n_clusters: 3 },
slopes: vec![],
}],
};
assert_eq!(super::common::theta_width(Some(&re)), 3 + 1);
assert_eq!(super::common::theta_width(None), 0);
}
#[test]
fn varcorr_block_is_scaled_lambda_gram() {
let vech = super::common::varcorr_block(&[2.0, 0.5, 1.0], 2, 1.0);
assert_eq!(vech.len(), 3);
assert!((vech[0] - 4.0).abs() < 1e-12, "D00 {}", vech[0]);
assert!((vech[1] - 1.0).abs() < 1e-12, "D10 {}", vech[1]);
assert!((vech[2] - 1.25).abs() < 1e-12, "D11 {}", vech[2]);
let scaled = super::common::varcorr_block(&[2.0, 0.5, 1.0], 2, 3.0);
assert!((scaled[0] - 12.0).abs() < 1e-12);
assert!((scaled[2] - 3.75).abs() < 1e-12);
}
fn fit_with_varcorr(vech: Vec<f64>) -> Fit {
Fit {
beta: vec![],
se: vec![],
vcov: vec![],
tau2: vec![],
dispersion: 1.0,
converged: true,
varcorr: vec![vech],
stddev_se: vec![],
aliased: vec![],
n_eval: 0,
deviance: f64::NAN,
singular: false,
loglik: f64::NAN,
df: 0,
reml: false,
fitted: vec![],
ranef: vec![],
ranef_levels: vec![],
}
}
#[test]
fn stddev_corr_q1_trivial() {
let f = fit_with_varcorr(vec![9.0]);
let (sd, corr) = f.stddev_corr(0);
assert_eq!(sd, vec![3.0]);
assert_eq!(corr, vec![vec![1.0]]);
}
#[test]
fn stddev_corr_q2_hand_math() {
let f = fit_with_varcorr(vec![4.0, 1.0, 1.25]);
let (sd, corr) = f.stddev_corr(0);
let sd1 = 1.25_f64.sqrt();
assert!((sd[0] - 2.0).abs() < 1e-12);
assert!((sd[1] - sd1).abs() < 1e-12);
assert_eq!(corr[0][0], 1.0);
assert_eq!(corr[1][1], 1.0);
let rho = 1.0 / (2.0 * sd1);
assert!((corr[0][1] - rho).abs() < 1e-12);
assert!((corr[1][0] - rho).abs() < 1e-12);
}
#[test]
fn stddev_corr_q3_hand_math() {
let f = fit_with_varcorr(vec![4.0, 1.0, 2.0, 9.0, 3.0, 16.0]);
let (sd, corr) = f.stddev_corr(0);
assert_eq!(sd, vec![2.0, 3.0, 4.0]);
#[allow(clippy::needless_range_loop)]
for i in 0..3 {
assert_eq!(corr[i][i], 1.0);
}
assert!((corr[0][1] - 1.0 / 6.0).abs() < 1e-12);
assert!((corr[1][0] - 1.0 / 6.0).abs() < 1e-12);
assert!((corr[0][2] - 0.25).abs() < 1e-12);
assert!((corr[2][0] - 0.25).abs() < 1e-12);
assert!((corr[1][2] - 0.25).abs() < 1e-12);
assert!((corr[2][1] - 0.25).abs() < 1e-12);
}
#[test]
fn assemble_varcorr_one_block_per_grouping() {
let g = crate::lmm::LmmGroupings::from_cluster_spec_ext(
&ModelSpec {
family: Family::Gaussian,
re: Some(ReStructure {
sizing: Sizing::FixedClusters { n_clusters: 4 },
slopes: vec![1],
extra_groupings: vec![Grouping {
relation: GroupingRelation::Crossed { n_clusters: 3 },
slopes: vec![],
}],
}),
},
16,
&[1],
&[],
);
let theta = [2.0, 0.5, 1.0, 0.7];
let vc = super::assemble_varcorr(&theta, &g, 1.0);
assert_eq!(vc.len(), 2);
assert_eq!(vc[0], vec![4.0, 1.0, 1.25]);
assert!((vc[1][0] - 0.49).abs() < 1e-12, "extra D {}", vc[1][0]);
}
#[test]
fn spec_sized_from_ids_derives_counts() {
let re = ReStructure {
sizing: Sizing::FixedClusters { n_clusters: 1 }, slopes: vec![],
extra_groupings: vec![
Grouping {
relation: GroupingRelation::Crossed { n_clusters: 1 },
slopes: vec![],
},
Grouping {
relation: GroupingRelation::NestedWithin { n_per_parent: 1 },
slopes: vec![],
},
],
};
let model = ModelSpec {
family: Family::Gaussian,
re: Some(re),
};
let ids = GroupIds {
primary: vec![0, 1, 2, 0, 1, 2], extra: vec![vec![0, 0, 1, 1, 2, 2], vec![0, 1, 2, 3, 4, 5]], };
let sized = super::spec_sized_from_ids(&model, &ids);
let sre = sized.re.unwrap();
assert_eq!(sre.sizing, Sizing::FixedClusters { n_clusters: 3 });
assert_eq!(
sre.extra_groupings[0].relation,
GroupingRelation::Crossed { n_clusters: 3 }
);
assert_eq!(
sre.extra_groupings[1].relation,
GroupingRelation::NestedWithin { n_per_parent: 2 }
);
}
#[test]
fn spec_sized_from_ids_nested_unbalanced_uses_true_max_per_parent() {
let re = ReStructure {
sizing: Sizing::FixedClusters { n_clusters: 1 },
slopes: vec![],
extra_groupings: vec![Grouping {
relation: GroupingRelation::NestedWithin { n_per_parent: 1 },
slopes: vec![],
}],
};
let model = ModelSpec {
family: Family::Gaussian,
re: Some(re),
};
let ids = GroupIds {
primary: vec![0, 1, 1, 2, 2, 2],
extra: vec![vec![0, 3, 4, 6, 7, 8]],
};
let sized = super::spec_sized_from_ids(&model, &ids);
let sre = sized.re.unwrap();
assert_eq!(sre.sizing, Sizing::FixedClusters { n_clusters: 3 });
assert_eq!(
sre.extra_groupings[0].relation,
GroupingRelation::NestedWithin { n_per_parent: 3 }
);
}
#[test]
fn spec_sized_from_ids_nested_unbalanced_first_parent_widest() {
let re = ReStructure {
sizing: Sizing::FixedClusters { n_clusters: 1 },
slopes: vec![],
extra_groupings: vec![Grouping {
relation: GroupingRelation::NestedWithin { n_per_parent: 1 },
slopes: vec![],
}],
};
let model = ModelSpec {
family: Family::Gaussian,
re: Some(re),
};
let ids = GroupIds {
primary: vec![0, 0, 0, 1, 1, 2],
extra: vec![vec![0, 1, 2, 3, 4, 5]],
};
let sized = super::spec_sized_from_ids(&model, &ids);
let sre = sized.re.unwrap();
assert_eq!(
sre.extra_groupings[0].relation,
GroupingRelation::NestedWithin { n_per_parent: 3 }
);
}
#[test]
fn fit_extra_grouping_q_too_large_routes_sparse() {
let model = ModelSpec {
family: Family::Gaussian,
re: Some(ReStructure {
sizing: Sizing::FixedClusters { n_clusters: 4 },
slopes: vec![],
extra_groupings: vec![Grouping {
relation: GroupingRelation::Crossed { n_clusters: 4 },
slopes: vec![1, 2, 3, 4], }],
}),
};
assert!(matches!(classify_design(&model, 1), Solver::Sparse));
let n = 0;
let p = 5; let fit = fit_cold(
&[],
&[],
n,
p,
&model,
&GroupIds::from_sizing(model.re.as_ref().unwrap(), n),
&FitOptions {
target_indices: vec![1],
..FitOptions::default()
},
);
assert!(!fit.converged);
}
#[test]
fn fit_too_many_extra_groupings_routes_sparse() {
let extra_groupings: Vec<Grouping> = (0..7)
.map(|_| Grouping {
relation: GroupingRelation::Crossed { n_clusters: 2 },
slopes: vec![],
})
.collect();
let model = ModelSpec {
family: Family::Gaussian,
re: Some(ReStructure {
sizing: Sizing::FixedClusters { n_clusters: 2 },
slopes: vec![],
extra_groupings,
}),
};
assert!(matches!(classify_design(&model, 1), Solver::Sparse));
let (n, p) = (8, 1);
let x = vec![1.0f64; n * p];
let y = vec![0.0f64; n];
let ids = GroupIds {
primary: vec![0; n],
extra: vec![vec![0; n]; 7],
};
let fit = fit_cold(
&x,
&y,
n,
p,
&model,
&ids,
&FitOptions {
target_indices: vec![0],
..FitOptions::default()
},
);
assert_eq!(fit.beta.len(), p);
}
#[test]
fn fit_over_envelope_non_gaussian_never_panics() {
let families = [
Family::Binomial {
link: BinomialLink::Logit,
},
Family::Poisson {
link: crate::PoissonLink::Log,
},
Family::Gamma {
link: crate::GammaLink::Log,
},
Family::NegativeBinomial {
link: NegBinomialLink::Log,
},
];
for family in families {
let extra_groupings: Vec<Grouping> = (0..7)
.map(|_| Grouping {
relation: GroupingRelation::Crossed { n_clusters: 2 },
slopes: vec![],
})
.collect();
let model = ModelSpec {
family,
re: Some(ReStructure {
sizing: Sizing::FixedClusters { n_clusters: 2 },
slopes: vec![],
extra_groupings,
}),
};
assert!(matches!(classify_design(&model, 1), Solver::Sparse));
let (n, p) = (8, 1);
let x = vec![1.0f64; n * p];
let y = vec![1.0f64; n];
let ids = GroupIds {
primary: vec![0; n],
extra: vec![vec![0; n]; 7],
};
let fit = fit_cold(
&x,
&y,
n,
p,
&model,
&ids,
&FitOptions {
target_indices: vec![0],
..FitOptions::default()
},
);
assert_eq!(fit.beta.len(), p, "{family:?} over-count returns a Fit");
let model_w = ModelSpec {
family,
re: Some(ReStructure {
sizing: Sizing::FixedClusters { n_clusters: 4 },
slopes: vec![],
extra_groupings: vec![Grouping {
relation: GroupingRelation::Crossed { n_clusters: 4 },
slopes: vec![1, 2, 3, 4],
}],
}),
};
assert!(matches!(classify_design(&model_w, 1), Solver::Sparse));
let (n, p) = (16, 5);
let mut st = 11u64;
let x: Vec<f64> = (0..n)
.flat_map(|_| {
let mut r = [0.0f64; 5];
r[0] = 1.0;
for v in r[1..].iter_mut() {
*v = lcg(&mut st);
}
r
})
.collect();
let y = vec![1.0f64; n];
let ids = GroupIds {
primary: (0..n as u32).map(|i| i % 4).collect(),
extra: vec![(0..n as u32).map(|i| (i / 4) % 4).collect()],
};
let fit = fit_cold(
&x,
&y,
n,
p,
&model_w,
&ids,
&FitOptions {
target_indices: vec![1],
..FitOptions::default()
},
);
assert_eq!(fit.beta.len(), p, "{family:?} over-width returns a Fit");
}
}
#[test]
fn fit_cold_equals_fit_warm_none() {
let (x, y, n, p) = lmm_hand_dataset();
let model = ModelSpec {
family: Family::Gaussian,
re: Some(ReStructure {
sizing: Sizing::FixedClusters { n_clusters: 6 },
slopes: vec![],
extra_groupings: vec![],
}),
};
let ids = GroupIds::from_sizing(model.re.as_ref().unwrap(), n);
let opts = FitOptions {
target_indices: vec![1, 2],
..FitOptions::default()
};
let cold = fit_cold(&x, &y, n, p, &model, &ids, &opts);
let warm_none = fit_warm(&x, &y, n, p, &model, &ids, None, &opts);
let bits = |v: &[f64]| v.iter().map(|x| x.to_bits()).collect::<Vec<_>>();
assert_eq!(bits(&cold.beta), bits(&warm_none.beta));
assert_eq!(bits(&cold.se), bits(&warm_none.se));
assert_eq!(bits(&cold.tau2), bits(&warm_none.tau2));
assert_eq!(cold.dispersion.to_bits(), warm_none.dispersion.to_bits());
assert_eq!(cold.converged, warm_none.converged);
}
#[test]
fn fit_warm_start_reaches_cold_beta() {
let (x, y, n, p) = lmm_hand_dataset();
let model = ModelSpec {
family: Family::Gaussian,
re: Some(ReStructure {
sizing: Sizing::FixedClusters { n_clusters: 6 },
slopes: vec![],
extra_groupings: vec![],
}),
};
let ids = GroupIds::from_sizing(model.re.as_ref().unwrap(), n);
let opts = FitOptions {
target_indices: vec![1, 2],
..FitOptions::default()
};
let cold = fit_cold(&x, &y, n, p, &model, &ids, &opts);
let start = StartValues {
beta: vec![0.0; p],
theta: vec![5.0],
};
let warm = fit_warm(&x, &y, n, p, &model, &ids, Some(&start), &opts);
assert!(cold.converged && warm.converged, "both fits must converge");
for j in [1usize, 2] {
let (a, b) = (cold.beta[j], warm.beta[j]);
let d = (a - b).abs();
assert!(
d <= 1e-7 || d <= 1e-6 * a.abs().max(b.abs()),
"LMM MLE must be start-independent: β[{j}] cold {a} vs warm {b}"
);
}
}
pub(super) fn dense_ids(raw: &[u32]) -> (Vec<u32>, usize) {
use std::collections::HashMap;
let mut map: HashMap<u32, u32> = HashMap::new();
let mut next = 0u32;
let ids: Vec<u32> = raw
.iter()
.map(|&r| {
*map.entry(r).or_insert_with(|| {
let v = next;
next += 1;
v
})
})
.collect();
(ids, next as usize)
}
pub(super) fn dense_str(raw: &[String]) -> (Vec<u32>, usize) {
use std::collections::HashMap;
let mut map: HashMap<String, u32> = HashMap::new();
let mut next = 0u32;
let ids: Vec<u32> = raw
.iter()
.map(|r| {
*map.entry(r.clone()).or_insert_with(|| {
let v = next;
next += 1;
v
})
})
.collect();
(ids, next as usize)
}
#[test]
fn classify_routes_at_the_cap_edge() {
let in_env = ModelSpec {
family: Family::Gaussian,
re: Some(ReStructure {
sizing: Sizing::FixedClusters { n_clusters: 10 },
slopes: vec![],
extra_groupings: vec![],
}),
};
assert!(matches!(
super::classify_design_pub(&in_env, 1),
super::Solver::NoZ
));
let over = ModelSpec {
family: Family::Gaussian,
re: Some(ReStructure {
sizing: Sizing::FixedClusters { n_clusters: 10 },
slopes: vec![],
extra_groupings: (0..(crate::consts::MAX_EXTRA_GROUPINGS + 1))
.map(|_| Grouping {
relation: GroupingRelation::Crossed { n_clusters: 4 },
slopes: vec![],
})
.collect(),
}),
};
assert!(matches!(
super::classify_design_pub(&over, 1),
super::Solver::Sparse
));
let wide = ModelSpec {
family: Family::Gaussian,
re: Some(ReStructure {
sizing: Sizing::FixedClusters { n_clusters: 10 },
slopes: (1..=crate::consts::MAX_PRIMARY_Q as u32).collect(), extra_groupings: vec![],
}),
};
assert!(matches!(
super::classify_design_pub(&wide, 1),
super::Solver::Sparse
));
}
#[test]
fn classify_routes_many_crossed_levels_to_sparse() {
let cap = crate::consts::MAX_CROSSED_LEVELS as u32;
let spec = |extras: Vec<Grouping>| ModelSpec {
family: Family::Gaussian,
re: Some(ReStructure {
sizing: Sizing::FixedClusters { n_clusters: 10 },
slopes: vec![],
extra_groupings: extras,
}),
};
let crossed = |n_clusters: u32| Grouping {
relation: GroupingRelation::Crossed { n_clusters },
slopes: vec![],
};
let over = spec(vec![crossed(cap + 1)]);
assert!(matches!(
super::classify_design_pub(&over, 1),
super::Solver::Sparse
));
let sum_over = spec(vec![crossed(cap / 2 + 1), crossed(cap / 2 + 1)]);
assert!(matches!(
super::classify_design_pub(&sum_over, 1),
super::Solver::Sparse
));
let at_cap = spec(vec![crossed(cap)]);
assert!(matches!(
super::classify_design_pub(&at_cap, 1),
super::Solver::NoZ
));
let nested = spec(vec![Grouping {
relation: GroupingRelation::NestedWithin {
n_per_parent: cap + 1,
},
slopes: vec![],
}]);
assert!(matches!(
super::classify_design_pub(&nested, 1),
super::Solver::NoZ
));
}
#[test]
fn classify_routes_slope_extras_to_sparse_all_families() {
let spec = |family: Family, extra_slopes: Vec<u32>| ModelSpec {
family,
re: Some(ReStructure {
sizing: Sizing::FixedClusters { n_clusters: 10 },
slopes: vec![],
extra_groupings: vec![Grouping {
relation: GroupingRelation::Crossed { n_clusters: 5 },
slopes: extra_slopes,
}],
}),
};
let g_slope = spec(Family::Gaussian, vec![1]);
assert!(matches!(
super::classify_design_pub(&g_slope, 1),
super::Solver::Sparse
));
let g_int = spec(Family::Gaussian, vec![]);
assert!(matches!(
super::classify_design_pub(&g_int, 1),
super::Solver::NoZ
));
let p_slope = spec(
Family::Poisson {
link: crate::PoissonLink::Log,
},
vec![1],
);
assert!(matches!(
super::classify_design_pub(&p_slope, 1),
super::Solver::Sparse
));
}
#[test]
fn classify_fixed_only_is_noz() {
let ols = ModelSpec {
family: Family::Gaussian,
re: None,
};
assert!(matches!(
super::classify_design_pub(&ols, 1),
super::Solver::NoZ
));
}
#[test]
fn fixed_only_fit_runs_zero_bobyqa_evals() {
let n = 24;
let p = 2;
let mut st = 3u64;
let mut x = vec![0.0f64; n * p];
let mut xv = vec![0.0f64; n];
for i in 0..n {
let x1 = lcg(&mut st);
x[i * p] = 1.0;
x[i * p + 1] = x1;
xv[i] = x1;
}
let families: [(Family, Vec<f64>); 5] = [
(
Family::Gaussian,
(0..n).map(|i| 0.5 + 0.4 * xv[i]).collect(),
),
(
Family::Binomial {
link: BinomialLink::Logit,
},
(0..n).map(|i| f64::from(u32::from(xv[i] > 0.0))).collect(),
),
(
Family::Poisson {
link: crate::PoissonLink::Log,
},
(0..n).map(|i| f64::from(1 + (i % 4) as u32)).collect(),
),
(
Family::Gamma {
link: crate::GammaLink::Log,
},
(0..n).map(|i| 1.0 + 0.5 * (xv[i] + 1.0)).collect(),
),
(
Family::NegativeBinomial {
link: NegBinomialLink::Log,
},
(0..n).map(|i| f64::from(1 + (i % 4) as u32)).collect(),
),
];
for (family, y) in families {
let model = ModelSpec { family, re: None };
let f = fit_cold(
&x,
&y,
n,
p,
&model,
&GroupIds::default(),
&FitOptions {
target_indices: vec![0, 1],
..FitOptions::default()
},
);
assert_eq!(
f.n_eval, 0,
"{family:?} fixed-only fit must enter BOBYQA zero times"
);
}
}
fn assert_vcov_agrees_with_se(fit: &Fit, p: usize, ctx: &str) {
assert_eq!(fit.vcov.len(), p, "{ctx}: vcov is not p rows");
for row in &fit.vcov {
assert_eq!(row.len(), p, "{ctx}: vcov is not p×p");
}
for j in 0..p {
if fit.se[j].is_finite() {
assert!(
fit.vcov[j][j].is_finite(),
"{ctx}: finite se[{j}] alongside NaN vcov[{j}][{j}]"
);
let want = fit.se[j] * fit.se[j];
assert!(
(fit.vcov[j][j] - want).abs() <= 1e-9 * want.abs().max(1e-12),
"{ctx}: vcov[{j}][{j}] = {} vs se[{j}]² = {want}",
fit.vcov[j][j]
);
}
}
for i in 0..p {
for j in 0..p {
if fit.vcov[i][j].is_finite() || fit.vcov[j][i].is_finite() {
assert_eq!(fit.vcov[i][j], fit.vcov[j][i], "{ctx}: vcov not symmetric");
}
}
}
}
#[test]
fn vcov_diagonal_is_se_squared_on_every_path() {
let (x, y, n, p) = lmm_hand_dataset();
let all: Vec<u32> = (0..p as u32).collect();
let opts = FitOptions {
target_indices: all.clone(),
..FitOptions::default()
};
let ols = fit_cold(
&x,
&y,
n,
p,
&ModelSpec {
family: Family::Gaussian,
re: None,
},
&GroupIds::default(),
&opts,
);
assert!(ols.converged);
assert_vcov_agrees_with_se(&ols, p, "ols");
let ids = GroupIds {
primary: (0..n).map(|i| (i % 6) as u32).collect(),
extra: vec![],
};
let lmm_model = ModelSpec {
family: Family::Gaussian,
re: Some(ReStructure {
sizing: Sizing::FixedClusters { n_clusters: 6 },
slopes: vec![],
extra_groupings: vec![],
}),
};
let lmm = fit_cold(&x, &y, n, p, &lmm_model, &ids, &opts);
assert!(lmm.converged);
assert_vcov_agrees_with_se(&lmm, p, "lmm");
let yb: Vec<f64> = y.iter().map(|&v| f64::from(v > 0.5)).collect();
let binom = Family::Binomial {
link: BinomialLink::Logit,
};
let glm = fit_cold(
&x,
&yb,
n,
p,
&ModelSpec {
family: binom,
re: None,
},
&GroupIds::default(),
&opts,
);
assert!(glm.converged);
assert_vcov_agrees_with_se(&glm, p, "glm");
for wald_se in [WaldSe::Hessian, WaldSe::Rx] {
let glmm = fit_cold(
&x,
&yb,
n,
p,
&ModelSpec {
family: binom,
re: Some(ReStructure {
sizing: Sizing::FixedClusters { n_clusters: 6 },
slopes: vec![],
extra_groupings: vec![],
}),
},
&ids,
&FitOptions {
target_indices: all.clone(),
wald_se,
..FitOptions::default()
},
);
assert!(glmm.converged, "glmm {wald_se:?} did not converge");
assert_vcov_agrees_with_se(&glmm, p, &format!("glmm {wald_se:?}"));
}
}
#[test]
fn vcov_is_nan_outside_the_target_block() {
let (x, y, n, p) = lmm_hand_dataset();
let fit = fit_cold(
&x,
&y,
n,
p,
&ModelSpec {
family: Family::Gaussian,
re: None,
},
&GroupIds::default(),
&FitOptions {
target_indices: vec![2], ..FitOptions::default()
},
);
assert!(fit.converged);
assert!(fit.se[2].is_finite() && fit.vcov[2][2].is_finite());
assert!(fit.se[0].is_nan() && fit.se[1].is_nan());
for j in [0usize, 1] {
for i in 0..p {
assert!(
fit.vcov[i][j].is_nan() && fit.vcov[j][i].is_nan(),
"vcov must be NaN outside the target block at ({i},{j})"
);
}
}
assert_vcov_agrees_with_se(&fit, p, "targets subset");
}
#[test]
fn vcov_rows_are_nan_for_aliased_columns() {
let (x, y, n, _) = lmm_hand_dataset();
let p = 4;
let mut xa = vec![0.0f64; n * p];
for i in 0..n {
xa[i * p] = x[i * 3];
xa[i * p + 1] = x[i * 3 + 1];
xa[i * p + 2] = x[i * 3 + 2];
xa[i * p + 3] = x[i * 3 + 1]; }
let fit = fit_cold(
&xa,
&y,
n,
p,
&ModelSpec {
family: Family::Gaussian,
re: None,
},
&GroupIds::default(),
&FitOptions {
target_indices: (0..p as u32).collect(),
..FitOptions::default()
},
);
assert!(fit.aliased[3], "duplicate column must be detected aliased");
for i in 0..p {
assert!(
fit.vcov[i][3].is_nan(),
"aliased column keeps a NaN vcov col"
);
assert!(
fit.vcov[3][i].is_nan(),
"aliased column keeps a NaN vcov row"
);
}
assert_vcov_agrees_with_se(&fit, p, "aliased");
}