use std::borrow::Cow;
use faer::Mat;
use crate::{
Family, GroupIds, Grouping, GroupingRelation, ModelSpec, ReStructure, Sizing, StartValues,
};
use super::{Boundary, Diagnostics, Fit, FitOptions, Note};
#[derive(Clone, Copy)]
pub struct FitDiagnostics {
pub converged: bool,
pub boundary_hit: u8,
pub pinned_components: u64,
pub pivot: f64,
pub pivot_col: u32,
pub ill_conditioned: bool,
pub pirls_exhausted: u32,
pub final_pirls_exhausted: bool,
pub hessian_fallback: bool,
}
impl FitDiagnostics {
pub(crate) fn fixed_only(converged: bool) -> Self {
FitDiagnostics {
converged,
boundary_hit: 0,
pinned_components: 0,
pivot: f64::NAN,
pivot_col: 0,
ill_conditioned: false,
pirls_exhausted: 0,
final_pirls_exhausted: false,
hessian_fallback: false,
}
}
}
pub(super) fn materialize_diagnostics(
d: &FitDiagnostics,
p: usize,
varcorr: &[Vec<f64>],
) -> Diagnostics {
let pinned = pinned_flags(d.pinned_components, varcorr);
let mut notes = vec![];
if d.ill_conditioned {
notes.push(Note::IllConditioned {
columns: vec![d.pivot_col],
pivot: d.pivot,
});
}
if d.pirls_exhausted > 0 || d.final_pirls_exhausted {
notes.push(Note::PirlsExhausted {
evals: d.pirls_exhausted,
final_eval: d.final_pirls_exhausted,
});
}
if d.hessian_fallback {
notes.push(Note::HessianSeFallback);
}
Diagnostics {
converged: d.converged,
singular: d.boundary_hit == 1,
aliased: vec![false; p],
boundary: match d.boundary_hit {
0 => Boundary::Interior,
1 => Boundary::AtBoundary,
2 => Boundary::NoOptimum,
other => unreachable!("FitDiagnostics::boundary_hit is 0/1/2, got {other}"),
},
pinned,
notes,
}
}
pub(crate) fn pinned_flags(mask: u64, varcorr: &[Vec<f64>]) -> Vec<Vec<bool>> {
if mask == 0 {
return vec![];
}
let mut k = 0usize;
varcorr
.iter()
.map(|vech| {
(0..super::vech_q(vech.len()))
.map(|_| {
let bit = k < u64::BITS as usize && (mask >> k) & 1 == 1;
k += 1;
bit
})
.collect()
})
.collect()
}
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 warm_theta(start: Option<&StartValues>) -> Option<&[f64]> {
start.map(|s| s.theta.as_slice()).filter(|t| !t.is_empty())
}
pub(super) fn varcorr_block(
theta_block: &[f64],
q: usize,
scale: f64,
row_scales: &[f64],
) -> Vec<f64> {
let mut lam = vec![0.0f64; q * q];
crate::lmm::primary_lambda(theta_block, q, &mut lam); back_map_lambda(&mut lam, q, row_scales);
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 (b, &q) in std::iter::once(&groupings.primary_q)
.chain(groupings.extra_q.iter())
.enumerate()
{
let vech = q * (q + 1) / 2;
let row_scales = block_row_scales(groupings, b, q);
out.push(varcorr_block(
&theta[cursor..cursor + vech],
q,
scale,
&row_scales,
));
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 { .. } | Family::InverseGaussian { .. } => 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);
back_map_lambda(&mut lam, q, &block_row_scales(g, 0, q));
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
}
fn back_map_lambda(lam: &mut [f64], q: usize, row_scales: &[f64]) {
for r in 1..q {
let s_r = row_scales[r];
if s_r == 1.0 {
continue;
}
for c in 0..=r {
lam[r * q + c] /= s_r;
}
}
}
fn block_row_scales(g: &crate::lmm::LmmGroupings, b: usize, q: usize) -> Vec<f64> {
(0..q).map(|r| g.block_row_scale(b, r)).collect()
}
pub(crate) fn re_scale_grid(g: &crate::lmm::LmmGroupings) -> Vec<Vec<f64>> {
std::iter::once(g.primary_q)
.chain(g.extra_q.iter().copied())
.enumerate()
.map(|(b, q)| block_row_scales(g, b, q))
.collect()
}
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);
back_map_lambda(&mut lam, q, &block_row_scales(g, 0, q));
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);
back_map_lambda(&mut lam_g, q_g, &block_row_scales(g, e + 1, q_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
}
#[allow(clippy::too_many_arguments)] pub(crate) fn lmm_fitted(
x: &[f64],
n: usize,
p: usize,
beta: &[f64],
ranef: &[f64],
g: &crate::lmm::LmmGroupings,
primary_ids: &[u32],
extra_ids: &[Vec<u32>],
offset: Option<&[f64]>,
) -> Vec<f64> {
let counts = ranef_level_counts(g);
let mut block_start = Vec::with_capacity(counts.len());
let mut acc = 0usize;
let q_of = |e: usize| {
if e == 0 {
g.primary_q
} else {
g.extra_q[e - 1]
}
};
for (e, &levels) in counts.iter().enumerate() {
block_start.push(acc);
acc += levels * q_of(e);
}
(0..n)
.map(|i| {
let row = &x[i * p..(i + 1) * p];
let mut eta = offset.map_or(0.0, |o| o[i]);
for (j, &b) in beta.iter().enumerate() {
eta += row[j] * b;
}
let mut add_block = |e: usize, level: usize, slope_cols: &[usize]| {
let q = q_of(e);
let base = block_start[e] + level * q;
eta += ranef[base]; for (d, &sc) in slope_cols.iter().enumerate() {
eta += ranef[base + 1 + d] * row[sc];
}
};
add_block(0, primary_ids[i] as usize, &g.primary_slope_cols);
for (e, level_ids) in extra_ids.iter().enumerate() {
add_block(e + 1, level_ids[i] as usize, &g.extra_slope_cols[e]);
}
eta
})
.collect()
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct Perm {
swapped_extra: Option<usize>,
}
impl Perm {
pub const IDENTITY: Perm = Perm {
swapped_extra: None,
};
pub fn is_identity(&self) -> bool {
self.swapped_extra.is_none()
}
pub fn swap_slots<T>(&self, v: &mut [T]) {
if let Some(k) = self.swapped_extra {
if v.is_empty() {
return;
}
v.swap(0, k + 1);
}
}
fn swap_blocks(&self, v: &mut Vec<f64>, widths: &[usize]) {
let Some(k) = self.swapped_extra else {
return;
};
if widths.is_empty() {
return;
}
let mut starts = Vec::with_capacity(widths.len());
let mut acc = 0usize;
for &w in widths {
starts.push(acc);
acc += w;
}
debug_assert_eq!(acc, v.len(), "block widths must cover the whole vector");
let mut order: Vec<usize> = (0..widths.len()).collect();
order.swap(0, k + 1);
let mut out = Vec::with_capacity(v.len());
for g in order {
out.extend_from_slice(&v[starts[g]..starts[g] + widths[g]]);
}
*v = out;
}
}
pub(super) fn unpermute_fit(perm: Perm, fit: &mut Fit) {
if perm.is_identity() {
return;
}
perm.swap_blocks(&mut fit.ranef, &fit.ranef_levels);
perm.swap_slots(&mut fit.ranef_levels);
perm.swap_slots(&mut fit.tau2);
perm.swap_slots(&mut fit.stddev_se);
perm.swap_slots(&mut fit.varcorr);
perm.swap_slots(&mut fit.diagnostics.pinned);
}
fn size_rule_perm(re: &ReStructure, primary_levels: usize, extras: &[Grouping]) -> Perm {
let eligible = re.slopes.is_empty()
&& extras
.iter()
.all(|g| g.slopes.is_empty() && matches!(g.relation, GroupingRelation::Crossed { .. }));
if !eligible {
return Perm::IDENTITY;
}
let mut best = primary_levels;
let mut swapped_extra = None;
for (k, g) in extras.iter().enumerate() {
let GroupingRelation::Crossed { n_clusters } = g.relation else {
continue;
};
if n_clusters as usize > best {
best = n_clusters as usize;
swapped_extra = Some(k);
}
}
Perm { swapped_extra }
}
pub(super) fn spec_sized_from_ids<'a>(
model: &ModelSpec,
ids: &'a GroupIds,
) -> (ModelSpec, Cow<'a, GroupIds>, Perm) {
let Some(re) = model.re.as_ref() else {
return (model.clone(), Cow::Borrowed(ids), Perm::IDENTITY);
};
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();
let perm = size_rule_perm(re, n_primary, &extra_groupings);
let mut sized = ModelSpec {
family: model.family,
re: Some(ReStructure {
sizing: Sizing::FixedClusters {
n_clusters: n_primary as u32,
},
slopes: re.slopes.clone(),
extra_groupings,
}),
};
let Some(k) = perm.swapped_extra else {
return (sized, Cow::Borrowed(ids), perm);
};
let sized_re = sized.re.as_mut().expect("just built with re: Some");
let GroupingRelation::Crossed {
n_clusters: extra_levels,
} = sized_re.extra_groupings[k].relation
else {
unreachable!("the size rule only fires when every extra is Crossed");
};
sized_re.sizing = Sizing::FixedClusters {
n_clusters: extra_levels,
};
sized_re.extra_groupings[k].relation = GroupingRelation::Crossed {
n_clusters: n_primary as u32,
};
std::mem::swap(
&mut sized_re.slopes,
&mut sized_re.extra_groupings[k].slopes,
);
let mut swapped_ids = ids.clone();
std::mem::swap(&mut swapped_ids.primary, &mut swapped_ids.extra[k]);
(sized, Cow::Owned(swapped_ids), perm)
}
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]) -> Option<ModelSpec> {
let Some(re) = model.re.as_ref() else {
return Some(model.clone());
};
let remap = |cols: &[u32]| -> Option<Vec<u32>> {
cols.iter()
.map(|&c| {
let r = to_reduced[c as usize];
(r != usize::MAX).then_some(r as u32)
})
.collect()
};
let extra_groupings = re
.extra_groupings
.iter()
.map(|g| {
Some(Grouping {
relation: g.relation.clone(),
slopes: remap(&g.slopes)?,
})
})
.collect::<Option<Vec<_>>>()?;
Some(ModelSpec {
family: model.family,
re: Some(ReStructure {
sizing: re.sizing.clone(),
slopes: remap(&re.slopes)?,
extra_groupings,
}),
})
}
fn unfittable_random_slope_fit(p: usize, model: &ModelSpec, aliased: &[bool]) -> Fit {
Fit {
beta: vec![f64::NAN; p],
se: vec![f64::NAN; p],
vcov: nan_vcov(p),
tau2: vec![f64::NAN; theta_width(model.re.as_ref())],
dispersion: f64::NAN,
diagnostics: Diagnostics {
aliased: aliased.to_vec(),
..Diagnostics::from_flags(false, false, p)
},
varcorr: vec![],
stddev_se: vec![],
n_eval: 0,
deviance: f64::NAN,
loglik: f64::NAN,
df: 0,
reml: matches!(model.family, Family::Gaussian),
fitted: vec![],
ranef: vec![],
ranef_levels: vec![],
}
}
#[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();
debug_assert!(
pk < p,
"fit_rank_deficient: `aliased` must drop at least one column, or the \
recursive fit_warm re-enters on identical input"
);
let mut to_reduced = vec![usize::MAX; p];
for (r, &orig) in kept.iter().enumerate() {
to_reduced[orig] = r;
}
let Some(model_r) = remap_spec_slopes(model, &to_reduced) else {
return unfittable_random_slope_fit(p, model, aliased);
};
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 start_r = start.map(|s| StartValues {
beta: if s.beta.is_empty() {
Vec::new()
} else {
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 aliased_out = aliased.to_vec();
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];
aliased_out[orig] |= fr.diagnostics.aliased[r];
}
let mut diagnostics = fr.diagnostics;
diagnostics.aliased = aliased_out;
for note in &mut diagnostics.notes {
match note {
Note::IllConditioned { columns, .. } => {
for c in columns.iter_mut() {
*c = kept[*c as usize] as u32;
}
}
Note::PirlsExhausted { .. }
| Note::UnusedGroupingLevels { .. }
| Note::ReDesignScaleSpread { .. }
| Note::HessianSeFallback => {}
}
}
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,
diagnostics,
varcorr: fr.varcorr,
stddev_se: fr.stddev_se,
n_eval: fr.n_eval,
deviance: fr.deviance,
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;
};
assert!(
!matches!(model.family, Family::InverseGaussian { .. }),
"inverse-Gaussian mixed models are not implemented (fixed-effects GLM only)"
);
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(any(test, feature = "loop_advanced"))]
pub fn spec_sized_from_ids_pub<'a>(
model: &ModelSpec,
ids: &'a GroupIds,
) -> (ModelSpec, Cow<'a, GroupIds>, Perm) {
spec_sized_from_ids(model, ids)
}
pub(super) fn fill_col_major(dst: &mut Mat<f64>, x: &[f64], n: usize, p: usize) {
for i in 0..n {
for j in 0..p {
dst[(i, j)] = x[i * p + j];
}
}
}
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));
fill_col_major(&mut x_mat, x, n, p);
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
}