#[cfg(feature = "loop_advanced")]
use crate::lmm::{LmmFitScratch, LmmSuffStats, LmmWorkspace};
#[cfg(feature = "loop_advanced")]
use crate::{Family, GroupIds, ModelSpec};
#[cfg(all(test, feature = "loop_advanced"))]
use crate::StartValues;
#[cfg(feature = "loop_advanced")]
use super::common::{assert_group_ids, spec_sized_from_ids, Perm};
#[cfg(feature = "loop_advanced")]
use super::lmm::accumulate_lmm_rows;
#[cfg(all(test, feature = "loop_advanced"))]
use super::lmm::fit_lmm_into;
#[cfg(feature = "loop_advanced")]
use super::{classify_design, Solver};
#[cfg(all(test, feature = "loop_advanced"))]
use super::{Fit, FitOptions};
#[cfg(feature = "loop_advanced")]
type LmmObjective<'a> = dyn FnMut(&[f64]) -> f64 + 'a;
#[cfg(feature = "loop_advanced")]
pub type LmmTrace<'a> = dyn FnMut(usize, &[f64], f64) + 'a;
#[cfg(feature = "loop_advanced")]
#[allow(private_interfaces)]
pub enum LmmSeamWs {
Dense {
suff: Box<LmmSuffStats>,
fit: Box<LmmFitScratch>,
perm: Perm,
},
Sparse {
ws: Box<crate::sparse::SparseLmmWorkspace>,
perm: Perm,
},
}
#[cfg(feature = "loop_advanced")]
impl LmmSeamWs {
pub fn perm(&self) -> Perm {
match self {
LmmSeamWs::Dense { perm, .. } | LmmSeamWs::Sparse { perm, .. } => *perm,
}
}
}
#[cfg(feature = "loop_advanced")]
pub fn build_lmm_seam_ws(
x: &[f64],
y: &[f64],
n: usize,
p: usize,
model: &ModelSpec,
ids: &GroupIds,
) -> (LmmSeamWs, crate::lmm::LmmGroupings) {
assert!(
matches!(model.family, Family::Gaussian) && model.re.is_some(),
"dev objective seam covers Gaussian LMM only"
);
assert_group_ids(model.re.as_ref().unwrap(), ids, n);
let (sized, ids, perm) = spec_sized_from_ids(model, ids);
let re = sized.re.as_ref().unwrap();
let slope_cols: Vec<usize> = re.slopes.iter().map(|&c| c as usize).collect();
let extra_slope_cols: Vec<Vec<usize>> = re
.extra_groupings
.iter()
.map(|g| g.slopes.iter().map(|&c| c as usize).collect())
.collect();
match classify_design(&sized, 1) {
Solver::NoZ => {
let mut ws =
LmmWorkspace::for_cluster_spec_ext(p, &sized, n, &slope_cols, &extra_slope_cols);
let x_mat = super::common::to_col_major(x, n, p);
accumulate_lmm_rows(
&mut ws,
x_mat.as_ref().subrows(0, n),
y,
n,
p,
&ids.primary,
&ids.extra,
None,
);
let LmmWorkspace { suff, mut fit, .. } = ws;
crate::lmm::precompute_balanced_collapse(&suff, &mut fit);
let g = suff.groupings.clone();
(
LmmSeamWs::Dense {
suff: Box::new(suff),
fit: Box::new(fit),
perm,
},
g,
)
}
Solver::Sparse => {
let mut g = crate::lmm::LmmGroupings::from_cluster_spec_ext(
&sized,
n,
&slope_cols,
&extra_slope_cols,
);
let xm = faer::MatRef::from_row_major_slice(x, n, p);
g.set_slope_scales(xm, None);
let g = g;
let ws = crate::sparse::SparseLmmWorkspace::new(
&g,
xm,
&ids.primary,
&ids.extra,
y,
n,
p,
None,
);
(
LmmSeamWs::Sparse {
ws: Box::new(ws),
perm,
},
g,
)
}
}
}
#[cfg(feature = "loop_advanced")]
fn with_lmm_objective<R>(
x: &[f64],
y: &[f64],
n: usize,
p: usize,
model: &ModelSpec,
ids: &GroupIds,
f: impl FnOnce(&mut LmmObjective<'_>, &crate::lmm::LmmGroupings) -> R,
) -> R {
let (mut ws, g) = build_lmm_seam_ws(x, y, n, p, model, ids);
match &mut ws {
LmmSeamWs::Dense { suff, fit, .. } => {
let mut obj = |theta: &[f64]| crate::lmm::reml_deviance(theta, suff, fit);
f(&mut obj, &g)
}
LmmSeamWs::Sparse { ws, .. } => {
let mut obj = |theta: &[f64]| crate::sparse::sparse_reml_deviance(theta, ws);
f(&mut obj, &g)
}
}
}
#[cfg(feature = "loop_advanced")]
pub fn lmm_objective_at(
x: &[f64],
y: &[f64],
n: usize,
p: usize,
model: &ModelSpec,
ids: &GroupIds,
theta: &[f64],
) -> f64 {
with_lmm_objective(x, y, n, p, model, ids, |obj, g| {
assert_eq!(
theta.len(),
g.n_theta(),
"theta length must match the model"
);
obj(theta)
})
}
#[cfg(feature = "loop_advanced")]
pub struct LmmSweepOutcome {
pub deviance: f64,
pub theta: Vec<f64>,
pub n_eval: usize,
pub converged: bool,
}
#[cfg(feature = "loop_advanced")]
#[allow(clippy::too_many_arguments)] fn lmm_sweep_search(
obj: &mut LmmObjective<'_>,
g: &crate::lmm::LmmGroupings,
theta0: Option<&[f64]>,
rho_end: f64,
max_fun: Option<usize>,
mut trace: Option<&mut LmmTrace<'_>>,
) -> LmmSweepOutcome {
use bobyqa::{Bobyqa, Config, Status};
let n_theta = g.n_theta();
let (blind, lower, upper) = g.blind_theta_and_bounds();
let mut theta = match theta0 {
Some(t) => {
assert_eq!(t.len(), n_theta, "theta0 length must match the model");
t.to_vec()
}
None => blind,
};
let min_diag = g
.diagonal_theta()
.iter()
.map(|&i| theta[i])
.fold(f64::INFINITY, f64::min);
let rho_begin = (0.1 * min_diag)
.min(crate::lmm::RHO_BEGIN)
.max(10.0 * rho_end);
let npt = if n_theta >= 3 {
(3 * n_theta).div_ceil(2) + 1
} else {
2 * n_theta + 1
};
let mut config = Config::new(n_theta);
config.rho_begin = rho_begin;
config.rho_end = rho_end;
config.npt = npt;
crate::lmm::apply_campaign_overrides(&mut config, n_theta);
if let Some(mf) = max_fun {
config.max_fun = mf;
}
let mut solver = Bobyqa::new(n_theta, config).expect("dev sweep config valid");
let mut k = 0usize;
let out = solver.minimize(
|xs| {
let v = obj(xs);
k += 1;
if let Some(t) = trace.as_mut() {
t(k, xs, v);
}
v
},
&mut theta,
&lower,
&upper,
);
LmmSweepOutcome {
deviance: obj(&theta),
theta,
n_eval: out.n_eval,
converged: matches!(out.status, Status::Converged),
}
}
#[cfg(feature = "loop_advanced")]
#[allow(clippy::too_many_arguments)] pub fn lmm_sweep_fit_on(
ws: &mut LmmSeamWs,
g: &crate::lmm::LmmGroupings,
theta0: Option<&[f64]>,
rho_end: f64,
max_fun: Option<usize>,
trace: Option<&mut LmmTrace<'_>>,
) -> LmmSweepOutcome {
match ws {
LmmSeamWs::Dense { suff, fit, .. } => {
let mut obj = |theta: &[f64]| crate::lmm::reml_deviance(theta, suff, fit);
lmm_sweep_search(&mut obj, g, theta0, rho_end, max_fun, trace)
}
LmmSeamWs::Sparse { ws, .. } => {
let mut obj = |theta: &[f64]| crate::sparse::sparse_reml_deviance(theta, ws);
lmm_sweep_search(&mut obj, g, theta0, rho_end, max_fun, trace)
}
}
}
#[cfg(feature = "loop_advanced")]
#[allow(clippy::too_many_arguments)] pub fn lmm_sweep_fit(
x: &[f64],
y: &[f64],
n: usize,
p: usize,
model: &ModelSpec,
ids: &GroupIds,
theta0: Option<&[f64]>,
rho_end: f64,
max_fun: Option<usize>,
trace: Option<&mut LmmTrace<'_>>,
) -> LmmSweepOutcome {
let (mut ws, g) = build_lmm_seam_ws(x, y, n, p, model, ids);
lmm_sweep_fit_on(&mut ws, &g, theta0, rho_end, max_fun, trace)
}
#[cfg(all(test, feature = "loop_advanced"))]
pub fn build_lmm_workspace(p: usize, model: &ModelSpec, n: usize) -> LmmWorkspace {
let re = model
.re
.as_ref()
.expect("build_lmm_workspace requires a mixed model (re: Some)");
let slope_cols: Vec<usize> = re.slopes.iter().map(|&c| c as usize).collect();
let extra_slope_cols: Vec<Vec<usize>> = re
.extra_groupings
.iter()
.map(|g| g.slopes.iter().map(|&c| c as usize).collect())
.collect();
LmmWorkspace::for_cluster_spec_ext(p, model, n, &slope_cols, &extra_slope_cols)
}
#[cfg(all(test, feature = "loop_advanced"))]
#[allow(clippy::too_many_arguments)] pub fn refit_lmm(
ws: &mut LmmWorkspace,
x: &[f64],
y: &[f64],
n: usize,
p: usize,
ids: &GroupIds,
opts: &FitOptions,
start: Option<&StartValues>,
) -> Fit {
let y_shifted: Vec<f64>;
let y_eff: &[f64] = match &opts.offset {
Some(o) => {
y_shifted = y.iter().zip(o).map(|(&yi, &oi)| yi - oi).collect();
&y_shifted
}
None => y,
};
let x_mat = super::common::to_col_major(x, n, p);
accumulate_lmm_rows(
ws,
x_mat.as_ref().subrows(0, n),
y_eff,
n,
p,
&ids.primary,
&ids.extra,
opts.weights.as_deref(),
);
fit_lmm_into(ws, x, ids, n, p, opts, start)
}