Skip to main content

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