1use std::fmt::Debug;
4use std::marker::PhantomData;
5use std::time::Instant;
6
7use rand::RngExt;
8use solverforge_core::domain::PlanningSolution;
9use solverforge_core::score::Score;
10use solverforge_scoring::Director;
11use tracing::{debug, info, trace};
12
13use crate::heuristic::r#move::Move;
14use crate::heuristic::selector::move_selector::{MoveCandidateRef, MoveCursor, MoveStreamContext};
15use crate::heuristic::selector::MoveSelector;
16use crate::phase::control::{
17 settle_search_interrupt, should_interrupt_after_step, should_interrupt_before_candidate,
18 should_interrupt_before_evaluation, StepInterrupt,
19};
20use crate::phase::localsearch::evaluation::{
21 evaluate_candidate, record_evaluated_move, CandidateEvaluation,
22};
23use crate::phase::localsearch::{Acceptor, LocalSearchForager};
24use crate::phase::Phase;
25use crate::scope::ProgressCallback;
26use crate::scope::{PhaseScope, SolverScope, StepScope};
27use crate::stats::{format_duration, whole_units_per_second, AppliedMoveTelemetry};
28
29const STEP_ACCEPTED_LABEL_LIMIT: usize = 32;
30
31#[derive(Clone, Copy)]
32struct StepMoveLabelCount {
33 label: &'static str,
34 count: u64,
35}
36
37struct StepMoveLabelCounts {
38 entries: [StepMoveLabelCount; STEP_ACCEPTED_LABEL_LIMIT],
39 overflow: u64,
40}
41
42impl StepMoveLabelCounts {
43 const EMPTY_ENTRY: StepMoveLabelCount = StepMoveLabelCount {
44 label: "",
45 count: 0,
46 };
47
48 fn new() -> Self {
49 Self {
50 entries: [Self::EMPTY_ENTRY; STEP_ACCEPTED_LABEL_LIMIT],
51 overflow: 0,
52 }
53 }
54
55 fn record(&mut self, label: &'static str) {
56 for entry in &mut self.entries {
57 if entry.count > 0 && entry.label == label {
58 entry.count += 1;
59 return;
60 }
61 }
62 for entry in &mut self.entries {
63 if entry.count == 0 {
64 entry.label = label;
65 entry.count = 1;
66 return;
67 }
68 }
69 self.overflow += 1;
70 }
71
72 fn for_each_ignored_except_selected(
73 &self,
74 selected_label: Option<&'static str>,
75 mut visitor: impl FnMut(&'static str, u64),
76 ) {
77 let mut selected_remaining = selected_label;
78 for entry in &self.entries {
79 if entry.count == 0 {
80 continue;
81 }
82 let ignored = if selected_remaining == Some(entry.label) {
83 selected_remaining = None;
84 entry.count.saturating_sub(1)
85 } else {
86 entry.count
87 };
88 if ignored > 0 {
89 visitor(entry.label, ignored);
90 }
91 }
92 if self.overflow > 0 {
93 visitor("move", self.overflow);
94 }
95 }
96}
97
98pub struct LocalSearchPhase<S, M, MS, A, Fo>
119where
120 S: PlanningSolution,
121 M: Move<S>,
122 MS: MoveSelector<S, M>,
123 A: Acceptor<S>,
124 Fo: LocalSearchForager<S, M>,
125{
126 move_selector: MS,
127 acceptor: A,
128 forager: Fo,
129 step_limit: Option<u64>,
130 _phantom: PhantomData<fn() -> (S, M)>,
131}
132
133fn candidate_selector_label<S, M>(mov: &MoveCandidateRef<'_, S, M>) -> String
134where
135 S: PlanningSolution,
136 M: Move<S>,
137{
138 if mov.variable_name() == "compound_scalar" || mov.variable_name() == "conflict_repair" {
139 return mov.variable_name().to_string();
140 }
141 let mut label = None;
142 mov.for_each_affected_entity(&mut |affected| {
143 if label.is_none() {
144 label = Some(affected.variable_name.to_string());
145 }
146 });
147 label.unwrap_or_else(|| "move".to_string())
148}
149
150impl<S, M, MS, A, Fo> LocalSearchPhase<S, M, MS, A, Fo>
151where
152 S: PlanningSolution,
153 M: Move<S> + 'static,
154 MS: MoveSelector<S, M>,
155 A: Acceptor<S>,
156 Fo: LocalSearchForager<S, M>,
157{
158 pub fn new(move_selector: MS, acceptor: A, forager: Fo, step_limit: Option<u64>) -> Self {
159 Self {
160 move_selector,
161 acceptor,
162 forager,
163 step_limit,
164 _phantom: PhantomData,
165 }
166 }
167}
168
169impl<S, M, MS, A, Fo> Debug for LocalSearchPhase<S, M, MS, A, Fo>
170where
171 S: PlanningSolution,
172 M: Move<S>,
173 MS: MoveSelector<S, M> + Debug,
174 A: Acceptor<S> + Debug,
175 Fo: LocalSearchForager<S, M> + Debug,
176{
177 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
178 f.debug_struct("LocalSearchPhase")
179 .field("move_selector", &self.move_selector)
180 .field("acceptor", &self.acceptor)
181 .field("forager", &self.forager)
182 .field("step_limit", &self.step_limit)
183 .finish()
184 }
185}
186
187impl<S, D, BestCb, M, MS, A, Fo> Phase<S, D, BestCb> for LocalSearchPhase<S, M, MS, A, Fo>
188where
189 S: PlanningSolution,
190 D: Director<S>,
191 BestCb: ProgressCallback<S>,
192 M: Move<S>,
193 MS: MoveSelector<S, M>,
194 A: Acceptor<S>,
195 Fo: LocalSearchForager<S, M>,
196{
197 fn solve(&mut self, solver_scope: &mut SolverScope<S, D, BestCb>) {
198 let mut phase_scope = PhaseScope::new(solver_scope, 0);
199 let phase_index = phase_scope.phase_index();
200
201 let mut last_step_score = phase_scope.calculate_score();
203
204 info!(
205 event = "phase_start",
206 phase = "Local Search",
207 phase_index = phase_index,
208 score = %last_step_score,
209 );
210
211 self.acceptor.phase_started(&last_step_score);
213
214 let start_time = Instant::now();
215 let mut local_moves_generated: u64 = 0;
216 let mut local_moves_evaluated: u64 = 0;
217 let mut last_progress_time = Instant::now();
218 let mut last_progress_moves: u64 = 0;
219 loop {
220 if phase_scope.solver_scope_mut().should_terminate() {
222 break;
223 }
224
225 if let Some(limit) = self.step_limit {
227 if phase_scope.step_count() >= limit {
228 break;
229 }
230 }
231
232 let mut step_scope = StepScope::new(&mut phase_scope);
233
234 let best_score = step_scope
239 .phase_scope()
240 .solver_scope()
241 .best_score()
242 .copied()
243 .unwrap_or(last_step_score);
244 self.forager.step_started(best_score, last_step_score);
245 self.acceptor.step_started();
246 let requires_move_signatures = self.acceptor.requires_move_signatures();
247
248 let mut interrupted_step = false;
249 let mut accepted_moves_this_step = 0u64;
250 let mut moves_generated_this_step = 0u64;
251 let mut moves_evaluated_this_step = 0u64;
252 let mut accepted_move_labels_this_step = StepMoveLabelCounts::new();
253 if should_interrupt_before_candidate(&step_scope) {
254 interrupted_step = true;
255 }
256 let generation_started = Instant::now();
257 let step_index = step_scope.step_index();
258 let stream_context = MoveStreamContext::new(
259 step_index,
260 step_scope
261 .phase_scope_mut()
262 .solver_scope_mut()
263 .rng()
264 .random::<u64>(),
265 self.forager.accepted_count_limit(),
266 );
267 let mut cursor = self
268 .move_selector
269 .open_cursor_with_context(step_scope.score_director(), stream_context);
270 step_scope
271 .phase_scope_mut()
272 .record_generation_time(generation_started.elapsed());
273
274 while !self.forager.is_quit_early() {
275 if interrupted_step || should_interrupt_before_candidate(&step_scope) {
276 interrupted_step = true;
277 break;
278 }
279 if step_scope
280 .phase_scope_mut()
281 .solver_scope_mut()
282 .should_terminate()
283 {
284 interrupted_step = true;
285 break;
286 }
287
288 let generation_started = Instant::now();
289 let Some(candidate_id) = cursor.next_candidate() else {
290 break;
291 };
292 let selector_index = cursor.selector_index(candidate_id);
293 let mov = cursor
294 .candidate(candidate_id)
295 .expect("discovered candidate id must remain borrowable");
296 let move_label = mov.telemetry_label();
297 let selector_label = selector_index.map(|_| candidate_selector_label(&mov));
298 let generation_elapsed = generation_started.elapsed();
299 local_moves_generated += 1;
300 moves_generated_this_step += 1;
301 step_scope
302 .phase_scope_mut()
303 .record_move_kind_generated(move_label);
304 if let Some(selector_index) = selector_index {
305 step_scope
306 .phase_scope_mut()
307 .record_selector_generated_move_with_label(
308 selector_index,
309 selector_label.as_deref().unwrap_or("selector"),
310 generation_elapsed,
311 );
312 } else {
313 step_scope
314 .phase_scope_mut()
315 .record_generated_move(generation_elapsed);
316 }
317
318 if should_interrupt_before_evaluation(&step_scope) {
319 interrupted_step = true;
320 break;
321 }
322 if step_scope
323 .phase_scope_mut()
324 .solver_scope_mut()
325 .should_terminate()
326 {
327 interrupted_step = true;
328 break;
329 }
330 local_moves_evaluated += 1;
331 moves_evaluated_this_step += 1;
332
333 if local_moves_evaluated & 0x1FFF == 0 {
334 let now = Instant::now();
335 if now.duration_since(last_progress_time).as_secs() >= 1 {
336 let current_speed = whole_units_per_second(
337 local_moves_evaluated - last_progress_moves,
338 now.duration_since(last_progress_time),
339 );
340 debug!(
341 event = "progress",
342 steps = step_scope.step_index(),
343 moves_generated = local_moves_generated,
344 moves_evaluated = local_moves_evaluated,
345 moves_accepted = step_scope.phase_scope().solver_scope().stats().moves_accepted,
346 score_calculations = step_scope.phase_scope().solver_scope().stats().score_calculations,
347 speed = current_speed,
348 acceptance_rate = format!(
349 "{:.1}%",
350 step_scope.phase_scope().solver_scope().stats().acceptance_rate() * 100.0
351 ),
352 current_score = %last_step_score,
353 best_score = %best_score,
354 );
355 step_scope.phase_scope().solver_scope().report_progress();
356 last_progress_time = now;
357 last_progress_moves = local_moves_evaluated;
358 }
359 }
360
361 let evaluation_started = Instant::now();
362 let move_score = match evaluate_candidate(
363 &mov,
364 &mut step_scope,
365 last_step_score,
366 selector_index,
367 evaluation_started,
368 ) {
369 CandidateEvaluation::Scored(score) => {
370 step_scope.phase_scope_mut().record_move_kind_evaluated(
371 move_label,
372 score.compare(&last_step_score),
373 );
374 score
375 }
376 CandidateEvaluation::NotDoable
377 | CandidateEvaluation::RejectedByHardImprovement(_) => continue,
378 };
379 let move_signature = if requires_move_signatures {
380 Some(mov.tabu_signature(step_scope.score_director()))
381 } else {
382 None
383 };
384
385 let accepted = self.acceptor.is_accepted(
386 &last_step_score,
387 &move_score,
388 move_signature.as_ref(),
389 );
390
391 record_evaluated_move(&mut step_scope, selector_index, evaluation_started);
392 if accepted {
393 step_scope
394 .phase_scope_mut()
395 .record_move_kind_accepted(move_label);
396 accepted_move_labels_this_step.record(move_label);
397 if let Some(selector_index) = selector_index {
398 step_scope
399 .phase_scope_mut()
400 .record_selector_move_accepted(selector_index);
401 } else {
402 step_scope.phase_scope_mut().record_move_accepted();
403 }
404 accepted_moves_this_step += 1;
405 } else if let Some(selector_index) = selector_index {
406 step_scope
407 .phase_scope_mut()
408 .record_selector_move_acceptor_rejected(selector_index);
409 step_scope
410 .phase_scope_mut()
411 .record_move_kind_acceptor_rejected(
412 move_label,
413 move_score.compare(&last_step_score),
414 );
415 } else {
416 step_scope.phase_scope_mut().record_move_acceptor_rejected();
417 step_scope
418 .phase_scope_mut()
419 .record_move_kind_acceptor_rejected(
420 move_label,
421 move_score.compare(&last_step_score),
422 );
423 }
424
425 trace!(
426 event = "step",
427 step = step_scope.step_index(),
428 move_index = candidate_id.index(),
429 score = %move_score,
430 accepted = accepted,
431 );
432
433 if accepted {
434 self.forager.add_move_index(candidate_id, move_score);
435 }
436 }
437
438 if !interrupted_step && should_interrupt_after_step(&step_scope) {
439 interrupted_step = true;
440 }
441
442 if interrupted_step {
443 match settle_search_interrupt(&mut step_scope) {
444 StepInterrupt::Restart => continue,
445 StepInterrupt::TerminatePhase => break,
446 }
447 }
448
449 let mut accepted_move_signature = None;
451 if let Some((selected_index, selected_score)) = self.forager.pick_move_index() {
452 let selector_index = cursor.selector_index(selected_index);
453 let selected_move = cursor
454 .candidate(selected_index)
455 .expect("selected candidate id must remain borrowable until commit");
456 let selected_move_label = selected_move.telemetry_label();
457 if requires_move_signatures {
458 accepted_move_signature =
459 Some(selected_move.tabu_signature(step_scope.score_director()));
460 }
461 let previous_score = last_step_score;
462 step_scope.apply_committed_move(&selected_move);
463 if let Some(selector_index) = selector_index {
464 step_scope
465 .phase_scope_mut()
466 .record_selector_move_applied(selector_index);
467 } else {
468 step_scope.phase_scope_mut().record_move_applied();
469 }
470 step_scope.set_step_score(selected_score);
471 let score_improvement =
472 if previous_score.is_feasible() && selected_score > previous_score {
473 selected_score.to_scalar() - previous_score.to_scalar()
474 } else {
475 0.0
476 };
477 step_scope
478 .phase_scope_mut()
479 .record_move_kind_applied(selected_move_label, score_improvement);
480 if step_scope.phase_scope().can_record_applied_move_trace() {
481 let score_before = previous_score.to_scalar();
482 let score_after = selected_score.to_scalar();
483 step_scope
484 .phase_scope_mut()
485 .record_applied_move_trace(AppliedMoveTelemetry {
486 step_index,
487 move_label: selected_move_label,
488 selected_candidate_index: selected_index.index(),
489 moves_generated_this_step,
490 moves_evaluated_this_step,
491 moves_accepted_this_step: accepted_moves_this_step,
492 moves_forager_ignored_this_step: accepted_moves_this_step
493 .saturating_sub(1),
494 score_before,
495 score_after,
496 score_delta: score_after - score_before,
497 hard_feasible_before: previous_score.is_feasible(),
498 hard_feasible_after: selected_score.is_feasible(),
499 });
500 }
501
502 last_step_score = selected_score;
504
505 step_scope.phase_scope_mut().update_best_solution();
507 if accepted_moves_this_step > 1 {
508 step_scope
509 .phase_scope_mut()
510 .record_moves_forager_ignored(accepted_moves_this_step - 1);
511 accepted_move_labels_this_step.for_each_ignored_except_selected(
512 Some(selected_move_label),
513 |label, count| {
514 step_scope
515 .phase_scope_mut()
516 .record_move_kind_forager_ignored(label, count);
517 },
518 );
519 }
520 } else if accepted_moves_this_step > 0 {
521 step_scope
522 .phase_scope_mut()
523 .record_moves_forager_ignored(accepted_moves_this_step);
524 accepted_move_labels_this_step.for_each_ignored_except_selected(
525 None,
526 |label, count| {
527 step_scope
528 .phase_scope_mut()
529 .record_move_kind_forager_ignored(label, count);
530 },
531 );
532 }
533 self.acceptor
544 .step_ended(&last_step_score, accepted_move_signature.as_ref());
545
546 step_scope.complete();
547 }
548
549 self.acceptor.phase_ended();
551
552 let duration = start_time.elapsed();
553 let steps = phase_scope.step_count();
554 let stats = phase_scope.stats();
555 let speed = whole_units_per_second(stats.moves_evaluated, duration);
556 let acceptance_rate = stats.acceptance_rate() * 100.0;
557 let calc_speed = whole_units_per_second(stats.score_calculations, duration);
558
559 let best_score_str = phase_scope
560 .solver_scope()
561 .best_score()
562 .map(|s| format!("{}", s))
563 .unwrap_or_else(|| "none".to_string());
564
565 info!(
566 event = "phase_end",
567 phase = "Local Search",
568 phase_index = phase_index,
569 duration = %format_duration(duration),
570 steps = steps,
571 moves_generated = stats.moves_generated,
572 moves_evaluated = stats.moves_evaluated,
573 moves_accepted = stats.moves_accepted,
574 score_calculations = stats.score_calculations,
575 generation_time = %format_duration(stats.generation_time()),
576 evaluation_time = %format_duration(stats.evaluation_time()),
577 moves_speed = speed,
578 calc_speed = calc_speed,
579 acceptance_rate = format!("{:.1}%", acceptance_rate),
580 score = best_score_str,
581 );
582 }
583
584 fn phase_type_name(&self) -> &'static str {
585 "LocalSearch"
586 }
587}
588
589#[cfg(test)]
590mod tests;