test(history): end-to-end ConvergenceOptions propagation tests
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.
This commit is contained in:
+8
-3
@@ -9,9 +9,14 @@ pub struct ConvergenceOptions {
|
||||
pub max_iter: usize,
|
||||
pub epsilon: f64,
|
||||
/// EP damping factor in natural-parameter space: each per-factor
|
||||
/// update 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]`.
|
||||
/// 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,
|
||||
}
|
||||
|
||||
|
||||
@@ -1714,4 +1714,97 @@ mod tests {
|
||||
fn history_builder_rejects_zero_score_sigma() {
|
||||
let _ = History::builder().score_sigma(0.0).build();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn history_propagates_convergence_to_inner_run_chain() {
|
||||
use crate::ConvergenceOptions;
|
||||
|
||||
let events_for =
|
||||
|h: &mut History<i64, ConstantDrift, crate::observer::NullObserver, &'static str>| {
|
||||
h.event(0)
|
||||
.team(["a"])
|
||||
.team(["b"])
|
||||
.team(["c"])
|
||||
.team(["d"])
|
||||
.ranking([0u32, 1, 2, 3])
|
||||
.commit()
|
||||
.unwrap();
|
||||
};
|
||||
|
||||
let mut h_capped: History<i64, _, _, &'static str> = History::builder()
|
||||
.convergence(ConvergenceOptions {
|
||||
max_iter: 1,
|
||||
..ConvergenceOptions::default()
|
||||
})
|
||||
.build();
|
||||
events_for(&mut h_capped);
|
||||
h_capped.converge().unwrap();
|
||||
|
||||
let mut h_full: History<i64, _, _, &'static str> = History::builder().build();
|
||||
events_for(&mut h_full);
|
||||
h_full.converge().unwrap();
|
||||
|
||||
let curves_capped = h_capped.learning_curves();
|
||||
let curves_full = h_full.learning_curves();
|
||||
|
||||
let mut max_diff: f64 = 0.0;
|
||||
for (key, capped_pts) in curves_capped.iter() {
|
||||
let full_pts = curves_full.get(key).expect("agent missing in full");
|
||||
for (capped, full) in capped_pts.iter().zip(full_pts.iter()) {
|
||||
max_diff = max_diff.max((capped.1.mu() - full.1.mu()).abs());
|
||||
max_diff = max_diff.max((capped.1.sigma() - full.1.sigma()).abs());
|
||||
}
|
||||
}
|
||||
assert!(
|
||||
max_diff > 1e-6,
|
||||
"max_iter=1 inner loop should differ from default; max_diff={max_diff}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn history_with_damping_reaches_same_fixed_point_as_undamped() {
|
||||
use crate::ConvergenceOptions;
|
||||
|
||||
let events_for =
|
||||
|h: &mut History<i64, ConstantDrift, crate::observer::NullObserver, &'static str>| {
|
||||
h.event(0)
|
||||
.team(["a"])
|
||||
.team(["b"])
|
||||
.team(["c"])
|
||||
.team(["d"])
|
||||
.ranking([0u32, 1, 2, 3])
|
||||
.commit()
|
||||
.unwrap();
|
||||
};
|
||||
|
||||
let mut h_undamped: History<i64, _, _, &'static str> = History::builder().build();
|
||||
events_for(&mut h_undamped);
|
||||
h_undamped.converge().unwrap();
|
||||
|
||||
let mut h_damped: History<i64, _, _, &'static str> = History::builder()
|
||||
.convergence(ConvergenceOptions {
|
||||
alpha: 0.5,
|
||||
max_iter: 200,
|
||||
..ConvergenceOptions::default()
|
||||
})
|
||||
.build();
|
||||
events_for(&mut h_damped);
|
||||
h_damped.converge().unwrap();
|
||||
|
||||
let curves_u = h_undamped.learning_curves();
|
||||
let curves_d = h_damped.learning_curves();
|
||||
|
||||
let mut max_diff: f64 = 0.0;
|
||||
for (key, u_pts) in curves_u.iter() {
|
||||
let d_pts = curves_d.get(key).expect("agent missing in damped");
|
||||
for (u, d) in u_pts.iter().zip(d_pts.iter()) {
|
||||
max_diff = max_diff.max((u.1.mu() - d.1.mu()).abs());
|
||||
max_diff = max_diff.max((u.1.sigma() - d.1.sigma()).abs());
|
||||
}
|
||||
}
|
||||
assert!(
|
||||
max_diff < 1e-3,
|
||||
"α=0.5 should reach the same fixed point as α=1.0; max_diff={max_diff}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user