docs: complete the public API documentation contract
Closes the last open item in #25. `cargo clippy -W missing_errors_doc -W missing_panics_doc -W must_use_candidate -W doc_markdown` went from 56 warnings to zero. The 13 hand-written sections name the actual variants each function returns rather than gesturing at "an error". Establishing that meant reading the error paths — `Game::ranked` alone returns four distinct variants, and `record_draw` can hit TieWithoutDrawProbability where `record_winner` provably cannot, since a two-team decisive outcome has nothing to tie. Documenting those as interchangeable would have been worse than leaving them undocumented, because a reader would trust it. Two existing doc comments already described panics in prose but not under a `# Panics` heading, so neither rustdoc nor clippy surfaced them: `Outcome::winner` and `EventBuilder::weights`. Both now carry the heading, and `Outcome::winner` gained the note that it ties every loser, so `n >= 3` needs a positive p_draw — the crate's easiest error to hit by accident. The 43 mechanical fixes (31 `#[must_use]` on pure accessors, 11 missing backticks) were applied with `cargo clippy --fix`. `#[must_use]` on Gaussian's arithmetic and on `posteriors()` matters: discarding those results is always a bug, and until now nothing said so. Also documented why `[profile.release] debug = true` exists — cargo-flamegraph needs the symbols, and library profile settings are ignored downstream, so it reads as an oversight without the note. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01T5SYDExxL4vZgvunrcNSMc
This commit is contained in:
@@ -65,6 +65,10 @@ plotters-backend = "0.3"
|
|||||||
time = { version = "0.3", features = ["parsing"] }
|
time = { version = "0.3", features = ["parsing"] }
|
||||||
trueskill-tt = { path = ".", features = ["approx"] }
|
trueskill-tt = { path = ".", features = ["approx"] }
|
||||||
|
|
||||||
|
# Debug symbols in release are for `just flame` (cargo-flamegraph), which needs
|
||||||
|
# them to symbolicate. Profile settings in a library are ignored by downstream
|
||||||
|
# consumers, so these only affect local builds — this is deliberate, not an
|
||||||
|
# oversight.
|
||||||
[profile.release]
|
[profile.release]
|
||||||
debug = true
|
debug = true
|
||||||
|
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ pub struct Team<K> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl<K> Team<K> {
|
impl<K> Team<K> {
|
||||||
|
#[must_use]
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
members: SmallVec::new(),
|
members: SmallVec::new(),
|
||||||
|
|||||||
@@ -50,8 +50,10 @@ where
|
|||||||
|
|
||||||
/// Set per-member weights for the most recently added team.
|
/// Set per-member weights for the most recently added team.
|
||||||
///
|
///
|
||||||
/// Panics in debug builds if called before `.team(...)` or if the length
|
/// # Panics
|
||||||
/// doesn't match the team's member count.
|
///
|
||||||
|
/// Panics if called before any `.team(...)`. In debug builds, also panics
|
||||||
|
/// if the number of weights does not match the team's member count.
|
||||||
pub fn weights<I: IntoIterator<Item = f64>>(mut self, weights: I) -> Self {
|
pub fn weights<I: IntoIterator<Item = f64>>(mut self, weights: I) -> Self {
|
||||||
let idx = self
|
let idx = self
|
||||||
.current_team_idx
|
.current_team_idx
|
||||||
@@ -103,6 +105,10 @@ where
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Commit the event to the history.
|
/// Commit the event to the history.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Forwards to [`History::add_events`] and returns its errors.
|
||||||
pub fn commit(self) -> Result<(), InferenceError> {
|
pub fn commit(self) -> Result<(), InferenceError> {
|
||||||
self.history.add_events(std::iter::once(self.event))
|
self.history.add_events(std::iter::once(self.event))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ pub struct MarginFactor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl MarginFactor {
|
impl MarginFactor {
|
||||||
|
#[must_use]
|
||||||
pub fn new(diff: VarId, m_obs: f64, sigma: f64) -> Self {
|
pub fn new(diff: VarId, m_obs: f64, sigma: f64) -> Self {
|
||||||
debug_assert!(sigma > 0.0, "score sigma must be positive");
|
debug_assert!(sigma > 0.0, "score sigma must be positive");
|
||||||
Self {
|
Self {
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ pub struct VarStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl VarStore {
|
impl VarStore {
|
||||||
|
#[must_use]
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self::default()
|
Self::default()
|
||||||
}
|
}
|
||||||
@@ -28,10 +29,12 @@ impl VarStore {
|
|||||||
self.marginals.clear();
|
self.marginals.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
pub fn len(&self) -> usize {
|
pub fn len(&self) -> usize {
|
||||||
self.marginals.len()
|
self.marginals.len()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
pub fn is_empty(&self) -> bool {
|
pub fn is_empty(&self) -> bool {
|
||||||
self.marginals.is_empty()
|
self.marginals.is_empty()
|
||||||
}
|
}
|
||||||
@@ -42,6 +45,7 @@ impl VarStore {
|
|||||||
id
|
id
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
pub fn get(&self, id: VarId) -> Gaussian {
|
pub fn get(&self, id: VarId) -> Gaussian {
|
||||||
self.marginals[id.0 as usize]
|
self.marginals[id.0 as usize]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,12 +5,12 @@ use crate::factor::{Factor, VarId, VarStore};
|
|||||||
/// On each propagation:
|
/// On each propagation:
|
||||||
/// - Reads marginals at `team_a` and `team_b` (which already incorporate any
|
/// - Reads marginals at `team_a` and `team_b` (which already incorporate any
|
||||||
/// incoming messages from neighboring factors).
|
/// incoming messages from neighboring factors).
|
||||||
/// - Computes `new_diff = team_a - team_b` (variance addition; see Gaussian::Sub).
|
/// - Computes `new_diff = team_a - team_b` (variance addition; see `Gaussian::Sub`).
|
||||||
/// - Writes the new marginal to `diff`.
|
/// - Writes the new marginal to `diff`.
|
||||||
/// - Returns the delta against the previous diff value.
|
/// - Returns the delta against the previous diff value.
|
||||||
///
|
///
|
||||||
/// This factor does NOT store an outgoing message; the diff variable is
|
/// This factor does NOT store an outgoing message; the diff variable is
|
||||||
/// effectively replaced on each propagation. The TruncFactor on the same diff
|
/// effectively replaced on each propagation. The `TruncFactor` on the same diff
|
||||||
/// var holds the EP-divide message that produces the cavity.
|
/// var holds the EP-divide message that produces the cavity.
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct RankDiffFactor {
|
pub struct RankDiffFactor {
|
||||||
|
|||||||
+2
-1
@@ -15,13 +15,14 @@ pub struct TruncFactor {
|
|||||||
pub diff: VarId,
|
pub diff: VarId,
|
||||||
pub margin: f64,
|
pub margin: f64,
|
||||||
pub tie: bool,
|
pub tie: bool,
|
||||||
/// Outgoing message to the diff variable (initial: N_INF, the EP identity).
|
/// Outgoing message to the diff variable (initial: `N_INF`, the EP identity).
|
||||||
pub(crate) msg: Gaussian,
|
pub(crate) msg: Gaussian,
|
||||||
/// Cached evidence (linear, not log) computed from the cavity on first propagation.
|
/// Cached evidence (linear, not log) computed from the cavity on first propagation.
|
||||||
pub(crate) evidence_cached: Option<f64>,
|
pub(crate) evidence_cached: Option<f64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TruncFactor {
|
impl TruncFactor {
|
||||||
|
#[must_use]
|
||||||
pub fn new(diff: VarId, margin: f64, tie: bool) -> Self {
|
pub fn new(diff: VarId, margin: f64, tie: bool) -> Self {
|
||||||
Self {
|
Self {
|
||||||
diff,
|
diff,
|
||||||
|
|||||||
+28
@@ -145,6 +145,7 @@ impl<T: Time, D: Drift<T>> OwnedGame<T, D> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
pub fn posteriors(&self) -> Vec<Vec<Gaussian>> {
|
pub fn posteriors(&self) -> Vec<Vec<Gaussian>> {
|
||||||
self.likelihoods
|
self.likelihoods
|
||||||
.iter()
|
.iter()
|
||||||
@@ -153,6 +154,7 @@ impl<T: Time, D: Drift<T>> OwnedGame<T, D> {
|
|||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
pub fn log_evidence(&self) -> f64 {
|
pub fn log_evidence(&self) -> f64 {
|
||||||
self.log_evidence
|
self.log_evidence
|
||||||
}
|
}
|
||||||
@@ -409,6 +411,7 @@ impl<'a, T: Time, D: Drift<T>> Game<'a, T, D> {
|
|||||||
self.likelihoods = likelihoods;
|
self.likelihoods = likelihoods;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
pub fn posteriors(&self) -> Vec<Vec<Gaussian>> {
|
pub fn posteriors(&self) -> Vec<Vec<Gaussian>> {
|
||||||
self.likelihoods
|
self.likelihoods
|
||||||
.iter()
|
.iter()
|
||||||
@@ -422,12 +425,21 @@ impl<'a, T: Time, D: Drift<T>> Game<'a, T, D> {
|
|||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
pub fn log_evidence(&self) -> f64 {
|
pub fn log_evidence(&self) -> f64 {
|
||||||
self.log_evidence
|
self.log_evidence
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: Time, D: Drift<T>> Game<'_, T, D> {
|
impl<T: Time, D: Drift<T>> Game<'_, T, D> {
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// - `InvalidProbability` if `options.p_draw` is outside `[0.0, 1.0)`.
|
||||||
|
/// - `MismatchedShape` if the outcome's rank count differs from `teams.len()`.
|
||||||
|
/// - `WrongOutcomeKind` if `outcome` is not `Outcome::Ranked`.
|
||||||
|
/// - `TieWithoutDrawProbability` if the outcome ties two teams while
|
||||||
|
/// `p_draw` is zero: the truncation margin is then zero and the two-sided
|
||||||
|
/// tie update evaluates `0/0`.
|
||||||
pub fn ranked(
|
pub fn ranked(
|
||||||
teams: &[&[Rating<T, D>]],
|
teams: &[&[Rating<T, D>]],
|
||||||
outcome: crate::Outcome,
|
outcome: crate::Outcome,
|
||||||
@@ -478,6 +490,12 @@ impl<T: Time, D: Drift<T>> Game<'_, T, D> {
|
|||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// - `InvalidParameter` if `options.score_sigma` is not strictly positive,
|
||||||
|
/// or is NaN.
|
||||||
|
/// - `MismatchedShape` if the outcome's score count differs from `teams.len()`.
|
||||||
|
/// - `WrongOutcomeKind` if `outcome` is not `Outcome::Scored`.
|
||||||
pub fn scored(
|
pub fn scored(
|
||||||
teams: &[&[Rating<T, D>]],
|
teams: &[&[Rating<T, D>]],
|
||||||
outcome: crate::Outcome,
|
outcome: crate::Outcome,
|
||||||
@@ -515,6 +533,12 @@ impl<T: Time, D: Drift<T>> Game<'_, T, D> {
|
|||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Delegates to [`Game::ranked`] with default options, so it returns the
|
||||||
|
/// same errors — in practice `WrongOutcomeKind` for a non-ranked outcome,
|
||||||
|
/// or `TieWithoutDrawProbability` for a draw, since the default `p_draw`
|
||||||
|
/// applies rather than one you chose.
|
||||||
pub fn one_v_one(
|
pub fn one_v_one(
|
||||||
a: &Rating<T, D>,
|
a: &Rating<T, D>,
|
||||||
b: &Rating<T, D>,
|
b: &Rating<T, D>,
|
||||||
@@ -525,6 +549,10 @@ impl<T: Time, D: Drift<T>> Game<'_, T, D> {
|
|||||||
Ok((post[0][0], post[1][0]))
|
Ok((post[0][0], post[1][0]))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Wraps each player in a one-member team and delegates to
|
||||||
|
/// [`Game::ranked`], so it returns the same errors.
|
||||||
pub fn free_for_all(
|
pub fn free_for_all(
|
||||||
players: &[&Rating<T, D>],
|
players: &[&Rating<T, D>],
|
||||||
outcome: crate::Outcome,
|
outcome: crate::Outcome,
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ pub struct Gaussian {
|
|||||||
|
|
||||||
impl Gaussian {
|
impl Gaussian {
|
||||||
/// Construct from mean and standard deviation.
|
/// Construct from mean and standard deviation.
|
||||||
|
#[must_use]
|
||||||
pub const fn from_ms(mu: f64, sigma: f64) -> Self {
|
pub const fn from_ms(mu: f64, sigma: f64) -> Self {
|
||||||
if sigma == f64::INFINITY {
|
if sigma == f64::INFINITY {
|
||||||
Self { pi: 0.0, tau: 0.0 }
|
Self { pi: 0.0, tau: 0.0 }
|
||||||
@@ -64,16 +65,19 @@ impl Gaussian {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
|
#[must_use]
|
||||||
pub fn pi(&self) -> f64 {
|
pub fn pi(&self) -> f64 {
|
||||||
self.pi
|
self.pi
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
|
#[must_use]
|
||||||
pub fn tau(&self) -> f64 {
|
pub fn tau(&self) -> f64 {
|
||||||
self.tau
|
self.tau
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
|
#[must_use]
|
||||||
pub fn mu(&self) -> f64 {
|
pub fn mu(&self) -> f64 {
|
||||||
// A non-positive precision is an improper (uninformative) Gaussian — its mean is
|
// A non-positive precision is an improper (uninformative) Gaussian — its mean is
|
||||||
// undefined. Treat it like `pi == 0` and return 0. EP message cancellation can land
|
// undefined. Treat it like `pi == 0` and return 0. EP message cancellation can land
|
||||||
@@ -102,6 +106,7 @@ impl Gaussian {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
|
#[must_use]
|
||||||
pub fn sigma(&self) -> f64 {
|
pub fn sigma(&self) -> f64 {
|
||||||
// A non-positive precision is improper → infinite standard deviation. Guarding
|
// A non-positive precision is improper → infinite standard deviation. Guarding
|
||||||
// `pi <= 0.0` (not just `== 0.0`) keeps `1.0 / pi.sqrt()` from returning NaN when EP
|
// `pi <= 0.0` (not just `== 0.0`) keeps `1.0 / pi.sqrt()` from returning NaN when EP
|
||||||
@@ -145,6 +150,7 @@ impl Gaussian {
|
|||||||
/// Used by within-game inference to stabilise oscillating fixed-point
|
/// Used by within-game inference to stabilise oscillating fixed-point
|
||||||
/// loops on hard graphs. `alpha = 1.0` returns `new` exactly;
|
/// loops on hard graphs. `alpha = 1.0` returns `new` exactly;
|
||||||
/// `alpha < 1.0` shrinks each per-step update.
|
/// `alpha < 1.0` shrinks each per-step update.
|
||||||
|
#[must_use]
|
||||||
pub fn damp_natural(self, new: Gaussian, alpha: f64) -> Gaussian {
|
pub fn damp_natural(self, new: Gaussian, alpha: f64) -> Gaussian {
|
||||||
Gaussian::from_natural(
|
Gaussian::from_natural(
|
||||||
alpha * new.pi() + (1.0 - alpha) * self.pi(),
|
alpha * new.pi() + (1.0 - alpha) * self.pi(),
|
||||||
|
|||||||
+41
-1
@@ -198,6 +198,7 @@ impl Default for History<i64, ConstantDrift, NullObserver, &'static str> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl History<i64, ConstantDrift, NullObserver, &'static str> {
|
impl History<i64, ConstantDrift, NullObserver, &'static str> {
|
||||||
|
#[must_use]
|
||||||
pub fn builder() -> HistoryBuilder<i64, ConstantDrift, NullObserver, &'static str> {
|
pub fn builder() -> HistoryBuilder<i64, ConstantDrift, NullObserver, &'static str> {
|
||||||
HistoryBuilder::default()
|
HistoryBuilder::default()
|
||||||
}
|
}
|
||||||
@@ -205,6 +206,7 @@ impl History<i64, ConstantDrift, NullObserver, &'static str> {
|
|||||||
|
|
||||||
impl<K: Eq + Hash + Clone> History<i64, ConstantDrift, NullObserver, K> {
|
impl<K: Eq + Hash + Clone> History<i64, ConstantDrift, NullObserver, K> {
|
||||||
/// Like `builder()` but uses a custom key type `K` instead of the default `&'static str`.
|
/// Like `builder()` but uses a custom key type `K` instead of the default `&'static str`.
|
||||||
|
#[must_use]
|
||||||
pub fn builder_with_key() -> HistoryBuilder<i64, ConstantDrift, NullObserver, K> {
|
pub fn builder_with_key() -> HistoryBuilder<i64, ConstantDrift, NullObserver, K> {
|
||||||
HistoryBuilder {
|
HistoryBuilder {
|
||||||
mu: MU,
|
mu: MU,
|
||||||
@@ -552,7 +554,11 @@ impl<T: Time, D: Drift<T>, O: Observer<T>, K: Eq + Hash + Clone> History<T, D, O
|
|||||||
|
|
||||||
/// 2-team win probability: returns `[P(team0 wins), P(team1 wins)]`.
|
/// 2-team win probability: returns `[P(team0 wins), P(team1 wins)]`.
|
||||||
///
|
///
|
||||||
/// Panics if `teams.len() != 2`. N-team support lands in T4.
|
/// N-team support lands in T4.
|
||||||
|
///
|
||||||
|
/// # Panics
|
||||||
|
///
|
||||||
|
/// Panics if `teams.len() != 2`.
|
||||||
pub fn predict_outcome(&self, teams: &[&[&K]]) -> Vec<f64> {
|
pub fn predict_outcome(&self, teams: &[&[&K]]) -> Vec<f64> {
|
||||||
assert_eq!(teams.len(), 2, "predict_outcome T2: 2 teams only");
|
assert_eq!(teams.len(), 2, "predict_outcome T2: 2 teams only");
|
||||||
let gather = |team: &[&K]| -> Gaussian {
|
let gather = |team: &[&K]| -> Gaussian {
|
||||||
@@ -574,6 +580,15 @@ impl<T: Time, D: Drift<T>, O: Observer<T>, K: Eq + Hash + Clone> History<T, D, O
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Run the full forward+backward convergence loop and return a summary.
|
/// Run the full forward+backward convergence loop and return a summary.
|
||||||
|
///
|
||||||
|
/// Failing to reach `epsilon` within `max_iter` is not an error: the
|
||||||
|
/// returned report carries `converged: false` and the final step.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// `NonFiniteResult` 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.
|
||||||
pub fn converge(&mut self) -> Result<ConvergenceReport, InferenceError> {
|
pub fn converge(&mut self) -> Result<ConvergenceReport, InferenceError> {
|
||||||
use std::time::Instant;
|
use std::time::Instant;
|
||||||
|
|
||||||
@@ -830,6 +845,13 @@ impl<T: Time, D: Drift<T>, O: Observer<T>, K: Eq + Hash + Clone> History<T, D, O
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Record a single two-competitor event that `winner` won.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Ingests through the same path as [`History::add_events`], so it returns
|
||||||
|
/// the same errors. A two-team decisive outcome cannot tie, so
|
||||||
|
/// `TieWithoutDrawProbability` is not reachable here.
|
||||||
pub fn record_winner<Q>(&mut self, winner: &Q, loser: &Q, time: T) -> Result<(), InferenceError>
|
pub fn record_winner<Q>(&mut self, winner: &Q, loser: &Q, time: T) -> Result<(), InferenceError>
|
||||||
where
|
where
|
||||||
K: Borrow<Q>,
|
K: Borrow<Q>,
|
||||||
@@ -847,6 +869,13 @@ impl<T: Time, D: Drift<T>, O: Observer<T>, K: Eq + Hash + Clone> History<T, D, O
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Record a single two-competitor event that ended level.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Ingests through the same path as [`History::add_events`]. Note
|
||||||
|
/// `TieWithoutDrawProbability` *is* reachable here: a draw needs a
|
||||||
|
/// positive `p_draw`.
|
||||||
pub fn record_draw<Q>(&mut self, a: &Q, b: &Q, time: T) -> Result<(), InferenceError>
|
pub fn record_draw<Q>(&mut self, a: &Q, b: &Q, time: T) -> Result<(), InferenceError>
|
||||||
where
|
where
|
||||||
K: Borrow<Q>,
|
K: Borrow<Q>,
|
||||||
@@ -870,6 +899,17 @@ impl<T: Time, D: Drift<T>, O: Observer<T>, K: Eq + Hash + Clone> History<T, D, O
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Bulk-ingest typed events.
|
/// Bulk-ingest typed events.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// - `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
|
||||||
|
/// the team's membership.
|
||||||
|
/// - `InvalidParameter` if a per-event `score_sigma` override is not
|
||||||
|
/// strictly positive.
|
||||||
|
/// - `TieWithoutDrawProbability` if an event ties two teams while the
|
||||||
|
/// history's `p_draw` is zero. This includes `Outcome::winner(w, n)` for
|
||||||
|
/// `n >= 3`, which ties every loser.
|
||||||
pub fn add_events<I>(&mut self, events: I) -> Result<(), InferenceError>
|
pub fn add_events<I>(&mut self, events: I) -> Result<(), InferenceError>
|
||||||
where
|
where
|
||||||
I: IntoIterator<Item = crate::event::Event<T, K>>,
|
I: IntoIterator<Item = crate::event::Event<T, K>>,
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ impl<K> KeyTable<K>
|
|||||||
where
|
where
|
||||||
K: Eq + Hash + Clone,
|
K: Eq + Hash + Clone,
|
||||||
{
|
{
|
||||||
|
#[must_use]
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
forward: HashMap::new(),
|
forward: HashMap::new(),
|
||||||
@@ -54,6 +55,7 @@ where
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
pub fn key(&self, idx: Index) -> Option<&K> {
|
pub fn key(&self, idx: Index) -> Option<&K> {
|
||||||
self.reverse.get(idx.0)
|
self.reverse.get(idx.0)
|
||||||
}
|
}
|
||||||
@@ -62,10 +64,12 @@ where
|
|||||||
self.forward.keys()
|
self.forward.keys()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
pub fn len(&self) -> usize {
|
pub fn len(&self) -> usize {
|
||||||
self.reverse.len()
|
self.reverse.len()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
pub fn is_empty(&self) -> bool {
|
pub fn is_empty(&self) -> bool {
|
||||||
self.reverse.is_empty()
|
self.reverse.is_empty()
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-2
@@ -1,6 +1,6 @@
|
|||||||
//! TrueSkill Through Time — Bayesian skill rating over a time axis.
|
//! `TrueSkill` Through Time — Bayesian skill rating over a time axis.
|
||||||
//!
|
//!
|
||||||
//! Where plain TrueSkill gives each competitor one running estimate, TrueSkill
|
//! Where plain `TrueSkill` gives each competitor one running estimate, `TrueSkill`
|
||||||
//! Through Time treats a whole history as a single model and infers skill *at
|
//! Through Time treats a whole history as a single model and infers skill *at
|
||||||
//! every point in time*. Evidence flows both directions: a result today
|
//! every point in time*. Evidence flows both directions: a result today
|
||||||
//! sharpens the estimate of who someone was last year, so early estimates stop
|
//! sharpens the estimate of who someone was last year, so early estimates stop
|
||||||
@@ -361,6 +361,7 @@ pub(crate) fn sort_time<T: Copy + Ord>(xs: &[T], reverse: bool) -> Vec<usize> {
|
|||||||
/// Panics if fewer than two rating groups are supplied, or if any group is
|
/// Panics if fewer than two rating groups are supplied, or if any group is
|
||||||
/// empty — match quality is a property of a contest between at least two
|
/// empty — match quality is a property of a contest between at least two
|
||||||
/// non-empty sides.
|
/// non-empty sides.
|
||||||
|
#[must_use]
|
||||||
pub fn quality(rating_groups: &[&[Gaussian]], beta: f64) -> f64 {
|
pub fn quality(rating_groups: &[&[Gaussian]], beta: f64) -> f64 {
|
||||||
assert!(
|
assert!(
|
||||||
rating_groups.len() >= 2,
|
rating_groups.len() >= 2,
|
||||||
|
|||||||
@@ -29,7 +29,13 @@ pub enum Outcome {
|
|||||||
impl Outcome {
|
impl Outcome {
|
||||||
/// `n`-team outcome where team `winner` won and everyone else tied for last.
|
/// `n`-team outcome where team `winner` won and everyone else tied for last.
|
||||||
///
|
///
|
||||||
|
/// Note this ties every loser, so for `n >= 3` it needs a positive
|
||||||
|
/// `p_draw` — see `InferenceError::TieWithoutDrawProbability`.
|
||||||
|
///
|
||||||
|
/// # Panics
|
||||||
|
///
|
||||||
/// Panics if `winner >= n`.
|
/// Panics if `winner >= n`.
|
||||||
|
#[must_use]
|
||||||
pub fn winner(winner: u32, n: u32) -> Self {
|
pub fn winner(winner: u32, n: u32) -> Self {
|
||||||
assert!(winner < n, "winner index {winner} out of range 0..{n}");
|
assert!(winner < n, "winner index {winner} out of range 0..{n}");
|
||||||
let ranks: SmallVec<[u32; 4]> = (0..n).map(|i| if i == winner { 0 } else { 1 }).collect();
|
let ranks: SmallVec<[u32; 4]> = (0..n).map(|i| if i == winner { 0 } else { 1 }).collect();
|
||||||
@@ -37,6 +43,7 @@ impl Outcome {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// All `n` teams tied.
|
/// All `n` teams tied.
|
||||||
|
#[must_use]
|
||||||
pub fn draw(n: u32) -> Self {
|
pub fn draw(n: u32) -> Self {
|
||||||
Self::Ranked(SmallVec::from_vec(vec![0; n as usize]))
|
Self::Ranked(SmallVec::from_vec(vec![0; n as usize]))
|
||||||
}
|
}
|
||||||
@@ -68,6 +75,7 @@ impl Outcome {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
pub fn team_count(&self) -> usize {
|
pub fn team_count(&self) -> usize {
|
||||||
match self {
|
match self {
|
||||||
Self::Ranked(r) => r.len(),
|
Self::Ranked(r) => r.len(),
|
||||||
|
|||||||
+2
-2
@@ -1,7 +1,7 @@
|
|||||||
//! Schedule trait and built-in implementations.
|
//! Schedule trait and built-in implementations.
|
||||||
//!
|
//!
|
||||||
//! A schedule drives factor propagation to convergence. The default
|
//! A schedule drives factor propagation to convergence. The default
|
||||||
//! `EpsilonOrMax` performs one TeamSum sweep (setup) then alternating
|
//! `EpsilonOrMax` performs one `TeamSum` sweep (setup) then alternating
|
||||||
//! forward/backward sweeps over the iterating factors until the max
|
//! forward/backward sweeps over the iterating factors until the max
|
||||||
//! delta drops below epsilon or `max` iterations is reached.
|
//! delta drops below epsilon or `max` iterations is reached.
|
||||||
|
|
||||||
@@ -23,7 +23,7 @@ pub trait Schedule: Send + Sync {
|
|||||||
/// Default schedule: sweep forward then backward until step ≤ eps or iter == max.
|
/// Default schedule: sweep forward then backward until step ≤ eps or iter == max.
|
||||||
///
|
///
|
||||||
/// Matches the existing `Game::likelihoods` loop bit-for-bit when given the
|
/// Matches the existing `Game::likelihoods` loop bit-for-bit when given the
|
||||||
/// same factor layout (TeamSums first, then alternating RankDiff/Trunc pairs).
|
/// same factor layout (`TeamSums` first, then alternating RankDiff/Trunc pairs).
|
||||||
#[derive(Debug, Clone, Copy)]
|
#[derive(Debug, Clone, Copy)]
|
||||||
pub struct EpsilonOrMax {
|
pub struct EpsilonOrMax {
|
||||||
pub eps: f64,
|
pub eps: f64,
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ use crate::{Index, competitor::Competitor, drift::Drift, time::Time};
|
|||||||
|
|
||||||
/// Dense Vec-backed store for competitor state in History.
|
/// Dense Vec-backed store for competitor state in History.
|
||||||
///
|
///
|
||||||
/// Indexed directly by Index.0, eliminating HashMap hashing in the
|
/// Indexed directly by Index.0, eliminating `HashMap` hashing in the
|
||||||
/// forward/backward sweep. Uses `Vec<Option<Competitor<T, D>>>` so slots can be
|
/// forward/backward sweep. Uses `Vec<Option<Competitor<T, D>>>` so slots can be
|
||||||
/// absent without an explicit present mask.
|
/// absent without an explicit present mask.
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
@@ -21,6 +21,7 @@ impl<T: Time, D: Drift<T>> Default for CompetitorStore<T, D> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl<T: Time, D: Drift<T>> CompetitorStore<T, D> {
|
impl<T: Time, D: Drift<T>> CompetitorStore<T, D> {
|
||||||
|
#[must_use]
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self::default()
|
Self::default()
|
||||||
}
|
}
|
||||||
@@ -39,6 +40,7 @@ impl<T: Time, D: Drift<T>> CompetitorStore<T, D> {
|
|||||||
self.competitors[idx.0] = Some(competitor);
|
self.competitors[idx.0] = Some(competitor);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
pub fn get(&self, idx: Index) -> Option<&Competitor<T, D>> {
|
pub fn get(&self, idx: Index) -> Option<&Competitor<T, D>> {
|
||||||
self.competitors.get(idx.0).and_then(|slot| slot.as_ref())
|
self.competitors.get(idx.0).and_then(|slot| slot.as_ref())
|
||||||
}
|
}
|
||||||
@@ -49,14 +51,17 @@ impl<T: Time, D: Drift<T>> CompetitorStore<T, D> {
|
|||||||
.and_then(|slot| slot.as_mut())
|
.and_then(|slot| slot.as_mut())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
pub fn contains(&self, idx: Index) -> bool {
|
pub fn contains(&self, idx: Index) -> bool {
|
||||||
self.get(idx).is_some()
|
self.get(idx).is_some()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
pub fn len(&self) -> usize {
|
pub fn len(&self) -> usize {
|
||||||
self.n_present
|
self.n_present
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
pub fn is_empty(&self) -> bool {
|
pub fn is_empty(&self) -> bool {
|
||||||
self.n_present == 0
|
self.n_present == 0
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
use crate::{Index, time_slice::Skill};
|
use crate::{Index, time_slice::Skill};
|
||||||
|
|
||||||
/// Dense Vec-backed store for per-agent skill state within a TimeSlice.
|
/// Dense Vec-backed store for per-agent skill state within a `TimeSlice`.
|
||||||
///
|
///
|
||||||
/// Indexed directly by Index.0, eliminating HashMap hashing in the inner
|
/// Indexed directly by Index.0, eliminating `HashMap` hashing in the inner
|
||||||
/// convergence loop. Uses a parallel `present` mask so iteration skips
|
/// convergence loop. Uses a parallel `present` mask so iteration skips
|
||||||
/// absent slots without incurring per-slot Option overhead in the hot path.
|
/// absent slots without incurring per-slot Option overhead in the hot path.
|
||||||
#[derive(Debug, Default)]
|
#[derive(Debug, Default)]
|
||||||
|
|||||||
@@ -370,6 +370,13 @@ impl<T: Time> TimeSlice<T> {
|
|||||||
.collect::<HashMap<_, _>>()
|
.collect::<HashMap<_, _>>()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Sweep this slice's events once, starting at index `from`.
|
||||||
|
///
|
||||||
|
/// # Panics
|
||||||
|
///
|
||||||
|
/// Panics if an event references a competitor with no entry in this
|
||||||
|
/// slice's skill store. `add_events` inserts one for every participant, so
|
||||||
|
/// this cannot happen for slices built through the public API.
|
||||||
pub fn iteration<D: Drift<T>>(&mut self, from: usize, agents: &CompetitorStore<T, D>) {
|
pub fn iteration<D: Drift<T>>(&mut self, from: usize, agents: &CompetitorStore<T, D>) {
|
||||||
if from == 0 && self.color_groups_dirty {
|
if from == 0 && self.color_groups_dirty {
|
||||||
self.recompute_color_groups();
|
self.recompute_color_groups();
|
||||||
|
|||||||
Reference in New Issue
Block a user