solverforge_solver/phase/localsearch/
phase.rs1use std::fmt::Debug;
4use std::marker::PhantomData;
5use std::time::Instant;
6
7use solverforge_core::domain::PlanningSolution;
8use solverforge_scoring::{Director, RecordingDirector};
9use tracing::{debug, info, trace};
10
11use crate::heuristic::r#move::{Move, MoveArena};
12use crate::heuristic::selector::MoveSelector;
13use crate::phase::control::{
14 append_interruptibly, settle_search_interrupt, should_interrupt_evaluation, StepInterrupt,
15};
16use crate::phase::localsearch::{Acceptor, LocalSearchForager};
17use crate::phase::Phase;
18use crate::scope::ProgressCallback;
19use crate::scope::{PhaseScope, SolverScope, StepScope};
20
21pub struct LocalSearchPhase<S, M, MS, A, Fo>
42where
43 S: PlanningSolution,
44 M: Move<S>,
45 MS: MoveSelector<S, M>,
46 A: Acceptor<S>,
47 Fo: LocalSearchForager<S, M>,
48{
49 move_selector: MS,
50 acceptor: A,
51 forager: Fo,
52 arena: MoveArena<M>,
53 step_limit: Option<u64>,
54 _phantom: PhantomData<fn() -> (S, M)>,
55}
56
57impl<S, M, MS, A, Fo> LocalSearchPhase<S, M, MS, A, Fo>
58where
59 S: PlanningSolution,
60 M: Move<S> + 'static,
61 MS: MoveSelector<S, M>,
62 A: Acceptor<S>,
63 Fo: LocalSearchForager<S, M>,
64{
65 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: Director<S>,
100 BestCb: ProgressCallback<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 loop {
127 if phase_scope.solver_scope_mut().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
146 .phase_scope()
147 .solver_scope()
148 .best_score()
149 .copied()
150 .unwrap_or(last_step_score);
151 self.forager.step_started(best_score, last_step_score);
152 self.acceptor.step_started();
153
154 self.arena.reset();
159 let mut interrupted_step = false;
160
161 if self.move_selector.is_never_ending() {
162 'streaming: loop {
168 let batch_start = self.arena.len();
169 let generation_interrupted = {
170 let iter = self.move_selector.iter_moves(step_scope.score_director());
171 append_interruptibly(&step_scope, &mut self.arena, iter.take(64))
172 };
173 if generation_interrupted {
174 interrupted_step = true;
175 break;
176 }
177 let batch_end = self.arena.len();
178 if batch_end == batch_start {
179 break;
180 }
181
182 for i in batch_start..batch_end {
183 if should_interrupt_evaluation(&step_scope, i) {
184 interrupted_step = true;
185 break 'streaming;
186 }
187
188 local_moves_evaluated += 1;
189
190 if local_moves_evaluated & 0x1FFF == 0 {
192 let now = Instant::now();
193 if now.duration_since(last_progress_time).as_secs() >= 1 {
194 let moves_delta = local_moves_evaluated - last_progress_moves;
195 let elapsed_secs =
196 now.duration_since(last_progress_time).as_secs_f64();
197 let current_speed = (moves_delta as f64 / elapsed_secs) as u64;
198 debug!(
199 event = "progress",
200 steps = step_scope.step_index(),
201 moves_evaluated = local_moves_evaluated,
202 moves_accepted = step_scope.phase_scope().solver_scope().stats().moves_accepted,
203 score_calculations = step_scope.phase_scope().solver_scope().stats().score_calculations,
204 speed = current_speed,
205 acceptance_rate = format!(
206 "{:.1}%",
207 step_scope.phase_scope().solver_scope().stats().acceptance_rate() * 100.0
208 ),
209 current_score = %last_step_score,
210 best_score = %best_score,
211 );
212 step_scope.phase_scope().solver_scope().report_progress();
213 last_progress_time = now;
214 last_progress_moves = local_moves_evaluated;
215 }
216 }
217
218 let m = self.arena.get(i).unwrap();
219
220 if !m.is_doable(step_scope.score_director()) {
221 step_scope
222 .phase_scope_mut()
223 .solver_scope_mut()
224 .stats_mut()
225 .record_move(false);
226 continue;
227 }
228
229 let move_score = {
230 let mut recording =
231 RecordingDirector::new(step_scope.score_director_mut());
232 m.do_move(&mut recording);
233 let score = recording.calculate_score();
234 recording.undo_changes();
235 score
236 };
237
238 step_scope
239 .phase_scope_mut()
240 .solver_scope_mut()
241 .stats_mut()
242 .record_score_calculation();
243
244 let accepted = self.acceptor.is_accepted(&last_step_score, &move_score);
245
246 step_scope
247 .phase_scope_mut()
248 .solver_scope_mut()
249 .stats_mut()
250 .record_move(accepted);
251
252 trace!(
253 event = "step",
254 step = step_scope.step_index(),
255 move_index = i,
256 score = %move_score,
257 accepted = accepted,
258 );
259
260 if accepted {
261 self.forager.add_move_index(i, move_score);
262 }
263
264 if self.forager.is_quit_early() {
265 break 'streaming;
266 }
267 }
268 }
269 } else {
270 let generation_interrupted = {
272 let iter = self.move_selector.iter_moves(step_scope.score_director());
273 append_interruptibly(&step_scope, &mut self.arena, iter)
274 };
275 if generation_interrupted {
276 interrupted_step = true;
277 }
278
279 if !interrupted_step {
280 let rng = step_scope.phase_scope_mut().solver_scope_mut().rng();
282 self.arena.shuffle(rng);
283 }
284
285 if !interrupted_step {
286 for i in 0..self.arena.len() {
288 if should_interrupt_evaluation(&step_scope, i) {
289 interrupted_step = true;
290 break;
291 }
292
293 local_moves_evaluated += 1;
294
295 if local_moves_evaluated & 0x1FFF == 0 {
297 let now = Instant::now();
298 if now.duration_since(last_progress_time).as_secs() >= 1 {
299 let moves_delta = local_moves_evaluated - last_progress_moves;
300 let elapsed_secs =
301 now.duration_since(last_progress_time).as_secs_f64();
302 let current_speed = (moves_delta as f64 / elapsed_secs) as u64;
303 debug!(
304 event = "progress",
305 steps = step_scope.step_index(),
306 moves_evaluated = local_moves_evaluated,
307 moves_accepted = step_scope.phase_scope().solver_scope().stats().moves_accepted,
308 score_calculations = step_scope.phase_scope().solver_scope().stats().score_calculations,
309 speed = current_speed,
310 acceptance_rate = format!(
311 "{:.1}%",
312 step_scope.phase_scope().solver_scope().stats().acceptance_rate() * 100.0
313 ),
314 current_score = %last_step_score,
315 best_score = %best_score,
316 );
317 step_scope.phase_scope().solver_scope().report_progress();
318 last_progress_time = now;
319 last_progress_moves = local_moves_evaluated;
320 }
321 }
322
323 let m = self.arena.get(i).unwrap();
324
325 if !m.is_doable(step_scope.score_director()) {
326 step_scope
328 .phase_scope_mut()
329 .solver_scope_mut()
330 .stats_mut()
331 .record_move(false);
332 continue;
333 }
334
335 let move_score = {
340 let mut recording =
341 RecordingDirector::new(step_scope.score_director_mut());
342
343 m.do_move(&mut recording);
345
346 let score = recording.calculate_score();
348
349 recording.undo_changes();
351
352 score
353 };
354
355 step_scope
357 .phase_scope_mut()
358 .solver_scope_mut()
359 .stats_mut()
360 .record_score_calculation();
361
362 let accepted = self.acceptor.is_accepted(&last_step_score, &move_score);
364
365 step_scope
367 .phase_scope_mut()
368 .solver_scope_mut()
369 .stats_mut()
370 .record_move(accepted);
371
372 trace!(
373 event = "step",
374 step = step_scope.step_index(),
375 move_index = i,
376 score = %move_score,
377 accepted = accepted,
378 );
379
380 if accepted {
382 self.forager.add_move_index(i, move_score);
383 }
384
385 if self.forager.is_quit_early() {
387 break;
388 }
389 }
390 }
391 }
392
393 if interrupted_step {
394 match settle_search_interrupt(&mut step_scope) {
395 StepInterrupt::Restart => continue,
396 StepInterrupt::TerminatePhase => break,
397 }
398 }
399
400 if let Some((selected_index, selected_score)) = self.forager.pick_move_index() {
402 self.arena
407 .get(selected_index)
408 .unwrap()
409 .do_move(step_scope.score_director_mut());
410 step_scope.set_step_score(selected_score);
411
412 last_step_score = selected_score;
414
415 step_scope.phase_scope_mut().update_best_solution();
417 }
418 self.acceptor.step_ended(&last_step_score);
429
430 step_scope.complete();
431 }
432
433 self.acceptor.phase_ended();
435
436 let duration = start_time.elapsed();
437 let steps = phase_scope.step_count();
438 let secs = duration.as_secs_f64();
439 let speed = if secs > 0.0 {
440 (local_moves_evaluated as f64 / secs) as u64
441 } else {
442 0
443 };
444
445 let stats = phase_scope.solver_scope().stats();
446 let acceptance_rate = stats.acceptance_rate() * 100.0;
447 let calc_speed = if secs > 0.0 {
448 (stats.score_calculations as f64 / secs) as u64
449 } else {
450 0
451 };
452
453 let best_score_str = phase_scope
454 .solver_scope()
455 .best_score()
456 .map(|s| format!("{}", s))
457 .unwrap_or_else(|| "none".to_string());
458
459 info!(
460 event = "phase_end",
461 phase = "Local Search",
462 phase_index = phase_index,
463 duration_ms = duration.as_millis() as u64,
464 steps = steps,
465 moves_evaluated = stats.moves_evaluated,
466 moves_accepted = stats.moves_accepted,
467 score_calculations = stats.score_calculations,
468 moves_speed = speed,
469 calc_speed = calc_speed,
470 acceptance_rate = format!("{:.1}%", acceptance_rate),
471 score = best_score_str,
472 );
473 }
474
475 fn phase_type_name(&self) -> &'static str {
476 "LocalSearch"
477 }
478}
479
480#[cfg(test)]
481#[path = "phase_tests.rs"]
482mod tests;