solverforge_solver/termination/
unimproved.rs

1//! Termination conditions based on lack of improvement.
2
3use std::cell::RefCell;
4use std::fmt::Debug;
5use std::marker::PhantomData;
6use std::time::{Duration, Instant};
7
8use solverforge_core::domain::PlanningSolution;
9use solverforge_core::score::Score;
10
11use super::Termination;
12use crate::scope::SolverScope;
13
14/// Terminates if no improvement occurs for a specified number of steps.
15///
16/// This is useful to avoid spending too much time when the solver has
17/// plateaued and is unlikely to find better solutions.
18///
19/// # Example
20///
21/// ```
22/// use solverforge_solver::termination::UnimprovedStepCountTermination;
23/// use solverforge_core::score::SimpleScore;
24/// use solverforge_core::domain::PlanningSolution;
25///
26/// #[derive(Clone)]
27/// struct MySolution;
28/// impl PlanningSolution for MySolution {
29///     type Score = SimpleScore;
30///     fn score(&self) -> Option<Self::Score> { None }
31///     fn set_score(&mut self, _: Option<Self::Score>) {}
32/// }
33///
34/// // Terminate after 100 steps without improvement
35/// let term = UnimprovedStepCountTermination::<MySolution>::new(100);
36/// ```
37pub struct UnimprovedStepCountTermination<S: PlanningSolution> {
38    limit: u64,
39    state: RefCell<UnimprovedState<S::Score>>,
40    _phantom: PhantomData<fn() -> S>,
41}
42
43impl<S: PlanningSolution> Debug for UnimprovedStepCountTermination<S> {
44    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
45        let state = self.state.borrow();
46        f.debug_struct("UnimprovedStepCountTermination")
47            .field("limit", &self.limit)
48            .field("steps_since_improvement", &state.steps_since_improvement)
49            .finish()
50    }
51}
52
53#[derive(Clone)]
54struct UnimprovedState<Sc: Score> {
55    last_best_score: Option<Sc>,
56    steps_since_improvement: u64,
57    last_checked_step: Option<u64>,
58}
59
60impl<Sc: Score> Default for UnimprovedState<Sc> {
61    fn default() -> Self {
62        Self {
63            last_best_score: None,
64            steps_since_improvement: 0,
65            last_checked_step: None,
66        }
67    }
68}
69
70impl<S: PlanningSolution> UnimprovedStepCountTermination<S> {
71    /// Creates a termination that stops after `limit` steps without improvement.
72    pub fn new(limit: u64) -> Self {
73        Self {
74            limit,
75            state: RefCell::new(UnimprovedState::default()),
76            _phantom: PhantomData,
77        }
78    }
79}
80
81// Safety: The RefCell is only accessed from within is_terminated,
82// which is called from a single thread during solving.
83unsafe impl<S: PlanningSolution> Send for UnimprovedStepCountTermination<S> {}
84
85impl<S: PlanningSolution> Termination<S> for UnimprovedStepCountTermination<S> {
86    fn is_terminated(&self, solver_scope: &SolverScope<S>) -> bool {
87        let mut state = self.state.borrow_mut();
88        let current_step = solver_scope.total_step_count();
89
90        // Avoid rechecking on the same step
91        if state.last_checked_step == Some(current_step) {
92            return state.steps_since_improvement >= self.limit;
93        }
94        state.last_checked_step = Some(current_step);
95
96        let current_best = solver_scope.best_score();
97
98        match (&state.last_best_score, current_best) {
99            (None, Some(score)) => {
100                // First score recorded
101                state.last_best_score = Some(score.clone());
102                state.steps_since_improvement = 0;
103            }
104            (Some(last), Some(current)) => {
105                if *current > *last {
106                    // Improvement found
107                    state.last_best_score = Some(current.clone());
108                    state.steps_since_improvement = 0;
109                } else {
110                    // No improvement
111                    state.steps_since_improvement += 1;
112                }
113            }
114            (Some(_), None) => {
115                // Score became unavailable (shouldn't happen normally)
116                state.steps_since_improvement += 1;
117            }
118            (None, None) => {
119                // No score yet
120            }
121        }
122
123        state.steps_since_improvement >= self.limit
124    }
125}
126
127/// Terminates if no improvement occurs for a specified duration.
128///
129/// This is useful for time-boxed optimization where you want to ensure
130/// progress is being made, but also allow more time if improvements are found.
131///
132/// # Example
133///
134/// ```
135/// use std::time::Duration;
136/// use solverforge_solver::termination::UnimprovedTimeTermination;
137/// use solverforge_core::score::SimpleScore;
138/// use solverforge_core::domain::PlanningSolution;
139///
140/// #[derive(Clone)]
141/// struct MySolution;
142/// impl PlanningSolution for MySolution {
143///     type Score = SimpleScore;
144///     fn score(&self) -> Option<Self::Score> { None }
145///     fn set_score(&mut self, _: Option<Self::Score>) {}
146/// }
147///
148/// // Terminate after 5 seconds without improvement
149/// let term = UnimprovedTimeTermination::<MySolution>::seconds(5);
150/// ```
151pub struct UnimprovedTimeTermination<S: PlanningSolution> {
152    limit: Duration,
153    state: RefCell<UnimprovedTimeState<S::Score>>,
154    _phantom: PhantomData<fn() -> S>,
155}
156
157impl<S: PlanningSolution> Debug for UnimprovedTimeTermination<S> {
158    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
159        f.debug_struct("UnimprovedTimeTermination")
160            .field("limit", &self.limit)
161            .finish()
162    }
163}
164
165struct UnimprovedTimeState<Sc: Score> {
166    last_best_score: Option<Sc>,
167    last_improvement_time: Option<Instant>,
168}
169
170impl<Sc: Score> Default for UnimprovedTimeState<Sc> {
171    fn default() -> Self {
172        Self {
173            last_best_score: None,
174            last_improvement_time: None,
175        }
176    }
177}
178
179impl<S: PlanningSolution> UnimprovedTimeTermination<S> {
180    /// Creates a termination that stops after `limit` time without improvement.
181    pub fn new(limit: Duration) -> Self {
182        Self {
183            limit,
184            state: RefCell::new(UnimprovedTimeState::default()),
185            _phantom: PhantomData,
186        }
187    }
188
189    /// Creates a termination with limit in milliseconds.
190    pub fn millis(ms: u64) -> Self {
191        Self::new(Duration::from_millis(ms))
192    }
193
194    /// Creates a termination with limit in seconds.
195    pub fn seconds(secs: u64) -> Self {
196        Self::new(Duration::from_secs(secs))
197    }
198}
199
200// Safety: The RefCell is only accessed from within is_terminated,
201// which is called from a single thread during solving.
202unsafe impl<S: PlanningSolution> Send for UnimprovedTimeTermination<S> {}
203
204impl<S: PlanningSolution> Termination<S> for UnimprovedTimeTermination<S> {
205    fn is_terminated(&self, solver_scope: &SolverScope<S>) -> bool {
206        let mut state = self.state.borrow_mut();
207        let current_best = solver_scope.best_score();
208        let now = Instant::now();
209
210        match (&state.last_best_score, current_best) {
211            (None, Some(score)) => {
212                // First score recorded
213                state.last_best_score = Some(score.clone());
214                state.last_improvement_time = Some(now);
215                false
216            }
217            (Some(last), Some(current)) => {
218                if *current > *last {
219                    // Improvement found
220                    state.last_best_score = Some(current.clone());
221                    state.last_improvement_time = Some(now);
222                    false
223                } else {
224                    // No improvement - check time
225                    state
226                        .last_improvement_time
227                        .map(|t| now.duration_since(t) >= self.limit)
228                        .unwrap_or(false)
229                }
230            }
231            (Some(_), None) => {
232                // Score became unavailable
233                state
234                    .last_improvement_time
235                    .map(|t| now.duration_since(t) >= self.limit)
236                    .unwrap_or(false)
237            }
238            (None, None) => {
239                // No score yet, don't terminate
240                false
241            }
242        }
243    }
244}