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;
12use crate::heuristic::selector::move_selector::MoveCursor;
13use crate::heuristic::selector::MoveSelector;
14use crate::phase::control::{
15 settle_search_interrupt, should_interrupt_evaluation, should_interrupt_generation,
16 StepInterrupt,
17};
18use crate::phase::localsearch::{Acceptor, LocalSearchForager};
19use crate::phase::Phase;
20use crate::scope::ProgressCallback;
21use crate::scope::{PhaseScope, SolverScope, StepScope};
22use crate::stats::{format_duration, whole_units_per_second};
23
24pub struct LocalSearchPhase<S, M, MS, A, Fo>
45where
46 S: PlanningSolution,
47 M: Move<S>,
48 MS: MoveSelector<S, M>,
49 A: Acceptor<S>,
50 Fo: LocalSearchForager<S, M>,
51{
52 move_selector: MS,
53 acceptor: A,
54 forager: Fo,
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 step_limit,
73 _phantom: PhantomData,
74 }
75 }
76}
77
78impl<S, M, MS, A, Fo> Debug for LocalSearchPhase<S, M, MS, A, Fo>
79where
80 S: PlanningSolution,
81 M: Move<S>,
82 MS: MoveSelector<S, M> + Debug,
83 A: Acceptor<S> + Debug,
84 Fo: LocalSearchForager<S, M> + Debug,
85{
86 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
87 f.debug_struct("LocalSearchPhase")
88 .field("move_selector", &self.move_selector)
89 .field("acceptor", &self.acceptor)
90 .field("forager", &self.forager)
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 score = %last_step_score,
118 );
119
120 self.acceptor.phase_started(&last_step_score);
122
123 let start_time = Instant::now();
124 let mut local_moves_generated: u64 = 0;
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 loop {
129 if phase_scope.solver_scope_mut().should_terminate() {
131 break;
132 }
133
134 if let Some(limit) = self.step_limit {
136 if phase_scope.step_count() >= limit {
137 break;
138 }
139 }
140
141 let mut step_scope = StepScope::new(&mut phase_scope);
142
143 let best_score = step_scope
148 .phase_scope()
149 .solver_scope()
150 .best_score()
151 .copied()
152 .unwrap_or(last_step_score);
153 self.forager.step_started(best_score, last_step_score);
154 self.acceptor.step_started();
155 let requires_move_signatures = self.acceptor.requires_move_signatures();
156
157 let mut interrupted_step = false;
158 let mut generated_moves = 0usize;
159 let mut evaluated_moves = 0usize;
160 let generation_started = Instant::now();
161 let mut cursor = self.move_selector.open_cursor(step_scope.score_director());
162 step_scope
163 .phase_scope_mut()
164 .record_generation_time(generation_started.elapsed());
165
166 while !self.forager.is_quit_early() {
167 if should_interrupt_generation(&step_scope, generated_moves) {
168 interrupted_step = true;
169 break;
170 }
171
172 let generation_started = Instant::now();
173 let Some((candidate_index, mov)) = cursor.next_candidate() else {
174 break;
175 };
176 let generation_elapsed = generation_started.elapsed();
177 generated_moves += 1;
178 local_moves_generated += 1;
179 step_scope
180 .phase_scope_mut()
181 .record_generated_move(generation_elapsed);
182
183 if should_interrupt_evaluation(&step_scope, evaluated_moves) {
184 interrupted_step = true;
185 break;
186 }
187 evaluated_moves += 1;
188 local_moves_evaluated += 1;
189
190 if local_moves_evaluated & 0x1FFF == 0 {
191 let now = Instant::now();
192 if now.duration_since(last_progress_time).as_secs() >= 1 {
193 let current_speed = whole_units_per_second(
194 local_moves_evaluated - last_progress_moves,
195 now.duration_since(last_progress_time),
196 );
197 debug!(
198 event = "progress",
199 steps = step_scope.step_index(),
200 moves_generated = local_moves_generated,
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 evaluation_started = Instant::now();
219 if !mov.is_doable(step_scope.score_director()) {
220 step_scope
221 .phase_scope_mut()
222 .record_evaluated_move(evaluation_started.elapsed());
223 continue;
224 }
225
226 let move_score = {
227 let mut recording = RecordingDirector::new(step_scope.score_director_mut());
228 mov.do_move(&mut recording);
229 let score = recording.calculate_score();
230 recording.undo_changes();
231 score
232 };
233
234 step_scope.phase_scope_mut().record_score_calculation();
235
236 let move_signature = if requires_move_signatures {
237 Some(mov.tabu_signature(step_scope.score_director()))
238 } else {
239 None
240 };
241
242 let accepted = self.acceptor.is_accepted(
243 &last_step_score,
244 &move_score,
245 move_signature.as_ref(),
246 );
247
248 step_scope
249 .phase_scope_mut()
250 .record_evaluated_move(evaluation_started.elapsed());
251 if accepted {
252 step_scope.phase_scope_mut().record_move_accepted();
253 }
254
255 trace!(
256 event = "step",
257 step = step_scope.step_index(),
258 move_index = candidate_index,
259 score = %move_score,
260 accepted = accepted,
261 );
262
263 if accepted {
264 self.forager.add_move_index(candidate_index, move_score);
265 }
266 }
267
268 if interrupted_step {
269 match settle_search_interrupt(&mut step_scope) {
270 StepInterrupt::Restart => continue,
271 StepInterrupt::TerminatePhase => break,
272 }
273 }
274
275 let mut accepted_move_signature = None;
277 if let Some((selected_index, selected_score)) = self.forager.pick_move_index() {
278 let selected_move = cursor.take_candidate(selected_index);
279 if requires_move_signatures {
280 accepted_move_signature =
281 Some(selected_move.tabu_signature(step_scope.score_director()));
282 }
283 step_scope.apply_committed_move(&selected_move);
284 step_scope.set_step_score(selected_score);
285
286 last_step_score = selected_score;
288
289 step_scope.phase_scope_mut().update_best_solution();
291 }
292 self.acceptor
303 .step_ended(&last_step_score, accepted_move_signature.as_ref());
304
305 step_scope.complete();
306 }
307
308 self.acceptor.phase_ended();
310
311 let duration = start_time.elapsed();
312 let steps = phase_scope.step_count();
313 let stats = phase_scope.stats();
314 let speed = whole_units_per_second(stats.moves_evaluated, duration);
315 let acceptance_rate = stats.acceptance_rate() * 100.0;
316 let calc_speed = whole_units_per_second(stats.score_calculations, duration);
317
318 let best_score_str = phase_scope
319 .solver_scope()
320 .best_score()
321 .map(|s| format!("{}", s))
322 .unwrap_or_else(|| "none".to_string());
323
324 info!(
325 event = "phase_end",
326 phase = "Local Search",
327 phase_index = phase_index,
328 duration = %format_duration(duration),
329 steps = steps,
330 moves_generated = stats.moves_generated,
331 moves_evaluated = stats.moves_evaluated,
332 moves_accepted = stats.moves_accepted,
333 score_calculations = stats.score_calculations,
334 generation_time = %format_duration(stats.generation_time()),
335 evaluation_time = %format_duration(stats.evaluation_time()),
336 moves_speed = speed,
337 calc_speed = calc_speed,
338 acceptance_rate = format!("{:.1}%", acceptance_rate),
339 score = best_score_str,
340 );
341 }
342
343 fn phase_type_name(&self) -> &'static str {
344 "LocalSearch"
345 }
346}
347
348#[cfg(test)]
349mod tests;