use super::*;
struct GaussianLl;
impl LogLikelihood for GaussianLl {
fn n_params(&self) -> usize {
2
}
fn log_likelihood(&self, params: &[f64], data: &[f64]) -> f64 {
let mu = *params.first().unwrap_or(&0.0);
let sigma = *params.get(1).unwrap_or(&0.0);
if sigma <= 0.0 {
return f64::NEG_INFINITY;
}
let n = count_to_f64(data.len());
let var = sigma * sigma;
let sse: f64 = data.iter().map(|x| (x - mu) * (x - mu)).sum();
let two_pi = 2.0 * std::f64::consts::PI;
(-n / 2.0).mul_add((two_pi * var).ln(), -(sse / (2.0 * var)))
}
}
const DATA: [f64; 8] = [2.0, 4.0, 4.0, 4.0, 5.0, 5.0, 7.0, 9.0];
fn param(fit: &MleFit, i: usize) -> Result<f64> {
fit.params()
.get(i)
.copied()
.ok_or_else(|| Error::InvalidInput(format!("missing parameter {i}")))
}
#[test]
fn fit_mle_rejects_bad_input() {
assert!(
matches!(
fit_mle(&GaussianLl, &[], &[5.0, 2.0], 1e-8),
Err(Error::InsufficientData)
),
"empty data should be InsufficientData"
);
assert!(
matches!(
fit_mle(&GaussianLl, &DATA, &[5.0], 1e-8),
Err(Error::InvalidInput(_))
),
"wrong init length should be InvalidInput"
);
assert!(
matches!(
fit_mle(&GaussianLl, &DATA, &[5.0, 2.0], 0.0),
Err(Error::InvalidInput(_))
),
"non-positive tolerance should be InvalidInput"
);
}
#[test]
fn fit_mle_rejects_domain_invalid_init() {
assert!(
matches!(
fit_mle(&GaussianLl, &DATA, &[0.0, -1.0], 1e-8),
Err(Error::InvalidInput(_))
),
"domain-invalid init should be InvalidInput"
);
}
#[test]
fn fit_mle_from_near_boundary_init_reaches_interior() -> Result<()> {
let fit = fit_mle(&GaussianLl, &DATA, &[4.0, 0.7], 1e-8)?;
let mu = param(&fit, 0)?;
let sigma = param(&fit, 1)?;
assert!((mu - 5.0).abs() < 1e-5, "mu_hat was {mu}");
assert!((sigma - 2.0).abs() < 1e-5, "sigma_hat was {sigma}");
assert!(sigma > 0.0, "sigma left valid domain: {sigma}");
assert!(fit.converged(), "expected convergence");
assert!(fit.log_likelihood().is_finite(), "ll not finite");
Ok(())
}
#[test]
fn log_likelihood_matches_scipy() {
let at_opt = GaussianLl.log_likelihood(&[5.0, 2.0], &DATA);
assert!(
(at_opt - (-16.896_685_710_116_945)).abs() < 1e-9,
"ll at (5,2) was {at_opt}"
);
let off = GaussianLl.log_likelihood(&[3.0, 1.5], &DATA);
assert!(
(off - (-24.817_451_352_724_916)).abs() < 1e-9,
"ll at (3,1.5) was {off}"
);
}
#[test]
fn fit_mle_recovers_estimates_and_exposes_diagnostics() -> Result<()> {
let fit = fit_mle(&GaussianLl, &DATA, &[4.0, 1.0], 1e-8)?;
let mu = param(&fit, 0)?;
let sigma = param(&fit, 1)?;
assert!((mu - 5.0).abs() < 1e-5, "mu_hat was {mu}");
assert!((sigma - 2.0).abs() < 1e-5, "sigma_hat was {sigma}");
assert!(sigma > 0.0, "sigma left valid domain: {sigma}");
assert_eq!(fit.params().len(), 2, "params len");
assert!(fit.converged(), "expected convergence");
assert!(fit.iterations() >= 1, "iterations {}", fit.iterations());
assert!(fit.log_likelihood().is_finite(), "ll not finite");
Ok(())
}
#[test]
fn aic_bic_satisfy_their_identities() -> Result<()> {
let fit = fit_mle(&GaussianLl, &DATA, &[4.0, 1.0], 1e-8)?;
let ll = fit.log_likelihood();
let (k, n) = (2.0_f64, 8.0_f64);
assert!(
(fit.aic() - 2.0f64.mul_add(k, -2.0 * ll)).abs() < 1e-12,
"aic identity broken: {}",
fit.aic()
);
assert!(
(fit.bic() - k.mul_add(n.ln(), -2.0 * ll)).abs() < 1e-12,
"bic identity broken: {}",
fit.bic()
);
Ok(())
}
#[test]
fn from_closed_form_matches_fit_mle_criteria() -> Result<()> {
let fit = fit_mle(&GaussianLl, &DATA, &[4.0, 1.0], 1e-8)?;
let closed = MleFit::from_closed_form(fit.params().to_vec(), fit.log_likelihood(), DATA.len());
assert!(
(closed.aic() - fit.aic()).abs() < 1e-12,
"aic mismatch: {} vs {}",
closed.aic(),
fit.aic()
);
assert!(
(closed.bic() - fit.bic()).abs() < 1e-12,
"bic mismatch: {} vs {}",
closed.bic(),
fit.bic()
);
assert!(closed.converged(), "closed form is exact, so converged");
assert_eq!(closed.iterations(), 0, "closed form does no iterations");
Ok(())
}
#[test]
fn maximum_likelihood_honors_convergence_tolerance() -> Result<()> {
use crate::likelihood::MaximumLikelihood;
let loose = MaximumLikelihood {
convergence_tolerance: 1e6,
..Default::default()
};
let loose_fit = loose.fit(&GaussianLl, &DATA, &[4.0, 1.0])?;
let loose_mu = param(&loose_fit, 0)?;
let tight = MaximumLikelihood {
convergence_tolerance: 1e-10,
..Default::default()
};
let tight_fit = tight.fit(&GaussianLl, &DATA, &[4.0, 1.0])?;
let tight_mu = param(&tight_fit, 0)?;
assert!((tight_mu - 5.0).abs() < 1e-5, "tight mu was {tight_mu}");
assert!(
(loose_mu - tight_mu).abs() > 1e-3,
"loose ({loose_mu}) and tight ({tight_mu}) tolerances behaved identically"
);
let defaulted = MaximumLikelihood::default();
let default_fit = defaulted.fit(&GaussianLl, &DATA, &[4.0, 1.0])?;
let default_mu = param(&default_fit, 0)?;
assert!(
(default_mu - 5.0).abs() < 1e-5,
"default mu was {default_mu}"
);
Ok(())
}