1use std::fmt::Debug;
8use std::marker::PhantomData;
9
10use solverforge_core::domain::PlanningSolution;
11use solverforge_scoring::Director;
12
13use crate::heuristic::selector::k_opt::{KOptConfig, KOptMoveSelector};
14use crate::heuristic::selector::move_selector::MoveCursor;
15use crate::phase::control::{
16 settle_search_interrupt, should_interrupt_evaluation, StepInterrupt, GENERATION_POLL_INTERVAL,
17};
18use crate::phase::Phase;
19use crate::scope::{PhaseScope, SolverScope, StepScope};
20use crate::stats::{CandidateTraceDisposition, CandidateTracePullToken, CandidateTraceSource};
21
22use super::super::PhaseFactory;
23
24pub struct KOptPhaseBuilder<S, V>
80where
81 S: PlanningSolution,
82 V: Clone + Send + Sync + Debug + 'static,
83{
84 list_len: fn(&S, usize) -> usize,
85 list_get: fn(&S, usize, usize) -> Option<V>,
86 sublist_remove: fn(&mut S, usize, usize, usize) -> Vec<V>,
87 sublist_insert: fn(&mut S, usize, usize, Vec<V>),
88 variable_name: &'static str,
89 descriptor_index: usize,
90 k: usize,
91 step_limit: Option<u64>,
92 _marker: PhantomData<(fn() -> S, fn() -> V)>,
93}
94
95impl<S, V> KOptPhaseBuilder<S, V>
96where
97 S: PlanningSolution,
98 V: Clone + Send + Sync + Debug + 'static,
99{
100 pub fn new(
102 list_len: fn(&S, usize) -> usize,
103 list_get: fn(&S, usize, usize) -> Option<V>,
104 sublist_remove: fn(&mut S, usize, usize, usize) -> Vec<V>,
105 sublist_insert: fn(&mut S, usize, usize, Vec<V>),
106 variable_name: &'static str,
107 descriptor_index: usize,
108 ) -> Self {
109 Self {
110 list_len,
111 list_get,
112 sublist_remove,
113 sublist_insert,
114 variable_name,
115 descriptor_index,
116 k: 3, step_limit: Some(1000),
118 _marker: PhantomData,
119 }
120 }
121
122 pub fn with_k(mut self, k: usize) -> Self {
123 assert!((2..=5).contains(&k), "k must be between 2 and 5");
124 self.k = k;
125 self
126 }
127
128 pub fn with_step_limit(mut self, limit: u64) -> Self {
129 self.step_limit = Some(limit);
130 self
131 }
132
133 pub fn without_step_limit(mut self) -> Self {
135 self.step_limit = None;
136 self
137 }
138}
139
140impl<S, V> Debug for KOptPhaseBuilder<S, V>
141where
142 S: PlanningSolution,
143 V: Clone + Send + Sync + Debug + 'static,
144{
145 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
146 f.debug_struct("KOptPhaseBuilder")
147 .field("k", &self.k)
148 .field("variable_name", &self.variable_name)
149 .field("descriptor_index", &self.descriptor_index)
150 .field("step_limit", &self.step_limit)
151 .finish()
152 }
153}
154
155pub struct KOptPhase<S, V>
159where
160 S: PlanningSolution,
161 V: Clone + Send + Sync + Debug + 'static,
162{
163 config: KOptConfig,
164 list_len: fn(&S, usize) -> usize,
165 list_get: fn(&S, usize, usize) -> Option<V>,
166 sublist_remove: fn(&mut S, usize, usize, usize) -> Vec<V>,
167 sublist_insert: fn(&mut S, usize, usize, Vec<V>),
168 variable_name: &'static str,
169 descriptor_index: usize,
170 step_limit: Option<u64>,
171 _marker: PhantomData<(fn() -> S, fn() -> V)>,
172}
173
174impl<S, V> Debug for KOptPhase<S, V>
175where
176 S: PlanningSolution,
177 V: Clone + Send + Sync + Debug + 'static,
178{
179 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
180 f.debug_struct("KOptPhase")
181 .field("k", &self.config.k)
182 .field("variable_name", &self.variable_name)
183 .finish()
184 }
185}
186
187impl<S, V, D> Phase<S, D> for KOptPhase<S, V>
188where
189 S: PlanningSolution,
190 V: Clone + Send + Sync + Debug + 'static,
191 D: Director<S>,
192{
193 fn solve(&mut self, solver_scope: &mut SolverScope<S, D>) {
194 use crate::heuristic::r#move::Move;
195 use crate::heuristic::selector::entity::FromSolutionEntitySelector;
196 use crate::heuristic::selector::move_selector::MoveSelector;
197
198 let mut phase_scope = PhaseScope::with_phase_type(solver_scope, 0, "K-Opt");
199
200 let mut last_step_score = phase_scope.calculate_score();
202
203 let entity_selector = FromSolutionEntitySelector::new(self.descriptor_index);
205 let move_selector = KOptMoveSelector::<S, V, _>::new(
206 entity_selector,
207 self.config.clone(),
208 self.list_len,
209 self.list_get,
210 self.sublist_remove,
211 self.sublist_insert,
212 self.variable_name,
213 self.descriptor_index,
214 );
215
216 let step_limit = self.step_limit.unwrap_or(u64::MAX);
217 let mut steps = 0u64;
218 while steps < step_limit && !phase_scope.solver_scope_mut().should_terminate() {
219 let mut step_scope = StepScope::new(&mut phase_scope);
220 let mut cursor = move_selector.open_cursor(step_scope.score_director());
221 let mut best_move_id = None;
222 let mut best_score = None;
223 let mut best_trace_token: Option<CandidateTracePullToken> = None;
224 let mut interrupted_step = false;
225 let mut candidate_ordinal = 0usize;
226
227 while let Some(candidate_id) = cursor.next_candidate() {
228 let candidate = cursor
229 .candidate(candidate_id)
230 .expect("k-opt candidate id must remain live after pull");
231 let trace_token = step_scope.phase_scope_mut().record_candidate_pull(
232 CandidateTraceSource::KOpt,
233 None,
234 candidate_id.index(),
235 None,
236 &candidate,
237 );
238 if should_interrupt_evaluation(&step_scope, candidate_ordinal) {
239 if let Some(token) = trace_token {
240 step_scope
241 .phase_scope_mut()
242 .record_candidate_trace_disposition(
243 token,
244 CandidateTraceDisposition::InterruptedBeforeEvaluation,
245 );
246 }
247 interrupted_step = true;
248 break;
249 }
250 candidate_ordinal += 1;
251
252 let is_doable = cursor
253 .candidate(candidate_id)
254 .expect("k-opt candidate id must remain live during evaluation")
255 .is_doable(step_scope.score_director());
256 if !is_doable {
257 if let Some(token) = trace_token {
258 let phase_scope = step_scope.phase_scope_mut();
259 phase_scope.record_candidate_trace_disposition(
260 token,
261 CandidateTraceDisposition::Evaluated,
262 );
263 phase_scope.record_candidate_trace_disposition(
264 token,
265 CandidateTraceDisposition::NotDoable,
266 );
267 }
268 assert!(cursor.release_candidate(candidate_id));
269 if candidate_ordinal.is_multiple_of(GENERATION_POLL_INTERVAL) {
270 step_scope.phase_scope_mut().report_progress_if_due();
271 }
272 continue;
273 }
274
275 let move_score = {
276 let mv = cursor
277 .candidate(candidate_id)
278 .expect("k-opt candidate id must remain live during evaluation");
279 let score_state = step_scope.score_director().snapshot_score_state();
280 let undo = mv.do_move(step_scope.score_director_mut());
281 let move_score = step_scope.calculate_score();
282 mv.undo_move(step_scope.score_director_mut(), undo);
283 step_scope
284 .score_director_mut()
285 .restore_score_state(score_state);
286 move_score
287 };
288 if let Some(token) = trace_token {
289 step_scope
290 .phase_scope_mut()
291 .record_candidate_trace_disposition(
292 token,
293 CandidateTraceDisposition::Evaluated,
294 );
295 }
296
297 if move_score > last_step_score
298 && best_score.as_ref().is_none_or(|b| move_score > *b)
299 {
300 if let Some(previous_best) = best_move_id.replace(candidate_id) {
301 assert!(cursor.release_candidate(previous_best));
302 }
303 if let Some(token) = std::mem::replace(&mut best_trace_token, trace_token) {
304 step_scope
305 .phase_scope_mut()
306 .record_candidate_trace_disposition(
307 token,
308 CandidateTraceDisposition::ForagerIgnored,
309 );
310 }
311 best_score = Some(move_score);
312 } else {
313 assert!(cursor.release_candidate(candidate_id));
314 if let Some(token) = trace_token {
315 step_scope
316 .phase_scope_mut()
317 .record_candidate_trace_disposition(
318 token,
319 CandidateTraceDisposition::ForagerIgnored,
320 );
321 }
322 }
323 if candidate_ordinal.is_multiple_of(GENERATION_POLL_INTERVAL) {
324 step_scope.phase_scope_mut().report_progress_if_due();
325 }
326 }
327
328 if interrupted_step {
329 match settle_search_interrupt(&mut step_scope) {
330 StepInterrupt::Restart => {
331 if let Some(token) = best_trace_token.take() {
332 step_scope
333 .phase_scope_mut()
334 .record_candidate_trace_disposition(
335 token,
336 CandidateTraceDisposition::ForagerIgnored,
337 );
338 }
339 continue;
340 }
341 StepInterrupt::TerminatePhase => {
342 if let Some(token) = best_trace_token.take() {
343 step_scope
344 .phase_scope_mut()
345 .record_candidate_trace_disposition(
346 token,
347 CandidateTraceDisposition::ForagerIgnored,
348 );
349 }
350 break;
351 }
352 }
353 }
354
355 if let (Some(selected_id), Some(score)) = (best_move_id, best_score) {
357 if let Some(token) = best_trace_token {
358 step_scope
359 .phase_scope_mut()
360 .record_candidate_trace_disposition(
361 token,
362 CandidateTraceDisposition::Selected,
363 );
364 }
365 step_scope.apply_committed_change(|score_director| {
366 cursor.apply_owned_candidate(selected_id, score_director);
367 });
368 if let Some(token) = best_trace_token {
369 step_scope
370 .phase_scope_mut()
371 .record_candidate_trace_disposition(
372 token,
373 CandidateTraceDisposition::Applied,
374 );
375 }
376 step_scope.set_step_score(score);
377 last_step_score = score;
378 step_scope.phase_scope_mut().update_best_solution();
379 } else {
380 break;
382 }
383
384 step_scope.complete();
385 steps += 1;
386 }
387
388 if phase_scope.solver_scope().best_solution().is_none() {
390 phase_scope.update_best_solution();
391 }
392 }
393
394 fn phase_type_name(&self) -> &'static str {
395 "KOpt"
396 }
397}
398
399impl<S, V, D> PhaseFactory<S, D> for KOptPhaseBuilder<S, V>
400where
401 S: PlanningSolution,
402 V: Clone + Send + Sync + Debug + 'static,
403 D: Director<S>,
404{
405 type Phase = KOptPhase<S, V>;
406
407 fn create(&self) -> Self::Phase {
408 KOptPhase {
409 config: KOptConfig::new(self.k),
410 list_len: self.list_len,
411 list_get: self.list_get,
412 sublist_remove: self.sublist_remove,
413 sublist_insert: self.sublist_insert,
414 variable_name: self.variable_name,
415 descriptor_index: self.descriptor_index,
416 step_limit: self.step_limit,
417 _marker: PhantomData,
418 }
419 }
420}