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:
+1
-1
@@ -73,7 +73,7 @@ pub enum InferenceError {
|
|||||||
/// `epsilon`.
|
/// `epsilon`.
|
||||||
///
|
///
|
||||||
/// A fit that stops short is wrong by a little, which is the worst
|
/// A fit that stops short is wrong by a little, which is the worst
|
||||||
/// available failure: every rating is finite, the ordering looks sensible,
|
/// available failure: every posterior is finite, the ordering looks sensible,
|
||||||
/// and nothing in the numbers says they were still moving. Reported rather
|
/// and nothing in the numbers says they were still moving. Reported rather
|
||||||
/// than returned as a flag on an `Ok`, because a flag has to be checked
|
/// than returned as a flag on an `Ok`, because a flag has to be checked
|
||||||
/// and `let _ = h.converge()` is the natural way not to.
|
/// and `let _ = h.converge()` is the natural way not to.
|
||||||
|
|||||||
+16
-14
@@ -284,7 +284,9 @@ impl<'a, T: Time, D: Drift<T>> Game<'a, T, D> {
|
|||||||
self.teams[t]
|
self.teams[t]
|
||||||
.iter()
|
.iter()
|
||||||
.zip(self.weights[t].iter())
|
.zip(self.weights[t].iter())
|
||||||
.fold(N00, |p, (player, &w)| p + (player.performance() * w))
|
.fold(N00, |p, (competitor, &w)| {
|
||||||
|
p + (competitor.performance() * w)
|
||||||
|
})
|
||||||
}));
|
}));
|
||||||
|
|
||||||
let n_diffs = n_teams.saturating_sub(1);
|
let n_diffs = n_teams.saturating_sub(1);
|
||||||
@@ -361,18 +363,18 @@ impl<'a, T: Time, D: Drift<T>> Game<'a, T, D> {
|
|||||||
.iter()
|
.iter()
|
||||||
.zip(self.weights.iter())
|
.zip(self.weights.iter())
|
||||||
.enumerate()
|
.enumerate()
|
||||||
.map(|(orig_i, (players, weights))| {
|
.map(|(orig_i, (competitors, weights))| {
|
||||||
let si = arena.inv_buf[orig_i];
|
let si = arena.inv_buf[orig_i];
|
||||||
let m = arena.lhood_win[si] * arena.lhood_lose[si];
|
let m = arena.lhood_win[si] * arena.lhood_lose[si];
|
||||||
// Already folded into `team_prior` at the top of the chain,
|
// Already folded into `team_prior` at the top of the chain,
|
||||||
// indexed by sorted position.
|
// indexed by sorted position.
|
||||||
let performance = arena.team_prior[si];
|
let performance = arena.team_prior[si];
|
||||||
players
|
competitors
|
||||||
.iter()
|
.iter()
|
||||||
.zip(weights.iter())
|
.zip(weights.iter())
|
||||||
.map(|(player, &w)| {
|
.map(|(competitor, &w)| {
|
||||||
((m - performance.exclude(player.performance() * w)) * (1.0 / w))
|
((m - performance.exclude(competitor.performance() * w)) * (1.0 / w))
|
||||||
.forget(player.beta.powi(2))
|
.forget(competitor.beta.powi(2))
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
})
|
})
|
||||||
@@ -577,7 +579,7 @@ impl<T: Time, D: Drift<T>> Game<'_, T, D> {
|
|||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Convenience wrapper over [`Game::ranked`] for two single-player teams.
|
/// Convenience wrapper over [`Game::ranked`] for two single-competitor teams.
|
||||||
///
|
///
|
||||||
/// # Errors
|
/// # Errors
|
||||||
///
|
///
|
||||||
@@ -597,14 +599,14 @@ impl<T: Time, D: Drift<T>> Game<'_, T, D> {
|
|||||||
|
|
||||||
/// # Errors
|
/// # Errors
|
||||||
///
|
///
|
||||||
/// Wraps each player in a one-member team and delegates to
|
/// Wraps each competitor in a one-member team and delegates to
|
||||||
/// [`Game::ranked`], so it returns the same errors.
|
/// [`Game::ranked`], so it returns the same errors.
|
||||||
pub fn free_for_all(
|
pub fn free_for_all(
|
||||||
players: &[&Rating<T, D>],
|
competitors: &[&Rating<T, D>],
|
||||||
outcome: crate::Outcome,
|
outcome: crate::Outcome,
|
||||||
options: &GameOptions,
|
options: &GameOptions,
|
||||||
) -> Result<OwnedGame<T, D>, crate::InferenceError> {
|
) -> Result<OwnedGame<T, D>, crate::InferenceError> {
|
||||||
let teams: Vec<Vec<Rating<T, D>>> = players.iter().map(|p| vec![**p]).collect();
|
let teams: Vec<Vec<Rating<T, D>>> = competitors.iter().map(|p| vec![**p]).collect();
|
||||||
let team_refs: Vec<&[Rating<T, D>]> = teams.iter().map(|t| t.as_slice()).collect();
|
let team_refs: Vec<&[Rating<T, D>]> = teams.iter().map(|t| t.as_slice()).collect();
|
||||||
Self::ranked(&team_refs, outcome, options)
|
Self::ranked(&team_refs, outcome, options)
|
||||||
}
|
}
|
||||||
@@ -1428,8 +1430,8 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn run_chain_honours_max_iter_in_convergence_options() {
|
fn run_chain_honours_max_iter_in_convergence_options() {
|
||||||
let players: Vec<R> = (0..4).map(|_| R::default()).collect();
|
let competitors: Vec<R> = (0..4).map(|_| R::default()).collect();
|
||||||
let teams: Vec<Vec<_>> = players.iter().map(|p| vec![*p]).collect();
|
let teams: Vec<Vec<_>> = competitors.iter().map(|p| vec![*p]).collect();
|
||||||
let result = vec![3.0, 2.0, 1.0, 0.0];
|
let result = vec![3.0, 2.0, 1.0, 0.0];
|
||||||
let weights = vec![vec![1.0]; 4];
|
let weights = vec![vec![1.0]; 4];
|
||||||
|
|
||||||
@@ -1476,8 +1478,8 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn run_chain_with_damping_converges_to_same_posterior() {
|
fn run_chain_with_damping_converges_to_same_posterior() {
|
||||||
let players: Vec<R> = (0..4).map(|_| R::default()).collect();
|
let competitors: Vec<R> = (0..4).map(|_| R::default()).collect();
|
||||||
let teams: Vec<Vec<_>> = players.iter().map(|p| vec![*p]).collect();
|
let teams: Vec<Vec<_>> = competitors.iter().map(|p| vec![*p]).collect();
|
||||||
let result = vec![3.0, 2.0, 1.0, 0.0];
|
let result = vec![3.0, 2.0, 1.0, 0.0];
|
||||||
let weights = vec![vec![1.0]; 4];
|
let weights = vec![vec![1.0]; 4];
|
||||||
|
|
||||||
|
|||||||
+110
-90
@@ -268,7 +268,7 @@ impl<T: Time, D: Drift<T>, O: Observer<T>, K: Eq + Hash + Clone> HistoryBuilder<
|
|||||||
History {
|
History {
|
||||||
size: 0,
|
size: 0,
|
||||||
time_slices: Vec::new(),
|
time_slices: Vec::new(),
|
||||||
agents: CompetitorStore::new(),
|
competitors: CompetitorStore::new(),
|
||||||
keys: KeyTable::new(),
|
keys: KeyTable::new(),
|
||||||
mu: self.mu,
|
mu: self.mu,
|
||||||
sigma: self.sigma,
|
sigma: self.sigma,
|
||||||
@@ -391,7 +391,7 @@ pub struct History<
|
|||||||
> {
|
> {
|
||||||
size: usize,
|
size: usize,
|
||||||
pub(crate) time_slices: Vec<TimeSlice<T>>,
|
pub(crate) time_slices: Vec<TimeSlice<T>>,
|
||||||
pub(crate) agents: CompetitorStore<T, D>,
|
pub(crate) competitors: CompetitorStore<T, D>,
|
||||||
keys: KeyTable<K>,
|
keys: KeyTable<K>,
|
||||||
mu: f64,
|
mu: f64,
|
||||||
sigma: f64,
|
sigma: f64,
|
||||||
@@ -470,17 +470,18 @@ impl<T: Time, D: Drift<T>, O: Observer<T>, K: Eq + Hash + Clone> History<T, D, O
|
|||||||
return step;
|
return step;
|
||||||
}
|
}
|
||||||
|
|
||||||
competitor::clean(self.agents.values_mut(), false);
|
competitor::clean(self.competitors.values_mut(), false);
|
||||||
|
|
||||||
for j in (0..self.time_slices.len() - 1).rev() {
|
for j in (0..self.time_slices.len() - 1).rev() {
|
||||||
for agent in self.time_slices[j + 1].skills.keys() {
|
for competitor in self.time_slices[j + 1].skills.keys() {
|
||||||
self.agents.get_mut(agent).unwrap().message =
|
self.competitors.get_mut(competitor).unwrap().message = Some(
|
||||||
Some(self.time_slices[j + 1].backward_prior_out(&agent, &self.agents));
|
self.time_slices[j + 1].backward_prior_out(&competitor, &self.competitors),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let old = self.time_slices[j].posteriors();
|
let old = self.time_slices[j].posteriors();
|
||||||
|
|
||||||
self.time_slices[j].new_backward_info(&self.agents);
|
self.time_slices[j].new_backward_info(&self.competitors);
|
||||||
self.observer.on_slice_processed(
|
self.observer.on_slice_processed(
|
||||||
&self.time_slices[j].time,
|
&self.time_slices[j].time,
|
||||||
j,
|
j,
|
||||||
@@ -494,17 +495,17 @@ impl<T: Time, D: Drift<T>, O: Observer<T>, K: Eq + Hash + Clone> History<T, D, O
|
|||||||
.fold(step, |step, (a, old)| tuple_max(step, old.delta(new[a])));
|
.fold(step, |step, (a, old)| tuple_max(step, old.delta(new[a])));
|
||||||
}
|
}
|
||||||
|
|
||||||
competitor::clean(self.agents.values_mut(), false);
|
competitor::clean(self.competitors.values_mut(), false);
|
||||||
|
|
||||||
for j in 1..self.time_slices.len() {
|
for j in 1..self.time_slices.len() {
|
||||||
for agent in self.time_slices[j - 1].skills.keys() {
|
for competitor in self.time_slices[j - 1].skills.keys() {
|
||||||
self.agents.get_mut(agent).unwrap().message =
|
self.competitors.get_mut(competitor).unwrap().message =
|
||||||
Some(self.time_slices[j - 1].forward_prior_out(&agent));
|
Some(self.time_slices[j - 1].forward_prior_out(&competitor));
|
||||||
}
|
}
|
||||||
|
|
||||||
let old = self.time_slices[j].posteriors();
|
let old = self.time_slices[j].posteriors();
|
||||||
|
|
||||||
self.time_slices[j].new_forward_info(&self.agents);
|
self.time_slices[j].new_forward_info(&self.competitors);
|
||||||
self.observer.on_slice_processed(
|
self.observer.on_slice_processed(
|
||||||
&self.time_slices[j].time,
|
&self.time_slices[j].time,
|
||||||
j,
|
j,
|
||||||
@@ -521,7 +522,7 @@ impl<T: Time, D: Drift<T>, O: Observer<T>, K: Eq + Hash + Clone> History<T, D, O
|
|||||||
if self.time_slices.len() == 1 {
|
if self.time_slices.len() == 1 {
|
||||||
let old = self.time_slices[0].posteriors();
|
let old = self.time_slices[0].posteriors();
|
||||||
|
|
||||||
self.time_slices[0].iteration(0, &self.agents);
|
self.time_slices[0].iteration(0, &self.competitors);
|
||||||
self.observer.on_slice_processed(
|
self.observer.on_slice_processed(
|
||||||
&self.time_slices[0].time,
|
&self.time_slices[0].time,
|
||||||
0,
|
0,
|
||||||
@@ -676,7 +677,7 @@ impl<T: Time, D: Drift<T>, O: Observer<T>, K: Eq + Hash + Clone> History<T, D, O
|
|||||||
|
|
||||||
let key = format!("{:?}", member.key);
|
let key = format!("{:?}", member.key);
|
||||||
let idx = self.keys.get_or_create(&member.key);
|
let idx = self.keys.get_or_create(&member.key);
|
||||||
if self.agents.contains(idx) {
|
if self.competitors.contains(idx) {
|
||||||
return Err(InferenceError::AlreadyRegistered { key });
|
return Err(InferenceError::AlreadyRegistered { key });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -699,7 +700,7 @@ impl<T: Time, D: Drift<T>, O: Observer<T>, K: Eq + Hash + Clone> History<T, D, O
|
|||||||
drift_scale: member.drift_scale,
|
drift_scale: member.drift_scale,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
self.agents.insert(
|
self.competitors.insert(
|
||||||
idx,
|
idx,
|
||||||
Competitor {
|
Competitor {
|
||||||
rating,
|
rating,
|
||||||
@@ -725,7 +726,9 @@ impl<T: Time, D: Drift<T>, O: Observer<T>, K: Eq + Hash + Clone> History<T, D, O
|
|||||||
Q: std::hash::Hash + Eq + ?Sized,
|
Q: std::hash::Hash + Eq + ?Sized,
|
||||||
{
|
{
|
||||||
let idx = self.keys.get(key)?;
|
let idx = self.keys.get(key)?;
|
||||||
self.agents.contains(idx).then(|| self.agents[idx].rating)
|
self.competitors
|
||||||
|
.contains(idx)
|
||||||
|
.then(|| self.competitors[idx].rating)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[must_use]
|
#[must_use]
|
||||||
@@ -771,8 +774,8 @@ impl<T: Time, D: Drift<T>, O: Observer<T>, K: Eq + Hash + Clone> History<T, D, O
|
|||||||
let mut data: HashMap<K, Vec<(T, Gaussian)>> = HashMap::new();
|
let mut data: HashMap<K, Vec<(T, Gaussian)>> = HashMap::new();
|
||||||
|
|
||||||
for (time, step) in self.filtered_pass() {
|
for (time, step) in self.filtered_pass() {
|
||||||
for (agent, posterior) in step.posteriors {
|
for (competitor, posterior) in step.posteriors {
|
||||||
if let Some(key) = self.keys.key(agent).cloned() {
|
if let Some(key) = self.keys.key(competitor).cloned() {
|
||||||
data.entry(key).or_default().push((time, posterior));
|
data.entry(key).or_default().push((time, posterior));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -804,7 +807,7 @@ impl<T: Time, D: Drift<T>, O: Observer<T>, K: Eq + Hash + Clone> History<T, D, O
|
|||||||
.filter_map(|(time, step)| {
|
.filter_map(|(time, step)| {
|
||||||
step.posteriors
|
step.posteriors
|
||||||
.iter()
|
.iter()
|
||||||
.find(|(agent, _)| *agent == idx)
|
.find(|(competitor, _)| *competitor == idx)
|
||||||
.map(|&(_, posterior)| (time, posterior))
|
.map(|&(_, posterior)| (time, posterior))
|
||||||
})
|
})
|
||||||
.collect()
|
.collect()
|
||||||
@@ -823,7 +826,7 @@ impl<T: Time, D: Drift<T>, O: Observer<T>, K: Eq + Hash + Clone> History<T, D, O
|
|||||||
// Bound before the closure so it captures the store rather than all of
|
// Bound before the closure so it captures the store rather than all of
|
||||||
// `&self`: capturing `&History` would drag `KeyTable<K>` in and demand
|
// `&self`: capturing `&History` would drag `KeyTable<K>` in and demand
|
||||||
// `K: Sync` from every caller, which the key type need not satisfy.
|
// `K: Sync` from every caller, which the key type need not satisfy.
|
||||||
let agents = &self.agents;
|
let competitors = &self.competitors;
|
||||||
|
|
||||||
#[cfg(feature = "rayon")]
|
#[cfg(feature = "rayon")]
|
||||||
{
|
{
|
||||||
@@ -831,7 +834,7 @@ impl<T: Time, D: Drift<T>, O: Observer<T>, K: Eq + Hash + Clone> History<T, D, O
|
|||||||
let per_slice: Vec<f64> = self
|
let per_slice: Vec<f64> = self
|
||||||
.time_slices
|
.time_slices
|
||||||
.par_iter()
|
.par_iter()
|
||||||
.map(|ts| ts.log_evidence(targets, forward, agents))
|
.map(|ts| ts.log_evidence(targets, forward, competitors))
|
||||||
.collect();
|
.collect();
|
||||||
per_slice.into_iter().sum()
|
per_slice.into_iter().sum()
|
||||||
}
|
}
|
||||||
@@ -839,7 +842,7 @@ impl<T: Time, D: Drift<T>, O: Observer<T>, K: Eq + Hash + Clone> History<T, D, O
|
|||||||
{
|
{
|
||||||
self.time_slices
|
self.time_slices
|
||||||
.iter()
|
.iter()
|
||||||
.map(|ts| ts.log_evidence(targets, forward, agents))
|
.map(|ts| ts.log_evidence(targets, forward, competitors))
|
||||||
.sum()
|
.sum()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -872,10 +875,10 @@ impl<T: Time, D: Drift<T>, O: Observer<T>, K: Eq + Hash + Clone> History<T, D, O
|
|||||||
let mut pass = Vec::with_capacity(self.time_slices.len());
|
let mut pass = Vec::with_capacity(self.time_slices.len());
|
||||||
|
|
||||||
for slice in &self.time_slices {
|
for slice in &self.time_slices {
|
||||||
let step = slice.filtered_step(&messages, &self.agents);
|
let step = slice.filtered_step(&messages, &self.competitors);
|
||||||
|
|
||||||
for &(agent, posterior) in &step.posteriors {
|
for &(competitor, posterior) in &step.posteriors {
|
||||||
messages.insert(agent, posterior);
|
messages.insert(competitor, posterior);
|
||||||
}
|
}
|
||||||
|
|
||||||
pass.push((slice.time, step));
|
pass.push((slice.time, step));
|
||||||
@@ -1120,13 +1123,13 @@ impl<T: Time, D: Drift<T>, O: Observer<T>, K: Eq + Hash + Clone> History<T, D, O
|
|||||||
let mut n = 0usize;
|
let mut n = 0usize;
|
||||||
|
|
||||||
for (slice_idx, slice) in self.time_slices.iter().enumerate() {
|
for (slice_idx, slice) in self.time_slices.iter().enumerate() {
|
||||||
for (agent, elapsed) in slice.appearances() {
|
for (competitor, elapsed) in slice.appearances() {
|
||||||
let rating = &self.agents[agent].rating;
|
let rating = &self.competitors[competitor].rating;
|
||||||
let row = match previous.get(&agent) {
|
let row = match previous.get(&competitor) {
|
||||||
None => {
|
None => {
|
||||||
let row = n;
|
let row = n;
|
||||||
n += 1;
|
n += 1;
|
||||||
first_rows.push((row, agent));
|
first_rows.push((row, competitor));
|
||||||
row
|
row
|
||||||
}
|
}
|
||||||
Some(&prev) => {
|
Some(&prev) => {
|
||||||
@@ -1143,16 +1146,17 @@ impl<T: Time, D: Drift<T>, O: Observer<T>, K: Eq + Hash + Clone> History<T, D, O
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
previous.insert(agent, row);
|
previous.insert(competitor, row);
|
||||||
latest.insert(agent, (row, slice_idx));
|
latest.insert(competitor, (row, slice_idx));
|
||||||
at_slice.insert((agent, slice_idx), row);
|
at_slice.insert((competitor, slice_idx), row);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut lambda = vec![0.0; n * n];
|
let mut lambda = vec![0.0; n * n];
|
||||||
|
|
||||||
for (row, agent) in first_rows {
|
for (row, competitor) in first_rows {
|
||||||
lambda[row * n + row] += 1.0 / self.agents[agent].rating.prior.sigma().powi(2);
|
lambda[row * n + row] +=
|
||||||
|
1.0 / self.competitors[competitor].rating.prior.sigma().powi(2);
|
||||||
}
|
}
|
||||||
for (a, b, drift) in drift_links {
|
for (a, b, drift) in drift_links {
|
||||||
lambda[a * n + a] += 1.0 / drift;
|
lambda[a * n + a] += 1.0 / drift;
|
||||||
@@ -1161,7 +1165,7 @@ impl<T: Time, D: Drift<T>, O: Observer<T>, K: Eq + Hash + Clone> History<T, D, O
|
|||||||
lambda[b * n + a] -= 1.0 / drift;
|
lambda[b * n + a] -= 1.0 / drift;
|
||||||
}
|
}
|
||||||
for (slice_idx, slice) in self.time_slices.iter().enumerate() {
|
for (slice_idx, slice) in self.time_slices.iter().enumerate() {
|
||||||
for (contrast, noise) in slice.scored_contrasts(&self.agents) {
|
for (contrast, noise) in slice.scored_contrasts(&self.competitors) {
|
||||||
for (ia, ca) in &contrast {
|
for (ia, ca) in &contrast {
|
||||||
let ra = at_slice[&(*ia, slice_idx)];
|
let ra = at_slice[&(*ia, slice_idx)];
|
||||||
for (ib, cb) in &contrast {
|
for (ib, cb) in &contrast {
|
||||||
@@ -1511,7 +1515,7 @@ impl<T: Time, D: Drift<T>, O: Observer<T>, K: Eq + Hash + Clone> History<T, D, O
|
|||||||
let beta = self
|
let beta = self
|
||||||
.keys
|
.keys
|
||||||
.get(*key)
|
.get(*key)
|
||||||
.map_or(self.beta, |index| self.agents[index].rating.beta);
|
.map_or(self.beta, |index| self.competitors[index].rating.beta);
|
||||||
performance_noise += beta * beta;
|
performance_noise += beta * beta;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1795,8 +1799,8 @@ impl<T: Time, D: Drift<T>, O: Observer<T>, K: Eq + Hash + Clone> History<T, D, O
|
|||||||
// defect already rejected for `sigma` and `beta`. A non-finite gamma
|
// defect already rejected for `sigma` and `beta`. A non-finite gamma
|
||||||
// poisons every posterior derived from it.
|
// poisons every posterior derived from it.
|
||||||
for slice in &self.time_slices {
|
for slice in &self.time_slices {
|
||||||
for (agent, elapsed) in slice.appearances() {
|
for (competitor, elapsed) in slice.appearances() {
|
||||||
let drift = self.agents[agent]
|
let drift = self.competitors[competitor]
|
||||||
.rating
|
.rating
|
||||||
.drift_variance_for_elapsed(elapsed);
|
.drift_variance_for_elapsed(elapsed);
|
||||||
if !drift.is_finite() || drift < 0.0 {
|
if !drift.is_finite() || drift < 0.0 {
|
||||||
@@ -2006,14 +2010,14 @@ impl<T: Time, D: Drift<T>, O: Observer<T>, K: Eq + Hash + Clone> History<T, D, O
|
|||||||
let mut conflict_scan: Vec<Index> = priors.keys().copied().collect();
|
let mut conflict_scan: Vec<Index> = priors.keys().copied().collect();
|
||||||
conflict_scan.sort_unstable();
|
conflict_scan.sort_unstable();
|
||||||
|
|
||||||
for agent in &conflict_scan {
|
for competitor in &conflict_scan {
|
||||||
let batch = priors[agent];
|
let batch = priors[competitor];
|
||||||
let held = self.declared.get(agent).copied().unwrap_or_default();
|
let held = self.declared.get(competitor).copied().unwrap_or_default();
|
||||||
|
|
||||||
if let (Some(existing), Some(new)) = (held.prior, batch.prior) {
|
if let (Some(existing), Some(new)) = (held.prior, batch.prior) {
|
||||||
if existing != new {
|
if existing != new {
|
||||||
return Err(InferenceError::ConflictingCompetitorConfig {
|
return Err(InferenceError::ConflictingCompetitorConfig {
|
||||||
competitor: agent.get(),
|
competitor: competitor.get(),
|
||||||
field: "prior",
|
field: "prior",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -2021,15 +2025,15 @@ impl<T: Time, D: Drift<T>, O: Observer<T>, K: Eq + Hash + Clone> History<T, D, O
|
|||||||
if let (Some(existing), Some(new)) = (held.drift_scale, batch.drift_scale) {
|
if let (Some(existing), Some(new)) = (held.drift_scale, batch.drift_scale) {
|
||||||
if existing != new {
|
if existing != new {
|
||||||
return Err(InferenceError::ConflictingCompetitorConfig {
|
return Err(InferenceError::ConflictingCompetitorConfig {
|
||||||
competitor: agent.get(),
|
competitor: competitor.get(),
|
||||||
field: "drift_scale",
|
field: "drift_scale",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (agent, batch) in &priors {
|
for (competitor, batch) in &priors {
|
||||||
let entry = self.declared.entry(*agent).or_default();
|
let entry = self.declared.entry(*competitor).or_default();
|
||||||
if batch.prior.is_some() {
|
if batch.prior.is_some() {
|
||||||
entry.prior = batch.prior;
|
entry.prior = batch.prior;
|
||||||
}
|
}
|
||||||
@@ -2038,22 +2042,22 @@ impl<T: Time, D: Drift<T>, O: Observer<T>, K: Eq + Hash + Clone> History<T, D, O
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
competitor::clean(self.agents.values_mut(), true);
|
competitor::clean(self.competitors.values_mut(), true);
|
||||||
|
|
||||||
let mut this_agent = Vec::with_capacity(1024);
|
let mut this_agent = Vec::with_capacity(1024);
|
||||||
|
|
||||||
for agent in composition.iter().flatten().flatten() {
|
for competitor in composition.iter().flatten().flatten() {
|
||||||
if this_agent.contains(agent) {
|
if this_agent.contains(competitor) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
this_agent.push(*agent);
|
this_agent.push(*competitor);
|
||||||
|
|
||||||
// From `declared` rather than `priors`: a competitor configured by
|
// From `declared` rather than `priors`: a competitor configured by
|
||||||
// `register` before any event has nothing in this batch's map.
|
// `register` before any event has nothing in this batch's map.
|
||||||
let config = self.declared.get(agent).copied().unwrap_or_default();
|
let config = self.declared.get(competitor).copied().unwrap_or_default();
|
||||||
|
|
||||||
if self.agents.contains(*agent) {
|
if self.competitors.contains(*competitor) {
|
||||||
// Seeding a competitor the history already knows. This used to
|
// Seeding a competitor the history already knows. This used to
|
||||||
// be dropped on the floor: `remove` was only reached on the
|
// be dropped on the floor: `remove` was only reached on the
|
||||||
// create path, so a prior applied on a competitor's very first
|
// create path, so a prior applied on a competitor's very first
|
||||||
@@ -2062,7 +2066,7 @@ impl<T: Time, D: Drift<T>, O: Observer<T>, K: Eq + Hash + Clone> History<T, D, O
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
let rating = &mut self.agents.get_mut(*agent).unwrap().rating;
|
let rating = &mut self.competitors.get_mut(*competitor).unwrap().rating;
|
||||||
if let Some(prior) = config.prior {
|
if let Some(prior) = config.prior {
|
||||||
rating.prior = prior;
|
rating.prior = prior;
|
||||||
}
|
}
|
||||||
@@ -2082,7 +2086,7 @@ impl<T: Time, D: Drift<T>, O: Observer<T>, K: Eq + Hash + Clone> History<T, D, O
|
|||||||
// `clean` has just nulled every message, so the earliest
|
// `clean` has just nulled every message, so the earliest
|
||||||
// slice's forward is exactly the prior.
|
// slice's forward is exactly the prior.
|
||||||
for slice in &mut self.time_slices {
|
for slice in &mut self.time_slices {
|
||||||
if let Some(skill) = slice.skills.get_mut(*agent) {
|
if let Some(skill) = slice.skills.get_mut(*competitor) {
|
||||||
skill.forward = seeded;
|
skill.forward = seeded;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -2101,8 +2105,8 @@ impl<T: Time, D: Drift<T>, O: Observer<T>, K: Eq + Hash + Clone> History<T, D, O
|
|||||||
rating.drift_scale = scale;
|
rating.drift_scale = scale;
|
||||||
}
|
}
|
||||||
|
|
||||||
self.agents.insert(
|
self.competitors.insert(
|
||||||
*agent,
|
*competitor,
|
||||||
Competitor {
|
Competitor {
|
||||||
rating,
|
rating,
|
||||||
message: None,
|
message: None,
|
||||||
@@ -2144,20 +2148,20 @@ impl<T: Time, D: Drift<T>, O: Observer<T>, K: Eq + Hash + Clone> History<T, D, O
|
|||||||
let time_slice = &mut self.time_slices[k];
|
let time_slice = &mut self.time_slices[k];
|
||||||
|
|
||||||
if k > 0 {
|
if k > 0 {
|
||||||
time_slice.new_forward_info(&self.agents);
|
time_slice.new_forward_info(&self.competitors);
|
||||||
}
|
}
|
||||||
|
|
||||||
for agent_idx in &this_agent {
|
for agent_idx in &this_agent {
|
||||||
if let Some(skill) = time_slice.skills.get_mut(*agent_idx) {
|
if let Some(skill) = time_slice.skills.get_mut(*agent_idx) {
|
||||||
skill.elapsed = time_slice::compute_elapsed(
|
skill.elapsed = time_slice::compute_elapsed(
|
||||||
self.agents[*agent_idx].last_time.as_ref(),
|
self.competitors[*agent_idx].last_time.as_ref(),
|
||||||
&time_slice.time,
|
&time_slice.time,
|
||||||
);
|
);
|
||||||
|
|
||||||
let agent = self.agents.get_mut(*agent_idx).unwrap();
|
let competitor = self.competitors.get_mut(*agent_idx).unwrap();
|
||||||
|
|
||||||
agent.last_time = Some(time_slice.time);
|
competitor.last_time = Some(time_slice.time);
|
||||||
agent.message = Some(time_slice.forward_prior_out(agent_idx));
|
competitor.message = Some(time_slice.forward_prior_out(agent_idx));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2184,29 +2188,41 @@ impl<T: Time, D: Drift<T>, O: Observer<T>, K: Eq + Hash + Clone> History<T, D, O
|
|||||||
|
|
||||||
if self.time_slices.len() > k && self.time_slices[k].time == t {
|
if self.time_slices.len() > k && self.time_slices[k].time == t {
|
||||||
let time_slice = &mut self.time_slices[k];
|
let time_slice = &mut self.time_slices[k];
|
||||||
time_slice.add_events(composition, results, weights, kinds_chunk, &self.agents);
|
time_slice.add_events(
|
||||||
|
composition,
|
||||||
|
results,
|
||||||
|
weights,
|
||||||
|
kinds_chunk,
|
||||||
|
&self.competitors,
|
||||||
|
);
|
||||||
|
|
||||||
for agent_idx in time_slice.skills.keys() {
|
for agent_idx in time_slice.skills.keys() {
|
||||||
let agent = self.agents.get_mut(agent_idx).unwrap();
|
let competitor = self.competitors.get_mut(agent_idx).unwrap();
|
||||||
|
|
||||||
agent.last_time = Some(t);
|
competitor.last_time = Some(t);
|
||||||
agent.message = Some(time_slice.forward_prior_out(&agent_idx));
|
competitor.message = Some(time_slice.forward_prior_out(&agent_idx));
|
||||||
}
|
}
|
||||||
|
|
||||||
k += 1;
|
k += 1;
|
||||||
} else {
|
} else {
|
||||||
let mut time_slice = TimeSlice::new(t, self.p_draw, self.convergence);
|
let mut time_slice = TimeSlice::new(t, self.p_draw, self.convergence);
|
||||||
time_slice.add_events(composition, results, weights, kinds_chunk, &self.agents);
|
time_slice.add_events(
|
||||||
|
composition,
|
||||||
|
results,
|
||||||
|
weights,
|
||||||
|
kinds_chunk,
|
||||||
|
&self.competitors,
|
||||||
|
);
|
||||||
|
|
||||||
self.time_slices.insert(k, time_slice);
|
self.time_slices.insert(k, time_slice);
|
||||||
|
|
||||||
let time_slice = &self.time_slices[k];
|
let time_slice = &self.time_slices[k];
|
||||||
|
|
||||||
for agent_idx in time_slice.skills.keys() {
|
for agent_idx in time_slice.skills.keys() {
|
||||||
let agent = self.agents.get_mut(agent_idx).unwrap();
|
let competitor = self.competitors.get_mut(agent_idx).unwrap();
|
||||||
|
|
||||||
agent.last_time = Some(t);
|
competitor.last_time = Some(t);
|
||||||
agent.message = Some(time_slice.forward_prior_out(&agent_idx));
|
competitor.message = Some(time_slice.forward_prior_out(&agent_idx));
|
||||||
}
|
}
|
||||||
|
|
||||||
k += 1;
|
k += 1;
|
||||||
@@ -2218,19 +2234,19 @@ impl<T: Time, D: Drift<T>, O: Observer<T>, K: Eq + Hash + Clone> History<T, D, O
|
|||||||
while self.time_slices.len() > k {
|
while self.time_slices.len() > k {
|
||||||
let time_slice = &mut self.time_slices[k];
|
let time_slice = &mut self.time_slices[k];
|
||||||
|
|
||||||
time_slice.new_forward_info(&self.agents);
|
time_slice.new_forward_info(&self.competitors);
|
||||||
|
|
||||||
for agent_idx in &this_agent {
|
for agent_idx in &this_agent {
|
||||||
if let Some(skill) = time_slice.skills.get_mut(*agent_idx) {
|
if let Some(skill) = time_slice.skills.get_mut(*agent_idx) {
|
||||||
skill.elapsed = time_slice::compute_elapsed(
|
skill.elapsed = time_slice::compute_elapsed(
|
||||||
self.agents[*agent_idx].last_time.as_ref(),
|
self.competitors[*agent_idx].last_time.as_ref(),
|
||||||
&time_slice.time,
|
&time_slice.time,
|
||||||
);
|
);
|
||||||
|
|
||||||
let agent = self.agents.get_mut(*agent_idx).unwrap();
|
let competitor = self.competitors.get_mut(*agent_idx).unwrap();
|
||||||
|
|
||||||
agent.last_time = Some(time_slice.time);
|
competitor.last_time = Some(time_slice.time);
|
||||||
agent.message = Some(time_slice.forward_prior_out(agent_idx));
|
competitor.message = Some(time_slice.forward_prior_out(agent_idx));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2606,9 +2622,9 @@ impl<T: Time, D: Drift<T>, O: Observer<T>, K: Eq + Hash + Clone> Joint<'_, T, D,
|
|||||||
if slice.time > time {
|
if slice.time > time {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
for (agent, _) in slice.appearances() {
|
for (competitor, _) in slice.appearances() {
|
||||||
if let Some(row) = self.at_slice.get(&(agent, slice_idx)) {
|
if let Some(row) = self.at_slice.get(&(competitor, slice_idx)) {
|
||||||
as_of.insert(agent, (*row, slice_idx));
|
as_of.insert(competitor, (*row, slice_idx));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2657,7 +2673,7 @@ impl<T: Time, D: Drift<T>, O: Observer<T>, K: Eq + Hash + Clone> Joint<'_, T, D,
|
|||||||
.keys
|
.keys
|
||||||
.get(*key)
|
.get(*key)
|
||||||
.map_or(self.history.beta, |index| {
|
.map_or(self.history.beta, |index| {
|
||||||
self.history.agents[index].rating.beta
|
self.history.competitors[index].rating.beta
|
||||||
});
|
});
|
||||||
noise += beta * beta;
|
noise += beta * beta;
|
||||||
}
|
}
|
||||||
@@ -2812,7 +2828,11 @@ mod tests {
|
|||||||
|
|
||||||
let w = [vec![1.0], vec![1.0]];
|
let w = [vec![1.0], vec![1.0]];
|
||||||
let p = Game::ranked_with_arena(
|
let p = Game::ranked_with_arena(
|
||||||
h.time_slices[1].events[0].within_priors(false, &h.time_slices[1].skills, &h.agents),
|
h.time_slices[1].events[0].within_priors(
|
||||||
|
false,
|
||||||
|
&h.time_slices[1].skills,
|
||||||
|
&h.competitors,
|
||||||
|
),
|
||||||
&[0.0, 1.0],
|
&[0.0, 1.0],
|
||||||
&w,
|
&w,
|
||||||
P_DRAW,
|
P_DRAW,
|
||||||
@@ -3731,7 +3751,7 @@ mod tests {
|
|||||||
|
|
||||||
let mut max_diff: f64 = 0.0;
|
let mut max_diff: f64 = 0.0;
|
||||||
for (key, capped_pts) in curves_capped.iter() {
|
for (key, capped_pts) in curves_capped.iter() {
|
||||||
let full_pts = curves_full.get(key).expect("agent missing in full");
|
let full_pts = curves_full.get(key).expect("competitor missing in full");
|
||||||
for (capped, full) in capped_pts.iter().zip(full_pts.iter()) {
|
for (capped, full) in capped_pts.iter().zip(full_pts.iter()) {
|
||||||
max_diff = max_diff.max((capped.1.mu() - full.1.mu()).abs());
|
max_diff = max_diff.max((capped.1.mu() - full.1.mu()).abs());
|
||||||
max_diff = max_diff.max((capped.1.sigma() - full.1.sigma()).abs());
|
max_diff = max_diff.max((capped.1.sigma() - full.1.sigma()).abs());
|
||||||
@@ -3778,7 +3798,7 @@ mod tests {
|
|||||||
|
|
||||||
let mut max_diff: f64 = 0.0;
|
let mut max_diff: f64 = 0.0;
|
||||||
for (key, u_pts) in curves_u.iter() {
|
for (key, u_pts) in curves_u.iter() {
|
||||||
let d_pts = curves_d.get(key).expect("agent missing in damped");
|
let d_pts = curves_d.get(key).expect("competitor missing in damped");
|
||||||
for (u, d) in u_pts.iter().zip(d_pts.iter()) {
|
for (u, d) in u_pts.iter().zip(d_pts.iter()) {
|
||||||
max_diff = max_diff.max((u.1.mu() - d.1.mu()).abs());
|
max_diff = max_diff.max((u.1.mu() - d.1.mu()).abs());
|
||||||
max_diff = max_diff.max((u.1.sigma() - d.1.sigma()).abs());
|
max_diff = max_diff.max((u.1.sigma() - d.1.sigma()).abs());
|
||||||
@@ -3824,10 +3844,10 @@ mod tests {
|
|||||||
let curves_a = h_a.learning_curves();
|
let curves_a = h_a.learning_curves();
|
||||||
let curves_b = h_b.learning_curves();
|
let curves_b = h_b.learning_curves();
|
||||||
for (key, a_pts) in curves_a.iter() {
|
for (key, a_pts) in curves_a.iter() {
|
||||||
let b_pts = curves_b.get(key).expect("agent missing in path B");
|
let b_pts = curves_b.get(key).expect("competitor missing in path B");
|
||||||
for (a, b) in a_pts.iter().zip(b_pts.iter()) {
|
for (a, b) in a_pts.iter().zip(b_pts.iter()) {
|
||||||
assert_eq!(a.1.pi(), b.1.pi(), "mismatch at agent {key:?}");
|
assert_eq!(a.1.pi(), b.1.pi(), "mismatch at competitor {key:?}");
|
||||||
assert_eq!(a.1.tau(), b.1.tau(), "mismatch at agent {key:?}");
|
assert_eq!(a.1.tau(), b.1.tau(), "mismatch at competitor {key:?}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3866,10 +3886,10 @@ mod tests {
|
|||||||
let curves_a = h_a.learning_curves();
|
let curves_a = h_a.learning_curves();
|
||||||
let curves_b = h_b.learning_curves();
|
let curves_b = h_b.learning_curves();
|
||||||
for (key, a_pts) in curves_a.iter() {
|
for (key, a_pts) in curves_a.iter() {
|
||||||
let b_pts = curves_b.get(key).expect("agent missing in path B");
|
let b_pts = curves_b.get(key).expect("competitor missing in path B");
|
||||||
for (a, b) in a_pts.iter().zip(b_pts.iter()) {
|
for (a, b) in a_pts.iter().zip(b_pts.iter()) {
|
||||||
assert_eq!(a.1.pi(), b.1.pi(), "mismatch at agent {key:?}");
|
assert_eq!(a.1.pi(), b.1.pi(), "mismatch at competitor {key:?}");
|
||||||
assert_eq!(a.1.tau(), b.1.tau(), "mismatch at agent {key:?}");
|
assert_eq!(a.1.tau(), b.1.tau(), "mismatch at competitor {key:?}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3889,7 +3909,7 @@ mod tests {
|
|||||||
let curves_c = h_c.learning_curves();
|
let curves_c = h_c.learning_curves();
|
||||||
let mut max_diff: f64 = 0.0;
|
let mut max_diff: f64 = 0.0;
|
||||||
for (key, a_pts) in curves_a.iter() {
|
for (key, a_pts) in curves_a.iter() {
|
||||||
let c_pts = curves_c.get(key).expect("agent missing in path C");
|
let c_pts = curves_c.get(key).expect("competitor missing in path C");
|
||||||
for (a, c) in a_pts.iter().zip(c_pts.iter()) {
|
for (a, c) in a_pts.iter().zip(c_pts.iter()) {
|
||||||
max_diff = max_diff.max((a.1.mu() - c.1.mu()).abs());
|
max_diff = max_diff.max((a.1.mu() - c.1.mu()).abs());
|
||||||
max_diff = max_diff.max((a.1.sigma() - c.1.sigma()).abs());
|
max_diff = max_diff.max((a.1.sigma() - c.1.sigma()).abs());
|
||||||
@@ -3931,10 +3951,10 @@ mod tests {
|
|||||||
let curves_a = h_a.learning_curves();
|
let curves_a = h_a.learning_curves();
|
||||||
let curves_b = h_b.learning_curves();
|
let curves_b = h_b.learning_curves();
|
||||||
for (key, a_pts) in curves_a.iter() {
|
for (key, a_pts) in curves_a.iter() {
|
||||||
let b_pts = curves_b.get(key).expect("agent missing");
|
let b_pts = curves_b.get(key).expect("competitor missing");
|
||||||
for (a, b) in a_pts.iter().zip(b_pts.iter()) {
|
for (a, b) in a_pts.iter().zip(b_pts.iter()) {
|
||||||
assert_eq!(a.1.pi(), b.1.pi(), "mismatch at agent {key:?}");
|
assert_eq!(a.1.pi(), b.1.pi(), "mismatch at competitor {key:?}");
|
||||||
assert_eq!(a.1.tau(), b.1.tau(), "mismatch at agent {key:?}");
|
assert_eq!(a.1.tau(), b.1.tau(), "mismatch at competitor {key:?}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+11
-11
@@ -746,14 +746,14 @@ pub(crate) fn sort_time<T: Copy + Ord>(xs: &[T], reverse: bool) -> Vec<usize> {
|
|||||||
x.into_iter().map(|(i, _)| i).collect()
|
x.into_iter().map(|(i, _)| i).collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Calculates the match quality of the given rating groups. A result is the draw probability in the association
|
/// Calculates the match quality of the given teams. A result is the draw probability in the association
|
||||||
///
|
///
|
||||||
/// Supports any number of groups. Values range roughly `[0, 1]`; 1 means a
|
/// Supports any number of groups. Values range roughly `[0, 1]`; 1 means a
|
||||||
/// perfectly balanced match.
|
/// perfectly balanced match.
|
||||||
///
|
///
|
||||||
/// # Panics
|
/// # Panics
|
||||||
///
|
///
|
||||||
/// Panics if fewer than two rating groups are supplied, or if any group is
|
/// Panics if fewer than two teams are supplied, or if any group is
|
||||||
/// empty — match quality is a property of a contest between at least two
|
/// empty — match quality is a property of a contest between at least two
|
||||||
/// non-empty sides.
|
/// non-empty sides.
|
||||||
///
|
///
|
||||||
@@ -764,18 +764,18 @@ pub(crate) fn sort_time<T: Copy + Ord>(xs: &[T], reverse: bool) -> Vec<usize> {
|
|||||||
/// converted, because the input has no meaningful answer rather than an
|
/// converted, because the input has no meaningful answer rather than an
|
||||||
/// awkward one.
|
/// awkward one.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn quality(rating_groups: &[&[Gaussian]], beta: f64) -> f64 {
|
pub fn quality(teams: &[&[Gaussian]], beta: f64) -> f64 {
|
||||||
assert!(
|
assert!(
|
||||||
rating_groups.len() >= 2,
|
teams.len() >= 2,
|
||||||
"quality() requires at least 2 rating groups, got {}",
|
"quality() requires at least 2 teams, got {}",
|
||||||
rating_groups.len()
|
teams.len()
|
||||||
);
|
);
|
||||||
assert!(
|
assert!(
|
||||||
rating_groups.iter().all(|group| !group.is_empty()),
|
teams.iter().all(|group| !group.is_empty()),
|
||||||
"quality() requires every rating group to be non-empty"
|
"quality() requires every team to be non-empty"
|
||||||
);
|
);
|
||||||
|
|
||||||
let flatten_ratings = rating_groups
|
let flatten_ratings = teams
|
||||||
.iter()
|
.iter()
|
||||||
.flat_map(|group| group.iter())
|
.flat_map(|group| group.iter())
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
@@ -796,14 +796,14 @@ pub fn quality(rating_groups: &[&[Gaussian]], beta: f64) -> f64 {
|
|||||||
variance_matrix[(i, i)] = rating.sigma().powi(2);
|
variance_matrix[(i, i)] = rating.sigma().powi(2);
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut rotated_a_matrix = Matrix::new(rating_groups.len() - 1, length);
|
let mut rotated_a_matrix = Matrix::new(teams.len() - 1, length);
|
||||||
|
|
||||||
// Row `row` contrasts group `row` (+weight) against group `row + 1`
|
// Row `row` contrasts group `row` (+weight) against group `row + 1`
|
||||||
// (-weight). `t` is the column where the current group's players start;
|
// (-weight). `t` is the column where the current group's players start;
|
||||||
// the negative block begins immediately after it.
|
// the negative block begins immediately after it.
|
||||||
let mut t = 0;
|
let mut t = 0;
|
||||||
|
|
||||||
for (row, group) in rating_groups.windows(2).enumerate() {
|
for (row, group) in teams.windows(2).enumerate() {
|
||||||
let current = group[0];
|
let current = group[0];
|
||||||
let next = group[1];
|
let next = group[1];
|
||||||
|
|
||||||
|
|||||||
+93
-88
@@ -50,12 +50,12 @@ pub enum EventKind {
|
|||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
struct Item {
|
struct Item {
|
||||||
agent: Index,
|
competitor: Index,
|
||||||
/// This competitor's slot in the owning slice's `SkillStore`, resolved
|
/// This competitor's slot in the owning slice's `SkillStore`, resolved
|
||||||
/// once at ingestion.
|
/// once at ingestion.
|
||||||
///
|
///
|
||||||
/// The convergence loop reaches skills through this rather than through
|
/// 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`.
|
/// that the store is compact rather than indexed by the global `Index`.
|
||||||
slot: u32,
|
slot: u32,
|
||||||
likelihood: Gaussian,
|
likelihood: Gaussian,
|
||||||
@@ -66,9 +66,9 @@ impl Item {
|
|||||||
&self,
|
&self,
|
||||||
forward: bool,
|
forward: bool,
|
||||||
skills: &SkillStore,
|
skills: &SkillStore,
|
||||||
agents: &CompetitorStore<T, D>,
|
competitors: &CompetitorStore<T, D>,
|
||||||
) -> Rating<T, D> {
|
) -> Rating<T, D> {
|
||||||
let r = &agents[self.agent].rating;
|
let r = &competitors[self.competitor].rating;
|
||||||
let skill = skills.at(self.slot);
|
let skill = skills.at(self.slot);
|
||||||
|
|
||||||
if forward {
|
if forward {
|
||||||
@@ -98,7 +98,7 @@ impl Event {
|
|||||||
pub(crate) fn iter_agents(&self) -> impl Iterator<Item = Index> + '_ {
|
pub(crate) fn iter_agents(&self) -> impl Iterator<Item = Index> + '_ {
|
||||||
self.teams
|
self.teams
|
||||||
.iter()
|
.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> {
|
fn outputs(&self) -> Vec<f64> {
|
||||||
@@ -112,14 +112,14 @@ impl Event {
|
|||||||
&self,
|
&self,
|
||||||
forward: bool,
|
forward: bool,
|
||||||
skills: &SkillStore,
|
skills: &SkillStore,
|
||||||
agents: &CompetitorStore<T, D>,
|
competitors: &CompetitorStore<T, D>,
|
||||||
) -> Vec<Vec<Rating<T, D>>> {
|
) -> Vec<Vec<Rating<T, D>>> {
|
||||||
self.teams
|
self.teams
|
||||||
.iter()
|
.iter()
|
||||||
.map(|team| {
|
.map(|team| {
|
||||||
team.items
|
team.items
|
||||||
.iter()
|
.iter()
|
||||||
.map(|item| item.within_prior(forward, skills, agents))
|
.map(|item| item.within_prior(forward, skills, competitors))
|
||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
@@ -133,12 +133,12 @@ impl Event {
|
|||||||
fn compute<T: Time, D: Drift<T>>(
|
fn compute<T: Time, D: Drift<T>>(
|
||||||
&self,
|
&self,
|
||||||
skills: &SkillStore,
|
skills: &SkillStore,
|
||||||
agents: &CompetitorStore<T, D>,
|
competitors: &CompetitorStore<T, D>,
|
||||||
p_draw: f64,
|
p_draw: f64,
|
||||||
convergence: crate::ConvergenceOptions,
|
convergence: crate::ConvergenceOptions,
|
||||||
arena: &mut ScratchArena,
|
arena: &mut ScratchArena,
|
||||||
) -> EventUpdate {
|
) -> EventUpdate {
|
||||||
let teams = self.within_priors(false, skills, agents);
|
let teams = self.within_priors(false, skills, competitors);
|
||||||
let result = self.outputs();
|
let result = self.outputs();
|
||||||
let g = match self.kind {
|
let g = match self.kind {
|
||||||
EventKind::Ranked => {
|
EventKind::Ranked => {
|
||||||
@@ -179,12 +179,12 @@ impl Event {
|
|||||||
fn iteration_direct<T: Time, D: Drift<T>>(
|
fn iteration_direct<T: Time, D: Drift<T>>(
|
||||||
&mut self,
|
&mut self,
|
||||||
skills: &mut SkillStore,
|
skills: &mut SkillStore,
|
||||||
agents: &CompetitorStore<T, D>,
|
competitors: &CompetitorStore<T, D>,
|
||||||
p_draw: f64,
|
p_draw: f64,
|
||||||
convergence: crate::ConvergenceOptions,
|
convergence: crate::ConvergenceOptions,
|
||||||
arena: &mut ScratchArena,
|
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);
|
self.apply(skills, update);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -288,7 +288,7 @@ impl<T: Time> TimeSlice<T> {
|
|||||||
results: Option<Vec<Vec<f64>>>,
|
results: Option<Vec<Vec<f64>>>,
|
||||||
weights: Option<Vec<Vec<Vec<f64>>>>,
|
weights: Option<Vec<Vec<Vec<f64>>>>,
|
||||||
kinds: Vec<EventKind>,
|
kinds: Vec<EventKind>,
|
||||||
agents: &CompetitorStore<T, D>,
|
competitors: &CompetitorStore<T, D>,
|
||||||
) {
|
) {
|
||||||
let mut unique = Vec::with_capacity(10);
|
let mut unique = Vec::with_capacity(10);
|
||||||
|
|
||||||
@@ -303,9 +303,9 @@ impl<T: Time> TimeSlice<T> {
|
|||||||
});
|
});
|
||||||
|
|
||||||
for idx in this_agent {
|
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) {
|
if let Some(skill) = self.skills.get_mut(*idx) {
|
||||||
skill.elapsed = elapsed;
|
skill.elapsed = elapsed;
|
||||||
@@ -332,12 +332,12 @@ impl<T: Time> TimeSlice<T> {
|
|||||||
.map(|(t, team)| {
|
.map(|(t, team)| {
|
||||||
let items = team
|
let items = team
|
||||||
.iter()
|
.iter()
|
||||||
.map(|&agent| Item {
|
.map(|&competitor| Item {
|
||||||
agent,
|
competitor,
|
||||||
// Every participant was inserted into `skills`
|
// Every participant was inserted into `skills`
|
||||||
// just above, so the slot always resolves.
|
// just above, so the slot always resolves.
|
||||||
slot: skills
|
slot: skills
|
||||||
.slot_of(agent)
|
.slot_of(competitor)
|
||||||
.expect("participant must be present in the slice store"),
|
.expect("participant must be present in the slice store"),
|
||||||
likelihood: N_INF,
|
likelihood: N_INF,
|
||||||
})
|
})
|
||||||
@@ -376,7 +376,7 @@ impl<T: Time> TimeSlice<T> {
|
|||||||
|
|
||||||
self.color_groups_dirty = true;
|
self.color_groups_dirty = true;
|
||||||
|
|
||||||
self.iteration(from, agents);
|
self.iteration(from, competitors);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn posteriors(&self) -> HashMap<Index, Gaussian> {
|
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
|
/// Panics if an event references a competitor with no entry in this
|
||||||
/// slice's skill store. `add_events` inserts one for every participant, so
|
/// slice's skill store. `add_events` inserts one for every participant, so
|
||||||
/// this cannot happen for slices built through the public API.
|
/// 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 {
|
if from == 0 && self.color_groups_dirty {
|
||||||
self.recompute_color_groups();
|
self.recompute_color_groups();
|
||||||
}
|
}
|
||||||
@@ -401,7 +401,7 @@ impl<T: Time> TimeSlice<T> {
|
|||||||
if from > 0 || self.color_groups.is_empty() {
|
if from > 0 || self.color_groups.is_empty() {
|
||||||
// Initial pass (add_events) or no color groups yet: simple sequential sweep.
|
// Initial pass (add_events) or no color groups yet: simple sequential sweep.
|
||||||
for event in self.events.iter_mut().skip(from) {
|
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 result = event.outputs();
|
||||||
|
|
||||||
let g = match event.kind {
|
let g = match event.kind {
|
||||||
@@ -436,14 +436,14 @@ impl<T: Time> TimeSlice<T> {
|
|||||||
event.log_evidence = g.log_evidence;
|
event.log_evidence = g.log_evidence;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
self.sweep_color_groups(agents);
|
self.sweep_color_groups(competitors);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Full event sweep using the color-group partition. Colors are processed
|
/// Full event sweep using the color-group partition. Colors are processed
|
||||||
/// sequentially; within each color the inner loop is parallel under rayon.
|
/// 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
|
/// can observe another's writes. That makes the sweep separable: inference
|
||||||
/// runs concurrently over shared `&self.skills`, and the resulting updates
|
/// runs concurrently over shared `&self.skills`, and the resulting updates
|
||||||
/// are folded in afterwards in index order. Splitting it this way needs no
|
/// 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
|
/// across thread counts because the apply order does not depend on which
|
||||||
/// worker finished first.
|
/// worker finished first.
|
||||||
#[cfg(feature = "rayon")]
|
#[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::*;
|
use rayon::prelude::*;
|
||||||
|
|
||||||
thread_local! {
|
thread_local! {
|
||||||
@@ -483,7 +483,7 @@ impl<T: Time> TimeSlice<T> {
|
|||||||
let mut arena = cell.borrow_mut();
|
let mut arena = cell.borrow_mut();
|
||||||
arena.reset();
|
arena.reset();
|
||||||
|
|
||||||
ev.compute(skills, agents, p_draw, convergence, &mut arena)
|
ev.compute(skills, competitors, p_draw, convergence, &mut arena)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
@@ -495,7 +495,7 @@ impl<T: Time> TimeSlice<T> {
|
|||||||
for ev in &mut self.events[range] {
|
for ev in &mut self.events[range] {
|
||||||
ev.iteration_direct(
|
ev.iteration_direct(
|
||||||
&mut self.skills,
|
&mut self.skills,
|
||||||
agents,
|
competitors,
|
||||||
p_draw,
|
p_draw,
|
||||||
self.convergence,
|
self.convergence,
|
||||||
&mut self.arena,
|
&mut self.arena,
|
||||||
@@ -509,7 +509,7 @@ impl<T: Time> TimeSlice<T> {
|
|||||||
/// Events within each color group are updated inline — no EventOutput allocation —
|
/// Events within each color group are updated inline — no EventOutput allocation —
|
||||||
/// matching the T2 performance profile.
|
/// matching the T2 performance profile.
|
||||||
#[cfg(not(feature = "rayon"))]
|
#[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() {
|
for color_idx in 0..self.color_groups.groups.len() {
|
||||||
if self.color_groups.groups[color_idx].is_empty() {
|
if self.color_groups.groups[color_idx].is_empty() {
|
||||||
continue;
|
continue;
|
||||||
@@ -523,7 +523,7 @@ impl<T: Time> TimeSlice<T> {
|
|||||||
for ev in &mut self.events[range] {
|
for ev in &mut self.events[range] {
|
||||||
ev.iteration_direct(
|
ev.iteration_direct(
|
||||||
&mut self.skills,
|
&mut self.skills,
|
||||||
agents,
|
competitors,
|
||||||
p_draw,
|
p_draw,
|
||||||
self.convergence,
|
self.convergence,
|
||||||
&mut self.arena,
|
&mut self.arena,
|
||||||
@@ -544,7 +544,7 @@ impl<T: Time> TimeSlice<T> {
|
|||||||
/// schedule default.
|
/// schedule default.
|
||||||
pub(crate) fn iterate_to_convergence<D: Drift<T>>(
|
pub(crate) fn iterate_to_convergence<D: Drift<T>>(
|
||||||
&mut self,
|
&mut self,
|
||||||
agents: &CompetitorStore<T, D>,
|
competitors: &CompetitorStore<T, D>,
|
||||||
) -> usize {
|
) -> usize {
|
||||||
use crate::{tuple_gt, tuple_max};
|
use crate::{tuple_gt, tuple_max};
|
||||||
|
|
||||||
@@ -557,7 +557,7 @@ impl<T: Time> TimeSlice<T> {
|
|||||||
while tuple_gt(step, epsilon) && i < max_iter {
|
while tuple_gt(step, epsilon) && i < max_iter {
|
||||||
let old = self.posteriors();
|
let old = self.posteriors();
|
||||||
|
|
||||||
self.iteration(0, agents);
|
self.iteration(0, competitors);
|
||||||
|
|
||||||
let new = self.posteriors();
|
let new = self.posteriors();
|
||||||
|
|
||||||
@@ -575,37 +575,37 @@ impl<T: Time> TimeSlice<T> {
|
|||||||
i
|
i
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn forward_prior_out(&self, agent: &Index) -> Gaussian {
|
pub(crate) fn forward_prior_out(&self, competitor: &Index) -> Gaussian {
|
||||||
let skill = self.skills.get(*agent).unwrap();
|
let skill = self.skills.get(*competitor).unwrap();
|
||||||
skill.forward * skill.likelihood
|
skill.forward * skill.likelihood
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn backward_prior_out<D: Drift<T>>(
|
pub(crate) fn backward_prior_out<D: Drift<T>>(
|
||||||
&self,
|
&self,
|
||||||
agent: &Index,
|
competitor: &Index,
|
||||||
agents: &CompetitorStore<T, D>,
|
competitors: &CompetitorStore<T, D>,
|
||||||
) -> Gaussian {
|
) -> Gaussian {
|
||||||
let skill = self.skills.get(*agent).unwrap();
|
let skill = self.skills.get(*competitor).unwrap();
|
||||||
let n = skill.likelihood * skill.backward;
|
let n = skill.likelihood * skill.backward;
|
||||||
n.forget(
|
n.forget(
|
||||||
agents[*agent]
|
competitors[*competitor]
|
||||||
.rating
|
.rating
|
||||||
.drift_variance_for_elapsed(skill.elapsed),
|
.drift_variance_for_elapsed(skill.elapsed),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn new_backward_info<D: Drift<T>>(&mut self, agents: &CompetitorStore<T, D>) {
|
pub(crate) fn new_backward_info<D: Drift<T>>(&mut self, competitors: &CompetitorStore<T, D>) {
|
||||||
for (agent, skill) in self.skills.iter_mut() {
|
for (competitor, skill) in self.skills.iter_mut() {
|
||||||
skill.backward = agents[agent].message.unwrap_or(N_INF);
|
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>) {
|
pub(crate) fn new_forward_info<D: Drift<T>>(&mut self, competitors: &CompetitorStore<T, D>) {
|
||||||
for (agent, skill) in self.skills.iter_mut() {
|
for (competitor, skill) in self.skills.iter_mut() {
|
||||||
skill.forward = agents[agent].receive_for_elapsed(skill.elapsed);
|
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.
|
/// 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>>(
|
pub(crate) fn filtered_step<D: Drift<T>>(
|
||||||
&self,
|
&self,
|
||||||
incoming: &HashMap<Index, Gaussian>,
|
incoming: &HashMap<Index, Gaussian>,
|
||||||
agents: &CompetitorStore<T, D>,
|
competitors: &CompetitorStore<T, D>,
|
||||||
) -> FilteredStep {
|
) -> FilteredStep {
|
||||||
let mut scratch = TimeSlice {
|
let mut scratch = TimeSlice {
|
||||||
events: self.events.clone(),
|
events: self.events.clone(),
|
||||||
@@ -641,16 +641,16 @@ impl<T: Time> TimeSlice<T> {
|
|||||||
event.log_evidence = 0.0;
|
event.log_evidence = 0.0;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (agent, skill) in self.skills.iter() {
|
for (competitor, skill) in self.skills.iter() {
|
||||||
let rating = &agents[agent].rating;
|
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)),
|
Some(message) => message.forget(rating.drift_variance_for_elapsed(skill.elapsed)),
|
||||||
None => rating.prior,
|
None => rating.prior,
|
||||||
};
|
};
|
||||||
|
|
||||||
let slot = scratch.skills.insert(
|
let slot = scratch.skills.insert(
|
||||||
agent,
|
competitor,
|
||||||
Skill {
|
Skill {
|
||||||
forward,
|
forward,
|
||||||
backward: N_INF,
|
backward: N_INF,
|
||||||
@@ -666,19 +666,19 @@ impl<T: Time> TimeSlice<T> {
|
|||||||
// than leave it to be rediscovered after it breaks.
|
// than leave it to be rediscovered after it breaks.
|
||||||
debug_assert_eq!(
|
debug_assert_eq!(
|
||||||
Some(slot),
|
Some(slot),
|
||||||
self.skills.slot_of(agent),
|
self.skills.slot_of(competitor),
|
||||||
"scratch slot must match the real slice's slot for {agent:?}"
|
"scratch slot must match the real slice's slot for {competitor:?}"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
scratch.iterate_to_convergence(agents);
|
scratch.iterate_to_convergence(competitors);
|
||||||
|
|
||||||
FilteredStep {
|
FilteredStep {
|
||||||
log_evidence: scratch.events.iter().map(|event| event.log_evidence).sum(),
|
log_evidence: scratch.events.iter().map(|event| event.log_evidence).sum(),
|
||||||
posteriors: scratch
|
posteriors: scratch
|
||||||
.skills
|
.skills
|
||||||
.iter()
|
.iter()
|
||||||
.map(|(agent, skill)| (agent, skill.posterior()))
|
.map(|(competitor, skill)| (competitor, skill.posterior()))
|
||||||
.collect(),
|
.collect(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -687,7 +687,7 @@ impl<T: Time> TimeSlice<T> {
|
|||||||
&self,
|
&self,
|
||||||
targets: &[Index],
|
targets: &[Index],
|
||||||
forward: bool,
|
forward: bool,
|
||||||
agents: &CompetitorStore<T, D>,
|
competitors: &CompetitorStore<T, D>,
|
||||||
) -> f64 {
|
) -> f64 {
|
||||||
// Hashed once rather than scanned per player per event, so a
|
// Hashed once rather than scanned per player per event, so a
|
||||||
// `log_evidence_for` with many keys is not quadratic.
|
// `log_evidence_for` with many keys is not quadratic.
|
||||||
@@ -696,7 +696,7 @@ impl<T: Time> TimeSlice<T> {
|
|||||||
let mut arena = ScratchArena::new();
|
let mut arena = ScratchArena::new();
|
||||||
|
|
||||||
let run_event = |event: &Event, arena: &mut ScratchArena| -> f64 {
|
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();
|
let result = event.outputs();
|
||||||
match event.kind {
|
match event.kind {
|
||||||
EventKind::Ranked => {
|
EventKind::Ranked => {
|
||||||
@@ -741,7 +741,7 @@ impl<T: Time> TimeSlice<T> {
|
|||||||
.teams
|
.teams
|
||||||
.iter()
|
.iter()
|
||||||
.flat_map(|team| &team.items)
|
.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))
|
.map(|event| run_event(event, &mut arena))
|
||||||
.sum()
|
.sum()
|
||||||
@@ -753,7 +753,7 @@ impl<T: Time> TimeSlice<T> {
|
|||||||
.teams
|
.teams
|
||||||
.iter()
|
.iter()
|
||||||
.flat_map(|team| &team.items)
|
.flat_map(|team| &team.items)
|
||||||
.any(|item| target_set.contains(&item.agent))
|
.any(|item| target_set.contains(&item.competitor))
|
||||||
})
|
})
|
||||||
.map(|event| event.log_evidence)
|
.map(|event| event.log_evidence)
|
||||||
.sum()
|
.sum()
|
||||||
@@ -769,7 +769,12 @@ impl<T: Time> TimeSlice<T> {
|
|||||||
event
|
event
|
||||||
.teams
|
.teams
|
||||||
.iter()
|
.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<_>>()
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
@@ -831,7 +836,7 @@ impl<T: Time> TimeSlice<T> {
|
|||||||
/// approximations that inference does not retain.
|
/// approximations that inference does not retain.
|
||||||
pub(crate) fn scored_contrasts<D: Drift<T>>(
|
pub(crate) fn scored_contrasts<D: Drift<T>>(
|
||||||
&self,
|
&self,
|
||||||
agents: &CompetitorStore<T, D>,
|
competitors: &CompetitorStore<T, D>,
|
||||||
) -> Vec<(Vec<(Index, f64)>, f64)> {
|
) -> Vec<(Vec<(Index, f64)>, f64)> {
|
||||||
let mut out = Vec::new();
|
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 (team, sign) in [(hi, 1.0), (lo, -1.0)] {
|
||||||
for (m, item) in event.teams[team].items.iter().enumerate() {
|
for (m, item) in event.teams[team].items.iter().enumerate() {
|
||||||
let w = event.weights[team][m];
|
let w = event.weights[team][m];
|
||||||
noise += w * w * agents[item.agent].rating.beta.powi(2);
|
noise += w * w * competitors[item.competitor].rating.beta.powi(2);
|
||||||
contrast.push((item.agent, sign * w));
|
contrast.push((item.competitor, sign * w));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -906,11 +911,11 @@ mod tests {
|
|||||||
let e = index_map.get_or_create("e");
|
let e = index_map.get_or_create("e");
|
||||||
let f = index_map.get_or_create("f");
|
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] {
|
for competitor in [a, b, c, d, e, f] {
|
||||||
agents.insert(
|
competitors.insert(
|
||||||
agent,
|
competitor,
|
||||||
Competitor {
|
Competitor {
|
||||||
rating: Rating::new(
|
rating: Rating::new(
|
||||||
Gaussian::from_ms(25.0, 25.0 / 3.0),
|
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]]),
|
Some(vec![vec![1.0, 0.0], vec![0.0, 1.0], vec![1.0, 0.0]]),
|
||||||
None,
|
None,
|
||||||
vec![EventKind::Ranked; 3],
|
vec![EventKind::Ranked; 3],
|
||||||
&agents,
|
&competitors,
|
||||||
);
|
);
|
||||||
|
|
||||||
let post = time_slice.posteriors();
|
let post = time_slice.posteriors();
|
||||||
@@ -969,7 +974,7 @@ mod tests {
|
|||||||
epsilon = 1e-6
|
epsilon = 1e-6
|
||||||
);
|
);
|
||||||
|
|
||||||
assert_eq!(time_slice.iterate_to_convergence(&agents), 1);
|
assert_eq!(time_slice.iterate_to_convergence(&competitors), 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -983,11 +988,11 @@ mod tests {
|
|||||||
let e = index_map.get_or_create("e");
|
let e = index_map.get_or_create("e");
|
||||||
let f = index_map.get_or_create("f");
|
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] {
|
for competitor in [a, b, c, d, e, f] {
|
||||||
agents.insert(
|
competitors.insert(
|
||||||
agent,
|
competitor,
|
||||||
Competitor {
|
Competitor {
|
||||||
rating: Rating::new(
|
rating: Rating::new(
|
||||||
Gaussian::from_ms(25.0, 25.0 / 3.0),
|
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]]),
|
Some(vec![vec![1.0, 0.0], vec![0.0, 1.0], vec![1.0, 0.0]]),
|
||||||
None,
|
None,
|
||||||
vec![EventKind::Ranked; 3],
|
vec![EventKind::Ranked; 3],
|
||||||
&agents,
|
&competitors,
|
||||||
);
|
);
|
||||||
|
|
||||||
let post = time_slice.posteriors();
|
let post = time_slice.posteriors();
|
||||||
@@ -1031,7 +1036,7 @@ mod tests {
|
|||||||
epsilon = 1e-6
|
epsilon = 1e-6
|
||||||
);
|
);
|
||||||
|
|
||||||
assert!(time_slice.iterate_to_convergence(&agents) > 1);
|
assert!(time_slice.iterate_to_convergence(&competitors) > 1);
|
||||||
|
|
||||||
let post = time_slice.posteriors();
|
let post = time_slice.posteriors();
|
||||||
|
|
||||||
@@ -1063,11 +1068,11 @@ mod tests {
|
|||||||
let e = index_map.get_or_create("e");
|
let e = index_map.get_or_create("e");
|
||||||
let f = index_map.get_or_create("f");
|
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] {
|
for competitor in [a, b, c, d, e, f] {
|
||||||
agents.insert(
|
competitors.insert(
|
||||||
agent,
|
competitor,
|
||||||
Competitor {
|
Competitor {
|
||||||
rating: Rating::new(
|
rating: Rating::new(
|
||||||
Gaussian::from_ms(25.0, 25.0 / 3.0),
|
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]]),
|
Some(vec![vec![1.0, 0.0], vec![0.0, 1.0], vec![1.0, 0.0]]),
|
||||||
None,
|
None,
|
||||||
vec![EventKind::Ranked; 3],
|
vec![EventKind::Ranked; 3],
|
||||||
&agents,
|
&competitors,
|
||||||
);
|
);
|
||||||
|
|
||||||
time_slice.iterate_to_convergence(&agents);
|
time_slice.iterate_to_convergence(&competitors);
|
||||||
|
|
||||||
let post = time_slice.posteriors();
|
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]]),
|
Some(vec![vec![1.0, 0.0], vec![0.0, 1.0], vec![1.0, 0.0]]),
|
||||||
None,
|
None,
|
||||||
vec![EventKind::Ranked; 3],
|
vec![EventKind::Ranked; 3],
|
||||||
&agents,
|
&competitors,
|
||||||
);
|
);
|
||||||
|
|
||||||
assert_eq!(time_slice.events.len(), 6);
|
assert_eq!(time_slice.events.len(), 6);
|
||||||
|
|
||||||
time_slice.iterate_to_convergence(&agents);
|
time_slice.iterate_to_convergence(&competitors);
|
||||||
|
|
||||||
let post = time_slice.posteriors();
|
let post = time_slice.posteriors();
|
||||||
|
|
||||||
@@ -1166,11 +1171,11 @@ mod tests {
|
|||||||
let c = index_map.get_or_create("c");
|
let c = index_map.get_or_create("c");
|
||||||
let d = index_map.get_or_create("d");
|
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] {
|
for competitor in [a, b, c, d] {
|
||||||
agents.insert(
|
competitors.insert(
|
||||||
agent,
|
competitor,
|
||||||
Competitor {
|
Competitor {
|
||||||
rating: Rating::new(
|
rating: Rating::new(
|
||||||
Gaussian::from_ms(25.0, 25.0 / 3.0),
|
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]]),
|
Some(vec![vec![1.0, 0.0], vec![1.0, 0.0], vec![1.0, 0.0]]),
|
||||||
None,
|
None,
|
||||||
vec![EventKind::Ranked; 3],
|
vec![EventKind::Ranked; 3],
|
||||||
&agents,
|
&competitors,
|
||||||
);
|
);
|
||||||
|
|
||||||
assert_eq!(ts.color_groups.n_colors(), 2);
|
assert_eq!(ts.color_groups.n_colors(), 2);
|
||||||
@@ -1204,14 +1209,14 @@ mod tests {
|
|||||||
assert_eq!(ts.color_groups.color_range(1), 2..3);
|
assert_eq!(ts.color_groups.color_range(1), 2..3);
|
||||||
|
|
||||||
// Events at positions 0 and 1 (color 0) must be disjoint — verify by
|
// 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
|
// checking that the competitor sets of self.events[0] and self.events[1] do
|
||||||
// not include the agent at self.events[2].
|
// not include the competitor at self.events[2].
|
||||||
let agents_in_ev2: Vec<Index> = ts.events[2].iter_agents().collect();
|
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_ev0: Vec<Index> = ts.events[0].iter_agents().collect();
|
||||||
let agents_in_ev1: Vec<Index> = ts.events[1].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).
|
// ev0 and ev1 must be disjoint from each other (color-0 invariant).
|
||||||
assert!(agents_in_ev0.iter().all(|ag| !agents_in_ev1.contains(ag)));
|
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_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));
|
let ev2_overlaps_ev1 = agents_in_ev2.iter().any(|ag| agents_in_ev1.contains(ag));
|
||||||
assert!(ev2_overlaps_ev0 || ev2_overlaps_ev1);
|
assert!(ev2_overlaps_ev0 || ev2_overlaps_ev1);
|
||||||
|
|||||||
+3
-3
@@ -1,4 +1,4 @@
|
|||||||
//! `quality()` beyond two rating groups.
|
//! `quality()` beyond two teams.
|
||||||
//!
|
//!
|
||||||
//! The historical golden (two equal singletons) is asserted in
|
//! The historical golden (two equal singletons) is asserted in
|
||||||
//! `src/lib.rs::tests::test_quality`. These cover the N-group generalisation,
|
//! `src/lib.rs::tests::test_quality`. These cover the N-group generalisation,
|
||||||
@@ -82,14 +82,14 @@ fn uneven_group_sizes_work() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
#[should_panic(expected = "at least 2 rating groups")]
|
#[should_panic(expected = "at least 2 teams")]
|
||||||
fn single_group_panics_with_clear_message() {
|
fn single_group_panics_with_clear_message() {
|
||||||
let r = rating(25.0, 3.0);
|
let r = rating(25.0, 3.0);
|
||||||
let _ = quality(&[&[r]], BETA);
|
let _ = quality(&[&[r]], BETA);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
#[should_panic(expected = "at least 2 rating groups")]
|
#[should_panic(expected = "at least 2 teams")]
|
||||||
fn zero_groups_panics_with_clear_message() {
|
fn zero_groups_panics_with_clear_message() {
|
||||||
let _ = quality(&[], BETA);
|
let _ = quality(&[], BETA);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user