1use std::fmt::Debug;
4use std::marker::PhantomData;
5use std::time::Instant;
6
7use rand::rngs::SmallRng;
8use rand::SeedableRng;
9use solverforge_core::domain::PlanningSolution;
10use solverforge_scoring::{RecordingScoreDirector, ScoreDirector};
11use tracing::{debug, info, trace};
12
13use crate::heuristic::r#move::{Move, MoveArena};
14use crate::heuristic::selector::MoveSelector;
15use crate::phase::localsearch::{Acceptor, LocalSearchForager};
16use crate::phase::Phase;
17use crate::scope::BestSolutionCallback;
18use crate::scope::{PhaseScope, SolverScope, StepScope};
19
20pub struct LocalSearchPhase<S, M, MS, A, Fo>
41where
42 S: PlanningSolution,
43 M: Move<S>,
44 MS: MoveSelector<S, M>,
45 A: Acceptor<S>,
46 Fo: LocalSearchForager<S, M>,
47{
48 move_selector: MS,
49 acceptor: A,
50 forager: Fo,
51 arena: MoveArena<M>,
52 step_limit: Option<u64>,
53 _phantom: PhantomData<fn() -> (S, M)>,
54}
55
56impl<S, M, MS, A, Fo> LocalSearchPhase<S, M, MS, A, Fo>
57where
58 S: PlanningSolution,
59 M: Move<S> + 'static,
60 MS: MoveSelector<S, M>,
61 A: Acceptor<S>,
62 Fo: LocalSearchForager<S, M>,
63{
64 pub fn new(move_selector: MS, acceptor: A, forager: Fo, step_limit: Option<u64>) -> Self {
66 Self {
67 move_selector,
68 acceptor,
69 forager,
70 arena: MoveArena::new(),
71 step_limit,
72 _phantom: PhantomData,
73 }
74 }
75}
76
77impl<S, M, MS, A, Fo> Debug for LocalSearchPhase<S, M, MS, A, Fo>
78where
79 S: PlanningSolution,
80 M: Move<S>,
81 MS: MoveSelector<S, M> + Debug,
82 A: Acceptor<S> + Debug,
83 Fo: LocalSearchForager<S, M> + Debug,
84{
85 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
86 f.debug_struct("LocalSearchPhase")
87 .field("move_selector", &self.move_selector)
88 .field("acceptor", &self.acceptor)
89 .field("forager", &self.forager)
90 .field("arena", &self.arena)
91 .field("step_limit", &self.step_limit)
92 .finish()
93 }
94}
95
96impl<S, D, BestCb, M, MS, A, Fo> Phase<S, D, BestCb> for LocalSearchPhase<S, M, MS, A, Fo>
97where
98 S: PlanningSolution,
99 D: ScoreDirector<S>,
100 BestCb: BestSolutionCallback<S>,
101 M: Move<S>,
102 MS: MoveSelector<S, M>,
103 A: Acceptor<S>,
104 Fo: LocalSearchForager<S, M>,
105{
106 fn solve(&mut self, solver_scope: &mut SolverScope<S, D, BestCb>) {
107 let mut phase_scope = PhaseScope::new(solver_scope, 0);
108 let phase_index = phase_scope.phase_index();
109
110 let mut last_step_score = phase_scope.calculate_score();
112
113 info!(
114 event = "phase_start",
115 phase = "Local Search",
116 phase_index = phase_index,
117 );
118
119 self.acceptor.phase_started(&last_step_score);
121
122 let start_time = Instant::now();
123 let mut local_moves_evaluated: u64 = 0;
124 let mut last_progress_time = Instant::now();
125 let mut last_progress_moves: u64 = 0;
126 let mut rng = SmallRng::from_os_rng();
127
128 loop {
129 if phase_scope.solver_scope().should_terminate() {
131 break;
132 }
133
134 if let Some(limit) = self.step_limit {
136 if phase_scope.step_count() >= limit {
137 break;
138 }
139 }
140
141 let mut step_scope = StepScope::new(&mut phase_scope);
142
143 let best_score = step_scope
147 .phase_scope()
148 .solver_scope()
149 .best_score()
150 .copied()
151 .unwrap_or(last_step_score);
152 self.forager.step_started(best_score, last_step_score);
153 self.acceptor.step_started();
154
155 self.arena.reset();
159
160 if self.move_selector.is_never_ending() {
161 'streaming: loop {
166 let batch_start = self.arena.len();
167 {
168 let iter = self.move_selector.iter_moves(step_scope.score_director());
169 for mov in iter.take(64) {
170 self.arena.push(mov);
171 }
172 }
173 let batch_end = self.arena.len();
174 if batch_end == batch_start {
175 break;
176 }
177
178 for i in batch_start..batch_end {
179 local_moves_evaluated += 1;
180
181 if local_moves_evaluated & 0x1FFF == 0 {
183 let now = Instant::now();
184 if now.duration_since(last_progress_time).as_secs() >= 1 {
185 let moves_delta = local_moves_evaluated - last_progress_moves;
186 let elapsed_secs =
187 now.duration_since(last_progress_time).as_secs_f64();
188 let current_speed = (moves_delta as f64 / elapsed_secs) as u64;
189 debug!(
190 event = "progress",
191 steps = step_scope.step_index(),
192 moves_evaluated = local_moves_evaluated,
193 speed = current_speed,
194 score = %last_step_score,
195 );
196 last_progress_time = now;
197 last_progress_moves = local_moves_evaluated;
198 }
199 }
200
201 let m = self.arena.get(i).unwrap();
202
203 if !m.is_doable(step_scope.score_director()) {
204 step_scope
205 .phase_scope_mut()
206 .solver_scope_mut()
207 .stats_mut()
208 .record_move(false);
209 continue;
210 }
211
212 let move_score = {
213 let mut recording =
214 RecordingScoreDirector::new(step_scope.score_director_mut());
215 m.do_move(&mut recording);
216 let score = recording.calculate_score();
217 recording.undo_changes();
218 score
219 };
220
221 step_scope
222 .phase_scope_mut()
223 .solver_scope_mut()
224 .stats_mut()
225 .record_score_calculation();
226
227 let accepted = self.acceptor.is_accepted(&last_step_score, &move_score);
228
229 step_scope
230 .phase_scope_mut()
231 .solver_scope_mut()
232 .stats_mut()
233 .record_move(accepted);
234
235 trace!(
236 event = "step",
237 step = step_scope.step_index(),
238 move_index = i,
239 score = %move_score,
240 accepted = accepted,
241 );
242
243 if accepted {
244 self.forager.add_move_index(i, move_score);
245 }
246
247 if self.forager.is_quit_early() {
248 break 'streaming;
249 }
250 }
251 }
252 } else {
253 self.arena
255 .extend(self.move_selector.iter_moves(step_scope.score_director()));
256
257 self.arena.shuffle(&mut rng);
259
260 for i in 0..self.arena.len() {
262 local_moves_evaluated += 1;
263
264 if local_moves_evaluated & 0x1FFF == 0 {
266 let now = Instant::now();
267 if now.duration_since(last_progress_time).as_secs() >= 1 {
268 let moves_delta = local_moves_evaluated - last_progress_moves;
269 let elapsed_secs = now.duration_since(last_progress_time).as_secs_f64();
270 let current_speed = (moves_delta as f64 / elapsed_secs) as u64;
271 debug!(
272 event = "progress",
273 steps = step_scope.step_index(),
274 moves_evaluated = local_moves_evaluated,
275 speed = current_speed,
276 score = %last_step_score,
277 );
278 last_progress_time = now;
279 last_progress_moves = local_moves_evaluated;
280 }
281 }
282
283 let m = self.arena.get(i).unwrap();
284
285 if !m.is_doable(step_scope.score_director()) {
286 step_scope
288 .phase_scope_mut()
289 .solver_scope_mut()
290 .stats_mut()
291 .record_move(false);
292 continue;
293 }
294
295 let move_score = {
299 let mut recording =
300 RecordingScoreDirector::new(step_scope.score_director_mut());
301
302 m.do_move(&mut recording);
304
305 let score = recording.calculate_score();
307
308 recording.undo_changes();
310
311 score
312 };
313
314 step_scope
316 .phase_scope_mut()
317 .solver_scope_mut()
318 .stats_mut()
319 .record_score_calculation();
320
321 let accepted = self.acceptor.is_accepted(&last_step_score, &move_score);
323
324 step_scope
326 .phase_scope_mut()
327 .solver_scope_mut()
328 .stats_mut()
329 .record_move(accepted);
330
331 trace!(
332 event = "step",
333 step = step_scope.step_index(),
334 move_index = i,
335 score = %move_score,
336 accepted = accepted,
337 );
338
339 if accepted {
341 self.forager.add_move_index(i, move_score);
342 }
343
344 if self.forager.is_quit_early() {
346 break;
347 }
348 }
349 }
350
351 if let Some((selected_index, selected_score)) = self.forager.pick_move_index() {
353 self.arena
357 .get(selected_index)
358 .unwrap()
359 .do_move(step_scope.score_director_mut());
360 step_scope.set_step_score(selected_score);
361
362 last_step_score = selected_score;
364
365 step_scope.phase_scope_mut().update_best_solution();
367 }
368 self.acceptor.step_ended(&last_step_score);
377
378 step_scope.complete();
379 }
380
381 self.acceptor.phase_ended();
383
384 let duration = start_time.elapsed();
385 let steps = phase_scope.step_count();
386 let secs = duration.as_secs_f64();
387 let speed = if secs > 0.0 {
388 (local_moves_evaluated as f64 / secs) as u64
389 } else {
390 0
391 };
392
393 let stats = phase_scope.solver_scope().stats();
394 let acceptance_rate = stats.acceptance_rate() * 100.0;
395 let calc_speed = if secs > 0.0 {
396 (stats.score_calculations as f64 / secs) as u64
397 } else {
398 0
399 };
400
401 let best_score_str = phase_scope
402 .solver_scope()
403 .best_score()
404 .map(|s| format!("{}", s))
405 .unwrap_or_else(|| "none".to_string());
406
407 info!(
408 event = "phase_end",
409 phase = "Local Search",
410 phase_index = phase_index,
411 duration_ms = duration.as_millis() as u64,
412 steps = steps,
413 moves_speed = speed,
414 calc_speed = calc_speed,
415 acceptance_rate = format!("{:.1}%", acceptance_rate),
416 score = best_score_str,
417 );
418 }
419
420 fn phase_type_name(&self) -> &'static str {
421 "LocalSearch"
422 }
423}
424
425#[cfg(test)]
426mod tests {
427 use super::*;
428 use crate::heuristic::selector::ChangeMoveSelector;
429 use crate::phase::localsearch::{AcceptedCountForager, HillClimbingAcceptor};
430 use crate::test_utils::{
431 create_nqueens_director, get_queen_row, set_queen_row, NQueensSolution,
432 };
433 use solverforge_core::score::SimpleScore;
434
435 type NQueensMove = crate::heuristic::r#move::ChangeMove<NQueensSolution, i64>;
436
437 fn create_move_selector(
438 values: Vec<i64>,
439 ) -> ChangeMoveSelector<
440 NQueensSolution,
441 i64,
442 crate::heuristic::selector::FromSolutionEntitySelector,
443 crate::heuristic::selector::StaticTypedValueSelector<NQueensSolution, i64>,
444 > {
445 ChangeMoveSelector::simple(get_queen_row, set_queen_row, 0, "row", values)
446 }
447
448 #[test]
449 fn test_local_search_hill_climbing() {
450 let director = create_nqueens_director(&[0, 0, 0, 0]);
451 let mut solver_scope = SolverScope::new(director);
452 solver_scope.start_solving();
453
454 let initial_score = solver_scope.calculate_score();
455 assert!(initial_score < SimpleScore::of(0));
456
457 let values: Vec<i64> = (0..4).collect();
458 let move_selector = create_move_selector(values);
459 let acceptor = HillClimbingAcceptor::new();
460 let forager: AcceptedCountForager<_> = AcceptedCountForager::new(1);
461 let mut phase: LocalSearchPhase<_, NQueensMove, _, _, _> =
462 LocalSearchPhase::new(move_selector, acceptor, forager, Some(100));
463
464 phase.solve(&mut solver_scope);
465
466 let final_score = solver_scope.best_score().copied().unwrap_or(initial_score);
467 assert!(final_score >= initial_score);
468
469 assert!(solver_scope.stats().moves_evaluated > 0);
471 }
472
473 #[test]
474 fn test_local_search_reaches_optimal() {
475 let director = create_nqueens_director(&[0, 2, 1, 3]);
476 let mut solver_scope = SolverScope::new(director);
477 solver_scope.start_solving();
478
479 let initial_score = solver_scope.calculate_score();
480
481 let values: Vec<i64> = (0..4).collect();
482 let move_selector = create_move_selector(values);
483 let acceptor = HillClimbingAcceptor::new();
484 let forager: AcceptedCountForager<_> = AcceptedCountForager::new(1);
485 let mut phase: LocalSearchPhase<_, NQueensMove, _, _, _> =
486 LocalSearchPhase::new(move_selector, acceptor, forager, Some(50));
487
488 phase.solve(&mut solver_scope);
489
490 let final_score = solver_scope.best_score().copied().unwrap_or(initial_score);
491 assert!(final_score >= initial_score);
492 }
493
494 #[test]
495 fn test_local_search_step_limit() {
496 let director = create_nqueens_director(&[0, 0, 0, 0]);
497 let mut solver_scope = SolverScope::new(director);
498 solver_scope.start_solving();
499
500 let values: Vec<i64> = (0..4).collect();
501 let move_selector = create_move_selector(values);
502 let acceptor = HillClimbingAcceptor::new();
503 let forager: AcceptedCountForager<_> = AcceptedCountForager::new(1);
504 let mut phase: LocalSearchPhase<_, NQueensMove, _, _, _> =
505 LocalSearchPhase::new(move_selector, acceptor, forager, Some(3));
506
507 phase.solve(&mut solver_scope);
508
509 assert!(solver_scope.stats().step_count <= 3);
511 }
512}