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