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(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,
}
}
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
}