diff --git a/src/game.rs b/src/game.rs index cc80139..cb16408 100644 --- a/src/game.rs +++ b/src/game.rs @@ -233,12 +233,14 @@ impl<'a, T: Time, D: Drift> Game<'a, T, D> { this } - fn likelihoods(&mut self, arena: &mut ScratchArena) { + fn run_chain(&self, arena: &mut ScratchArena, mut make_link: F) -> (f64, Vec>) + where + F: FnMut(usize, &[usize], &mut crate::factor::VarStore) -> DiffFactor, + { arena.reset(); let n_teams = self.teams.len(); - // Sort teams by result descending; reuse arena.sort_buf to avoid allocation. arena.sort_buf.extend(0..n_teams); arena.sort_buf.sort_by(|&i, &j| { self.result[j] @@ -246,7 +248,6 @@ impl<'a, T: Time, D: Drift> Game<'a, T, D> { .unwrap_or(Ordering::Equal) }); - // Team performance priors written into arena buffer (capacity reused across games). arena.team_prior.extend(arena.sort_buf.iter().map(|&t| { self.teams[t] .iter() @@ -256,30 +257,10 @@ impl<'a, T: Time, D: Drift> Game<'a, T, D> { let n_diffs = n_teams.saturating_sub(1); - // One DiffFactor per adjacent sorted-team pair; each owns a diff VarId. - // links stays local (fresh state per game; Vec capacity is typically small). let mut links: Vec = (0..n_diffs) - .map(|i| { - let tie = self.result[arena.sort_buf[i]] == self.result[arena.sort_buf[i + 1]]; - let margin = if self.p_draw == 0.0 { - 0.0 - } else { - let a: f64 = self.teams[arena.sort_buf[i]] - .iter() - .map(|p| p.beta.powi(2)) - .sum(); - let b: f64 = self.teams[arena.sort_buf[i + 1]] - .iter() - .map(|p| p.beta.powi(2)) - .sum(); - compute_margin(self.p_draw, (a + b).sqrt()) - }; - let vid = arena.vars.alloc(N_INF); - DiffFactor::Trunc(TruncFactor::new(vid, margin, tie)) - }) + .map(|i| make_link(i, &arena.sort_buf, &mut arena.vars)) .collect(); - // Per-team messages from neighbouring RankDiff factors (replaces TeamMessage). arena.lhood_lose.resize(n_teams, N_INF); arena.lhood_win.resize(n_teams, N_INF); @@ -289,7 +270,6 @@ impl<'a, T: Time, D: Drift> Game<'a, T, D> { while tuple_gt(step, 1e-6) && iter < 10 { step = (0.0_f64, 0.0_f64); - // Forward sweep: diffs 0 .. n_diffs-2 (all but the last). for (e, lf) in links[..n_diffs.saturating_sub(1)].iter_mut().enumerate() { let pw = arena.team_prior[e] * arena.lhood_lose[e]; let pl = arena.team_prior[e + 1] * arena.lhood_win[e + 1]; @@ -303,7 +283,6 @@ impl<'a, T: Time, D: Drift> Game<'a, T, D> { arena.lhood_lose[e + 1] = new_ll; } - // Backward sweep: diffs n_diffs-1 .. 1 (reverse, all but the first). for (rev_i, lf) in links[1..].iter_mut().rev().enumerate() { let e = n_diffs - 1 - rev_i; let pw = arena.team_prior[e] * arena.lhood_lose[e]; @@ -337,8 +316,7 @@ impl<'a, T: Time, D: Drift> Game<'a, T, D> { arena.lhood_lose[n_teams - 1] = pw_last - links[n_diffs - 1].msg(); } - // Evidence = product of per-diff evidences (each cached on first propagation). - self.evidence = links.iter().map(|l| l.evidence()).product(); + let evidence: f64 = links.iter().map(|l| l.evidence()).product(); // Inverse permutation: inv_buf[orig_i] = sorted_i. arena.inv_buf.resize(n_teams, 0); @@ -346,7 +324,7 @@ impl<'a, T: Time, D: Drift> Game<'a, T, D> { arena.inv_buf[orig_i] = si; } - self.likelihoods = self + let likelihoods = self .teams .iter() .zip(self.weights.iter()) @@ -368,120 +346,38 @@ impl<'a, T: Time, D: Drift> Game<'a, T, D> { .collect::>() }) .collect::>(); + + (evidence, likelihoods) + } + + fn likelihoods(&mut self, arena: &mut ScratchArena) { + let (evidence, likelihoods) = self.run_chain(arena, |i, sort_buf, vars| { + let tie = self.result[sort_buf[i]] == self.result[sort_buf[i + 1]]; + let margin = if self.p_draw == 0.0 { + 0.0 + } else { + let a: f64 = self.teams[sort_buf[i]].iter().map(|p| p.beta.powi(2)).sum(); + let b: f64 = self.teams[sort_buf[i + 1]] + .iter() + .map(|p| p.beta.powi(2)) + .sum(); + compute_margin(self.p_draw, (a + b).sqrt()) + }; + let vid = vars.alloc(N_INF); + DiffFactor::Trunc(TruncFactor::new(vid, margin, tie)) + }); + self.evidence = evidence; + self.likelihoods = likelihoods; } fn likelihoods_scored(&mut self, arena: &mut ScratchArena, score_sigma: f64) { - arena.reset(); - - let n_teams = self.teams.len(); - - arena.sort_buf.extend(0..n_teams); - arena.sort_buf.sort_by(|&i, &j| { - self.result[j] - .partial_cmp(&self.result[i]) - .unwrap_or(Ordering::Equal) + let (evidence, likelihoods) = self.run_chain(arena, |i, sort_buf, vars| { + let m_obs = self.result[sort_buf[i]] - self.result[sort_buf[i + 1]]; + let vid = vars.alloc(N_INF); + DiffFactor::Margin(MarginFactor::new(vid, m_obs, score_sigma)) }); - - arena.team_prior.extend(arena.sort_buf.iter().map(|&t| { - self.teams[t] - .iter() - .zip(self.weights[t].iter()) - .fold(N00, |p, (player, &w)| p + (player.performance() * w)) - })); - - let n_diffs = n_teams.saturating_sub(1); - - let mut links: Vec = (0..n_diffs) - .map(|i| { - // After descending-by-score sort, m_obs >= 0 for every adjacent pair. - let m_obs = self.result[arena.sort_buf[i]] - self.result[arena.sort_buf[i + 1]]; - let vid = arena.vars.alloc(N_INF); - DiffFactor::Margin(MarginFactor::new(vid, m_obs, score_sigma)) - }) - .collect(); - - arena.lhood_lose.resize(n_teams, N_INF); - arena.lhood_win.resize(n_teams, N_INF); - - let mut step = (f64::INFINITY, f64::INFINITY); - let mut iter = 0; - - while tuple_gt(step, 1e-6) && iter < 10 { - step = (0.0_f64, 0.0_f64); - - for (e, lf) in links[..n_diffs.saturating_sub(1)].iter_mut().enumerate() { - let pw = arena.team_prior[e] * arena.lhood_lose[e]; - let pl = arena.team_prior[e + 1] * arena.lhood_win[e + 1]; - let raw = pw - pl; - arena.vars.set(lf.diff(), raw * lf.msg()); - let d = lf.propagate(&mut arena.vars); - step = tuple_max(step, d); - - let new_ll = pw - lf.msg(); - step = tuple_max(step, arena.lhood_lose[e + 1].delta(new_ll)); - arena.lhood_lose[e + 1] = new_ll; - } - - for (rev_i, lf) in links[1..].iter_mut().rev().enumerate() { - let e = n_diffs - 1 - rev_i; - let pw = arena.team_prior[e] * arena.lhood_lose[e]; - let pl = arena.team_prior[e + 1] * arena.lhood_win[e + 1]; - let raw = pw - pl; - arena.vars.set(lf.diff(), raw * lf.msg()); - let d = lf.propagate(&mut arena.vars); - step = tuple_max(step, d); - - let new_lw = pl + lf.msg(); - step = tuple_max(step, arena.lhood_win[e].delta(new_lw)); - arena.lhood_win[e] = new_lw; - } - - iter += 1; - } - - if n_diffs == 1 { - let raw = (arena.team_prior[0] * arena.lhood_lose[0]) - - (arena.team_prior[1] * arena.lhood_win[1]); - arena.vars.set(links[0].diff(), raw * links[0].msg()); - links[0].propagate(&mut arena.vars); - } - - if n_diffs > 0 { - let pl1 = arena.team_prior[1] * arena.lhood_win[1]; - arena.lhood_win[0] = pl1 + links[0].msg(); - let pw_last = arena.team_prior[n_teams - 2] * arena.lhood_lose[n_teams - 2]; - arena.lhood_lose[n_teams - 1] = pw_last - links[n_diffs - 1].msg(); - } - - self.evidence = links.iter().map(|l| l.evidence()).product(); - - arena.inv_buf.resize(n_teams, 0); - for (si, &orig_i) in arena.sort_buf.iter().enumerate() { - arena.inv_buf[orig_i] = si; - } - - self.likelihoods = self - .teams - .iter() - .zip(self.weights.iter()) - .enumerate() - .map(|(orig_i, (players, weights))| { - let si = arena.inv_buf[orig_i]; - let m = arena.lhood_win[si] * arena.lhood_lose[si]; - let performance = players - .iter() - .zip(weights.iter()) - .fold(N00, |p, (player, &w)| p + (player.performance() * w)); - players - .iter() - .zip(weights.iter()) - .map(|(player, &w)| { - ((m - performance.exclude(player.performance() * w)) * (1.0 / w)) - .forget(player.beta.powi(2)) - }) - .collect::>() - }) - .collect::>(); + self.evidence = evidence; + self.likelihoods = likelihoods; } pub fn posteriors(&self) -> Vec> {