use crate::lmm::LmmGroupings;
use bobyqa::Status;
use faer::dyn_stack::{MemBuffer, MemStack};
use faer::linalg::cholesky::llt::factor::{
cholesky_in_place, cholesky_in_place_scratch, LltRegularization,
};
use faer::linalg::cholesky::llt::solve::solve_in_place;
use faer::{Mat, MatRef, Par, Spec};
use super::fill_lambda_small;
pub(crate) struct SparseGlmmWorkspace {
pub(crate) g: LmmGroupings,
lam_off_decl: Vec<usize>,
lam_small: Vec<f64>,
width: usize,
m_cols: Vec<u32>,
m_vals: Vec<f64>,
eta_fixed: Vec<f64>,
eta: Vec<f64>,
prob: Vec<f64>,
w: Vec<f64>,
mu: Vec<f64>,
pub(super) prior_w: Vec<f64>,
u: Vec<f64>,
u_prev: Vec<f64>,
a: Mat<f64>,
a_chol: Mat<f64>,
a_llt_mem: MemBuffer,
a_rhs: Vec<f64>,
beta: Vec<f64>,
beta_prev: Vec<f64>,
beta_rhs: Vec<f64>,
xtwx: Mat<f64>,
wx: Mat<f64>,
xtwm: Mat<f64>,
ainv_mtwx: Mat<f64>,
schur: Mat<f64>,
schur_llt_mem: MemBuffer,
k: usize,
p: usize,
pirls_tol_override: Option<f64>,
}
impl SparseGlmmWorkspace {
pub(crate) fn new(
g: &LmmGroupings,
cluster_ids: &[u32],
extra_ids: &[Vec<u32>],
n: usize,
p: usize,
) -> Self {
let q_p = g.primary_q;
let mut lam_len = q_p * q_p;
let mut lam_off_decl = vec![0usize; g.extra_offsets.len()];
if let Some(nf) = g.nested {
lam_off_decl[nf.decl] = lam_len;
lam_len += nf.q * nf.q;
}
for cf in &g.crossed {
lam_off_decl[cf.decl] = lam_len;
lam_len += cf.q * cf.q;
}
let width = q_p + g.extra_q.iter().sum::<usize>();
let mut m_cols = vec![0u32; n * width];
for i in 0..n {
let mut t = i * width;
let f = cluster_ids[i] as usize;
for c in 0..q_p {
m_cols[t] = (c * g.n_primary + f) as u32;
t += 1;
}
for (e, ids_e) in extra_ids.iter().enumerate() {
let q_g = g.extra_q[e];
let base = g.extra_offsets[e] + ids_e[i] as usize * q_g;
for c in 0..q_g {
m_cols[t] = (base + c) as u32;
t += 1;
}
}
}
let k = g.k_total;
SparseGlmmWorkspace {
g: g.clone(),
lam_off_decl,
lam_small: vec![0.0; lam_len.max(1)],
width,
m_cols,
m_vals: vec![0.0; n * width],
eta_fixed: vec![0.0; n.max(1)],
eta: vec![0.0; n.max(1)],
prob: vec![0.0; n.max(1)],
w: vec![0.0; n.max(1)],
mu: vec![0.0; n.max(1)],
prior_w: vec![1.0; n.max(1)],
u: vec![0.0; k.max(1)],
u_prev: vec![0.0; k.max(1)],
a: Mat::zeros(k.max(1), k.max(1)),
a_chol: Mat::zeros(k.max(1), k.max(1)),
a_llt_mem: MemBuffer::new(cholesky_in_place_scratch::<f64>(
k.max(1),
Par::Seq,
Spec::default(),
)),
a_rhs: vec![0.0; k.max(1)],
beta: vec![0.0; p.max(1)],
beta_prev: vec![0.0; p.max(1)],
beta_rhs: vec![0.0; p.max(1)],
xtwx: Mat::zeros(p.max(1), p.max(1)),
wx: Mat::zeros(n.max(1), p.max(1)),
xtwm: Mat::zeros(p.max(1), k.max(1)),
ainv_mtwx: Mat::zeros(k.max(1), p.max(1)),
schur: Mat::zeros(p.max(1), p.max(1)),
schur_llt_mem: MemBuffer::new(cholesky_in_place_scratch::<f64>(
p.max(1),
Par::Seq,
Spec::default(),
)),
k,
p,
pirls_tol_override: None,
}
}
#[cfg(all(feature = "parallel", not(target_arch = "wasm32")))]
fn clone_worker(&self) -> SparseGlmmWorkspace {
let Self {
g,
lam_off_decl,
lam_small,
width,
m_cols,
m_vals,
eta_fixed,
eta,
prob,
w,
mu,
prior_w,
u,
u_prev,
a,
a_chol,
a_llt_mem: _,
a_rhs,
beta,
beta_prev,
beta_rhs,
xtwx,
wx,
xtwm,
ainv_mtwx,
schur,
schur_llt_mem: _,
k,
p,
pirls_tol_override,
} = self;
SparseGlmmWorkspace {
g: g.clone(),
lam_off_decl: lam_off_decl.clone(),
lam_small: lam_small.clone(),
width: *width,
m_cols: m_cols.clone(),
m_vals: m_vals.clone(),
eta_fixed: eta_fixed.clone(),
eta: eta.clone(),
prob: prob.clone(),
w: w.clone(),
mu: mu.clone(),
prior_w: prior_w.clone(),
u: u.clone(),
u_prev: u_prev.clone(),
a: a.clone(),
a_chol: a_chol.clone(),
a_llt_mem: MemBuffer::new(cholesky_in_place_scratch::<f64>(
(*k).max(1),
Par::Seq,
Spec::default(),
)),
a_rhs: a_rhs.clone(),
beta: beta.clone(),
beta_prev: beta_prev.clone(),
beta_rhs: beta_rhs.clone(),
xtwx: xtwx.clone(),
wx: wx.clone(),
xtwm: xtwm.clone(),
ainv_mtwx: ainv_mtwx.clone(),
schur: schur.clone(),
schur_llt_mem: MemBuffer::new(cholesky_in_place_scratch::<f64>(
(*p).max(1),
Par::Seq,
Spec::default(),
)),
k: *k,
p: *p,
pirls_tol_override: *pirls_tol_override,
}
}
fn fill_m_vals(&mut self, x: MatRef<f64>, n: usize) {
let g = &self.g;
let q_p = g.primary_q;
for i in 0..n {
let mut t = i * self.width;
for c in 0..q_p {
let mut acc = 0.0;
for r in c..q_p {
let z = if r == 0 {
1.0
} else {
x[(i, g.primary_slope_cols[r - 1])]
};
acc += z * self.lam_small[r * q_p + c];
}
self.m_vals[t] = acc;
t += 1;
}
for e in 0..g.extra_offsets.len() {
let q_g = g.extra_q[e];
let lo = self.lam_off_decl[e];
for c in 0..q_g {
let mut acc = 0.0;
for r in c..q_g {
let z = if r == 0 {
1.0
} else {
x[(i, g.extra_slope_cols[e][r - 1])]
};
acc += z * self.lam_small[lo + r * q_g + c];
}
self.m_vals[t] = acc;
t += 1;
}
}
}
}
fn refresh_eta_fixed(&mut self, x: MatRef<f64>, n: usize) {
for i in 0..n {
let mut e = 0.0;
for (j, &b) in self.beta[..self.p].iter().enumerate() {
e += x[(i, j)] * b;
}
self.eta_fixed[i] = e;
}
}
fn pirls(
&mut self,
family: crate::Family,
nb_theta: f64,
x: MatRef<f64>,
y: &[f64],
n: usize,
profile: bool,
) -> (f64, f64, f64, bool) {
let (k, p, width) = (self.k, self.p, self.width);
self.refresh_eta_fixed(x, n);
let tol = self
.pirls_tol_override
.unwrap_or_else(|| crate::glmm::pirls_tol(family));
let mut pen_accepted = f64::INFINITY;
let mut mixed_prev = f64::INFINITY;
let mut halvings = 0usize;
let mut converged = false;
let mut dev = f64::NAN;
let mut pen = f64::NAN;
let mut logdet = 0.0;
for _ in 0..crate::glmm::PIRLS_MAX_ITERS {
dev = 0.0;
#[allow(clippy::needless_range_loop)]
for i in 0..n {
let base = i * width;
let mut acc = 0.0;
for t in base..base + width {
acc += self.m_vals[t] * self.u[self.m_cols[t] as usize];
}
self.mu[i] = acc;
let e = crate::family::clamp_eta(family, self.eta_fixed[i] + acc);
self.eta[i] = e;
let (mui, wi, _) = crate::family::irls_weight_and_resid(family, nb_theta, y[i], e);
self.prob[i] = mui;
self.w[i] = (self.prior_w[i] * wi).max(crate::glm::WEIGHT_CLAMP);
dev += self.prior_w[i] * crate::family::dev_resid(family, nb_theta, y[i], mui);
}
let pen_u: f64 = self.u[..k].iter().map(|v| v * v).sum();
let penalized = dev + pen_u;
if penalized - pen_accepted > tol * (1.0 + penalized.abs()) {
if halvings < crate::glmm::PIRLS_MAX_HALVINGS {
halvings += 1;
for c in 0..k {
self.u[c] = 0.5 * (self.u[c] + self.u_prev[c]);
}
if profile {
for j in 0..p {
self.beta[j] = 0.5 * (self.beta[j] + self.beta_prev[j]);
}
self.refresh_eta_fixed(x, n);
}
continue;
}
return (f64::NAN, f64::NAN, f64::NAN, false);
}
halvings = 0;
pen_accepted = penalized;
self.u_prev[..k].copy_from_slice(&self.u[..k]);
if profile {
self.beta_prev[..p].copy_from_slice(&self.beta[..p]);
}
for c in 0..k {
for r in 0..k {
self.a[(r, c)] = 0.0;
}
self.a_rhs[c] = 0.0;
}
if profile {
for v in self.beta_rhs[..p].iter_mut() {
*v = 0.0;
}
}
for i in 0..n {
let wi = self.w[i];
let dmu = crate::family::mu_eta(family, self.eta[i]);
let v = crate::family::variance(family, nb_theta, self.prob[i]);
let rho = self.prior_w[i] * dmu * (y[i] - self.prob[i]) / v;
let q_i = wi * self.mu[i] + rho;
let base = i * width;
for ta in base..base + width {
let ca = self.m_cols[ta] as usize;
let va = self.m_vals[ta];
let wva = wi * va;
for tb in base..base + width {
let cb = self.m_cols[tb] as usize;
let vb = self.m_vals[tb];
self.a[(ca, cb)] += wva * vb;
}
self.a_rhs[ca] += va * q_i;
}
if profile {
for j in 0..p {
self.beta_rhs[j] += x[(i, j)] * rho;
}
}
}
for r in 0..k {
self.a[(r, r)] += 1.0;
}
self.a_chol.copy_from_triangular_lower(self.a.as_ref());
if cholesky_in_place(
self.a_chol.as_mut(),
LltRegularization::default(),
Par::Seq,
MemStack::new(&mut self.a_llt_mem),
Spec::default(),
)
.is_err()
{
return (f64::NAN, f64::NAN, f64::NAN, false);
}
logdet = 0.0;
for r in 0..k {
logdet += self.a_chol[(r, r)].ln();
}
solve_in_place(
self.a_chol.as_ref(),
faer::MatMut::from_column_major_slice_mut(&mut self.a_rhs[..k], k, 1),
Par::Seq,
MemStack::new(&mut self.a_llt_mem),
);
pen = 0.0;
for c in 0..k {
self.u[c] = self.a_rhs[c];
pen += self.u[c] * self.u[c];
}
if profile {
for r in 0..p {
for c in 0..k {
self.xtwm[(r, c)] = 0.0;
}
}
for i in 0..n {
let wi = self.w[i];
let base = i * width;
for r in 0..p {
let xw = x[(i, r)] * wi;
for t in base..base + width {
self.xtwm[(r, self.m_cols[t] as usize)] += xw * self.m_vals[t];
}
}
}
for r in 0..p {
for i in 0..n {
self.wx[(i, r)] = self.w[i] * x[(i, r)];
}
}
faer::linalg::matmul::matmul(
self.xtwx.as_mut(),
faer::Accum::Replace,
x.transpose(),
self.wx.as_ref(),
1.0,
Par::Seq,
);
for r in 0..k {
for c in 0..p {
self.ainv_mtwx[(r, c)] = self.xtwm[(c, r)];
}
}
solve_in_place(
self.a_chol.as_ref(),
self.ainv_mtwx.as_mut(),
Par::Seq,
MemStack::new(&mut self.a_llt_mem),
);
for r in 0..p {
for c in 0..p {
let mut s = self.xtwx[(r, c)];
for j in 0..k {
s -= self.xtwm[(r, j)] * self.ainv_mtwx[(j, c)];
}
self.schur[(r, c)] = s;
}
}
for r in 0..p {
let mut acc = 0.0;
for c in 0..k {
acc += self.xtwm[(r, c)] * (self.u[c] - self.u_prev[c]);
}
self.beta_rhs[r] -= acc;
}
if cholesky_in_place(
self.schur.as_mut(),
LltRegularization::default(),
Par::Seq,
MemStack::new(&mut self.schur_llt_mem),
Spec::default(),
)
.is_err()
{
return (f64::NAN, f64::NAN, f64::NAN, false);
}
solve_in_place(
self.schur.as_ref(),
faer::MatMut::from_column_major_slice_mut(&mut self.beta_rhs[..p], p, 1),
Par::Seq,
MemStack::new(&mut self.schur_llt_mem),
);
for j in 0..p {
self.beta[j] += self.beta_rhs[j];
}
for c in 0..k {
let mut acc = 0.0;
for j in 0..p {
acc += self.ainv_mtwx[(c, j)] * self.beta_rhs[j];
}
self.u[c] -= acc;
}
self.refresh_eta_fixed(x, n);
pen = 0.0;
for c in 0..k {
pen += self.u[c] * self.u[c];
}
}
let mixed = dev + pen;
if (mixed - mixed_prev).abs() < tol * (1.0 + mixed.abs()) {
converged = true;
break;
}
mixed_prev = mixed;
}
(dev, pen, logdet, converged)
}
}
#[allow(clippy::too_many_arguments)]
pub(super) fn sparse_glmm_deviance(
family: crate::Family,
nb_theta: f64,
params: &[f64],
ws: &mut SparseGlmmWorkspace,
x: MatRef<f64>,
y: &[f64],
n: usize,
profile_beta: bool,
) -> f64 {
let n_theta = ws.g.n_theta();
let p = ws.p;
fill_lambda_small(¶ms[..n_theta], &ws.g, &mut ws.lam_small);
ws.fill_m_vals(x, n);
if !profile_beta {
ws.beta[..p].copy_from_slice(¶ms[n_theta..n_theta + p]);
}
if ws.pirls_tol_override.is_some() {
for v in ws.u.iter_mut() {
*v = 0.0;
}
}
let (dev, pen, logdet, conv) = ws.pirls(family, nb_theta, x, y, n, profile_beta);
if !conv || !dev.is_finite() {
return f64::INFINITY;
}
let data_term = if matches!(family, crate::Family::Gamma { .. }) {
crate::family::gamma_aic(y, &ws.prob[..n], dev, n, Some(&ws.prior_w[..n]))
} else {
dev
};
data_term + pen + 2.0 * logdet
}
fn sparse_glmm_schur(ws: &mut SparseGlmmWorkspace, x: MatRef<f64>, n: usize) -> Option<Mat<f64>> {
use faer::linalg::solvers::Solve;
let (k, p, width) = (ws.k, ws.p, ws.width);
let mut xtwx = Mat::<f64>::zeros(p, p);
for r in 0..p {
for c in 0..=r {
let mut s = 0.0;
for i in 0..n {
s += x[(i, r)] * ws.w[i] * x[(i, c)];
}
xtwx[(r, c)] = s;
xtwx[(c, r)] = s;
}
}
let mut xtwm = Mat::<f64>::zeros(p, k);
for i in 0..n {
let wi = ws.w[i];
let base = i * width;
for r in 0..p {
let xw = x[(i, r)] * wi;
for t in base..base + width {
xtwm[(r, ws.m_cols[t] as usize)] += xw * ws.m_vals[t];
}
}
}
let ac = match ws.a.as_ref().llt(faer::Side::Lower) {
Ok(c) => c,
Err(_) => return None,
};
let mut ainv_mtwx = Mat::<f64>::zeros(k, p);
for r in 0..k {
for c in 0..p {
ainv_mtwx[(r, c)] = xtwm[(c, r)];
}
}
ac.solve_in_place(ainv_mtwx.as_mut());
let mut schur = Mat::<f64>::zeros(p, p);
for r in 0..p {
for c in 0..p {
let mut s = xtwx[(r, c)];
for j in 0..k {
s -= xtwm[(r, j)] * ainv_mtwx[(j, c)];
}
schur[(r, c)] = s;
}
}
Some(schur)
}
const SPARSE_FD_STEP_REL: f64 = 1e-4;
fn fd_hess_entry(
i: usize,
j: usize,
steps: &[f64],
f0: f64,
ev: &mut impl FnMut(&[usize], &[f64]) -> f64,
) -> Option<f64> {
let h = if i == j {
crate::glmm::fd_second_diff(ev, i, steps[i], f0)
} else {
crate::glmm::fd_mixed_diff(ev, i, j, steps[i], steps[j])
};
h.is_finite().then_some(h)
}
#[allow(clippy::too_many_arguments)]
fn sparse_fd_hessian_cov(
family: crate::Family,
nb_theta: f64,
gamma_hat: &[f64],
ws: &mut SparseGlmmWorkspace,
x: MatRef<f64>,
y: &[f64],
n: usize,
parallel_inner: bool,
) -> Option<(Mat<f64>, Vec<f64>)> {
use faer::linalg::solvers::Solve;
let m = gamma_hat.len();
let n_theta = ws.g.n_theta();
let p = ws.p;
let f0 = sparse_glmm_deviance(family, nb_theta, gamma_hat, ws, x, y, n, false);
if !f0.is_finite() {
return None;
}
let steps: Vec<f64> = gamma_hat
.iter()
.map(|&g| SPARSE_FD_STEP_REL * g.abs().max(1.0))
.collect();
let mut hess = Mat::<f64>::zeros(m, m);
let use_par = cfg!(all(feature = "parallel", not(target_arch = "wasm32"))) && parallel_inner;
if use_par {
#[cfg(all(feature = "parallel", not(target_arch = "wasm32")))]
{
use rayon::prelude::*;
let cells: Vec<(usize, usize)> =
(0..m).flat_map(|i| (i..m).map(move |j| (i, j))).collect();
let ws_ro: &SparseGlmmWorkspace = ws;
let steps = &steps;
let results: Vec<(usize, usize, Option<f64>)> = cells
.par_iter()
.map_init(
|| (ws_ro.clone_worker(), gamma_hat.to_vec()),
|(wws, pt), &(i, j)| {
let mut ev = |coords: &[usize], deltas: &[f64]| -> f64 {
pt.copy_from_slice(gamma_hat);
for (&c, &d) in coords.iter().zip(deltas) {
pt[c] += d;
}
sparse_glmm_deviance(family, nb_theta, pt, wws, x, y, n, false)
};
(i, j, fd_hess_entry(i, j, steps, f0, &mut ev))
},
)
.collect();
if results.iter().any(|(_, _, h)| h.is_none()) {
return None;
}
for (i, j, h) in results {
let h = h.expect("checked all-Some above");
hess[(i, j)] = h;
hess[(j, i)] = h;
}
}
} else {
let mut pt = gamma_hat.to_vec();
for i in 0..m {
for j in i..m {
let mut ev = |coords: &[usize], deltas: &[f64]| -> f64 {
pt.copy_from_slice(gamma_hat);
for (&c, &d) in coords.iter().zip(deltas) {
pt[c] += d;
}
sparse_glmm_deviance(family, nb_theta, &pt, ws, x, y, n, false)
};
let hij = fd_hess_entry(i, j, &steps, f0, &mut ev)?;
hess[(i, j)] = hij;
hess[(j, i)] = hij;
}
}
}
let chol = hess.as_ref().llt(faer::Side::Lower).ok()?;
let mut inv = Mat::<f64>::identity(m, m);
chol.solve_in_place(inv.as_mut());
let mut cov = Mat::<f64>::zeros(p, p);
for a in 0..p {
for b in 0..p {
cov[(a, b)] = 2.0 * inv[(n_theta + a, n_theta + b)];
}
}
let theta_se: Vec<f64> = (0..n_theta)
.map(|kk| (2.0 * inv[(kk, kk)]).max(0.0).sqrt())
.collect();
Some((cov, theta_se))
}
fn sparse_glmm_nan_fit(p: usize, n_theta: usize) -> crate::Fit {
crate::Fit {
beta: vec![f64::NAN; p],
se: vec![f64::NAN; p],
vcov: crate::fit::nan_vcov(p),
tau2: vec![f64::NAN; n_theta],
dispersion: f64::NAN,
converged: false,
varcorr: vec![],
stddev_se: vec![],
aliased: vec![false; p],
n_eval: 0,
deviance: f64::NAN,
singular: false,
}
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn fit_glmm_sparse(
x: &[f64],
y: &[f64],
n: usize,
p: usize,
model: &crate::ModelSpec,
cluster_ids: &[u32],
extra_ids: &[Vec<u32>],
nb_theta: f64,
start: Option<&crate::StartValues>,
opts: &crate::FitOptions,
) -> (crate::Fit, f64) {
let re = model
.re
.as_ref()
.expect("fit_glmm_sparse requires a mixed model (re: Some)");
let family = model.family;
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();
let g = LmmGroupings::from_cluster_spec_ext(model, n, &slope_cols, &extra_slope_cols);
let n_theta = g.n_theta();
if n == 0 || p == 0 {
return (sparse_glmm_nan_fit(p, n_theta), f64::INFINITY);
}
let xm = MatRef::from_row_major_slice(x, n, p);
let mut ws = SparseGlmmWorkspace::new(&g, cluster_ids, extra_ids, n, p);
if let Some(w) = &opts.weights {
ws.prior_w[..n].copy_from_slice(w);
}
let (theta0, mut lower, mut upper) = g.blind_theta_and_bounds();
let mut params = vec![0.0f64; n_theta + p];
match start {
Some(s) => {
for (t, &v) in params[..n_theta].iter_mut().zip(&s.theta) {
*t = v.max(crate::lmm::THETA_TRUTH_FLOOR);
}
}
None => params[..n_theta].copy_from_slice(&theta0),
}
let beta_start = match start {
Some(s) => s.beta.clone(),
None => crate::fit::glm_warm_start_beta(family, nb_theta, xm, y, n, p),
};
for (slot, &b) in params[n_theta..].iter_mut().zip(&beta_start) {
*slot = b.clamp(-crate::glmm::BETA_BOX, crate::glmm::BETA_BOX);
}
lower.extend(std::iter::repeat_n(-crate::glmm::BETA_BOX, p));
upper.extend(std::iter::repeat_n(crate::glmm::BETA_BOX, p));
let rho_begin = (0.1 * crate::lmm::THETA0).min(crate::lmm::RHO_BEGIN);
let n_eval_stage1;
{
let npt1 = if n_theta >= 3 {
(3 * n_theta).div_ceil(2) + 1
} else {
2 * n_theta + 1
};
let mut config1 = bobyqa::Config {
rho_begin,
rho_end: crate::lmm::GLMM_RHO_END,
npt: npt1,
..bobyqa::Config::new(n_theta)
};
crate::lmm::apply_campaign_overrides(&mut config1, n_theta);
let mut solver1 = bobyqa::Bobyqa::new(n_theta, config1)
.expect("BOBYQA config constants are valid by construction");
let beta0: Vec<f64> = params[n_theta..].to_vec();
let mut theta1: Vec<f64> = params[..n_theta].to_vec();
let out1 = solver1.minimize(
|theta| {
ws.beta[..p].copy_from_slice(&beta0);
sparse_glmm_deviance(family, nb_theta, theta, &mut ws, xm, y, n, true)
},
&mut theta1,
&lower[..n_theta],
&upper[..n_theta],
);
n_eval_stage1 = out1.n_eval;
ws.beta[..p].copy_from_slice(&beta0);
let d1 = sparse_glmm_deviance(family, nb_theta, &theta1, &mut ws, xm, y, n, true);
if d1.is_finite() {
params[..n_theta].copy_from_slice(&theta1);
for (slot, &b) in params[n_theta..].iter_mut().zip(&ws.beta[..p]) {
*slot = b.clamp(-crate::glmm::BETA_BOX, crate::glmm::BETA_BOX);
}
}
}
let mut config = bobyqa::Config {
rho_begin,
rho_end: crate::lmm::GLMM_RHO_END,
..bobyqa::Config::new(n_theta + p)
};
crate::lmm::apply_campaign_overrides(&mut config, n_theta + p);
let mut solver = bobyqa::Bobyqa::new(n_theta + p, config)
.expect("BOBYQA config constants are valid by construction");
let out = solver.minimize(
|gamma| sparse_glmm_deviance(family, nb_theta, gamma, &mut ws, xm, y, n, false),
&mut params,
&lower,
&upper,
);
debug_assert!(out.status != Status::InvalidArgs);
let mut ok = matches!(out.status, Status::Converged);
let mut pinned = false;
if ok {
for &ti in g.diagonal_theta() {
if params[ti] <= crate::lmm::PIN_THETA {
params[ti] = 0.0;
pinned = true;
}
}
}
let n_eval = n_eval_stage1 + out.n_eval;
let mut final_deviance = f64::INFINITY;
if ok {
final_deviance = sparse_glmm_deviance(family, nb_theta, ¶ms, &mut ws, xm, y, n, false);
ok = final_deviance.is_finite();
}
if !ok {
return (sparse_glmm_nan_fit(p, n_theta), f64::INFINITY);
}
let beta: Vec<f64> = params[n_theta..].to_vec();
let sigma_sq = crate::family::glmm_sigma_sq(
family,
&y[..n],
&ws.prob[..n],
&ws.u[..ws.k],
Some(&ws.prior_w[..n]),
);
let tau2: Vec<f64> = params[..n_theta]
.iter()
.map(|&t| t * t * sigma_sq)
.collect();
let dispersion = match family {
crate::Family::Gamma { .. } => match opts.dispersion {
Some(v) => v,
None => crate::family::pearson_dispersion(
&y[..n],
&ws.prob[..n],
family,
nb_theta,
n,
p,
Some(&ws.prior_w[..n]),
),
},
_ => 1.0,
};
let varcorr = crate::fit::assemble_varcorr(¶ms[..n_theta], &g, sigma_sq);
let mut se = vec![f64::NAN; p];
let mut vcov = crate::fit::nan_vcov(p);
let mut stddev_se = vec![f64::NAN; n_theta];
let cov_from_schur = |schur: Mat<f64>, se: &mut [f64], vcov: &mut Vec<Vec<f64>>| -> bool {
let sc = match schur.as_ref().llt(faer::Side::Lower) {
Ok(c) => c,
Err(_) => return false,
};
*vcov = crate::fit::vcov_from_chol(sc.L(), p, &opts.target_indices, sigma_sq);
for &tj in &opts.target_indices {
let tj = tj as usize;
let vd = vcov[tj][tj];
if vd.is_finite() && vd >= 0.0 {
se[tj] = vd.sqrt();
}
}
true
};
match opts.wald_se {
crate::WaldSe::Rx => {
let schur = match sparse_glmm_schur(&mut ws, xm, n) {
Some(s) => s,
None => return (sparse_glmm_nan_fit(p, n_theta), f64::INFINITY),
};
if !cov_from_schur(schur, &mut se, &mut vcov) {
return (sparse_glmm_nan_fit(p, n_theta), f64::INFINITY);
}
}
crate::WaldSe::Hessian => {
ws.pirls_tol_override = Some(crate::glmm::PIRLS_TOL_REL_FD);
match sparse_fd_hessian_cov(
family,
nb_theta,
¶ms,
&mut ws,
xm,
y,
n,
opts.parallel_inner,
) {
Some((cov, tse)) => {
for &tj in &opts.target_indices {
let tj = tj as usize;
let vd = cov[(tj, tj)];
if vd.is_finite() && vd >= 0.0 {
se[tj] = vd.sqrt();
}
}
for &ta in &opts.target_indices {
for &tb in &opts.target_indices {
let (a, b) = (ta as usize, tb as usize);
if b > a {
continue;
}
vcov[a][b] = cov[(a, b)];
vcov[b][a] = cov[(a, b)];
}
}
stddev_se.copy_from_slice(&tse);
}
None => {
let _ =
sparse_glmm_deviance(family, nb_theta, ¶ms, &mut ws, xm, y, n, false);
let schur = match sparse_glmm_schur(&mut ws, xm, n) {
Some(s) => s,
None => return (sparse_glmm_nan_fit(p, n_theta), f64::INFINITY),
};
if !cov_from_schur(schur, &mut se, &mut vcov) {
return (sparse_glmm_nan_fit(p, n_theta), f64::INFINITY);
}
}
}
ws.pirls_tol_override = None; }
}
let mut fit = crate::Fit {
beta,
se,
vcov,
tau2,
dispersion,
converged: true,
varcorr,
stddev_se,
aliased: vec![false; p],
n_eval,
deviance: final_deviance,
singular: pinned,
};
fit.singular = fit.singular || fit.has_negligible_component();
(fit, final_deviance)
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn fit_glmm_nb_sparse(
x: &[f64],
y: &[f64],
n: usize,
p: usize,
model: &crate::ModelSpec,
cluster_ids: &[u32],
extra_ids: &[Vec<u32>],
_start: Option<&crate::StartValues>,
opts: &crate::FitOptions,
) -> crate::Fit {
let nb_spec = crate::ModelSpec {
family: crate::Family::NegativeBinomial {
link: crate::NegBinomialLink::Log,
},
re: model.re.clone(),
};
let theta = crate::fit::golden_max_ln_theta(|t| {
let th = t.exp();
let (_fit, dev) =
fit_glmm_sparse(x, y, n, p, &nb_spec, cluster_ids, extra_ids, th, None, opts);
-0.5 * dev + crate::fit::nb_profile_loglik(y, y, th, opts.weights.as_deref())
});
let mut fit_result = fit_glmm_sparse(
x,
y,
n,
p,
&nb_spec,
cluster_ids,
extra_ids,
theta,
None,
opts,
)
.0;
fit_result.dispersion = theta;
fit_result
}