fix!: correct eight wrong # Errors sections and seal the error variants
Documentation (#78). Every item below was measured against the code rather than read: - `expected_information_gain` and `predict_ranking` had `# Errors` immediately followed by `# Preconditions`, with the error list stranded at the bottom of the latter — rustdoc rendered a BLANK Errors section on both. The heading now sits with its content. - `predict_outcome`, `predict_ranking` and the free `expected_information_gain` all omitted `GridTooCoarse`. - `predict_margin` claimed `JointUnavailable` "if the LATEST slice holds ranked events". Measured with an early ranked slice and a late scored one: it fails. The condition is *any* slice. - `add_events` documented three errors and can return five more; it also claimed a weights `MismatchedShape` that is unreachable through it, since weights arrive one-per-`Member`. That check belongs to `EventBuilder::weights`, and the doc now says so. - `converge` and `converge_partial` both omitted the drift-variance `InvalidParameter`. `History` gains a hand-written `Debug` (#76). Summarising, not exhaustive — a derived one would print every competitor's skill at every slice. It exists because without it a consumer cannot `#[derive(Debug)]` on any struct holding a `History`, which is how both known consumers store one. `#[non_exhaustive]` on all 17 `InferenceError` struct variants and on `Outcome::Scored` (#74). The enum carried the attribute; no variant did, so adding a field to any of them — and downstream construction of any of them — were both in the public contract. This crate added two variants in two days. The options structs are deliberately NOT sealed. `ConvergenceOptions` and `GameOptions` are constructed by struct literal at 65 sites of which only 8 use `..default()`, and specifying all three convergence fields is a natural complete statement rather than a partial one. That is a real trade-off rather than an oversight, and it is left as a decision on #74. Also spells `UnknownKeys::Reject` explicitly at both sites that wildcarded it. `#[non_exhaustive]` on your own enum gives no exhaustiveness safety net if you then match `_`. Sealing the variants pushed ten test sites from constructing errors to `matches!`, which is the better assertion anyway — an `assert_eq!` against a constructed error breaks whenever a field is added, which is the exact fragility the attribute exists to prevent. BREAKING CHANGE: `InferenceError`'s struct variants and `Outcome::Scored` are `#[non_exhaustive]` — downstream patterns need `..` and downstream construction is no longer possible. Refs #78, #76, #74 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011hcFjNDmHXZF8URGLku5zZ
This commit is contained in:
@@ -43,26 +43,31 @@ pub enum UnknownKeys {
|
|||||||
#[non_exhaustive]
|
#[non_exhaustive]
|
||||||
pub enum InferenceError {
|
pub enum InferenceError {
|
||||||
/// Expected and actual lengths of some array-shaped input differ.
|
/// Expected and actual lengths of some array-shaped input differ.
|
||||||
|
#[non_exhaustive]
|
||||||
MismatchedShape {
|
MismatchedShape {
|
||||||
kind: &'static str,
|
kind: &'static str,
|
||||||
expected: usize,
|
expected: usize,
|
||||||
got: usize,
|
got: usize,
|
||||||
},
|
},
|
||||||
/// An `Outcome` of the wrong variant was supplied for the requested inference.
|
/// An `Outcome` of the wrong variant was supplied for the requested inference.
|
||||||
|
#[non_exhaustive]
|
||||||
WrongOutcomeKind {
|
WrongOutcomeKind {
|
||||||
context: &'static str,
|
context: &'static str,
|
||||||
expected: &'static str,
|
expected: &'static str,
|
||||||
got: &'static str,
|
got: &'static str,
|
||||||
},
|
},
|
||||||
/// A probability value is outside `[0, 1]`.
|
/// A probability value is outside `[0, 1]`.
|
||||||
|
#[non_exhaustive]
|
||||||
InvalidProbability { value: f64 },
|
InvalidProbability { value: f64 },
|
||||||
/// A scalar parameter is outside its valid range.
|
/// A scalar parameter is outside its valid range.
|
||||||
|
#[non_exhaustive]
|
||||||
InvalidParameter { name: &'static str, value: f64 },
|
InvalidParameter { name: &'static str, value: f64 },
|
||||||
/// An event contains tied teams, but the draw probability is zero.
|
/// An event contains tied teams, but the draw probability is zero.
|
||||||
///
|
///
|
||||||
/// A zero draw probability asserts that draws cannot occur, so a tied
|
/// A zero draw probability asserts that draws cannot occur, so a tied
|
||||||
/// result has no representable likelihood. Configure a positive `p_draw`
|
/// result has no representable likelihood. Configure a positive `p_draw`
|
||||||
/// (via `HistoryBuilder::p_draw` or `GameOptions::p_draw`) to admit ties.
|
/// (via `HistoryBuilder::p_draw` or `GameOptions::p_draw`) to admit ties.
|
||||||
|
#[non_exhaustive]
|
||||||
TieWithoutDrawProbability { teams: (usize, usize) },
|
TieWithoutDrawProbability { teams: (usize, usize) },
|
||||||
/// The convergence sweep hit `max_iter` with the step still above
|
/// The convergence sweep hit `max_iter` with the step still above
|
||||||
/// `epsilon`.
|
/// `epsilon`.
|
||||||
@@ -77,6 +82,7 @@ pub enum InferenceError {
|
|||||||
/// oscillating rather than converging, in which case `alpha < 1.0` damps
|
/// oscillating rather than converging, in which case `alpha < 1.0` damps
|
||||||
/// the within-game EP loop. [`History::converge_partial`](crate::History::converge_partial)
|
/// the within-game EP loop. [`History::converge_partial`](crate::History::converge_partial)
|
||||||
/// returns the short fit instead when that is genuinely what is wanted.
|
/// returns the short fit instead when that is genuinely what is wanted.
|
||||||
|
#[non_exhaustive]
|
||||||
NotConverged {
|
NotConverged {
|
||||||
iterations: usize,
|
iterations: usize,
|
||||||
final_step: (f64, f64),
|
final_step: (f64, f64),
|
||||||
@@ -86,6 +92,7 @@ pub enum InferenceError {
|
|||||||
///
|
///
|
||||||
/// Indicates numerical breakdown; the resulting skills are meaningless
|
/// Indicates numerical breakdown; the resulting skills are meaningless
|
||||||
/// and must not be treated as a converged estimate.
|
/// and must not be treated as a converged estimate.
|
||||||
|
#[non_exhaustive]
|
||||||
NonFiniteResult {
|
NonFiniteResult {
|
||||||
context: &'static str,
|
context: &'static str,
|
||||||
step: (f64, f64),
|
step: (f64, f64),
|
||||||
@@ -99,6 +106,7 @@ pub enum InferenceError {
|
|||||||
/// "last one wins" would make the result depend on iteration order.
|
/// "last one wins" would make the result depend on iteration order.
|
||||||
/// Declaring the same value repeatedly is fine and is the expected shape
|
/// Declaring the same value repeatedly is fine and is the expected shape
|
||||||
/// when a competitor's configuration is a property of the domain.
|
/// when a competitor's configuration is a property of the domain.
|
||||||
|
#[non_exhaustive]
|
||||||
ConflictingCompetitorConfig {
|
ConflictingCompetitorConfig {
|
||||||
competitor: usize,
|
competitor: usize,
|
||||||
field: &'static str,
|
field: &'static str,
|
||||||
@@ -113,6 +121,7 @@ pub enum InferenceError {
|
|||||||
/// `UnknownKey { team: 0, member: 0 }` learns nothing about *which* of its
|
/// `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
|
/// keys the history has not seen, and the natural handling — fall back to a
|
||||||
/// neutral value — turns the whole thing into a plausible constant.
|
/// neutral value — turns the whole thing into a plausible constant.
|
||||||
|
#[non_exhaustive]
|
||||||
UnknownKey {
|
UnknownKey {
|
||||||
team: usize,
|
team: usize,
|
||||||
member: usize,
|
member: usize,
|
||||||
@@ -128,8 +137,10 @@ pub enum InferenceError {
|
|||||||
///
|
///
|
||||||
/// To change an existing competitor's configuration, supply it on an event
|
/// To change an existing competitor's configuration, supply it on an event
|
||||||
/// through `Member`; that refits the whole history.
|
/// through `Member`; that refits the whole history.
|
||||||
|
#[non_exhaustive]
|
||||||
AlreadyRegistered { key: String },
|
AlreadyRegistered { key: String },
|
||||||
/// A prediction was given a team with no members.
|
/// A prediction was given a team with no members.
|
||||||
|
#[non_exhaustive]
|
||||||
EmptyTeam { team: usize },
|
EmptyTeam { team: usize },
|
||||||
/// The prediction grid cannot resolve the narrowest feature in the matchup.
|
/// 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
|
/// `predict_win_probabilities` answers the same matchup through adaptive
|
||||||
/// quadrature and is accurate here; use it when only the per-team win
|
/// quadrature and is accurate here; use it when only the per-team win
|
||||||
/// probabilities are needed.
|
/// probabilities are needed.
|
||||||
|
#[non_exhaustive]
|
||||||
GridTooCoarse {
|
GridTooCoarse {
|
||||||
/// Nodes required to resolve the narrowest feature.
|
/// Nodes required to resolve the narrowest feature.
|
||||||
needed: usize,
|
needed: usize,
|
||||||
@@ -154,8 +166,10 @@ pub enum InferenceError {
|
|||||||
max: usize,
|
max: usize,
|
||||||
},
|
},
|
||||||
/// A joint posterior was requested where one cannot be formed exactly.
|
/// A joint posterior was requested where one cannot be formed exactly.
|
||||||
|
#[non_exhaustive]
|
||||||
JointUnavailable { reason: &'static str },
|
JointUnavailable { reason: &'static str },
|
||||||
/// Fewer than two teams were supplied to a prediction.
|
/// Fewer than two teams were supplied to a prediction.
|
||||||
|
#[non_exhaustive]
|
||||||
NotEnoughTeams { got: usize },
|
NotEnoughTeams { got: usize },
|
||||||
/// The full outcome distribution was requested for too many teams.
|
/// 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
|
/// enumerate on a caller's behalf; ask for individual rankings with
|
||||||
/// `predict_ranking`, or for `predict_win_probabilities`, both of which
|
/// `predict_ranking`, or for `predict_win_probabilities`, both of which
|
||||||
/// stay cheap at any team count.
|
/// stay cheap at any team count.
|
||||||
|
#[non_exhaustive]
|
||||||
TooManyTeams { got: usize, max: usize },
|
TooManyTeams { got: usize, max: usize },
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+59
-14
@@ -963,7 +963,7 @@ impl<T: Time, D: Drift<T>, O: Observer<T>, K: Eq + Hash + Clone> History<T, D, O
|
|||||||
Some(skill) => skill,
|
Some(skill) => skill,
|
||||||
None => match self.unknown_keys {
|
None => match self.unknown_keys {
|
||||||
crate::UnknownKeys::Prior => Gaussian::from_ms(self.mu, self.sigma),
|
crate::UnknownKeys::Prior => Gaussian::from_ms(self.mu, self.sigma),
|
||||||
_ => {
|
crate::UnknownKeys::Reject => {
|
||||||
return Err(InferenceError::UnknownKey {
|
return Err(InferenceError::UnknownKey {
|
||||||
team: team_idx,
|
team: team_idx,
|
||||||
member: member_idx,
|
member: member_idx,
|
||||||
@@ -1220,7 +1220,7 @@ impl<T: Time, D: Drift<T>, O: Observer<T>, K: Eq + Hash + Clone> History<T, D, O
|
|||||||
mean += coefficient * self.mu;
|
mean += coefficient * self.mu;
|
||||||
*unseen.entry(format!("{key:?}")).or_insert(0.0) += coefficient;
|
*unseen.entry(format!("{key:?}")).or_insert(0.0) += coefficient;
|
||||||
}
|
}
|
||||||
_ => {
|
crate::UnknownKeys::Reject => {
|
||||||
return Err(InferenceError::UnknownKey {
|
return Err(InferenceError::UnknownKey {
|
||||||
team: 0,
|
team: 0,
|
||||||
member,
|
member,
|
||||||
@@ -1482,7 +1482,8 @@ impl<T: Time, D: Drift<T>, O: Observer<T>, K: Eq + Hash + Clone> History<T, D, O
|
|||||||
///
|
///
|
||||||
/// `MismatchedShape` unless exactly two teams are supplied, `EmptyTeam`,
|
/// `MismatchedShape` unless exactly two teams are supplied, `EmptyTeam`,
|
||||||
/// `UnknownKey` under [`UnknownKeys::Reject`](crate::UnknownKeys::Reject),
|
/// `UnknownKey` under [`UnknownKeys::Reject`](crate::UnknownKeys::Reject),
|
||||||
/// and `JointUnavailable` if the latest slice holds ranked events.
|
/// and `JointUnavailable` if the history is empty or holds ranked events in
|
||||||
|
/// *any* slice — not merely the latest one.
|
||||||
pub fn predict_margin(&self, teams: &[&[&K]]) -> Result<Gaussian, InferenceError>
|
pub fn predict_margin(&self, teams: &[&[&K]]) -> Result<Gaussian, InferenceError>
|
||||||
where
|
where
|
||||||
K: std::fmt::Debug,
|
K: std::fmt::Debug,
|
||||||
@@ -1538,8 +1539,6 @@ impl<T: Time, D: Drift<T>, O: Observer<T>, K: Eq + Hash + Clone> History<T, D, O
|
|||||||
/// elsewhere. See [`expected_information_gain`](crate::expected_information_gain)
|
/// elsewhere. See [`expected_information_gain`](crate::expected_information_gain)
|
||||||
/// for the scale, the analytic `ln k` ceiling, and the cost.
|
/// for the scale, the analytic `ln k` ceiling, and the cost.
|
||||||
///
|
///
|
||||||
/// # Errors
|
|
||||||
///
|
|
||||||
/// # Preconditions
|
/// # Preconditions
|
||||||
///
|
///
|
||||||
/// Every key must already be known to the history — that is, must have
|
/// Every key must already be known to the history — that is, must have
|
||||||
@@ -1550,8 +1549,12 @@ impl<T: Time, D: Drift<T>, O: Observer<T>, K: Eq + Hash + Clone> History<T, D, O
|
|||||||
/// whole-team miss into a plausible constant, which is invisible to any
|
/// whole-team miss into a plausible constant, which is invisible to any
|
||||||
/// test that does not assert on variation.
|
/// test that does not assert on variation.
|
||||||
///
|
///
|
||||||
/// As [`History::member_skills`], plus `TooManyTeams` and anything
|
/// # Errors
|
||||||
/// inference returns for a hypothetical outcome.
|
///
|
||||||
|
/// `NotEnoughTeams`, `EmptyTeam`, `UnknownKey` and `TooManyTeams` for the
|
||||||
|
/// shape of the request, `GridTooCoarse` when the performance sigmas are
|
||||||
|
/// too far apart to integrate on one grid, and anything inference returns
|
||||||
|
/// for a hypothetical outcome.
|
||||||
pub fn expected_information_gain(&self, teams: &[&[&K]]) -> Result<f64, InferenceError>
|
pub fn expected_information_gain(&self, teams: &[&[&K]]) -> Result<f64, InferenceError>
|
||||||
where
|
where
|
||||||
K: std::fmt::Debug,
|
K: std::fmt::Debug,
|
||||||
@@ -1652,6 +1655,8 @@ impl<T: Time, D: Drift<T>, O: Observer<T>, K: Eq + Hash + Clone> History<T, D, O
|
|||||||
/// # Errors
|
/// # Errors
|
||||||
///
|
///
|
||||||
/// `NotEnoughTeams`, `EmptyTeam`, `UnknownKey`, or `TooManyTeams`.
|
/// `NotEnoughTeams`, `EmptyTeam`, `UnknownKey`, or `TooManyTeams`.
|
||||||
|
/// `GridTooCoarse` when the performance sigmas are too far apart to
|
||||||
|
/// integrate on one grid.
|
||||||
pub fn predict_outcome(&self, teams: &[&[&K]]) -> Result<Prediction, InferenceError>
|
pub fn predict_outcome(&self, teams: &[&[&K]]) -> Result<Prediction, InferenceError>
|
||||||
where
|
where
|
||||||
K: std::fmt::Debug,
|
K: std::fmt::Debug,
|
||||||
@@ -1680,8 +1685,6 @@ impl<T: Time, D: Drift<T>, O: Observer<T>, K: Eq + Hash + Clone> History<T, D, O
|
|||||||
/// space, so it stays cheap at any team count — use it when you know which
|
/// space, so it stays cheap at any team count — use it when you know which
|
||||||
/// orderings you care about.
|
/// orderings you care about.
|
||||||
///
|
///
|
||||||
/// # Errors
|
|
||||||
///
|
|
||||||
/// # Preconditions
|
/// # Preconditions
|
||||||
///
|
///
|
||||||
/// Every key must already be known to the history — that is, must have
|
/// Every key must already be known to the history — that is, must have
|
||||||
@@ -1692,8 +1695,11 @@ impl<T: Time, D: Drift<T>, O: Observer<T>, K: Eq + Hash + Clone> History<T, D, O
|
|||||||
/// whole-team miss into a plausible constant, which is invisible to any
|
/// whole-team miss into a plausible constant, which is invisible to any
|
||||||
/// test that does not assert on variation.
|
/// test that does not assert on variation.
|
||||||
///
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
/// `NotEnoughTeams`, `EmptyTeam`, `UnknownKey`, or `MismatchedShape` if
|
/// `NotEnoughTeams`, `EmptyTeam`, `UnknownKey`, or `MismatchedShape` if
|
||||||
/// `ranks` does not have one entry per team.
|
/// `ranks` does not have one entry per team. `GridTooCoarse` when the
|
||||||
|
/// performance sigmas are too far apart to integrate on one grid.
|
||||||
pub fn predict_ranking(&self, teams: &[&[&K]], ranks: &[u32]) -> Result<f64, InferenceError>
|
pub fn predict_ranking(&self, teams: &[&[&K]], ranks: &[u32]) -> Result<f64, InferenceError>
|
||||||
where
|
where
|
||||||
K: std::fmt::Debug,
|
K: std::fmt::Debug,
|
||||||
@@ -1739,6 +1745,10 @@ impl<T: Time, D: Drift<T>, O: Observer<T>, K: Eq + Hash + Clone> History<T, D, O
|
|||||||
/// `NonFiniteResult` if a sweep produces a NaN or infinite step. EP has
|
/// `NonFiniteResult` if a sweep produces a NaN or infinite step. EP has
|
||||||
/// broken down at that point and further iterations cannot recover, so the
|
/// broken down at that point and further iterations cannot recover, so the
|
||||||
/// loop stops rather than reporting a NaN step as convergence.
|
/// loop stops rather than reporting a NaN step as convergence.
|
||||||
|
///
|
||||||
|
/// `InvalidParameter` if a competitor's drift model yields a negative or
|
||||||
|
/// non-finite variance — which also covers a custom [`Drift`]
|
||||||
|
/// implementation, the one case no constructor can check.
|
||||||
pub fn converge(&mut self) -> Result<ConvergenceReport, InferenceError> {
|
pub fn converge(&mut self) -> Result<ConvergenceReport, InferenceError> {
|
||||||
let report = self.converge_partial()?;
|
let report = self.converge_partial()?;
|
||||||
|
|
||||||
@@ -2289,10 +2299,18 @@ impl<T: Time, D: Drift<T>, O: Observer<T>, K: Eq + Hash + Clone> History<T, D, O
|
|||||||
/// # Errors
|
/// # Errors
|
||||||
///
|
///
|
||||||
/// - `MismatchedShape` if an event's outcome does not describe the same
|
/// - `MismatchedShape` if an event's outcome does not describe the same
|
||||||
/// number of teams the event has, or if per-member weights do not match
|
/// number of teams the event has. (Weights cannot mismatch here — they
|
||||||
/// the team's membership.
|
/// come one-per-`Member`; that check belongs to
|
||||||
/// - `InvalidParameter` if a per-event `score_sigma` override is not
|
/// [`EventBuilder::weights`](crate::EventBuilder::weights), which builds
|
||||||
/// strictly positive.
|
/// them from a separate list.)
|
||||||
|
/// - `NotEnoughTeams` for an event with fewer than two teams, and
|
||||||
|
/// `EmptyTeam` for a team with no members.
|
||||||
|
/// - `InvalidParameter` for a per-event `score_sigma` override that is not
|
||||||
|
/// strictly positive, a non-finite score, rank or weight, or a
|
||||||
|
/// `drift_scale` that is negative or non-finite.
|
||||||
|
/// - `ConflictingCompetitorConfig` if one competitor is given two different
|
||||||
|
/// values for `prior` or `drift_scale`, whether within one batch or
|
||||||
|
/// across batches.
|
||||||
/// - `TieWithoutDrawProbability` if an event ties two teams while the
|
/// - `TieWithoutDrawProbability` if an event ties two teams while the
|
||||||
/// history's `p_draw` is zero. This includes `Outcome::winner(w, n)` for
|
/// history's `p_draw` is zero. This includes `Outcome::winner(w, n)` for
|
||||||
/// `n >= 3`, which ties every loser.
|
/// `n >= 3`, which ties every loser.
|
||||||
@@ -2420,6 +2438,33 @@ impl<T: Time, D: Drift<T>, O: Observer<T>, K: Eq + Hash + Clone> History<T, D, O
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Summarising rather than exhaustive.
|
||||||
|
///
|
||||||
|
/// A `History` owns every competitor's skill at every time slice, so a derived
|
||||||
|
/// `Debug` would print the entire fit — megabytes for a real history, and
|
||||||
|
/// useless in a log. This prints the shape instead. Same reasoning as `Joint`'s,
|
||||||
|
/// which omits its `n^2` factorisation.
|
||||||
|
///
|
||||||
|
/// It exists at all because without it a consumer cannot `#[derive(Debug)]` on
|
||||||
|
/// any struct holding a `History`, which is how both known consumers store it.
|
||||||
|
impl<T: Time, D: Drift<T>, O: Observer<T>, K: Eq + Hash + Clone> std::fmt::Debug
|
||||||
|
for History<T, D, O, K>
|
||||||
|
{
|
||||||
|
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.
|
/// A factorised joint posterior, reusable across many queries.
|
||||||
///
|
///
|
||||||
/// Built by [`History::joint`]. Every question the joint answers — the width of
|
/// Built by [`History::joint`]. Every question the joint answers — the width of
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ use smallvec::SmallVec;
|
|||||||
#[non_exhaustive]
|
#[non_exhaustive]
|
||||||
pub enum Outcome {
|
pub enum Outcome {
|
||||||
Ranked(SmallVec<[u32; 4]>),
|
Ranked(SmallVec<[u32; 4]>),
|
||||||
|
#[non_exhaustive]
|
||||||
Scored {
|
Scored {
|
||||||
scores: SmallVec<[f64; 4]>,
|
scores: SmallVec<[f64; 4]>,
|
||||||
/// Per-event noise override. `None` means inherit
|
/// Per-event noise override. `None` means inherit
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ fn hitting_the_cap_is_an_error() {
|
|||||||
iterations,
|
iterations,
|
||||||
final_step,
|
final_step,
|
||||||
epsilon,
|
epsilon,
|
||||||
|
..
|
||||||
} => {
|
} => {
|
||||||
assert_eq!(iterations, 1);
|
assert_eq!(iterations, 1);
|
||||||
assert!(
|
assert!(
|
||||||
|
|||||||
@@ -160,6 +160,7 @@ fn event_builder_rejects_a_weights_length_mismatch() {
|
|||||||
kind: "weights",
|
kind: "weights",
|
||||||
expected: 1,
|
expected: 1,
|
||||||
got: 2,
|
got: 2,
|
||||||
|
..
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
"expected a weights MismatchedShape, got {err:?}"
|
"expected a weights MismatchedShape, got {err:?}"
|
||||||
|
|||||||
@@ -277,13 +277,11 @@ fn reject(scale: f64) -> InferenceError {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn negative_scale_is_rejected() {
|
fn negative_scale_is_rejected() {
|
||||||
assert_eq!(
|
assert!(matches!(
|
||||||
reject(-1.0),
|
reject(-1.0),
|
||||||
InferenceError::InvalidParameter {
|
InferenceError::InvalidParameter { name: "drift_scale", value, .. }
|
||||||
name: "drift_scale",
|
if value == -1.0
|
||||||
value: -1.0
|
));
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -135,7 +135,8 @@ fn weights_still_guards_a_members_team() {
|
|||||||
InferenceError::MismatchedShape {
|
InferenceError::MismatchedShape {
|
||||||
kind: "weights",
|
kind: "weights",
|
||||||
expected: 2,
|
expected: 2,
|
||||||
got: 1
|
got: 1,
|
||||||
|
..
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
"{err:?}"
|
"{err:?}"
|
||||||
|
|||||||
+4
-4
@@ -155,7 +155,7 @@ mod malformed_games {
|
|||||||
let err = Game::<i64, _>::ranked(&[&[a]], Outcome::winner(0, 1), &GameOptions::default())
|
let err = Game::<i64, _>::ranked(&[&[a]], Outcome::winner(0, 1), &GameOptions::default())
|
||||||
.unwrap_err();
|
.unwrap_err();
|
||||||
assert!(
|
assert!(
|
||||||
matches!(err, InferenceError::NotEnoughTeams { got: 1 }),
|
matches!(err, InferenceError::NotEnoughTeams { got: 1, .. }),
|
||||||
"{err:?}"
|
"{err:?}"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -173,7 +173,7 @@ mod malformed_games {
|
|||||||
)
|
)
|
||||||
.unwrap_err();
|
.unwrap_err();
|
||||||
assert!(
|
assert!(
|
||||||
matches!(err, InferenceError::NotEnoughTeams { got: 1 }),
|
matches!(err, InferenceError::NotEnoughTeams { got: 1, .. }),
|
||||||
"{err:?}"
|
"{err:?}"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -184,7 +184,7 @@ mod malformed_games {
|
|||||||
Game::<i64, ConstantDrift>::ranked(&[], Outcome::ranking([]), &GameOptions::default())
|
Game::<i64, ConstantDrift>::ranked(&[], Outcome::ranking([]), &GameOptions::default())
|
||||||
.unwrap_err();
|
.unwrap_err();
|
||||||
assert!(
|
assert!(
|
||||||
matches!(err, InferenceError::NotEnoughTeams { got: 0 }),
|
matches!(err, InferenceError::NotEnoughTeams { got: 0, .. }),
|
||||||
"{err:?}"
|
"{err:?}"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -198,7 +198,7 @@ mod malformed_games {
|
|||||||
Game::<i64, _>::ranked(&[&[], &[a]], Outcome::winner(0, 2), &GameOptions::default())
|
Game::<i64, _>::ranked(&[&[], &[a]], Outcome::winner(0, 2), &GameOptions::default())
|
||||||
.unwrap_err();
|
.unwrap_err();
|
||||||
assert!(
|
assert!(
|
||||||
matches!(err, InferenceError::EmptyTeam { team: 0 }),
|
matches!(err, InferenceError::EmptyTeam { team: 0, .. }),
|
||||||
"{err:?}"
|
"{err:?}"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ fn a_one_team_event_is_an_error_not_a_panic() {
|
|||||||
}])
|
}])
|
||||||
.unwrap_err();
|
.unwrap_err();
|
||||||
assert!(
|
assert!(
|
||||||
matches!(err, InferenceError::NotEnoughTeams { got: 1 }),
|
matches!(err, InferenceError::NotEnoughTeams { got: 1, .. }),
|
||||||
"{err:?}"
|
"{err:?}"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -56,7 +56,7 @@ fn a_zero_team_event_is_an_error() {
|
|||||||
}])
|
}])
|
||||||
.unwrap_err();
|
.unwrap_err();
|
||||||
assert!(
|
assert!(
|
||||||
matches!(err, InferenceError::NotEnoughTeams { got: 0 }),
|
matches!(err, InferenceError::NotEnoughTeams { got: 0, .. }),
|
||||||
"{err:?}"
|
"{err:?}"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -75,7 +75,7 @@ fn an_empty_team_is_an_error_rather_than_a_free_win() {
|
|||||||
}])
|
}])
|
||||||
.unwrap_err();
|
.unwrap_err();
|
||||||
assert!(
|
assert!(
|
||||||
matches!(err, InferenceError::EmptyTeam { team: 0 }),
|
matches!(err, InferenceError::EmptyTeam { team: 0, .. }),
|
||||||
"{err:?}"
|
"{err:?}"
|
||||||
);
|
);
|
||||||
// Nothing was recorded, so the history is still empty.
|
// Nothing was recorded, so the history is still empty.
|
||||||
@@ -93,7 +93,7 @@ fn an_empty_team_is_reported_by_position() {
|
|||||||
}])
|
}])
|
||||||
.unwrap_err();
|
.unwrap_err();
|
||||||
assert!(
|
assert!(
|
||||||
matches!(err, InferenceError::EmptyTeam { team: 1 }),
|
matches!(err, InferenceError::EmptyTeam { team: 1, .. }),
|
||||||
"{err:?}"
|
"{err:?}"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -170,7 +170,7 @@ fn the_event_builder_inherits_the_shape_checks() {
|
|||||||
let mut h = history();
|
let mut h = history();
|
||||||
let err = h.event(1).team(["a"]).winner(0).commit().unwrap_err();
|
let err = h.event(1).team(["a"]).winner(0).commit().unwrap_err();
|
||||||
assert!(
|
assert!(
|
||||||
matches!(err, InferenceError::NotEnoughTeams { got: 1 }),
|
matches!(err, InferenceError::NotEnoughTeams { got: 1, .. }),
|
||||||
"{err:?}"
|
"{err:?}"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ fn overflow_during_inference_is_reported_not_hidden() {
|
|||||||
|
|
||||||
for (name, sigma, beta, score_sigma, scores) in cases {
|
for (name, sigma, beta, score_sigma, scores) in cases {
|
||||||
match scored_fit(sigma, beta, score_sigma, scores) {
|
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_eq!(context, "History::converge", "{name}");
|
||||||
assert!(
|
assert!(
|
||||||
!step.0.is_finite() || !step.1.is_finite(),
|
!step.0.is_finite() || !step.1.is_finite(),
|
||||||
|
|||||||
@@ -148,6 +148,6 @@ fn shape_errors_are_reported() {
|
|||||||
let empty: [&&str; 0] = [];
|
let empty: [&&str; 0] = [];
|
||||||
assert!(matches!(
|
assert!(matches!(
|
||||||
h.predict_margin(&[&[&"veteran"], &empty]),
|
h.predict_margin(&[&[&"veteran"], &empty]),
|
||||||
Err(InferenceError::EmptyTeam { team: 1 })
|
Err(InferenceError::EmptyTeam { team: 1, .. })
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|||||||
+31
-37
@@ -20,13 +20,13 @@ fn unknown_keys_are_reported_not_silently_dropped() {
|
|||||||
let err = h
|
let err = h
|
||||||
.predict_outcome(&[&[&"a"], &[&"ghost"]])
|
.predict_outcome(&[&[&"a"], &[&"ghost"]])
|
||||||
.expect_err("an unknown key must not yield a confident prediction");
|
.expect_err("an unknown key must not yield a confident prediction");
|
||||||
assert_eq!(
|
assert!(
|
||||||
err,
|
matches!(
|
||||||
InferenceError::UnknownKey {
|
&err,
|
||||||
team: 1,
|
InferenceError::UnknownKey { team: 1, member: 0, key, .. }
|
||||||
member: 0,
|
if key == "\"ghost\""
|
||||||
key: "\"ghost\"".to_owned(),
|
),
|
||||||
}
|
"{err:?}"
|
||||||
);
|
);
|
||||||
|
|
||||||
// Every prediction entry point, not just one.
|
// 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() {
|
fn an_entirely_unknown_team_is_an_error() {
|
||||||
let h = history_with(&["a", "b"], 0.0);
|
let h = history_with(&["a", "b"], 0.0);
|
||||||
let err = h.predict_outcome(&[&[&"a"], &[&"x", &"y"]]).unwrap_err();
|
let err = h.predict_outcome(&[&[&"a"], &[&"x", &"y"]]).unwrap_err();
|
||||||
assert_eq!(
|
assert!(
|
||||||
err,
|
matches!(
|
||||||
InferenceError::UnknownKey {
|
&err,
|
||||||
team: 1,
|
InferenceError::UnknownKey { team: 1, member: 0, key, .. }
|
||||||
member: 0,
|
if key == "\"x\""
|
||||||
key: "\"x\"".to_owned(),
|
),
|
||||||
}
|
"{err:?}"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,18 +56,18 @@ fn an_entirely_unknown_team_is_an_error() {
|
|||||||
fn degenerate_team_shapes_are_errors_rather_than_panics() {
|
fn degenerate_team_shapes_are_errors_rather_than_panics() {
|
||||||
let h = history_with(&["a", "b"], 0.0);
|
let h = history_with(&["a", "b"], 0.0);
|
||||||
|
|
||||||
assert_eq!(
|
assert!(matches!(
|
||||||
h.predict_outcome(&[&[&"a"]]).unwrap_err(),
|
h.predict_outcome(&[&[&"a"]]).unwrap_err(),
|
||||||
InferenceError::NotEnoughTeams { got: 1 }
|
InferenceError::NotEnoughTeams { got: 1, .. }
|
||||||
);
|
),);
|
||||||
assert_eq!(
|
assert!(matches!(
|
||||||
h.predict_outcome(&[]).unwrap_err(),
|
h.predict_outcome(&[]).unwrap_err(),
|
||||||
InferenceError::NotEnoughTeams { got: 0 }
|
InferenceError::NotEnoughTeams { got: 0, .. }
|
||||||
);
|
),);
|
||||||
assert_eq!(
|
assert!(matches!(
|
||||||
h.predict_outcome(&[&[&"a"], &[]]).unwrap_err(),
|
h.predict_outcome(&[&[&"a"], &[]]).unwrap_err(),
|
||||||
InferenceError::EmptyTeam { team: 1 }
|
InferenceError::EmptyTeam { team: 1, .. }
|
||||||
);
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[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 refs: Vec<&[&&str]> = too_many.iter().map(Vec::as_slice).collect();
|
||||||
|
|
||||||
let err = h.predict_outcome(&refs).unwrap_err();
|
let err = h.predict_outcome(&refs).unwrap_err();
|
||||||
assert_eq!(
|
assert!(matches!(
|
||||||
err,
|
err,
|
||||||
InferenceError::TooManyTeams {
|
InferenceError::TooManyTeams { got: 8, max, .. } if max == MAX_PREDICTED_TEAMS
|
||||||
got: 8,
|
));
|
||||||
max: MAX_PREDICTED_TEAMS
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// The cheap paths stay available at any size.
|
// The cheap paths stay available at any size.
|
||||||
let wins = h.predict_win_probabilities(&refs).unwrap();
|
let wins = h.predict_win_probabilities(&refs).unwrap();
|
||||||
@@ -282,15 +279,12 @@ fn information_gain_respects_the_entropy_ceiling() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn information_gain_reports_unknown_keys() {
|
fn information_gain_reports_unknown_keys() {
|
||||||
let h = history_with(&["a", "b"], 0.0);
|
let h = history_with(&["a", "b"], 0.0);
|
||||||
assert_eq!(
|
assert!(matches!(
|
||||||
h.expected_information_gain(&[&[&"a"], &[&"ghost"]])
|
&h.expected_information_gain(&[&[&"a"], &[&"ghost"]])
|
||||||
.unwrap_err(),
|
.unwrap_err(),
|
||||||
InferenceError::UnknownKey {
|
InferenceError::UnknownKey { team: 1, member: 0, key, .. }
|
||||||
team: 1,
|
if key == "\"ghost\""
|
||||||
member: 0,
|
));
|
||||||
key: "\"ghost\"".to_owned(),
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A draw-enabled history has three outcomes to weigh rather than two, so the
|
/// A draw-enabled history has three outcomes to weigh rather than two, so the
|
||||||
|
|||||||
@@ -154,7 +154,7 @@ fn the_known_ceiling_violation_no_longer_answers_wrongly() {
|
|||||||
gain <= 2.0_f64.ln() + 1e-9,
|
gain <= 2.0_f64.ln() + 1e-9,
|
||||||
"returned {gain}, over the ln 2 ceiling"
|
"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}");
|
assert!(needed > max, "needed {needed} should exceed max {max}");
|
||||||
}
|
}
|
||||||
Err(e) => panic!("unexpected error {e:?}"),
|
Err(e) => panic!("unexpected error {e:?}"),
|
||||||
|
|||||||
Reference in New Issue
Block a user