From 33a7d90b89e90c7054006f14b89078e07249cb06 Mon Sep 17 00:00:00 2001 From: Anders Olsson Date: Fri, 24 Apr 2026 12:09:23 +0200 Subject: [PATCH] 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 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 --- src/history.rs | 110 ++++++++++++++++++------------------------------- src/lib.rs | 10 ++--- 2 files changed, 44 insertions(+), 76 deletions(-) diff --git a/src/history.rs b/src/history.rs index 9238e55..f162b3c 100644 --- a/src/history.rs +++ b/src/history.rs @@ -15,7 +15,6 @@ use crate::{ #[derive(Clone)] pub struct HistoryBuilder = ConstantDrift> { - time: bool, mu: f64, sigma: f64, beta: f64, @@ -26,11 +25,6 @@ pub struct HistoryBuilder = ConstantDrift> { } impl> HistoryBuilder { - 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> HistoryBuilder { pub fn drift>(self, drift: D2) -> HistoryBuilder { HistoryBuilder { drift, - time: self.time, mu: self.mu, sigma: self.sigma, beta: self.beta, @@ -74,7 +67,6 @@ impl> HistoryBuilder { 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 { impl Default for HistoryBuilder { fn default() -> Self { Self { - time: true, mu: MU, sigma: SIGMA, beta: BETA, @@ -111,7 +102,6 @@ pub struct History = ConstantDrift> { size: usize, pub(crate) time_slices: Vec>, pub(crate) agents: CompetitorStore, - time: bool, mu: f64, sigma: f64, beta: f64, @@ -126,7 +116,6 @@ impl Default for History { size: 0, time_slices: Vec::new(), agents: CompetitorStore::new(), - time: true, mu: MU, sigma: SIGMA, beta: BETA, @@ -275,18 +264,13 @@ impl> History { weights: Vec>>, mut priors: HashMap>, ) { - 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> History { } let n = composition.len(); - let o = if self.time { - sort_time(×, false) - } else { - (0..composition.len()).collect::>() - }; + 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> History { } } - 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> History { (i..j).map(|e| weights[o[e]].clone()).collect::>() }; - 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> History { 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 = (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 = (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 = (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 = (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 = (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 = (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 = (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 = (1..=n as i64).collect(); + h.add_events(composition, vec![], times, weights); let lc = h.learning_curves(); diff --git a/src/lib.rs b/src/lib.rs index 9147875..f9aa348 100644 --- a/src/lib.rs +++ b/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 { - let mut x = xs.iter().enumerate().collect::>(); +pub(crate) fn sort_time(xs: &[T], reverse: bool) -> Vec { + 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]