1use ndarray::Array1;
4
5use crate::error::{DmlError, Result};
6
7#[derive(Debug, Clone)]
15pub struct LinearScore {
16 pub psi_a: Array1<f64>,
17 pub psi_b: Array1<f64>,
18}
19
20#[derive(Debug, Clone)]
22pub(crate) struct ScoreSolution {
23 pub theta: f64,
24 pub se: f64,
25 pub psi: Array1<f64>,
27 pub j: f64,
29}
30
31impl LinearScore {
32 pub fn new(psi_a: Array1<f64>, psi_b: Array1<f64>) -> Result<Self> {
33 if psi_a.len() != psi_b.len() || psi_a.is_empty() {
34 return Err(DmlError::DimensionMismatch(
35 "psi_a and psi_b must have the same non-zero length".into(),
36 ));
37 }
38 Ok(Self { psi_a, psi_b })
39 }
40
41 pub(crate) fn solve(&self) -> Result<ScoreSolution> {
44 let n = self.psi_a.len() as f64;
45 let j = self.psi_a.sum() / n;
46 if j == 0.0 || !j.is_finite() {
47 return Err(DmlError::InvalidArgument(
48 "mean of psi_a is zero or not finite: theta is not identified".into(),
49 ));
50 }
51 let theta = -(self.psi_b.sum() / n) / j;
52 let psi = &self.psi_a * theta + &self.psi_b;
53 let sigma2 = psi.dot(&psi) / n / (j * j);
54 Ok(ScoreSolution {
55 theta,
56 se: (sigma2 / n).sqrt(),
57 psi,
58 j,
59 })
60 }
61}
62
63#[cfg(test)]
64mod tests {
65 use super::*;
66 use ndarray::array;
67
68 #[test]
69 fn solves_mean_score_equation() {
70 let s = LinearScore::new(array![-1.0, -2.0, -3.0], array![2.0, 1.0, 3.0]).unwrap();
71 let sol = s.solve().unwrap();
72 assert!((sol.theta - 1.0).abs() < 1e-15);
73 assert!(sol.psi.sum().abs() < 1e-12);
74 assert_eq!(sol.j, -2.0);
75 assert!((sol.se - (2.0_f64 / 3.0 / 4.0 / 3.0).sqrt()).abs() < 1e-15);
77 }
78
79 #[test]
80 fn unidentified_score_errors() {
81 let s = LinearScore::new(array![0.0, 0.0], array![1.0, 2.0]).unwrap();
82 assert!(s.solve().is_err());
83 assert!(LinearScore::new(array![1.0], array![1.0, 2.0]).is_err());
84 }
85}