solverforge_solver/phase/localsearch/
forager.rs1use std::fmt::Debug;
7use std::marker::PhantomData;
8
9use solverforge_core::domain::PlanningSolution;
10
11use crate::heuristic::r#move::Move;
12
13pub trait LocalSearchForager<S, M>: Send + Debug
24where
25 S: PlanningSolution,
26 M: Move<S>,
27{
28 fn step_started(&mut self);
30
31 fn add_move_index(&mut self, index: usize, score: S::Score);
35
36 fn is_quit_early(&self) -> bool;
39
40 fn pick_move_index(&mut self) -> Option<(usize, S::Score)>;
44}
45
46pub struct AcceptedCountForager<S>
51where
52 S: PlanningSolution,
53{
54 accepted_count_limit: usize,
56 accepted_moves: Vec<(usize, S::Score)>,
58 _phantom: PhantomData<fn() -> S>,
59}
60
61impl<S> AcceptedCountForager<S>
62where
63 S: PlanningSolution,
64{
65 pub fn new(accepted_count_limit: usize) -> Self {
70 Self {
71 accepted_count_limit,
72 accepted_moves: Vec::new(),
73 _phantom: PhantomData,
74 }
75 }
76}
77
78impl<S> Clone for AcceptedCountForager<S>
79where
80 S: PlanningSolution,
81{
82 fn clone(&self) -> Self {
83 Self {
84 accepted_count_limit: self.accepted_count_limit,
85 accepted_moves: Vec::new(), _phantom: PhantomData,
87 }
88 }
89}
90
91impl<S> Debug for AcceptedCountForager<S>
92where
93 S: PlanningSolution,
94{
95 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
96 f.debug_struct("AcceptedCountForager")
97 .field("accepted_count_limit", &self.accepted_count_limit)
98 .field("accepted_count", &self.accepted_moves.len())
99 .finish()
100 }
101}
102
103impl<S, M> LocalSearchForager<S, M> for AcceptedCountForager<S>
104where
105 S: PlanningSolution,
106 M: Move<S>,
107{
108 fn step_started(&mut self) {
109 self.accepted_moves.clear();
110 }
111
112 fn add_move_index(&mut self, index: usize, score: S::Score) {
113 self.accepted_moves.push((index, score));
114 }
115
116 fn is_quit_early(&self) -> bool {
117 self.accepted_moves.len() >= self.accepted_count_limit
118 }
119
120 fn pick_move_index(&mut self) -> Option<(usize, S::Score)> {
121 if self.accepted_moves.is_empty() {
122 return None;
123 }
124
125 let mut best_idx = 0;
127 let mut best_score = self.accepted_moves[0].1;
128
129 for (i, &(_, score)) in self.accepted_moves.iter().enumerate().skip(1) {
130 if score > best_score {
131 best_idx = i;
132 best_score = score;
133 }
134 }
135
136 Some(self.accepted_moves.swap_remove(best_idx))
138 }
139}
140
141pub struct FirstAcceptedForager<S>
145where
146 S: PlanningSolution,
147{
148 accepted_move: Option<(usize, S::Score)>,
150 _phantom: PhantomData<fn() -> S>,
151}
152
153impl<S> Debug for FirstAcceptedForager<S>
154where
155 S: PlanningSolution,
156{
157 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
158 f.debug_struct("FirstAcceptedForager")
159 .field("has_move", &self.accepted_move.is_some())
160 .finish()
161 }
162}
163
164impl<S> FirstAcceptedForager<S>
165where
166 S: PlanningSolution,
167{
168 pub fn new() -> Self {
170 Self {
171 accepted_move: None,
172 _phantom: PhantomData,
173 }
174 }
175}
176
177impl<S> Clone for FirstAcceptedForager<S>
178where
179 S: PlanningSolution,
180{
181 fn clone(&self) -> Self {
182 Self {
183 accepted_move: None, _phantom: PhantomData,
185 }
186 }
187}
188
189impl<S> Default for FirstAcceptedForager<S>
190where
191 S: PlanningSolution,
192{
193 fn default() -> Self {
194 Self::new()
195 }
196}
197
198impl<S, M> LocalSearchForager<S, M> for FirstAcceptedForager<S>
199where
200 S: PlanningSolution,
201 M: Move<S>,
202{
203 fn step_started(&mut self) {
204 self.accepted_move = None;
205 }
206
207 fn add_move_index(&mut self, index: usize, score: S::Score) {
208 if self.accepted_move.is_none() {
209 self.accepted_move = Some((index, score));
210 }
211 }
212
213 fn is_quit_early(&self) -> bool {
214 self.accepted_move.is_some()
215 }
216
217 fn pick_move_index(&mut self) -> Option<(usize, S::Score)> {
218 self.accepted_move.take()
219 }
220}
221
222#[cfg(test)]
223mod tests {
224 use super::*;
225 use crate::heuristic::r#move::ChangeMove;
226 use solverforge_core::score::SimpleScore;
227
228 #[derive(Clone, Debug)]
229 struct DummySolution {
230 score: Option<SimpleScore>,
231 }
232
233 impl PlanningSolution for DummySolution {
234 type Score = SimpleScore;
235
236 fn score(&self) -> Option<Self::Score> {
237 self.score
238 }
239
240 fn set_score(&mut self, score: Option<Self::Score>) {
241 self.score = score;
242 }
243 }
244
245 type TestMove = ChangeMove<DummySolution, i32>;
246
247 #[test]
248 fn test_accepted_count_forager_collects_indices() {
249 let mut forager = AcceptedCountForager::<DummySolution>::new(3);
250 <AcceptedCountForager<DummySolution> as LocalSearchForager<DummySolution, TestMove>>::step_started(&mut forager);
251
252 <AcceptedCountForager<DummySolution> as LocalSearchForager<DummySolution, TestMove>>::add_move_index(&mut forager, 0, SimpleScore::of(-10));
253 assert!(!<AcceptedCountForager<DummySolution> as LocalSearchForager<DummySolution, TestMove>>::is_quit_early(&forager));
254
255 <AcceptedCountForager<DummySolution> as LocalSearchForager<DummySolution, TestMove>>::add_move_index(&mut forager, 1, SimpleScore::of(-5));
256 assert!(!<AcceptedCountForager<DummySolution> as LocalSearchForager<DummySolution, TestMove>>::is_quit_early(&forager));
257
258 <AcceptedCountForager<DummySolution> as LocalSearchForager<DummySolution, TestMove>>::add_move_index(&mut forager, 2, SimpleScore::of(-8));
259 assert!(<AcceptedCountForager<DummySolution> as LocalSearchForager<DummySolution, TestMove>>::is_quit_early(&forager));
260 }
261
262 #[test]
263 fn test_accepted_count_forager_picks_best_index() {
264 let mut forager = AcceptedCountForager::<DummySolution>::new(10);
265 <AcceptedCountForager<DummySolution> as LocalSearchForager<DummySolution, TestMove>>::step_started(&mut forager);
266
267 <AcceptedCountForager<DummySolution> as LocalSearchForager<DummySolution, TestMove>>::add_move_index(&mut forager, 0, SimpleScore::of(-10));
268 <AcceptedCountForager<DummySolution> as LocalSearchForager<DummySolution, TestMove>>::add_move_index(&mut forager, 1, SimpleScore::of(-5)); <AcceptedCountForager<DummySolution> as LocalSearchForager<DummySolution, TestMove>>::add_move_index(&mut forager, 2, SimpleScore::of(-8));
270
271 let (index, score) = <AcceptedCountForager<DummySolution> as LocalSearchForager<
272 DummySolution,
273 TestMove,
274 >>::pick_move_index(&mut forager)
275 .unwrap();
276 assert_eq!(index, 1);
277 assert_eq!(score, SimpleScore::of(-5));
278 }
279
280 #[test]
281 fn test_accepted_count_forager_empty() {
282 let mut forager = AcceptedCountForager::<DummySolution>::new(3);
283 <AcceptedCountForager<DummySolution> as LocalSearchForager<DummySolution, TestMove>>::step_started(&mut forager);
284
285 assert!(<AcceptedCountForager<DummySolution> as LocalSearchForager<
286 DummySolution,
287 TestMove,
288 >>::pick_move_index(&mut forager)
289 .is_none());
290 }
291
292 #[test]
293 fn test_first_accepted_forager() {
294 let mut forager = FirstAcceptedForager::<DummySolution>::new();
295 <FirstAcceptedForager<DummySolution> as LocalSearchForager<DummySolution, TestMove>>::step_started(&mut forager);
296
297 assert!(!<FirstAcceptedForager<DummySolution> as LocalSearchForager<DummySolution, TestMove>>::is_quit_early(&forager));
298
299 <FirstAcceptedForager<DummySolution> as LocalSearchForager<DummySolution, TestMove>>::add_move_index(&mut forager, 0, SimpleScore::of(-10));
300 assert!(<FirstAcceptedForager<DummySolution> as LocalSearchForager<DummySolution, TestMove>>::is_quit_early(&forager));
301
302 <FirstAcceptedForager<DummySolution> as LocalSearchForager<DummySolution, TestMove>>::add_move_index(&mut forager, 1, SimpleScore::of(-5));
304
305 let (index, score) = <FirstAcceptedForager<DummySolution> as LocalSearchForager<
306 DummySolution,
307 TestMove,
308 >>::pick_move_index(&mut forager)
309 .unwrap();
310 assert_eq!(index, 0);
312 assert_eq!(score, SimpleScore::of(-10));
313 }
314
315 #[test]
316 fn test_forager_resets_on_step() {
317 let mut forager = AcceptedCountForager::<DummySolution>::new(3);
318
319 <AcceptedCountForager<DummySolution> as LocalSearchForager<DummySolution, TestMove>>::step_started(&mut forager);
320 <AcceptedCountForager<DummySolution> as LocalSearchForager<DummySolution, TestMove>>::add_move_index(&mut forager, 0, SimpleScore::of(-10));
321
322 <AcceptedCountForager<DummySolution> as LocalSearchForager<DummySolution, TestMove>>::step_started(&mut forager);
323 assert!(<AcceptedCountForager<DummySolution> as LocalSearchForager<
325 DummySolution,
326 TestMove,
327 >>::pick_move_index(&mut forager)
328 .is_none());
329 }
330}