use crate::{ N_INF, N00, factor::{Factor, VarId, VarStore}, gaussian::Gaussian, }; /// Computes the weighted sum of player performances into a team-perf var. /// /// Inputs are pre-computed player performance Gaussians (i.e., player priors /// already with beta² noise added via `Player::performance()`). The factor /// runs once per game and writes the weighted sum to the output var. #[derive(Debug)] pub(crate) struct TeamSumFactor { pub(crate) inputs: Vec<(Gaussian, f64)>, pub(crate) out: VarId, } impl Factor for TeamSumFactor { fn propagate(&mut self, vars: &mut VarStore) -> (f64, f64) { let perf = self.inputs.iter().fold(N00, |acc, (g, w)| acc + (*g * *w)); let old = vars.get(self.out); vars.set(self.out, perf); old.delta(perf) } } #[cfg(test)] mod tests { use super::*; #[test] fn single_player_unit_weight() { let mut vars = VarStore::new(); let out = vars.alloc(N_INF); let g = Gaussian::from_ms(25.0, 5.0); let mut f = TeamSumFactor { inputs: vec![(g, 1.0)], out, }; f.propagate(&mut vars); let result = vars.get(out); assert!((result.mu() - 25.0).abs() < 1e-12); assert!((result.sigma() - 5.0).abs() < 1e-12); } #[test] fn two_players_summed() { let mut vars = VarStore::new(); let out = vars.alloc(N_INF); let g1 = Gaussian::from_ms(20.0, 3.0); let g2 = Gaussian::from_ms(30.0, 4.0); let mut f = TeamSumFactor { inputs: vec![(g1, 1.0), (g2, 1.0)], out, }; f.propagate(&mut vars); let result = vars.get(out); // sum: mu = 20 + 30 = 50, var = 9 + 16 = 25, sigma = 5 assert!((result.mu() - 50.0).abs() < 1e-12); assert!((result.sigma() - 5.0).abs() < 1e-12); } #[test] fn weighted_inputs() { let mut vars = VarStore::new(); let out = vars.alloc(N_INF); let g = Gaussian::from_ms(10.0, 2.0); let mut f = TeamSumFactor { inputs: vec![(g, 2.0)], out, }; f.propagate(&mut vars); let result = vars.get(out); // g * 2.0: mu = 10*2 = 20, sigma = 2*2 = 4 assert!((result.mu() - 20.0).abs() < 1e-12); assert!((result.sigma() - 4.0).abs() < 1e-12); } #[test] fn delta_is_zero_on_repeat_propagate() { let mut vars = VarStore::new(); let out = vars.alloc(N_INF); let g = Gaussian::from_ms(5.0, 1.0); let mut f = TeamSumFactor { inputs: vec![(g, 1.0)], out, }; f.propagate(&mut vars); let (dmu, dsig) = f.propagate(&mut vars); assert!(dmu < 1e-12, "expected ~0 delta on repeat, got {}", dmu); assert!(dsig < 1e-12); } }