use std::marker::PhantomData; use crate::{ BETA, GAMMA, drift::{ConstantDrift, Drift}, gaussian::Gaussian, time::Time, }; /// Static rating configuration: prior skill, performance noise `beta`, drift. /// /// A configuration rather than a person: the per-history temporal state /// (messages, last appearance) lives on `Competitor`. #[derive(Clone, Copy, Debug, PartialEq)] pub struct Rating = ConstantDrift> { pub(crate) prior: Gaussian, pub(crate) beta: f64, pub(crate) drift: D, /// Multiplier on the drift *variance* this competitor accumulates; 1.0 is /// the neutral default. Set per competitor via `Member::with_drift_scale`. pub(crate) drift_scale: f64, pub(crate) _time: PhantomData, } impl> Rating { /// # Panics /// /// Panics unless `beta` is finite and non-negative, matching /// `HistoryBuilder::beta`. /// /// 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 because `beta` enters only as `beta^2`: measured, a /// negative beta returned results **bit identical** to its absolute value, /// and a NaN beta reached `Game::ranked`, which returned `Ok` carrying a /// `Gaussian { pi: NaN, tau: NaN }` — there is no `converge` on that path to /// catch it. pub fn new(prior: Gaussian, beta: f64, drift: D) -> Self { assert!( beta.is_finite() && beta >= 0.0, "beta must be finite and non-negative (got {beta}); it is only ever \ squared, so a negative value would silently behave as its absolute value" ); Self { prior, beta, drift, drift_scale: 1.0, _time: PhantomData, } } /// Scale how fast this competitor drifts, relative to `drift`. /// /// Multiplies the drift *variance*, so the scale is in the same units as /// `gamma`. `0.0` pins the competitor still. #[must_use] pub fn with_drift_scale(mut self, drift_scale: f64) -> Self { self.drift_scale = drift_scale; self } /// The configured prior skill estimate. pub fn prior(&self) -> Gaussian { self.prior } /// Performance noise: how much a single showing varies around the skill. #[must_use] pub fn beta(&self) -> f64 { self.beta } /// The drift model governing how skill may move between events. #[must_use] pub fn drift(&self) -> D { self.drift } /// This competitor's multiplier on the drift variance; 1.0 is neutral. #[must_use] pub fn drift_scale(&self) -> f64 { self.drift_scale } /// Drift variance accumulated over `from -> to`, scaled for this competitor. /// /// The single place the scale is applied for a `Time`-typed span. Callers /// must go through this rather than `self.drift` directly, so a competitor's /// scale cannot be silently skipped. pub(crate) fn drift_variance_delta(&self, from: &T, to: &T) -> f64 { self.drift.variance_delta(from, to) * self.drift_scale * self.drift_scale } /// Drift variance for a cached elapsed count, scaled for this competitor. /// /// The counterpart of `drift_variance_delta` for the cached-elapsed paths. pub(crate) fn drift_variance_for_elapsed(&self, elapsed: i64) -> f64 { self.drift.variance_for_elapsed(elapsed) * self.drift_scale * self.drift_scale } pub(crate) fn performance(&self) -> Gaussian { self.prior.forget(self.beta.powi(2)) } } impl Default for Rating { fn default() -> Self { Self { prior: Gaussian::default(), beta: BETA, drift: ConstantDrift::new(GAMMA), drift_scale: 1.0, _time: PhantomData, } } }