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:
@@ -69,7 +69,20 @@ impl<T: Time, D: Drift<T>, O: Observer<T>, K: Eq + Hash + Clone> HistoryBuilder<
|
||||
}
|
||||
}
|
||||
|
||||
/// Probability that two evenly-matched sides draw.
|
||||
///
|
||||
/// Must be in `[0.0, 1.0)`. A zero draw probability asserts that draws
|
||||
/// cannot occur, so ingesting a tied outcome then fails with
|
||||
/// `InferenceError::TieWithoutDrawProbability`.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// Panics if `p_draw` is outside `[0.0, 1.0)` or is NaN.
|
||||
pub fn p_draw(mut self, p_draw: f64) -> Self {
|
||||
assert!(
|
||||
(0.0..1.0).contains(&p_draw),
|
||||
"p_draw must be in [0.0, 1.0) (got {p_draw})"
|
||||
);
|
||||
self.p_draw = p_draw;
|
||||
self
|
||||
}
|
||||
@@ -79,6 +92,11 @@ impl<T: Time, D: Drift<T>, O: Observer<T>, K: Eq + Hash + Clone> HistoryBuilder<
|
||||
self
|
||||
}
|
||||
|
||||
/// Default observation noise for scored outcomes.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// Panics if `score_sigma` is not strictly positive.
|
||||
pub fn score_sigma(mut self, score_sigma: f64) -> Self {
|
||||
assert!(
|
||||
score_sigma > 0.0,
|
||||
@@ -88,7 +106,24 @@ impl<T: Time, D: Drift<T>, O: Observer<T>, K: Eq + Hash + Clone> HistoryBuilder<
|
||||
self
|
||||
}
|
||||
|
||||
/// Convergence tolerance, iteration cap, and EP damping.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// Panics if `alpha` is outside `(0.0, 1.0]`, or if `epsilon` is negative
|
||||
/// or NaN. An `alpha` of zero would leave every EP update unapplied, so
|
||||
/// inference would silently return the priors.
|
||||
pub fn convergence(mut self, opts: ConvergenceOptions) -> Self {
|
||||
assert!(
|
||||
opts.alpha > 0.0 && opts.alpha <= 1.0,
|
||||
"convergence alpha must be in (0.0, 1.0] (got {})",
|
||||
opts.alpha
|
||||
);
|
||||
assert!(
|
||||
opts.epsilon >= 0.0,
|
||||
"convergence epsilon must be non-negative (got {})",
|
||||
opts.epsilon
|
||||
);
|
||||
self.convergence = opts;
|
||||
self
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user