solverforge_solver/phase/localsearch/acceptor/
step_counting.rs

1//! Step Counting Hill Climbing acceptor.
2
3use std::fmt::Debug;
4
5use solverforge_core::domain::PlanningSolution;
6
7use super::Acceptor;
8
9/// Step Counting Hill Climbing acceptor - allows limited non-improving moves.
10///
11/// This acceptor combines hill climbing with a step limit that resets whenever
12/// an improving move is made. It accepts:
13/// 1. Any improving move (resets the step counter)
14/// 2. Non-improving moves if step count since last improvement is below threshold
15///
16/// This enables exploration while still requiring eventual improvement.
17///
18/// # Example
19///
20/// ```
21/// use solverforge_solver::phase::localsearch::StepCountingHillClimbingAcceptor;
22/// use solverforge_core::score::SimpleScore;
23/// use solverforge_core::domain::PlanningSolution;
24///
25/// #[derive(Clone)]
26/// struct MySolution;
27/// impl PlanningSolution for MySolution {
28///     type Score = SimpleScore;
29///     fn score(&self) -> Option<Self::Score> { None }
30///     fn set_score(&mut self, _: Option<Self::Score>) {}
31/// }
32///
33/// // Allow up to 100 non-improving steps before requiring improvement
34/// let acceptor = StepCountingHillClimbingAcceptor::<MySolution>::new(100);
35/// ```
36pub struct StepCountingHillClimbingAcceptor<S: PlanningSolution> {
37    /// Maximum steps allowed without improvement.
38    step_count_limit: u64,
39    /// Current steps since last improvement.
40    steps_since_improvement: u64,
41    /// Best score seen so far.
42    best_score: Option<S::Score>,
43}
44
45impl<S: PlanningSolution> Debug for StepCountingHillClimbingAcceptor<S> {
46    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
47        f.debug_struct("StepCountingHillClimbingAcceptor")
48            .field("step_count_limit", &self.step_count_limit)
49            .field("steps_since_improvement", &self.steps_since_improvement)
50            .finish()
51    }
52}
53
54impl<S: PlanningSolution> Clone for StepCountingHillClimbingAcceptor<S> {
55    fn clone(&self) -> Self {
56        Self {
57            step_count_limit: self.step_count_limit,
58            steps_since_improvement: self.steps_since_improvement,
59            best_score: self.best_score,
60        }
61    }
62}
63
64impl<S: PlanningSolution> StepCountingHillClimbingAcceptor<S> {
65    /// Creates a new Step Counting Hill Climbing acceptor.
66    ///
67    /// # Arguments
68    /// * `step_count_limit` - Maximum steps allowed without finding improvement
69    pub fn new(step_count_limit: u64) -> Self {
70        Self {
71            step_count_limit,
72            steps_since_improvement: 0,
73            best_score: None,
74        }
75    }
76}
77
78impl<S: PlanningSolution> Default for StepCountingHillClimbingAcceptor<S> {
79    fn default() -> Self {
80        Self::new(100)
81    }
82}
83
84impl<S: PlanningSolution> Acceptor<S> for StepCountingHillClimbingAcceptor<S> {
85    fn is_accepted(&self, last_step_score: &S::Score, move_score: &S::Score) -> bool {
86        // Always accept improving moves
87        if move_score > last_step_score {
88            return true;
89        }
90
91        // Accept non-improving moves if within step count limit
92        self.steps_since_improvement < self.step_count_limit
93    }
94
95    fn phase_started(&mut self, initial_score: &S::Score) {
96        self.best_score = Some(*initial_score);
97        self.steps_since_improvement = 0;
98    }
99
100    fn step_ended(&mut self, step_score: &S::Score) {
101        // Check if this step improved on the best score
102        let improved = match &self.best_score {
103            Some(best) => step_score > best,
104            None => true,
105        };
106
107        if improved {
108            self.best_score = Some(*step_score);
109            self.steps_since_improvement = 0;
110        } else {
111            self.steps_since_improvement += 1;
112        }
113    }
114
115    fn phase_ended(&mut self) {
116        self.best_score = None;
117        self.steps_since_improvement = 0;
118    }
119}
120
121#[cfg(test)]
122mod tests {
123    use super::*;
124    use solverforge_core::score::SimpleScore;
125
126    #[derive(Clone)]
127    struct TestSolution {
128        score: Option<SimpleScore>,
129    }
130
131    impl PlanningSolution for TestSolution {
132        type Score = SimpleScore;
133        fn score(&self) -> Option<Self::Score> {
134            self.score
135        }
136        fn set_score(&mut self, score: Option<Self::Score>) {
137            self.score = score;
138        }
139    }
140
141    #[test]
142    fn test_accepts_improving_moves() {
143        let mut acceptor = StepCountingHillClimbingAcceptor::<TestSolution>::new(5);
144        acceptor.phase_started(&SimpleScore::of(-100));
145
146        // Improving move: -100 -> -50
147        assert!(acceptor.is_accepted(&SimpleScore::of(-100), &SimpleScore::of(-50)));
148    }
149
150    #[test]
151    fn test_accepts_non_improving_within_limit() {
152        let mut acceptor = StepCountingHillClimbingAcceptor::<TestSolution>::new(5);
153        acceptor.phase_started(&SimpleScore::of(-100));
154
155        // Non-improving move accepted because step count < limit
156        assert!(acceptor.is_accepted(&SimpleScore::of(-100), &SimpleScore::of(-110)));
157    }
158
159    #[test]
160    fn test_rejects_after_limit_exceeded() {
161        let mut acceptor = StepCountingHillClimbingAcceptor::<TestSolution>::new(3);
162        acceptor.phase_started(&SimpleScore::of(-100));
163
164        // Accept non-improving moves until limit is reached
165        assert!(acceptor.is_accepted(&SimpleScore::of(-100), &SimpleScore::of(-110))); // step 0
166        acceptor.step_ended(&SimpleScore::of(-110));
167
168        assert!(acceptor.is_accepted(&SimpleScore::of(-110), &SimpleScore::of(-120))); // step 1
169        acceptor.step_ended(&SimpleScore::of(-120));
170
171        assert!(acceptor.is_accepted(&SimpleScore::of(-120), &SimpleScore::of(-130))); // step 2
172        acceptor.step_ended(&SimpleScore::of(-130));
173
174        // Now at step 3, should reject non-improving
175        assert!(!acceptor.is_accepted(&SimpleScore::of(-130), &SimpleScore::of(-140)));
176        // step 3
177    }
178
179    #[test]
180    fn test_resets_on_improvement() {
181        let mut acceptor = StepCountingHillClimbingAcceptor::<TestSolution>::new(3);
182        acceptor.phase_started(&SimpleScore::of(-100));
183
184        // Two non-improving steps
185        acceptor.step_ended(&SimpleScore::of(-110));
186        acceptor.step_ended(&SimpleScore::of(-120));
187        assert_eq!(acceptor.steps_since_improvement, 2);
188
189        // Now an improving step (better than -100 initial)
190        acceptor.step_ended(&SimpleScore::of(-50));
191        assert_eq!(acceptor.steps_since_improvement, 0);
192
193        // Can take more non-improving steps again
194        acceptor.step_ended(&SimpleScore::of(-60));
195        acceptor.step_ended(&SimpleScore::of(-70));
196        assert!(acceptor.is_accepted(&SimpleScore::of(-70), &SimpleScore::of(-80)));
197        // still within limit
198    }
199
200    #[test]
201    fn test_improving_always_accepted_even_after_limit() {
202        let mut acceptor = StepCountingHillClimbingAcceptor::<TestSolution>::new(2);
203        acceptor.phase_started(&SimpleScore::of(-100));
204
205        // Exhaust the limit
206        acceptor.step_ended(&SimpleScore::of(-110));
207        acceptor.step_ended(&SimpleScore::of(-120));
208
209        // Non-improving should be rejected
210        assert!(!acceptor.is_accepted(&SimpleScore::of(-120), &SimpleScore::of(-130)));
211
212        // But improving should still be accepted
213        assert!(acceptor.is_accepted(&SimpleScore::of(-120), &SimpleScore::of(-50)));
214    }
215
216    #[test]
217    fn test_phase_reset() {
218        let mut acceptor = StepCountingHillClimbingAcceptor::<TestSolution>::new(3);
219        acceptor.phase_started(&SimpleScore::of(-100));
220        acceptor.step_ended(&SimpleScore::of(-110));
221        acceptor.step_ended(&SimpleScore::of(-120));
222        acceptor.phase_ended();
223
224        // After reset, counter should be back to 0
225        acceptor.phase_started(&SimpleScore::of(-200));
226        assert_eq!(acceptor.steps_since_improvement, 0);
227    }
228}