use ndarray::Array1;
use crate::error::{DmlError, Result};
#[derive(Debug, Clone)]
pub struct LinearScore {
pub psi_a: Array1<f64>,
pub psi_b: Array1<f64>,
}
#[derive(Debug, Clone)]
pub(crate) struct ScoreSolution {
pub theta: f64,
pub se: f64,
pub psi: Array1<f64>,
pub j: f64,
}
impl LinearScore {
pub fn new(psi_a: Array1<f64>, psi_b: Array1<f64>) -> Result<Self> {
if psi_a.len() != psi_b.len() || psi_a.is_empty() {
return Err(DmlError::DimensionMismatch(
"psi_a and psi_b must have the same non-zero length".into(),
));
}
Ok(Self { psi_a, psi_b })
}
pub(crate) fn solve(&self) -> Result<ScoreSolution> {
let n = self.psi_a.len() as f64;
let j = self.psi_a.sum() / n;
if j == 0.0 || !j.is_finite() {
return Err(DmlError::InvalidArgument(
"mean of psi_a is zero or not finite: theta is not identified".into(),
));
}
let theta = -(self.psi_b.sum() / n) / j;
let psi = &self.psi_a * theta + &self.psi_b;
let sigma2 = psi.dot(&psi) / n / (j * j);
Ok(ScoreSolution {
theta,
se: (sigma2 / n).sqrt(),
psi,
j,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use ndarray::array;
#[test]
fn solves_mean_score_equation() {
let s = LinearScore::new(array![-1.0, -2.0, -3.0], array![2.0, 1.0, 3.0]).unwrap();
let sol = s.solve().unwrap();
assert!((sol.theta - 1.0).abs() < 1e-15);
assert!(sol.psi.sum().abs() < 1e-12);
assert_eq!(sol.j, -2.0);
assert!((sol.se - (2.0_f64 / 3.0 / 4.0 / 3.0).sqrt()).abs() < 1e-15);
}
#[test]
fn unidentified_score_errors() {
let s = LinearScore::new(array![0.0, 0.0], array![1.0, 2.0]).unwrap();
assert!(s.solve().is_err());
assert!(LinearScore::new(array![1.0], array![1.0, 2.0]).is_err());
}
}