1use std::fmt::Debug;
4use std::marker::PhantomData;
5use std::time::Instant;
6
7use rand::rngs::SmallRng;
8use rand::SeedableRng as _;
9use solverforge_core::domain::PlanningSolution;
10use solverforge_scoring::{Director, RecordingDirector};
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::ProgressCallback;
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 {
65 Self {
66 move_selector,
67 acceptor,
68 forager,
69 arena: MoveArena::new(),
70 step_limit,
71 _phantom: PhantomData,
72 }
73 }
74}
75
76impl<S, M, MS, A, Fo> Debug for LocalSearchPhase<S, M, MS, A, Fo>
77where
78 S: PlanningSolution,
79 M: Move<S>,
80 MS: MoveSelector<S, M> + Debug,
81 A: Acceptor<S> + Debug,
82 Fo: LocalSearchForager<S, M> + Debug,
83{
84 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
85 f.debug_struct("LocalSearchPhase")
86 .field("move_selector", &self.move_selector)
87 .field("acceptor", &self.acceptor)
88 .field("forager", &self.forager)
89 .field("arena", &self.arena)
90 .field("step_limit", &self.step_limit)
91 .finish()
92 }
93}
94
95impl<S, D, BestCb, M, MS, A, Fo> Phase<S, D, BestCb> for LocalSearchPhase<S, M, MS, A, Fo>
96where
97 S: PlanningSolution,
98 D: Director<S>,
99 BestCb: ProgressCallback<S>,
100 M: Move<S>,
101 MS: MoveSelector<S, M>,
102 A: Acceptor<S>,
103 Fo: LocalSearchForager<S, M>,
104{
105 fn solve(&mut self, solver_scope: &mut SolverScope<S, D, BestCb>) {
106 let mut phase_scope = PhaseScope::new(solver_scope, 0);
107 let phase_index = phase_scope.phase_index();
108
109 let mut last_step_score = phase_scope.calculate_score();
111
112 info!(
113 event = "phase_start",
114 phase = "Local Search",
115 phase_index = phase_index,
116 );
117
118 self.acceptor.phase_started(&last_step_score);
120
121 let start_time = Instant::now();
122 let mut local_moves_evaluated: u64 = 0;
123 let mut last_progress_time = Instant::now();
124 let mut last_progress_moves: u64 = 0;
125 let mut rng = SmallRng::from_rng(&mut rand::rng());
126
127 loop {
128 if phase_scope.solver_scope_mut().should_terminate() {
130 break;
131 }
132
133 if let Some(limit) = self.step_limit {
135 if phase_scope.step_count() >= limit {
136 break;
137 }
138 }
139
140 let mut step_scope = StepScope::new(&mut phase_scope);
141
142 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();
160
161 if self.move_selector.is_never_ending() {
162 'streaming: loop {
168 let batch_start = self.arena.len();
169 {
170 let iter = self.move_selector.iter_moves(step_scope.score_director());
171 for mov in iter.take(64) {
172 self.arena.push(mov);
173 }
174 }
175 let batch_end = self.arena.len();
176 if batch_end == batch_start {
177 break;
178 }
179
180 for i in batch_start..batch_end {
181 local_moves_evaluated += 1;
182
183 if local_moves_evaluated & 0x1FFF == 0 {
185 let now = Instant::now();
186 if now.duration_since(last_progress_time).as_secs() >= 1 {
187 let moves_delta = local_moves_evaluated - last_progress_moves;
188 let elapsed_secs =
189 now.duration_since(last_progress_time).as_secs_f64();
190 let current_speed = (moves_delta as f64 / elapsed_secs) as u64;
191 debug!(
192 event = "progress",
193 steps = step_scope.step_index(),
194 moves_evaluated = local_moves_evaluated,
195 moves_accepted = step_scope.phase_scope().solver_scope().stats().moves_accepted,
196 score_calculations = step_scope.phase_scope().solver_scope().stats().score_calculations,
197 speed = current_speed,
198 acceptance_rate = format!(
199 "{:.1}%",
200 step_scope.phase_scope().solver_scope().stats().acceptance_rate() * 100.0
201 ),
202 current_score = %last_step_score,
203 best_score = %best_score,
204 );
205 step_scope.phase_scope().solver_scope().report_progress();
206 last_progress_time = now;
207 last_progress_moves = local_moves_evaluated;
208 }
209 }
210
211 let m = self.arena.get(i).unwrap();
212
213 if !m.is_doable(step_scope.score_director()) {
214 step_scope
215 .phase_scope_mut()
216 .solver_scope_mut()
217 .stats_mut()
218 .record_move(false);
219 continue;
220 }
221
222 let move_score = {
223 let mut recording =
224 RecordingDirector::new(step_scope.score_director_mut());
225 m.do_move(&mut recording);
226 let score = recording.calculate_score();
227 recording.undo_changes();
228 score
229 };
230
231 step_scope
232 .phase_scope_mut()
233 .solver_scope_mut()
234 .stats_mut()
235 .record_score_calculation();
236
237 let accepted = self.acceptor.is_accepted(&last_step_score, &move_score);
238
239 step_scope
240 .phase_scope_mut()
241 .solver_scope_mut()
242 .stats_mut()
243 .record_move(accepted);
244
245 trace!(
246 event = "step",
247 step = step_scope.step_index(),
248 move_index = i,
249 score = %move_score,
250 accepted = accepted,
251 );
252
253 if accepted {
254 self.forager.add_move_index(i, move_score);
255 }
256
257 if self.forager.is_quit_early() {
258 break 'streaming;
259 }
260 }
261 }
262 } else {
263 self.move_selector
265 .append_moves(step_scope.score_director(), &mut self.arena);
266
267 self.arena.shuffle(&mut rng);
269
270 for i in 0..self.arena.len() {
272 local_moves_evaluated += 1;
273
274 if local_moves_evaluated & 0x1FFF == 0 {
276 let now = Instant::now();
277 if now.duration_since(last_progress_time).as_secs() >= 1 {
278 let moves_delta = local_moves_evaluated - last_progress_moves;
279 let elapsed_secs = now.duration_since(last_progress_time).as_secs_f64();
280 let current_speed = (moves_delta as f64 / elapsed_secs) as u64;
281 debug!(
282 event = "progress",
283 steps = step_scope.step_index(),
284 moves_evaluated = local_moves_evaluated,
285 moves_accepted = step_scope.phase_scope().solver_scope().stats().moves_accepted,
286 score_calculations = step_scope.phase_scope().solver_scope().stats().score_calculations,
287 speed = current_speed,
288 acceptance_rate = format!(
289 "{:.1}%",
290 step_scope.phase_scope().solver_scope().stats().acceptance_rate() * 100.0
291 ),
292 current_score = %last_step_score,
293 best_score = %best_score,
294 );
295 step_scope.phase_scope().solver_scope().report_progress();
296 last_progress_time = now;
297 last_progress_moves = local_moves_evaluated;
298 }
299 }
300
301 let m = self.arena.get(i).unwrap();
302
303 if !m.is_doable(step_scope.score_director()) {
304 step_scope
306 .phase_scope_mut()
307 .solver_scope_mut()
308 .stats_mut()
309 .record_move(false);
310 continue;
311 }
312
313 let move_score = {
318 let mut recording = RecordingDirector::new(step_scope.score_director_mut());
319
320 m.do_move(&mut recording);
322
323 let score = recording.calculate_score();
325
326 recording.undo_changes();
328
329 score
330 };
331
332 step_scope
334 .phase_scope_mut()
335 .solver_scope_mut()
336 .stats_mut()
337 .record_score_calculation();
338
339 let accepted = self.acceptor.is_accepted(&last_step_score, &move_score);
341
342 step_scope
344 .phase_scope_mut()
345 .solver_scope_mut()
346 .stats_mut()
347 .record_move(accepted);
348
349 trace!(
350 event = "step",
351 step = step_scope.step_index(),
352 move_index = i,
353 score = %move_score,
354 accepted = accepted,
355 );
356
357 if accepted {
359 self.forager.add_move_index(i, move_score);
360 }
361
362 if self.forager.is_quit_early() {
364 break;
365 }
366 }
367 }
368
369 if let Some((selected_index, selected_score)) = self.forager.pick_move_index() {
371 self.arena
376 .get(selected_index)
377 .unwrap()
378 .do_move(step_scope.score_director_mut());
379 step_scope.set_step_score(selected_score);
380
381 last_step_score = selected_score;
383
384 step_scope.phase_scope_mut().update_best_solution();
386 }
387 self.acceptor.step_ended(&last_step_score);
398
399 step_scope.complete();
400 }
401
402 self.acceptor.phase_ended();
404
405 let duration = start_time.elapsed();
406 let steps = phase_scope.step_count();
407 let secs = duration.as_secs_f64();
408 let speed = if secs > 0.0 {
409 (local_moves_evaluated as f64 / secs) as u64
410 } else {
411 0
412 };
413
414 let stats = phase_scope.solver_scope().stats();
415 let acceptance_rate = stats.acceptance_rate() * 100.0;
416 let calc_speed = if secs > 0.0 {
417 (stats.score_calculations as f64 / secs) as u64
418 } else {
419 0
420 };
421
422 let best_score_str = phase_scope
423 .solver_scope()
424 .best_score()
425 .map(|s| format!("{}", s))
426 .unwrap_or_else(|| "none".to_string());
427
428 info!(
429 event = "phase_end",
430 phase = "Local Search",
431 phase_index = phase_index,
432 duration_ms = duration.as_millis() as u64,
433 steps = steps,
434 moves_evaluated = stats.moves_evaluated,
435 moves_accepted = stats.moves_accepted,
436 score_calculations = stats.score_calculations,
437 moves_speed = speed,
438 calc_speed = calc_speed,
439 acceptance_rate = format!("{:.1}%", acceptance_rate),
440 score = best_score_str,
441 );
442 }
443
444 fn phase_type_name(&self) -> &'static str {
445 "LocalSearch"
446 }
447}
448
449#[cfg(test)]
450mod tests {
451 use super::*;
452 use crate::heuristic::selector::ChangeMoveSelector;
453 use crate::phase::localsearch::{AcceptedCountForager, HillClimbingAcceptor};
454 use crate::test_utils::{
455 create_nqueens_director, get_queen_row, set_queen_row, NQueensSolution,
456 };
457 type NQueensMove = crate::heuristic::r#move::ChangeMove<NQueensSolution, i64>;
458
459 fn create_move_selector(
460 values: Vec<i64>,
461 ) -> ChangeMoveSelector<
462 NQueensSolution,
463 i64,
464 crate::heuristic::selector::FromSolutionEntitySelector,
465 crate::heuristic::selector::StaticValueSelector<NQueensSolution, i64>,
466 > {
467 ChangeMoveSelector::simple(get_queen_row, set_queen_row, 0, "row", values)
468 }
469
470 #[test]
471 fn test_local_search_hill_climbing() {
472 let director = create_nqueens_director(&[0, 0, 0, 0]);
473 let mut solver_scope = SolverScope::new(director);
474 solver_scope.start_solving();
475
476 let initial_score = solver_scope.calculate_score();
477
478 let values: Vec<i64> = (0..4).collect();
479 let move_selector = create_move_selector(values);
480 let acceptor = HillClimbingAcceptor::new();
481 let forager: AcceptedCountForager<_> = AcceptedCountForager::new(1);
482 let mut phase: LocalSearchPhase<_, NQueensMove, _, _, _> =
483 LocalSearchPhase::new(move_selector, acceptor, forager, Some(100));
484
485 phase.solve(&mut solver_scope);
486
487 let final_score = solver_scope.best_score().copied().unwrap_or(initial_score);
488 assert!(final_score >= initial_score);
489
490 assert!(solver_scope.stats().moves_evaluated > 0);
492 }
493
494 #[test]
495 fn test_local_search_reaches_optimal() {
496 let director = create_nqueens_director(&[0, 2, 1, 3]);
497 let mut solver_scope = SolverScope::new(director);
498 solver_scope.start_solving();
499
500 let initial_score = solver_scope.calculate_score();
501
502 let values: Vec<i64> = (0..4).collect();
503 let move_selector = create_move_selector(values);
504 let acceptor = HillClimbingAcceptor::new();
505 let forager: AcceptedCountForager<_> = AcceptedCountForager::new(1);
506 let mut phase: LocalSearchPhase<_, NQueensMove, _, _, _> =
507 LocalSearchPhase::new(move_selector, acceptor, forager, Some(50));
508
509 phase.solve(&mut solver_scope);
510
511 let final_score = solver_scope.best_score().copied().unwrap_or(initial_score);
512 assert!(final_score >= initial_score);
513 }
514
515 #[test]
516 fn test_local_search_step_limit() {
517 let director = create_nqueens_director(&[0, 0, 0, 0]);
518 let mut solver_scope = SolverScope::new(director);
519 solver_scope.start_solving();
520
521 let values: Vec<i64> = (0..4).collect();
522 let move_selector = create_move_selector(values);
523 let acceptor = HillClimbingAcceptor::new();
524 let forager: AcceptedCountForager<_> = AcceptedCountForager::new(1);
525 let mut phase: LocalSearchPhase<_, NQueensMove, _, _, _> =
526 LocalSearchPhase::new(move_selector, acceptor, forager, Some(3));
527
528 phase.solve(&mut solver_scope);
529
530 assert!(solver_scope.stats().step_count <= 3);
532 }
533}