pub const MADSEN_DAMPING_CAP: f64 = 1e12;
pub const PLATEAU_DEFAULT_WINDOW: usize = 3;
pub const PLATEAU_DEFAULT_REL_TOL: f64 = 1e-8;
#[inline]
pub fn madsen_can_retry(damping: f64) -> bool {
damping.is_finite() && damping < MADSEN_DAMPING_CAP
}
#[inline]
pub fn madsen_retry_exhausted(damping: f64, attempts: usize, max_attempts: usize) -> bool {
attempts >= max_attempts || !damping.is_finite() || damping > MADSEN_DAMPING_CAP
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum LoopVerdict {
Continue,
Plateaued,
Exhausted,
}
#[derive(Clone, Debug)]
pub struct PlateauDetector {
window: usize,
rel_tol: f64,
streak: usize,
last_merit: Option<f64>,
}
impl PlateauDetector {
pub fn new(window: usize, rel_tol: f64) -> Self {
Self {
window: window.max(1),
rel_tol,
streak: 0,
last_merit: None,
}
}
pub fn standard() -> Self {
Self::new(PLATEAU_DEFAULT_WINDOW, PLATEAU_DEFAULT_REL_TOL)
}
pub fn note(&mut self, merit: f64) -> LoopVerdict {
if !merit.is_finite() {
self.streak = 0;
self.last_merit = None;
return LoopVerdict::Continue;
}
let verdict = match self.last_merit {
Some(prev) => {
let scale = prev.abs().max(merit.abs()).max(1.0);
if (prev - merit).abs() <= self.rel_tol * scale {
self.streak += 1;
if self.streak >= self.window {
LoopVerdict::Plateaued
} else {
LoopVerdict::Continue
}
} else {
self.streak = 0;
LoopVerdict::Continue
}
}
None => LoopVerdict::Continue,
};
self.last_merit = Some(merit);
verdict
}
}
#[derive(Clone, Debug)]
pub struct MadsenGuard {
attempts: usize,
max_attempts: usize,
reject_factor: f64,
}
pub const MADSEN_INITIAL_REJECT_FACTOR: f64 = 2.0;
impl MadsenGuard {
pub fn new(max_attempts: usize) -> Self {
Self {
attempts: 0,
max_attempts: max_attempts.max(1),
reject_factor: MADSEN_INITIAL_REJECT_FACTOR,
}
}
pub fn escalate(&mut self, damping: &mut f64) {
self.attempts += 1;
*damping *= self.reject_factor;
self.reject_factor *= 2.0;
}
pub fn verdict(&self, damping: f64) -> LoopVerdict {
if madsen_retry_exhausted(damping, self.attempts, self.max_attempts) {
LoopVerdict::Exhausted
} else {
LoopVerdict::Continue
}
}
pub fn attempts(&self) -> usize {
self.attempts
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn reject_storm_exhausts_in_bounded_steps() {
let mut guard = MadsenGuard::new(usize::MAX); let mut damping = 1.0;
let mut steps = 0usize;
while guard.verdict(damping) == LoopVerdict::Continue {
guard.escalate(&mut damping);
steps += 1;
assert!(steps <= 64, "escalation must reach the damping cap");
}
assert!(steps <= 16, "escalation took {steps} steps");
assert!(!madsen_can_retry(damping));
}
#[test]
fn attempt_cap_exhausts_even_at_benign_damping() {
let mut guard = MadsenGuard::new(3);
let mut damping = 1e-6;
for _ in 0..3 {
assert_eq!(guard.verdict(1e-6), LoopVerdict::Continue);
let mut local = damping;
guard.escalate(&mut local);
damping = 1e-6;
}
assert_eq!(guard.verdict(damping), LoopVerdict::Exhausted);
}
#[test]
fn plateau_fires_on_frozen_merit_and_not_during_descent() {
let mut det = PlateauDetector::new(3, 1e-8);
let mut merit = 100.0;
for _ in 0..50 {
assert_eq!(det.note(merit), LoopVerdict::Continue, "descent phase");
merit *= 0.9;
}
let mut fired_after = 0usize;
loop {
fired_after += 1;
assert!(fired_after <= 4, "plateau must fire within the window");
if det.note(merit) == LoopVerdict::Plateaued {
break;
}
}
assert!(fired_after >= 3, "must not fire before the streak window");
}
#[test]
fn plateau_resets_on_recovery_and_ignores_non_finite() {
let mut det = PlateauDetector::new(2, 1e-8);
det.note(10.0);
assert_eq!(det.note(10.0), LoopVerdict::Continue); assert_eq!(det.note(9.0), LoopVerdict::Continue); assert_eq!(det.note(9.0), LoopVerdict::Continue); assert_eq!(det.note(f64::NAN), LoopVerdict::Continue); assert_eq!(det.note(9.0), LoopVerdict::Continue); assert_eq!(det.note(9.0), LoopVerdict::Continue); assert_eq!(det.note(9.0), LoopVerdict::Plateaued); }
#[test]
fn policy_predicates_pin_the_reweight_semantics() {
assert!(madsen_can_retry(1e11));
assert!(!madsen_can_retry(MADSEN_DAMPING_CAP));
assert!(!madsen_can_retry(f64::INFINITY));
assert!(madsen_retry_exhausted(1.0, 5, 5));
assert!(madsen_retry_exhausted(f64::NAN, 0, 5));
assert!(madsen_retry_exhausted(1e13, 0, 5));
assert!(!madsen_retry_exhausted(1.0, 4, 5));
}
}