A tie with `p_draw == 0.0` produced NaN posteriors in release builds and `converge()` reported `converged: true`, because every comparison against NaN is false and `tuple_gt` therefore read NaN as "below epsilon". Two independent defects, fixed together: - Ingestion now rejects tied outcomes when the draw probability is zero, promoting the existing `debug_assert!` in `Game::ranked_with_arena` to a real `InferenceError::TieWithoutDrawProbability`. Validation sits in `add_events_with_prior`, the chokepoint every route reaches — including `record_draw`, which bypasses `Outcome` entirely. - `converge()` treats a non-finite step as failure and returns `InferenceError::NonFiniteResult` rather than claiming convergence. Also in this change: - `History::converge()` on an empty history returned a `usize` underflow panic from `0..len()-1`; it now short-circuits to a zero-iteration report. - `Outcome::scores_with_sigma` no longer panics on a non-positive sigma; the value is validated at ingestion so callers get an error instead. - `InferenceError` gains `WrongOutcomeKind`, replacing the misuse of `MismatchedShape` for variant mismatches (which rendered as the nonsense "expected length 0, got 0"), and is now `#[non_exhaustive]`. Note `Outcome::winner(w, n)` for n >= 3 ties every loser, so those events now require a positive `p_draw`. They previously returned NaN. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DnsaJg74eNSva3PJjK2eej
98 lines
3.3 KiB
Rust
98 lines
3.3 KiB
Rust
use std::fmt;
|
|
|
|
#[derive(Debug, Clone, PartialEq)]
|
|
#[non_exhaustive]
|
|
pub enum InferenceError {
|
|
/// Expected and actual lengths of some array-shaped input differ.
|
|
MismatchedShape {
|
|
kind: &'static str,
|
|
expected: usize,
|
|
got: usize,
|
|
},
|
|
/// An `Outcome` of the wrong variant was supplied for the requested inference.
|
|
WrongOutcomeKind {
|
|
context: &'static str,
|
|
expected: &'static str,
|
|
got: &'static str,
|
|
},
|
|
/// A probability value is outside `[0, 1]`.
|
|
InvalidProbability { value: f64 },
|
|
/// A scalar parameter is outside its valid range.
|
|
InvalidParameter { name: &'static str, value: f64 },
|
|
/// An event contains tied teams, but the draw probability is zero.
|
|
///
|
|
/// A zero draw probability asserts that draws cannot occur, so a tied
|
|
/// result has no representable likelihood. Configure a positive `p_draw`
|
|
/// (via `HistoryBuilder::p_draw` or `GameOptions::p_draw`) to admit ties.
|
|
TieWithoutDrawProbability { teams: (usize, usize) },
|
|
/// Convergence exceeded `max_iter` without falling below `epsilon`.
|
|
ConvergenceFailed {
|
|
last_step: (f64, f64),
|
|
iterations: usize,
|
|
},
|
|
/// Inference produced a non-finite value (NaN or infinity).
|
|
///
|
|
/// Indicates numerical breakdown; the resulting skills are meaningless
|
|
/// and must not be treated as a converged estimate.
|
|
NonFiniteResult {
|
|
context: &'static str,
|
|
step: (f64, f64),
|
|
},
|
|
/// Negative precision: a Gaussian with `pi < 0` slipped into an API call.
|
|
NegativePrecision { pi: f64 },
|
|
}
|
|
|
|
impl fmt::Display for InferenceError {
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
match self {
|
|
Self::MismatchedShape {
|
|
kind,
|
|
expected,
|
|
got,
|
|
} => {
|
|
write!(f, "{kind}: expected length {expected}, got {got}")
|
|
}
|
|
Self::WrongOutcomeKind {
|
|
context,
|
|
expected,
|
|
got,
|
|
} => {
|
|
write!(f, "{context}: expected {expected}, got {got}")
|
|
}
|
|
Self::InvalidProbability { value } => {
|
|
write!(f, "probability must be in [0, 1]; got {value}")
|
|
}
|
|
Self::TieWithoutDrawProbability { teams } => {
|
|
write!(
|
|
f,
|
|
"teams {} and {} are tied, but p_draw is 0.0; set a positive draw probability to admit ties",
|
|
teams.0, teams.1
|
|
)
|
|
}
|
|
Self::NonFiniteResult { context, step } => {
|
|
write!(
|
|
f,
|
|
"{context}: inference produced a non-finite result (step = {step:?})"
|
|
)
|
|
}
|
|
Self::InvalidParameter { name, value } => {
|
|
write!(f, "{name} is invalid: {value}")
|
|
}
|
|
Self::ConvergenceFailed {
|
|
last_step,
|
|
iterations,
|
|
} => {
|
|
write!(
|
|
f,
|
|
"convergence failed after {iterations} iterations; last step = {last_step:?}"
|
|
)
|
|
}
|
|
Self::NegativePrecision { pi } => {
|
|
write!(f, "precision must be non-negative; got {pi}")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
impl std::error::Error for InferenceError {}
|