refactor: one word per concept

Three vocabulary collisions, from #75.

**"rating" meant three things**, one the opposite of the exported type.
`Rating` is documented as static *configuration* — "this returns what it
was told", against every other accessor's "what inference inferred". But
`quality`'s parameter was `rating_groups: &[&[Gaussian]]` and its prose
said "rating groups" four times, where "rating" means a *posterior* — the
one thing `Rating` is documented not to be. Two error messages used it
that way too.

So a reader who learned `Rating = config` passed `Rating` values to
`quality`, which takes `Gaussian`; and one who learned "rating = what
comes out" was baffled that `h.rating(&k)` is not their skill.

"rating" is now reserved for the type. `quality(teams: &[&[Gaussian]])`,
and "every rating is finite" became "every posterior is finite".

**"agent" was a private fourth name for a competitor** — ~200 identifiers
against 236 uses of "competitor", and it leaked into two `pub` signatures
on `TimeSlice`. Now that #73 has made those internal this is a pure
rename, so the crate has one word for the entity throughout.

**"player" survived in one public signature** — `free_for_all(players:)`
plus two doc lines. Renamed, along with three internal closure bindings.
Doc examples that use "player" as a *key* are left alone: that is a
user's data, not the crate's vocabulary.

The panic-message expectations in tests/quality.rs moved with the prose,
which is the point of asserting on message text — the tests caught the
rename rather than papering over it.

