T0 + T1 + T2: engine redesign through new API surface #1
@@ -1,8 +1,14 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
|
N_INF, N00,
|
||||||
factor::{Factor, VarId, VarStore},
|
factor::{Factor, VarId, VarStore},
|
||||||
gaussian::Gaussian,
|
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)]
|
#[derive(Debug)]
|
||||||
pub(crate) struct TeamSumFactor {
|
pub(crate) struct TeamSumFactor {
|
||||||
pub(crate) inputs: Vec<(Gaussian, f64)>,
|
pub(crate) inputs: Vec<(Gaussian, f64)>,
|
||||||
@@ -10,7 +16,82 @@ pub(crate) struct TeamSumFactor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Factor for TeamSumFactor {
|
impl Factor for TeamSumFactor {
|
||||||
fn propagate(&mut self, _vars: &mut VarStore) -> (f64, f64) {
|
fn propagate(&mut self, vars: &mut VarStore) -> (f64, f64) {
|
||||||
unimplemented!("TeamSumFactor stub — implemented in Task 4")
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user