Skip to main content

solverforge_solver/termination/
unimproved.rs

1// Termination conditions based on lack of improvement.
2
3use std::fmt::Debug;
4use std::marker::PhantomData;
5use std::time::Duration;
6
7use solverforge_core::domain::PlanningSolution;
8use solverforge_core::score::Score;
9use solverforge_scoring::Director;
10
11use super::Termination;
12use crate::scope::ProgressCallback;
13use crate::scope::SolverScope;
14
15/// Terminates if no improvement occurs for a specified number of steps.
16///
17/// This is useful to avoid spending too much time when the solver has
18/// plateaued and is unlikely to find better solutions.
19///
20/// # Example
21///
22/// ```
23/// use solverforge_solver::termination::UnimprovedStepCountTermination;
24/// use solverforge_core::score::SoftScore;
25/// use solverforge_core::domain::PlanningSolution;
26///
27/// #[derive(Clone)]
28/// struct MySolution;
29/// impl PlanningSolution for MySolution {
30///     type Score = SoftScore;
31///     fn score(&self) -> Option<Self::Score> { None }
32///     fn set_score(&mut self, _: Option<Self::Score>) {}
33/// }
34///
35/// // Terminate after 100 steps without improvement
36/// let term = UnimprovedStepCountTermination::<MySolution>::new(100);
37/// ```
38pub struct UnimprovedStepCountTermination<S: PlanningSolution> {
39    limit: u64,
40    state: UnimprovedState<S::Score>,
41    _phantom: PhantomData<fn() -> S>,
42}
43
44impl<S: PlanningSolution> Debug for UnimprovedStepCountTermination<S> {
45    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
46        f.debug_struct("UnimprovedStepCountTermination")
47            .field("limit", &self.limit)
48            .field(
49                "steps_since_improvement",
50                &self.state.steps_since_improvement,
51            )
52            .finish()
53    }
54}
55
56#[derive(Clone)]
57struct UnimprovedState<Sc: Score> {
58    last_best_score: Option<Sc>,
59    steps_since_improvement: u64,
60    last_checked_step: Option<u64>,
61}
62
63impl<Sc: Score> Default for UnimprovedState<Sc> {
64    fn default() -> Self {
65        Self {
66            last_best_score: None,
67            steps_since_improvement: 0,
68            last_checked_step: None,
69        }
70    }
71}
72
73impl<S: PlanningSolution> UnimprovedStepCountTermination<S> {
74    pub fn new(limit: u64) -> Self {
75        Self {
76            limit,
77            state: UnimprovedState::default(),
78            _phantom: PhantomData,
79        }
80    }
81}
82
83impl<S: PlanningSolution, D: Director<S>, BestCb: ProgressCallback<S>> Termination<S, D, BestCb>
84    for UnimprovedStepCountTermination<S>
85{
86    fn is_terminated(&mut self, solver_scope: &SolverScope<S, D, BestCb>) -> bool {
87        let state = &mut self.state;
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);
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);
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::SoftScore;
138/// use solverforge_core::domain::PlanningSolution;
139///
140/// #[derive(Clone)]
141/// struct MySolution;
142/// impl PlanningSolution for MySolution {
143///     type Score = SoftScore;
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: 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_elapsed: Option<Duration>,
168}
169
170impl<Sc: Score> Default for UnimprovedTimeState<Sc> {
171    fn default() -> Self {
172        Self {
173            last_best_score: None,
174            last_improvement_elapsed: None,
175        }
176    }
177}
178
179impl<S: PlanningSolution> UnimprovedTimeTermination<S> {
180    pub fn new(limit: Duration) -> Self {
181        Self {
182            limit,
183            state: UnimprovedTimeState::default(),
184            _phantom: PhantomData,
185        }
186    }
187
188    pub fn millis(ms: u64) -> Self {
189        Self::new(Duration::from_millis(ms))
190    }
191
192    pub fn seconds(secs: u64) -> Self {
193        Self::new(Duration::from_secs(secs))
194    }
195}
196
197impl<S: PlanningSolution, D: Director<S>, BestCb: ProgressCallback<S>> Termination<S, D, BestCb>
198    for UnimprovedTimeTermination<S>
199{
200    fn is_terminated(&mut self, solver_scope: &SolverScope<S, D, BestCb>) -> bool {
201        let state = &mut self.state;
202        let current_best = solver_scope.best_score();
203        let now = solver_scope.elapsed().unwrap_or_default();
204
205        match (&state.last_best_score, current_best) {
206            (None, Some(score)) => {
207                // First score recorded
208                state.last_best_score = Some(*score);
209                state.last_improvement_elapsed = Some(now);
210                false
211            }
212            (Some(last), Some(current)) => {
213                if *current > *last {
214                    // Improvement found
215                    state.last_best_score = Some(*current);
216                    state.last_improvement_elapsed = Some(now);
217                    false
218                } else {
219                    // No improvement - check time
220                    state
221                        .last_improvement_elapsed
222                        .map(|t| now.saturating_sub(t) >= self.limit)
223                        .unwrap_or(false)
224                }
225            }
226            (Some(_), None) => {
227                // Score became unavailable
228                state
229                    .last_improvement_elapsed
230                    .map(|t| now.saturating_sub(t) >= self.limit)
231                    .unwrap_or(false)
232            }
233            (None, None) => {
234                // No score yet, don't terminate
235                false
236            }
237        }
238    }
239}