Not touched: "performance" (always skill widened by beta), "skill",
"member" and "team" are each used for exactly one thing already.

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:
2026-09-09 20:54:47 +02:00
co-authored by Claude Opus 5
parent 85c4d0d87d
commit fdd1539cab
6 changed files with 234 additions and 207 deletions
+93 -88
View File
@@ -50,12 +50,12 @@ pub enum EventKind {
#[derive(Clone, Debug)]
struct Item {
agent: Index,
competitor: Index,
/// This competitor's slot in the owning slice's `SkillStore`, resolved
/// once at ingestion.
///
/// The convergence loop reaches skills through this rather than through
/// `agent`, which is what keeps `HashMap` hashing out of the hot path now
/// `competitor`, which is what keeps `HashMap` hashing out of the hot path now
/// that the store is compact rather than indexed by the global `Index`.
slot: u32,
likelihood: Gaussian,
@@ -66,9 +66,9 @@ impl Item {
&self,
forward: bool,
skills: &SkillStore,
agents: &CompetitorStore<T, D>,
competitors: &CompetitorStore<T, D>,
) -> Rating<T, D> {
let r = &agents[self.agent].rating;
let r = &competitors[self.competitor].rating;
let skill = skills.at(self.slot);
if forward {
@@ -98,7 +98,7 @@ impl Event {
pub(crate) fn iter_agents(&self) -> impl Iterator<Item = Index> + '_ {
self.teams
.iter()
.flat_map(|t| t.items.iter().map(|it| it.agent))
.flat_map(|t| t.items.iter().map(|it| it.competitor))
}
fn outputs(&self) -> Vec<f64> {
@@ -112,14 +112,14 @@ impl Event {
&self,
forward: bool,
skills: &SkillStore,
agents: &CompetitorStore<T, D>,
competitors: &CompetitorStore<T, D>,
) -> Vec<Vec<Rating<T, D>>> {
self.teams
.iter()
.map(|team| {
team.items
.iter()
.map(|item| item.within_prior(forward, skills, agents))
.map(|item| item.within_prior(forward, skills, competitors))
.collect::<Vec<_>>()
})
.collect::<Vec<_>>()
@@ -133,12 +133,12 @@ impl Event {
fn compute<T: Time, D: Drift<T>>(
&self,
skills: &SkillStore,
agents: &CompetitorStore<T, D>,
competitors: &CompetitorStore<T, D>,
p_draw: f64,
convergence: crate::ConvergenceOptions,
arena: &mut ScratchArena,
) -> EventUpdate {
let teams = self.within_priors(false, skills, agents);
let teams = self.within_priors(false, skills, competitors);
let result = self.outputs();
let g = match self.kind {
EventKind::Ranked => {
@@ -179,12 +179,12 @@ impl Event {
fn iteration_direct<T: Time, D: Drift<T>>(
&mut self,
skills: &mut SkillStore,
agents: &CompetitorStore<T, D>,
competitors: &CompetitorStore<T, D>,
p_draw: f64,
convergence: crate::ConvergenceOptions,
arena: &mut ScratchArena,
) {
let update = self.compute(skills, agents, p_draw, convergence, arena);
let update = self.compute(skills, competitors, p_draw, convergence, arena);
self.apply(skills, update);
}
}
@@ -288,7 +288,7 @@ impl<T: Time> TimeSlice<T> {
results: Option<Vec<Vec<f64>>>,
weights: Option<Vec<Vec<Vec<f64>>>>,
kinds: Vec<EventKind>,
agents: &CompetitorStore<T, D>,
competitors: &CompetitorStore<T, D>,
) {
let mut unique = Vec::with_capacity(10);
@@ -303,9 +303,9 @@ impl<T: Time> TimeSlice<T> {
});
for idx in this_agent {
let elapsed = compute_elapsed(agents[*idx].last_time.as_ref(), &self.time);
let elapsed = compute_elapsed(competitors[*idx].last_time.as_ref(), &self.time);
let forward = agents[*idx].receive(&self.time);
let forward = competitors[*idx].receive(&self.time);
if let Some(skill) = self.skills.get_mut(*idx) {
skill.elapsed = elapsed;
@@ -332,12 +332,12 @@ impl<T: Time> TimeSlice<T> {
.map(|(t, team)| {
let items = team
.iter()
.map(|&agent| Item {
agent,
.map(|&competitor| Item {
competitor,
// Every participant was inserted into `skills`
// just above, so the slot always resolves.
slot: skills
.slot_of(agent)
.slot_of(competitor)
.expect("participant must be present in the slice store"),
likelihood: N_INF,
})
@@ -376,7 +376,7 @@ impl<T: Time> TimeSlice<T> {
self.color_groups_dirty = true;
self.iteration(from, agents);
self.iteration(from, competitors);
}
pub(crate) fn posteriors(&self) -> HashMap<Index, Gaussian> {
@@ -393,7 +393,7 @@ impl<T: Time> TimeSlice<T> {
/// Panics if an event references a competitor with no entry in this
/// slice's skill store. `add_events` inserts one for every participant, so
/// this cannot happen for slices built through the public API.
pub fn iteration<D: Drift<T>>(&mut self, from: usize, agents: &CompetitorStore<T, D>) {
pub fn iteration<D: Drift<T>>(&mut self, from: usize, competitors: &CompetitorStore<T, D>) {
if from == 0 && self.color_groups_dirty {
self.recompute_color_groups();
}
@@ -401,7 +401,7 @@ impl<T: Time> TimeSlice<T> {
if from > 0 || self.color_groups.is_empty() {
// Initial pass (add_events) or no color groups yet: simple sequential sweep.
for event in self.events.iter_mut().skip(from) {
let teams = event.within_priors(false, &self.skills, agents);
let teams = event.within_priors(false, &self.skills, competitors);
let result = event.outputs();
let g = match event.kind {
@@ -436,14 +436,14 @@ impl<T: Time> TimeSlice<T> {
event.log_evidence = g.log_evidence;
}
} else {
self.sweep_color_groups(agents);
self.sweep_color_groups(competitors);
}
}
/// Full event sweep using the color-group partition. Colors are processed
/// sequentially; within each color the inner loop is parallel under rayon.
///
/// Events in one color group touch disjoint agent sets, so none of them
/// Events in one color group touch disjoint competitor sets, so none of them
/// can observe another's writes. That makes the sweep separable: inference
/// runs concurrently over shared `&self.skills`, and the resulting updates
/// are folded in afterwards in index order. Splitting it this way needs no
@@ -451,7 +451,7 @@ impl<T: Time> TimeSlice<T> {
/// across thread counts because the apply order does not depend on which
/// worker finished first.
#[cfg(feature = "rayon")]
fn sweep_color_groups<D: Drift<T>>(&mut self, agents: &CompetitorStore<T, D>) {
fn sweep_color_groups<D: Drift<T>>(&mut self, competitors: &CompetitorStore<T, D>) {
use rayon::prelude::*;
thread_local! {
@@ -483,7 +483,7 @@ impl<T: Time> TimeSlice<T> {
let mut arena = cell.borrow_mut();
arena.reset();
ev.compute(skills, agents, p_draw, convergence, &mut arena)
ev.compute(skills, competitors, p_draw, convergence, &mut arena)
})
})
.collect();
@@ -495,7 +495,7 @@ impl<T: Time> TimeSlice<T> {
for ev in &mut self.events[range] {
ev.iteration_direct(
&mut self.skills,
agents,
competitors,
p_draw,
self.convergence,
&mut self.arena,
@@ -509,7 +509,7 @@ impl<T: Time> TimeSlice<T> {
/// Events within each color group are updated inline — no EventOutput allocation —
/// matching the T2 performance profile.
#[cfg(not(feature = "rayon"))]
fn sweep_color_groups<D: Drift<T>>(&mut self, agents: &CompetitorStore<T, D>) {
fn sweep_color_groups<D: Drift<T>>(&mut self, competitors: &CompetitorStore<T, D>) {
for color_idx in 0..self.color_groups.groups.len() {
if self.color_groups.groups[color_idx].is_empty() {
continue;
@@ -523,7 +523,7 @@ impl<T: Time> TimeSlice<T> {
for ev in &mut self.events[range] {
ev.iteration_direct(
&mut self.skills,
agents,
competitors,
p_draw,
self.convergence,
&mut self.arena,
@@ -544,7 +544,7 @@ impl<T: Time> TimeSlice<T> {
/// schedule default.
pub(crate) fn iterate_to_convergence<D: Drift<T>>(
&mut self,
agents: &CompetitorStore<T, D>,
competitors: &CompetitorStore<T, D>,
) -> usize {
use crate::{tuple_gt, tuple_max};
@@ -557,7 +557,7 @@ impl<T: Time> TimeSlice<T> {
while tuple_gt(step, epsilon) && i < max_iter {
let old = self.posteriors();
self.iteration(0, agents);
self.iteration(0, competitors);
let new = self.posteriors();
@@ -575,37 +575,37 @@ impl<T: Time> TimeSlice<T> {
i
}
pub(crate) fn forward_prior_out(&self, agent: &Index) -> Gaussian {
let skill = self.skills.get(*agent).unwrap();
pub(crate) fn forward_prior_out(&self, competitor: &Index) -> Gaussian {
let skill = self.skills.get(*competitor).unwrap();
skill.forward * skill.likelihood
}
pub(crate) fn backward_prior_out<D: Drift<T>>(
&self,
agent: &Index,
agents: &CompetitorStore<T, D>,
competitor: &Index,
competitors: &CompetitorStore<T, D>,
) -> Gaussian {
let skill = self.skills.get(*agent).unwrap();
let skill = self.skills.get(*competitor).unwrap();
let n = skill.likelihood * skill.backward;
n.forget(
agents[*agent]
competitors[*competitor]
.rating
.drift_variance_for_elapsed(skill.elapsed),
)
}
pub(crate) fn new_backward_info<D: Drift<T>>(&mut self, agents: &CompetitorStore<T, D>) {
for (agent, skill) in self.skills.iter_mut() {
skill.backward = agents[agent].message.unwrap_or(N_INF);
pub(crate) fn new_backward_info<D: Drift<T>>(&mut self, competitors: &CompetitorStore<T, D>) {
for (competitor, skill) in self.skills.iter_mut() {
skill.backward = competitors[competitor].message.unwrap_or(N_INF);
}
self.iteration(0, agents);
self.iteration(0, competitors);
}
pub(crate) fn new_forward_info<D: Drift<T>>(&mut self, agents: &CompetitorStore<T, D>) {
for (agent, skill) in self.skills.iter_mut() {
skill.forward = agents[agent].receive_for_elapsed(skill.elapsed);
pub(crate) fn new_forward_info<D: Drift<T>>(&mut self, competitors: &CompetitorStore<T, D>) {
for (competitor, skill) in self.skills.iter_mut() {
skill.forward = competitors[competitor].receive_for_elapsed(skill.elapsed);
}
self.iteration(0, agents);
self.iteration(0, competitors);
}
/// Run this slice's events on forward (filtering) information alone.
@@ -618,7 +618,7 @@ impl<T: Time> TimeSlice<T> {
pub(crate) fn filtered_step<D: Drift<T>>(
&self,
incoming: &HashMap<Index, Gaussian>,
agents: &CompetitorStore<T, D>,
competitors: &CompetitorStore<T, D>,
) -> FilteredStep {
let mut scratch = TimeSlice {
events: self.events.clone(),
@@ -641,16 +641,16 @@ impl<T: Time> TimeSlice<T> {
event.log_evidence = 0.0;
}
for (agent, skill) in self.skills.iter() {
let rating = &agents[agent].rating;
for (competitor, skill) in self.skills.iter() {
let rating = &competitors[competitor].rating;
let forward = match incoming.get(&agent) {
let forward = match incoming.get(&competitor) {
Some(message) => message.forget(rating.drift_variance_for_elapsed(skill.elapsed)),
None => rating.prior,
};
let slot = scratch.skills.insert(
agent,
competitor,
Skill {
forward,
backward: N_INF,
@@ -666,19 +666,19 @@ impl<T: Time> TimeSlice<T> {
// than leave it to be rediscovered after it breaks.
debug_assert_eq!(
Some(slot),
self.skills.slot_of(agent),
"scratch slot must match the real slice's slot for {agent:?}"
self.skills.slot_of(competitor),
"scratch slot must match the real slice's slot for {competitor:?}"
);
}
scratch.iterate_to_convergence(agents);
scratch.iterate_to_convergence(competitors);
FilteredStep {
log_evidence: scratch.events.iter().map(|event| event.log_evidence).sum(),
posteriors: scratch
.skills
.iter()
.map(|(agent, skill)| (agent, skill.posterior()))
.map(|(competitor, skill)| (competitor, skill.posterior()))
.collect(),
}
}
@@ -687,7 +687,7 @@ impl<T: Time> TimeSlice<T> {
&self,
targets: &[Index],
forward: bool,
agents: &CompetitorStore<T, D>,
competitors: &CompetitorStore<T, D>,
) -> f64 {
// Hashed once rather than scanned per player per event, so a
// `log_evidence_for` with many keys is not quadratic.
@@ -696,7 +696,7 @@ impl<T: Time> TimeSlice<T> {
let mut arena = ScratchArena::new();
let run_event = |event: &Event, arena: &mut ScratchArena| -> f64 {
let teams = event.within_priors(forward, &self.skills, agents);
let teams = event.within_priors(forward, &self.skills, competitors);
let result = event.outputs();
match event.kind {
EventKind::Ranked => {
@@ -741,7 +741,7 @@ impl<T: Time> TimeSlice<T> {
.teams
.iter()
.flat_map(|team| &team.items)
.any(|item| target_set.contains(&item.agent))
.any(|item| target_set.contains(&item.competitor))
})
.map(|event| run_event(event, &mut arena))
.sum()
@@ -753,7 +753,7 @@ impl<T: Time> TimeSlice<T> {
.teams
.iter()
.flat_map(|team| &team.items)
.any(|item| target_set.contains(&item.agent))
.any(|item| target_set.contains(&item.competitor))
})
.map(|event| event.log_evidence)
.sum()
@@ -769,7 +769,12 @@ impl<T: Time> TimeSlice<T> {
event
.teams
.iter()
.map(|team| team.items.iter().map(|item| item.agent).collect::<Vec<_>>())
.map(|team| {
team.items
.iter()
.map(|item| item.competitor)
.collect::<Vec<_>>()
})
.collect::<Vec<_>>()
})
.collect::<Vec<_>>()
@@ -831,7 +836,7 @@ impl<T: Time> TimeSlice<T> {
/// approximations that inference does not retain.
pub(crate) fn scored_contrasts<D: Drift<T>>(
&self,
agents: &CompetitorStore<T, D>,
competitors: &CompetitorStore<T, D>,
) -> Vec<(Vec<(Index, f64)>, f64)> {
let mut out = Vec::new();
@@ -857,8 +862,8 @@ impl<T: Time> TimeSlice<T> {
for (team, sign) in [(hi, 1.0), (lo, -1.0)] {
for (m, item) in event.teams[team].items.iter().enumerate() {
let w = event.weights[team][m];
noise += w * w * agents[item.agent].rating.beta.powi(2);
contrast.push((item.agent, sign * w));
noise += w * w * competitors[item.competitor].rating.beta.powi(2);
contrast.push((item.competitor, sign * w));
}
}
@@ -906,11 +911,11 @@ mod tests {
let e = index_map.get_or_create("e");
let f = index_map.get_or_create("f");
let mut agents: CompetitorStore<i64, ConstantDrift> = CompetitorStore::new();
let mut competitors: CompetitorStore<i64, ConstantDrift> = CompetitorStore::new();
for agent in [a, b, c, d, e, f] {
agents.insert(
agent,
for competitor in [a, b, c, d, e, f] {
competitors.insert(
competitor,
Competitor {
rating: Rating::new(
Gaussian::from_ms(25.0, 25.0 / 3.0),
@@ -933,7 +938,7 @@ mod tests {
Some(vec![vec![1.0, 0.0], vec![0.0, 1.0], vec![1.0, 0.0]]),
None,
vec![EventKind::Ranked; 3],
&agents,
&competitors,
);
let post = time_slice.posteriors();
@@ -969,7 +974,7 @@ mod tests {
epsilon = 1e-6
);
assert_eq!(time_slice.iterate_to_convergence(&agents), 1);
assert_eq!(time_slice.iterate_to_convergence(&competitors), 1);
}
#[test]
@@ -983,11 +988,11 @@ mod tests {
let e = index_map.get_or_create("e");
let f = index_map.get_or_create("f");
let mut agents: CompetitorStore<i64, ConstantDrift> = CompetitorStore::new();
let mut competitors: CompetitorStore<i64, ConstantDrift> = CompetitorStore::new();
for agent in [a, b, c, d, e, f] {
agents.insert(
agent,
for competitor in [a, b, c, d, e, f] {
competitors.insert(
competitor,
Competitor {
rating: Rating::new(
Gaussian::from_ms(25.0, 25.0 / 3.0),
@@ -1010,7 +1015,7 @@ mod tests {
Some(vec![vec![1.0, 0.0], vec![0.0, 1.0], vec![1.0, 0.0]]),
None,
vec![EventKind::Ranked; 3],
&agents,
&competitors,
);
let post = time_slice.posteriors();
@@ -1031,7 +1036,7 @@ mod tests {
epsilon = 1e-6
);
assert!(time_slice.iterate_to_convergence(&agents) > 1);
assert!(time_slice.iterate_to_convergence(&competitors) > 1);
let post = time_slice.posteriors();
@@ -1063,11 +1068,11 @@ mod tests {
let e = index_map.get_or_create("e");
let f = index_map.get_or_create("f");
let mut agents: CompetitorStore<i64, ConstantDrift> = CompetitorStore::new();
let mut competitors: CompetitorStore<i64, ConstantDrift> = CompetitorStore::new();
for agent in [a, b, c, d, e, f] {
agents.insert(
agent,
for competitor in [a, b, c, d, e, f] {
competitors.insert(
competitor,
Competitor {
rating: Rating::new(
Gaussian::from_ms(25.0, 25.0 / 3.0),
@@ -1090,10 +1095,10 @@ mod tests {
Some(vec![vec![1.0, 0.0], vec![0.0, 1.0], vec![1.0, 0.0]]),
None,
vec![EventKind::Ranked; 3],
&agents,
&competitors,
);
time_slice.iterate_to_convergence(&agents);
time_slice.iterate_to_convergence(&competitors);
let post = time_slice.posteriors();
@@ -1122,12 +1127,12 @@ mod tests {
Some(vec![vec![1.0, 0.0], vec![0.0, 1.0], vec![1.0, 0.0]]),
None,
vec![EventKind::Ranked; 3],
&agents,
&competitors,
);
assert_eq!(time_slice.events.len(), 6);
time_slice.iterate_to_convergence(&agents);
time_slice.iterate_to_convergence(&competitors);
let post = time_slice.posteriors();
@@ -1166,11 +1171,11 @@ mod tests {
let c = index_map.get_or_create("c");
let d = index_map.get_or_create("d");
let mut agents: CompetitorStore<i64, ConstantDrift> = CompetitorStore::new();
let mut competitors: CompetitorStore<i64, ConstantDrift> = CompetitorStore::new();
for agent in [a, b, c, d] {
agents.insert(
agent,
for competitor in [a, b, c, d] {
competitors.insert(
competitor,
Competitor {
rating: Rating::new(
Gaussian::from_ms(25.0, 25.0 / 3.0),
@@ -1193,7 +1198,7 @@ mod tests {
Some(vec![vec![1.0, 0.0], vec![1.0, 0.0], vec![1.0, 0.0]]),
None,
vec![EventKind::Ranked; 3],
&agents,
&competitors,
);
assert_eq!(ts.color_groups.n_colors(), 2);
@@ -1204,14 +1209,14 @@ mod tests {
assert_eq!(ts.color_groups.color_range(1), 2..3);
// Events at positions 0 and 1 (color 0) must be disjoint — verify by
// checking that the agent sets of self.events[0] and self.events[1] do
// not include the agent at self.events[2].
// checking that the competitor sets of self.events[0] and self.events[1] do
// not include the competitor at self.events[2].
let agents_in_ev2: Vec<Index> = ts.events[2].iter_agents().collect();
let agents_in_ev0: Vec<Index> = ts.events[0].iter_agents().collect();
let agents_in_ev1: Vec<Index> = ts.events[1].iter_agents().collect();
// ev0 and ev1 must be disjoint from each other (color-0 invariant).
assert!(agents_in_ev0.iter().all(|ag| !agents_in_ev1.contains(ag)));
// ev2 must share an agent with ev0 or ev1 (it needed its own color).
// ev2 must share an competitor with ev0 or ev1 (it needed its own color).
let ev2_overlaps_ev0 = agents_in_ev2.iter().any(|ag| agents_in_ev0.contains(ag));
let ev2_overlaps_ev1 = agents_in_ev2.iter().any(|ag| agents_in_ev1.contains(ag));
assert!(ev2_overlaps_ev0 || ev2_overlaps_ev1);