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 settle_search_interrupt, should_interrupt_evaluation, should_interrupt_generation,
15 StepInterrupt,
16};
17use crate::phase::localsearch::{Acceptor, LocalSearchForager};
18use crate::phase::Phase;
19use crate::scope::ProgressCallback;
20use crate::scope::{PhaseScope, SolverScope, StepScope};
21use crate::stats::{format_duration, whole_units_per_second};
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_generated: u64 = 0;
126 let mut local_moves_evaluated: u64 = 0;
127 let mut last_progress_time = Instant::now();
128 let mut last_progress_moves: u64 = 0;
129 loop {
130 if phase_scope.solver_scope_mut().should_terminate() {
132 break;
133 }
134
135 if let Some(limit) = self.step_limit {
137 if phase_scope.step_count() >= limit {
138 break;
139 }
140 }
141
142 let mut step_scope = StepScope::new(&mut phase_scope);
143
144 let best_score = step_scope
149 .phase_scope()
150 .solver_scope()
151 .best_score()
152 .copied()
153 .unwrap_or(last_step_score);
154 self.forager.step_started(best_score, last_step_score);
155 self.acceptor.step_started();
156
157 self.arena.reset();
158 let mut interrupted_step = false;
159 let mut generated_moves = 0usize;
160 let mut evaluated_moves = 0usize;
161 let generation_started = Instant::now();
162 let mut cursor = self.move_selector.open_cursor(step_scope.score_director());
163 step_scope
164 .phase_scope_mut()
165 .record_generation_time(generation_started.elapsed());
166
167 while !self.forager.is_quit_early() {
168 if should_interrupt_generation(&step_scope, generated_moves) {
169 interrupted_step = true;
170 break;
171 }
172
173 let generation_started = Instant::now();
174 let Some(mov) = cursor.next() else {
175 break;
176 };
177 let generation_elapsed = generation_started.elapsed();
178 generated_moves += 1;
179 local_moves_generated += 1;
180 step_scope
181 .phase_scope_mut()
182 .record_generated_move(generation_elapsed);
183
184 if should_interrupt_evaluation(&step_scope, evaluated_moves) {
185 interrupted_step = true;
186 break;
187 }
188 evaluated_moves += 1;
189 local_moves_evaluated += 1;
190
191 if local_moves_evaluated & 0x1FFF == 0 {
192 let now = Instant::now();
193 if now.duration_since(last_progress_time).as_secs() >= 1 {
194 let current_speed = whole_units_per_second(
195 local_moves_evaluated - last_progress_moves,
196 now.duration_since(last_progress_time),
197 );
198 debug!(
199 event = "progress",
200 steps = step_scope.step_index(),
201 moves_generated = local_moves_generated,
202 moves_evaluated = local_moves_evaluated,
203 moves_accepted = step_scope.phase_scope().solver_scope().stats().moves_accepted,
204 score_calculations = step_scope.phase_scope().solver_scope().stats().score_calculations,
205 speed = current_speed,
206 acceptance_rate = format!(
207 "{:.1}%",
208 step_scope.phase_scope().solver_scope().stats().acceptance_rate() * 100.0
209 ),
210 current_score = %last_step_score,
211 best_score = %best_score,
212 );
213 step_scope.phase_scope().solver_scope().report_progress();
214 last_progress_time = now;
215 last_progress_moves = local_moves_evaluated;
216 }
217 }
218
219 let evaluation_started = Instant::now();
220 if !mov.is_doable(step_scope.score_director()) {
221 step_scope
222 .phase_scope_mut()
223 .record_evaluated_move(evaluation_started.elapsed());
224 continue;
225 }
226
227 let move_score = {
228 let mut recording = RecordingDirector::new(step_scope.score_director_mut());
229 mov.do_move(&mut recording);
230 let score = recording.calculate_score();
231 recording.undo_changes();
232 score
233 };
234
235 step_scope.phase_scope_mut().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 .record_evaluated_move(evaluation_started.elapsed());
242 if accepted {
243 step_scope.phase_scope_mut().record_move_accepted();
244 }
245
246 trace!(
247 event = "step",
248 step = step_scope.step_index(),
249 move_index = evaluated_moves - 1,
250 score = %move_score,
251 accepted = accepted,
252 );
253
254 if accepted {
255 let accepted_index = self.arena.len();
256 self.arena.push(mov);
257 self.forager.add_move_index(accepted_index, move_score);
258 }
259 }
260
261 if interrupted_step {
262 match settle_search_interrupt(&mut step_scope) {
263 StepInterrupt::Restart => continue,
264 StepInterrupt::TerminatePhase => break,
265 }
266 }
267
268 if let Some((selected_index, selected_score)) = self.forager.pick_move_index() {
270 let selected_move = self.arena.take(selected_index);
271 selected_move.do_move(step_scope.score_director_mut());
272 step_scope.set_step_score(selected_score);
273
274 last_step_score = selected_score;
276
277 step_scope.phase_scope_mut().update_best_solution();
279 }
280 self.acceptor.step_ended(&last_step_score);
291
292 step_scope.complete();
293 }
294
295 self.acceptor.phase_ended();
297
298 let duration = start_time.elapsed();
299 let steps = phase_scope.step_count();
300 let stats = phase_scope.stats();
301 let speed = whole_units_per_second(stats.moves_evaluated, duration);
302 let acceptance_rate = stats.acceptance_rate() * 100.0;
303 let calc_speed = whole_units_per_second(stats.score_calculations, duration);
304
305 let best_score_str = phase_scope
306 .solver_scope()
307 .best_score()
308 .map(|s| format!("{}", s))
309 .unwrap_or_else(|| "none".to_string());
310
311 info!(
312 event = "phase_end",
313 phase = "Local Search",
314 phase_index = phase_index,
315 duration = %format_duration(duration),
316 steps = steps,
317 moves_generated = stats.moves_generated,
318 moves_evaluated = stats.moves_evaluated,
319 moves_accepted = stats.moves_accepted,
320 score_calculations = stats.score_calculations,
321 generation_time = %format_duration(stats.generation_time()),
322 evaluation_time = %format_duration(stats.evaluation_time()),
323 moves_speed = speed,
324 calc_speed = calc_speed,
325 acceptance_rate = format!("{:.1}%", acceptance_rate),
326 score = best_score_str,
327 );
328 }
329
330 fn phase_type_name(&self) -> &'static str {
331 "LocalSearch"
332 }
333}
334
335#[cfg(test)]
336#[path = "phase_tests.rs"]
337mod tests;