test(game): integration tests for ConvergenceOptions behavior
Two end-to-end tests on a 4-team ranked game: - max_iter=1 produces measurably different posteriors than the default, proving run_chain reads convergence.max_iter - alpha=0.5 with extra iterations reaches the same fixed point as alpha=1.0, proving damping doesn't break convergence on benign graphs
This commit is contained in:
+95
@@ -1322,4 +1322,99 @@ mod tests {
|
||||
assert_ulps_eq!(p[1][0], post_2vs1[1][0], epsilon = 1e-6);
|
||||
assert_ulps_eq!(p[1][1], t_b[1].prior, epsilon = 1e-6);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn run_chain_honours_max_iter_in_convergence_options() {
|
||||
let players: Vec<R> = (0..4).map(|_| R::default()).collect();
|
||||
let teams: Vec<Vec<_>> = players.iter().map(|p| vec![*p]).collect();
|
||||
let result = vec![3.0, 2.0, 1.0, 0.0];
|
||||
let weights = vec![vec![1.0]; 4];
|
||||
|
||||
// Capped at 1 iteration: cannot fully propagate down a 4-team chain.
|
||||
let mut arena = ScratchArena::new();
|
||||
let g_capped = Game::ranked_with_arena(
|
||||
teams.clone(),
|
||||
&result,
|
||||
&weights,
|
||||
0.0,
|
||||
crate::ConvergenceOptions {
|
||||
max_iter: 1,
|
||||
..crate::ConvergenceOptions::default()
|
||||
},
|
||||
&mut arena,
|
||||
);
|
||||
let posteriors_capped = g_capped.posteriors();
|
||||
|
||||
// Same inputs, plenty of iterations: fully converged.
|
||||
let mut arena = ScratchArena::new();
|
||||
let g_full = Game::ranked_with_arena(
|
||||
teams,
|
||||
&result,
|
||||
&weights,
|
||||
0.0,
|
||||
crate::ConvergenceOptions::default(),
|
||||
&mut arena,
|
||||
);
|
||||
let posteriors_full = g_full.posteriors();
|
||||
|
||||
// The two posteriors should differ — capped did not converge.
|
||||
let mut max_diff: f64 = 0.0;
|
||||
for (team_capped, team_full) in posteriors_capped.iter().zip(posteriors_full.iter()) {
|
||||
for (g_capped, g_full) in team_capped.iter().zip(team_full.iter()) {
|
||||
max_diff = max_diff.max((g_capped.mu() - g_full.mu()).abs());
|
||||
max_diff = max_diff.max((g_capped.sigma() - g_full.sigma()).abs());
|
||||
}
|
||||
}
|
||||
assert!(
|
||||
max_diff > 1e-6,
|
||||
"max_iter=1 should differ from full convergence; max_diff={max_diff}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn run_chain_with_damping_converges_to_same_posterior() {
|
||||
let players: Vec<R> = (0..4).map(|_| R::default()).collect();
|
||||
let teams: Vec<Vec<_>> = players.iter().map(|p| vec![*p]).collect();
|
||||
let result = vec![3.0, 2.0, 1.0, 0.0];
|
||||
let weights = vec![vec![1.0]; 4];
|
||||
|
||||
let mut arena = ScratchArena::new();
|
||||
let g_undamped = Game::ranked_with_arena(
|
||||
teams.clone(),
|
||||
&result,
|
||||
&weights,
|
||||
0.0,
|
||||
crate::ConvergenceOptions::default(),
|
||||
&mut arena,
|
||||
);
|
||||
let posteriors_undamped = g_undamped.posteriors();
|
||||
|
||||
// alpha=0.5 with extra iterations: should reach the same fixed point.
|
||||
let mut arena = ScratchArena::new();
|
||||
let g_damped = Game::ranked_with_arena(
|
||||
teams,
|
||||
&result,
|
||||
&weights,
|
||||
0.0,
|
||||
crate::ConvergenceOptions {
|
||||
alpha: 0.5,
|
||||
max_iter: 100,
|
||||
..crate::ConvergenceOptions::default()
|
||||
},
|
||||
&mut arena,
|
||||
);
|
||||
let posteriors_damped = g_damped.posteriors();
|
||||
|
||||
let mut max_diff: f64 = 0.0;
|
||||
for (team_u, team_d) in posteriors_undamped.iter().zip(posteriors_damped.iter()) {
|
||||
for (g_u, g_d) in team_u.iter().zip(team_d.iter()) {
|
||||
max_diff = max_diff.max((g_u.mu() - g_d.mu()).abs());
|
||||
max_diff = max_diff.max((g_u.sigma() - g_d.sigma()).abs());
|
||||
}
|
||||
}
|
||||
assert!(
|
||||
max_diff < 1e-4,
|
||||
"α=0.5 should reach the same fixed point as α=1.0; max_diff={max_diff}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user