use faer::Mat;
use crate::lmm::{fit_lmm, LmmWorkspace};
use crate::ols::{OlsScratch, OlsSuffStats, PANEL_ROWS};
use crate::{Estimator, GroupingRelation, ModelSpec, Sizing};
pub struct Fit {
pub beta: Vec<f64>,
pub se: Vec<f64>,
pub tau2: Vec<f64>,
pub converged: bool,
}
pub struct FitOptions {
pub target_indices: Vec<u32>,
}
pub fn fit(x: &[f64], y: &[f64], n: usize, p: usize, model: &ModelSpec, opts: &FitOptions) -> Fit {
assert_eq!(
x.len(),
n * p,
"x must have n*p elements in row-major layout"
);
assert_eq!(y.len(), n, "y must have n elements");
match model.estimator {
Estimator::Ols => fit_ols(x, y, n, p, opts),
Estimator::Mle => fit_mle(x, y, n, p, model, opts),
Estimator::Glm => unimplemented!("Estimator::Glm is not yet wired in glmm::fit (both unclustered GLM and clustered GLMM are unimplemented); use the glmm::mcpower surface for now"),
}
}
fn fit_ols(x: &[f64], y: &[f64], n: usize, p: usize, opts: &FitOptions) -> Fit {
let t = opts.target_indices.len();
let p1 = p.max(1); let mut fit_betas = vec![0.0f64; p1];
let mut fit_var_diag = vec![0.0f64; t.max(1)];
let mut fit_t_sq = vec![0.0f64; t.max(1)];
let mut fit_u_scratch = vec![0.0f64; p1];
let mut fit_factor = Mat::<f64>::zeros(p1, p1);
let mut fit_rhs = Mat::<f64>::zeros(p1, 1);
let mut suff_xtx = Mat::<f64>::zeros(p1, p1);
let mut suff_xty = vec![0.0f64; p1];
let mut suff_yty = 0.0f64;
let mut suff_sum_y = 0.0f64;
let mut suff_n_rows = 0usize;
let mut suff_xtx_work = Mat::<f64>::zeros(p1, p1);
let mut panel_x = vec![0.0f64; PANEL_ROWS * p1];
let mut panel_y = vec![0.0f64; PANEL_ROWS];
let mut x_mat = Mat::<f64>::zeros(n.max(1), p1);
for i in 0..n {
for j in 0..p {
x_mat[(i, j)] = x[i * p + j];
}
}
{
let mut suff = OlsSuffStats {
xtx: suff_xtx.as_mut(),
xty: &mut suff_xty,
yty: &mut suff_yty,
sum_y: &mut suff_sum_y,
n_rows: &mut suff_n_rows,
panel_x: &mut panel_x,
panel_y: &mut panel_y,
};
if n > 0 && p > 0 {
suff.add_rows(x_mat.as_ref().subrows(0, n), y);
}
}
let view = {
let scratch = OlsScratch {
fit_betas: &mut fit_betas,
fit_var_diag: &mut fit_var_diag,
fit_t_sq: &mut fit_t_sq,
fit_u_scratch: &mut fit_u_scratch,
fit_factor: fit_factor.as_mut(),
fit_rhs: fit_rhs.as_mut(),
};
crate::ols::fit_suff_stats_t_sq(
suff_xtx.as_ref(),
&suff_xty,
suff_yty,
suff_sum_y,
suff_n_rows,
&opts.target_indices,
1e-12,
suff_xtx_work.as_mut(),
scratch,
)
};
let beta = view.betas.to_vec();
let converged = view.converged;
let mut se = vec![f64::NAN; p];
for (i, &ti) in opts.target_indices.iter().enumerate() {
let vd = view.var_diag[i];
if vd.is_finite() && vd >= 0.0 {
se[ti as usize] = vd.sqrt();
}
}
Fit {
beta,
se,
tau2: vec![],
converged,
}
}
fn primary_cluster_of_row(sizing: &Sizing, i: usize) -> u32 {
sizing.cluster_of_row(i) as u32
}
fn extra_level_of_row(model: &ModelSpec, g: usize, i: usize) -> u32 {
let rel = &model.extra_groupings[g].relation;
let level = match &model.sizing {
Sizing::FixedClusters { n_clusters } => {
let s = (*n_clusters).max(1) as usize;
let mut stride = s;
for h in &model.extra_groupings[..g] {
stride *= block_levels(&h.relation);
}
let within = (i / stride) % block_levels(rel);
match rel {
GroupingRelation::Crossed { .. } => within,
GroupingRelation::NestedWithin { n_per_parent } => {
(i % s) * (*n_per_parent).max(1) as usize + within
}
}
}
Sizing::FixedSize { cluster_size } => {
let cs = (*cluster_size).max(1) as usize;
let np = block_levels(rel);
(i / cs) * np + (i % cs) % np
}
};
level as u32
}
fn block_levels(rel: &GroupingRelation) -> usize {
match rel {
GroupingRelation::Crossed { n_clusters } => (*n_clusters).max(1) as usize,
GroupingRelation::NestedWithin { n_per_parent } => (*n_per_parent).max(1) as usize,
}
}
fn fit_mle(x: &[f64], y: &[f64], n: usize, p: usize, model: &ModelSpec, opts: &FitOptions) -> Fit {
let slope_cols: Vec<usize> = model.slopes.iter().map(|s| s.column as usize).collect();
let mut ws = LmmWorkspace::for_cluster_spec(p, model, n, &slope_cols);
let cluster_ids: Vec<u32> = (0..n)
.map(|i| primary_cluster_of_row(&model.sizing, i))
.collect();
let extra_ids: Vec<Vec<u32>> = (0..model.extra_groupings.len())
.map(|g| (0..n).map(|i| extra_level_of_row(model, g, i)).collect())
.collect();
let p1 = p.max(1);
let mut x_mat = Mat::<f64>::zeros(n.max(1), p1);
for i in 0..n {
for j in 0..p {
x_mat[(i, j)] = x[i * p + j];
}
}
ws.suff.reset();
if n > 0 && p > 0 {
ws.suff
.add_rows_multi(x_mat.as_ref().subrows(0, n), y, &cluster_ids, &extra_ids);
}
let theta_truth = ws.theta_truth.clone();
let lmm_fit = fit_lmm(&mut ws, &opts.target_indices, Some(&theta_truth));
let beta = ws.fit.betas.clone();
let sigma_sq = lmm_fit.sigma_sq;
let mut se = vec![f64::NAN; p];
for &ti in &opts.target_indices {
let vd = ws.fit.var_diag[ti as usize];
if vd.is_finite() && vd >= 0.0 {
se[ti as usize] = vd.sqrt();
}
}
let tau2: Vec<f64> = if lmm_fit.converged {
ws.theta.iter().map(|&t| t * t * sigma_sq).collect()
} else {
ws.theta.iter().map(|_| f64::NAN).collect()
};
Fit {
beta,
se,
tau2,
converged: lmm_fit.converged,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{Estimator, ModelSpec, Sizing, WaldSe};
#[test]
fn fit_ols_recovers_slope() {
let n = 20;
let p = 2;
let x: Vec<f64> = (0..n).flat_map(|i| [1.0, i as f64]).collect(); let y: Vec<f64> = (0..n).map(|i| 2.0 * i as f64).collect();
let model = ModelSpec {
sizing: Sizing::FixedClusters { n_clusters: 1 },
tau_squared: 0.0,
slopes: vec![],
extra_groupings: vec![],
estimator: Estimator::Ols,
wald_se: WaldSe::Hessian,
};
let f = fit(
&x,
&y,
n,
p,
&model,
&FitOptions {
target_indices: vec![1],
},
);
assert!(f.converged);
assert!((f.beta[1] - 2.0).abs() < 1e-6);
}
fn lcg(state: &mut u64) -> f64 {
*state = state
.wrapping_mul(6364136223846793005)
.wrapping_add(1442695040888963407);
(((*state >> 11) as f64) / ((1u64 << 53) as f64)) * 2.0 - 1.0
}
fn lmm_hand_dataset() -> (Vec<f64>, Vec<f64>, usize, usize) {
let n = 48usize;
let p = 3;
let n_clusters = 6usize;
let mut st = 42u64;
let u_c: Vec<f64> = (0..n_clusters).map(|_| 0.6 * lcg(&mut st)).collect();
let mut x = vec![0.0f64; n * p];
let mut y = vec![0.0f64; n];
for i in 0..n {
let c = i % n_clusters;
let x1 = lcg(&mut st);
let x2 = lcg(&mut st);
x[i * p] = 1.0;
x[i * p + 1] = x1;
x[i * p + 2] = x2;
y[i] = 0.5 + 0.4 * x1 - 0.2 * x2 + u_c[c] + 0.8 * lcg(&mut st);
}
(x, y, n, p)
}
#[test]
fn fit_lmm_smoke() {
let (x, y, n, p) = lmm_hand_dataset();
let model = ModelSpec {
sizing: Sizing::FixedClusters { n_clusters: 6 },
tau_squared: 0.25,
slopes: vec![],
extra_groupings: vec![],
estimator: Estimator::Mle,
wald_se: WaldSe::Hessian,
};
let f = fit(
&x,
&y,
n,
p,
&model,
&FitOptions {
target_indices: vec![1, 2],
},
);
assert!(f.converged, "LMM should converge on clean clustered data");
assert!(
f.tau2[0].is_finite() && f.tau2[0] >= 0.0,
"tau2[0] must be a finite non-negative variance, got {}",
f.tau2[0]
);
}
}