`Rating` already derived `PartialEq`, but that derive is only reachable through `D: PartialEq` — and `ConstantDrift`, the crate's own only `Drift` impl, did not satisfy it. So the derive was there and unusable. Found by writing the comparison from a consumer's position rather than reading the derive list. `ConstantDrift`, `ConvergenceOptions` and `GameOptions` now derive `PartialEq`. All three are pure configuration; comparing two is the natural thing to want and nothing about them makes equality ambiguous. `tests/trait_impls.rs` pins the surface, written the way the failure was reported: a consumer struct that *holds* a `History` and derives `Debug`. It also asserts `History`'s `Debug` summarises rather than dumping its skill stores, so a future derive cannot quietly replace the hand-written impl. `Clone` on `History` stays off. It is a decision, not an omission: a history owns every slice's skill store and arena, so cloning one is proportional to the whole fit, and no consumer has wanted it. Closes #76. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011hcFjNDmHXZF8URGLku5zZ
90 lines
3.0 KiB
Rust
90 lines
3.0 KiB
Rust
//! Convergence configuration and reporting.
|
|
|
|
use std::time::Duration;
|
|
|
|
use smallvec::SmallVec;
|
|
|
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
|
pub struct ConvergenceOptions {
|
|
pub max_iter: usize,
|
|
pub epsilon: f64,
|
|
/// EP damping factor in natural-parameter space: each per-factor
|
|
/// update inside a single game writes `α·new + (1−α)·old`. `1.0` is
|
|
/// undamped (default); `< 1.0` stabilises oscillating fixed-point
|
|
/// loops at the cost of more iterations. Must be in `(0.0, 1.0]`.
|
|
///
|
|
/// Applies only to the within-game EP loop (`run_chain`). The outer
|
|
/// `History::converge` cross-history sweep is undamped regardless of
|
|
/// this value — cross-slice damping is a different concept and not
|
|
/// in scope.
|
|
pub alpha: f64,
|
|
}
|
|
|
|
impl ConvergenceOptions {
|
|
/// Reject values that would make inference silently meaningless.
|
|
///
|
|
/// `HistoryBuilder::convergence` asserts these eagerly, but the fields are
|
|
/// public and `GameOptions` carries a `ConvergenceOptions` — so a caller
|
|
/// can hand `Game::ranked` a set the builder never saw. In release the
|
|
/// engine's `debug_assert!`s are gone, and an `alpha` of zero leaves every
|
|
/// EP update unapplied: inference returns the priors, with every likelihood
|
|
/// uninformative and nothing to indicate anything went wrong.
|
|
///
|
|
/// # Errors
|
|
///
|
|
/// `InvalidParameter` if `alpha` is outside `(0.0, 1.0]` or `epsilon` is
|
|
/// negative. NaN fails both comparisons and is rejected.
|
|
pub(crate) fn validate(&self) -> Result<(), crate::InferenceError> {
|
|
if !(self.alpha > 0.0 && self.alpha <= 1.0) {
|
|
return Err(crate::InferenceError::InvalidParameter {
|
|
name: "alpha",
|
|
value: self.alpha,
|
|
});
|
|
}
|
|
if self.epsilon.is_nan() || self.epsilon < 0.0 {
|
|
return Err(crate::InferenceError::InvalidParameter {
|
|
name: "epsilon",
|
|
value: self.epsilon,
|
|
});
|
|
}
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
impl Default for ConvergenceOptions {
|
|
fn default() -> Self {
|
|
Self {
|
|
max_iter: crate::ITERATIONS,
|
|
epsilon: crate::EPSILON,
|
|
alpha: 1.0,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Post-hoc summary of a `History::converge` call.
|
|
///
|
|
/// From [`History::converge`](crate::History::converge) this always describes a
|
|
/// converged fit — stopping at `max_iter` is
|
|
/// [`InferenceError::NotConverged`](crate::InferenceError::NotConverged) there.
|
|
/// From [`History::converge_partial`](crate::History::converge_partial) it may
|
|
/// not be, and `converged` is what says so.
|
|
#[derive(Clone, Debug, PartialEq)]
|
|
pub struct ConvergenceReport {
|
|
pub iterations: usize,
|
|
pub final_step: (f64, f64),
|
|
pub log_evidence: f64,
|
|
pub converged: bool,
|
|
pub per_iteration_time: SmallVec<[Duration; 32]>,
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn default_alpha_is_one_for_undamped_behavior() {
|
|
let opts = ConvergenceOptions::default();
|
|
assert_eq!(opts.alpha, 1.0);
|
|
}
|
|
}
|