refactor!: typed discriminators for InferenceError
Six of fifteen variants carried a `&'static str` discriminator, about
thirty magic strings between them, and the only thing a caller could do
with one was print it. Four new enums replace them:
Parameter 13 variants, replacing 9 strings in InvalidParameter
Shape 4 variants, replacing 10 in MismatchedShape
OutcomeKind 2 variants, replacing WrongOutcomeKind's three fields
CompetitorField 2 variants, replacing ConflictingCompetitorConfig's
`InvalidProbability` folds into `InvalidParameter` as
`Parameter::PDraw`. It was a bespoke variant for one scalar while every
other scalar shared `InvalidParameter`, and it omitted the parameter
name — so the same parameter had two mechanisms.
`JointUnavailable { reason: &'static str }` splits into `EmptyHistory`,
`JointRequiresScoredEvents` and `NotPositiveDefinite`. The three are
conditions a caller branches on differently — add events, use
`predict_win_probabilities`, or reconsider the priors — and telling them
apart used to mean string-matching English. One test already proved the
distinction was load-bearing: the blanket conversion mapped the
empty-history case onto the ranked one and `an_empty_history_has_no_joint`
caught it immediately.
`NonFiniteResult` splits into `NonFiniteStep { context, step }` and
`NonFiniteSkill { mu, sigma }`. One `step: (f64, f64)` field was
carrying a sweep step from `converge` and a skill's own moments from a
prediction — two situations in one variant, and a field name that could
only be right for one of them.
`InvalidParameter { name: "beta with point-mass skills" }` becomes
`NoPerformanceVariance`. It was never a parameter out of range: both
values are individually valid and it is their combination that leaves
nothing varying.
Three `Display` impls did not meet the standard the others set, and the
typed data is what makes fixing them possible:
before drift variance is invalid: NaN
after drift variance must be finite and non-negative (got NaN)
before kinds: expected length 3, got 2
after the outcome describes a different number of teams than the
event has: expected 3, got 2
before Game::ranked: expected Outcome::Ranked, got Outcome::Scored
after expected Outcome::Ranked, got Outcome::Scored; call
Game::scored for a scored outcome
`Parameter::range()` states each parameter's actual bounds, which no
`&'static str` name could have. `error::message_tests` renders every one
and asserts each is a sentence rather than a label, and that the three
above now carry a range or a next step.
The four internal `MismatchedShape` kinds — `results`, `times`, `kinds`,
and the weights array — collapse to `Shape::Internal`, whose `Display`
says plainly that reaching it is a bug in this crate. They are checks on
`add_events_with_prior`'s own parallel arrays and are unreachable
through the public API; they stay checked rather than becoming
`debug_assert!`s, because release is where this crate's defects hide.
Closes #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:
+57
-71
@@ -72,7 +72,7 @@ impl<T: Time, D: Drift<T>, O: Observer<T>, K: Eq + Hash + Clone, R: RatingRule<K
|
||||
/// # Panics
|
||||
///
|
||||
/// Panics if `mu` is not finite. A non-finite prior mean poisons every
|
||||
/// posterior derived from it: `converge` reports `NonFiniteResult`, but a
|
||||
/// posterior derived from it: `converge` reports `NonFiniteStep`, but a
|
||||
/// caller who reads `current_skill` first is handed `tau: NaN`.
|
||||
pub fn mu(mut self, mu: f64) -> Self {
|
||||
assert!(mu.is_finite(), "mu must be finite (got {mu})");
|
||||
@@ -855,14 +855,14 @@ impl<T: Time, D: Drift<T>, O: Observer<T>, K: Eq + Hash + Clone, R: RatingRule<K
|
||||
{
|
||||
if member.weight != 1.0 {
|
||||
return Err(InferenceError::InvalidParameter {
|
||||
name: "weight",
|
||||
parameter: crate::Parameter::Weight,
|
||||
value: member.weight,
|
||||
});
|
||||
}
|
||||
if let Some(scale) = member.drift_scale {
|
||||
if !scale.is_finite() || scale < 0.0 {
|
||||
return Err(InferenceError::InvalidParameter {
|
||||
name: "drift_scale",
|
||||
parameter: crate::Parameter::DriftScale,
|
||||
value: scale,
|
||||
});
|
||||
}
|
||||
@@ -1296,7 +1296,7 @@ impl<T: Time, D: Drift<T>, O: Observer<T>, K: Eq + Hash + Clone, R: RatingRule<K
|
||||
// `converge` refuses to report a NaN fit, but nothing stopped a
|
||||
// caller ignoring that error and predicting anyway. Measured on
|
||||
// a point-mass-prior history with `beta(0.0)`, after `converge`
|
||||
// returned `NonFiniteResult`: `quality` gave `Ok(NaN)`,
|
||||
// returned `NonFiniteStep`: `quality` gave `Ok(NaN)`,
|
||||
// `predict_outcome().total()` gave `NaN`, and
|
||||
// `predict_win_probabilities` gave `Ok([0.0, 0.0])` — finite,
|
||||
// plausible, and summing to zero against a doc that promises
|
||||
@@ -1317,10 +1317,9 @@ impl<T: Time, D: Drift<T>, O: Observer<T>, K: Eq + Hash + Clone, R: RatingRule<K
|
||||
// stored posterior today, and a prediction from an
|
||||
// uninformative skill would be meaningless if it did.
|
||||
if !skill.mu().is_finite() || !skill.sigma().is_finite() {
|
||||
return Err(InferenceError::NonFiniteResult {
|
||||
context: "prediction read a skill with no usable mean or \
|
||||
variance; the fit did not converge",
|
||||
step: (skill.mu(), skill.sigma()),
|
||||
return Err(InferenceError::NonFiniteSkill {
|
||||
mu: skill.mu(),
|
||||
sigma: skill.sigma(),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1349,10 +1348,7 @@ impl<T: Time, D: Drift<T>, O: Observer<T>, K: Eq + Hash + Clone, R: RatingRule<K
|
||||
// time and it is a property of the parameters, not a numerical
|
||||
// accident.
|
||||
if self.beta == 0.0 && gathered.iter().flatten().all(|g| g.sigma() == 0.0) {
|
||||
return Err(InferenceError::InvalidParameter {
|
||||
name: "beta with point-mass skills",
|
||||
value: 0.0,
|
||||
});
|
||||
return Err(InferenceError::NoPerformanceVariance);
|
||||
}
|
||||
|
||||
Ok(gathered)
|
||||
@@ -1440,9 +1436,9 @@ impl<T: Time, D: Drift<T>, O: Observer<T>, K: Eq + Hash + Clone, R: RatingRule<K
|
||||
/// `NotEnoughTeams`, `EmptyTeam` or `UnknownKey`.
|
||||
///
|
||||
/// Every prediction reads skills through one gate, which adds two errors to
|
||||
/// all of them: `NonFiniteResult` if a skill has no usable mean or variance
|
||||
/// — the fit did not converge — and `InvalidParameter` if `beta` is zero
|
||||
/// and every skill is a point mass, leaving no performance distribution to
|
||||
/// all of them: `NonFiniteSkill` if a skill has no usable mean or variance
|
||||
/// — the fit did not converge — and `NoPerformanceVariance` if `beta` is zero and
|
||||
/// every skill is a point mass, leaving no performance distribution to
|
||||
/// predict from.
|
||||
pub fn quality<Q>(&self, teams: &[&[&Q]]) -> Result<f64, InferenceError>
|
||||
where
|
||||
@@ -1481,7 +1477,7 @@ impl<T: Time, D: Drift<T>, O: Observer<T>, K: Eq + Hash + Clone, R: RatingRule<K
|
||||
/// stored `f64` before the factorisation ever runs. Measured, `drift_scale =
|
||||
/// 1e-10` returned a posterior variance **12 000x too small** — a 111x
|
||||
/// overconfident interval — as `Ok`, and the band just above it returned a
|
||||
/// misleading `JointUnavailable`.
|
||||
/// misleading `NotPositiveDefinite`.
|
||||
///
|
||||
/// Solved exactly in high precision the same system is perfectly well
|
||||
/// conditioned: it converges smoothly onto the collapsed value and is flat from
|
||||
@@ -1706,19 +1702,15 @@ impl<T: Time, D: Drift<T>, O: Observer<T>, K: Eq + Hash + Clone, R: RatingRule<K
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// `JointUnavailable` if the history is empty, contains ranked events, or
|
||||
/// yields a precision matrix that is not positive-definite.
|
||||
/// `EmptyHistory` for a history with no events, `JointRequiresScoredEvents`
|
||||
/// for one containing ranked events, and `NotPositiveDefinite` if the
|
||||
/// assembled matrix is indefinite.
|
||||
pub fn joint(&self) -> Result<Joint<'_, K, T, D, O, R>, InferenceError> {
|
||||
if self.time_slices.is_empty() {
|
||||
return Err(InferenceError::JointUnavailable {
|
||||
reason: "the history has no events",
|
||||
});
|
||||
return Err(InferenceError::EmptyHistory);
|
||||
}
|
||||
if !self.time_slices.iter().all(TimeSlice::all_scored) {
|
||||
return Err(InferenceError::JointUnavailable {
|
||||
reason: "the history contains ranked events, whose EP factors are \
|
||||
not retained after convergence",
|
||||
});
|
||||
return Err(InferenceError::JointRequiresScoredEvents);
|
||||
}
|
||||
|
||||
let TimeExpanded {
|
||||
@@ -1728,14 +1720,8 @@ impl<T: Time, D: Drift<T>, O: Observer<T>, K: Eq + Hash + Clone, R: RatingRule<K
|
||||
width,
|
||||
} = self.time_expanded_joint();
|
||||
|
||||
let cholesky = crate::joint::Cholesky::factor(lambda, width).ok_or(
|
||||
InferenceError::JointUnavailable {
|
||||
reason: "the precision matrix is not positive-definite; the usual \
|
||||
cause is a competitor with neither a proper prior nor any \
|
||||
evidence, but an extreme prior or drift can also make the \
|
||||
assembled matrix indefinite in floating point",
|
||||
},
|
||||
)?;
|
||||
let cholesky = crate::joint::Cholesky::factor(lambda, width)
|
||||
.ok_or(InferenceError::NotPositiveDefinite)?;
|
||||
|
||||
Ok(Joint {
|
||||
history: self,
|
||||
@@ -1771,8 +1757,8 @@ impl<T: Time, D: Drift<T>, O: Observer<T>, K: Eq + Hash + Clone, R: RatingRule<K
|
||||
///
|
||||
/// `MismatchedShape` unless exactly two teams are supplied, `EmptyTeam`,
|
||||
/// `UnknownKey` under [`UnknownKeys::Reject`](crate::UnknownKeys::Reject),
|
||||
/// and `JointUnavailable` if the history is empty or holds ranked events in
|
||||
/// *any* slice — not merely the latest one.
|
||||
/// and `EmptyHistory` / `JointRequiresScoredEvents` — the latter if *any*
|
||||
/// slice holds ranked events, not merely the latest one.
|
||||
pub fn predict_margin<Q>(&self, teams: &[&[&Q]]) -> Result<Gaussian, InferenceError>
|
||||
where
|
||||
K: Borrow<Q>,
|
||||
@@ -1780,7 +1766,7 @@ impl<T: Time, D: Drift<T>, O: Observer<T>, K: Eq + Hash + Clone, R: RatingRule<K
|
||||
{
|
||||
if teams.len() != 2 {
|
||||
return Err(InferenceError::MismatchedShape {
|
||||
kind: "predict_margin takes exactly 2 teams",
|
||||
shape: crate::Shape::Teams,
|
||||
expected: 2,
|
||||
got: teams.len(),
|
||||
});
|
||||
@@ -1847,9 +1833,9 @@ impl<T: Time, D: Drift<T>, O: Observer<T>, K: Eq + Hash + Clone, R: RatingRule<K
|
||||
/// for a hypothetical outcome.
|
||||
///
|
||||
/// Every prediction reads skills through one gate, which adds two errors to
|
||||
/// all of them: `NonFiniteResult` if a skill has no usable mean or variance
|
||||
/// — the fit did not converge — and `InvalidParameter` if `beta` is zero
|
||||
/// and every skill is a point mass, leaving no performance distribution to
|
||||
/// all of them: `NonFiniteSkill` if a skill has no usable mean or variance
|
||||
/// — the fit did not converge — and `NoPerformanceVariance` if `beta` is zero and
|
||||
/// every skill is a point mass, leaving no performance distribution to
|
||||
/// predict from.
|
||||
pub fn expected_information_gain<Q>(&self, teams: &[&[&Q]]) -> Result<f64, InferenceError>
|
||||
where
|
||||
@@ -1908,9 +1894,9 @@ impl<T: Time, D: Drift<T>, O: Observer<T>, K: Eq + Hash + Clone, R: RatingRule<K
|
||||
/// `NotEnoughTeams`, `EmptyTeam`, or `UnknownKey`.
|
||||
///
|
||||
/// Every prediction reads skills through one gate, which adds two errors to
|
||||
/// all of them: `NonFiniteResult` if a skill has no usable mean or variance
|
||||
/// — the fit did not converge — and `InvalidParameter` if `beta` is zero
|
||||
/// and every skill is a point mass, leaving no performance distribution to
|
||||
/// all of them: `NonFiniteSkill` if a skill has no usable mean or variance
|
||||
/// — the fit did not converge — and `NoPerformanceVariance` if `beta` is zero and
|
||||
/// every skill is a point mass, leaving no performance distribution to
|
||||
/// predict from.
|
||||
pub fn predict_win_probabilities<Q>(&self, teams: &[&[&Q]]) -> Result<Vec<f64>, InferenceError>
|
||||
where
|
||||
@@ -1963,9 +1949,9 @@ impl<T: Time, D: Drift<T>, O: Observer<T>, K: Eq + Hash + Clone, R: RatingRule<K
|
||||
/// integrate on one grid.
|
||||
///
|
||||
/// Every prediction reads skills through one gate, which adds two errors to
|
||||
/// all of them: `NonFiniteResult` if a skill has no usable mean or variance
|
||||
/// — the fit did not converge — and `InvalidParameter` if `beta` is zero
|
||||
/// and every skill is a point mass, leaving no performance distribution to
|
||||
/// all of them: `NonFiniteSkill` if a skill has no usable mean or variance
|
||||
/// — the fit did not converge — and `NoPerformanceVariance` if `beta` is zero and
|
||||
/// every skill is a point mass, leaving no performance distribution to
|
||||
/// predict from.
|
||||
pub fn predict_outcome<Q>(&self, teams: &[&[&Q]]) -> Result<Prediction, InferenceError>
|
||||
where
|
||||
@@ -2013,9 +1999,9 @@ impl<T: Time, D: Drift<T>, O: Observer<T>, K: Eq + Hash + Clone, R: RatingRule<K
|
||||
/// performance sigmas are too far apart to integrate on one grid.
|
||||
///
|
||||
/// Every prediction reads skills through one gate, which adds two errors to
|
||||
/// all of them: `NonFiniteResult` if a skill has no usable mean or variance
|
||||
/// — the fit did not converge — and `InvalidParameter` if `beta` is zero
|
||||
/// and every skill is a point mass, leaving no performance distribution to
|
||||
/// all of them: `NonFiniteSkill` if a skill has no usable mean or variance
|
||||
/// — the fit did not converge — and `NoPerformanceVariance` if `beta` is zero and
|
||||
/// every skill is a point mass, leaving no performance distribution to
|
||||
/// predict from.
|
||||
pub fn predict_ranking<Q>(&self, teams: &[&[&Q]], ranks: &[u32]) -> Result<f64, InferenceError>
|
||||
where
|
||||
@@ -2024,7 +2010,7 @@ impl<T: Time, D: Drift<T>, O: Observer<T>, K: Eq + Hash + Clone, R: RatingRule<K
|
||||
{
|
||||
if ranks.len() != teams.len() {
|
||||
return Err(InferenceError::MismatchedShape {
|
||||
kind: "ranks vs teams",
|
||||
shape: crate::Shape::OutcomeVsTeams,
|
||||
expected: teams.len(),
|
||||
got: ranks.len(),
|
||||
});
|
||||
@@ -2060,7 +2046,7 @@ impl<T: Time, D: Drift<T>, O: Observer<T>, K: Eq + Hash + Clone, R: RatingRule<K
|
||||
/// `NotConverged` if the sweep hits `max_iter` with the step still above
|
||||
/// `epsilon`.
|
||||
///
|
||||
/// `NonFiniteResult` if a sweep produces a NaN or infinite step. EP has
|
||||
/// `NonFiniteStep` if a sweep produces a NaN or infinite step. EP has
|
||||
/// broken down at that point and further iterations cannot recover, so the
|
||||
/// loop stops rather than reporting a NaN step as convergence.
|
||||
///
|
||||
@@ -2092,7 +2078,7 @@ impl<T: Time, D: Drift<T>, O: Observer<T>, K: Eq + Hash + Clone, R: RatingRule<K
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// `NonFiniteResult` if a sweep produces a NaN or infinite step.
|
||||
/// `NonFiniteStep` if a sweep produces a NaN or infinite step.
|
||||
///
|
||||
/// `InvalidParameter` if a competitor's drift model yields a negative or
|
||||
/// non-finite variance. Checked here, before any sweeping, so it applies to
|
||||
@@ -2123,7 +2109,7 @@ impl<T: Time, D: Drift<T>, O: Observer<T>, K: Eq + Hash + Clone, R: RatingRule<K
|
||||
.drift_variance_for_elapsed(elapsed);
|
||||
if !drift.is_finite() || drift < 0.0 {
|
||||
return Err(InferenceError::InvalidParameter {
|
||||
name: "drift variance",
|
||||
parameter: crate::Parameter::DriftVariance,
|
||||
value: drift,
|
||||
});
|
||||
}
|
||||
@@ -2160,7 +2146,7 @@ impl<T: Time, D: Drift<T>, O: Observer<T>, K: Eq + Hash + Clone, R: RatingRule<K
|
||||
if !crate::step_is_finite(step) {
|
||||
self.observer.on_converged(i, step, false);
|
||||
|
||||
return Err(InferenceError::NonFiniteResult {
|
||||
return Err(InferenceError::NonFiniteStep {
|
||||
context: "History::converge",
|
||||
step,
|
||||
});
|
||||
@@ -2198,14 +2184,14 @@ impl<T: Time, D: Drift<T>, O: Observer<T>, K: Eq + Hash + Clone, R: RatingRule<K
|
||||
let got = results.as_ref().map_or(0, Vec::len);
|
||||
|
||||
return Err(InferenceError::MismatchedShape {
|
||||
kind: "results",
|
||||
shape: crate::Shape::Internal,
|
||||
expected: composition.len(),
|
||||
got,
|
||||
});
|
||||
}
|
||||
if times.len() != composition.len() {
|
||||
return Err(InferenceError::MismatchedShape {
|
||||
kind: "times",
|
||||
shape: crate::Shape::Internal,
|
||||
expected: composition.len(),
|
||||
got: times.len(),
|
||||
});
|
||||
@@ -2217,14 +2203,14 @@ impl<T: Time, D: Drift<T>, O: Observer<T>, K: Eq + Hash + Clone, R: RatingRule<K
|
||||
let got = weights.as_ref().map_or(0, Vec::len);
|
||||
|
||||
return Err(InferenceError::MismatchedShape {
|
||||
kind: "weights",
|
||||
shape: crate::Shape::Weights,
|
||||
expected: composition.len(),
|
||||
got,
|
||||
});
|
||||
}
|
||||
if kinds.len() != composition.len() {
|
||||
return Err(InferenceError::MismatchedShape {
|
||||
kind: "kinds",
|
||||
shape: crate::Shape::Internal,
|
||||
expected: composition.len(),
|
||||
got: kinds.len(),
|
||||
});
|
||||
@@ -2254,19 +2240,19 @@ impl<T: Time, D: Drift<T>, O: Observer<T>, K: Eq + Hash + Clone, R: RatingRule<K
|
||||
}
|
||||
|
||||
// A non-finite outcome poisons the history rather than failing it:
|
||||
// `converge` does report `NonFiniteResult`, but a caller who reads
|
||||
// `converge` does report `NonFiniteStep`, but a caller who reads
|
||||
// `current_skill` before converging is handed a NaN posterior with
|
||||
// nothing to say it is one.
|
||||
if let Some(results) = results.as_ref() {
|
||||
for (event_results, kind) in results.iter().zip(kinds.iter()) {
|
||||
let name = match kind {
|
||||
EventKind::Ranked => "rank",
|
||||
EventKind::Scored { .. } => "score",
|
||||
let parameter = match kind {
|
||||
EventKind::Ranked => crate::Parameter::Rank,
|
||||
EventKind::Scored { .. } => crate::Parameter::Score,
|
||||
};
|
||||
for value in event_results {
|
||||
if !value.is_finite() {
|
||||
return Err(InferenceError::InvalidParameter {
|
||||
name,
|
||||
parameter,
|
||||
value: *value,
|
||||
});
|
||||
}
|
||||
@@ -2289,7 +2275,7 @@ impl<T: Time, D: Drift<T>, O: Observer<T>, K: Eq + Hash + Clone, R: RatingRule<K
|
||||
for weight in team_weights {
|
||||
if !weight.is_finite() {
|
||||
return Err(InferenceError::InvalidParameter {
|
||||
name: "weight",
|
||||
parameter: crate::Parameter::Weight,
|
||||
value: *weight,
|
||||
});
|
||||
}
|
||||
@@ -2338,7 +2324,7 @@ impl<T: Time, D: Drift<T>, O: Observer<T>, K: Eq + Hash + Clone, R: RatingRule<K
|
||||
if existing != new {
|
||||
return Err(InferenceError::ConflictingCompetitorConfig {
|
||||
competitor: competitor.get(),
|
||||
field: "prior",
|
||||
field: crate::CompetitorField::Prior,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -2346,7 +2332,7 @@ impl<T: Time, D: Drift<T>, O: Observer<T>, K: Eq + Hash + Clone, R: RatingRule<K
|
||||
if existing != new {
|
||||
return Err(InferenceError::ConflictingCompetitorConfig {
|
||||
competitor: competitor.get(),
|
||||
field: "drift_scale",
|
||||
field: crate::CompetitorField::DriftScale,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -2677,7 +2663,7 @@ impl<T: Time, D: Drift<T>, O: Observer<T>, K: Eq + Hash + Clone, R: RatingRule<K
|
||||
for ev in events {
|
||||
if ev.outcome.team_count() != ev.teams.len() {
|
||||
return Err(InferenceError::MismatchedShape {
|
||||
kind: "outcome vs teams",
|
||||
shape: crate::Shape::OutcomeVsTeams,
|
||||
expected: ev.teams.len(),
|
||||
got: ev.outcome.team_count(),
|
||||
});
|
||||
@@ -2700,7 +2686,7 @@ impl<T: Time, D: Drift<T>, O: Observer<T>, K: Eq + Hash + Clone, R: RatingRule<K
|
||||
// accept a sign the caller cannot have meant.
|
||||
if !scale.is_finite() || scale < 0.0 {
|
||||
return Err(InferenceError::InvalidParameter {
|
||||
name: "drift_scale",
|
||||
parameter: crate::Parameter::DriftScale,
|
||||
value: scale,
|
||||
});
|
||||
}
|
||||
@@ -2724,7 +2710,7 @@ impl<T: Time, D: Drift<T>, O: Observer<T>, K: Eq + Hash + Clone, R: RatingRule<K
|
||||
if entry.prior.is_some_and(|held| held != prior) {
|
||||
return Err(InferenceError::ConflictingCompetitorConfig {
|
||||
competitor: idx.get(),
|
||||
field: "prior",
|
||||
field: crate::CompetitorField::Prior,
|
||||
});
|
||||
}
|
||||
entry.prior = Some(prior);
|
||||
@@ -2733,7 +2719,7 @@ impl<T: Time, D: Drift<T>, O: Observer<T>, K: Eq + Hash + Clone, R: RatingRule<K
|
||||
if entry.drift_scale.is_some_and(|held| held != scale) {
|
||||
return Err(InferenceError::ConflictingCompetitorConfig {
|
||||
competitor: idx.get(),
|
||||
field: "drift_scale",
|
||||
field: crate::CompetitorField::DriftScale,
|
||||
});
|
||||
}
|
||||
entry.drift_scale = Some(scale);
|
||||
@@ -2759,7 +2745,7 @@ impl<T: Time, D: Drift<T>, O: Observer<T>, K: Eq + Hash + Clone, R: RatingRule<K
|
||||
let resolved = score_sigma.unwrap_or(self.score_sigma);
|
||||
if resolved <= 0.0 || resolved.is_nan() {
|
||||
return Err(InferenceError::InvalidParameter {
|
||||
name: "score_sigma",
|
||||
parameter: crate::Parameter::ScoreSigma,
|
||||
value: resolved,
|
||||
});
|
||||
}
|
||||
@@ -3011,7 +2997,7 @@ impl<K: Eq + Hash + Clone, T: Time, D: Drift<T>, O: Observer<T>, R: RatingRule<K
|
||||
{
|
||||
if teams.len() != 2 {
|
||||
return Err(InferenceError::MismatchedShape {
|
||||
kind: "expected_variance_reduction takes exactly 2 teams",
|
||||
shape: crate::Shape::Teams,
|
||||
expected: 2,
|
||||
got: teams.len(),
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user