use faer::Mat;
use crate::{
Family, GroupIds, Grouping, GroupingRelation, ModelSpec, ReStructure, Sizing, StartValues,
};
use super::{Fit, FitOptions};
pub(super) fn theta_width(re: Option<&ReStructure>) -> usize {
let Some(re) = re else { return 0 };
let q_p = 1 + re.slopes.len();
let mut w = q_p * (q_p + 1) / 2;
for g in &re.extra_groupings {
let q_g = 1 + g.slopes.len();
w += q_g * (q_g + 1) / 2;
}
w
}
pub(super) fn varcorr_block(theta_block: &[f64], q: usize, scale: f64) -> Vec<f64> {
let mut lam = vec![0.0f64; q * q];
crate::lmm::primary_lambda(theta_block, q, &mut lam); let mut vech = Vec::with_capacity(q * (q + 1) / 2);
for c in 0..q {
for r in c..q {
let mut d = 0.0;
for k in 0..=c {
d += lam[r * q + k] * lam[c * q + k];
}
vech.push(scale * d);
}
}
vech
}
pub(crate) fn assemble_varcorr(
theta: &[f64],
groupings: &crate::lmm::LmmGroupings,
scale: f64,
) -> Vec<Vec<f64>> {
let mut out = Vec::with_capacity(1 + groupings.extra_q.len());
let mut cursor = 0usize;
for &q in std::iter::once(&groupings.primary_q).chain(groupings.extra_q.iter()) {
let vech = q * (q + 1) / 2;
out.push(varcorr_block(&theta[cursor..cursor + vech], q, scale));
cursor += vech;
}
out
}
pub(crate) fn lmm_loglik(deviance: f64, n: usize, p: usize) -> f64 {
if !deviance.is_finite() || n <= p {
return f64::NAN;
}
-0.5 * (deviance + (n - p) as f64 * (1.0 + (2.0 * std::f64::consts::PI).ln()))
}
pub(crate) fn glmm_loglik(
family: Family,
nb_theta: f64,
deviance: f64,
y: &[f64],
prior_w: Option<&[f64]>,
) -> f64 {
if !deviance.is_finite() {
return f64::NAN;
}
match family {
Family::Gamma { .. } => -0.5 * deviance,
_ => -0.5 * deviance + crate::family::saturated_loglik(family, nb_theta, y, prior_w),
}
}
pub(crate) fn model_df(
family: Family,
p_retained: usize,
n_theta: usize,
dispersion_fixed: bool,
) -> usize {
let scale = match family {
Family::Gaussian | Family::NegativeBinomial { .. } => 1,
Family::Gamma { .. } => usize::from(!dispersion_fixed),
Family::Binomial { .. } | Family::Poisson { .. } => 0,
};
p_retained + n_theta + scale
}
fn extra_vech_start(g: &crate::lmm::LmmGroupings, e: usize) -> usize {
if let Some(nf) = g.nested {
if nf.decl == e {
return nf.vech_start;
}
}
g.crossed
.iter()
.find(|cf| cf.decl == e)
.map(|cf| cf.vech_start)
.expect("an extra grouping is either nested or crossed")
}
pub(crate) fn ranef_level_counts(g: &crate::lmm::LmmGroupings) -> Vec<usize> {
let mut counts = Vec::with_capacity(1 + g.extra_q.len());
counts.push(g.n_primary);
for e in 0..g.extra_q.len() {
let is_nested = g.nested.map(|nf| nf.decl) == Some(e);
counts.push(if is_nested {
g.n_primary * g.nested_per_parent
} else {
g.crossed
.iter()
.find(|cf| cf.decl == e)
.map(|cf| cf.n_levels)
.expect("an extra grouping is either nested or crossed")
});
}
counts
}
pub(crate) fn assemble_ranef_dense(
theta: &[f64],
g: &crate::lmm::LmmGroupings,
u: &[f64],
) -> Vec<f64> {
let q = g.primary_q;
let mut lam = vec![0.0f64; q * q];
crate::lmm::primary_lambda(theta, q, &mut lam);
let mut out = Vec::with_capacity(g.k_total);
for lvl in 0..g.n_primary {
let base = lvl * q;
for r in 0..q {
let mut b = 0.0;
for c in 0..=r {
b += lam[r * q + c] * u[base + c];
}
out.push(b);
}
}
debug_assert!(!g.extra_slopes_any, "dense GLMM extras are intercept-only");
let counts = ranef_level_counts(g);
for (e, &off) in g.extra_offsets.iter().enumerate() {
let theta_e = theta[extra_vech_start(g, e)];
for l in 0..counts[e + 1] {
out.push(theta_e * u[off + l]);
}
}
out
}
pub(crate) fn assemble_ranef_sparse(
theta: &[f64],
g: &crate::lmm::LmmGroupings,
u: &[f64],
) -> Vec<f64> {
let q = g.primary_q;
let s = g.n_primary;
let mut lam = vec![0.0f64; q * q];
crate::lmm::primary_lambda(theta, q, &mut lam);
let mut out = Vec::with_capacity(g.k_total);
for f in 0..s {
for r in 0..q {
let mut b = 0.0;
for c in 0..=r {
b += lam[r * q + c] * u[c * s + f];
}
out.push(b);
}
}
let counts = ranef_level_counts(g);
for (e, &off) in g.extra_offsets.iter().enumerate() {
let q_g = g.extra_q[e];
let mut lam_g = vec![0.0f64; q_g * q_g];
crate::lmm::primary_lambda(&theta[extra_vech_start(g, e)..], q_g, &mut lam_g);
for l in 0..counts[e + 1] {
let base = off + l * q_g;
for r in 0..q_g {
let mut b = 0.0;
for c in 0..=r {
b += lam_g[r * q_g + c] * u[base + c];
}
out.push(b);
}
}
}
out
}
pub(super) fn spec_sized_from_ids(model: &ModelSpec, ids: &GroupIds) -> ModelSpec {
let Some(re) = model.re.as_ref() else {
return model.clone();
};
let level_count = |v: &[u32]| v.iter().copied().max().map(|m| m as usize + 1).unwrap_or(1);
let n_primary = level_count(&ids.primary);
let extra_groupings: Vec<Grouping> = re
.extra_groupings
.iter()
.enumerate()
.map(|(g, gr)| {
let relation = match gr.relation {
GroupingRelation::Crossed { .. } => {
let children = level_count(&ids.extra[g]);
GroupingRelation::Crossed {
n_clusters: children as u32,
}
}
GroupingRelation::NestedWithin { .. } => {
let mut per_parent: Vec<std::collections::HashSet<u32>> =
vec![Default::default(); n_primary];
for (&p, &c) in ids.primary.iter().zip(&ids.extra[g]) {
per_parent[p as usize].insert(c);
}
let n_per_parent = per_parent.iter().map(|s| s.len()).max().unwrap_or(1).max(1);
GroupingRelation::NestedWithin {
n_per_parent: n_per_parent as u32,
}
}
};
Grouping {
relation,
slopes: gr.slopes.clone(),
}
})
.collect();
ModelSpec {
family: model.family,
re: Some(ReStructure {
sizing: Sizing::FixedClusters {
n_clusters: n_primary as u32,
},
slopes: re.slopes.clone(),
extra_groupings,
}),
}
}
pub(super) fn detect_aliased(x: &[f64], n: usize, p: usize) -> Vec<bool> {
let mut gram = Mat::<f64>::zeros(p, p);
for i in 0..n {
for a in 0..p {
let xa = x[i * p + a];
for b in 0..=a {
gram[(a, b)] += xa * x[i * p + b];
}
}
}
crate::ols::aliased_columns(gram.as_ref(), p, crate::ols::ALIAS_EPS)
}
fn remap_spec_slopes(model: &ModelSpec, to_reduced: &[usize]) -> ModelSpec {
let Some(re) = model.re.as_ref() else {
return model.clone();
};
let remap = |cols: &[u32]| -> Vec<u32> {
cols.iter()
.map(|&c| {
let r = to_reduced[c as usize];
assert!(
r != usize::MAX,
"rank-deficient random-slope column {c}: an aliased fixed column is used as an RE slope (unsupported)"
);
r as u32
})
.collect()
};
let extra_groupings = re
.extra_groupings
.iter()
.map(|g| Grouping {
relation: g.relation.clone(),
slopes: remap(&g.slopes),
})
.collect();
ModelSpec {
family: model.family,
re: Some(ReStructure {
sizing: re.sizing.clone(),
slopes: remap(&re.slopes),
extra_groupings,
}),
}
}
#[allow(clippy::too_many_arguments)]
pub(super) fn fit_rank_deficient(
x: &[f64],
y: &[f64],
n: usize,
p: usize,
model: &ModelSpec,
ids: &GroupIds,
start: Option<&StartValues>,
opts: &FitOptions,
aliased: &[bool],
) -> Fit {
let kept: Vec<usize> = (0..p).filter(|&j| !aliased[j]).collect();
let pk = kept.len();
let mut to_reduced = vec![usize::MAX; p];
for (r, &orig) in kept.iter().enumerate() {
to_reduced[orig] = r;
}
let mut xr = vec![0.0f64; n * pk];
for i in 0..n {
for (r, &orig) in kept.iter().enumerate() {
xr[i * pk + r] = x[i * p + orig];
}
}
let model_r = remap_spec_slopes(model, &to_reduced);
let start_r = start.map(|s| StartValues {
beta: kept.iter().map(|&o| s.beta[o]).collect(),
theta: s.theta.clone(),
});
let targets_r: Vec<u32> = opts
.target_indices
.iter()
.filter(|&&t| !aliased[t as usize])
.map(|&t| to_reduced[t as usize] as u32)
.collect();
let opts_r = FitOptions {
target_indices: targets_r,
..opts.clone()
};
let fr = super::fit_warm(&xr, y, n, pk, &model_r, ids, start_r.as_ref(), &opts_r);
let mut beta = vec![f64::NAN; p];
let mut se = vec![f64::NAN; p];
for (r, &orig) in kept.iter().enumerate() {
beta[orig] = fr.beta[r];
se[orig] = fr.se[r];
}
let mut vcov = nan_vcov(p);
for (ri, &oi) in kept.iter().enumerate() {
for (rj, &oj) in kept.iter().enumerate() {
vcov[oi][oj] = fr.vcov[ri][rj];
}
}
Fit {
beta,
se,
vcov,
tau2: fr.tau2,
dispersion: fr.dispersion,
converged: fr.converged,
varcorr: fr.varcorr,
stddev_se: fr.stddev_se,
aliased: aliased.to_vec(),
n_eval: fr.n_eval,
deviance: fr.deviance,
singular: fr.singular,
loglik: fr.loglik,
df: fr.df,
reml: fr.reml,
fitted: fr.fitted,
ranef: fr.ranef,
ranef_levels: fr.ranef_levels,
}
}
pub(super) fn assert_group_ids(re: &ReStructure, ids: &GroupIds, n: usize) {
assert_eq!(
ids.primary.len(),
n,
"GroupIds.primary must have n elements"
);
assert_eq!(
ids.extra.len(),
re.extra_groupings.len(),
"GroupIds.extra must align 1:1 with re.extra_groupings (declaration order)"
);
for (g, e) in ids.extra.iter().enumerate() {
assert_eq!(e.len(), n, "GroupIds.extra[{g}] must have n elements");
}
}
pub(super) fn assert_model_shape(model: &ModelSpec, p: usize, nagq: u8) {
assert!(
(1..=crate::consts::MAX_NAGQ).contains(&nagq) && nagq % 2 == 1,
"nagq={} must be odd in 1..={}",
nagq,
crate::consts::MAX_NAGQ
);
if nagq > 1 {
let re = model
.re
.as_ref()
.expect("nagq>1 requires a mixed model (re: Some)");
let single_factor = re.extra_groupings.is_empty();
let agq_family = matches!(
model.family,
Family::Binomial { .. } | Family::Poisson { .. }
);
assert!(
single_factor && agq_family,
"nagq>1 legal only on a single grouping factor, binomial/Poisson GLMM"
);
let q_p = 1 + re.slopes.len();
assert!(
q_p <= 3,
"nagq>1 with q_p={q_p} random effects per group exceeds the temporary \
q_p≤3 cap (a cost/oracle-coverage boundary, not a code limit)"
);
}
let Some(re) = model.re.as_ref() else {
return;
};
for &col in &re.slopes {
assert!(
(col as usize) < p,
"primary slope column {col} out of range (p={p})"
);
}
for g in &re.extra_groupings {
for &col in &g.slopes {
assert!(
(col as usize) < p,
"extra-grouping slope column {col} out of range (p={p})"
);
}
}
let n_nested = re
.extra_groupings
.iter()
.filter(|g| matches!(g.relation, GroupingRelation::NestedWithin { .. }))
.count();
assert!(
n_nested <= 1,
"at most one NestedWithin extra grouping is supported (got {n_nested})"
);
}
#[cfg(test)]
pub(crate) fn assert_model_shape_pub(model: &ModelSpec, p: usize, nagq: u8) {
assert_model_shape(model, p, nagq);
}
#[cfg(test)]
pub(crate) fn spec_sized_from_ids_pub(model: &ModelSpec, ids: &GroupIds) -> ModelSpec {
spec_sized_from_ids(model, ids)
}
pub(super) fn to_col_major(x: &[f64], n: usize, p: usize) -> Mat<f64> {
let mut x_mat = Mat::<f64>::zeros(n.max(1), p.max(1));
for i in 0..n {
for j in 0..p {
x_mat[(i, j)] = x[i * p + j];
}
}
x_mat
}
pub(super) fn fill_se_compact(var_diag: &[f64], target_indices: &[u32], se: &mut [f64]) {
for (i, &ti) in target_indices.iter().enumerate() {
let vd = var_diag[i];
if vd.is_finite() && vd >= 0.0 {
se[ti as usize] = vd.sqrt();
}
}
}
pub(super) fn fill_se_by_predictor(var_diag: &[f64], target_indices: &[u32], se: &mut [f64]) {
for &ti in target_indices {
let vd = var_diag[ti as usize];
if vd.is_finite() && vd >= 0.0 {
se[ti as usize] = vd.sqrt();
}
}
}
pub(crate) fn nan_vcov(p: usize) -> Vec<Vec<f64>> {
vec![vec![f64::NAN; p]; p]
}
pub(crate) fn vcov_from_chol(
l: faer::MatRef<'_, f64>,
p: usize,
target_indices: &[u32],
scale: f64,
) -> Vec<Vec<f64>> {
let mut vcov = nan_vcov(p);
let mut cols: Vec<(usize, Vec<f64>)> = Vec::with_capacity(target_indices.len());
for &tj in target_indices {
let tj = tj as usize;
if tj >= p {
continue;
}
let mut u = vec![0.0f64; p];
for i in 0..p {
let mut acc = if i == tj { 1.0 } else { 0.0 };
for k in 0..i {
acc -= l[(i, k)] * u[k];
}
let l_ii = l[(i, i)];
u[i] = if l_ii == 0.0 { f64::NAN } else { acc / l_ii };
}
cols.push((tj, u));
}
for (a, (i, ui)) in cols.iter().enumerate() {
for (j, uj) in cols[a..].iter() {
let v = scale * ui.iter().zip(uj).map(|(x, y)| x * y).sum::<f64>();
vcov[*i][*j] = v;
vcov[*j][*i] = v;
}
}
vcov
}