use std::borrow::Cow;
use cobre_core::StoppingRuleResult;
pub const RULE_ITERATION_LIMIT: &str = "iteration_limit";
pub const RULE_TIME_LIMIT: &str = "time_limit";
pub const RULE_BOUND_STALLING: &str = "bound_stalling";
pub const RULE_SIMULATION_BASED: &str = "simulation_based";
pub const RULE_GRACEFUL_SHUTDOWN: &str = "graceful_shutdown";
#[derive(Debug, Clone)]
pub struct MonitorState {
pub iteration: u64,
pub wall_time_seconds: f64,
pub lower_bound: f64,
pub lower_bound_history: Vec<f64>,
pub shutdown_requested: bool,
pub simulation_costs: Option<Vec<f64>>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum StoppingMode {
Any,
All,
}
#[derive(Debug, Clone)]
pub enum StoppingRule {
IterationLimit {
limit: u64,
},
TimeLimit {
seconds: f64,
},
BoundStalling {
tolerance: f64,
iterations: u64,
},
SimulationBased {
period: u64,
distance_tolerance: f64,
replications: u32,
bound_stability_window: u64,
},
GracefulShutdown,
}
impl StoppingRule {
#[must_use]
pub fn evaluate(&self, state: &MonitorState) -> StoppingRuleResult {
match self {
Self::IterationLimit { limit } => {
let triggered = state.iteration >= *limit;
StoppingRuleResult {
rule_name: RULE_ITERATION_LIMIT,
triggered,
detail: Cow::Owned(format!("iteration {}/{}", state.iteration, limit)),
}
}
Self::TimeLimit { seconds } => {
let triggered = state.wall_time_seconds >= *seconds;
StoppingRuleResult {
rule_name: RULE_TIME_LIMIT,
triggered,
detail: Cow::Owned(format!(
"elapsed {:.1}s / {:.1}s limit",
state.wall_time_seconds, seconds
)),
}
}
Self::BoundStalling {
tolerance,
iterations,
} => Self::evaluate_bound_stalling(state, *tolerance, *iterations),
Self::SimulationBased {
period,
distance_tolerance,
replications: _,
bound_stability_window: _,
} => Self::evaluate_simulation_based(state, *period, *distance_tolerance),
Self::GracefulShutdown => {
let triggered = state.shutdown_requested;
StoppingRuleResult {
rule_name: RULE_GRACEFUL_SHUTDOWN,
triggered,
detail: if triggered {
Cow::Borrowed("shutdown signal received")
} else {
Cow::Borrowed("no shutdown signal")
},
}
}
}
}
fn evaluate_bound_stalling(
state: &MonitorState,
tolerance: f64,
iterations: u64,
) -> StoppingRuleResult {
#[allow(clippy::cast_possible_truncation)]
let window = iterations as usize;
if state.lower_bound_history.len() < window {
return StoppingRuleResult {
rule_name: RULE_BOUND_STALLING,
triggered: false,
detail: Cow::Owned(format!(
"insufficient history: {}/{} iterations",
state.lower_bound_history.len(),
window
)),
};
}
let history_len = state.lower_bound_history.len();
let lb_window_start = state.lower_bound_history[history_len - window];
let lb_current = state.lower_bound;
let denominator = lb_current.abs().max(1.0_f64);
let delta = (lb_current - lb_window_start) / denominator;
let triggered = delta.abs() < tolerance;
StoppingRuleResult {
rule_name: RULE_BOUND_STALLING,
triggered,
detail: Cow::Owned(format!(
"relative improvement {:.6} / tolerance {:.6} over {} iterations",
delta.abs(),
tolerance,
window
)),
}
}
fn evaluate_simulation_based(
state: &MonitorState,
period: u64,
distance_tolerance: f64,
) -> StoppingRuleResult {
if period == 0 || state.iteration % period != 0 {
return StoppingRuleResult {
rule_name: RULE_SIMULATION_BASED,
triggered: false,
detail: Cow::Owned(format!(
"not a check iteration ({}/{})",
state.iteration, period
)),
};
}
let Some(ref current_costs) = state.simulation_costs else {
return StoppingRuleResult {
rule_name: RULE_SIMULATION_BASED,
triggered: false,
detail: Cow::Borrowed(
"no simulation results available (bound stability check failed or first check)",
),
};
};
let distance: f64 = current_costs
.iter()
.map(|&c| {
let denom = c.abs().max(1.0_f64);
let normalized = c / denom;
normalized * normalized
})
.sum::<f64>()
.sqrt();
let triggered = distance < distance_tolerance;
StoppingRuleResult {
rule_name: RULE_SIMULATION_BASED,
triggered,
detail: Cow::Owned(format!(
"simulation distance {distance:.6} / tolerance {distance_tolerance:.6}"
)),
}
}
}
#[derive(Debug, Clone)]
pub struct StoppingRuleSet {
pub rules: Vec<StoppingRule>,
pub mode: StoppingMode,
}
impl StoppingRuleSet {
#[must_use]
pub fn evaluate(&self, state: &MonitorState) -> (bool, Vec<StoppingRuleResult>) {
if state.shutdown_requested {
let results: Vec<StoppingRuleResult> =
self.rules.iter().map(|r| r.evaluate(state)).collect();
return (true, results);
}
let results: Vec<StoppingRuleResult> =
self.rules.iter().map(|r| r.evaluate(state)).collect();
let non_shutdown_triggered: Vec<bool> = self
.rules
.iter()
.zip(results.iter())
.filter(|(rule, _)| !matches!(rule, StoppingRule::GracefulShutdown))
.map(|(_, result)| result.triggered)
.collect();
let should_stop = match self.mode {
StoppingMode::Any => non_shutdown_triggered.iter().any(|&t| t),
StoppingMode::All => {
!non_shutdown_triggered.is_empty() && non_shutdown_triggered.iter().all(|&t| t)
}
};
(should_stop, results)
}
}
#[cfg(test)]
mod tests {
use super::{MonitorState, StoppingMode, StoppingRule, StoppingRuleSet};
fn make_state(iteration: u64, wall_time: f64, lb: f64, history: Vec<f64>) -> MonitorState {
MonitorState {
iteration,
wall_time_seconds: wall_time,
lower_bound: lb,
lower_bound_history: history,
shutdown_requested: false,
simulation_costs: None,
}
}
#[test]
fn iteration_limit_triggered_at_limit() {
let rule = StoppingRule::IterationLimit { limit: 10 };
let state = make_state(10, 0.0, 0.0, vec![]);
let result = rule.evaluate(&state);
assert!(result.triggered);
assert_eq!(result.rule_name, "iteration_limit");
}
#[test]
fn iteration_limit_triggered_above_limit() {
let rule = StoppingRule::IterationLimit { limit: 10 };
let state = make_state(15, 0.0, 0.0, vec![]);
let result = rule.evaluate(&state);
assert!(result.triggered);
}
#[test]
fn iteration_limit_not_triggered_below_limit() {
let rule = StoppingRule::IterationLimit { limit: 10 };
let state = make_state(9, 0.0, 0.0, vec![]);
let result = rule.evaluate(&state);
assert!(!result.triggered);
}
#[test]
fn time_limit_triggered_at_threshold() {
let rule = StoppingRule::TimeLimit { seconds: 3600.0 };
let state = make_state(1, 3600.0, 0.0, vec![]);
let result = rule.evaluate(&state);
assert!(result.triggered);
assert_eq!(result.rule_name, "time_limit");
}
#[test]
fn time_limit_triggered_above_threshold() {
let rule = StoppingRule::TimeLimit { seconds: 3600.0 };
let state = make_state(1, 3700.0, 0.0, vec![]);
let result = rule.evaluate(&state);
assert!(result.triggered);
}
#[test]
fn time_limit_not_triggered_below_threshold() {
let rule = StoppingRule::TimeLimit { seconds: 3600.0 };
let state = make_state(1, 1000.0, 0.0, vec![]);
let result = rule.evaluate(&state);
assert!(!result.triggered);
}
#[test]
fn bound_stalling_not_triggered_with_insufficient_history() {
let rule = StoppingRule::BoundStalling {
tolerance: 0.01,
iterations: 5,
};
let state = make_state(3, 0.0, 100.0, vec![90.0, 95.0, 100.0]);
let result = rule.evaluate(&state);
assert!(!result.triggered);
assert_eq!(result.rule_name, "bound_stalling");
}
#[test]
fn bound_stalling_triggered_when_lb_stable() {
let rule = StoppingRule::BoundStalling {
tolerance: 0.011,
iterations: 5,
};
let history = vec![80.0, 99.0, 99.5, 99.8, 99.9, 100.0];
let state = make_state(6, 0.0, 100.0, history);
let result = rule.evaluate(&state);
assert!(result.triggered);
}
#[test]
fn bound_stalling_not_triggered_when_lb_improving() {
let rule = StoppingRule::BoundStalling {
tolerance: 0.01,
iterations: 5,
};
let history = vec![50.0, 60.0, 70.0, 80.0, 90.0, 100.0];
let state = make_state(6, 0.0, 100.0, history);
let result = rule.evaluate(&state);
assert!(!result.triggered);
}
#[test]
fn bound_stalling_near_zero_lb_uses_max_guard() {
let rule = StoppingRule::BoundStalling {
tolerance: 0.01,
iterations: 3,
};
let history = vec![0.0, 0.0, 0.0, 0.001];
let state = make_state(4, 0.0, 0.001, history);
let result = rule.evaluate(&state);
assert!(result.triggered);
}
#[test]
fn graceful_shutdown_triggered_when_requested() {
let rule = StoppingRule::GracefulShutdown;
let mut state = make_state(1, 0.0, 0.0, vec![]);
state.shutdown_requested = true;
let result = rule.evaluate(&state);
assert!(result.triggered);
assert_eq!(result.rule_name, "graceful_shutdown");
}
#[test]
fn graceful_shutdown_not_triggered_when_not_requested() {
let rule = StoppingRule::GracefulShutdown;
let state = make_state(1, 0.0, 0.0, vec![]);
let result = rule.evaluate(&state);
assert!(!result.triggered);
}
#[test]
fn rule_set_any_mode_stops_on_first_triggered_rule() {
let rule_set = StoppingRuleSet {
rules: vec![
StoppingRule::IterationLimit { limit: 100 },
StoppingRule::TimeLimit { seconds: 3600.0 },
],
mode: StoppingMode::Any,
};
let state = make_state(100, 1000.0, 0.0, vec![]);
let (should_stop, results) = rule_set.evaluate(&state);
assert!(should_stop);
assert_eq!(results.len(), 2);
assert!(results[0].triggered);
assert!(!results[1].triggered);
}
#[test]
fn rule_set_any_mode_does_not_stop_when_no_rules_trigger() {
let rule_set = StoppingRuleSet {
rules: vec![
StoppingRule::IterationLimit { limit: 100 },
StoppingRule::TimeLimit { seconds: 3600.0 },
],
mode: StoppingMode::Any,
};
let state = make_state(50, 1000.0, 0.0, vec![]);
let (should_stop, _) = rule_set.evaluate(&state);
assert!(!should_stop);
}
#[test]
fn rule_set_all_mode_stops_only_when_all_rules_trigger() {
let rule_set = StoppingRuleSet {
rules: vec![
StoppingRule::IterationLimit { limit: 100 },
StoppingRule::TimeLimit { seconds: 3600.0 },
],
mode: StoppingMode::All,
};
let state = make_state(100, 4000.0, 0.0, vec![]);
let (should_stop, results) = rule_set.evaluate(&state);
assert!(should_stop);
assert!(results[0].triggered);
assert!(results[1].triggered);
}
#[test]
fn rule_set_all_mode_does_not_stop_when_only_one_triggers() {
let rule_set = StoppingRuleSet {
rules: vec![
StoppingRule::IterationLimit { limit: 100 },
StoppingRule::TimeLimit { seconds: 3600.0 },
],
mode: StoppingMode::All,
};
let state = make_state(100, 1000.0, 0.0, vec![]);
let (should_stop, _) = rule_set.evaluate(&state);
assert!(!should_stop);
}
#[test]
fn rule_set_graceful_shutdown_bypasses_all_mode() {
let rule_set = StoppingRuleSet {
rules: vec![
StoppingRule::IterationLimit { limit: 100 },
StoppingRule::GracefulShutdown,
],
mode: StoppingMode::All,
};
let mut state = make_state(1, 0.0, 0.0, vec![]);
state.shutdown_requested = true;
let (should_stop, _) = rule_set.evaluate(&state);
assert!(should_stop);
}
#[test]
fn rule_set_graceful_shutdown_bypasses_any_mode() {
let rule_set = StoppingRuleSet {
rules: vec![StoppingRule::GracefulShutdown],
mode: StoppingMode::Any,
};
let mut state = make_state(1, 0.0, 0.0, vec![]);
state.shutdown_requested = true;
let (should_stop, _) = rule_set.evaluate(&state);
assert!(should_stop);
}
#[test]
fn rule_set_returns_all_results_regardless_of_mode() {
let rule_set = StoppingRuleSet {
rules: vec![
StoppingRule::IterationLimit { limit: 10 },
StoppingRule::TimeLimit { seconds: 3600.0 },
StoppingRule::GracefulShutdown,
],
mode: StoppingMode::Any,
};
let state = make_state(10, 100.0, 0.0, vec![]);
let (_, results) = rule_set.evaluate(&state);
assert_eq!(results.len(), 3);
}
#[test]
fn ac_iteration_limit_triggered_at_10() {
let rule = StoppingRule::IterationLimit { limit: 10 };
let state = make_state(10, 0.0, 0.0, vec![]);
let result = rule.evaluate(&state);
assert!(result.triggered);
assert_eq!(result.rule_name, "iteration_limit");
}
#[test]
fn ac_bound_stalling_with_6_history_entries() {
let rule = StoppingRule::BoundStalling {
tolerance: 0.01,
iterations: 5,
};
let history = vec![80.0, 99.1, 99.4, 99.7, 99.9, 100.0];
let state = make_state(6, 0.0, 100.0, history);
let result = rule.evaluate(&state);
assert!(result.triggered);
}
#[test]
fn ac_rule_set_any_mode_stops_at_iteration_100() {
let rule_set = StoppingRuleSet {
rules: vec![
StoppingRule::IterationLimit { limit: 100 },
StoppingRule::TimeLimit { seconds: 3600.0 },
],
mode: StoppingMode::Any,
};
let state = make_state(100, 1000.0, 0.0, vec![]);
let (should_stop, _) = rule_set.evaluate(&state);
assert!(should_stop);
}
}