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::info;
10
11use crate::heuristic::r#move::Move;
12use crate::heuristic::selector::move_selector::{CandidateId, 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::vnd::telemetry::{candidate_selector_label, VndProgress};
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 VndPhase<T, M>(pub T, PhantomData<fn() -> M>);
74
75impl<T: Debug, M> Debug for VndPhase<T, M> {
76 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
77 f.debug_tuple("VndPhase").field(&self.0).finish()
78 }
79}
80
81impl<T, M> VndPhase<T, M> {
82 pub fn new(neighborhoods: T) -> Self {
83 Self(neighborhoods, PhantomData)
84 }
85}
86
87macro_rules! impl_vnd_phase {
89 ($idx:tt: $MS:ident) => {
91 impl<S, D, BestCb, M, $MS> Phase<S, D, BestCb> for VndPhase<($MS,), M>
92 where
93 S: PlanningSolution,
94 D: Director<S>,
95 BestCb: ProgressCallback<S>,
96 M: Move<S>,
97 $MS: MoveSelector<S, M>,
98 {
99 fn solve(&mut self, solver_scope: &mut SolverScope<S, D, BestCb>) {
100 let phase_name = "Variable Neighborhood Descent";
101 let mut phase_scope = PhaseScope::with_phase_type(solver_scope, 0, phase_name);
102 let phase_index = phase_scope.phase_index();
103 let mut current_score = phase_scope.calculate_score();
104 let mut progress = VndProgress::new();
105
106 info!(
107 event = "phase_start",
108 phase = phase_name,
109 phase_index = phase_index,
110 score = %current_score,
111 );
112 phase_scope.solver_scope().report_progress();
113
114 loop {
115 let mut step_scope = StepScope::new(&mut phase_scope);
116 let mut cursor = (self.0).$idx.open_cursor(step_scope.score_director());
117
118 match find_best_improving_move(&mut cursor, &mut step_scope, ¤t_score, &mut progress) {
119 MoveSearchResult::Found(selected_move, selected_score, selector_index) => {
120 step_scope.apply_committed_move(&selected_move);
121 if let Some(selector_index) = selector_index {
122 step_scope
123 .phase_scope_mut()
124 .record_selector_move_accepted(selector_index);
125 step_scope
126 .phase_scope_mut()
127 .record_selector_move_applied(selector_index);
128 } else {
129 step_scope.phase_scope_mut().record_move_accepted();
130 step_scope.phase_scope_mut().record_move_applied();
131 }
132 step_scope.set_step_score(selected_score);
133 current_score = selected_score;
134 step_scope.phase_scope_mut().update_best_solution();
135 step_scope.complete();
136 }
137 MoveSearchResult::NotFound => {
138 step_scope.complete();
139 break;
140 }
141 MoveSearchResult::Interrupted => match settle_search_interrupt(&mut step_scope) {
142 StepInterrupt::Restart => continue,
143 StepInterrupt::TerminatePhase => break,
144 },
145 }
146 }
147
148 phase_scope.solver_scope().report_progress();
149 let duration = phase_scope.elapsed();
150 let steps = phase_scope.step_count();
151 let speed = whole_units_per_second(progress.moves_evaluated(), duration);
152 let stats = phase_scope.stats();
153 info!(
154 event = "phase_end",
155 phase = phase_name,
156 phase_index = phase_index,
157 duration = %format_duration(duration),
158 steps = steps,
159 moves_generated = stats.moves_generated,
160 moves_evaluated = stats.moves_evaluated,
161 moves_accepted = stats.moves_accepted,
162 score_calculations = stats.score_calculations,
163 generation_time = %format_duration(stats.generation_time()),
164 evaluation_time = %format_duration(stats.evaluation_time()),
165 speed = speed,
166 score = %current_score,
167 );
168 }
169
170 fn phase_type_name(&self) -> &'static str {
171 "VariableNeighborhoodDescent"
172 }
173 }
174 };
175
176 ($($idx:tt: $MS:ident),+) => {
178 impl<S, D, BestCb, M, $($MS),+> Phase<S, D, BestCb> for VndPhase<($($MS,)+), M>
179 where
180 S: PlanningSolution,
181 D: Director<S>,
182 BestCb: ProgressCallback<S>,
183 M: Move<S>,
184 $($MS: MoveSelector<S, M>,)+
185 {
186 fn solve(&mut self, solver_scope: &mut SolverScope<S, D, BestCb>) {
187 const COUNT: usize = impl_vnd_phase!(@count $($idx),+);
188 let phase_name = "Variable Neighborhood Descent";
189 let mut phase_scope = PhaseScope::with_phase_type(solver_scope, 0, phase_name);
190 let phase_index = phase_scope.phase_index();
191 let mut current_score = phase_scope.calculate_score();
192 let mut progress = VndProgress::new();
193 let mut k = 0usize;
194
195 info!(
196 event = "phase_start",
197 phase = phase_name,
198 phase_index = phase_index,
199 score = %current_score,
200 );
201 phase_scope.solver_scope().report_progress();
202
203 while k < COUNT {
204 let mut step_scope = StepScope::new(&mut phase_scope);
205 let search_result = match k {
206 $($idx => {
207 let mut cursor = (self.0).$idx.open_cursor(step_scope.score_director());
208 find_best_improving_move(&mut cursor, &mut step_scope, ¤t_score, &mut progress)
209 },)+
210 _ => MoveSearchResult::NotFound,
211 };
212
213 match search_result {
214 MoveSearchResult::Found(selected_move, selected_score, selector_index) => {
215 step_scope.apply_committed_move(&selected_move);
216 if let Some(selector_index) = selector_index {
217 step_scope
218 .phase_scope_mut()
219 .record_selector_move_accepted(selector_index);
220 step_scope
221 .phase_scope_mut()
222 .record_selector_move_applied(selector_index);
223 } else {
224 step_scope.phase_scope_mut().record_move_accepted();
225 step_scope.phase_scope_mut().record_move_applied();
226 }
227 step_scope.set_step_score(selected_score);
228 current_score = selected_score;
229 step_scope.phase_scope_mut().update_best_solution();
230 step_scope.complete();
231 k = 0; }
233 MoveSearchResult::NotFound => {
234 step_scope.complete();
235 k += 1; }
237 MoveSearchResult::Interrupted => match settle_search_interrupt(&mut step_scope) {
238 StepInterrupt::Restart => continue,
239 StepInterrupt::TerminatePhase => break,
240 },
241 }
242 }
243
244 phase_scope.solver_scope().report_progress();
245 let duration = phase_scope.elapsed();
246 let steps = phase_scope.step_count();
247 let speed = whole_units_per_second(progress.moves_evaluated(), duration);
248 let stats = phase_scope.stats();
249 info!(
250 event = "phase_end",
251 phase = phase_name,
252 phase_index = phase_index,
253 duration = %format_duration(duration),
254 steps = steps,
255 moves_generated = stats.moves_generated,
256 moves_evaluated = stats.moves_evaluated,
257 moves_accepted = stats.moves_accepted,
258 score_calculations = stats.score_calculations,
259 generation_time = %format_duration(stats.generation_time()),
260 evaluation_time = %format_duration(stats.evaluation_time()),
261 speed = speed,
262 score = %current_score,
263 );
264 }
265
266 fn phase_type_name(&self) -> &'static str {
267 "VariableNeighborhoodDescent"
268 }
269 }
270 };
271
272 (@count $($idx:tt),+) => {
274 0 $(+ { let _ = $idx; 1 })+
275 };
276}
277
278enum MoveSearchResult<M, Sc> {
283 Found(M, Sc, Option<usize>),
284 NotFound,
285 Interrupted,
286}
287
288fn find_best_improving_move<S, D, BestCb, M, C>(
289 cursor: &mut C,
290 step_scope: &mut StepScope<'_, '_, '_, S, D, BestCb>,
291 current_score: &S::Score,
292 progress: &mut VndProgress,
293) -> MoveSearchResult<M, S::Score>
294where
295 S: PlanningSolution,
296 D: Director<S>,
297 BestCb: ProgressCallback<S>,
298 M: Move<S>,
299 C: MoveCursor<S, M>,
300{
301 let mut best: Option<(CandidateId, S::Score)> = None;
302
303 let mut generated = 0usize;
304 let mut evaluated = 0usize;
305 loop {
306 if should_interrupt_generation(step_scope, generated) {
307 return MoveSearchResult::Interrupted;
308 }
309 let generation_started = Instant::now();
310 let Some(candidate_index) = cursor.next_candidate() else {
311 break;
312 };
313 let generation_elapsed = generation_started.elapsed();
314 generated += 1;
315 let mov = cursor
316 .candidate(candidate_index)
317 .expect("discovered candidate id must remain borrowable");
318 let selector_index = cursor.selector_index(candidate_index);
319 let selector_label = selector_index.map(|_| candidate_selector_label(&mov));
320 if let Some(selector_index) = selector_index {
321 step_scope
322 .phase_scope_mut()
323 .record_selector_generated_move_with_label(
324 selector_index,
325 selector_label.as_deref().unwrap_or("selector"),
326 generation_elapsed,
327 );
328 } else {
329 step_scope
330 .phase_scope_mut()
331 .record_generated_move(generation_elapsed);
332 }
333 progress.record_generated();
334
335 if should_interrupt_evaluation(step_scope, evaluated) {
336 return MoveSearchResult::Interrupted;
337 }
338 evaluated += 1;
339 let evaluation_started = Instant::now();
340 if !mov.is_doable(step_scope.score_director()) {
341 if let Some(selector_index) = selector_index {
342 step_scope
343 .phase_scope_mut()
344 .record_selector_evaluated_move(selector_index, evaluation_started.elapsed());
345 step_scope
346 .phase_scope_mut()
347 .record_selector_move_not_doable(selector_index);
348 } else {
349 step_scope
350 .phase_scope_mut()
351 .record_evaluated_move(evaluation_started.elapsed());
352 step_scope.phase_scope_mut().record_move_not_doable();
353 }
354 progress.record_evaluated();
355 progress.maybe_report(step_scope, current_score);
356 continue;
357 }
358
359 let mut recording = RecordingDirector::new(step_scope.score_director_mut());
360 mov.do_move(&mut recording);
361 let move_score = recording.calculate_score();
362 recording.undo_changes();
363 step_scope.phase_scope_mut().record_score_calculation();
364
365 let hard_delta = hard_score_delta(*current_score, move_score);
366 match hard_delta {
367 Some(HardScoreDelta::Improving) => {
368 step_scope.phase_scope_mut().record_move_hard_improving();
369 }
370 Some(HardScoreDelta::Neutral) => {
371 step_scope.phase_scope_mut().record_move_hard_neutral();
372 }
373 Some(HardScoreDelta::Worse) => {
374 step_scope.phase_scope_mut().record_move_hard_worse();
375 }
376 None => {}
377 }
378
379 if mov.requires_hard_improvement() && hard_delta != Some(HardScoreDelta::Improving) {
380 if let Some(selector_index) = selector_index {
381 step_scope
382 .phase_scope_mut()
383 .record_selector_evaluated_move(selector_index, evaluation_started.elapsed());
384 step_scope
385 .phase_scope_mut()
386 .record_selector_move_acceptor_rejected(selector_index);
387 } else {
388 step_scope
389 .phase_scope_mut()
390 .record_evaluated_move(evaluation_started.elapsed());
391 step_scope.phase_scope_mut().record_move_acceptor_rejected();
392 }
393 progress.record_evaluated();
394 progress.maybe_report(step_scope, current_score);
395 continue;
396 }
397
398 if let Some(selector_index) = selector_index {
399 step_scope
400 .phase_scope_mut()
401 .record_selector_evaluated_move(selector_index, evaluation_started.elapsed());
402 } else {
403 step_scope
404 .phase_scope_mut()
405 .record_evaluated_move(evaluation_started.elapsed());
406 }
407 progress.record_evaluated();
408 progress.maybe_report(step_scope, current_score);
409
410 if move_score > *current_score {
411 match &best {
412 Some((_, best_score)) if move_score > *best_score => {
413 best = Some((candidate_index, move_score));
414 }
415 None => {
416 best = Some((candidate_index, move_score));
417 }
418 _ => {}
419 }
420 }
421 }
422
423 match best {
424 Some((index, score)) => {
425 let selector_index = cursor.selector_index(index);
426 MoveSearchResult::Found(cursor.take_candidate(index), score, selector_index)
427 }
428 None => MoveSearchResult::NotFound,
429 }
430}
431
432impl_vnd_phase!(0: MS0);
433impl_vnd_phase!(0: MS0, 1: MS1);
434impl_vnd_phase!(0: MS0, 1: MS1, 2: MS2);
435impl_vnd_phase!(0: MS0, 1: MS1, 2: MS2, 3: MS3);
436impl_vnd_phase!(0: MS0, 1: MS1, 2: MS2, 3: MS3, 4: MS4);
437impl_vnd_phase!(0: MS0, 1: MS1, 2: MS2, 3: MS3, 4: MS4, 5: MS5);
438impl_vnd_phase!(0: MS0, 1: MS1, 2: MS2, 3: MS3, 4: MS4, 5: MS5, 6: MS6);
439impl_vnd_phase!(0: MS0, 1: MS1, 2: MS2, 3: MS3, 4: MS4, 5: MS5, 6: MS6, 7: MS7);
440
441#[cfg(test)]
442#[path = "phase_tests.rs"]
443mod tests;