use super::ols::{fit_ols, fit_ols_prebuilt, ols_view_to_fit, OlsWorkspace};
use super::*;
use crate::test_support::assert_near;
use crate::{Family, GroupIds, ModelSpec};
fn ols_hand_dataset() -> (Vec<f64>, Vec<f64>, usize, usize) {
let n = 20;
let p = 3;
let mut x = Vec::with_capacity(n * p);
let mut y = Vec::with_capacity(n);
for i in 0..n {
let a = i as f64;
let b = ((i * 7) % 11) as f64 - 5.0;
x.extend_from_slice(&[1.0, a, b]);
y.push(0.5 + 1.3 * a - 0.7 * b + ((i % 3) as f64 - 1.0));
}
(x, y, n, p)
}
#[test]
fn fit_ols_prebuilt_reused_ws_near_identical_to_fresh() {
let (x, y, n, p) = ols_hand_dataset();
let opts = FitOptions {
target_indices: vec![1, 2],
..FitOptions::default()
};
let fresh = fit_ols(&x, &y, n, p, &opts);
let mut ws = OlsWorkspace::new(n, p, opts.target_indices.len(), opts.weights.is_some());
let x_mat = super::common::to_col_major(&x, n, p);
let reused = {
let v = fit_ols_prebuilt(&mut ws, x_mat.as_ref().subrows(0, n), &y, n, p, &opts);
ols_view_to_fit(&v, &x, &y, n, p, &opts)
};
let reused2 = {
let v = fit_ols_prebuilt(&mut ws, x_mat.as_ref().subrows(0, n), &y, n, p, &opts);
ols_view_to_fit(&v, &x, &y, n, p, &opts)
};
assert_near(&fresh.beta, &reused.beta, "beta reused vs fresh");
assert_near(&fresh.se, &reused.se, "se reused vs fresh");
assert_near(&[fresh.dispersion], &[reused.dispersion], "dispersion");
assert_near(&reused.beta, &reused2.beta, "beta second reuse");
}
#[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 {
family: Family::Gaussian,
re: None,
};
let f = fit_cold(
&x,
&y,
n,
p,
&model,
&GroupIds::default(),
&FitOptions {
target_indices: vec![1],
..FitOptions::default()
},
);
assert!(f.converged());
assert!((f.beta[1] - 2.0).abs() < 1e-6);
assert!(f.deviance.is_nan());
assert!(!f.singular());
assert!(f.tau2.is_empty());
assert!(f.dispersion >= 0.0 && f.dispersion < 1e-9);
}
#[test]
fn fit_ols_weighted_matches_r_lm() {
let xv = [
0.2, 1.4, -0.8, 2.1, 0.5, -1.3, 1.9, 0.0, -0.6, 1.1, 2.4, -1.7,
];
let w = vec![1.0, 2.0, 1.0, 3.0, 1.0, 2.0, 1.0, 4.0, 2.0, 1.0, 3.0, 2.0];
let y = vec![
0.8, 3.1, -1.2, 4.6, 1.4, -2.0, 4.1, 0.3, -0.9, 2.6, 5.2, -3.1,
];
let n = 12;
let mut x = Vec::with_capacity(n * 2);
for &xi in &xv {
x.extend_from_slice(&[1.0, xi]);
}
const REF_BETA: [f64; 2] = [0.371528122456273, 1.996237765292144];
const REF_SE: [f64; 2] = [0.0289002251717619, 0.0195893362923423];
const REF_VCOV_01: f64 = -0.0002002132676736411;
let model = ModelSpec {
family: Family::Gaussian,
re: None,
};
let opts = FitOptions {
target_indices: vec![0, 1],
weights: Some(w),
..FitOptions::default()
};
let f = fit_cold(&x, &y, n, 2, &model, &GroupIds::default(), &opts);
assert!(f.converged());
for j in 0..2 {
assert!((f.beta[j] - REF_BETA[j]).abs() < 1e-9, "beta[{j}]");
assert!((f.se[j] - REF_SE[j]).abs() < 1e-9, "se[{j}]");
}
for (i, j) in [(0, 1), (1, 0)] {
assert!(
(f.vcov[i][j] - REF_VCOV_01).abs() < 1e-12,
"vcov[{i}][{j}] = {}, R = {REF_VCOV_01}",
f.vcov[i][j]
);
}
const REF_LOGLIK: f64 = 11.7602761183173;
assert!(
(f.loglik - REF_LOGLIK).abs() < 1e-9,
"loglik {} vs R {REF_LOGLIK}",
f.loglik
);
assert_eq!(f.df, 3);
assert!(!f.reml);
assert_eq!(f.fitted.len(), n);
for (i, &xi) in xv.iter().enumerate() {
let eta = f.beta[0] + f.beta[1] * xi;
assert!((f.fitted[i] - eta).abs() < 1e-12, "fitted[{i}]");
}
}
#[test]
fn fit_ols_offset_matches_r_lm() {
let xv = [
0.2, 1.4, -0.8, 2.1, 0.5, -1.3, 1.9, 0.0, -0.6, 1.1, 2.4, -1.7,
];
let w = vec![1.0, 2.0, 1.0, 3.0, 1.0, 2.0, 1.0, 4.0, 2.0, 1.0, 3.0, 2.0];
let y = vec![
0.8, 3.1, -1.2, 4.6, 1.4, -2.0, 4.1, 0.3, -0.9, 2.6, 5.2, -3.1,
];
let n = 12;
let o: Vec<f64> = (0..n).map(|i| 0.3 * (i % 4) as f64).collect();
let mut x = Vec::with_capacity(n * 2);
for &xi in &xv {
x.extend_from_slice(&[1.0, xi]);
}
const REF_BETA: [f64; 2] = [-0.159548531835057, 1.964134686017193];
const REF_LOGLIK: f64 = -5.65572764128147;
let model = ModelSpec {
family: Family::Gaussian,
re: None,
};
let opts = FitOptions {
target_indices: vec![0, 1],
weights: Some(w),
offset: Some(o.clone()),
..FitOptions::default()
};
let f = fit_cold(&x, &y, n, 2, &model, &GroupIds::default(), &opts);
assert!(f.converged());
for (j, (&b, &r)) in f.beta.iter().zip(&REF_BETA).enumerate() {
assert!((b - r).abs() < 1e-9, "beta[{j}] {b} vs R {r}");
}
assert!(
(f.loglik - REF_LOGLIK).abs() < 1e-9,
"loglik {} vs R {REF_LOGLIK}",
f.loglik
);
for (i, &xi) in xv.iter().enumerate() {
let eta = o[i] + f.beta[0] + f.beta[1] * xi;
assert!((f.fitted[i] - eta).abs() < 1e-12, "fitted[{i}]");
}
let f_none = fit_cold(
&x,
&y,
n,
2,
&model,
&GroupIds::default(),
&FitOptions {
offset: None,
..opts.clone()
},
);
let f_zero = fit_cold(
&x,
&y,
n,
2,
&model,
&GroupIds::default(),
&FitOptions {
offset: Some(vec![0.0; n]),
..opts
},
);
assert_eq!(f_none.beta, f_zero.beta);
assert_eq!(f_none.loglik.to_bits(), f_zero.loglik.to_bits());
}
#[test]
fn fit_ols_constant_weights_invariant() {
let xv = [0.2, 1.4, -0.8, 2.1, 0.5, -1.3, 1.9, 0.0];
let y: Vec<f64> = xv
.iter()
.enumerate()
.map(|(i, v)| 1.0 + 2.0 * v + if i == 0 { 0.01 } else { 0.0 })
.collect();
let n = 8;
let mut x = Vec::with_capacity(n * 2);
for &xi in &xv {
x.extend_from_slice(&[1.0, xi]);
}
let model = ModelSpec {
family: Family::Gaussian,
re: None,
};
let base = FitOptions {
target_indices: vec![0, 1],
..FitOptions::default()
};
let f0 = fit_cold(&x, &y, n, 2, &model, &GroupIds::default(), &base);
let opts = FitOptions {
weights: Some(vec![3.0; n]),
..base
};
let f1 = fit_cold(&x, &y, n, 2, &model, &GroupIds::default(), &opts);
for j in 0..2 {
assert!((f0.beta[j] - f1.beta[j]).abs() < 1e-12);
assert!((f0.se[j] - f1.se[j]).abs() < 1e-12);
}
}
#[test]
fn fit_ols_weighted_collinear_fits_with_an_honest_se() {
let n = 60;
let p = 3;
let split = 40;
const WSMALL: f64 = 1e-11;
let mut x = Vec::with_capacity(n * p);
let mut y = Vec::with_capacity(n);
let mut w = Vec::with_capacity(n);
for i in 0..n {
let a = ((i * 13) % 17) as f64 - 8.0;
let delta = if i < split { 0.0 } else { 1.0 };
x.extend_from_slice(&[1.0, a, a + delta]);
y.push(0.5 + 1.3 * a + 0.477 * (a + delta) + ((i % 3) as f64 - 1.0));
w.push(if i < split { 1.0 } else { WSMALL });
}
let opts = FitOptions {
target_indices: vec![0, 1, 2],
weights: Some(w),
..FitOptions::default()
};
let model = ModelSpec {
family: Family::Gaussian,
re: None,
};
let ids = GroupIds {
primary: vec![],
extra: vec![],
};
assert!(
!super::common::detect_aliased(&x, n, p).iter().any(|&a| a),
"the RAW design is full rank — this test is only meaningful if the \
alias gate lets it through"
);
let mut ws = OlsWorkspace::new(n, p, opts.target_indices.len(), true);
let x_mat = super::common::to_col_major(&x, n, p);
let view = fit_ols_prebuilt(&mut ws, x_mat.as_ref().subrows(0, n), &y, n, p, &opts);
assert!(
view.converged,
"the fit is computable and must be returned, not refused"
);
assert!(
view.pivot < crate::ols::PIVOT_MIN,
"the weighted pivot must land in the ill-conditioned band, got {}",
view.pivot
);
let f = fit_cold(&x, &y, n, p, &model, &ids, &opts);
assert!(f.converged(), "the fit is computable and must be returned");
assert_eq!(
f.aliased(),
vec![false; p],
"nothing is redundant in the raw design, so no column is dropped"
);
for j in [1usize, 2] {
assert!(
f.se[j] > 100.0 * f.beta[j].abs(),
"β[{j}] = {} must carry an SE orders above it, got {}",
f.beta[j],
f.se[j]
);
}
}