68be7ab5b7
Two integration tests on a 4-team ranked event: - max_iter=1 set on HistoryBuilder produces measurably different posteriors than default, proving the inner loop honors the propagated max_iter - alpha=0.5 with extra iterations reaches the same fixed point as alpha=1.0, proving damping doesn't break correctness on the History path Also updates the alpha doc comment to clarify it applies only to the within-game EP loop, not the outer cross-history sweep.
54 lines
1.4 KiB
Rust
54 lines
1.4 KiB
Rust
//! Convergence configuration and reporting.
|
|
|
|
use std::time::Duration;
|
|
|
|
use smallvec::SmallVec;
|
|
|
|
#[derive(Clone, Copy, Debug)]
|
|
pub struct ConvergenceOptions {
|
|
pub max_iter: usize,
|
|
pub epsilon: f64,
|
|
/// EP damping factor in natural-parameter space: each per-factor
|
|
/// update inside a single game writes `α·new + (1−α)·old`. `1.0` is
|
|
/// undamped (default); `< 1.0` stabilises oscillating fixed-point
|
|
/// loops at the cost of more iterations. Must be in `(0.0, 1.0]`.
|
|
///
|
|
/// Applies only to the within-game EP loop (`run_chain`). The outer
|
|
/// `History::converge` cross-history sweep is undamped regardless of
|
|
/// this value — cross-slice damping is a different concept and not
|
|
/// in scope.
|
|
pub alpha: f64,
|
|
}
|
|
|
|
impl Default for ConvergenceOptions {
|
|
fn default() -> Self {
|
|
Self {
|
|
max_iter: crate::ITERATIONS,
|
|
epsilon: crate::EPSILON,
|
|
alpha: 1.0,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Post-hoc summary of a `History::converge` call.
|
|
#[derive(Clone, Debug)]
|
|
pub struct ConvergenceReport {
|
|
pub iterations: usize,
|
|
pub final_step: (f64, f64),
|
|
pub log_evidence: f64,
|
|
pub converged: bool,
|
|
pub per_iteration_time: SmallVec<[Duration; 32]>,
|
|
pub slices_skipped: usize,
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn default_alpha_is_one_for_undamped_behavior() {
|
|
let opts = ConvergenceOptions::default();
|
|
assert_eq!(opts.alpha, 1.0);
|
|
}
|
|
}
|