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 self.arena
158 .extend(self.move_selector.iter_moves(step_scope.score_director()));
159
160 self.arena.shuffle(&mut rng);
162
163 for i in 0..self.arena.len() {
165 local_moves_evaluated += 1;
166
167 if local_moves_evaluated & 0x1FFF == 0 {
169 let now = Instant::now();
170 if now.duration_since(last_progress_time).as_secs() >= 1 {
171 let moves_delta = local_moves_evaluated - last_progress_moves;
172 let elapsed_secs = now.duration_since(last_progress_time).as_secs_f64();
173 let current_speed = (moves_delta as f64 / elapsed_secs) as u64;
174 debug!(
175 event = "progress",
176 steps = step_scope.step_index(),
177 moves_evaluated = local_moves_evaluated,
178 speed = current_speed,
179 score = %last_step_score,
180 );
181 last_progress_time = now;
182 last_progress_moves = local_moves_evaluated;
183 }
184 }
185
186 let m = self.arena.get(i).unwrap();
187
188 if !m.is_doable(step_scope.score_director()) {
189 step_scope
191 .phase_scope_mut()
192 .solver_scope_mut()
193 .stats_mut()
194 .record_move(false);
195 continue;
196 }
197
198 let move_score = {
202 let mut recording =
203 RecordingScoreDirector::new(step_scope.score_director_mut());
204
205 m.do_move(&mut recording);
207
208 let score = recording.calculate_score();
210
211 recording.undo_changes();
213
214 score
215 };
216
217 step_scope
219 .phase_scope_mut()
220 .solver_scope_mut()
221 .stats_mut()
222 .record_score_calculation();
223
224 let accepted = self.acceptor.is_accepted(&last_step_score, &move_score);
226
227 step_scope
229 .phase_scope_mut()
230 .solver_scope_mut()
231 .stats_mut()
232 .record_move(accepted);
233
234 trace!(
235 event = "step",
236 step = step_scope.step_index(),
237 move_index = i,
238 score = %move_score,
239 accepted = accepted,
240 );
241
242 if accepted {
244 self.forager.add_move_index(i, move_score);
245 }
246
247 if self.forager.is_quit_early() {
249 break;
250 }
251 }
252
253 if let Some((selected_index, selected_score)) = self.forager.pick_move_index() {
255 self.arena
259 .get(selected_index)
260 .unwrap()
261 .do_move(step_scope.score_director_mut());
262 step_scope.set_step_score(selected_score);
263
264 last_step_score = selected_score;
266
267 step_scope.phase_scope_mut().update_best_solution();
269 }
270 self.acceptor.step_ended(&last_step_score);
279
280 step_scope.complete();
281 }
282
283 self.acceptor.phase_ended();
285
286 let duration = start_time.elapsed();
287 let steps = phase_scope.step_count();
288 let secs = duration.as_secs_f64();
289 let speed = if secs > 0.0 {
290 (local_moves_evaluated as f64 / secs) as u64
291 } else {
292 0
293 };
294
295 let stats = phase_scope.solver_scope().stats();
296 let acceptance_rate = stats.acceptance_rate() * 100.0;
297 let calc_speed = if secs > 0.0 {
298 (stats.score_calculations as f64 / secs) as u64
299 } else {
300 0
301 };
302
303 let best_score_str = phase_scope
304 .solver_scope()
305 .best_score()
306 .map(|s| format!("{}", s))
307 .unwrap_or_else(|| "none".to_string());
308
309 info!(
310 event = "phase_end",
311 phase = "Local Search",
312 phase_index = phase_index,
313 duration_ms = duration.as_millis() as u64,
314 steps = steps,
315 moves_speed = speed,
316 calc_speed = calc_speed,
317 acceptance_rate = format!("{:.1}%", acceptance_rate),
318 score = best_score_str,
319 );
320 }
321
322 fn phase_type_name(&self) -> &'static str {
323 "LocalSearch"
324 }
325}
326
327#[cfg(test)]
328mod tests {
329 use super::*;
330 use crate::heuristic::selector::ChangeMoveSelector;
331 use crate::phase::localsearch::{AcceptedCountForager, HillClimbingAcceptor};
332 use crate::test_utils::{
333 create_nqueens_director, get_queen_row, set_queen_row, NQueensSolution,
334 };
335 use solverforge_core::score::SimpleScore;
336
337 type NQueensMove = crate::heuristic::r#move::ChangeMove<NQueensSolution, i64>;
338
339 fn create_move_selector(
340 values: Vec<i64>,
341 ) -> ChangeMoveSelector<
342 NQueensSolution,
343 i64,
344 crate::heuristic::selector::FromSolutionEntitySelector,
345 crate::heuristic::selector::StaticTypedValueSelector<NQueensSolution, i64>,
346 > {
347 ChangeMoveSelector::simple(get_queen_row, set_queen_row, 0, "row", values)
348 }
349
350 #[test]
351 fn test_local_search_hill_climbing() {
352 let director = create_nqueens_director(&[0, 0, 0, 0]);
353 let mut solver_scope = SolverScope::new(director);
354 solver_scope.start_solving();
355
356 let initial_score = solver_scope.calculate_score();
357 assert!(initial_score < SimpleScore::of(0));
358
359 let values: Vec<i64> = (0..4).collect();
360 let move_selector = create_move_selector(values);
361 let acceptor = HillClimbingAcceptor::new();
362 let forager: AcceptedCountForager<_> = AcceptedCountForager::new(1);
363 let mut phase: LocalSearchPhase<_, NQueensMove, _, _, _> =
364 LocalSearchPhase::new(move_selector, acceptor, forager, Some(100));
365
366 phase.solve(&mut solver_scope);
367
368 let final_score = solver_scope.best_score().copied().unwrap_or(initial_score);
369 assert!(final_score >= initial_score);
370
371 assert!(solver_scope.stats().moves_evaluated > 0);
373 }
374
375 #[test]
376 fn test_local_search_reaches_optimal() {
377 let director = create_nqueens_director(&[0, 2, 1, 3]);
378 let mut solver_scope = SolverScope::new(director);
379 solver_scope.start_solving();
380
381 let initial_score = solver_scope.calculate_score();
382
383 let values: Vec<i64> = (0..4).collect();
384 let move_selector = create_move_selector(values);
385 let acceptor = HillClimbingAcceptor::new();
386 let forager: AcceptedCountForager<_> = AcceptedCountForager::new(1);
387 let mut phase: LocalSearchPhase<_, NQueensMove, _, _, _> =
388 LocalSearchPhase::new(move_selector, acceptor, forager, Some(50));
389
390 phase.solve(&mut solver_scope);
391
392 let final_score = solver_scope.best_score().copied().unwrap_or(initial_score);
393 assert!(final_score >= initial_score);
394 }
395
396 #[test]
397 fn test_local_search_step_limit() {
398 let director = create_nqueens_director(&[0, 0, 0, 0]);
399 let mut solver_scope = SolverScope::new(director);
400 solver_scope.start_solving();
401
402 let values: Vec<i64> = (0..4).collect();
403 let move_selector = create_move_selector(values);
404 let acceptor = HillClimbingAcceptor::new();
405 let forager: AcceptedCountForager<_> = AcceptedCountForager::new(1);
406 let mut phase: LocalSearchPhase<_, NQueensMove, _, _, _> =
407 LocalSearchPhase::new(move_selector, acceptor, forager, Some(3));
408
409 phase.solve(&mut solver_scope);
410
411 assert!(solver_scope.stats().step_count <= 3);
413 }
414}