refactor: dedupe Game::likelihoods and likelihoods_scored via run_chain

Both methods were 95-line near-duplicates differing only in the closure
that builds the per-diff DiffFactor. Extract the shared body as a
private run_chain<F>(&self, arena, make_link) helper that returns
(evidence, likelihoods); the two callers shrink to ~10 lines each.

Pure code-shape change: posteriors and evidence remain bit-equal; all
existing tests (lib + integration) pass unchanged.
This commit is contained in:
2026-05-08 14:32:29 +02:00
parent 7481c31ad8
commit 68b589b965
+35 -139
View File
@@ -233,12 +233,14 @@ impl<'a, T: Time, D: Drift<T>> Game<'a, T, D> {
this
}
fn likelihoods(&mut self, arena: &mut ScratchArena) {
fn run_chain<F>(&self, arena: &mut ScratchArena, mut make_link: F) -> (f64, Vec<Vec<Gaussian>>)
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<T>> 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<T>> 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<DiffFactor> = (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<T>> 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<T>> 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<T>> 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<T>> 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<T>> Game<'a, T, D> {
.collect::<Vec<_>>()
})
.collect::<Vec<_>>();
(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<DiffFactor> = (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::<Vec<_>>()
})
.collect::<Vec<_>>();
self.evidence = evidence;
self.likelihoods = likelihoods;
}
pub fn posteriors(&self) -> Vec<Vec<Gaussian>> {