`posterior_of`, `posterior_of_at` and `expected_variance_reduction`
existed twice: once on `Joint`, and once on `History` as one-shot
wrappers whose whole body was `self.joint()?.<same>(..)`.
The wrappers re-factorised on every call — their own docs said so,
warning the reader to take a `Joint` instead — and they were what
smuggled the scored-only precondition onto the flat surface. A user
following the quickstart builds a ranked history, sees `posterior_of` in
the method list, and it never works. `h.joint()?.posterior_of(..)` is
one call longer and tells the truth: you need a joint, and a joint needs
a scored history.
That leaves three tiers instead of a flat surface with a hidden
precondition: `History` fits and reads, `predict_*` forecasts, `Joint`
answers exact joint questions.
`predict_margin` was itself calling `self.posterior_of`; it goes through
`self.joint()?` directly now.
The `Joint` methods' docs referred back to the wrappers for their real
content ("Identical to `History::posterior_of`, without re-paying the
factorisation"), so they now carry it: what a linear functional means,
which appearance each competitor is read at, and why
`expected_variance_reduction` belongs on the handle.
`tests/joint_handle.rs` had three tests comparing the wrapper against
the handle. That comparison is gone, but the property behind it is not —
they now compare a *reused* joint against a *fresh* one per question,
which is the actual correctness claim behind caching the factorisation
(#51), without the wrapper in the middle.
Closes #78.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011hcFjNDmHXZF8URGLku5zZ
76 lines
2.6 KiB
Rust
76 lines
2.6 KiB
Rust
//! Cost of the joint posterior: factorising versus querying.
|
|
//!
|
|
//! The split is the whole point of `History::joint`. Factorising is `O(n^3)` in
|
|
//! the history's appearances and depends only on the fit; a query is `O(n^2)`
|
|
//! and depends only on the question. `posterior_of_one_shot` pays both every
|
|
//! time, `joint_query` pays only the second.
|
|
|
|
use criterion::{Criterion, criterion_group, criterion_main};
|
|
use smallvec::smallvec;
|
|
use trueskill_tt::{ConstantDrift, ConvergenceOptions, Event, History, Member, Outcome, Team};
|
|
|
|
/// 30 slices of 8 duels: 480 appearances over 100 competitors.
|
|
fn fitted() -> History<i64, ConstantDrift, trueskill_tt::NullObserver, String> {
|
|
let mut h: History<i64, ConstantDrift, _, String> = History::builder()
|
|
.key_type::<String>()
|
|
.mu(0.0)
|
|
.sigma(6.0)
|
|
.beta(1.0)
|
|
.score_sigma(2.0)
|
|
.drift(ConstantDrift::new(0.05))
|
|
.convergence(ConvergenceOptions {
|
|
max_iter: 30,
|
|
epsilon: 1e-10,
|
|
alpha: 1.0,
|
|
})
|
|
.build();
|
|
|
|
let mut events: Vec<Event<i64, String>> = Vec::new();
|
|
let mut k = 0usize;
|
|
for t in 0..30i64 {
|
|
for _ in 0..8 {
|
|
k += 1;
|
|
events.push(Event {
|
|
time: t,
|
|
teams: smallvec![
|
|
Team::with_members([Member::new(format!("p{}", k % 100))]),
|
|
Team::with_members([Member::new(format!("p{}", (k + 37) % 100))]),
|
|
],
|
|
outcome: Outcome::scores([
|
|
(k as f64 * 0.3).sin().abs() * 20.0,
|
|
(k as f64 * 0.3).cos().abs() * 20.0,
|
|
]),
|
|
});
|
|
}
|
|
}
|
|
h.add_events(events).unwrap();
|
|
let _ = h.converge().unwrap();
|
|
h
|
|
}
|
|
|
|
fn bench_joint(c: &mut Criterion) {
|
|
let h = fitted();
|
|
let a = "p0".to_string();
|
|
let b = "p1".to_string();
|
|
let terms = [(&a, 1.0), (&b, -1.0)];
|
|
|
|
c.bench_function("joint_factorise_480_appearances", |bencher| {
|
|
bencher.iter(|| std::hint::black_box(h.joint().unwrap().variables()));
|
|
});
|
|
|
|
// Factorise-and-query, the cost the deleted `History::posterior_of`
|
|
// wrapper paid on every call. Kept as the baseline the cached query below
|
|
// is measured against.
|
|
c.bench_function("posterior_of_one_shot_480_appearances", |bencher| {
|
|
bencher.iter(|| std::hint::black_box(h.joint().unwrap().posterior_of(&terms).unwrap()));
|
|
});
|
|
|
|
let joint = h.joint().unwrap();
|
|
c.bench_function("joint_query_480_appearances", |bencher| {
|
|
bencher.iter(|| std::hint::black_box(joint.posterior_of(&terms).unwrap()));
|
|
});
|
|
}
|
|
|
|
criterion_group!(benches, bench_joint);
|
|
criterion_main!(benches);
|