Merge api/game-rename (#69)
This commit is contained in:
+120
-81
@@ -102,24 +102,51 @@ impl Default for GameOptions {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Owned variant of `Game` returned by public constructors.
|
/// One match, fitted on its own.
|
||||||
///
|
///
|
||||||
/// Unlike `Game<'a, T, D>` (which borrows its result/weights slices from
|
/// Rate a single match against ratings you already hold and read the updated
|
||||||
/// History's internal state), `OwnedGame<T, D>` owns the team ratings, so it
|
/// beliefs straight back. There is no history behind it: nothing is stored,
|
||||||
/// can be returned freely from public constructors. The inference inputs
|
/// nothing propagates backward, and the priors you hand in are the only
|
||||||
/// themselves are not retained — nothing reads them back.
|
/// evidence used. That makes it the wrong tool for the thing this crate exists
|
||||||
|
/// for — [`History`](crate::History) is what infers skill *through time*,
|
||||||
|
/// revising past estimates as later matches arrive, and a sequence of `Game`s
|
||||||
|
/// chained by hand is a forward-only filter, not the same answer.
|
||||||
///
|
///
|
||||||
/// A fitted single match, and nothing more: see [`Game`] for why that is not
|
/// Reach for it when a history would be overkill or unavailable: a one-off
|
||||||
/// the same as a step of a [`History`](crate::History).
|
/// matchup, replaying a rating step from stored numbers, checking the engine
|
||||||
|
/// against a reference, or a caller that keeps its own persistence and only
|
||||||
|
/// wants the update rule.
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use trueskill_tt::{ConstantDrift, Game, GameOptions, Gaussian, Outcome, Rating};
|
||||||
|
///
|
||||||
|
/// let strong: Rating = Rating::new(Gaussian::from_ms(30.0, 3.0), 1.0, ConstantDrift::new(0.0));
|
||||||
|
/// let weak: Rating = Rating::new(Gaussian::from_ms(20.0, 3.0), 1.0, ConstantDrift::new(0.0));
|
||||||
|
///
|
||||||
|
/// // The underdog wins.
|
||||||
|
/// let game = Game::ranked(
|
||||||
|
/// &[&[weak], &[strong]],
|
||||||
|
/// Outcome::winner(0, 2),
|
||||||
|
/// &GameOptions::default(),
|
||||||
|
/// )?;
|
||||||
|
///
|
||||||
|
/// let posteriors = game.posteriors();
|
||||||
|
/// assert!(posteriors[0][0].mu() > weak.prior().mu(), "the winner gained");
|
||||||
|
/// assert!(posteriors[1][0].mu() < strong.prior().mu(), "the loser lost");
|
||||||
|
///
|
||||||
|
/// // An upset is improbable, and `log_evidence` says so.
|
||||||
|
/// assert!(game.log_evidence() < 0.5_f64.ln());
|
||||||
|
/// # Ok::<(), trueskill_tt::InferenceError>(())
|
||||||
|
/// ```
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub struct OwnedGame<T: Time, D: Drift<T>> {
|
pub struct Game<T: Time, D: Drift<T>> {
|
||||||
teams: Vec<Vec<Rating<T, D>>>,
|
teams: Vec<Vec<Rating<T, D>>>,
|
||||||
pub(crate) likelihoods: Vec<Vec<Gaussian>>,
|
pub(crate) likelihoods: Vec<Vec<Gaussian>>,
|
||||||
pub(crate) log_evidence: f64,
|
pub(crate) log_evidence: f64,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: Time, D: Drift<T>> OwnedGame<T, D> {
|
impl<T: Time, D: Drift<T>> Game<T, D> {
|
||||||
pub(crate) fn new(
|
pub(crate) fn new(
|
||||||
teams: Vec<Vec<Rating<T, D>>>,
|
teams: Vec<Vec<Rating<T, D>>>,
|
||||||
result: Vec<f64>,
|
result: Vec<f64>,
|
||||||
@@ -131,7 +158,8 @@ impl<T: Time, D: Drift<T>> OwnedGame<T, D> {
|
|||||||
|
|
||||||
// `Game` takes the teams by value and is dropped here, so take the vec
|
// `Game` takes the teams by value and is dropped here, so take the vec
|
||||||
// back out of it rather than handing it a clone.
|
// back out of it rather than handing it a clone.
|
||||||
let g = Game::ranked_with_arena(teams, &result, &weights, p_draw, convergence, &mut arena);
|
let g =
|
||||||
|
GameRef::ranked_with_arena(teams, &result, &weights, p_draw, convergence, &mut arena);
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
teams: g.teams,
|
teams: g.teams,
|
||||||
@@ -149,7 +177,7 @@ impl<T: Time, D: Drift<T>> OwnedGame<T, D> {
|
|||||||
) -> Self {
|
) -> Self {
|
||||||
let mut arena = ScratchArena::new();
|
let mut arena = ScratchArena::new();
|
||||||
|
|
||||||
let g = Game::scored_with_arena(
|
let g = GameRef::scored_with_arena(
|
||||||
teams,
|
teams,
|
||||||
&scores,
|
&scores,
|
||||||
&weights,
|
&weights,
|
||||||
@@ -204,27 +232,15 @@ impl<T: Time, D: Drift<T>> OwnedGame<T, D> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// One match's factor graph, fitted on its own.
|
/// The borrowing form of [`Game`], used only inside the crate.
|
||||||
///
|
///
|
||||||
/// Rate a single match against ratings you already hold and get the updated
|
/// `History` keeps each event's result and weight slices in its own storage
|
||||||
/// beliefs straight back. There is no history behind it: nothing is stored,
|
/// and sweeps them thousands of times, so the inference core borrows them
|
||||||
/// nothing propagates backward, and the priors you hand in are the only
|
/// rather than copying. That borrow is the whole difference between this and
|
||||||
/// evidence used. That makes it the wrong tool for the thing this crate exists
|
/// [`Game`]; it is why this type cannot be handed to a caller, and why it is
|
||||||
/// for — [`History`](crate::History) is what infers skill *through time*,
|
/// not part of the public API.
|
||||||
/// revising past estimates as later matches arrive, and a sequence of `Game`s
|
|
||||||
/// chained by hand is a forward-only filter, not the same answer.
|
|
||||||
///
|
|
||||||
/// Reach for `Game` when a history would be overkill or unavailable: a
|
|
||||||
/// one-off matchup, replaying a rating step from stored numbers, checking the
|
|
||||||
/// engine against a reference, or a caller that keeps its own persistence and
|
|
||||||
/// only wants the update rule.
|
|
||||||
///
|
|
||||||
/// The type is mostly a namespace. Its constructors — [`Game::ranked`],
|
|
||||||
/// [`Game::scored`], [`Game::one_v_one`], [`Game::free_for_all`] — return an
|
|
||||||
/// [`OwnedGame`], because `Game<'a, …>` borrows the result and weight slices
|
|
||||||
/// that `History` keeps internally and so cannot be handed out.
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct Game<'a, T: Time = i64, D: Drift<T> = crate::drift::ConstantDrift> {
|
pub(crate) struct GameRef<'a, T: Time = i64, D: Drift<T> = crate::drift::ConstantDrift> {
|
||||||
teams: Vec<Vec<Rating<T, D>>>,
|
teams: Vec<Vec<Rating<T, D>>>,
|
||||||
result: &'a [f64],
|
result: &'a [f64],
|
||||||
weights: &'a [Vec<f64>],
|
weights: &'a [Vec<f64>],
|
||||||
@@ -234,7 +250,7 @@ pub struct Game<'a, T: Time = i64, D: Drift<T> = crate::drift::ConstantDrift> {
|
|||||||
pub(crate) log_evidence: f64,
|
pub(crate) log_evidence: f64,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a, T: Time, D: Drift<T>> Game<'a, T, D> {
|
impl<'a, T: Time, D: Drift<T>> GameRef<'a, T, D> {
|
||||||
pub(crate) fn ranked_with_arena(
|
pub(crate) fn ranked_with_arena(
|
||||||
teams: Vec<Vec<Rating<T, D>>>,
|
teams: Vec<Vec<Rating<T, D>>>,
|
||||||
result: &'a [f64],
|
result: &'a [f64],
|
||||||
@@ -476,11 +492,12 @@ impl<'a, T: Time, D: Drift<T>> Game<'a, T, D> {
|
|||||||
self.likelihoods = likelihoods;
|
self.likelihoods = likelihoods;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Updated skill belief for every competitor, as `[team][member]` in the
|
/// As [`Game::posteriors`].
|
||||||
/// order the teams and members were passed in — prior times this match's
|
///
|
||||||
/// likelihood, exactly as [`OwnedGame::posteriors`].
|
/// Test-only: inference reads `likelihoods` directly, and `GameRef` is not
|
||||||
#[must_use]
|
/// public, so the only callers are this module's own goldens.
|
||||||
pub fn posteriors(&self) -> Vec<Vec<Gaussian>> {
|
#[cfg(test)]
|
||||||
|
pub(crate) fn posteriors(&self) -> Vec<Vec<Gaussian>> {
|
||||||
self.likelihoods
|
self.likelihoods
|
||||||
.iter()
|
.iter()
|
||||||
.zip(self.teams.iter())
|
.zip(self.teams.iter())
|
||||||
@@ -492,16 +509,9 @@ impl<'a, T: Time, D: Drift<T>> Game<'a, T, D> {
|
|||||||
})
|
})
|
||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Natural log of how probable this outcome was under the priors, summed
|
|
||||||
/// over the diff chain's links — as [`OwnedGame::log_evidence`].
|
|
||||||
#[must_use]
|
|
||||||
pub fn log_evidence(&self) -> f64 {
|
|
||||||
self.log_evidence
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: Time, D: Drift<T>> Game<'_, T, D> {
|
impl<T: Time, D: Drift<T>> Game<T, D> {
|
||||||
/// Reject the team shapes inference cannot represent.
|
/// Reject the team shapes inference cannot represent.
|
||||||
///
|
///
|
||||||
/// `run_chain` builds one diff link per adjacent pair of teams, so fewer
|
/// `run_chain` builds one diff link per adjacent pair of teams, so fewer
|
||||||
@@ -525,6 +535,12 @@ impl<T: Time, D: Drift<T>> Game<'_, T, D> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Fit one match from an ordinal result.
|
||||||
|
///
|
||||||
|
/// `teams` is `[team][member]`, and `outcome` ranks those teams in the
|
||||||
|
/// same order. Read the result with [`posteriors`](Game::posteriors) and
|
||||||
|
/// [`log_evidence`](Game::log_evidence).
|
||||||
|
///
|
||||||
/// # Errors
|
/// # Errors
|
||||||
///
|
///
|
||||||
/// - `InvalidParameter` if `options.convergence` is out of range — an
|
/// - `InvalidParameter` if `options.convergence` is out of range — an
|
||||||
@@ -542,7 +558,7 @@ impl<T: Time, D: Drift<T>> Game<'_, T, D> {
|
|||||||
teams: &[&[Rating<T, D>]],
|
teams: &[&[Rating<T, D>]],
|
||||||
outcome: crate::Outcome,
|
outcome: crate::Outcome,
|
||||||
options: &GameOptions,
|
options: &GameOptions,
|
||||||
) -> Result<OwnedGame<T, D>, crate::InferenceError> {
|
) -> Result<Self, crate::InferenceError> {
|
||||||
options.convergence.validate()?;
|
options.convergence.validate()?;
|
||||||
Self::validate_teams(teams)?;
|
Self::validate_teams(teams)?;
|
||||||
if !(0.0..1.0).contains(&options.p_draw) {
|
if !(0.0..1.0).contains(&options.p_draw) {
|
||||||
@@ -581,7 +597,7 @@ impl<T: Time, D: Drift<T>> Game<'_, T, D> {
|
|||||||
let teams_owned: Vec<Vec<Rating<T, D>>> = teams.iter().map(|t| t.to_vec()).collect();
|
let teams_owned: Vec<Vec<Rating<T, D>>> = teams.iter().map(|t| t.to_vec()).collect();
|
||||||
let weights: Vec<Vec<f64>> = teams.iter().map(|t| vec![1.0; t.len()]).collect();
|
let weights: Vec<Vec<f64>> = teams.iter().map(|t| vec![1.0; t.len()]).collect();
|
||||||
|
|
||||||
Ok(OwnedGame::new(
|
Ok(Self::new(
|
||||||
teams_owned,
|
teams_owned,
|
||||||
result,
|
result,
|
||||||
weights,
|
weights,
|
||||||
@@ -590,6 +606,12 @@ impl<T: Time, D: Drift<T>> Game<'_, T, D> {
|
|||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Fit one match from continuous scores.
|
||||||
|
///
|
||||||
|
/// Unlike [`ranked`](Game::ranked), the *size* of each adjacent gap is
|
||||||
|
/// evidence: beating a team by ten says more than beating them by one.
|
||||||
|
/// How much more is set by `options.score_sigma`.
|
||||||
|
///
|
||||||
/// # Errors
|
/// # Errors
|
||||||
///
|
///
|
||||||
/// - `InvalidParameter` if `options.score_sigma` is not strictly positive
|
/// - `InvalidParameter` if `options.score_sigma` is not strictly positive
|
||||||
@@ -602,7 +624,7 @@ impl<T: Time, D: Drift<T>> Game<'_, T, D> {
|
|||||||
teams: &[&[Rating<T, D>]],
|
teams: &[&[Rating<T, D>]],
|
||||||
outcome: crate::Outcome,
|
outcome: crate::Outcome,
|
||||||
options: &GameOptions,
|
options: &GameOptions,
|
||||||
) -> Result<OwnedGame<T, D>, crate::InferenceError> {
|
) -> Result<Self, crate::InferenceError> {
|
||||||
options.convergence.validate()?;
|
options.convergence.validate()?;
|
||||||
Self::validate_teams(teams)?;
|
Self::validate_teams(teams)?;
|
||||||
if options.score_sigma <= 0.0 || options.score_sigma.is_nan() {
|
if options.score_sigma <= 0.0 || options.score_sigma.is_nan() {
|
||||||
@@ -638,7 +660,7 @@ impl<T: Time, D: Drift<T>> Game<'_, T, D> {
|
|||||||
}
|
}
|
||||||
let teams_owned: Vec<Vec<Rating<T, D>>> = teams.iter().map(|t| t.to_vec()).collect();
|
let teams_owned: Vec<Vec<Rating<T, D>>> = teams.iter().map(|t| t.to_vec()).collect();
|
||||||
let weights: Vec<Vec<f64>> = teams.iter().map(|t| vec![1.0; t.len()]).collect();
|
let weights: Vec<Vec<f64>> = teams.iter().map(|t| vec![1.0; t.len()]).collect();
|
||||||
Ok(OwnedGame::new_scored(
|
Ok(Self::new_scored(
|
||||||
teams_owned,
|
teams_owned,
|
||||||
scores,
|
scores,
|
||||||
weights,
|
weights,
|
||||||
@@ -647,7 +669,24 @@ impl<T: Time, D: Drift<T>> Game<'_, T, D> {
|
|||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Convenience wrapper over [`Game::ranked`] for two single-competitor teams.
|
/// Two single-competitor teams: the common case, without the nesting.
|
||||||
|
///
|
||||||
|
/// Returns a `Game` like every other constructor. It used to return
|
||||||
|
/// `(Gaussian, Gaussian)` — the posteriors alone — which made it the one
|
||||||
|
/// member of the family you could not ask for
|
||||||
|
/// [`log_evidence`](Game::log_evidence). Call `.posteriors()` for the old
|
||||||
|
/// shape:
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// # use trueskill_tt::{ConstantDrift, Game, GameOptions, Gaussian, Outcome, Rating};
|
||||||
|
/// # let a: Rating = Rating::new(Gaussian::from_ms(25.0, 8.0), 4.0, ConstantDrift::new(0.0));
|
||||||
|
/// # let b = a;
|
||||||
|
/// let game = Game::one_v_one(&a, &b, Outcome::winner(0, 2), &GameOptions::default())?;
|
||||||
|
/// let post = game.posteriors();
|
||||||
|
/// let (a_post, b_post) = (post[0][0], post[1][0]);
|
||||||
|
/// # let _ = (a_post, b_post);
|
||||||
|
/// # Ok::<(), trueskill_tt::InferenceError>(())
|
||||||
|
/// ```
|
||||||
///
|
///
|
||||||
/// # Errors
|
/// # Errors
|
||||||
///
|
///
|
||||||
@@ -659,12 +698,12 @@ impl<T: Time, D: Drift<T>> Game<'_, T, D> {
|
|||||||
b: &Rating<T, D>,
|
b: &Rating<T, D>,
|
||||||
outcome: crate::Outcome,
|
outcome: crate::Outcome,
|
||||||
options: &GameOptions,
|
options: &GameOptions,
|
||||||
) -> Result<(Gaussian, Gaussian), crate::InferenceError> {
|
) -> Result<Self, crate::InferenceError> {
|
||||||
let game = Self::ranked(&[&[*a], &[*b]], outcome, options)?;
|
Self::ranked(&[&[*a], &[*b]], outcome, options)
|
||||||
let post = game.posteriors();
|
|
||||||
Ok((post[0][0], post[1][0]))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A free-for-all: every competitor is their own one-member team.
|
||||||
|
///
|
||||||
/// # Errors
|
/// # Errors
|
||||||
///
|
///
|
||||||
/// Wraps each competitor in a one-member team and delegates to
|
/// Wraps each competitor in a one-member team and delegates to
|
||||||
@@ -673,7 +712,7 @@ impl<T: Time, D: Drift<T>> Game<'_, T, D> {
|
|||||||
competitors: &[&Rating<T, D>],
|
competitors: &[&Rating<T, D>],
|
||||||
outcome: crate::Outcome,
|
outcome: crate::Outcome,
|
||||||
options: &GameOptions,
|
options: &GameOptions,
|
||||||
) -> Result<OwnedGame<T, D>, crate::InferenceError> {
|
) -> Result<Self, crate::InferenceError> {
|
||||||
let teams: Vec<Vec<Rating<T, D>>> = competitors.iter().map(|p| vec![**p]).collect();
|
let teams: Vec<Vec<Rating<T, D>>> = competitors.iter().map(|p| vec![**p]).collect();
|
||||||
let team_refs: Vec<&[Rating<T, D>]> = teams.iter().map(|t| t.as_slice()).collect();
|
let team_refs: Vec<&[Rating<T, D>]> = teams.iter().map(|t| t.as_slice()).collect();
|
||||||
Self::ranked(&team_refs, outcome, options)
|
Self::ranked(&team_refs, outcome, options)
|
||||||
@@ -703,7 +742,7 @@ mod tests {
|
|||||||
);
|
);
|
||||||
|
|
||||||
let w = [vec![1.0], vec![1.0]];
|
let w = [vec![1.0], vec![1.0]];
|
||||||
let g = Game::ranked_with_arena(
|
let g = GameRef::ranked_with_arena(
|
||||||
vec![vec![t_a], vec![t_b]],
|
vec![vec![t_a], vec![t_b]],
|
||||||
&[0.0, 1.0],
|
&[0.0, 1.0],
|
||||||
&w,
|
&w,
|
||||||
@@ -731,7 +770,7 @@ mod tests {
|
|||||||
);
|
);
|
||||||
|
|
||||||
let w = [vec![1.0], vec![1.0]];
|
let w = [vec![1.0], vec![1.0]];
|
||||||
let g = Game::ranked_with_arena(
|
let g = GameRef::ranked_with_arena(
|
||||||
vec![vec![t_a], vec![t_b]],
|
vec![vec![t_a], vec![t_b]],
|
||||||
&[0.0, 1.0],
|
&[0.0, 1.0],
|
||||||
&w,
|
&w,
|
||||||
@@ -759,7 +798,7 @@ mod tests {
|
|||||||
);
|
);
|
||||||
|
|
||||||
let w = [vec![1.0], vec![1.0]];
|
let w = [vec![1.0], vec![1.0]];
|
||||||
let g = Game::ranked_with_arena(
|
let g = GameRef::ranked_with_arena(
|
||||||
vec![vec![t_a], vec![t_b]],
|
vec![vec![t_a], vec![t_b]],
|
||||||
&[0.0, 1.0],
|
&[0.0, 1.0],
|
||||||
&w,
|
&w,
|
||||||
@@ -793,7 +832,7 @@ mod tests {
|
|||||||
];
|
];
|
||||||
|
|
||||||
let w = [vec![1.0], vec![1.0], vec![1.0]];
|
let w = [vec![1.0], vec![1.0], vec![1.0]];
|
||||||
let g = Game::ranked_with_arena(
|
let g = GameRef::ranked_with_arena(
|
||||||
teams.clone(),
|
teams.clone(),
|
||||||
&[1.0, 2.0, 0.0],
|
&[1.0, 2.0, 0.0],
|
||||||
&w,
|
&w,
|
||||||
@@ -810,7 +849,7 @@ mod tests {
|
|||||||
assert_ulps_eq!(b, Gaussian::from_ms(31.311358, 6.698818), epsilon = 1e-6);
|
assert_ulps_eq!(b, Gaussian::from_ms(31.311358, 6.698818), epsilon = 1e-6);
|
||||||
|
|
||||||
let w = [vec![1.0], vec![1.0], vec![1.0]];
|
let w = [vec![1.0], vec![1.0], vec![1.0]];
|
||||||
let g = Game::ranked_with_arena(
|
let g = GameRef::ranked_with_arena(
|
||||||
teams.clone(),
|
teams.clone(),
|
||||||
&[2.0, 1.0, 0.0],
|
&[2.0, 1.0, 0.0],
|
||||||
&w,
|
&w,
|
||||||
@@ -827,7 +866,7 @@ mod tests {
|
|||||||
assert_ulps_eq!(b, Gaussian::from_ms(25.000000, 6.238469), epsilon = 1e-6);
|
assert_ulps_eq!(b, Gaussian::from_ms(25.000000, 6.238469), epsilon = 1e-6);
|
||||||
|
|
||||||
let w = [vec![1.0], vec![1.0], vec![1.0]];
|
let w = [vec![1.0], vec![1.0], vec![1.0]];
|
||||||
let g = Game::ranked_with_arena(
|
let g = GameRef::ranked_with_arena(
|
||||||
teams,
|
teams,
|
||||||
&[1.0, 2.0, 0.0],
|
&[1.0, 2.0, 0.0],
|
||||||
&w,
|
&w,
|
||||||
@@ -867,7 +906,7 @@ mod tests {
|
|||||||
);
|
);
|
||||||
|
|
||||||
let w = [vec![1.0], vec![1.0]];
|
let w = [vec![1.0], vec![1.0]];
|
||||||
let g = Game::ranked_with_arena(
|
let g = GameRef::ranked_with_arena(
|
||||||
vec![vec![t_a], vec![t_b]],
|
vec![vec![t_a], vec![t_b]],
|
||||||
&[0.0, 0.0],
|
&[0.0, 0.0],
|
||||||
&w,
|
&w,
|
||||||
@@ -899,7 +938,7 @@ mod tests {
|
|||||||
);
|
);
|
||||||
|
|
||||||
let w = [vec![1.0], vec![1.0]];
|
let w = [vec![1.0], vec![1.0]];
|
||||||
let g = Game::ranked_with_arena(
|
let g = GameRef::ranked_with_arena(
|
||||||
vec![vec![t_a], vec![t_b]],
|
vec![vec![t_a], vec![t_b]],
|
||||||
&[0.0, 0.0],
|
&[0.0, 0.0],
|
||||||
&w,
|
&w,
|
||||||
@@ -935,7 +974,7 @@ mod tests {
|
|||||||
);
|
);
|
||||||
|
|
||||||
let w = [vec![1.0], vec![1.0], vec![1.0]];
|
let w = [vec![1.0], vec![1.0], vec![1.0]];
|
||||||
let g = Game::ranked_with_arena(
|
let g = GameRef::ranked_with_arena(
|
||||||
vec![vec![t_a], vec![t_b], vec![t_c]],
|
vec![vec![t_a], vec![t_b], vec![t_c]],
|
||||||
&[0.0, 0.0, 0.0],
|
&[0.0, 0.0, 0.0],
|
||||||
&w,
|
&w,
|
||||||
@@ -972,7 +1011,7 @@ mod tests {
|
|||||||
);
|
);
|
||||||
|
|
||||||
let w = [vec![1.0], vec![1.0], vec![1.0]];
|
let w = [vec![1.0], vec![1.0], vec![1.0]];
|
||||||
let g = Game::ranked_with_arena(
|
let g = GameRef::ranked_with_arena(
|
||||||
vec![vec![t_a], vec![t_b], vec![t_c]],
|
vec![vec![t_a], vec![t_b], vec![t_c]],
|
||||||
&[0.0, 0.0, 0.0],
|
&[0.0, 0.0, 0.0],
|
||||||
&w,
|
&w,
|
||||||
@@ -1024,7 +1063,7 @@ mod tests {
|
|||||||
];
|
];
|
||||||
|
|
||||||
let w = [vec![1.0, 1.0], vec![1.0], vec![1.0, 1.0]];
|
let w = [vec![1.0, 1.0], vec![1.0], vec![1.0, 1.0]];
|
||||||
let g = Game::ranked_with_arena(
|
let g = GameRef::ranked_with_arena(
|
||||||
vec![t_a, t_b, t_c],
|
vec![t_a, t_b, t_c],
|
||||||
&[1.0, 0.0, 0.0],
|
&[1.0, 0.0, 0.0],
|
||||||
&w,
|
&w,
|
||||||
@@ -1058,7 +1097,7 @@ mod tests {
|
|||||||
)];
|
)];
|
||||||
|
|
||||||
let w = [w_a, w_b];
|
let w = [w_a, w_b];
|
||||||
let g = Game::ranked_with_arena(
|
let g = GameRef::ranked_with_arena(
|
||||||
vec![t_a.clone(), t_b.clone()],
|
vec![t_a.clone(), t_b.clone()],
|
||||||
&[1.0, 0.0],
|
&[1.0, 0.0],
|
||||||
&w,
|
&w,
|
||||||
@@ -1083,7 +1122,7 @@ mod tests {
|
|||||||
let w_b = vec![0.7];
|
let w_b = vec![0.7];
|
||||||
|
|
||||||
let w = [w_a, w_b];
|
let w = [w_a, w_b];
|
||||||
let g = Game::ranked_with_arena(
|
let g = GameRef::ranked_with_arena(
|
||||||
vec![t_a.clone(), t_b.clone()],
|
vec![t_a.clone(), t_b.clone()],
|
||||||
&[1.0, 0.0],
|
&[1.0, 0.0],
|
||||||
&w,
|
&w,
|
||||||
@@ -1108,7 +1147,7 @@ mod tests {
|
|||||||
let w_b = vec![0.7];
|
let w_b = vec![0.7];
|
||||||
|
|
||||||
let w = [w_a, w_b];
|
let w = [w_a, w_b];
|
||||||
let g = Game::ranked_with_arena(
|
let g = GameRef::ranked_with_arena(
|
||||||
vec![t_a, t_b],
|
vec![t_a, t_b],
|
||||||
&[1.0, 0.0],
|
&[1.0, 0.0],
|
||||||
&w,
|
&w,
|
||||||
@@ -1144,7 +1183,7 @@ mod tests {
|
|||||||
)];
|
)];
|
||||||
|
|
||||||
let w = [w_a, w_b];
|
let w = [w_a, w_b];
|
||||||
let g = Game::ranked_with_arena(
|
let g = GameRef::ranked_with_arena(
|
||||||
vec![t_a, t_b],
|
vec![t_a, t_b],
|
||||||
&[1.0, 0.0],
|
&[1.0, 0.0],
|
||||||
&w,
|
&w,
|
||||||
@@ -1180,7 +1219,7 @@ mod tests {
|
|||||||
)];
|
)];
|
||||||
|
|
||||||
let w = [w_a, w_b];
|
let w = [w_a, w_b];
|
||||||
let g = Game::ranked_with_arena(
|
let g = GameRef::ranked_with_arena(
|
||||||
vec![t_a, t_b],
|
vec![t_a, t_b],
|
||||||
&[1.0, 0.0],
|
&[1.0, 0.0],
|
||||||
&w,
|
&w,
|
||||||
@@ -1226,7 +1265,7 @@ mod tests {
|
|||||||
let result = vec![10.0, 0.0]; // a beat b by 10
|
let result = vec![10.0, 0.0]; // a beat b by 10
|
||||||
let weights = [vec![1.0], vec![1.0]];
|
let weights = [vec![1.0], vec![1.0]];
|
||||||
let mut arena = ScratchArena::new();
|
let mut arena = ScratchArena::new();
|
||||||
let g = Game::scored_with_arena(
|
let g = GameRef::scored_with_arena(
|
||||||
teams,
|
teams,
|
||||||
&result,
|
&result,
|
||||||
&weights,
|
&weights,
|
||||||
@@ -1246,7 +1285,7 @@ mod tests {
|
|||||||
|
|
||||||
// Tighter score_sigma should produce a stronger update.
|
// Tighter score_sigma should produce a stronger update.
|
||||||
let mut arena2 = ScratchArena::new();
|
let mut arena2 = ScratchArena::new();
|
||||||
let g_tight = Game::scored_with_arena(
|
let g_tight = GameRef::scored_with_arena(
|
||||||
vec![vec![prior], vec![prior]],
|
vec![vec![prior], vec![prior]],
|
||||||
&result,
|
&result,
|
||||||
&weights,
|
&weights,
|
||||||
@@ -1357,7 +1396,7 @@ mod tests {
|
|||||||
let w_b = vec![0.9, 0.6];
|
let w_b = vec![0.9, 0.6];
|
||||||
|
|
||||||
let w = [w_a, w_b];
|
let w = [w_a, w_b];
|
||||||
let g = Game::ranked_with_arena(
|
let g = GameRef::ranked_with_arena(
|
||||||
vec![t_a.clone(), t_b.clone()],
|
vec![t_a.clone(), t_b.clone()],
|
||||||
&[1.0, 0.0],
|
&[1.0, 0.0],
|
||||||
&w,
|
&w,
|
||||||
@@ -1392,7 +1431,7 @@ mod tests {
|
|||||||
let w_b = vec![0.7, 0.4];
|
let w_b = vec![0.7, 0.4];
|
||||||
|
|
||||||
let w = [w_a, w_b];
|
let w = [w_a, w_b];
|
||||||
let g = Game::ranked_with_arena(
|
let g = GameRef::ranked_with_arena(
|
||||||
vec![t_a.clone(), t_b.clone()],
|
vec![t_a.clone(), t_b.clone()],
|
||||||
&[1.0, 0.0],
|
&[1.0, 0.0],
|
||||||
&w,
|
&w,
|
||||||
@@ -1427,7 +1466,7 @@ mod tests {
|
|||||||
let w_b = vec![0.7, 2.4];
|
let w_b = vec![0.7, 2.4];
|
||||||
|
|
||||||
let w = [w_a, w_b];
|
let w = [w_a, w_b];
|
||||||
let g = Game::ranked_with_arena(
|
let g = GameRef::ranked_with_arena(
|
||||||
vec![t_a.clone(), t_b.clone()],
|
vec![t_a.clone(), t_b.clone()],
|
||||||
&[1.0, 0.0],
|
&[1.0, 0.0],
|
||||||
&w,
|
&w,
|
||||||
@@ -1459,7 +1498,7 @@ mod tests {
|
|||||||
);
|
);
|
||||||
|
|
||||||
let w = [vec![1.0, 1.0], vec![1.0]];
|
let w = [vec![1.0, 1.0], vec![1.0]];
|
||||||
let g = Game::ranked_with_arena(
|
let g = GameRef::ranked_with_arena(
|
||||||
vec![
|
vec![
|
||||||
t_a.clone(),
|
t_a.clone(),
|
||||||
vec![R::new(
|
vec![R::new(
|
||||||
@@ -1480,7 +1519,7 @@ mod tests {
|
|||||||
let w_b = vec![1.0, 0.0];
|
let w_b = vec![1.0, 0.0];
|
||||||
|
|
||||||
let w = [w_a, w_b];
|
let w = [w_a, w_b];
|
||||||
let g = Game::ranked_with_arena(
|
let g = GameRef::ranked_with_arena(
|
||||||
vec![t_a, t_b.clone()],
|
vec![t_a, t_b.clone()],
|
||||||
&[1.0, 0.0],
|
&[1.0, 0.0],
|
||||||
&w,
|
&w,
|
||||||
@@ -1505,7 +1544,7 @@ mod tests {
|
|||||||
|
|
||||||
// Capped at 1 iteration: cannot fully propagate down a 4-team chain.
|
// Capped at 1 iteration: cannot fully propagate down a 4-team chain.
|
||||||
let mut arena = ScratchArena::new();
|
let mut arena = ScratchArena::new();
|
||||||
let g_capped = Game::ranked_with_arena(
|
let g_capped = GameRef::ranked_with_arena(
|
||||||
teams.clone(),
|
teams.clone(),
|
||||||
&result,
|
&result,
|
||||||
&weights,
|
&weights,
|
||||||
@@ -1520,7 +1559,7 @@ mod tests {
|
|||||||
|
|
||||||
// Same inputs, plenty of iterations: fully converged.
|
// Same inputs, plenty of iterations: fully converged.
|
||||||
let mut arena = ScratchArena::new();
|
let mut arena = ScratchArena::new();
|
||||||
let g_full = Game::ranked_with_arena(
|
let g_full = GameRef::ranked_with_arena(
|
||||||
teams,
|
teams,
|
||||||
&result,
|
&result,
|
||||||
&weights,
|
&weights,
|
||||||
@@ -1552,7 +1591,7 @@ mod tests {
|
|||||||
let weights = vec![vec![1.0]; 4];
|
let weights = vec![vec![1.0]; 4];
|
||||||
|
|
||||||
let mut arena = ScratchArena::new();
|
let mut arena = ScratchArena::new();
|
||||||
let g_undamped = Game::ranked_with_arena(
|
let g_undamped = GameRef::ranked_with_arena(
|
||||||
teams.clone(),
|
teams.clone(),
|
||||||
&result,
|
&result,
|
||||||
&weights,
|
&weights,
|
||||||
@@ -1564,7 +1603,7 @@ mod tests {
|
|||||||
|
|
||||||
// alpha=0.5 with extra iterations: should reach the same fixed point.
|
// alpha=0.5 with extra iterations: should reach the same fixed point.
|
||||||
let mut arena = ScratchArena::new();
|
let mut arena = ScratchArena::new();
|
||||||
let g_damped = Game::ranked_with_arena(
|
let g_damped = GameRef::ranked_with_arena(
|
||||||
teams,
|
teams,
|
||||||
&result,
|
&result,
|
||||||
&weights,
|
&weights,
|
||||||
|
|||||||
+3
-3
@@ -3053,8 +3053,8 @@ mod tests {
|
|||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::{
|
use crate::{
|
||||||
ConstantDrift, EPSILON, Event, Game, Gaussian, Member, Outcome, P_DRAW, Team,
|
ConstantDrift, EPSILON, Event, Gaussian, Member, Outcome, P_DRAW, Team,
|
||||||
arena::ScratchArena,
|
arena::ScratchArena, game::GameRef,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// #17: a slice's footprint must be O(competitors in the slice), not
|
/// #17: a slice's footprint must be O(competitors in the slice), not
|
||||||
@@ -3170,7 +3170,7 @@ mod tests {
|
|||||||
let observed = h.time_slices[1].skills.get(a).unwrap().posterior();
|
let observed = h.time_slices[1].skills.get(a).unwrap().posterior();
|
||||||
|
|
||||||
let w = [vec![1.0], vec![1.0]];
|
let w = [vec![1.0], vec![1.0]];
|
||||||
let p = Game::ranked_with_arena(
|
let p = GameRef::ranked_with_arena(
|
||||||
h.time_slices[1].events[0].within_priors(
|
h.time_slices[1].events[0].within_priors(
|
||||||
false,
|
false,
|
||||||
&h.time_slices[1].skills,
|
&h.time_slices[1].skills,
|
||||||
|
|||||||
+1
-1
@@ -154,7 +154,7 @@ pub use drift::{ConstantDrift, Drift};
|
|||||||
pub use error::{InferenceError, UnknownKeys};
|
pub use error::{InferenceError, UnknownKeys};
|
||||||
pub use event::{Event, Member, Team};
|
pub use event::{Event, Member, Team};
|
||||||
pub use event_builder::EventBuilder;
|
pub use event_builder::EventBuilder;
|
||||||
pub use game::{Game, GameOptions, OwnedGame};
|
pub use game::{Game, GameOptions};
|
||||||
pub use gaussian::Gaussian;
|
pub use gaussian::Gaussian;
|
||||||
pub use history::{History, HistoryBuilder, Joint};
|
pub use history::{History, HistoryBuilder, Joint};
|
||||||
use matrix::Matrix;
|
use matrix::Matrix;
|
||||||
|
|||||||
+14
-9
@@ -9,7 +9,7 @@ use crate::{
|
|||||||
arena::ScratchArena,
|
arena::ScratchArena,
|
||||||
color_group::ColorGroups,
|
color_group::ColorGroups,
|
||||||
drift::Drift,
|
drift::Drift,
|
||||||
game::Game,
|
game::GameRef,
|
||||||
gaussian::Gaussian,
|
gaussian::Gaussian,
|
||||||
rating::Rating,
|
rating::Rating,
|
||||||
storage::{CompetitorStore, SkillStore},
|
storage::{CompetitorStore, SkillStore},
|
||||||
@@ -141,10 +141,15 @@ impl Event {
|
|||||||
let teams = self.within_priors(false, skills, competitors);
|
let teams = self.within_priors(false, skills, competitors);
|
||||||
let result = self.outputs();
|
let result = self.outputs();
|
||||||
let g = match self.kind {
|
let g = match self.kind {
|
||||||
EventKind::Ranked => {
|
EventKind::Ranked => GameRef::ranked_with_arena(
|
||||||
Game::ranked_with_arena(teams, &result, &self.weights, p_draw, convergence, arena)
|
teams,
|
||||||
}
|
&result,
|
||||||
EventKind::Scored { score_sigma } => Game::scored_with_arena(
|
&self.weights,
|
||||||
|
p_draw,
|
||||||
|
convergence,
|
||||||
|
arena,
|
||||||
|
),
|
||||||
|
EventKind::Scored { score_sigma } => GameRef::scored_with_arena(
|
||||||
teams,
|
teams,
|
||||||
&result,
|
&result,
|
||||||
&self.weights,
|
&self.weights,
|
||||||
@@ -409,7 +414,7 @@ impl<T: Time> TimeSlice<T> {
|
|||||||
let result = event.outputs();
|
let result = event.outputs();
|
||||||
|
|
||||||
let g = match event.kind {
|
let g = match event.kind {
|
||||||
EventKind::Ranked => Game::ranked_with_arena(
|
EventKind::Ranked => GameRef::ranked_with_arena(
|
||||||
teams,
|
teams,
|
||||||
&result,
|
&result,
|
||||||
&event.weights,
|
&event.weights,
|
||||||
@@ -417,7 +422,7 @@ impl<T: Time> TimeSlice<T> {
|
|||||||
self.convergence,
|
self.convergence,
|
||||||
&mut self.arena,
|
&mut self.arena,
|
||||||
),
|
),
|
||||||
EventKind::Scored { score_sigma } => Game::scored_with_arena(
|
EventKind::Scored { score_sigma } => GameRef::scored_with_arena(
|
||||||
teams,
|
teams,
|
||||||
&result,
|
&result,
|
||||||
&event.weights,
|
&event.weights,
|
||||||
@@ -724,7 +729,7 @@ impl<T: Time> TimeSlice<T> {
|
|||||||
let result = event.outputs();
|
let result = event.outputs();
|
||||||
match event.kind {
|
match event.kind {
|
||||||
EventKind::Ranked => {
|
EventKind::Ranked => {
|
||||||
Game::ranked_with_arena(
|
GameRef::ranked_with_arena(
|
||||||
teams,
|
teams,
|
||||||
&result,
|
&result,
|
||||||
&event.weights,
|
&event.weights,
|
||||||
@@ -735,7 +740,7 @@ impl<T: Time> TimeSlice<T> {
|
|||||||
.log_evidence
|
.log_evidence
|
||||||
}
|
}
|
||||||
EventKind::Scored { score_sigma } => {
|
EventKind::Scored { score_sigma } => {
|
||||||
Game::scored_with_arena(
|
GameRef::scored_with_arena(
|
||||||
teams,
|
teams,
|
||||||
&result,
|
&result,
|
||||||
&event.weights,
|
&event.weights,
|
||||||
|
|||||||
@@ -23,8 +23,10 @@ fn ts_rating(mu: f64, sigma: f64, beta: f64, gamma: f64) -> R {
|
|||||||
fn game_1v1_golden_matches_historical() {
|
fn game_1v1_golden_matches_historical() {
|
||||||
let a = ts_rating(25.0, 25.0 / 3.0, 25.0 / 6.0, 25.0 / 300.0);
|
let a = ts_rating(25.0, 25.0 / 3.0, 25.0 / 6.0, 25.0 / 300.0);
|
||||||
let b = ts_rating(25.0, 25.0 / 3.0, 25.0 / 6.0, 25.0 / 300.0);
|
let b = ts_rating(25.0, 25.0 / 3.0, 25.0 / 6.0, 25.0 / 300.0);
|
||||||
let (a_post, b_post) =
|
let post = Game::<i64, _>::one_v_one(&a, &b, Outcome::winner(0, 2), &GameOptions::default())
|
||||||
Game::<i64, _>::one_v_one(&a, &b, Outcome::winner(0, 2), &GameOptions::default()).unwrap();
|
.unwrap()
|
||||||
|
.posteriors();
|
||||||
|
let (a_post, b_post) = (post[0][0], post[1][0]);
|
||||||
// Historical golden from pre-T2 test_1vs1 (team 0 wins):
|
// Historical golden from pre-T2 test_1vs1 (team 0 wins):
|
||||||
assert_ulps_eq!(
|
assert_ulps_eq!(
|
||||||
a_post,
|
a_post,
|
||||||
|
|||||||
+15
-5
@@ -32,10 +32,16 @@ fn game_ranked_1v1_golden() {
|
|||||||
fn game_one_v_one_shortcut() {
|
fn game_one_v_one_shortcut() {
|
||||||
let a = default_rating();
|
let a = default_rating();
|
||||||
let b = default_rating();
|
let b = default_rating();
|
||||||
let (a_post, b_post) =
|
let game =
|
||||||
Game::<i64, _>::one_v_one(&a, &b, Outcome::winner(0, 2), &GameOptions::default()).unwrap();
|
Game::<i64, _>::one_v_one(&a, &b, Outcome::winner(0, 2), &GameOptions::default()).unwrap();
|
||||||
|
let post = game.posteriors();
|
||||||
|
let (a_post, b_post) = (post[0][0], post[1][0]);
|
||||||
assert!(a_post.mu() > 25.0);
|
assert!(a_post.mu() > 25.0);
|
||||||
assert!(b_post.mu() < 25.0);
|
assert!(b_post.mu() < 25.0);
|
||||||
|
|
||||||
|
// It returns a game like every other constructor, so evidence is askable.
|
||||||
|
// Two identical ratings make either result equally likely.
|
||||||
|
assert!((game.log_evidence() - 0.5_f64.ln()).abs() < 1e-12);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -118,8 +124,10 @@ fn one_v_one_honours_the_draw_probability_it_is_given() {
|
|||||||
p_draw: 0.25,
|
p_draw: 0.25,
|
||||||
..GameOptions::default()
|
..GameOptions::default()
|
||||||
};
|
};
|
||||||
let (a_post, b_post) = Game::<i64, _>::one_v_one(&a, &b, Outcome::draw(2), &options)
|
let post = Game::<i64, _>::one_v_one(&a, &b, Outcome::draw(2), &options)
|
||||||
.expect("a draw is representable once p_draw is positive");
|
.expect("a draw is representable once p_draw is positive")
|
||||||
|
.posteriors();
|
||||||
|
let (a_post, b_post) = (post[0][0], post[1][0]);
|
||||||
|
|
||||||
// A symmetric draw leaves the means alone and sharpens both sides.
|
// A symmetric draw leaves the means alone and sharpens both sides.
|
||||||
assert!((a_post.mu() - b_post.mu()).abs() < 1e-9);
|
assert!((a_post.mu() - b_post.mu()).abs() < 1e-9);
|
||||||
@@ -135,8 +143,10 @@ fn one_v_one_honours_convergence_options() {
|
|||||||
convergence: ConvergenceOptions::default(),
|
convergence: ConvergenceOptions::default(),
|
||||||
..GameOptions::default()
|
..GameOptions::default()
|
||||||
};
|
};
|
||||||
let (a_post, _) = Game::<i64, _>::one_v_one(&a, &b, Outcome::winner(0, 2), &options).unwrap();
|
let post = Game::<i64, _>::one_v_one(&a, &b, Outcome::winner(0, 2), &options)
|
||||||
assert!(a_post.mu() > 25.0);
|
.unwrap()
|
||||||
|
.posteriors();
|
||||||
|
assert!(post[0][0].mu() > 25.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// `Game` is a public entry point that does not pass through `History`'s
|
/// `Game` is a public entry point that does not pass through `History`'s
|
||||||
|
|||||||
Reference in New Issue
Block a user