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