solverforge_solver/phase/localsearch/
mod.rs1mod acceptor;
7mod forager;
8
9use std::fmt::Debug;
10use std::marker::PhantomData;
11
12use solverforge_core::domain::PlanningSolution;
13use solverforge_scoring::{RecordingScoreDirector, ScoreDirector};
14
15use crate::heuristic::r#move::{Move, MoveArena};
16use crate::heuristic::selector::MoveSelector;
17use crate::phase::Phase;
18use crate::scope::{PhaseScope, SolverScope, StepScope};
19
20pub use acceptor::{
21 Acceptor, DiversifiedLateAcceptanceAcceptor, EntityTabuAcceptor, GreatDelugeAcceptor,
22 HillClimbingAcceptor, LateAcceptanceAcceptor, MoveTabuAcceptor, SimulatedAnnealingAcceptor,
23 StepCountingHillClimbingAcceptor, TabuSearchAcceptor, ValueTabuAcceptor,
24};
25pub use forager::{AcceptedCountForager, FirstAcceptedForager, LocalSearchForager};
26
27#[derive(Debug, Clone)]
29pub struct LocalSearchConfig {
30 pub acceptor_type: AcceptorType,
32 pub step_limit: Option<u64>,
34 pub accepted_count_limit: Option<usize>,
36}
37
38impl Default for LocalSearchConfig {
39 fn default() -> Self {
40 Self {
41 acceptor_type: AcceptorType::HillClimbing,
42 step_limit: Some(1000),
43 accepted_count_limit: Some(1),
44 }
45 }
46}
47
48#[derive(Debug, Clone, Copy, PartialEq, Eq)]
50pub enum AcceptorType {
51 HillClimbing,
53 SimulatedAnnealing,
55}
56
57pub struct LocalSearchPhase<S, M>
74where
75 S: PlanningSolution,
76 M: Move<S>,
77{
78 move_selector: Box<dyn MoveSelector<S, M>>,
80 acceptor: Box<dyn Acceptor<S>>,
82 forager: Box<dyn LocalSearchForager<S, M>>,
84 arena: MoveArena<M>,
86 step_limit: Option<u64>,
88 _phantom: PhantomData<M>,
89}
90
91impl<S, M> LocalSearchPhase<S, M>
92where
93 S: PlanningSolution,
94 M: Move<S> + 'static,
95{
96 pub fn new(
98 move_selector: Box<dyn MoveSelector<S, M>>,
99 acceptor: Box<dyn Acceptor<S>>,
100 forager: Box<dyn LocalSearchForager<S, M>>,
101 step_limit: Option<u64>,
102 ) -> Self {
103 Self {
104 move_selector,
105 acceptor,
106 forager,
107 arena: MoveArena::new(),
108 step_limit,
109 _phantom: PhantomData,
110 }
111 }
112}
113
114impl<S, M> Debug for LocalSearchPhase<S, M>
115where
116 S: PlanningSolution,
117 M: Move<S>,
118{
119 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
120 f.debug_struct("LocalSearchPhase")
121 .field("move_selector", &self.move_selector)
122 .field("acceptor", &self.acceptor)
123 .field("forager", &self.forager)
124 .field("arena", &self.arena)
125 .field("step_limit", &self.step_limit)
126 .finish()
127 }
128}
129
130impl<S, M> Phase<S> for LocalSearchPhase<S, M>
131where
132 S: PlanningSolution,
133 M: Move<S>,
134{
135 fn solve(&mut self, solver_scope: &mut SolverScope<S>) {
136 let mut phase_scope = PhaseScope::new(solver_scope, 0);
137
138 let mut last_step_score = phase_scope.calculate_score();
140
141 self.acceptor.phase_started(&last_step_score);
143
144 loop {
145 if phase_scope.solver_scope().is_terminate_early() {
147 break;
148 }
149
150 if let Some(limit) = self.step_limit {
152 if phase_scope.step_count() >= limit {
153 break;
154 }
155 }
156
157 let mut step_scope = StepScope::new(&mut phase_scope);
158
159 self.forager.step_started();
161
162 self.arena.reset();
164 self.arena
165 .extend(self.move_selector.iter_moves(step_scope.score_director()));
166
167 for i in 0..self.arena.len() {
169 let m = self.arena.get(i).unwrap();
170
171 if !m.is_doable(step_scope.score_director()) {
172 continue;
173 }
174
175 {
177 let mut recording =
178 RecordingScoreDirector::new(step_scope.score_director_mut());
179
180 m.do_move(&mut recording);
182
183 let move_score = recording.calculate_score();
185
186 let accepted = self.acceptor.is_accepted(&last_step_score, &move_score);
188
189 if accepted {
191 self.forager.add_move(m.clone(), move_score);
192 }
193
194 recording.undo_changes();
196 }
197
198 if self.forager.is_quit_early() {
200 break;
201 }
202 }
203
204 if let Some((selected_move, selected_score)) = self.forager.pick_move() {
206 selected_move.do_move(step_scope.score_director_mut());
208 step_scope.set_step_score(selected_score.clone());
209
210 last_step_score = selected_score;
212
213 step_scope.phase_scope_mut().update_best_solution();
215 } else {
216 break;
218 }
219
220 step_scope.complete();
221 }
222
223 self.acceptor.phase_ended();
225 }
226
227 fn phase_type_name(&self) -> &'static str {
228 "LocalSearch"
229 }
230}
231
232#[cfg(test)]
233mod tests {
234 use super::*;
235 use crate::heuristic::r#move::ChangeMove;
236 use crate::heuristic::selector::{ChangeMoveSelector, MoveSelector};
237 use crate::manager::SolverPhaseFactory;
238 use solverforge_core::domain::{EntityDescriptor, SolutionDescriptor, TypedEntityExtractor};
239 use solverforge_core::score::SimpleScore;
240 use solverforge_scoring::SimpleScoreDirector;
241 use std::any::TypeId;
242
243 #[derive(Clone, Debug)]
244 struct Queen {
245 column: i32,
246 row: Option<i32>,
247 }
248
249 #[derive(Clone, Debug)]
250 struct NQueensSolution {
251 queens: Vec<Queen>,
252 score: Option<SimpleScore>,
253 }
254
255 impl PlanningSolution for NQueensSolution {
256 type Score = SimpleScore;
257
258 fn score(&self) -> Option<Self::Score> {
259 self.score
260 }
261
262 fn set_score(&mut self, score: Option<Self::Score>) {
263 self.score = score;
264 }
265 }
266
267 fn get_queens(s: &NQueensSolution) -> &Vec<Queen> {
268 &s.queens
269 }
270
271 fn get_queens_mut(s: &mut NQueensSolution) -> &mut Vec<Queen> {
272 &mut s.queens
273 }
274
275 fn get_queen_row(s: &NQueensSolution, idx: usize) -> Option<i32> {
277 s.queens.get(idx).and_then(|q| q.row)
278 }
279
280 fn set_queen_row(s: &mut NQueensSolution, idx: usize, v: Option<i32>) {
281 if let Some(queen) = s.queens.get_mut(idx) {
282 queen.row = v;
283 }
284 }
285
286 fn calculate_conflicts(solution: &NQueensSolution) -> SimpleScore {
287 let mut conflicts = 0i64;
288
289 for (i, q1) in solution.queens.iter().enumerate() {
290 if let Some(row1) = q1.row {
291 for q2 in solution.queens.iter().skip(i + 1) {
292 if let Some(row2) = q2.row {
293 if row1 == row2 {
295 conflicts += 1;
296 }
297 let col_diff = (q2.column - q1.column).abs();
299 let row_diff = (row2 - row1).abs();
300 if col_diff == row_diff {
301 conflicts += 1;
302 }
303 }
304 }
305 }
306 }
307
308 SimpleScore::of(-conflicts)
309 }
310
311 fn create_test_director(
312 rows: &[i32],
313 ) -> SimpleScoreDirector<NQueensSolution, impl Fn(&NQueensSolution) -> SimpleScore> {
314 let queens: Vec<_> = rows
315 .iter()
316 .enumerate()
317 .map(|(col, &row)| Queen {
318 column: col as i32,
319 row: Some(row),
320 })
321 .collect();
322
323 let solution = NQueensSolution {
324 queens,
325 score: None,
326 };
327
328 let extractor = Box::new(TypedEntityExtractor::new(
329 "Queen",
330 "queens",
331 get_queens,
332 get_queens_mut,
333 ));
334 let entity_desc = EntityDescriptor::new("Queen", TypeId::of::<Queen>(), "queens")
335 .with_extractor(extractor);
336
337 let descriptor =
338 SolutionDescriptor::new("NQueensSolution", TypeId::of::<NQueensSolution>())
339 .with_entity(entity_desc);
340
341 SimpleScoreDirector::with_calculator(solution, descriptor, calculate_conflicts)
342 }
343
344 type NQueensMove = ChangeMove<NQueensSolution, i32>;
345
346 fn create_move_selector(
347 values: Vec<i32>,
348 ) -> Box<dyn MoveSelector<NQueensSolution, NQueensMove>> {
349 Box::new(ChangeMoveSelector::<NQueensSolution, i32>::simple(
350 get_queen_row,
351 set_queen_row,
352 0,
353 "row",
354 values,
355 ))
356 }
357
358 #[test]
359 fn test_local_search_hill_climbing() {
360 use crate::manager::LocalSearchPhaseFactory;
361
362 let director = create_test_director(&[0, 0, 0, 0]);
364 let mut solver_scope = SolverScope::new(Box::new(director));
365
366 let initial_score = solver_scope.calculate_score();
368 assert!(initial_score < SimpleScore::of(0));
369
370 let values: Vec<i32> = (0..4).collect();
371 let factory =
372 LocalSearchPhaseFactory::<NQueensSolution, NQueensMove, _>::hill_climbing(move || {
373 create_move_selector(values.clone())
374 })
375 .with_step_limit(100);
376 let mut phase = factory.create_phase();
377
378 phase.solve(&mut solver_scope);
379
380 let final_score = solver_scope.best_score().cloned().unwrap_or(initial_score);
382 assert!(final_score >= initial_score);
383 }
384
385 #[test]
386 fn test_local_search_reaches_optimal() {
387 use crate::manager::LocalSearchPhaseFactory;
388
389 let director = create_test_director(&[0, 2, 1, 3]);
391 let mut solver_scope = SolverScope::new(Box::new(director));
392
393 let initial_score = solver_scope.calculate_score();
394
395 let values: Vec<i32> = (0..4).collect();
396 let factory =
397 LocalSearchPhaseFactory::<NQueensSolution, NQueensMove, _>::hill_climbing(move || {
398 create_move_selector(values.clone())
399 })
400 .with_step_limit(50);
401 let mut phase = factory.create_phase();
402
403 phase.solve(&mut solver_scope);
404
405 let final_score = solver_scope.best_score().cloned().unwrap_or(initial_score);
407 assert!(final_score >= initial_score);
408 }
409
410 #[test]
411 fn test_local_search_step_limit() {
412 use crate::manager::LocalSearchPhaseFactory;
413
414 let director = create_test_director(&[0, 0, 0, 0]);
415 let mut solver_scope = SolverScope::new(Box::new(director));
416
417 let values: Vec<i32> = (0..4).collect();
418 let factory =
419 LocalSearchPhaseFactory::<NQueensSolution, NQueensMove, _>::hill_climbing(move || {
420 create_move_selector(values.clone())
421 })
422 .with_step_limit(3);
423 let mut phase = factory.create_phase();
424
425 phase.solve(&mut solver_scope);
426
427 }
430}