diff --git a/src/game.rs b/src/game.rs index d943846..1b9ede3 100644 --- a/src/game.rs +++ b/src/game.rs @@ -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 = (0..4).map(|_| R::default()).collect(); + let teams: 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 = (0..4).map(|_| R::default()).collect(); + let teams: 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}" + ); + } }