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::control::{
16 append_interruptibly, settle_search_interrupt, should_interrupt_evaluation, StepInterrupt,
17};
18use crate::phase::localsearch::{Acceptor, LocalSearchForager};
19use crate::phase::Phase;
20use crate::scope::ProgressCallback;
21use crate::scope::{PhaseScope, SolverScope, StepScope};
22
23pub struct LocalSearchPhase<S, M, MS, A, Fo>
44where
45 S: PlanningSolution,
46 M: Move<S>,
47 MS: MoveSelector<S, M>,
48 A: Acceptor<S>,
49 Fo: LocalSearchForager<S, M>,
50{
51 move_selector: MS,
52 acceptor: A,
53 forager: Fo,
54 arena: MoveArena<M>,
55 step_limit: Option<u64>,
56 _phantom: PhantomData<fn() -> (S, M)>,
57}
58
59impl<S, M, MS, A, Fo> LocalSearchPhase<S, M, MS, A, Fo>
60where
61 S: PlanningSolution,
62 M: Move<S> + 'static,
63 MS: MoveSelector<S, M>,
64 A: Acceptor<S>,
65 Fo: LocalSearchForager<S, M>,
66{
67 pub fn new(move_selector: MS, acceptor: A, forager: Fo, step_limit: Option<u64>) -> Self {
68 Self {
69 move_selector,
70 acceptor,
71 forager,
72 arena: MoveArena::new(),
73 step_limit,
74 _phantom: PhantomData,
75 }
76 }
77}
78
79impl<S, M, MS, A, Fo> Debug for LocalSearchPhase<S, M, MS, A, Fo>
80where
81 S: PlanningSolution,
82 M: Move<S>,
83 MS: MoveSelector<S, M> + Debug,
84 A: Acceptor<S> + Debug,
85 Fo: LocalSearchForager<S, M> + Debug,
86{
87 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
88 f.debug_struct("LocalSearchPhase")
89 .field("move_selector", &self.move_selector)
90 .field("acceptor", &self.acceptor)
91 .field("forager", &self.forager)
92 .field("arena", &self.arena)
93 .field("step_limit", &self.step_limit)
94 .finish()
95 }
96}
97
98impl<S, D, BestCb, M, MS, A, Fo> Phase<S, D, BestCb> for LocalSearchPhase<S, M, MS, A, Fo>
99where
100 S: PlanningSolution,
101 D: Director<S>,
102 BestCb: ProgressCallback<S>,
103 M: Move<S>,
104 MS: MoveSelector<S, M>,
105 A: Acceptor<S>,
106 Fo: LocalSearchForager<S, M>,
107{
108 fn solve(&mut self, solver_scope: &mut SolverScope<S, D, BestCb>) {
109 let mut phase_scope = PhaseScope::new(solver_scope, 0);
110 let phase_index = phase_scope.phase_index();
111
112 let mut last_step_score = phase_scope.calculate_score();
114
115 info!(
116 event = "phase_start",
117 phase = "Local Search",
118 phase_index = phase_index,
119 );
120
121 self.acceptor.phase_started(&last_step_score);
123
124 let start_time = Instant::now();
125 let mut local_moves_evaluated: u64 = 0;
126 let mut last_progress_time = Instant::now();
127 let mut last_progress_moves: u64 = 0;
128 let mut rng = SmallRng::from_rng(&mut rand::rng());
129
130 loop {
131 if phase_scope.solver_scope_mut().should_terminate() {
133 break;
134 }
135
136 if let Some(limit) = self.step_limit {
138 if phase_scope.step_count() >= limit {
139 break;
140 }
141 }
142
143 let mut step_scope = StepScope::new(&mut phase_scope);
144
145 let best_score = step_scope
150 .phase_scope()
151 .solver_scope()
152 .best_score()
153 .copied()
154 .unwrap_or(last_step_score);
155 self.forager.step_started(best_score, last_step_score);
156 self.acceptor.step_started();
157
158 self.arena.reset();
163 let mut interrupted_step = false;
164
165 if self.move_selector.is_never_ending() {
166 'streaming: loop {
172 let batch_start = self.arena.len();
173 let generation_interrupted = {
174 let iter = self.move_selector.iter_moves(step_scope.score_director());
175 append_interruptibly(&step_scope, &mut self.arena, iter.take(64))
176 };
177 if generation_interrupted {
178 interrupted_step = true;
179 break;
180 }
181 let batch_end = self.arena.len();
182 if batch_end == batch_start {
183 break;
184 }
185
186 for i in batch_start..batch_end {
187 if should_interrupt_evaluation(&step_scope, i) {
188 interrupted_step = true;
189 break 'streaming;
190 }
191
192 local_moves_evaluated += 1;
193
194 if local_moves_evaluated & 0x1FFF == 0 {
196 let now = Instant::now();
197 if now.duration_since(last_progress_time).as_secs() >= 1 {
198 let moves_delta = local_moves_evaluated - last_progress_moves;
199 let elapsed_secs =
200 now.duration_since(last_progress_time).as_secs_f64();
201 let current_speed = (moves_delta as f64 / elapsed_secs) as u64;
202 debug!(
203 event = "progress",
204 steps = step_scope.step_index(),
205 moves_evaluated = local_moves_evaluated,
206 moves_accepted = step_scope.phase_scope().solver_scope().stats().moves_accepted,
207 score_calculations = step_scope.phase_scope().solver_scope().stats().score_calculations,
208 speed = current_speed,
209 acceptance_rate = format!(
210 "{:.1}%",
211 step_scope.phase_scope().solver_scope().stats().acceptance_rate() * 100.0
212 ),
213 current_score = %last_step_score,
214 best_score = %best_score,
215 );
216 step_scope.phase_scope().solver_scope().report_progress();
217 last_progress_time = now;
218 last_progress_moves = local_moves_evaluated;
219 }
220 }
221
222 let m = self.arena.get(i).unwrap();
223
224 if !m.is_doable(step_scope.score_director()) {
225 step_scope
226 .phase_scope_mut()
227 .solver_scope_mut()
228 .stats_mut()
229 .record_move(false);
230 continue;
231 }
232
233 let move_score = {
234 let mut recording =
235 RecordingDirector::new(step_scope.score_director_mut());
236 m.do_move(&mut recording);
237 let score = recording.calculate_score();
238 recording.undo_changes();
239 score
240 };
241
242 step_scope
243 .phase_scope_mut()
244 .solver_scope_mut()
245 .stats_mut()
246 .record_score_calculation();
247
248 let accepted = self.acceptor.is_accepted(&last_step_score, &move_score);
249
250 step_scope
251 .phase_scope_mut()
252 .solver_scope_mut()
253 .stats_mut()
254 .record_move(accepted);
255
256 trace!(
257 event = "step",
258 step = step_scope.step_index(),
259 move_index = i,
260 score = %move_score,
261 accepted = accepted,
262 );
263
264 if accepted {
265 self.forager.add_move_index(i, move_score);
266 }
267
268 if self.forager.is_quit_early() {
269 break 'streaming;
270 }
271 }
272 }
273 } else {
274 let generation_interrupted = {
276 let iter = self.move_selector.iter_moves(step_scope.score_director());
277 append_interruptibly(&step_scope, &mut self.arena, iter)
278 };
279 if generation_interrupted {
280 interrupted_step = true;
281 }
282
283 if !interrupted_step {
284 self.arena.shuffle(&mut rng);
286 }
287
288 if !interrupted_step {
289 for i in 0..self.arena.len() {
291 if should_interrupt_evaluation(&step_scope, i) {
292 interrupted_step = true;
293 break;
294 }
295
296 local_moves_evaluated += 1;
297
298 if local_moves_evaluated & 0x1FFF == 0 {
300 let now = Instant::now();
301 if now.duration_since(last_progress_time).as_secs() >= 1 {
302 let moves_delta = local_moves_evaluated - last_progress_moves;
303 let elapsed_secs =
304 now.duration_since(last_progress_time).as_secs_f64();
305 let current_speed = (moves_delta as f64 / elapsed_secs) as u64;
306 debug!(
307 event = "progress",
308 steps = step_scope.step_index(),
309 moves_evaluated = local_moves_evaluated,
310 moves_accepted = step_scope.phase_scope().solver_scope().stats().moves_accepted,
311 score_calculations = step_scope.phase_scope().solver_scope().stats().score_calculations,
312 speed = current_speed,
313 acceptance_rate = format!(
314 "{:.1}%",
315 step_scope.phase_scope().solver_scope().stats().acceptance_rate() * 100.0
316 ),
317 current_score = %last_step_score,
318 best_score = %best_score,
319 );
320 step_scope.phase_scope().solver_scope().report_progress();
321 last_progress_time = now;
322 last_progress_moves = local_moves_evaluated;
323 }
324 }
325
326 let m = self.arena.get(i).unwrap();
327
328 if !m.is_doable(step_scope.score_director()) {
329 step_scope
331 .phase_scope_mut()
332 .solver_scope_mut()
333 .stats_mut()
334 .record_move(false);
335 continue;
336 }
337
338 let move_score = {
343 let mut recording =
344 RecordingDirector::new(step_scope.score_director_mut());
345
346 m.do_move(&mut recording);
348
349 let score = recording.calculate_score();
351
352 recording.undo_changes();
354
355 score
356 };
357
358 step_scope
360 .phase_scope_mut()
361 .solver_scope_mut()
362 .stats_mut()
363 .record_score_calculation();
364
365 let accepted = self.acceptor.is_accepted(&last_step_score, &move_score);
367
368 step_scope
370 .phase_scope_mut()
371 .solver_scope_mut()
372 .stats_mut()
373 .record_move(accepted);
374
375 trace!(
376 event = "step",
377 step = step_scope.step_index(),
378 move_index = i,
379 score = %move_score,
380 accepted = accepted,
381 );
382
383 if accepted {
385 self.forager.add_move_index(i, move_score);
386 }
387
388 if self.forager.is_quit_early() {
390 break;
391 }
392 }
393 }
394 }
395
396 if interrupted_step {
397 match settle_search_interrupt(&mut step_scope) {
398 StepInterrupt::Restart => continue,
399 StepInterrupt::TerminatePhase => break,
400 }
401 }
402
403 if let Some((selected_index, selected_score)) = self.forager.pick_move_index() {
405 self.arena
410 .get(selected_index)
411 .unwrap()
412 .do_move(step_scope.score_director_mut());
413 step_scope.set_step_score(selected_score);
414
415 last_step_score = selected_score;
417
418 step_scope.phase_scope_mut().update_best_solution();
420 }
421 self.acceptor.step_ended(&last_step_score);
432
433 step_scope.complete();
434 }
435
436 self.acceptor.phase_ended();
438
439 let duration = start_time.elapsed();
440 let steps = phase_scope.step_count();
441 let secs = duration.as_secs_f64();
442 let speed = if secs > 0.0 {
443 (local_moves_evaluated as f64 / secs) as u64
444 } else {
445 0
446 };
447
448 let stats = phase_scope.solver_scope().stats();
449 let acceptance_rate = stats.acceptance_rate() * 100.0;
450 let calc_speed = if secs > 0.0 {
451 (stats.score_calculations as f64 / secs) as u64
452 } else {
453 0
454 };
455
456 let best_score_str = phase_scope
457 .solver_scope()
458 .best_score()
459 .map(|s| format!("{}", s))
460 .unwrap_or_else(|| "none".to_string());
461
462 info!(
463 event = "phase_end",
464 phase = "Local Search",
465 phase_index = phase_index,
466 duration_ms = duration.as_millis() as u64,
467 steps = steps,
468 moves_evaluated = stats.moves_evaluated,
469 moves_accepted = stats.moves_accepted,
470 score_calculations = stats.score_calculations,
471 moves_speed = speed,
472 calc_speed = calc_speed,
473 acceptance_rate = format!("{:.1}%", acceptance_rate),
474 score = best_score_str,
475 );
476 }
477
478 fn phase_type_name(&self) -> &'static str {
479 "LocalSearch"
480 }
481}
482
483#[cfg(test)]
484mod tests {
485 use super::*;
486 use crate::heuristic::selector::ChangeMoveSelector;
487 use crate::phase::localsearch::{AcceptedCountForager, HillClimbingAcceptor};
488 use crate::test_utils::{
489 create_nqueens_director, get_queen_row, set_queen_row, NQueensSolution,
490 };
491 type NQueensMove = crate::heuristic::r#move::ChangeMove<NQueensSolution, i64>;
492
493 fn create_move_selector(
494 values: Vec<i64>,
495 ) -> ChangeMoveSelector<
496 NQueensSolution,
497 i64,
498 crate::heuristic::selector::FromSolutionEntitySelector,
499 crate::heuristic::selector::StaticValueSelector<NQueensSolution, i64>,
500 > {
501 ChangeMoveSelector::simple(get_queen_row, set_queen_row, 0, "row", values)
502 }
503
504 #[test]
505 fn test_local_search_hill_climbing() {
506 let director = create_nqueens_director(&[0, 0, 0, 0]);
507 let mut solver_scope = SolverScope::new(director);
508 solver_scope.start_solving();
509
510 let initial_score = solver_scope.calculate_score();
511
512 let values: Vec<i64> = (0..4).collect();
513 let move_selector = create_move_selector(values);
514 let acceptor = HillClimbingAcceptor::new();
515 let forager: AcceptedCountForager<_> = AcceptedCountForager::new(1);
516 let mut phase: LocalSearchPhase<_, NQueensMove, _, _, _> =
517 LocalSearchPhase::new(move_selector, acceptor, forager, Some(100));
518
519 phase.solve(&mut solver_scope);
520
521 let final_score = solver_scope.best_score().copied().unwrap_or(initial_score);
522 assert!(final_score >= initial_score);
523
524 assert!(solver_scope.stats().moves_evaluated > 0);
526 }
527
528 #[test]
529 fn test_local_search_reaches_optimal() {
530 let director = create_nqueens_director(&[0, 2, 1, 3]);
531 let mut solver_scope = SolverScope::new(director);
532 solver_scope.start_solving();
533
534 let initial_score = solver_scope.calculate_score();
535
536 let values: Vec<i64> = (0..4).collect();
537 let move_selector = create_move_selector(values);
538 let acceptor = HillClimbingAcceptor::new();
539 let forager: AcceptedCountForager<_> = AcceptedCountForager::new(1);
540 let mut phase: LocalSearchPhase<_, NQueensMove, _, _, _> =
541 LocalSearchPhase::new(move_selector, acceptor, forager, Some(50));
542
543 phase.solve(&mut solver_scope);
544
545 let final_score = solver_scope.best_score().copied().unwrap_or(initial_score);
546 assert!(final_score >= initial_score);
547 }
548
549 #[test]
550 fn test_local_search_step_limit() {
551 let director = create_nqueens_director(&[0, 0, 0, 0]);
552 let mut solver_scope = SolverScope::new(director);
553 solver_scope.start_solving();
554
555 let values: Vec<i64> = (0..4).collect();
556 let move_selector = create_move_selector(values);
557 let acceptor = HillClimbingAcceptor::new();
558 let forager: AcceptedCountForager<_> = AcceptedCountForager::new(1);
559 let mut phase: LocalSearchPhase<_, NQueensMove, _, _, _> =
560 LocalSearchPhase::new(move_selector, acceptor, forager, Some(3));
561
562 phase.solve(&mut solver_scope);
563
564 assert!(solver_scope.stats().step_count <= 3);
566 }
567}