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::Director;
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::SoftScore;
26/// use solverforge_core::domain::PlanningSolution;
27///
28/// #[derive(Clone)]
29/// struct MySolution;
30/// impl PlanningSolution for MySolution {
31///     type Score = SoftScore;
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    pub fn new(limit: u64) -> Self {
74        Self {
75            limit,
76            state: RefCell::new(UnimprovedState::default()),
77            _phantom: PhantomData,
78        }
79    }
80}
81
82// Safety: The RefCell is only accessed from within is_terminated,
83// which is called from a single thread during solving.
84unsafe impl<S: PlanningSolution> Send for UnimprovedStepCountTermination<S> {}
85
86impl<S: PlanningSolution, D: Director<S>, BestCb: BestSolutionCallback<S>> Termination<S, D, BestCb>
87    for UnimprovedStepCountTermination<S>
88{
89    fn is_terminated(&self, solver_scope: &SolverScope<S, D, BestCb>) -> bool {
90        let mut state = self.state.borrow_mut();
91        let current_step = solver_scope.total_step_count();
92
93        // Avoid rechecking on the same step
94        if state.last_checked_step == Some(current_step) {
95            return state.steps_since_improvement >= self.limit;
96        }
97        state.last_checked_step = Some(current_step);
98
99        let current_best = solver_scope.best_score();
100
101        match (&state.last_best_score, current_best) {
102            (None, Some(score)) => {
103                // First score recorded
104                state.last_best_score = Some(*score);
105                state.steps_since_improvement = 0;
106            }
107            (Some(last), Some(current)) => {
108                if *current > *last {
109                    // Improvement found
110                    state.last_best_score = Some(*current);
111                    state.steps_since_improvement = 0;
112                } else {
113                    // No improvement
114                    state.steps_since_improvement += 1;
115                }
116            }
117            (Some(_), None) => {
118                // Score became unavailable (shouldn't happen normally)
119                state.steps_since_improvement += 1;
120            }
121            (None, None) => {
122                // No score yet
123            }
124        }
125
126        state.steps_since_improvement >= self.limit
127    }
128}
129
130/// Terminates if no improvement occurs for a specified duration.
131///
132/// This is useful for time-boxed optimization where you want to ensure
133/// progress is being made, but also allow more time if improvements are found.
134///
135/// # Example
136///
137/// ```
138/// use std::time::Duration;
139/// use solverforge_solver::termination::UnimprovedTimeTermination;
140/// use solverforge_core::score::SoftScore;
141/// use solverforge_core::domain::PlanningSolution;
142///
143/// #[derive(Clone)]
144/// struct MySolution;
145/// impl PlanningSolution for MySolution {
146///     type Score = SoftScore;
147///     fn score(&self) -> Option<Self::Score> { None }
148///     fn set_score(&mut self, _: Option<Self::Score>) {}
149/// }
150///
151/// // Terminate after 5 seconds without improvement
152/// let term = UnimprovedTimeTermination::<MySolution>::seconds(5);
153/// ```
154pub struct UnimprovedTimeTermination<S: PlanningSolution> {
155    limit: Duration,
156    state: RefCell<UnimprovedTimeState<S::Score>>,
157    _phantom: PhantomData<fn() -> S>,
158}
159
160impl<S: PlanningSolution> Debug for UnimprovedTimeTermination<S> {
161    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
162        f.debug_struct("UnimprovedTimeTermination")
163            .field("limit", &self.limit)
164            .finish()
165    }
166}
167
168struct UnimprovedTimeState<Sc: Score> {
169    last_best_score: Option<Sc>,
170    last_improvement_time: Option<Instant>,
171}
172
173impl<Sc: Score> Default for UnimprovedTimeState<Sc> {
174    fn default() -> Self {
175        Self {
176            last_best_score: None,
177            last_improvement_time: None,
178        }
179    }
180}
181
182impl<S: PlanningSolution> UnimprovedTimeTermination<S> {
183    pub fn new(limit: Duration) -> Self {
184        Self {
185            limit,
186            state: RefCell::new(UnimprovedTimeState::default()),
187            _phantom: PhantomData,
188        }
189    }
190
191    pub fn millis(ms: u64) -> Self {
192        Self::new(Duration::from_millis(ms))
193    }
194
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, D: Director<S>, BestCb: BestSolutionCallback<S>> Termination<S, D, BestCb>
205    for UnimprovedTimeTermination<S>
206{
207    fn is_terminated(&self, solver_scope: &SolverScope<S, D, BestCb>) -> bool {
208        let mut state = self.state.borrow_mut();
209        let current_best = solver_scope.best_score();
210        let now = Instant::now();
211
212        match (&state.last_best_score, current_best) {
213            (None, Some(score)) => {
214                // First score recorded
215                state.last_best_score = Some(*score);
216                state.last_improvement_time = Some(now);
217                false
218            }
219            (Some(last), Some(current)) => {
220                if *current > *last {
221                    // Improvement found
222                    state.last_best_score = Some(*current);
223                    state.last_improvement_time = Some(now);
224                    false
225                } else {
226                    // No improvement - check time
227                    state
228                        .last_improvement_time
229                        .map(|t| now.duration_since(t) >= self.limit)
230                        .unwrap_or(false)
231                }
232            }
233            (Some(_), None) => {
234                // Score became unavailable
235                state
236                    .last_improvement_time
237                    .map(|t| now.duration_since(t) >= self.limit)
238                    .unwrap_or(false)
239            }
240            (None, None) => {
241                // No score yet, don't terminate
242                false
243            }
244        }
245    }
246}