use pounce_common::types::{Index, Number};
#[derive(Debug, Clone)]
pub struct HandoverOptions {
pub enabled: bool,
pub tol: Number,
pub stall_iter: Index,
pub max_attempts: u32,
}
impl Default for HandoverOptions {
fn default() -> Self {
Self {
enabled: false,
tol: 1e-4,
stall_iter: 0,
max_attempts: 3,
}
}
}
#[derive(Debug, Clone)]
pub struct HandoverTrigger {
opts: HandoverOptions,
attempts: u32,
last_attempt: Option<(Number, Index)>,
best: Option<(Number, Index)>,
}
impl HandoverTrigger {
pub fn new(opts: HandoverOptions) -> Self {
Self {
opts,
attempts: 0,
last_attempt: None,
best: None,
}
}
pub fn should_try(&mut self, iter: Index, err: Number) -> bool {
if !self.opts.enabled || !err.is_finite() || self.attempts >= self.opts.max_attempts {
return false;
}
match self.best {
Some((b, _)) if err > 0.5 * b => {}
_ => self.best = Some((err, iter)),
}
let stall = self.opts.stall_iter;
let stalled = stall > 0 && self.best.is_some_and(|(_, at)| iter - at >= stall);
let fire = err <= self.opts.tol || stalled;
if !fire {
return false;
}
if let Some((e0, i0)) = self.last_attempt {
let improved = err <= 0.1 * e0;
let waited = stall > 0 && iter - i0 >= stall;
if !(improved || waited) {
return false;
}
}
self.attempts += 1;
self.last_attempt = Some((err, iter));
self.best = Some((err, iter));
true
}
pub fn attempts(&self) -> u32 {
self.attempts
}
}
#[derive(Debug, Clone, Default)]
pub struct HandoverReport {
pub attempts: u32,
pub accepted_at_iter: Option<Index>,
pub last: Option<crate::crossover::CrossoverReport>,
}
pub fn identification_primal_tol(crossover_primal_tol: Number, mu: Number) -> Number {
let sep = if mu.is_finite() && mu > 0.0 {
mu.sqrt()
} else {
0.0
};
crossover_primal_tol.max(sep)
}
#[cfg(test)]
mod tests {
use super::*;
fn trig(tol: Number, stall: Index, max: u32) -> HandoverTrigger {
HandoverTrigger::new(HandoverOptions {
enabled: true,
tol,
stall_iter: stall,
max_attempts: max,
})
}
#[test]
fn off_never_fires() {
let mut t = HandoverTrigger::new(HandoverOptions::default());
assert!(!t.should_try(10, 0.0));
}
#[test]
fn tolerance_fires_once_reached() {
let mut t = trig(1e-4, 0, 3);
assert!(!t.should_try(1, 1.0));
assert!(!t.should_try(2, 1e-3));
assert!(t.should_try(3, 5e-5));
assert_eq!(t.attempts(), 1);
}
#[test]
fn a_declined_attempt_backs_off_until_an_order_of_magnitude() {
let mut t = trig(1e-4, 0, 3);
assert!(t.should_try(3, 5e-5));
assert!(!t.should_try(4, 3e-5), "not yet 10x below the last attempt");
assert!(!t.should_try(5, 6e-6));
assert!(t.should_try(6, 4e-6));
assert_eq!(t.attempts(), 2);
}
#[test]
fn stall_fires_without_reaching_the_tolerance() {
let mut t = trig(1e-8, 5, 3);
assert!(!t.should_try(0, 1e-2));
for (k, e) in [(1, 1.2e-2), (2, 0.9e-2), (3, 1.1e-2), (4, 0.8e-2)] {
assert!(!t.should_try(k, e));
}
assert!(t.should_try(5, 1.0e-2));
assert!(!t.should_try(8, 1.0e-2));
assert!(t.should_try(10, 1.0e-2));
}
#[test]
fn progress_resets_the_stall_window() {
let mut t = trig(1e-8, 3, 3);
assert!(!t.should_try(0, 1.0));
assert!(!t.should_try(2, 0.4)); assert!(!t.should_try(4, 0.35));
assert!(t.should_try(5, 0.35));
}
#[test]
fn attempts_are_capped() {
let mut t = trig(1.0, 1, 2);
assert!(t.should_try(0, 0.5));
assert!(t.should_try(1, 0.5));
assert!(!t.should_try(2, 0.5));
assert!(!t.should_try(3, 1e-9));
}
#[test]
fn identification_tolerance_follows_the_barrier_width() {
assert_eq!(identification_primal_tol(1e-6, 1e-4), 1e-2);
assert_eq!(identification_primal_tol(1e-6, 1e-14), 1e-6);
assert_eq!(identification_primal_tol(1e-6, Number::NAN), 1e-6);
}
}