feat!: validate mu, sigma and beta on HistoryBuilder
The last three unvalidated setters, beside `p_draw`, `score_sigma` and `convergence`, which all assert eagerly. Measured before choosing bounds: beta = 0 -> works, pi 0.0211 (vs 0.0193 at the default) beta = -4.17 -> bit-identical to +4.17 sigma = -8.33 -> bit-identical to +8.33 sigma = 0 -> NonFiniteResult, current_skill returns tau: NaN sigma = inf -> same mu = NaN -> same So the bounds are not the obvious ones. `beta = 0` is legitimate and meaningful — performance is then exactly skill, and the fit moves measurably rather than degenerating — so zero is allowed and a test pins that it reaches a different answer, since "allowed" would otherwise be indistinguishable from "unchecked". The negative cases are the quiet ones. `sigma` and `beta` enter inference only as squares, so a negative value behaves as its absolute value and the sign is dropped without comment. That is the same defect `Member::with_drift_scale` already rejects, for the reason already written there. The non-finite cases are detected today — `converge` reports NonFiniteResult — but a caller who reads `current_skill` first is handed `tau: NaN`, so rejecting at the boundary is what actually closes it. BREAKING CHANGE: `HistoryBuilder::mu`, `sigma` and `beta` now panic on values they previously accepted, matching the existing behaviour of `p_draw`, `score_sigma` and `convergence`. Refs #18 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011hcFjNDmHXZF8URGLku5zZ
This commit is contained in:
@@ -39,17 +39,55 @@ pub struct HistoryBuilder<
|
||||
}
|
||||
|
||||
impl<T: Time, D: Drift<T>, O: Observer<T>, K: Eq + Hash + Clone> HistoryBuilder<T, D, O, K> {
|
||||
/// Prior mean skill.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// Panics if `mu` is not finite. A non-finite prior mean poisons every
|
||||
/// posterior derived from it: `converge` reports `NonFiniteResult`, 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})");
|
||||
self.mu = mu;
|
||||
self
|
||||
}
|
||||
|
||||
/// Prior standard deviation.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// Panics unless `sigma` is finite and strictly positive.
|
||||
///
|
||||
/// Zero and infinity both give a prior precision that is not a number, and
|
||||
/// the whole fit comes back NaN. A *negative* sigma is the quieter half:
|
||||
/// it is only ever squared, so `-8.33` produces bit-identical results to
|
||||
/// `8.33` — a sign the caller cannot have meant, silently ignored.
|
||||
pub fn sigma(mut self, sigma: f64) -> Self {
|
||||
assert!(
|
||||
sigma.is_finite() && sigma > 0.0,
|
||||
"sigma must be finite and positive (got {sigma})"
|
||||
);
|
||||
self.sigma = sigma;
|
||||
self
|
||||
}
|
||||
|
||||
/// Per-event performance noise.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// Panics unless `beta` is finite and non-negative.
|
||||
///
|
||||
/// Zero is allowed and meaningful — performance is then exactly skill, and
|
||||
/// the fit differs measurably from a positive `beta` rather than
|
||||
/// degenerating. Negative is rejected for the same reason as a negative
|
||||
/// `sigma` or `Member::with_drift_scale`: `beta` enters only as `beta^2`,
|
||||
/// so a negative value behaves as its absolute value and the sign is lost
|
||||
/// without comment.
|
||||
pub fn beta(mut self, beta: f64) -> Self {
|
||||
assert!(
|
||||
beta.is_finite() && beta >= 0.0,
|
||||
"beta must be finite and non-negative (got {beta})"
|
||||
);
|
||||
self.beta = beta;
|
||||
self
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user