diff --git a/src/error.rs b/src/error.rs index 8f24478..6f4de66 100644 --- a/src/error.rs +++ b/src/error.rs @@ -43,26 +43,31 @@ pub enum UnknownKeys { #[non_exhaustive] pub enum InferenceError { /// Expected and actual lengths of some array-shaped input differ. + #[non_exhaustive] MismatchedShape { kind: &'static str, expected: usize, got: usize, }, /// An `Outcome` of the wrong variant was supplied for the requested inference. + #[non_exhaustive] WrongOutcomeKind { context: &'static str, expected: &'static str, got: &'static str, }, /// A probability value is outside `[0, 1]`. + #[non_exhaustive] InvalidProbability { value: f64 }, /// A scalar parameter is outside its valid range. + #[non_exhaustive] 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. + #[non_exhaustive] TieWithoutDrawProbability { teams: (usize, usize) }, /// The convergence sweep hit `max_iter` with the step still above /// `epsilon`. @@ -77,6 +82,7 @@ pub enum InferenceError { /// oscillating rather than converging, in which case `alpha < 1.0` damps /// the within-game EP loop. [`History::converge_partial`](crate::History::converge_partial) /// returns the short fit instead when that is genuinely what is wanted. + #[non_exhaustive] NotConverged { iterations: usize, final_step: (f64, f64), @@ -86,6 +92,7 @@ pub enum InferenceError { /// /// Indicates numerical breakdown; the resulting skills are meaningless /// and must not be treated as a converged estimate. + #[non_exhaustive] NonFiniteResult { context: &'static str, step: (f64, f64), @@ -99,6 +106,7 @@ pub enum InferenceError { /// "last one wins" would make the result depend on iteration order. /// Declaring the same value repeatedly is fine and is the expected shape /// when a competitor's configuration is a property of the domain. + #[non_exhaustive] ConflictingCompetitorConfig { competitor: usize, field: &'static str, @@ -113,6 +121,7 @@ pub enum InferenceError { /// `UnknownKey { team: 0, member: 0 }` learns nothing about *which* of its /// keys the history has not seen, and the natural handling — fall back to a /// neutral value — turns the whole thing into a plausible constant. + #[non_exhaustive] UnknownKey { team: usize, member: usize, @@ -128,8 +137,10 @@ pub enum InferenceError { /// /// To change an existing competitor's configuration, supply it on an event /// through `Member`; that refits the whole history. + #[non_exhaustive] AlreadyRegistered { key: String }, /// A prediction was given a team with no members. + #[non_exhaustive] EmptyTeam { team: usize }, /// The prediction grid cannot resolve the narrowest feature in the matchup. /// @@ -147,6 +158,7 @@ pub enum InferenceError { /// `predict_win_probabilities` answers the same matchup through adaptive /// quadrature and is accurate here; use it when only the per-team win /// probabilities are needed. + #[non_exhaustive] GridTooCoarse { /// Nodes required to resolve the narrowest feature. needed: usize, @@ -154,8 +166,10 @@ pub enum InferenceError { max: usize, }, /// A joint posterior was requested where one cannot be formed exactly. + #[non_exhaustive] JointUnavailable { reason: &'static str }, /// Fewer than two teams were supplied to a prediction. + #[non_exhaustive] NotEnoughTeams { got: usize }, /// The full outcome distribution was requested for too many teams. /// @@ -165,6 +179,7 @@ pub enum InferenceError { /// enumerate on a caller's behalf; ask for individual rankings with /// `predict_ranking`, or for `predict_win_probabilities`, both of which /// stay cheap at any team count. + #[non_exhaustive] TooManyTeams { got: usize, max: usize }, } diff --git a/src/history.rs b/src/history.rs index ebc3a63..1040842 100644 --- a/src/history.rs +++ b/src/history.rs @@ -963,7 +963,7 @@ impl, O: Observer, K: Eq + Hash + Clone> History skill, None => match self.unknown_keys { crate::UnknownKeys::Prior => Gaussian::from_ms(self.mu, self.sigma), - _ => { + crate::UnknownKeys::Reject => { return Err(InferenceError::UnknownKey { team: team_idx, member: member_idx, @@ -1220,7 +1220,7 @@ impl, O: Observer, K: Eq + Hash + Clone> History { + crate::UnknownKeys::Reject => { return Err(InferenceError::UnknownKey { team: 0, member, @@ -1482,7 +1482,8 @@ impl, O: Observer, K: Eq + Hash + Clone> History Result where K: std::fmt::Debug, @@ -1538,8 +1539,6 @@ impl, O: Observer, K: Eq + Hash + Clone> History, O: Observer, K: Eq + Hash + Clone> History Result where K: std::fmt::Debug, @@ -1652,6 +1655,8 @@ impl, O: Observer, K: Eq + Hash + Clone> History Result where K: std::fmt::Debug, @@ -1680,8 +1685,6 @@ impl, O: Observer, K: Eq + Hash + Clone> History, O: Observer, K: Eq + Hash + Clone> History Result where K: std::fmt::Debug, @@ -1739,6 +1745,10 @@ impl, O: Observer, K: Eq + Hash + Clone> History Result { let report = self.converge_partial()?; @@ -2289,10 +2299,18 @@ impl, O: Observer, K: Eq + Hash + Clone> History= 3`, which ties every loser. @@ -2420,6 +2438,33 @@ impl, O: Observer, K: Eq + Hash + Clone> History, O: Observer, K: Eq + Hash + Clone> std::fmt::Debug + for History +{ + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("History") + .field("competitors", &self.keys.len()) + .field("events", &self.size) + .field("time_slices", &self.time_slices.len()) + .field("mu", &self.mu) + .field("sigma", &self.sigma) + .field("beta", &self.beta) + .field("p_draw", &self.p_draw) + .field("score_sigma", &self.score_sigma) + .field("unknown_keys", &self.unknown_keys) + .finish_non_exhaustive() + } +} + /// A factorised joint posterior, reusable across many queries. /// /// Built by [`History::joint`]. Every question the joint answers — the width of diff --git a/src/outcome.rs b/src/outcome.rs index 526f595..0546b9b 100644 --- a/src/outcome.rs +++ b/src/outcome.rs @@ -18,6 +18,7 @@ use smallvec::SmallVec; #[non_exhaustive] pub enum Outcome { Ranked(SmallVec<[u32; 4]>), + #[non_exhaustive] Scored { scores: SmallVec<[f64; 4]>, /// Per-event noise override. `None` means inherit diff --git a/tests/convergence_strictness.rs b/tests/convergence_strictness.rs index 03ab150..0910cf9 100644 --- a/tests/convergence_strictness.rs +++ b/tests/convergence_strictness.rs @@ -54,6 +54,7 @@ fn hitting_the_cap_is_an_error() { iterations, final_step, epsilon, + .. } => { assert_eq!(iterations, 1); assert!( diff --git a/tests/degenerate_inputs.rs b/tests/degenerate_inputs.rs index e3e4ef7..0510630 100644 --- a/tests/degenerate_inputs.rs +++ b/tests/degenerate_inputs.rs @@ -160,6 +160,7 @@ fn event_builder_rejects_a_weights_length_mismatch() { kind: "weights", expected: 1, got: 2, + .. } ), "expected a weights MismatchedShape, got {err:?}" diff --git a/tests/drift_scale.rs b/tests/drift_scale.rs index 310a687..934fcb4 100644 --- a/tests/drift_scale.rs +++ b/tests/drift_scale.rs @@ -277,13 +277,11 @@ fn reject(scale: f64) -> InferenceError { #[test] fn negative_scale_is_rejected() { - assert_eq!( + assert!(matches!( reject(-1.0), - InferenceError::InvalidParameter { - name: "drift_scale", - value: -1.0 - } - ); + InferenceError::InvalidParameter { name: "drift_scale", value, .. } + if value == -1.0 + )); } #[test] diff --git a/tests/event_builder_members.rs b/tests/event_builder_members.rs index f182ab0..cd098eb 100644 --- a/tests/event_builder_members.rs +++ b/tests/event_builder_members.rs @@ -135,7 +135,8 @@ fn weights_still_guards_a_members_team() { InferenceError::MismatchedShape { kind: "weights", expected: 2, - got: 1 + got: 1, + .. } ), "{err:?}" diff --git a/tests/game.rs b/tests/game.rs index 5330523..6681990 100644 --- a/tests/game.rs +++ b/tests/game.rs @@ -155,7 +155,7 @@ mod malformed_games { let err = Game::::ranked(&[&[a]], Outcome::winner(0, 1), &GameOptions::default()) .unwrap_err(); assert!( - matches!(err, InferenceError::NotEnoughTeams { got: 1 }), + matches!(err, InferenceError::NotEnoughTeams { got: 1, .. }), "{err:?}" ); } @@ -173,7 +173,7 @@ mod malformed_games { ) .unwrap_err(); assert!( - matches!(err, InferenceError::NotEnoughTeams { got: 1 }), + matches!(err, InferenceError::NotEnoughTeams { got: 1, .. }), "{err:?}" ); } @@ -184,7 +184,7 @@ mod malformed_games { Game::::ranked(&[], Outcome::ranking([]), &GameOptions::default()) .unwrap_err(); assert!( - matches!(err, InferenceError::NotEnoughTeams { got: 0 }), + matches!(err, InferenceError::NotEnoughTeams { got: 0, .. }), "{err:?}" ); } @@ -198,7 +198,7 @@ mod malformed_games { Game::::ranked(&[&[], &[a]], Outcome::winner(0, 2), &GameOptions::default()) .unwrap_err(); assert!( - matches!(err, InferenceError::EmptyTeam { team: 0 }), + matches!(err, InferenceError::EmptyTeam { team: 0, .. }), "{err:?}" ); } diff --git a/tests/ingestion_shape.rs b/tests/ingestion_shape.rs index cd43ab4..5c2e43f 100644 --- a/tests/ingestion_shape.rs +++ b/tests/ingestion_shape.rs @@ -40,7 +40,7 @@ fn a_one_team_event_is_an_error_not_a_panic() { }]) .unwrap_err(); assert!( - matches!(err, InferenceError::NotEnoughTeams { got: 1 }), + matches!(err, InferenceError::NotEnoughTeams { got: 1, .. }), "{err:?}" ); } @@ -56,7 +56,7 @@ fn a_zero_team_event_is_an_error() { }]) .unwrap_err(); assert!( - matches!(err, InferenceError::NotEnoughTeams { got: 0 }), + matches!(err, InferenceError::NotEnoughTeams { got: 0, .. }), "{err:?}" ); } @@ -75,7 +75,7 @@ fn an_empty_team_is_an_error_rather_than_a_free_win() { }]) .unwrap_err(); assert!( - matches!(err, InferenceError::EmptyTeam { team: 0 }), + matches!(err, InferenceError::EmptyTeam { team: 0, .. }), "{err:?}" ); // Nothing was recorded, so the history is still empty. @@ -93,7 +93,7 @@ fn an_empty_team_is_reported_by_position() { }]) .unwrap_err(); assert!( - matches!(err, InferenceError::EmptyTeam { team: 1 }), + matches!(err, InferenceError::EmptyTeam { team: 1, .. }), "{err:?}" ); } @@ -170,7 +170,7 @@ fn the_event_builder_inherits_the_shape_checks() { let mut h = history(); let err = h.event(1).team(["a"]).winner(0).commit().unwrap_err(); assert!( - matches!(err, InferenceError::NotEnoughTeams { got: 1 }), + matches!(err, InferenceError::NotEnoughTeams { got: 1, .. }), "{err:?}" ); } diff --git a/tests/non_finite_results.rs b/tests/non_finite_results.rs index 26f7424..978b7b6 100644 --- a/tests/non_finite_results.rs +++ b/tests/non_finite_results.rs @@ -56,7 +56,7 @@ fn overflow_during_inference_is_reported_not_hidden() { for (name, sigma, beta, score_sigma, scores) in cases { match scored_fit(sigma, beta, score_sigma, scores) { - Err(InferenceError::NonFiniteResult { context, step }) => { + Err(InferenceError::NonFiniteResult { context, step, .. }) => { assert_eq!(context, "History::converge", "{name}"); assert!( !step.0.is_finite() || !step.1.is_finite(), diff --git a/tests/predict_margin.rs b/tests/predict_margin.rs index 95b70c5..c85866a 100644 --- a/tests/predict_margin.rs +++ b/tests/predict_margin.rs @@ -148,6 +148,6 @@ fn shape_errors_are_reported() { let empty: [&&str; 0] = []; assert!(matches!( h.predict_margin(&[&[&"veteran"], &empty]), - Err(InferenceError::EmptyTeam { team: 1 }) + Err(InferenceError::EmptyTeam { team: 1, .. }) )); } diff --git a/tests/prediction.rs b/tests/prediction.rs index d3595ea..5a0f792 100644 --- a/tests/prediction.rs +++ b/tests/prediction.rs @@ -20,13 +20,13 @@ fn unknown_keys_are_reported_not_silently_dropped() { let err = h .predict_outcome(&[&[&"a"], &[&"ghost"]]) .expect_err("an unknown key must not yield a confident prediction"); - assert_eq!( - err, - InferenceError::UnknownKey { - team: 1, - member: 0, - key: "\"ghost\"".to_owned(), - } + assert!( + matches!( + &err, + InferenceError::UnknownKey { team: 1, member: 0, key, .. } + if key == "\"ghost\"" + ), + "{err:?}" ); // Every prediction entry point, not just one. @@ -42,13 +42,13 @@ fn unknown_keys_are_reported_not_silently_dropped() { fn an_entirely_unknown_team_is_an_error() { let h = history_with(&["a", "b"], 0.0); let err = h.predict_outcome(&[&[&"a"], &[&"x", &"y"]]).unwrap_err(); - assert_eq!( - err, - InferenceError::UnknownKey { - team: 1, - member: 0, - key: "\"x\"".to_owned(), - } + assert!( + matches!( + &err, + InferenceError::UnknownKey { team: 1, member: 0, key, .. } + if key == "\"x\"" + ), + "{err:?}" ); } @@ -56,18 +56,18 @@ fn an_entirely_unknown_team_is_an_error() { fn degenerate_team_shapes_are_errors_rather_than_panics() { let h = history_with(&["a", "b"], 0.0); - assert_eq!( + assert!(matches!( h.predict_outcome(&[&[&"a"]]).unwrap_err(), - InferenceError::NotEnoughTeams { got: 1 } - ); - assert_eq!( + InferenceError::NotEnoughTeams { got: 1, .. } + ),); + assert!(matches!( h.predict_outcome(&[]).unwrap_err(), - InferenceError::NotEnoughTeams { got: 0 } - ); - assert_eq!( + InferenceError::NotEnoughTeams { got: 0, .. } + ),); + assert!(matches!( h.predict_outcome(&[&[&"a"], &[]]).unwrap_err(), - InferenceError::EmptyTeam { team: 1 } - ); + InferenceError::EmptyTeam { team: 1, .. } + )); } #[test] @@ -93,13 +93,10 @@ fn the_outcome_space_is_capped_rather_than_hanging() { let refs: Vec<&[&&str]> = too_many.iter().map(Vec::as_slice).collect(); let err = h.predict_outcome(&refs).unwrap_err(); - assert_eq!( + assert!(matches!( err, - InferenceError::TooManyTeams { - got: 8, - max: MAX_PREDICTED_TEAMS - } - ); + InferenceError::TooManyTeams { got: 8, max, .. } if max == MAX_PREDICTED_TEAMS + )); // The cheap paths stay available at any size. let wins = h.predict_win_probabilities(&refs).unwrap(); @@ -282,15 +279,12 @@ fn information_gain_respects_the_entropy_ceiling() { #[test] fn information_gain_reports_unknown_keys() { let h = history_with(&["a", "b"], 0.0); - assert_eq!( - h.expected_information_gain(&[&[&"a"], &[&"ghost"]]) + assert!(matches!( + &h.expected_information_gain(&[&[&"a"], &[&"ghost"]]) .unwrap_err(), - InferenceError::UnknownKey { - team: 1, - member: 0, - key: "\"ghost\"".to_owned(), - } - ); + InferenceError::UnknownKey { team: 1, member: 0, key, .. } + if key == "\"ghost\"" + )); } /// A draw-enabled history has three outcomes to weigh rather than two, so the diff --git a/tests/prediction_bounds.rs b/tests/prediction_bounds.rs index 0b9267e..307f8c6 100644 --- a/tests/prediction_bounds.rs +++ b/tests/prediction_bounds.rs @@ -154,7 +154,7 @@ fn the_known_ceiling_violation_no_longer_answers_wrongly() { gain <= 2.0_f64.ln() + 1e-9, "returned {gain}, over the ln 2 ceiling" ), - Err(InferenceError::GridTooCoarse { needed, max }) => { + Err(InferenceError::GridTooCoarse { needed, max, .. }) => { assert!(needed > max, "needed {needed} should exceed max {max}"); } Err(e) => panic!("unexpected error {e:?}"),