//! Observer trait for progress reporting during convergence. //! //! Replaces the old `verbose: bool` + `println!` path. Callers wire in any //! observer that implements the trait; default methods are no-ops so users //! override only what they need. use crate::time::Time; /// Receives progress callbacks during `History::converge`. /// /// All methods have default no-op implementations; implement only what's /// interesting. pub trait Observer: Send + Sync { /// Called after each convergence iteration across the whole history. fn on_iteration_end(&self, _iter: usize, _max_step: (f64, f64)) {} /// Called after each time slice is swept within an iteration. /// /// A convergence iteration sweeps every slice twice — once travelling /// backward through the history and once forward — so a multi-slice /// history fires this twice per slice per iteration. A single-slice /// history is swept once and fires once. fn on_slice_processed(&self, _time: &T, _slice_idx: usize, _n_events: usize) {} /// Called once when convergence completes (or max iters is reached). fn on_converged(&self, _iters: usize, _final_step: (f64, f64), _converged: bool) {} } /// Shared and boxed observers forward to what they point at. /// /// `History` takes its observer by value, so a caller who wants to *read* what /// an observer recorded has to keep a handle to it. Without these impls the /// natural spelling does not compile: /// /// ``` /// # use std::sync::{Arc, Mutex}; /// # use trueskill_tt::{History, Observer}; /// #[derive(Default)] /// struct Recorder { /// iterations: Mutex>, /// } /// /// impl Observer for Recorder { /// fn on_iteration_end(&self, iter: usize, _step: (f64, f64)) { /// self.iterations.lock().unwrap().push(iter); /// } /// } /// /// let recorder = Arc::new(Recorder::default()); /// let mut h = History::builder().observer(Arc::clone(&recorder)).build(); /// h.record_winner(&"a", &"b", 1).unwrap(); /// h.converge().unwrap(); /// /// // The caller's handle sees what the history's copy recorded. /// assert!(!recorder.iterations.lock().unwrap().is_empty()); /// ``` /// /// The alternative was for every observer to wrap each of its own fields in an /// `Arc` and derive `Clone` — one allocation and one lock per field, and a /// pattern each implementor had to rediscover. /// /// `?Sized` is deliberate: it makes `Arc>` and /// `Box>` work, so observers can be chosen at runtime. impl + ?Sized> Observer for std::sync::Arc { fn on_iteration_end(&self, iter: usize, max_step: (f64, f64)) { (**self).on_iteration_end(iter, max_step); } fn on_slice_processed(&self, time: &T, slice_idx: usize, n_events: usize) { (**self).on_slice_processed(time, slice_idx, n_events); } fn on_converged(&self, iters: usize, final_step: (f64, f64), converged: bool) { (**self).on_converged(iters, final_step, converged); } } impl + ?Sized> Observer for Box { fn on_iteration_end(&self, iter: usize, max_step: (f64, f64)) { (**self).on_iteration_end(iter, max_step); } fn on_slice_processed(&self, time: &T, slice_idx: usize, n_events: usize) { (**self).on_slice_processed(time, slice_idx, n_events); } fn on_converged(&self, iters: usize, final_step: (f64, f64), converged: bool) { (**self).on_converged(iters, final_step, converged); } } impl + ?Sized> Observer for &O { fn on_iteration_end(&self, iter: usize, max_step: (f64, f64)) { (**self).on_iteration_end(iter, max_step); } fn on_slice_processed(&self, time: &T, slice_idx: usize, n_events: usize) { (**self).on_slice_processed(time, slice_idx, n_events); } fn on_converged(&self, iters: usize, final_step: (f64, f64), converged: bool) { (**self).on_converged(iters, final_step, converged); } } /// ZST no-op observer; the default when none is configured. #[derive(Copy, Clone, Debug, Default)] pub struct NullObserver; impl Observer for NullObserver {} #[cfg(test)] mod tests { use super::*; #[test] fn null_observer_compiles_for_i64() { let o = NullObserver; >::on_iteration_end(&o, 1, (0.0, 0.0)); >::on_slice_processed(&o, &7, 0, 3); >::on_converged(&o, 5, (1e-6, 1e-6), true); } #[test] fn null_observer_compiles_for_untimed() { use crate::Untimed; let o = NullObserver; >::on_iteration_end(&o, 1, (0.0, 0.0)); } }