`K` is the one type parameter people change, and it was last. Naming a
history in a struct field meant writing all four to say one thing:
struct Ladder { history: History<i64, ConstantDrift, NullObserver, String> }
struct Analysis<'h> { joint: Joint<'h, i64, ConstantDrift, NullObserver, &'static str> }
Now:
struct Ladder { history: History<String> }
struct Analysis<'h> { joint: Joint<'h> }
`History<K, T, D, O>`, all four defaulted. Bounds may reference later
parameters, so `D: Drift<T> = ConstantDrift` is legal in third position.
`Joint` gains the same defaults, so `Joint<'h, String>` spells it.
72 call sites swapped, and the reorder makes most of them shorter: 18
now read `History<String>` and the `&'static str` ones read `History`.
The two turbofished builders shrink from
`HistoryBuilder::<Untimed, _, _, String>::new()` to
`HistoryBuilder::<String, Untimed>::new()`.
`Joint` keeps `O` structurally, defaulted rather than removed. #72 notes
it never touches the observer, which is true — but it borrows the whole
`&'h History<K, T, D, O>` and calls `History::resolve_terms`, so dropping
the parameter means either a view type or moving that method off
`History`. The default already buys the entire user-visible benefit,
which was the spelling.
Refs #72.
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<String> {
|
|
let mut h: History<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);
|