refactor: unify convergence defaults, validate builders, clear dead code
Convergence configuration had two disagreeing sources of truth and one
misleading report:
- `EpsilonOrMax::default()` capped at 10 iterations while
`ConvergenceOptions::default()` allowed 30, and which applied depended on
whether inference went through `run_chain` or a `Schedule`. The schedule
default now derives from `ConvergenceOptions`.
- A graph with no iterating factors reported `converged: false` with an
infinite step, despite being at its fixed point after the setup pass. It
now reports converged with a zero step.
- `TimeSlice::iterate_to_convergence` hard-coded an epsilon and a
20-iteration cap matching neither. It reads `self.convergence` and is
scoped to `#[cfg(test)]`, which is all it was ever used by.
`HistoryBuilder::p_draw` and `::convergence` now validate their arguments
like `score_sigma` already did, instead of accepting a negative `p_draw` or
an `alpha` of zero — the latter leaves every EP update unapplied, so
inference silently returns the priors.
Removing the `#[allow(dead_code)]` masks let the compiler report what they
were hiding: four `OwnedGame` fields that were stored and never read, two
`ColorGroups` helpers and three `SkillStore` helpers used only by tests, and
`iterate_to_convergence` above. Test-only items are now `#[cfg(test)]` and
the unread fields are gone.
Also exported `HistoryBuilder`, which was public but unreachable — callers
could chain `History::builder()` but could not name the type — and added
`Rating::{prior, beta, drift}` and `Index::get`, so handles the API hands
out can be read back.
Two goldens moved, both convergence residuals rather than exact values:
`iterate_to_convergence` now runs to 30 iterations instead of 20, landing
nearer the symmetric truth of 25.0.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DnsaJg74eNSva3PJjK2eej
This commit is contained in:
+27
-8
@@ -14,7 +14,6 @@ use crate::{
|
||||
rating::Rating,
|
||||
storage::{CompetitorStore, SkillStore},
|
||||
time::Time,
|
||||
tuple_gt, tuple_max,
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -504,18 +503,29 @@ impl<T: Time> TimeSlice<T> {
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
/// Iterate this slice alone until its posteriors stop moving, returning
|
||||
/// the number of iterations taken.
|
||||
///
|
||||
/// Only used by tests: production convergence is driven across slices by
|
||||
/// `History::converge`.
|
||||
///
|
||||
/// Honours `self.convergence`; it previously hard-coded an epsilon and a
|
||||
/// 20-iteration cap that matched neither `ConvergenceOptions` nor the
|
||||
/// schedule default.
|
||||
#[cfg(test)]
|
||||
pub(crate) fn iterate_to_convergence<D: Drift<T>>(
|
||||
&mut self,
|
||||
agents: &CompetitorStore<T, D>,
|
||||
) -> usize {
|
||||
let epsilon = 1e-6;
|
||||
let iterations = 20;
|
||||
use crate::{tuple_gt, tuple_max};
|
||||
|
||||
let epsilon = self.convergence.epsilon;
|
||||
let max_iter = self.convergence.max_iter;
|
||||
|
||||
let mut step = (f64::INFINITY, f64::INFINITY);
|
||||
let mut i = 0;
|
||||
|
||||
while tuple_gt(step, epsilon) && i < iterations {
|
||||
while tuple_gt(step, epsilon) && i < max_iter {
|
||||
let old = self.posteriors();
|
||||
|
||||
self.iteration(0, agents);
|
||||
@@ -527,6 +537,10 @@ impl<T: Time> TimeSlice<T> {
|
||||
});
|
||||
|
||||
i += 1;
|
||||
|
||||
if !crate::step_is_finite(step) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
i
|
||||
@@ -918,19 +932,24 @@ mod tests {
|
||||
|
||||
let post = time_slice.posteriors();
|
||||
|
||||
// These are convergence residuals, not exact values: by symmetry the
|
||||
// true mean is 25.0 and the iteration approaches it from above. The
|
||||
// previous expectation of 25.000003 was the residual after the
|
||||
// hard-coded 20-iteration cap; honouring `ConvergenceOptions` runs to
|
||||
// 30 and lands nearer the truth.
|
||||
assert_ulps_eq!(
|
||||
post[&a],
|
||||
Gaussian::from_ms(25.000003, 3.880150),
|
||||
Gaussian::from_ms(25.000001, 3.880150),
|
||||
epsilon = 1e-6
|
||||
);
|
||||
assert_ulps_eq!(
|
||||
post[&b],
|
||||
Gaussian::from_ms(25.000003, 3.880150),
|
||||
Gaussian::from_ms(25.000001, 3.880150),
|
||||
epsilon = 1e-6
|
||||
);
|
||||
assert_ulps_eq!(
|
||||
post[&c],
|
||||
Gaussian::from_ms(25.000003, 3.880150),
|
||||
Gaussian::from_ms(25.000001, 3.880150),
|
||||
epsilon = 1e-6
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user