docs: document the whole public surface and deny(missing_docs)
80 undocumented public items, including three that are first contact:
`History::current_skill` — the method the crate's own first example calls
— `EventBuilder`, the type `h.event(t)` hands you, and `Gaussian::mu()`.
Now zero, and `#![deny(missing_docs)]` keeps it that way.
Several docs are measurements rather than readings of the code:
- `Outcome::Ranked` says ranks are used ordinally, so `[0, 1, 2]` and
`[0, 5, 90]` are the same observation. Measured: bit-identical
posteriors for both.
- `OwnedGame::log_evidence` says two identically-rated competitors give
exactly `ln(0.5)`. Written as a doctest, so it runs.
- `Member::weight` says zero and negative are accepted. Measured.
- `ConvergenceReport::final_step` is `(|Δmu|, |Δsigma|)` in skill units,
NOT natural parameters. That one had to be traced through
`Gaussian::delta` rather than assumed from the neighbouring vocabulary.
- `GameOptions::score_sigma` rejects non-positive and NaN but accepts
`+inf`, which is what the guard actually says.
README: it is the front door for a crate on a private registry, and it
opened with a link dump followed by 130 lines on drift. The first
`record_winner → converge → current_skill` block was at line 226 of 307.
It now leads with what the crate is, an install line, a quickstart, a
"which entry point?" table, and the `converge`-is-strict rationale that
was the crate's most opinionated recent decision and went unmentioned.
The two canonical examples disagreed on spelling (`History::default()`
vs `History::builder().build()`, `current_skill("a")` vs
`current_skill(&"a")`); they now agree. Five new README blocks are
doctested, taking the suite from 19 to 25.
`pub use smallvec;`. Four public items name `SmallVec` in their
signatures, and the only `Joint` example failed to compile from a
consumer crate with `unresolved import smallvec` — the dependency was in
the API but not reachable. Both worked examples now use the re-export,
so they teach the path that works downstream.
Vocabulary, from #75: "agent" was a fourth word for competitor, 200
occurrences, and it had reached public signatures before #73 un-exported
`TimeSlice`. Now zero.
Closes #77. Refs #75.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011hcFjNDmHXZF8URGLku5zZ
This commit is contained in:
+80
-24
@@ -24,6 +24,19 @@ use crate::{
|
||||
tuple_gt, tuple_max,
|
||||
};
|
||||
|
||||
/// Configures a [`History`] before any events are added.
|
||||
///
|
||||
/// Everything a history needs that is not an event lives here: the prior
|
||||
/// (`mu`, `sigma`), the performance noise `beta`, the draw probability, the
|
||||
/// drift model, the convergence settings, the observer, and what to do about
|
||||
/// an unknown key. None of them can be changed after `build`, because they
|
||||
/// define the model the fit is of.
|
||||
///
|
||||
/// Two of the setters change the builder's *type* rather than a field —
|
||||
/// [`HistoryBuilder::drift`] and [`HistoryBuilder::observer`] — so bind the
|
||||
/// result rather than calling them on a `&mut`. [`HistoryBuilder::time_type`]
|
||||
/// and [`HistoryBuilder::key_type`] exist for the same reason: to name a type
|
||||
/// parameter that nothing in the call chain would otherwise infer.
|
||||
#[derive(Clone, Debug)]
|
||||
#[must_use = "a builder does nothing until `.build()`"]
|
||||
pub struct HistoryBuilder<
|
||||
@@ -99,6 +112,17 @@ impl<T: Time, D: Drift<T>, O: Observer<T>, K: Eq + Hash + Clone> HistoryBuilder<
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the drift model: how far skill may move between appearances.
|
||||
///
|
||||
/// Changes the builder's type, since `D` is a type parameter — bind the
|
||||
/// result. [`ConstantDrift`] is the default; a custom [`Drift`] impl is
|
||||
/// the way to express a calendar-dependent or per-competitor rule that
|
||||
/// elapsed ticks alone cannot.
|
||||
///
|
||||
/// Not validated here: the builder cannot inspect an arbitrary
|
||||
/// implementation. `converge` checks the variance each competitor actually
|
||||
/// accumulates and reports `InvalidParameter` if it is negative or
|
||||
/// non-finite.
|
||||
pub fn drift<D2: Drift<T>>(self, drift: D2) -> HistoryBuilder<T, D2, O, K> {
|
||||
HistoryBuilder {
|
||||
drift,
|
||||
@@ -248,6 +272,12 @@ impl<T: Time, D: Drift<T>, O: Observer<T>, K: Eq + Hash + Clone> HistoryBuilder<
|
||||
}
|
||||
}
|
||||
|
||||
/// Attach an [`Observer`] to be called as inference progresses.
|
||||
///
|
||||
/// Changes the builder's type — bind the result. The history takes the
|
||||
/// observer by value; to keep a handle on one that accumulates state, pass
|
||||
/// an `Arc` and keep a clone, or read it back with
|
||||
/// [`History::observer`] / [`History::into_observer`].
|
||||
pub fn observer<O2: Observer<T>>(self, observer: O2) -> HistoryBuilder<T, D, O2, K> {
|
||||
HistoryBuilder {
|
||||
mu: self.mu,
|
||||
@@ -264,6 +294,9 @@ impl<T: Time, D: Drift<T>, O: Observer<T>, K: Eq + Hash + Clone> HistoryBuilder<
|
||||
}
|
||||
}
|
||||
|
||||
/// Finish configuring and produce an empty [`History`].
|
||||
///
|
||||
/// Every parameter was validated as it was set, so this cannot fail.
|
||||
pub fn build(self) -> History<T, D, O, K> {
|
||||
History {
|
||||
size: 0,
|
||||
@@ -417,6 +450,12 @@ impl Default for History<i64, ConstantDrift, NullObserver, &'static str> {
|
||||
}
|
||||
|
||||
impl History<i64, ConstantDrift, NullObserver, &'static str> {
|
||||
/// Start configuring a history.
|
||||
///
|
||||
/// The defaults are `i64` time, [`ConstantDrift`], no observer and
|
||||
/// `&'static str` keys. Any of the four can be changed — the two type
|
||||
/// parameters that no argument would pin are named with
|
||||
/// [`HistoryBuilder::time_type`] and [`HistoryBuilder::key_type`].
|
||||
pub fn builder() -> HistoryBuilder<i64, ConstantDrift, NullObserver, &'static str> {
|
||||
HistoryBuilder::default()
|
||||
}
|
||||
@@ -444,6 +483,11 @@ impl<T: Time, K: Eq + Hash + Clone> HistoryBuilder<T, ConstantDrift, NullObserve
|
||||
}
|
||||
|
||||
impl<T: Time, D: Drift<T>, O: Observer<T>, K: Eq + Hash + Clone> History<T, D, O, K> {
|
||||
/// Promote a key to its [`Index`], creating the entry if it is new.
|
||||
///
|
||||
/// Interning a key does not register a competitor or give them a rating —
|
||||
/// it only reserves the slot. Use [`History::register`] to declare a
|
||||
/// competitor's configuration up front.
|
||||
pub fn intern<Q>(&mut self, key: &Q) -> Index
|
||||
where
|
||||
K: Borrow<Q>,
|
||||
@@ -452,6 +496,10 @@ impl<T: Time, D: Drift<T>, O: Observer<T>, K: Eq + Hash + Clone> History<T, D, O
|
||||
self.keys.get_or_create(key)
|
||||
}
|
||||
|
||||
/// Resolve an existing key to its [`Index`], or `None` if the history has
|
||||
/// never seen it.
|
||||
///
|
||||
/// The read-only counterpart of [`History::intern`]: it never creates.
|
||||
#[must_use]
|
||||
pub fn lookup<Q>(&self, key: &Q) -> Option<Index>
|
||||
where
|
||||
@@ -731,6 +779,15 @@ impl<T: Time, D: Drift<T>, O: Observer<T>, K: Eq + Hash + Clone> History<T, D, O
|
||||
.then(|| self.competitors[idx].rating)
|
||||
}
|
||||
|
||||
/// The competitor's latest posterior skill, or `None` if the history has
|
||||
/// never seen the key or they have no appearances.
|
||||
///
|
||||
/// "Latest" is their own last appearance, which need not be the last slice
|
||||
/// in the history. For everyone at once — a leaderboard — use
|
||||
/// [`History::current_skills`], which is one pass rather than one per key.
|
||||
///
|
||||
/// This reads whatever the fit currently holds. It does not converge, and
|
||||
/// it does not check that a previous `converge` succeeded.
|
||||
#[must_use]
|
||||
pub fn current_skill<Q>(&self, key: &Q) -> Option<Gaussian>
|
||||
where
|
||||
@@ -1144,8 +1201,7 @@ impl<T: Time, D: Drift<T>, O: Observer<T>, K: Eq + Hash + Clone> History<T, D, O
|
||||
// accident.
|
||||
if self.beta == 0.0 && gathered.iter().flatten().all(|g| g.sigma() == 0.0) {
|
||||
return Err(InferenceError::InvalidParameter {
|
||||
name: "beta is zero and every skill is a point mass, so there is \
|
||||
no performance distribution to predict from",
|
||||
name: "beta with point-mass skills",
|
||||
value: 0.0,
|
||||
});
|
||||
}
|
||||
@@ -1160,7 +1216,7 @@ impl<T: Time, D: Drift<T>, O: Observer<T>, K: Eq + Hash + Clone> History<T, D, O
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// As [`History::member_skills`].
|
||||
/// As `member_skills`.
|
||||
fn performances(&self, teams: &[&[&K]]) -> Result<(Vec<Gaussian>, Vec<usize>), InferenceError>
|
||||
where
|
||||
K: std::fmt::Debug,
|
||||
@@ -1566,7 +1622,7 @@ impl<T: Time, D: Drift<T>, O: Observer<T>, K: Eq + Hash + Clone> History<T, D, O
|
||||
/// substitution — and returns exactly what the one-shot call would.
|
||||
///
|
||||
/// ```
|
||||
/// # use smallvec::smallvec;
|
||||
/// # use trueskill_tt::smallvec::smallvec;
|
||||
/// # use trueskill_tt::{Event, History, Member, Outcome, Team};
|
||||
/// # let mut h = History::builder().score_sigma(1.0).build();
|
||||
/// # let round = |x, y, sx, sy, t| Event {
|
||||
@@ -1928,7 +1984,7 @@ impl<T: Time, D: Drift<T>, O: Observer<T>, K: Eq + Hash + Clone> History<T, D, O
|
||||
///
|
||||
/// It used to return `Ok` with `converged: false`, which was the worst
|
||||
/// available shape. A fit that stops short is *wrong by a little*: every
|
||||
/// rating is finite, the ordering looks sensible, and nothing about the
|
||||
/// posterior is finite, the ordering looks sensible, and nothing about the
|
||||
/// output says the numbers were still moving. Detection was opt-in, and
|
||||
/// `let _ = h.converge()` silently opted out — which is how a real defect
|
||||
/// hid in this crate's own test suite.
|
||||
@@ -2248,14 +2304,14 @@ impl<T: Time, D: Drift<T>, O: Observer<T>, K: Eq + Hash + Clone> History<T, D, O
|
||||
|
||||
competitor::clean(self.competitors.values_mut(), true);
|
||||
|
||||
let mut this_agent = Vec::with_capacity(1024);
|
||||
let mut these_competitors = Vec::with_capacity(1024);
|
||||
|
||||
for competitor in composition.iter().flatten().flatten() {
|
||||
if this_agent.contains(competitor) {
|
||||
if these_competitors.contains(competitor) {
|
||||
continue;
|
||||
}
|
||||
|
||||
this_agent.push(*competitor);
|
||||
these_competitors.push(*competitor);
|
||||
|
||||
// From `declared` rather than `priors`: a competitor configured by
|
||||
// `register` before any event has nothing in this batch's map.
|
||||
@@ -2355,17 +2411,17 @@ impl<T: Time, D: Drift<T>, O: Observer<T>, K: Eq + Hash + Clone> History<T, D, O
|
||||
time_slice.new_forward_info(&self.competitors);
|
||||
}
|
||||
|
||||
for agent_idx in &this_agent {
|
||||
if let Some(skill) = time_slice.skills.get_mut(*agent_idx) {
|
||||
for competitor_idx in &these_competitors {
|
||||
if let Some(skill) = time_slice.skills.get_mut(*competitor_idx) {
|
||||
skill.elapsed = time_slice::compute_elapsed(
|
||||
self.competitors[*agent_idx].last_time.as_ref(),
|
||||
self.competitors[*competitor_idx].last_time.as_ref(),
|
||||
&time_slice.time,
|
||||
);
|
||||
|
||||
let competitor = self.competitors.get_mut(*agent_idx).unwrap();
|
||||
let competitor = self.competitors.get_mut(*competitor_idx).unwrap();
|
||||
|
||||
competitor.last_time = Some(time_slice.time);
|
||||
competitor.message = Some(time_slice.forward_prior_out(agent_idx));
|
||||
competitor.message = Some(time_slice.forward_prior_out(competitor_idx));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2400,11 +2456,11 @@ impl<T: Time, D: Drift<T>, O: Observer<T>, K: Eq + Hash + Clone> History<T, D, O
|
||||
&self.competitors,
|
||||
);
|
||||
|
||||
for agent_idx in time_slice.skills.keys() {
|
||||
let competitor = self.competitors.get_mut(agent_idx).unwrap();
|
||||
for competitor_idx in time_slice.skills.keys() {
|
||||
let competitor = self.competitors.get_mut(competitor_idx).unwrap();
|
||||
|
||||
competitor.last_time = Some(t);
|
||||
competitor.message = Some(time_slice.forward_prior_out(&agent_idx));
|
||||
competitor.message = Some(time_slice.forward_prior_out(&competitor_idx));
|
||||
}
|
||||
|
||||
k += 1;
|
||||
@@ -2422,11 +2478,11 @@ impl<T: Time, D: Drift<T>, O: Observer<T>, K: Eq + Hash + Clone> History<T, D, O
|
||||
|
||||
let time_slice = &self.time_slices[k];
|
||||
|
||||
for agent_idx in time_slice.skills.keys() {
|
||||
let competitor = self.competitors.get_mut(agent_idx).unwrap();
|
||||
for competitor_idx in time_slice.skills.keys() {
|
||||
let competitor = self.competitors.get_mut(competitor_idx).unwrap();
|
||||
|
||||
competitor.last_time = Some(t);
|
||||
competitor.message = Some(time_slice.forward_prior_out(&agent_idx));
|
||||
competitor.message = Some(time_slice.forward_prior_out(&competitor_idx));
|
||||
}
|
||||
|
||||
k += 1;
|
||||
@@ -2440,17 +2496,17 @@ impl<T: Time, D: Drift<T>, O: Observer<T>, K: Eq + Hash + Clone> History<T, D, O
|
||||
|
||||
time_slice.new_forward_info(&self.competitors);
|
||||
|
||||
for agent_idx in &this_agent {
|
||||
if let Some(skill) = time_slice.skills.get_mut(*agent_idx) {
|
||||
for competitor_idx in &these_competitors {
|
||||
if let Some(skill) = time_slice.skills.get_mut(*competitor_idx) {
|
||||
skill.elapsed = time_slice::compute_elapsed(
|
||||
self.competitors[*agent_idx].last_time.as_ref(),
|
||||
self.competitors[*competitor_idx].last_time.as_ref(),
|
||||
&time_slice.time,
|
||||
);
|
||||
|
||||
let competitor = self.competitors.get_mut(*agent_idx).unwrap();
|
||||
let competitor = self.competitors.get_mut(*competitor_idx).unwrap();
|
||||
|
||||
competitor.last_time = Some(time_slice.time);
|
||||
competitor.message = Some(time_slice.forward_prior_out(agent_idx));
|
||||
competitor.message = Some(time_slice.forward_prior_out(competitor_idx));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user