T0 + T1 + T2: engine redesign through new API surface (#1)
Implements tiers T0, T1, T2 of `docs/superpowers/specs/2026-04-23-trueskill-engine-redesign-design.md`. All three tiers have landed together on this branch because they build on one another; this PR rolls them up for a single review pass. Per-tier plans: - T0: `docs/superpowers/plans/2026-04-23-t0-numerical-parity.md` - T1: `docs/superpowers/plans/2026-04-24-t1-factor-graph.md` - T2: `docs/superpowers/plans/2026-04-24-t2-new-api-surface.md` ## Summary ### T0 — Numerical parity (internal) - `Gaussian` switched to natural-parameter storage `(pi, tau)`; mul/div now ~7× faster (218 ps vs 1.57 ns). - `HashMap<Index, _>` → dense `Vec<_>` keyed by `Index.0` (via `AgentStore<D>`, `SkillStore`). - `ScratchArena` eliminates per-event allocations in `Game::likelihoods`. - `InferenceError` seed type added (1 variant). - 38 → 53 tests passing through T1. - Benchmark: `Batch::iteration` 29.84 → 21.25 µs. ### T1 — Factor graph machinery (internal) - `Factor` trait + `BuiltinFactor` enum (TeamSum / RankDiff / Trunc) driving within-game inference. - `VarStore` flat storage for variable marginals. - `Schedule` trait + `EpsilonOrMax` impl replacing the hand-rolled EP loop. - `Game::likelihoods` rebuilt on the factor-graph machinery; iteration counts and goldens preserved to within 1e-6. - 53 tests passing. - Benchmark: `Batch::iteration` 23.01 µs (slight regression absorbed in T2). ### T2 — New API surface (breaking) **Renames:** - `IndexMap → KeyTable`, `Player → Rating`, `Agent → Competitor`, `Batch → TimeSlice` **New types:** - `Time` trait with `Untimed` ZST and `i64` impls; `Drift<T>`, `Rating<T, D>`, `Competitor<T, D>`, `TimeSlice<T>`, `History<T, D, O, K>` all generic. - `Event<T, K>`, `Team<K>`, `Member<K>`, `Outcome` (`Ranked` variant; `#[non_exhaustive]`). - `Observer<T>` trait + `NullObserver`. - `ConvergenceOptions`, `ConvergenceReport`. - `GameOptions`, `OwnedGame<T, D>`. **Three-tier ingestion:** - `history.record_winner(&K, &K, T)` / `record_draw(&K, &K, T)` — 1v1 convenience. - `history.add_events(iter)` — typed bulk. - `history.event(T).team([...]).weights([...]).ranking([...]).commit()` — fluent. **Query API:** `current_skill`, `learning_curve`, `learning_curves` (keyed on `K`), `log_evidence`, `log_evidence_for`, `predict_quality`, `predict_outcome`. **Game constructors:** `ranked`, `one_v_one`, `free_for_all`, `custom` — all returning `Result<_, InferenceError>`. **`factors` module:** `Factor`, `Schedule`, `VarStore`, `VarId`, `BuiltinFactor`, `EpsilonOrMax`, `ScheduleReport`, `TeamSumFactor`, `RankDiffFactor`, `TruncFactor` now public. **Errors:** `InferenceError` gains `MismatchedShape`, `InvalidProbability`, `ConvergenceFailed`; boundary panics converted to `Result`. **Removed (breaking):** `History::convergence(iters, eps, verbose)`, `HistoryBuilder::gamma(f64)`, `HistoryBuilder::time(bool)`, `History.time: bool`, `learning_curves_by_index`, nested-Vec public `add_events`. ## Behavior change (documented in CHANGELOG) `Time = Untimed` has `elapsed_to → 0`, so no drift accumulates between slices. The old `time=false` mode implicitly forced `elapsed=1` on reappearance via an `i64::MAX` sentinel — that quirk is not reproducible under a typed time axis. Tests that depended on it now use `History::<i64, _>` with explicit `1..=n` timestamps. One test (`test_env_ttt`) had 3 Gaussian goldens updated to reflect the corrected semantics; documented in commit `33a7d90`. ## Final numbers | Metric | Before T0 | After T2 | Delta | |---|---|---|---| | `Batch::iteration` | 29.84 µs | 21.36 µs | **-28%** | | `Gaussian::mul` | 1.57 ns | 219 ps | **-86%** | | `Gaussian::div` | 1.57 ns | 219 ps | **-86%** | | Tests passing | 38 | 90 | +52 | All other Gaussian ops unchanged (~219 ps add/sub, ~264 ps pi/tau reads). ## Test plan - [x] `cargo test --features approx` — 90/90 pass (68 lib + 10 api_shape + 6 game + 4 record_winner + 2 equivalence) - [x] `cargo clippy --all-targets --features approx -- -D warnings` — clean - [x] `cargo +nightly fmt --check` — clean - [x] `cargo bench --bench batch` — 21.36 µs - [x] `cargo bench --bench gaussian` — unchanged from T1 - [x] `cargo run --example atp --features approx` — rewritten in new API, runs clean - [x] Historical Game-level goldens preserved in `tests/equivalence.rs` - [x] Public API matches spec Section 4 (verified by integration tests in `tests/api_shape.rs`) ## Commit history ~45 commits total across T0 + T1 + T2. Each task is self-contained and individually tested; the branch is bisectable. See `git log main..t2-new-api-surface` for the full list. ## Deferred to later tiers - `Outcome::Scored` + `MarginFactor` — T4 - `Damped` / `Residual` schedules — T4 - `Send + Sync` bounds + Rayon parallelism — T3 - N-team `predict_outcome` — T4 - `Game::custom` full ergonomics — T4 🤖 Generated with [Claude Code](https://claude.com/claude-code) Reviewed-on: #1 Co-authored-by: Anders Olsson <anders.e.olsson@gmail.com> Co-committed-by: Anders Olsson <anders.e.olsson@gmail.com>
This commit was merged in pull request #1.
This commit is contained in:
+420
-177
@@ -1,16 +1,85 @@
|
||||
use std::cmp::Ordering;
|
||||
|
||||
use crate::{
|
||||
N_INF, N00, approx, compute_margin,
|
||||
N_INF, N00,
|
||||
arena::ScratchArena,
|
||||
compute_margin,
|
||||
drift::Drift,
|
||||
evidence,
|
||||
factor::{Factor, trunc::TruncFactor},
|
||||
gaussian::Gaussian,
|
||||
message::{DiffMessage, TeamMessage},
|
||||
player::Player,
|
||||
sort_perm, tuple_gt, tuple_max,
|
||||
rating::Rating,
|
||||
time::Time,
|
||||
tuple_gt, tuple_max,
|
||||
};
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct GameOptions {
|
||||
pub p_draw: f64,
|
||||
pub convergence: crate::ConvergenceOptions,
|
||||
}
|
||||
|
||||
impl Default for GameOptions {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
p_draw: crate::P_DRAW,
|
||||
convergence: crate::ConvergenceOptions::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Owned variant of `Game` returned by public constructors.
|
||||
///
|
||||
/// Unlike `Game<'a, T, D>` (which borrows its result/weights slices from
|
||||
/// History's internal state), `OwnedGame<T, D>` owns its inputs so it can
|
||||
/// be returned freely from public constructors.
|
||||
#[derive(Debug)]
|
||||
pub struct Game<'a, D: Drift> {
|
||||
teams: Vec<Vec<Player<D>>>,
|
||||
#[allow(dead_code)]
|
||||
pub struct OwnedGame<T: Time, D: Drift<T>> {
|
||||
teams: Vec<Vec<Rating<T, D>>>,
|
||||
result: Vec<f64>,
|
||||
weights: Vec<Vec<f64>>,
|
||||
p_draw: f64,
|
||||
pub(crate) likelihoods: Vec<Vec<Gaussian>>,
|
||||
pub(crate) evidence: f64,
|
||||
}
|
||||
|
||||
impl<T: Time, D: Drift<T>> OwnedGame<T, D> {
|
||||
pub(crate) fn new(
|
||||
teams: Vec<Vec<Rating<T, D>>>,
|
||||
result: Vec<f64>,
|
||||
weights: Vec<Vec<f64>>,
|
||||
p_draw: f64,
|
||||
) -> Self {
|
||||
let mut arena = ScratchArena::new();
|
||||
let g = Game::ranked_with_arena(teams.clone(), &result, &weights, p_draw, &mut arena);
|
||||
let likelihoods = g.likelihoods;
|
||||
let evidence = g.evidence;
|
||||
Self {
|
||||
teams,
|
||||
result,
|
||||
weights,
|
||||
p_draw,
|
||||
likelihoods,
|
||||
evidence,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn posteriors(&self) -> Vec<Vec<Gaussian>> {
|
||||
self.likelihoods
|
||||
.iter()
|
||||
.zip(self.teams.iter())
|
||||
.map(|(l, t)| l.iter().zip(t.iter()).map(|(&l, r)| l * r.prior).collect())
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn log_evidence(&self) -> f64 {
|
||||
self.evidence.ln()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Game<'a, T: Time = i64, D: Drift<T> = crate::drift::ConstantDrift> {
|
||||
teams: Vec<Vec<Rating<T, D>>>,
|
||||
result: &'a [f64],
|
||||
weights: &'a [Vec<f64>],
|
||||
p_draw: f64,
|
||||
@@ -18,18 +87,18 @@ pub struct Game<'a, D: Drift> {
|
||||
pub(crate) evidence: f64,
|
||||
}
|
||||
|
||||
impl<'a, D: Drift> Game<'a, D> {
|
||||
pub fn new(
|
||||
teams: Vec<Vec<Player<D>>>,
|
||||
impl<'a, T: Time, D: Drift<T>> Game<'a, T, D> {
|
||||
pub(crate) fn ranked_with_arena(
|
||||
teams: Vec<Vec<Rating<T, D>>>,
|
||||
result: &'a [f64],
|
||||
weights: &'a [Vec<f64>],
|
||||
p_draw: f64,
|
||||
arena: &mut ScratchArena,
|
||||
) -> Self {
|
||||
debug_assert!(
|
||||
(result.len() == teams.len()),
|
||||
result.len() == teams.len(),
|
||||
"result must have the same length as teams"
|
||||
);
|
||||
|
||||
debug_assert!(
|
||||
weights
|
||||
.iter()
|
||||
@@ -37,19 +106,17 @@ impl<'a, D: Drift> Game<'a, D> {
|
||||
.all(|(w, t)| w.len() == t.len()),
|
||||
"weights must have the same dimensions as teams"
|
||||
);
|
||||
|
||||
debug_assert!(
|
||||
(0.0..1.0).contains(&p_draw),
|
||||
"draw probability.must be >= 0.0 and < 1.0"
|
||||
"draw probability must be >= 0.0 and < 1.0"
|
||||
);
|
||||
|
||||
debug_assert!(
|
||||
p_draw > 0.0 || {
|
||||
let mut r = result.to_vec();
|
||||
r.sort_unstable_by(|a, b| a.partial_cmp(b).unwrap());
|
||||
r.windows(2).all(|w| w[0] != w[1])
|
||||
},
|
||||
"draw must be > 0.0 if there is teams with draw"
|
||||
"draw must be > 0.0 if there are teams with draw"
|
||||
);
|
||||
|
||||
let mut this = Self {
|
||||
@@ -61,124 +128,144 @@ impl<'a, D: Drift> Game<'a, D> {
|
||||
evidence: 0.0,
|
||||
};
|
||||
|
||||
this.likelihoods();
|
||||
|
||||
this.likelihoods(arena);
|
||||
this
|
||||
}
|
||||
|
||||
fn likelihoods(&mut self) {
|
||||
let o = sort_perm(self.result, true);
|
||||
fn likelihoods(&mut self, arena: &mut ScratchArena) {
|
||||
arena.reset();
|
||||
|
||||
let mut team = o
|
||||
.iter()
|
||||
.map(|&e| {
|
||||
let performance = self.teams[e]
|
||||
.iter()
|
||||
.zip(self.weights[e].iter())
|
||||
.fold(N00, |p, (player, &weight)| {
|
||||
p + (player.performance() * weight)
|
||||
});
|
||||
let n_teams = self.teams.len();
|
||||
|
||||
TeamMessage {
|
||||
prior: performance,
|
||||
..Default::default()
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
// Sort teams by result descending; reuse arena.sort_buf to avoid allocation.
|
||||
arena.sort_buf.extend(0..n_teams);
|
||||
arena.sort_buf.sort_by(|&i, &j| {
|
||||
self.result[j]
|
||||
.partial_cmp(&self.result[i])
|
||||
.unwrap_or(Ordering::Equal)
|
||||
});
|
||||
|
||||
let mut diff = team
|
||||
.windows(2)
|
||||
.map(|w| DiffMessage {
|
||||
prior: w[0].prior - w[1].prior,
|
||||
likelihood: N_INF,
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
// Team performance priors written into arena buffer (capacity reused across games).
|
||||
arena.team_prior.extend(arena.sort_buf.iter().map(|&t| {
|
||||
self.teams[t]
|
||||
.iter()
|
||||
.zip(self.weights[t].iter())
|
||||
.fold(N00, |p, (player, &w)| p + (player.performance() * w))
|
||||
}));
|
||||
|
||||
let tie = o
|
||||
.windows(2)
|
||||
.map(|e| self.result[e[0]] == self.result[e[1]])
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let margin = if self.p_draw == 0.0 {
|
||||
vec![0.0; o.len() - 1]
|
||||
} else {
|
||||
o.windows(2)
|
||||
.map(|w| {
|
||||
let a: f64 = self.teams[w[0]].iter().map(|a| a.beta.powi(2)).sum();
|
||||
let b: f64 = self.teams[w[1]].iter().map(|a| a.beta.powi(2)).sum();
|
||||
let n_diffs = n_teams.saturating_sub(1);
|
||||
|
||||
// One TruncFactor per adjacent sorted-team pair; each owns a diff VarId.
|
||||
// trunc stays local (fresh state per game; Vec capacity is typically small).
|
||||
let mut trunc: Vec<TruncFactor> = (0..n_diffs)
|
||||
.map(|i| {
|
||||
let tie = self.result[arena.sort_buf[i]] == self.result[arena.sort_buf[i + 1]];
|
||||
let margin = if self.p_draw == 0.0 {
|
||||
0.0
|
||||
} else {
|
||||
let a: f64 = self.teams[arena.sort_buf[i]]
|
||||
.iter()
|
||||
.map(|p| p.beta.powi(2))
|
||||
.sum();
|
||||
let b: f64 = self.teams[arena.sort_buf[i + 1]]
|
||||
.iter()
|
||||
.map(|p| p.beta.powi(2))
|
||||
.sum();
|
||||
compute_margin(self.p_draw, (a + b).sqrt())
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
};
|
||||
};
|
||||
let vid = arena.vars.alloc(N_INF);
|
||||
TruncFactor::new(vid, margin, tie)
|
||||
})
|
||||
.collect();
|
||||
|
||||
self.evidence = 1.0;
|
||||
// Per-team messages from neighbouring RankDiff factors (replaces TeamMessage).
|
||||
arena.lhood_lose.resize(n_teams, N_INF);
|
||||
arena.lhood_win.resize(n_teams, N_INF);
|
||||
|
||||
let mut step = (f64::INFINITY, f64::INFINITY);
|
||||
let mut iter = 0;
|
||||
|
||||
while tuple_gt(step, 1e-6) && iter < 10 {
|
||||
step = (0.0, 0.0);
|
||||
step = (0.0_f64, 0.0_f64);
|
||||
|
||||
for e in 0..diff.len() - 1 {
|
||||
diff[e].prior = team[e].posterior_win() - team[e + 1].posterior_lose();
|
||||
// Forward sweep: diffs 0 .. n_diffs-2 (all but the last).
|
||||
for (e, tf) in trunc[..n_diffs.saturating_sub(1)].iter_mut().enumerate() {
|
||||
let pw = arena.team_prior[e] * arena.lhood_lose[e];
|
||||
let pl = arena.team_prior[e + 1] * arena.lhood_win[e + 1];
|
||||
let raw = pw - pl;
|
||||
arena.vars.set(tf.diff, raw * tf.msg);
|
||||
let d = tf.propagate(&mut arena.vars);
|
||||
step = tuple_max(step, d);
|
||||
|
||||
if iter == 0 {
|
||||
self.evidence *= evidence(&diff, &margin, &tie, e);
|
||||
}
|
||||
|
||||
diff[e].likelihood = approx(diff[e].prior, margin[e], tie[e]) / diff[e].prior;
|
||||
let likelihood_lose = team[e].posterior_win() - diff[e].likelihood;
|
||||
step = tuple_max(step, team[e + 1].likelihood_lose.delta(likelihood_lose));
|
||||
team[e + 1].likelihood_lose = likelihood_lose;
|
||||
let new_ll = pw - tf.msg;
|
||||
step = tuple_max(step, arena.lhood_lose[e + 1].delta(new_ll));
|
||||
arena.lhood_lose[e + 1] = new_ll;
|
||||
}
|
||||
|
||||
for e in (1..diff.len()).rev() {
|
||||
diff[e].prior = team[e].posterior_win() - team[e + 1].posterior_lose();
|
||||
// Backward sweep: diffs n_diffs-1 .. 1 (reverse, all but the first).
|
||||
for (rev_i, tf) in trunc[1..].iter_mut().rev().enumerate() {
|
||||
let e = n_diffs - 1 - rev_i;
|
||||
let pw = arena.team_prior[e] * arena.lhood_lose[e];
|
||||
let pl = arena.team_prior[e + 1] * arena.lhood_win[e + 1];
|
||||
let raw = pw - pl;
|
||||
arena.vars.set(tf.diff, raw * tf.msg);
|
||||
let d = tf.propagate(&mut arena.vars);
|
||||
step = tuple_max(step, d);
|
||||
|
||||
if iter == 0 && e == diff.len() - 1 {
|
||||
self.evidence *= evidence(&diff, &margin, &tie, e);
|
||||
}
|
||||
|
||||
diff[e].likelihood = approx(diff[e].prior, margin[e], tie[e]) / diff[e].prior;
|
||||
let likelihood_win = team[e + 1].posterior_lose() + diff[e].likelihood;
|
||||
step = tuple_max(step, team[e].likelihood_win.delta(likelihood_win));
|
||||
team[e].likelihood_win = likelihood_win;
|
||||
let new_lw = pl + tf.msg;
|
||||
step = tuple_max(step, arena.lhood_win[e].delta(new_lw));
|
||||
arena.lhood_win[e] = new_lw;
|
||||
}
|
||||
|
||||
iter += 1;
|
||||
}
|
||||
|
||||
if diff.len() == 1 {
|
||||
self.evidence = evidence(&diff, &margin, &tie, 0);
|
||||
|
||||
diff[0].prior = team[0].posterior_win() - team[1].posterior_lose();
|
||||
diff[0].likelihood = approx(diff[0].prior, margin[0], tie[0]) / diff[0].prior;
|
||||
// Special case: exactly 1 diff (2-team game); loop body was empty.
|
||||
if n_diffs == 1 {
|
||||
let raw = (arena.team_prior[0] * arena.lhood_lose[0])
|
||||
- (arena.team_prior[1] * arena.lhood_win[1]);
|
||||
arena.vars.set(trunc[0].diff, raw * trunc[0].msg);
|
||||
trunc[0].propagate(&mut arena.vars);
|
||||
}
|
||||
|
||||
let t_end = team.len() - 1;
|
||||
let d_end = diff.len() - 1;
|
||||
// Boundary updates: close the chain at both ends.
|
||||
if n_diffs > 0 {
|
||||
let pl1 = arena.team_prior[1] * arena.lhood_win[1];
|
||||
arena.lhood_win[0] = pl1 + trunc[0].msg;
|
||||
let pw_last = arena.team_prior[n_teams - 2] * arena.lhood_lose[n_teams - 2];
|
||||
arena.lhood_lose[n_teams - 1] = pw_last - trunc[n_diffs - 1].msg;
|
||||
}
|
||||
|
||||
team[0].likelihood_win = team[1].posterior_lose() + diff[0].likelihood;
|
||||
team[t_end].likelihood_lose = team[t_end - 1].posterior_win() - diff[d_end].likelihood;
|
||||
// Evidence = product of per-diff evidences (each cached on first propagation).
|
||||
self.evidence = trunc
|
||||
.iter()
|
||||
.map(|t| t.evidence_cached.unwrap_or(1.0))
|
||||
.product();
|
||||
|
||||
let m_t_ft = o.into_iter().map(|e| team[e].likelihood());
|
||||
// Inverse permutation: inv_buf[orig_i] = sorted_i.
|
||||
arena.inv_buf.resize(n_teams, 0);
|
||||
for (si, &orig_i) in arena.sort_buf.iter().enumerate() {
|
||||
arena.inv_buf[orig_i] = si;
|
||||
}
|
||||
|
||||
self.likelihoods = self
|
||||
.teams
|
||||
.iter()
|
||||
.zip(self.weights.iter())
|
||||
.zip(m_t_ft)
|
||||
.map(|((p, w), m)| {
|
||||
let performance = p.iter().zip(w.iter()).fold(N00, |p, (player, &weight)| {
|
||||
p + (player.performance() * weight)
|
||||
});
|
||||
|
||||
p.iter()
|
||||
.zip(w.iter())
|
||||
.map(|(p, &w)| {
|
||||
((m - performance.exclude(p.performance() * w)) * (1.0 / w))
|
||||
.forget(p.beta.powi(2))
|
||||
.enumerate()
|
||||
.map(|(orig_i, (players, weights))| {
|
||||
let si = arena.inv_buf[orig_i];
|
||||
let m = arena.lhood_win[si] * arena.lhood_lose[si];
|
||||
let performance = players
|
||||
.iter()
|
||||
.zip(weights.iter())
|
||||
.fold(N00, |p, (player, &w)| p + (player.performance() * w));
|
||||
players
|
||||
.iter()
|
||||
.zip(weights.iter())
|
||||
.map(|(player, &w)| {
|
||||
((m - performance.exclude(player.performance() * w)) * (1.0 / w))
|
||||
.forget(player.beta.powi(2))
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
@@ -197,6 +284,68 @@ impl<'a, D: Drift> Game<'a, D> {
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
}
|
||||
|
||||
pub fn log_evidence(&self) -> f64 {
|
||||
self.evidence.ln()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Time, D: Drift<T>> Game<'_, T, D> {
|
||||
pub fn ranked(
|
||||
teams: &[&[Rating<T, D>]],
|
||||
outcome: crate::Outcome,
|
||||
options: &GameOptions,
|
||||
) -> Result<OwnedGame<T, D>, crate::InferenceError> {
|
||||
if !(0.0..1.0).contains(&options.p_draw) {
|
||||
return Err(crate::InferenceError::InvalidProbability {
|
||||
value: options.p_draw,
|
||||
});
|
||||
}
|
||||
if outcome.team_count() != teams.len() {
|
||||
return Err(crate::InferenceError::MismatchedShape {
|
||||
kind: "outcome ranks vs teams",
|
||||
expected: teams.len(),
|
||||
got: outcome.team_count(),
|
||||
});
|
||||
}
|
||||
|
||||
let ranks = outcome.as_ranks();
|
||||
let max_rank = ranks.iter().copied().max().unwrap_or(0) as f64;
|
||||
let result: Vec<f64> = ranks.iter().map(|&r| max_rank - r as f64).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();
|
||||
|
||||
Ok(OwnedGame::new(teams_owned, result, weights, options.p_draw))
|
||||
}
|
||||
|
||||
pub fn one_v_one(
|
||||
a: &Rating<T, D>,
|
||||
b: &Rating<T, D>,
|
||||
outcome: crate::Outcome,
|
||||
) -> Result<(Gaussian, Gaussian), crate::InferenceError> {
|
||||
let game = Self::ranked(&[&[*a], &[*b]], outcome, &GameOptions::default())?;
|
||||
let post = game.posteriors();
|
||||
Ok((post[0][0], post[1][0]))
|
||||
}
|
||||
|
||||
pub fn free_for_all(
|
||||
players: &[&Rating<T, D>],
|
||||
outcome: crate::Outcome,
|
||||
options: &GameOptions,
|
||||
) -> Result<OwnedGame<T, D>, crate::InferenceError> {
|
||||
let teams: Vec<Vec<Rating<T, D>>> = players.iter().map(|p| vec![**p]).collect();
|
||||
let team_refs: Vec<&[Rating<T, D>]> = teams.iter().map(|t| t.as_slice()).collect();
|
||||
Self::ranked(&team_refs, outcome, options)
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
pub fn custom<S: crate::factors::Schedule>(
|
||||
factors: &mut [crate::factors::BuiltinFactor],
|
||||
vars: &mut crate::factors::VarStore,
|
||||
schedule: &S,
|
||||
) -> crate::factors::ScheduleReport {
|
||||
schedule.run(factors, vars)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -204,23 +353,31 @@ mod tests {
|
||||
use ::approx::assert_ulps_eq;
|
||||
|
||||
use super::*;
|
||||
use crate::{ConstantDrift, GAMMA, Gaussian, N_INF, Player};
|
||||
use crate::{ConstantDrift, GAMMA, Gaussian, N_INF, Rating, arena::ScratchArena};
|
||||
|
||||
type R = Rating<i64, ConstantDrift>;
|
||||
|
||||
#[test]
|
||||
fn test_1vs1() {
|
||||
let t_a = Player::new(
|
||||
let t_a = R::new(
|
||||
Gaussian::from_ms(25.0, 25.0 / 3.0),
|
||||
25.0 / 6.0,
|
||||
ConstantDrift(25.0 / 300.0),
|
||||
);
|
||||
let t_b = Player::new(
|
||||
let t_b = R::new(
|
||||
Gaussian::from_ms(25.0, 25.0 / 3.0),
|
||||
25.0 / 6.0,
|
||||
ConstantDrift(25.0 / 300.0),
|
||||
);
|
||||
|
||||
let w = [vec![1.0], vec![1.0]];
|
||||
let g = Game::new(vec![vec![t_a], vec![t_b]], &[0.0, 1.0], &w, 0.0);
|
||||
let g = Game::ranked_with_arena(
|
||||
vec![vec![t_a], vec![t_b]],
|
||||
&[0.0, 1.0],
|
||||
&w,
|
||||
0.0,
|
||||
&mut ScratchArena::new(),
|
||||
);
|
||||
let p = g.posteriors();
|
||||
|
||||
let a = p[0][0];
|
||||
@@ -229,19 +386,25 @@ mod tests {
|
||||
assert_ulps_eq!(a, Gaussian::from_ms(20.794779, 7.194481), epsilon = 1e-6);
|
||||
assert_ulps_eq!(b, Gaussian::from_ms(29.205220, 7.194481), epsilon = 1e-6);
|
||||
|
||||
let t_a = Player::new(
|
||||
let t_a = R::new(
|
||||
Gaussian::from_ms(29.0, 1.0),
|
||||
25.0 / 6.0,
|
||||
ConstantDrift(GAMMA),
|
||||
);
|
||||
let t_b = Player::new(
|
||||
let t_b = R::new(
|
||||
Gaussian::from_ms(25.0, 25.0 / 3.0),
|
||||
25.0 / 6.0,
|
||||
ConstantDrift(GAMMA),
|
||||
);
|
||||
|
||||
let w = [vec![1.0], vec![1.0]];
|
||||
let g = Game::new(vec![vec![t_a], vec![t_b]], &[0.0, 1.0], &w, 0.0);
|
||||
let g = Game::ranked_with_arena(
|
||||
vec![vec![t_a], vec![t_b]],
|
||||
&[0.0, 1.0],
|
||||
&w,
|
||||
0.0,
|
||||
&mut ScratchArena::new(),
|
||||
);
|
||||
let p = g.posteriors();
|
||||
|
||||
let a = p[0][0];
|
||||
@@ -250,11 +413,17 @@ mod tests {
|
||||
assert_ulps_eq!(a, Gaussian::from_ms(28.896475, 0.996604), epsilon = 1e-6);
|
||||
assert_ulps_eq!(b, Gaussian::from_ms(32.189211, 6.062063), epsilon = 1e-6);
|
||||
|
||||
let t_a = Player::new(Gaussian::from_ms(1.139, 0.531), 1.0, ConstantDrift(0.2125));
|
||||
let t_b = Player::new(Gaussian::from_ms(15.568, 0.51), 1.0, ConstantDrift(0.2125));
|
||||
let t_a = R::new(Gaussian::from_ms(1.139, 0.531), 1.0, ConstantDrift(0.2125));
|
||||
let t_b = R::new(Gaussian::from_ms(15.568, 0.51), 1.0, ConstantDrift(0.2125));
|
||||
|
||||
let w = [vec![1.0], vec![1.0]];
|
||||
let g = Game::new(vec![vec![t_a], vec![t_b]], &[0.0, 1.0], &w, 0.0);
|
||||
let g = Game::ranked_with_arena(
|
||||
vec![vec![t_a], vec![t_b]],
|
||||
&[0.0, 1.0],
|
||||
&w,
|
||||
0.0,
|
||||
&mut ScratchArena::new(),
|
||||
);
|
||||
|
||||
assert_eq!(g.likelihoods[0][0], N_INF);
|
||||
assert_eq!(g.likelihoods[1][0], N_INF);
|
||||
@@ -263,17 +432,17 @@ mod tests {
|
||||
#[test]
|
||||
fn test_1vs1vs1() {
|
||||
let teams = vec![
|
||||
vec![Player::new(
|
||||
vec![R::new(
|
||||
Gaussian::from_ms(25.0, 25.0 / 3.0),
|
||||
25.0 / 6.0,
|
||||
ConstantDrift(25.0 / 300.0),
|
||||
)],
|
||||
vec![Player::new(
|
||||
vec![R::new(
|
||||
Gaussian::from_ms(25.0, 25.0 / 3.0),
|
||||
25.0 / 6.0,
|
||||
ConstantDrift(25.0 / 300.0),
|
||||
)],
|
||||
vec![Player::new(
|
||||
vec![R::new(
|
||||
Gaussian::from_ms(25.0, 25.0 / 3.0),
|
||||
25.0 / 6.0,
|
||||
ConstantDrift(25.0 / 300.0),
|
||||
@@ -281,7 +450,13 @@ mod tests {
|
||||
];
|
||||
|
||||
let w = [vec![1.0], vec![1.0], vec![1.0]];
|
||||
let g = Game::new(teams.clone(), &[1.0, 2.0, 0.0], &w, 0.0);
|
||||
let g = Game::ranked_with_arena(
|
||||
teams.clone(),
|
||||
&[1.0, 2.0, 0.0],
|
||||
&w,
|
||||
0.0,
|
||||
&mut ScratchArena::new(),
|
||||
);
|
||||
let p = g.posteriors();
|
||||
|
||||
let a = p[0][0];
|
||||
@@ -291,7 +466,13 @@ mod tests {
|
||||
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 g = Game::new(teams.clone(), &[2.0, 1.0, 0.0], &w, 0.0);
|
||||
let g = Game::ranked_with_arena(
|
||||
teams.clone(),
|
||||
&[2.0, 1.0, 0.0],
|
||||
&w,
|
||||
0.0,
|
||||
&mut ScratchArena::new(),
|
||||
);
|
||||
let p = g.posteriors();
|
||||
|
||||
let a = p[0][0];
|
||||
@@ -301,33 +482,40 @@ mod tests {
|
||||
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 g = Game::new(teams, &[1.0, 2.0, 0.0], &w, 0.5);
|
||||
let g = Game::ranked_with_arena(teams, &[1.0, 2.0, 0.0], &w, 0.5, &mut ScratchArena::new());
|
||||
let p = g.posteriors();
|
||||
|
||||
let a = p[0][0];
|
||||
let b = p[1][0];
|
||||
let c = p[2][0];
|
||||
|
||||
assert_ulps_eq!(a, Gaussian::from_ms(24.999999, 6.092561), epsilon = 1e-6);
|
||||
// T1 ULP shift: mu rounds to 25.0 (was 24.999999) under natural-parameter storage.
|
||||
assert_ulps_eq!(a, Gaussian::from_ms(25.0, 6.092561), epsilon = 1e-6);
|
||||
assert_ulps_eq!(b, Gaussian::from_ms(33.379314, 6.483575), epsilon = 1e-6);
|
||||
assert_ulps_eq!(c, Gaussian::from_ms(16.620685, 6.483575), epsilon = 1e-6);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_1vs1_draw() {
|
||||
let t_a = Player::new(
|
||||
let t_a = R::new(
|
||||
Gaussian::from_ms(25.0, 25.0 / 3.0),
|
||||
25.0 / 6.0,
|
||||
ConstantDrift(25.0 / 300.0),
|
||||
);
|
||||
let t_b = Player::new(
|
||||
let t_b = R::new(
|
||||
Gaussian::from_ms(25.0, 25.0 / 3.0),
|
||||
25.0 / 6.0,
|
||||
ConstantDrift(25.0 / 300.0),
|
||||
);
|
||||
|
||||
let w = [vec![1.0], vec![1.0]];
|
||||
let g = Game::new(vec![vec![t_a], vec![t_b]], &[0.0, 0.0], &w, 0.25);
|
||||
let g = Game::ranked_with_arena(
|
||||
vec![vec![t_a], vec![t_b]],
|
||||
&[0.0, 0.0],
|
||||
&w,
|
||||
0.25,
|
||||
&mut ScratchArena::new(),
|
||||
);
|
||||
let p = g.posteriors();
|
||||
|
||||
let a = p[0][0];
|
||||
@@ -336,19 +524,25 @@ mod tests {
|
||||
assert_ulps_eq!(a, Gaussian::from_ms(24.999999, 6.469480), epsilon = 1e-6);
|
||||
assert_ulps_eq!(b, Gaussian::from_ms(24.999999, 6.469480), epsilon = 1e-6);
|
||||
|
||||
let t_a = Player::new(
|
||||
let t_a = R::new(
|
||||
Gaussian::from_ms(25.0, 3.0),
|
||||
25.0 / 6.0,
|
||||
ConstantDrift(25.0 / 300.0),
|
||||
);
|
||||
let t_b = Player::new(
|
||||
let t_b = R::new(
|
||||
Gaussian::from_ms(29.0, 2.0),
|
||||
25.0 / 6.0,
|
||||
ConstantDrift(25.0 / 300.0),
|
||||
);
|
||||
|
||||
let w = [vec![1.0], vec![1.0]];
|
||||
let g = Game::new(vec![vec![t_a], vec![t_b]], &[0.0, 0.0], &w, 0.25);
|
||||
let g = Game::ranked_with_arena(
|
||||
vec![vec![t_a], vec![t_b]],
|
||||
&[0.0, 0.0],
|
||||
&w,
|
||||
0.25,
|
||||
&mut ScratchArena::new(),
|
||||
);
|
||||
let p = g.posteriors();
|
||||
|
||||
let a = p[0][0];
|
||||
@@ -360,28 +554,29 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_1vs1vs1_draw() {
|
||||
let t_a = Player::new(
|
||||
let t_a = R::new(
|
||||
Gaussian::from_ms(25.0, 25.0 / 3.0),
|
||||
25.0 / 6.0,
|
||||
ConstantDrift(25.0 / 300.0),
|
||||
);
|
||||
let t_b = Player::new(
|
||||
let t_b = R::new(
|
||||
Gaussian::from_ms(25.0, 25.0 / 3.0),
|
||||
25.0 / 6.0,
|
||||
ConstantDrift(25.0 / 300.0),
|
||||
);
|
||||
let t_c = Player::new(
|
||||
let t_c = R::new(
|
||||
Gaussian::from_ms(25.0, 25.0 / 3.0),
|
||||
25.0 / 6.0,
|
||||
ConstantDrift(25.0 / 300.0),
|
||||
);
|
||||
|
||||
let w = [vec![1.0], vec![1.0], vec![1.0]];
|
||||
let g = Game::new(
|
||||
let g = Game::ranked_with_arena(
|
||||
vec![vec![t_a], vec![t_b], vec![t_c]],
|
||||
&[0.0, 0.0, 0.0],
|
||||
&w,
|
||||
0.25,
|
||||
&mut ScratchArena::new(),
|
||||
);
|
||||
let p = g.posteriors();
|
||||
|
||||
@@ -389,32 +584,35 @@ mod tests {
|
||||
let b = p[1][0];
|
||||
let c = p[2][0];
|
||||
|
||||
assert_ulps_eq!(a, Gaussian::from_ms(24.999999, 5.729068), epsilon = 1e-6);
|
||||
assert_ulps_eq!(b, Gaussian::from_ms(25.000000, 5.707423), epsilon = 1e-6);
|
||||
assert_ulps_eq!(c, Gaussian::from_ms(24.999999, 5.729068), epsilon = 1e-6);
|
||||
// Goldens updated for natural-parameter storage: mu rounds to 25.0 (was 24.999999),
|
||||
// sigma shifts by ~3e-7 ULPs (within 1e-6 of original). Both bounded differences.
|
||||
assert_ulps_eq!(a, Gaussian::from_ms(25.0, 5.729069), epsilon = 1e-6);
|
||||
assert_ulps_eq!(b, Gaussian::from_ms(25.0, 5.707424), epsilon = 1e-6);
|
||||
assert_ulps_eq!(c, Gaussian::from_ms(25.0, 5.729069), epsilon = 1e-6);
|
||||
|
||||
let t_a = Player::new(
|
||||
let t_a = R::new(
|
||||
Gaussian::from_ms(25.0, 3.0),
|
||||
25.0 / 6.0,
|
||||
ConstantDrift(25.0 / 300.0),
|
||||
);
|
||||
let t_b = Player::new(
|
||||
let t_b = R::new(
|
||||
Gaussian::from_ms(25.0, 3.0),
|
||||
25.0 / 6.0,
|
||||
ConstantDrift(25.0 / 300.0),
|
||||
);
|
||||
let t_c = Player::new(
|
||||
let t_c = R::new(
|
||||
Gaussian::from_ms(29.0, 2.0),
|
||||
25.0 / 6.0,
|
||||
ConstantDrift(25.0 / 300.0),
|
||||
);
|
||||
|
||||
let w = [vec![1.0], vec![1.0], vec![1.0]];
|
||||
let g = Game::new(
|
||||
let g = Game::ranked_with_arena(
|
||||
vec![vec![t_a], vec![t_b], vec![t_c]],
|
||||
&[0.0, 0.0, 0.0],
|
||||
&w,
|
||||
0.25,
|
||||
&mut ScratchArena::new(),
|
||||
);
|
||||
let p = g.posteriors();
|
||||
|
||||
@@ -430,29 +628,29 @@ mod tests {
|
||||
#[test]
|
||||
fn test_2vs1vs2_mixed() {
|
||||
let t_a = vec![
|
||||
Player::new(
|
||||
R::new(
|
||||
Gaussian::from_ms(12.0, 3.0),
|
||||
25.0 / 6.0,
|
||||
ConstantDrift(25.0 / 300.0),
|
||||
),
|
||||
Player::new(
|
||||
R::new(
|
||||
Gaussian::from_ms(18.0, 3.0),
|
||||
25.0 / 6.0,
|
||||
ConstantDrift(25.0 / 300.0),
|
||||
),
|
||||
];
|
||||
let t_b = vec![Player::new(
|
||||
let t_b = vec![R::new(
|
||||
Gaussian::from_ms(30.0, 3.0),
|
||||
25.0 / 6.0,
|
||||
ConstantDrift(25.0 / 300.0),
|
||||
)];
|
||||
let t_c = vec![
|
||||
Player::new(
|
||||
R::new(
|
||||
Gaussian::from_ms(14.0, 3.0),
|
||||
25.0 / 6.0,
|
||||
ConstantDrift(25.0 / 300.0),
|
||||
),
|
||||
Player::new(
|
||||
R::new(
|
||||
Gaussian::from_ms(16., 3.0),
|
||||
25.0 / 6.0,
|
||||
ConstantDrift(25.0 / 300.0),
|
||||
@@ -460,7 +658,13 @@ mod tests {
|
||||
];
|
||||
|
||||
let w = [vec![1.0, 1.0], vec![1.0], vec![1.0, 1.0]];
|
||||
let g = Game::new(vec![t_a, t_b, t_c], &[1.0, 0.0, 0.0], &w, 0.25);
|
||||
let g = Game::ranked_with_arena(
|
||||
vec![t_a, t_b, t_c],
|
||||
&[1.0, 0.0, 0.0],
|
||||
&w,
|
||||
0.25,
|
||||
&mut ScratchArena::new(),
|
||||
);
|
||||
let p = g.posteriors();
|
||||
|
||||
assert_ulps_eq!(p[0][0], Gaussian::from_ms(13.051, 2.864), epsilon = 1e-3);
|
||||
@@ -475,19 +679,25 @@ mod tests {
|
||||
let w_a = vec![1.0];
|
||||
let w_b = vec![2.0];
|
||||
|
||||
let t_a = vec![Player::new(
|
||||
let t_a = vec![R::new(
|
||||
Gaussian::from_ms(25.0, 25.0 / 3.0),
|
||||
25.0 / 6.0,
|
||||
ConstantDrift(0.0),
|
||||
)];
|
||||
let t_b = vec![Player::new(
|
||||
let t_b = vec![R::new(
|
||||
Gaussian::from_ms(25.0, 25.0 / 3.0),
|
||||
25.0 / 6.0,
|
||||
ConstantDrift(0.0),
|
||||
)];
|
||||
|
||||
let w = [w_a, w_b];
|
||||
let g = Game::new(vec![t_a.clone(), t_b.clone()], &[1.0, 0.0], &w, 0.0);
|
||||
let g = Game::ranked_with_arena(
|
||||
vec![t_a.clone(), t_b.clone()],
|
||||
&[1.0, 0.0],
|
||||
&w,
|
||||
0.0,
|
||||
&mut ScratchArena::new(),
|
||||
);
|
||||
let p = g.posteriors();
|
||||
|
||||
assert_ulps_eq!(
|
||||
@@ -505,7 +715,13 @@ mod tests {
|
||||
let w_b = vec![0.7];
|
||||
|
||||
let w = [w_a, w_b];
|
||||
let g = Game::new(vec![t_a.clone(), t_b.clone()], &[1.0, 0.0], &w, 0.0);
|
||||
let g = Game::ranked_with_arena(
|
||||
vec![t_a.clone(), t_b.clone()],
|
||||
&[1.0, 0.0],
|
||||
&w,
|
||||
0.0,
|
||||
&mut ScratchArena::new(),
|
||||
);
|
||||
let p = g.posteriors();
|
||||
|
||||
assert_ulps_eq!(
|
||||
@@ -523,7 +739,13 @@ mod tests {
|
||||
let w_b = vec![0.7];
|
||||
|
||||
let w = [w_a, w_b];
|
||||
let g = Game::new(vec![t_a, t_b], &[1.0, 0.0], &w, 0.0);
|
||||
let g = Game::ranked_with_arena(
|
||||
vec![t_a, t_b],
|
||||
&[1.0, 0.0],
|
||||
&w,
|
||||
0.0,
|
||||
&mut ScratchArena::new(),
|
||||
);
|
||||
let p = g.posteriors();
|
||||
|
||||
assert_ulps_eq!(
|
||||
@@ -540,19 +762,17 @@ mod tests {
|
||||
let w_a = vec![1.0];
|
||||
let w_b = vec![0.0];
|
||||
|
||||
let t_a = vec![Player::new(
|
||||
Gaussian::from_ms(2.0, 6.0),
|
||||
1.0,
|
||||
ConstantDrift(0.0),
|
||||
)];
|
||||
let t_b = vec![Player::new(
|
||||
Gaussian::from_ms(2.0, 6.0),
|
||||
1.0,
|
||||
ConstantDrift(0.0),
|
||||
)];
|
||||
let t_a = vec![R::new(Gaussian::from_ms(2.0, 6.0), 1.0, ConstantDrift(0.0))];
|
||||
let t_b = vec![R::new(Gaussian::from_ms(2.0, 6.0), 1.0, ConstantDrift(0.0))];
|
||||
|
||||
let w = [w_a, w_b];
|
||||
let g = Game::new(vec![t_a, t_b], &[1.0, 0.0], &w, 0.0);
|
||||
let g = Game::ranked_with_arena(
|
||||
vec![t_a, t_b],
|
||||
&[1.0, 0.0],
|
||||
&w,
|
||||
0.0,
|
||||
&mut ScratchArena::new(),
|
||||
);
|
||||
let p = g.posteriors();
|
||||
|
||||
assert_ulps_eq!(
|
||||
@@ -569,19 +789,17 @@ mod tests {
|
||||
let w_a = vec![1.0];
|
||||
let w_b = vec![-1.0];
|
||||
|
||||
let t_a = vec![Player::new(
|
||||
Gaussian::from_ms(2.0, 6.0),
|
||||
1.0,
|
||||
ConstantDrift(0.0),
|
||||
)];
|
||||
let t_b = vec![Player::new(
|
||||
Gaussian::from_ms(2.0, 6.0),
|
||||
1.0,
|
||||
ConstantDrift(0.0),
|
||||
)];
|
||||
let t_a = vec![R::new(Gaussian::from_ms(2.0, 6.0), 1.0, ConstantDrift(0.0))];
|
||||
let t_b = vec![R::new(Gaussian::from_ms(2.0, 6.0), 1.0, ConstantDrift(0.0))];
|
||||
|
||||
let w = [w_a, w_b];
|
||||
let g = Game::new(vec![t_a, t_b], &[1.0, 0.0], &w, 0.0);
|
||||
let g = Game::ranked_with_arena(
|
||||
vec![t_a, t_b],
|
||||
&[1.0, 0.0],
|
||||
&w,
|
||||
0.0,
|
||||
&mut ScratchArena::new(),
|
||||
);
|
||||
let p = g.posteriors();
|
||||
|
||||
assert_ulps_eq!(p[0][0], p[1][0], epsilon = 1e-6);
|
||||
@@ -590,12 +808,12 @@ mod tests {
|
||||
#[test]
|
||||
fn test_2vs2_weighted() {
|
||||
let t_a = vec![
|
||||
Player::new(
|
||||
R::new(
|
||||
Gaussian::from_ms(25.0, 25.0 / 3.0),
|
||||
25.0 / 6.0,
|
||||
ConstantDrift(0.0),
|
||||
),
|
||||
Player::new(
|
||||
R::new(
|
||||
Gaussian::from_ms(25.0, 25.0 / 3.0),
|
||||
25.0 / 6.0,
|
||||
ConstantDrift(0.0),
|
||||
@@ -604,12 +822,12 @@ mod tests {
|
||||
let w_a = vec![0.4, 0.8];
|
||||
|
||||
let t_b = vec![
|
||||
Player::new(
|
||||
R::new(
|
||||
Gaussian::from_ms(25.0, 25.0 / 3.0),
|
||||
25.0 / 6.0,
|
||||
ConstantDrift(0.0),
|
||||
),
|
||||
Player::new(
|
||||
R::new(
|
||||
Gaussian::from_ms(25.0, 25.0 / 3.0),
|
||||
25.0 / 6.0,
|
||||
ConstantDrift(0.0),
|
||||
@@ -618,7 +836,13 @@ mod tests {
|
||||
let w_b = vec![0.9, 0.6];
|
||||
|
||||
let w = [w_a, w_b];
|
||||
let g = Game::new(vec![t_a.clone(), t_b.clone()], &[1.0, 0.0], &w, 0.0);
|
||||
let g = Game::ranked_with_arena(
|
||||
vec![t_a.clone(), t_b.clone()],
|
||||
&[1.0, 0.0],
|
||||
&w,
|
||||
0.0,
|
||||
&mut ScratchArena::new(),
|
||||
);
|
||||
let p = g.posteriors();
|
||||
|
||||
assert_ulps_eq!(
|
||||
@@ -646,7 +870,13 @@ mod tests {
|
||||
let w_b = vec![0.7, 0.4];
|
||||
|
||||
let w = [w_a, w_b];
|
||||
let g = Game::new(vec![t_a.clone(), t_b.clone()], &[1.0, 0.0], &w, 0.0);
|
||||
let g = Game::ranked_with_arena(
|
||||
vec![t_a.clone(), t_b.clone()],
|
||||
&[1.0, 0.0],
|
||||
&w,
|
||||
0.0,
|
||||
&mut ScratchArena::new(),
|
||||
);
|
||||
let p = g.posteriors();
|
||||
|
||||
assert_ulps_eq!(
|
||||
@@ -674,7 +904,13 @@ mod tests {
|
||||
let w_b = vec![0.7, 2.4];
|
||||
|
||||
let w = [w_a, w_b];
|
||||
let g = Game::new(vec![t_a.clone(), t_b.clone()], &[1.0, 0.0], &w, 0.0);
|
||||
let g = Game::ranked_with_arena(
|
||||
vec![t_a.clone(), t_b.clone()],
|
||||
&[1.0, 0.0],
|
||||
&w,
|
||||
0.0,
|
||||
&mut ScratchArena::new(),
|
||||
);
|
||||
let p = g.posteriors();
|
||||
|
||||
assert_ulps_eq!(
|
||||
@@ -699,10 +935,10 @@ mod tests {
|
||||
);
|
||||
|
||||
let w = [vec![1.0, 1.0], vec![1.0]];
|
||||
let g = Game::new(
|
||||
let g = Game::ranked_with_arena(
|
||||
vec![
|
||||
t_a.clone(),
|
||||
vec![Player::new(
|
||||
vec![R::new(
|
||||
Gaussian::from_ms(25.0, 25.0 / 3.0),
|
||||
25.0 / 6.0,
|
||||
ConstantDrift(0.0),
|
||||
@@ -711,6 +947,7 @@ mod tests {
|
||||
&[1.0, 0.0],
|
||||
&w,
|
||||
0.0,
|
||||
&mut ScratchArena::new(),
|
||||
);
|
||||
let post_2vs1 = g.posteriors();
|
||||
|
||||
@@ -718,7 +955,13 @@ mod tests {
|
||||
let w_b = vec![1.0, 0.0];
|
||||
|
||||
let w = [w_a, w_b];
|
||||
let g = Game::new(vec![t_a, t_b.clone()], &[1.0, 0.0], &w, 0.0);
|
||||
let g = Game::ranked_with_arena(
|
||||
vec![t_a, t_b.clone()],
|
||||
&[1.0, 0.0],
|
||||
&w,
|
||||
0.0,
|
||||
&mut ScratchArena::new(),
|
||||
);
|
||||
let p = g.posteriors();
|
||||
|
||||
assert_ulps_eq!(p[0][0], post_2vs1[0][0], epsilon = 1e-6);
|
||||
|
||||
Reference in New Issue
Block a user