feat(history): parallel learning_curves under rayon feature

Per-slice posterior collection runs in parallel via par_iter; merge
into the per-key HashMap is sequential in slice order so iteration
order and HashMap insertion order are identical to the sequential
impl. Preserves deterministic output across thread counts.

Default-feature (no rayon) build unchanged — uses the T2 sequential
impl.

Part of T3.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-24 13:54:47 +02:00
parent 4b99485fc8
commit f3c074c24c

View File

@@ -262,17 +262,45 @@ impl<T: Time, D: Drift<T>, O: Observer<T>, K: Eq + Hash + Clone> History<T, D, O
/// Note: `key(idx)` is O(n) per lookup; this method is therefore O(n²) /// Note: `key(idx)` is O(n) per lookup; this method is therefore O(n²)
/// in the number of competitors. Acceptable for T2; T3 may optimize. /// in the number of competitors. Acceptable for T2; T3 may optimize.
pub fn learning_curves(&self) -> HashMap<K, Vec<(T, Gaussian)>> { pub fn learning_curves(&self) -> HashMap<K, Vec<(T, Gaussian)>> {
let mut data: HashMap<K, Vec<(T, Gaussian)>> = HashMap::new(); #[cfg(feature = "rayon")]
for slice in &self.time_slices { {
for (idx, skill) in slice.skills.iter() { use rayon::prelude::*;
if let Some(key) = self.keys.key(idx).cloned() {
data.entry(key) let per_slice: Vec<Vec<(Index, T, Gaussian)>> = self
.or_default() .time_slices
.push((slice.time, skill.posterior())); .par_iter()
.map(|ts| {
ts.skills
.iter()
.map(|(idx, sk)| (idx, ts.time, sk.posterior()))
.collect()
})
.collect();
let mut data: HashMap<K, Vec<(T, Gaussian)>> = HashMap::new();
for slice_contrib in per_slice {
for (idx, t, g) in slice_contrib {
if let Some(key) = self.keys.key(idx).cloned() {
data.entry(key).or_default().push((t, g));
}
} }
} }
data
}
#[cfg(not(feature = "rayon"))]
{
let mut data: HashMap<K, Vec<(T, Gaussian)>> = HashMap::new();
for slice in &self.time_slices {
for (idx, skill) in slice.skills.iter() {
if let Some(key) = self.keys.key(idx).cloned() {
data.entry(key)
.or_default()
.push((slice.time, skill.posterior()));
}
}
}
data
} }
data
} }
/// Skill estimate at the latest time slice the competitor appears in. /// Skill estimate at the latest time slice the competitor appears in.