1use 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::{MoveCandidateRef, 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::hard_delta::{hard_score_delta, HardScoreDelta};
19use crate::phase::localsearch::{Acceptor, LocalSearchForager};
20use crate::phase::Phase;
21use crate::scope::ProgressCallback;
22use crate::scope::{PhaseScope, SolverScope, StepScope};
23use crate::stats::{format_duration, whole_units_per_second};
24
25pub struct LocalSearchPhase<S, M, MS, A, Fo>
46where
47 S: PlanningSolution,
48 M: Move<S>,
49 MS: MoveSelector<S, M>,
50 A: Acceptor<S>,
51 Fo: LocalSearchForager<S, M>,
52{
53 move_selector: MS,
54 acceptor: A,
55 forager: Fo,
56 step_limit: Option<u64>,
57 _phantom: PhantomData<fn() -> (S, M)>,
58}
59
60fn candidate_selector_label<S, M>(mov: &MoveCandidateRef<'_, S, M>) -> String
61where
62 S: PlanningSolution,
63 M: Move<S>,
64{
65 if mov.variable_name() == "compound_scalar" || mov.variable_name() == "conflict_repair" {
66 return mov.variable_name().to_string();
67 }
68 let mut label = None;
69 mov.for_each_affected_entity(&mut |affected| {
70 if label.is_none() {
71 label = Some(affected.variable_name.to_string());
72 }
73 });
74 label.unwrap_or_else(|| "move".to_string())
75}
76
77impl<S, M, MS, A, Fo> LocalSearchPhase<S, M, MS, A, Fo>
78where
79 S: PlanningSolution,
80 M: Move<S> + 'static,
81 MS: MoveSelector<S, M>,
82 A: Acceptor<S>,
83 Fo: LocalSearchForager<S, M>,
84{
85 pub fn new(move_selector: MS, acceptor: A, forager: Fo, step_limit: Option<u64>) -> Self {
86 Self {
87 move_selector,
88 acceptor,
89 forager,
90 step_limit,
91 _phantom: PhantomData,
92 }
93 }
94}
95
96impl<S, M, MS, A, Fo> Debug for LocalSearchPhase<S, M, MS, A, Fo>
97where
98 S: PlanningSolution,
99 M: Move<S>,
100 MS: MoveSelector<S, M> + Debug,
101 A: Acceptor<S> + Debug,
102 Fo: LocalSearchForager<S, M> + Debug,
103{
104 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
105 f.debug_struct("LocalSearchPhase")
106 .field("move_selector", &self.move_selector)
107 .field("acceptor", &self.acceptor)
108 .field("forager", &self.forager)
109 .field("step_limit", &self.step_limit)
110 .finish()
111 }
112}
113
114impl<S, D, BestCb, M, MS, A, Fo> Phase<S, D, BestCb> for LocalSearchPhase<S, M, MS, A, Fo>
115where
116 S: PlanningSolution,
117 D: Director<S>,
118 BestCb: ProgressCallback<S>,
119 M: Move<S>,
120 MS: MoveSelector<S, M>,
121 A: Acceptor<S>,
122 Fo: LocalSearchForager<S, M>,
123{
124 fn solve(&mut self, solver_scope: &mut SolverScope<S, D, BestCb>) {
125 let mut phase_scope = PhaseScope::new(solver_scope, 0);
126 let phase_index = phase_scope.phase_index();
127
128 let mut last_step_score = phase_scope.calculate_score();
130
131 info!(
132 event = "phase_start",
133 phase = "Local Search",
134 phase_index = phase_index,
135 score = %last_step_score,
136 );
137
138 self.acceptor.phase_started(&last_step_score);
140
141 let start_time = Instant::now();
142 let mut local_moves_generated: u64 = 0;
143 let mut local_moves_evaluated: u64 = 0;
144 let mut last_progress_time = Instant::now();
145 let mut last_progress_moves: u64 = 0;
146 loop {
147 if phase_scope.solver_scope_mut().should_terminate() {
149 break;
150 }
151
152 if let Some(limit) = self.step_limit {
154 if phase_scope.step_count() >= limit {
155 break;
156 }
157 }
158
159 let mut step_scope = StepScope::new(&mut phase_scope);
160
161 let best_score = step_scope
166 .phase_scope()
167 .solver_scope()
168 .best_score()
169 .copied()
170 .unwrap_or(last_step_score);
171 self.forager.step_started(best_score, last_step_score);
172 self.acceptor.step_started();
173 let requires_move_signatures = self.acceptor.requires_move_signatures();
174
175 let mut interrupted_step = false;
176 let mut generated_moves = 0usize;
177 let mut evaluated_moves = 0usize;
178 let mut accepted_moves_this_step = 0u64;
179 let generation_started = Instant::now();
180 let mut cursor = self.move_selector.open_cursor(step_scope.score_director());
181 step_scope
182 .phase_scope_mut()
183 .record_generation_time(generation_started.elapsed());
184
185 while !self.forager.is_quit_early() {
186 if should_interrupt_generation(&step_scope, generated_moves) {
187 interrupted_step = true;
188 break;
189 }
190
191 let generation_started = Instant::now();
192 let Some(candidate_id) = cursor.next_candidate() else {
193 break;
194 };
195 let selector_index = cursor.selector_index(candidate_id);
196 let mov = cursor
197 .candidate(candidate_id)
198 .expect("discovered candidate id must remain borrowable");
199 let selector_label = selector_index.map(|_| candidate_selector_label(&mov));
200 let generation_elapsed = generation_started.elapsed();
201 generated_moves += 1;
202 local_moves_generated += 1;
203 if let Some(selector_index) = selector_index {
204 step_scope
205 .phase_scope_mut()
206 .record_selector_generated_move_with_label(
207 selector_index,
208 selector_label.as_deref().unwrap_or("selector"),
209 generation_elapsed,
210 );
211 } else {
212 step_scope
213 .phase_scope_mut()
214 .record_generated_move(generation_elapsed);
215 }
216
217 if should_interrupt_evaluation(&step_scope, evaluated_moves) {
218 interrupted_step = true;
219 break;
220 }
221 evaluated_moves += 1;
222 local_moves_evaluated += 1;
223
224 if local_moves_evaluated & 0x1FFF == 0 {
225 let now = Instant::now();
226 if now.duration_since(last_progress_time).as_secs() >= 1 {
227 let current_speed = whole_units_per_second(
228 local_moves_evaluated - last_progress_moves,
229 now.duration_since(last_progress_time),
230 );
231 debug!(
232 event = "progress",
233 steps = step_scope.step_index(),
234 moves_generated = local_moves_generated,
235 moves_evaluated = local_moves_evaluated,
236 moves_accepted = step_scope.phase_scope().solver_scope().stats().moves_accepted,
237 score_calculations = step_scope.phase_scope().solver_scope().stats().score_calculations,
238 speed = current_speed,
239 acceptance_rate = format!(
240 "{:.1}%",
241 step_scope.phase_scope().solver_scope().stats().acceptance_rate() * 100.0
242 ),
243 current_score = %last_step_score,
244 best_score = %best_score,
245 );
246 step_scope.phase_scope().solver_scope().report_progress();
247 last_progress_time = now;
248 last_progress_moves = local_moves_evaluated;
249 }
250 }
251
252 let evaluation_started = Instant::now();
253 if !mov.is_doable(step_scope.score_director()) {
254 if let Some(selector_index) = selector_index {
255 step_scope.phase_scope_mut().record_selector_evaluated_move(
256 selector_index,
257 evaluation_started.elapsed(),
258 );
259 step_scope
260 .phase_scope_mut()
261 .record_selector_move_not_doable(selector_index);
262 } else {
263 step_scope
264 .phase_scope_mut()
265 .record_evaluated_move(evaluation_started.elapsed());
266 step_scope.phase_scope_mut().record_move_not_doable();
267 }
268 continue;
269 }
270
271 let move_score = {
272 let mut recording = RecordingDirector::new(step_scope.score_director_mut());
273 mov.do_move(&mut recording);
274 let score = recording.calculate_score();
275 recording.undo_changes();
276 score
277 };
278
279 step_scope.phase_scope_mut().record_score_calculation();
280
281 let hard_delta = hard_score_delta(last_step_score, move_score);
282 match hard_delta {
283 Some(HardScoreDelta::Improving) => {
284 step_scope.phase_scope_mut().record_move_hard_improving();
285 }
286 Some(HardScoreDelta::Neutral) => {
287 step_scope.phase_scope_mut().record_move_hard_neutral();
288 }
289 Some(HardScoreDelta::Worse) => {
290 step_scope.phase_scope_mut().record_move_hard_worse();
291 }
292 None => {}
293 }
294
295 if mov.requires_hard_improvement() && hard_delta != Some(HardScoreDelta::Improving)
296 {
297 if let Some(selector_index) = selector_index {
298 step_scope.phase_scope_mut().record_selector_evaluated_move(
299 selector_index,
300 evaluation_started.elapsed(),
301 );
302 step_scope
303 .phase_scope_mut()
304 .record_selector_move_acceptor_rejected(selector_index);
305 } else {
306 step_scope
307 .phase_scope_mut()
308 .record_evaluated_move(evaluation_started.elapsed());
309 step_scope.phase_scope_mut().record_move_acceptor_rejected();
310 }
311 continue;
312 }
313
314 let move_signature = if requires_move_signatures {
315 Some(mov.tabu_signature(step_scope.score_director()))
316 } else {
317 None
318 };
319
320 let accepted = self.acceptor.is_accepted(
321 &last_step_score,
322 &move_score,
323 move_signature.as_ref(),
324 );
325
326 if let Some(selector_index) = selector_index {
327 step_scope.phase_scope_mut().record_selector_evaluated_move(
328 selector_index,
329 evaluation_started.elapsed(),
330 );
331 } else {
332 step_scope
333 .phase_scope_mut()
334 .record_evaluated_move(evaluation_started.elapsed());
335 }
336 if accepted {
337 if let Some(selector_index) = selector_index {
338 step_scope
339 .phase_scope_mut()
340 .record_selector_move_accepted(selector_index);
341 } else {
342 step_scope.phase_scope_mut().record_move_accepted();
343 }
344 accepted_moves_this_step += 1;
345 } else if let Some(selector_index) = selector_index {
346 step_scope
347 .phase_scope_mut()
348 .record_selector_move_acceptor_rejected(selector_index);
349 } else {
350 step_scope.phase_scope_mut().record_move_acceptor_rejected();
351 }
352
353 trace!(
354 event = "step",
355 step = step_scope.step_index(),
356 move_index = candidate_id.index(),
357 score = %move_score,
358 accepted = accepted,
359 );
360
361 if accepted {
362 self.forager.add_move_index(candidate_id, move_score);
363 }
364 }
365
366 if interrupted_step {
367 match settle_search_interrupt(&mut step_scope) {
368 StepInterrupt::Restart => continue,
369 StepInterrupt::TerminatePhase => break,
370 }
371 }
372
373 let mut accepted_move_signature = None;
375 if let Some((selected_index, selected_score)) = self.forager.pick_move_index() {
376 let selector_index = cursor.selector_index(selected_index);
377 let selected_move = cursor.take_candidate(selected_index);
378 if requires_move_signatures {
379 accepted_move_signature =
380 Some(selected_move.tabu_signature(step_scope.score_director()));
381 }
382 step_scope.apply_committed_move(&selected_move);
383 if let Some(selector_index) = selector_index {
384 step_scope
385 .phase_scope_mut()
386 .record_selector_move_applied(selector_index);
387 } else {
388 step_scope.phase_scope_mut().record_move_applied();
389 }
390 step_scope.set_step_score(selected_score);
391
392 last_step_score = selected_score;
394
395 step_scope.phase_scope_mut().update_best_solution();
397 if accepted_moves_this_step > 1 {
398 step_scope
399 .phase_scope_mut()
400 .record_moves_forager_ignored(accepted_moves_this_step - 1);
401 }
402 } else if accepted_moves_this_step > 0 {
403 step_scope
404 .phase_scope_mut()
405 .record_moves_forager_ignored(accepted_moves_this_step);
406 }
407 self.acceptor
418 .step_ended(&last_step_score, accepted_move_signature.as_ref());
419
420 step_scope.complete();
421 }
422
423 self.acceptor.phase_ended();
425
426 let duration = start_time.elapsed();
427 let steps = phase_scope.step_count();
428 let stats = phase_scope.stats();
429 let speed = whole_units_per_second(stats.moves_evaluated, duration);
430 let acceptance_rate = stats.acceptance_rate() * 100.0;
431 let calc_speed = whole_units_per_second(stats.score_calculations, duration);
432
433 let best_score_str = phase_scope
434 .solver_scope()
435 .best_score()
436 .map(|s| format!("{}", s))
437 .unwrap_or_else(|| "none".to_string());
438
439 info!(
440 event = "phase_end",
441 phase = "Local Search",
442 phase_index = phase_index,
443 duration = %format_duration(duration),
444 steps = steps,
445 moves_generated = stats.moves_generated,
446 moves_evaluated = stats.moves_evaluated,
447 moves_accepted = stats.moves_accepted,
448 score_calculations = stats.score_calculations,
449 generation_time = %format_duration(stats.generation_time()),
450 evaluation_time = %format_duration(stats.evaluation_time()),
451 moves_speed = speed,
452 calc_speed = calc_speed,
453 acceptance_rate = format!("{:.1}%", acceptance_rate),
454 score = best_score_str,
455 );
456 }
457
458 fn phase_type_name(&self) -> &'static str {
459 "LocalSearch"
460 }
461}
462
463#[cfg(test)]
464mod tests;