use faer::Mat;
use crate::glm::{glm_irls_fit, GlmScratch};
use crate::glmm::{build_z, GlmmFit, GlmmWorkspace, StructuredSchur};
use crate::{Family, ModelSpec, NegBinomialLink, StartValues};
use super::common::{
assemble_varcorr, fill_se_by_predictor, nan_vcov, to_col_major, warm_theta, FitDiagnostics,
};
use super::glm::{golden_max_ln_theta, nb_profile_loglik};
use super::{Diagnostics, Fit, FitOptions};
pub(crate) fn glm_warm_start_beta(
family: Family,
nb_theta: f64,
x: faer::MatRef<f64>,
y: &[f64],
n: usize,
p: usize,
offset: Option<&[f64]>,
) -> Vec<f64> {
let (n1, p1) = (n.max(1), p.max(1));
let mut irls_eta = vec![0.0f64; n1];
let mut irls_p = vec![0.0f64; n1];
let mut irls_w = vec![0.0f64; n1];
let mut irls_z = vec![0.0f64; n1];
let mut irls_betas = vec![0.0f64; p1];
let mut irls_betas_new = vec![0.0f64; p1];
let mut irls_u_scratch = vec![0.0f64; p1];
let mut irls_xtwx = Mat::<f64>::zeros(p1, p1);
let mut irls_xtwz = vec![0.0f64; p1];
let mut irls_l = Mat::<f64>::zeros(p1, p1);
let mut irls_wx = vec![0.0f64; n1 * p1];
let mut irls_var_diag: Vec<f64> = vec![];
let mut irls_t_sq: Vec<f64> = vec![];
let view = glm_irls_fit(
family,
nb_theta,
x,
y,
&[],
None,
None,
offset,
GlmScratch {
irls_eta: &mut irls_eta,
irls_p: &mut irls_p,
irls_w: &mut irls_w,
irls_z: &mut irls_z,
irls_betas: &mut irls_betas,
irls_betas_new: &mut irls_betas_new,
irls_var_diag: &mut irls_var_diag,
irls_t_sq: &mut irls_t_sq,
irls_u_scratch: &mut irls_u_scratch,
irls_xtwx: irls_xtwx.as_mut(),
irls_xtwz: &mut irls_xtwz,
irls_l: irls_l.as_mut(),
irls_wx: &mut irls_wx,
},
);
if view.converged && view.betas.iter().all(|b| b.is_finite()) {
view.betas.to_vec()
} else {
vec![0.0f64; p]
}
}
type BuiltGlmm = (GlmmWorkspace, Mat<f64>);
fn fit_glmm_build(
x: &[f64],
n: usize,
p: usize,
model: &ModelSpec,
cluster_ids: &[u32],
extra_ids: &[Vec<u32>],
opts: &FitOptions,
) -> Result<BuiltGlmm, Box<(Fit, Vec<f64>, f64)>> {
let re = model
.re
.as_ref()
.expect("fit_glmm requires a mixed model (re: Some)");
let slope_cols: Vec<usize> = re.slopes.iter().map(|&c| c as usize).collect();
let mut ws = GlmmWorkspace::for_cluster_spec(p, model, n, &slope_cols, opts.nagq);
ws.parallel_inner = opts.parallel_inner;
if let Some(w) = &opts.weights {
ws.prior_w[..n].copy_from_slice(w);
ws.weighted = true;
}
ws.offset = opts.offset.clone();
let x_mat = to_col_major(x, n, p);
if n == 0 || p == 0 {
return Err(Box::new((
Fit {
beta: vec![f64::NAN; p],
se: vec![f64::NAN; p],
vcov: nan_vcov(p),
tau2: vec![f64::NAN; ws.n_theta],
dispersion: f64::NAN,
diagnostics: Diagnostics::from_flags(false, false, p),
varcorr: vec![],
stddev_se: vec![],
n_eval: 0,
deviance: f64::NAN,
loglik: f64::NAN,
df: 0,
reml: false,
fitted: vec![],
ranef: vec![],
ranef_levels: vec![],
},
vec![],
f64::INFINITY,
)));
}
ws.groupings
.set_slope_scales(x_mat.as_ref().subrows(0, n), opts.weights.as_deref());
build_z(
&mut ws,
x_mat.as_ref().subrows(0, n),
cluster_ids,
extra_ids,
n,
);
ws.structured_schur = if ws.groupings.structured_extras_eligible() {
StructuredSchur::new(&ws.groupings, cluster_ids, extra_ids, n)
} else {
None
};
Ok((ws, x_mat))
}
#[cfg(test)]
#[allow(clippy::too_many_arguments)] pub(super) fn fit_glmm(
x: &[f64],
y: &[f64],
n: usize,
p: usize,
model: &ModelSpec,
cluster_ids: &[u32],
extra_ids: &[Vec<u32>],
nb_theta: f64,
start: Option<&StartValues>,
opts: &FitOptions,
) -> (Fit, Vec<f64>, f64) {
let (mut ws, x_mat) = match fit_glmm_build(x, n, p, model, cluster_ids, extra_ids, opts) {
Ok(built) => built,
Err(degenerate) => return *degenerate,
};
fit_glmm_prebuilt(
&mut ws,
x_mat.as_ref().subrows(0, n),
y,
n,
p,
model,
cluster_ids,
extra_ids,
nb_theta,
start,
opts,
)
}
pub(crate) struct GlmmResultView<'a> {
fit: GlmmFit,
nb_theta: f64,
ws: &'a GlmmWorkspace,
}
#[allow(dead_code)]
impl GlmmResultView<'_> {
pub(crate) fn t_sq(&self) -> &[f64] {
&self.ws.t_sq
}
pub(crate) fn betas(&self) -> &[f64] {
&self.ws.betas
}
pub(crate) fn var_diag(&self) -> &[f64] {
&self.ws.var_diag
}
pub(crate) fn diagnostics(&self) -> FitDiagnostics {
FitDiagnostics {
boundary_hit: self.fit.boundary_hit,
pinned_components: self.fit.pinned_components,
pirls_exhausted: self.ws.pirls_exhausted,
final_pirls_exhausted: self.ws.final_pirls_exhausted,
hessian_fallback: self.fit.hessian_fallback,
..FitDiagnostics::fixed_only(self.fit.converged)
}
}
pub(crate) fn joint_t_sq(&self) -> f64 {
self.fit.joint_t_sq
}
pub(crate) fn n_eval(&self) -> usize {
self.fit.n_eval
}
pub(crate) fn dispersion(&self) -> f64 {
self.fit.tau_squared_hat
}
pub(crate) fn theta(&self) -> &[f64] {
&self.ws.params[..self.ws.n_theta]
}
pub(crate) fn groupings(&self) -> &crate::lmm::LmmGroupings {
&self.ws.groupings
}
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn run_glmm_on<'a>(
ws: &'a mut GlmmWorkspace,
x_mat: faer::MatRef<'_, f64>,
y: &[f64],
n: usize,
p: usize,
model: &ModelSpec,
cluster_ids: &[u32],
extra_ids: &[Vec<u32>],
nb_theta: f64,
start: Option<&StartValues>,
opts: &FitOptions,
) -> GlmmResultView<'a> {
ws.nb_theta = nb_theta;
let beta_start = match start {
Some(s) if !s.beta.is_empty() => s.beta.clone(),
_ => glm_warm_start_beta(
model.family,
nb_theta,
x_mat,
y,
n,
p,
opts.offset.as_deref(),
),
};
let glmm_fit = crate::glmm::fit_glmm(
ws,
x_mat,
y,
cluster_ids,
extra_ids,
&opts.target_indices,
warm_theta(start),
&beta_start,
n,
opts.wald_se,
);
GlmmResultView {
fit: glmm_fit,
nb_theta,
ws,
}
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn glmm_view_to_fit(
view: &GlmmResultView<'_>,
y: &[f64],
n: usize,
p: usize,
model: &ModelSpec,
opts: &FitOptions,
) -> (Fit, Vec<f64>, f64) {
let ws = view.ws;
let glmm_fit = &view.fit;
let diag = view.diagnostics();
let converged = diag.converged;
let nb_theta = view.nb_theta;
let n_theta = ws.n_theta;
let beta = ws.betas.clone();
let mut se = vec![f64::NAN; p];
fill_se_by_predictor(&ws.var_diag, &opts.target_indices, &mut se);
let sigma_sq = if converged {
crate::family::glmm_sigma_sq(
model.family,
&y[..n],
&ws.prob[..n],
&ws.u[..ws.k],
ws.weighted.then(|| &ws.prior_w[..n]),
)
} else {
f64::NAN
};
let theta_scales = ws.groupings.theta_row_scales();
let tau2: Vec<f64> = if converged {
ws.params[..n_theta]
.iter()
.zip(theta_scales.iter())
.map(|(&t, &s)| (t / s) * (t / s) * sigma_sq)
.collect()
} else {
vec![f64::NAN; n_theta]
};
let dispersion = match model.family {
Family::Gamma { .. } if converged => match opts.dispersion {
Some(v) => v,
None => crate::family::pearson_dispersion(
&y[..n],
&ws.prob[..n],
model.family,
nb_theta,
n,
p,
Some(&ws.prior_w[..n]),
),
},
_ => 1.0,
};
let varcorr = if converged {
assemble_varcorr(&ws.params[..n_theta], &ws.groupings, sigma_sq)
} else {
vec![]
};
let stddev_se = if converged {
ws.theta_se[..n_theta]
.iter()
.zip(theta_scales.iter())
.map(|(&se, &s)| se / s)
.collect()
} else {
vec![f64::NAN; n_theta]
};
let vcov: Vec<Vec<f64>> = (0..p)
.map(|i| (0..p).map(|j| ws.vcov[(i, j)]).collect())
.collect();
let mu_hat = ws.prob[..n].to_vec();
let ranef_levels = super::common::ranef_level_counts(&ws.groupings);
let (fitted, ranef) = if converged {
(
mu_hat.clone(),
super::common::assemble_ranef_dense(
&ws.params[..n_theta],
&ws.groupings,
&ws.u[..ws.k],
),
)
} else {
(vec![], vec![])
};
let loglik = super::common::glmm_loglik(
model.family,
nb_theta,
if glmm_fit.deviance.is_finite() {
glmm_fit.deviance
} else {
f64::NAN
},
&y[..n],
ws.weighted.then(|| &ws.prior_w[..n]),
);
let mut fit = Fit {
beta,
se,
vcov,
tau2,
dispersion,
diagnostics: super::common::materialize_diagnostics(&diag, p, &varcorr),
varcorr,
stddev_se,
n_eval: glmm_fit.n_eval,
deviance: if glmm_fit.deviance.is_finite() {
glmm_fit.deviance
} else {
f64::NAN
},
loglik,
df: if converged {
super::common::model_df(model.family, p, n_theta, opts.dispersion.is_some())
} else {
0
},
reml: false,
fitted,
ranef,
ranef_levels,
};
fit.diagnostics.singular = fit.diagnostics.singular
|| fit.has_negligible_component(&super::common::re_scale_grid(&ws.groupings));
(fit, mu_hat, glmm_fit.deviance)
}
#[allow(clippy::too_many_arguments)]
fn fit_glmm_prebuilt(
ws: &mut GlmmWorkspace,
x_mat: faer::MatRef<f64>,
y: &[f64],
n: usize,
p: usize,
model: &ModelSpec,
cluster_ids: &[u32],
extra_ids: &[Vec<u32>],
nb_theta: f64,
start: Option<&StartValues>,
opts: &FitOptions,
) -> (Fit, Vec<f64>, f64) {
let view = run_glmm_on(
ws,
x_mat,
y,
n,
p,
model,
cluster_ids,
extra_ids,
nb_theta,
start,
opts,
);
glmm_view_to_fit(&view, y, n, p, model, opts)
}
#[allow(clippy::too_many_arguments)]
pub(super) fn fit_glmm_nb(
x: &[f64],
y: &[f64],
n: usize,
p: usize,
model: &ModelSpec,
cluster_ids: &[u32],
extra_ids: &[Vec<u32>],
_start: Option<&StartValues>,
opts: &FitOptions,
) -> Fit {
let nb_spec = ModelSpec {
family: Family::NegativeBinomial {
link: NegBinomialLink::Log,
},
re: model.re.clone(),
};
let (mut ws, x_mat) = match fit_glmm_build(x, n, p, &nb_spec, cluster_ids, extra_ids, opts) {
Ok(built) => built,
Err(degenerate) => return degenerate.0,
};
let x_ref = x_mat.as_ref().subrows(0, n);
let theta = golden_max_ln_theta(|t| {
let th = t.exp();
let (_fit, _mu, dev) = fit_glmm_prebuilt(
&mut ws,
x_ref,
y,
n,
p,
&nb_spec,
cluster_ids,
extra_ids,
th,
None,
opts,
);
-0.5 * dev + nb_profile_loglik(y, y, th, opts.weights.as_deref())
});
let mut fit_result = fit_glmm_prebuilt(
&mut ws,
x_ref,
y,
n,
p,
&nb_spec,
cluster_ids,
extra_ids,
theta,
None,
opts,
)
.0;
fit_result.dispersion = theta;
fit_result
}