solverforge_solver/phase/localsearch/acceptor/
move_tabu.rs1use std::fmt::Debug;
4
5use solverforge_core::domain::PlanningSolution;
6use solverforge_core::Score;
7
8use super::Acceptor;
9
10pub struct MoveTabuAcceptor {
28 move_tabu_size: usize,
30 move_tabu_list: Vec<u64>,
32 current_step_move: Option<u64>,
34 aspiration_enabled: bool,
36 best_score: Option<i64>,
38}
39
40impl Debug for MoveTabuAcceptor {
41 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
42 f.debug_struct("MoveTabuAcceptor")
43 .field("move_tabu_size", &self.move_tabu_size)
44 .field("tabu_list_len", &self.move_tabu_list.len())
45 .field("aspiration_enabled", &self.aspiration_enabled)
46 .finish()
47 }
48}
49
50impl Clone for MoveTabuAcceptor {
51 fn clone(&self) -> Self {
52 Self {
53 move_tabu_size: self.move_tabu_size,
54 move_tabu_list: self.move_tabu_list.clone(),
55 current_step_move: self.current_step_move,
56 aspiration_enabled: self.aspiration_enabled,
57 best_score: self.best_score,
58 }
59 }
60}
61
62impl MoveTabuAcceptor {
63 pub fn new(move_tabu_size: usize) -> Self {
72 assert!(move_tabu_size > 0, "move_tabu_size must be > 0, got 0");
73 Self {
74 move_tabu_size,
75 move_tabu_list: Vec::with_capacity(move_tabu_size),
76 current_step_move: None,
77 aspiration_enabled: true,
78 best_score: None,
79 }
80 }
81
82 pub fn without_aspiration(move_tabu_size: usize) -> Self {
91 assert!(move_tabu_size > 0, "move_tabu_size must be > 0, got 0");
92 Self {
93 move_tabu_size,
94 move_tabu_list: Vec::with_capacity(move_tabu_size),
95 current_step_move: None,
96 aspiration_enabled: false,
97 best_score: None,
98 }
99 }
100
101 pub fn record_move(&mut self, move_hash: u64) {
105 self.current_step_move = Some(move_hash);
106 }
107
108 pub fn is_move_tabu(&self, move_hash: u64) -> bool {
109 self.move_tabu_list.contains(&move_hash)
110 }
111
112 pub fn aspiration_enabled(&self) -> bool {
113 self.aspiration_enabled
114 }
115
116 fn score_to_i64<S: PlanningSolution>(score: &S::Score) -> i64 {
117 let levels = score.to_level_numbers();
118 *levels.last().unwrap_or(&0)
120 }
121}
122
123impl Default for MoveTabuAcceptor {
124 fn default() -> Self {
125 Self::new(7)
126 }
127}
128
129impl<S: PlanningSolution> Acceptor<S> for MoveTabuAcceptor {
130 fn is_accepted(&mut self, last_step_score: &S::Score, move_score: &S::Score) -> bool {
131 if self.aspiration_enabled {
133 if let Some(best) = self.best_score {
134 let move_value = Self::score_to_i64::<S>(move_score);
135 if move_value > best {
136 return true; }
138 }
139 }
140
141 if move_score > last_step_score {
143 return true;
144 }
145
146 if move_score >= last_step_score {
148 return true;
149 }
150
151 false
152 }
153
154 fn phase_started(&mut self, initial_score: &S::Score) {
155 self.move_tabu_list.clear();
156 self.current_step_move = None;
157 self.best_score = Some(Self::score_to_i64::<S>(initial_score));
158 }
159
160 fn phase_ended(&mut self) {
161 self.move_tabu_list.clear();
162 }
163
164 fn step_started(&mut self) {
165 self.current_step_move = None;
166 }
167
168 fn step_ended(&mut self, step_score: &S::Score) {
169 if let Some(move_hash) = self.current_step_move {
171 if self.move_tabu_list.len() >= self.move_tabu_size {
172 self.move_tabu_list.remove(0);
173 }
174 self.move_tabu_list.push(move_hash);
175 }
176 self.current_step_move = None;
177
178 let step_value = Self::score_to_i64::<S>(step_score);
180 if let Some(best) = self.best_score {
181 if step_value > best {
182 self.best_score = Some(step_value);
183 }
184 } else {
185 self.best_score = Some(step_value);
186 }
187 }
188}
189
190#[cfg(test)]
191mod tests {
192 use super::*;
193 use solverforge_core::score::SoftScore;
194
195 #[derive(Clone)]
196 struct TestSolution;
197 impl PlanningSolution for TestSolution {
198 type Score = SoftScore;
199 fn score(&self) -> Option<Self::Score> {
200 None
201 }
202 fn set_score(&mut self, _: Option<Self::Score>) {}
203 }
204
205 #[test]
206 fn test_new_acceptor() {
207 let acceptor = MoveTabuAcceptor::new(5);
208 assert!(!acceptor.is_move_tabu(42));
209 assert!(acceptor.aspiration_enabled());
210 }
211
212 #[test]
213 fn test_without_aspiration() {
214 let acceptor = MoveTabuAcceptor::without_aspiration(5);
215 assert!(!acceptor.aspiration_enabled());
216 }
217
218 #[test]
219 fn test_record_and_check() {
220 let mut acceptor = MoveTabuAcceptor::new(5);
221 Acceptor::<TestSolution>::phase_started(&mut acceptor, &SoftScore::of(0));
222
223 acceptor.record_move(42);
224 Acceptor::<TestSolution>::step_ended(&mut acceptor, &SoftScore::of(0));
225
226 assert!(acceptor.is_move_tabu(42));
227 assert!(!acceptor.is_move_tabu(99));
228 }
229
230 #[test]
231 fn test_tabu_expiration() {
232 let mut acceptor = MoveTabuAcceptor::new(2);
233 Acceptor::<TestSolution>::phase_started(&mut acceptor, &SoftScore::of(0));
234
235 acceptor.record_move(1);
237 Acceptor::<TestSolution>::step_ended(&mut acceptor, &SoftScore::of(0));
238 assert!(acceptor.is_move_tabu(1));
239
240 Acceptor::<TestSolution>::step_started(&mut acceptor);
242 acceptor.record_move(2);
243 Acceptor::<TestSolution>::step_ended(&mut acceptor, &SoftScore::of(0));
244 assert!(acceptor.is_move_tabu(1));
245 assert!(acceptor.is_move_tabu(2));
246
247 Acceptor::<TestSolution>::step_started(&mut acceptor);
249 acceptor.record_move(3);
250 Acceptor::<TestSolution>::step_ended(&mut acceptor, &SoftScore::of(0));
251
252 assert!(!acceptor.is_move_tabu(1)); assert!(acceptor.is_move_tabu(2));
254 assert!(acceptor.is_move_tabu(3));
255 }
256
257 #[test]
258 fn test_accepts_improving_move() {
259 let mut acceptor = MoveTabuAcceptor::new(5);
260 let last_score = SoftScore::of(-10);
261 let move_score = SoftScore::of(-5);
262 assert!(Acceptor::<TestSolution>::is_accepted(
263 &mut acceptor,
264 &last_score,
265 &move_score
266 ));
267 }
268
269 #[test]
270 fn test_phase_clears_tabu() {
271 let mut acceptor = MoveTabuAcceptor::new(5);
272 Acceptor::<TestSolution>::phase_started(&mut acceptor, &SoftScore::of(0));
273
274 acceptor.record_move(42);
275 Acceptor::<TestSolution>::step_ended(&mut acceptor, &SoftScore::of(0));
276 assert!(acceptor.is_move_tabu(42));
277
278 Acceptor::<TestSolution>::phase_ended(&mut acceptor);
279 assert!(!acceptor.is_move_tabu(42));
280 }
281
282 #[test]
283 fn test_no_move_recorded_no_tabu() {
284 let mut acceptor = MoveTabuAcceptor::new(5);
285 Acceptor::<TestSolution>::phase_started(&mut acceptor, &SoftScore::of(0));
286
287 Acceptor::<TestSolution>::step_ended(&mut acceptor, &SoftScore::of(0));
289
290 assert!(!acceptor.is_move_tabu(42));
292 }
293}