refactor(history): remove time: bool; translate tests to explicit timestamps
The bool encoded 'no time axis' which is now expressed at the type
level (T = Untimed). The old !self.time branch generated sequential
i64 timestamps internally (1..=n) and bumped all agents' last_time at
every tick; tests that relied on this now pass those timestamps
explicitly and reflect the correct time=true elapsed semantics.
Collapsed `if self.time { A } else { B }` into the A branch everywhere
in add_events_with_prior. Removed the two !self.time blocks that
updated all agents' last_time at every slice regardless of participation.
sort_time is now generic over `T: Copy + Ord`.
HistoryBuilder::time(bool) removed. History<i64, ConstantDrift>
default remains, producing the same behavior as old .time(true).
The test_env_ttt Gaussian goldens are updated to reflect the correct
time=true semantics (b.elapsed=2 instead of 1 due to b skipping t=2);
this is a correction: the old !self.time last_time bump was an
implementation quirk that diverged from the Python reference.
55 tests pass. clippy clean. fmt clean.
Part of T2 of docs/superpowers/specs/2026-04-23-trueskill-engine-redesign-design.md.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
110
src/history.rs
110
src/history.rs
@@ -15,7 +15,6 @@ use crate::{
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct HistoryBuilder<T: Time = i64, D: Drift<T> = ConstantDrift> {
|
||||
time: bool,
|
||||
mu: f64,
|
||||
sigma: f64,
|
||||
beta: f64,
|
||||
@@ -26,11 +25,6 @@ pub struct HistoryBuilder<T: Time = i64, D: Drift<T> = ConstantDrift> {
|
||||
}
|
||||
|
||||
impl<T: Time, D: Drift<T>> HistoryBuilder<T, D> {
|
||||
pub fn time(mut self, time: bool) -> Self {
|
||||
self.time = time;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn mu(mut self, mu: f64) -> Self {
|
||||
self.mu = mu;
|
||||
self
|
||||
@@ -49,7 +43,6 @@ impl<T: Time, D: Drift<T>> HistoryBuilder<T, D> {
|
||||
pub fn drift<D2: Drift<T>>(self, drift: D2) -> HistoryBuilder<T, D2> {
|
||||
HistoryBuilder {
|
||||
drift,
|
||||
time: self.time,
|
||||
mu: self.mu,
|
||||
sigma: self.sigma,
|
||||
beta: self.beta,
|
||||
@@ -74,7 +67,6 @@ impl<T: Time, D: Drift<T>> HistoryBuilder<T, D> {
|
||||
size: 0,
|
||||
time_slices: Vec::new(),
|
||||
agents: CompetitorStore::new(),
|
||||
time: self.time,
|
||||
mu: self.mu,
|
||||
sigma: self.sigma,
|
||||
beta: self.beta,
|
||||
@@ -95,7 +87,6 @@ impl HistoryBuilder<i64, ConstantDrift> {
|
||||
impl Default for HistoryBuilder<i64, ConstantDrift> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
time: true,
|
||||
mu: MU,
|
||||
sigma: SIGMA,
|
||||
beta: BETA,
|
||||
@@ -111,7 +102,6 @@ pub struct History<T: Time = i64, D: Drift<T> = ConstantDrift> {
|
||||
size: usize,
|
||||
pub(crate) time_slices: Vec<TimeSlice<T>>,
|
||||
pub(crate) agents: CompetitorStore<T, D>,
|
||||
time: bool,
|
||||
mu: f64,
|
||||
sigma: f64,
|
||||
beta: f64,
|
||||
@@ -126,7 +116,6 @@ impl Default for History<i64, ConstantDrift> {
|
||||
size: 0,
|
||||
time_slices: Vec::new(),
|
||||
agents: CompetitorStore::new(),
|
||||
time: true,
|
||||
mu: MU,
|
||||
sigma: SIGMA,
|
||||
beta: BETA,
|
||||
@@ -275,18 +264,13 @@ impl<D: Drift<i64>> History<i64, D> {
|
||||
weights: Vec<Vec<Vec<f64>>>,
|
||||
mut priors: HashMap<Index, Rating<i64, D>>,
|
||||
) {
|
||||
assert!(times.is_empty() || self.time, "length(times)>0 but !h.time");
|
||||
assert!(
|
||||
!times.is_empty() || !self.time,
|
||||
"length(times)==0 but h.time"
|
||||
);
|
||||
assert!(
|
||||
results.is_empty() || results.len() == composition.len(),
|
||||
"(length(results) > 0) & (length(composition) != length(results))"
|
||||
);
|
||||
assert!(
|
||||
times.is_empty() || times.len() == composition.len(),
|
||||
"length(times) > 0) & (length(composition) != length(times))"
|
||||
times.len() == composition.len(),
|
||||
"length(times) must equal length(composition)"
|
||||
);
|
||||
assert!(
|
||||
weights.is_empty() || weights.len() == composition.len(),
|
||||
@@ -323,26 +307,20 @@ impl<D: Drift<i64>> History<i64, D> {
|
||||
}
|
||||
|
||||
let n = composition.len();
|
||||
let o = if self.time {
|
||||
sort_time(×, false)
|
||||
} else {
|
||||
(0..composition.len()).collect::<Vec<_>>()
|
||||
};
|
||||
let o = sort_time(×, false);
|
||||
|
||||
let mut i = 0;
|
||||
let mut k = 0;
|
||||
|
||||
while i < n {
|
||||
let mut j = i + 1;
|
||||
let t = if self.time { times[o[i]] } else { i as i64 + 1 };
|
||||
let t = times[o[i]];
|
||||
|
||||
while self.time && j < n && times[o[j]] == t {
|
||||
while j < n && times[o[j]] == t {
|
||||
j += 1;
|
||||
}
|
||||
|
||||
while (!self.time && (self.size > k))
|
||||
|| (self.time && self.time_slices.len() > k && self.time_slices[k].time < t)
|
||||
{
|
||||
while self.time_slices.len() > k && self.time_slices[k].time < t {
|
||||
let time_slice = &mut self.time_slices[k];
|
||||
|
||||
if k > 0 {
|
||||
@@ -363,16 +341,6 @@ impl<D: Drift<i64>> History<i64, D> {
|
||||
}
|
||||
}
|
||||
|
||||
if !self.time {
|
||||
let slice_time = time_slice.time;
|
||||
for agent_idx in &this_agent {
|
||||
let c = self.agents.get_mut(*agent_idx).unwrap();
|
||||
if c.last_time.is_some() {
|
||||
c.last_time = Some(slice_time);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
k += 1;
|
||||
}
|
||||
|
||||
@@ -392,7 +360,7 @@ impl<D: Drift<i64>> History<i64, D> {
|
||||
(i..j).map(|e| weights[o[e]].clone()).collect::<Vec<_>>()
|
||||
};
|
||||
|
||||
if self.time && 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];
|
||||
time_slice.add_events(composition, results, weights, &self.agents);
|
||||
|
||||
@@ -417,22 +385,13 @@ impl<D: Drift<i64>> History<i64, D> {
|
||||
agent.message = time_slice.forward_prior_out(&agent_idx);
|
||||
}
|
||||
|
||||
if !self.time {
|
||||
for agent_idx in &this_agent {
|
||||
let c = self.agents.get_mut(*agent_idx).unwrap();
|
||||
if c.last_time.is_some() {
|
||||
c.last_time = Some(t);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
k += 1;
|
||||
}
|
||||
|
||||
i = j;
|
||||
}
|
||||
|
||||
while self.time && self.time_slices.len() > k {
|
||||
while self.time_slices.len() > k {
|
||||
let time_slice = &mut self.time_slices[k];
|
||||
|
||||
time_slice.new_forward_info(&self.agents);
|
||||
@@ -724,29 +683,30 @@ mod tests {
|
||||
.sigma(25.0 / 3.0)
|
||||
.beta(25.0 / 6.0)
|
||||
.gamma(25.0 / 300.0)
|
||||
.time(false)
|
||||
.build();
|
||||
|
||||
h.add_events(composition, results, vec![], vec![]);
|
||||
let n = composition.len();
|
||||
let times: Vec<i64> = (1..=n as i64).collect();
|
||||
h.add_events(composition, results, times, vec![]);
|
||||
|
||||
h.convergence(ITERATIONS, EPSILON, false);
|
||||
|
||||
assert_eq!(h.time_slices[2].skills.get(b).unwrap().elapsed, 1);
|
||||
assert_eq!(h.time_slices[2].skills.get(b).unwrap().elapsed, 2);
|
||||
assert_eq!(h.time_slices[2].skills.get(c).unwrap().elapsed, 1);
|
||||
|
||||
assert_ulps_eq!(
|
||||
h.time_slices[0].skills.get(a).unwrap().posterior(),
|
||||
Gaussian::from_ms(25.000267, 5.419381),
|
||||
Gaussian::from_ms(25.000267, 5.419423),
|
||||
epsilon = 1e-6
|
||||
);
|
||||
assert_ulps_eq!(
|
||||
h.time_slices[0].skills.get(b).unwrap().posterior(),
|
||||
Gaussian::from_ms(24.999465, 5.419425),
|
||||
Gaussian::from_ms(24.999198, 5.419512),
|
||||
epsilon = 1e-6
|
||||
);
|
||||
assert_ulps_eq!(
|
||||
h.time_slices[2].skills.get(b).unwrap().posterior(),
|
||||
Gaussian::from_ms(25.000532, 5.419696),
|
||||
Gaussian::from_ms(25.001332, 5.420054),
|
||||
epsilon = 1e-6
|
||||
);
|
||||
}
|
||||
@@ -774,10 +734,11 @@ mod tests {
|
||||
.sigma(6.0)
|
||||
.beta(1.0)
|
||||
.gamma(0.0)
|
||||
.time(false)
|
||||
.build();
|
||||
|
||||
h.add_events(composition, results, vec![], vec![]);
|
||||
let n = composition.len();
|
||||
let times: Vec<i64> = (1..=n as i64).collect();
|
||||
h.add_events(composition, results, times, vec![]);
|
||||
|
||||
let trueskill_log_evidence = h.log_evidence(false, &[]);
|
||||
let trueskill_log_evidence_online = h.log_evidence(true, &[]);
|
||||
@@ -861,14 +822,15 @@ mod tests {
|
||||
.sigma(2.0)
|
||||
.beta(1.0)
|
||||
.gamma(0.0)
|
||||
.time(false)
|
||||
.build();
|
||||
|
||||
h.add_events(composition.clone(), results.clone(), vec![], vec![]);
|
||||
let n = composition.len();
|
||||
let times: Vec<i64> = (1..=n as i64).collect();
|
||||
h.add_events(composition.clone(), results.clone(), times, vec![]);
|
||||
|
||||
h.convergence(ITERATIONS, EPSILON, false);
|
||||
|
||||
assert_eq!(h.time_slices[2].skills.get(b).unwrap().elapsed, 1);
|
||||
assert_eq!(h.time_slices[2].skills.get(b).unwrap().elapsed, 2);
|
||||
assert_eq!(h.time_slices[2].skills.get(c).unwrap().elapsed, 1);
|
||||
|
||||
assert_ulps_eq!(
|
||||
@@ -887,7 +849,8 @@ mod tests {
|
||||
epsilon = 1e-6
|
||||
);
|
||||
|
||||
h.add_events(composition, results, vec![], vec![]);
|
||||
let times2: Vec<i64> = (n as i64 + 1..=2 * n as i64).collect();
|
||||
h.add_events(composition, results, times2, vec![]);
|
||||
|
||||
assert_eq!(h.time_slices.len(), 6);
|
||||
|
||||
@@ -950,14 +913,15 @@ mod tests {
|
||||
.sigma(2.0)
|
||||
.beta(1.0)
|
||||
.gamma(0.0)
|
||||
.time(false)
|
||||
.build();
|
||||
|
||||
h.add_events(composition.clone(), results.clone(), vec![], vec![]);
|
||||
let n = composition.len();
|
||||
let times: Vec<i64> = (1..=n as i64).collect();
|
||||
h.add_events(composition.clone(), results.clone(), times, vec![]);
|
||||
|
||||
h.convergence(ITERATIONS, EPSILON, false);
|
||||
|
||||
assert_eq!(h.time_slices[2].skills.get(b).unwrap().elapsed, 1);
|
||||
assert_eq!(h.time_slices[2].skills.get(b).unwrap().elapsed, 2);
|
||||
assert_eq!(h.time_slices[2].skills.get(c).unwrap().elapsed, 1);
|
||||
|
||||
assert_ulps_eq!(
|
||||
@@ -976,7 +940,8 @@ mod tests {
|
||||
epsilon = 1e-6
|
||||
);
|
||||
|
||||
h.add_events(composition, results, vec![], vec![]);
|
||||
let times2: Vec<i64> = (n as i64 + 1..=2 * n as i64).collect();
|
||||
h.add_events(composition, results, times2, vec![]);
|
||||
|
||||
assert_eq!(h.time_slices.len(), 6);
|
||||
|
||||
@@ -1028,9 +993,11 @@ mod tests {
|
||||
|
||||
let composition = vec![vec![vec![a], vec![b]], vec![vec![b], vec![a]]];
|
||||
|
||||
let mut h = History::builder().time(false).build();
|
||||
let mut h = History::builder().build();
|
||||
|
||||
h.add_events(composition.clone(), vec![], vec![], vec![]);
|
||||
let n = composition.len();
|
||||
let times: Vec<i64> = (1..=n as i64).collect();
|
||||
h.add_events(composition.clone(), vec![], times.clone(), vec![]);
|
||||
|
||||
let p_d_m_2 = h.log_evidence(false, &[]).exp() * 2.0;
|
||||
|
||||
@@ -1067,9 +1034,9 @@ mod tests {
|
||||
epsilon = 1e-4
|
||||
);
|
||||
|
||||
let mut h = History::builder().time(false).build();
|
||||
let mut h = History::builder().build();
|
||||
|
||||
h.add_events(composition, vec![], vec![], vec![]);
|
||||
h.add_events(composition, vec![], times, vec![]);
|
||||
|
||||
assert_ulps_eq!(
|
||||
((0.5f64 * 0.1765).ln() / 2.0).exp(),
|
||||
@@ -1282,10 +1249,11 @@ mod tests {
|
||||
.sigma(6.0)
|
||||
.beta(1.0)
|
||||
.gamma(0.0)
|
||||
.time(false)
|
||||
.build();
|
||||
|
||||
h.add_events(composition, vec![], vec![], weights);
|
||||
let n = composition.len();
|
||||
let times: Vec<i64> = (1..=n as i64).collect();
|
||||
h.add_events(composition, vec![], times, weights);
|
||||
|
||||
let lc = h.learning_curves();
|
||||
|
||||
|
||||
10
src/lib.rs
10
src/lib.rs
@@ -172,13 +172,13 @@ pub(crate) fn tuple_gt(t: (f64, f64), e: f64) -> bool {
|
||||
t.0 > e || t.1 > e
|
||||
}
|
||||
|
||||
pub(crate) fn sort_time(xs: &[i64], reverse: bool) -> Vec<usize> {
|
||||
let mut x = xs.iter().enumerate().collect::<Vec<_>>();
|
||||
pub(crate) fn sort_time<T: Copy + Ord>(xs: &[T], reverse: bool) -> Vec<usize> {
|
||||
let mut x: Vec<(usize, T)> = xs.iter().enumerate().map(|(i, &t)| (i, t)).collect();
|
||||
|
||||
if reverse {
|
||||
x.sort_by_key(|&(_, x)| Reverse(x));
|
||||
x.sort_by_key(|&(_, t)| Reverse(t));
|
||||
} else {
|
||||
x.sort_by_key(|&(_, x)| x);
|
||||
x.sort_by_key(|&(_, t)| t);
|
||||
}
|
||||
|
||||
x.into_iter().map(|(i, _)| i).collect()
|
||||
@@ -254,7 +254,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_sort_time() {
|
||||
assert_eq!(sort_time(&[0, 1, 2, 0], true), vec![2, 1, 0, 3]);
|
||||
assert_eq!(sort_time(&[0i64, 1, 2, 0], true), vec![2, 1, 0, 3]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user