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::{ListUndoJournal, Move};
195 use crate::heuristic::selector::entity::FromSolutionEntitySelector;
196 use crate::heuristic::selector::move_selector::{MoveCandidateUndo, MoveSelector};
197
198 let mut phase_scope = PhaseScope::with_phase_type(solver_scope, 0, "K-Opt");
199 let mut journal_pool: Vec<MoveCandidateUndo<ListUndoJournal<V>>> = Vec::new();
202
203 let mut last_step_score = phase_scope.calculate_score();
205
206 let entity_selector = FromSolutionEntitySelector::new(self.descriptor_index);
208 let move_selector = KOptMoveSelector::<S, V, _>::new(
209 entity_selector,
210 self.config.clone(),
211 self.list_len,
212 self.list_get,
213 self.sublist_remove,
214 self.sublist_insert,
215 self.variable_name,
216 self.descriptor_index,
217 );
218
219 let step_limit = self.step_limit.unwrap_or(u64::MAX);
220 let mut steps = 0u64;
221 while steps < step_limit && !phase_scope.solver_scope_mut().should_terminate() {
222 let mut step_scope = StepScope::new(&mut phase_scope);
223 let mut cursor = move_selector.open_cursor(step_scope.score_director());
224 let mut best_move_id = None;
225 let mut best_score = None;
226 let mut best_trace_token: Option<CandidateTracePullToken> = None;
227 let mut interrupted_step = false;
228 let mut candidate_ordinal = 0usize;
229
230 while let Some(candidate_id) =
231 cursor.next_candidate(step_scope.score_director().working_solution())
232 {
233 let candidate = cursor
234 .candidate(candidate_id)
235 .expect("k-opt candidate id must remain live after pull");
236 let trace_token = step_scope.phase_scope_mut().record_candidate_pull(
237 CandidateTraceSource::KOpt,
238 None,
239 candidate_id.index(),
240 None,
241 &candidate,
242 );
243 if should_interrupt_evaluation(&step_scope, candidate_ordinal) {
244 if let Some(token) = trace_token {
245 step_scope
246 .phase_scope_mut()
247 .record_candidate_trace_disposition(
248 token,
249 CandidateTraceDisposition::InterruptedBeforeEvaluation,
250 );
251 }
252 interrupted_step = true;
253 break;
254 }
255 candidate_ordinal += 1;
256
257 let candidate = cursor
258 .candidate(candidate_id)
259 .expect("k-opt candidate id must remain live during evaluation");
260 let is_doable =
261 !crate::pinning::move_changes_pinned(&candidate, step_scope.score_director())
262 && candidate.is_doable(step_scope.score_director());
263 if !is_doable {
264 if let Some(token) = trace_token {
265 let phase_scope = step_scope.phase_scope_mut();
266 phase_scope.record_candidate_trace_disposition(
267 token,
268 CandidateTraceDisposition::Evaluated,
269 );
270 phase_scope.record_candidate_trace_disposition(
271 token,
272 CandidateTraceDisposition::NotDoable,
273 );
274 }
275 assert!(cursor.release_candidate(candidate_id));
276 if candidate_ordinal.is_multiple_of(GENERATION_POLL_INTERVAL) {
277 step_scope.phase_scope_mut().report_progress_if_due();
278 }
279 continue;
280 }
281
282 let move_score = {
283 let mv = cursor
284 .candidate(candidate_id)
285 .expect("k-opt candidate id must remain live during evaluation");
286 let score_state = step_scope.score_director().snapshot_score_state();
287 let undo =
288 mv.do_move_with_scratch(step_scope.score_director_mut(), &mut journal_pool);
289 let move_score = step_scope.calculate_score();
290 mv.undo_move_with_scratch(
291 step_scope.score_director_mut(),
292 undo,
293 &mut journal_pool,
294 );
295 step_scope
296 .score_director_mut()
297 .restore_score_state(score_state);
298 move_score
299 };
300 if let Some(token) = trace_token {
301 step_scope
302 .phase_scope_mut()
303 .record_candidate_trace_disposition(
304 token,
305 CandidateTraceDisposition::Evaluated,
306 );
307 }
308
309 if move_score > last_step_score
310 && best_score.as_ref().is_none_or(|b| move_score > *b)
311 {
312 if let Some(previous_best) = best_move_id.replace(candidate_id) {
313 assert!(cursor.release_candidate(previous_best));
314 }
315 if let Some(token) = std::mem::replace(&mut best_trace_token, trace_token) {
316 step_scope
317 .phase_scope_mut()
318 .record_candidate_trace_disposition(
319 token,
320 CandidateTraceDisposition::ForagerIgnored,
321 );
322 }
323 best_score = Some(move_score);
324 } else {
325 assert!(cursor.release_candidate(candidate_id));
326 if let Some(token) = trace_token {
327 step_scope
328 .phase_scope_mut()
329 .record_candidate_trace_disposition(
330 token,
331 CandidateTraceDisposition::ForagerIgnored,
332 );
333 }
334 }
335 if candidate_ordinal.is_multiple_of(GENERATION_POLL_INTERVAL) {
336 step_scope.phase_scope_mut().report_progress_if_due();
337 }
338 }
339
340 if interrupted_step {
341 match settle_search_interrupt(&mut step_scope) {
342 StepInterrupt::Restart => {
343 if let Some(token) = best_trace_token.take() {
344 step_scope
345 .phase_scope_mut()
346 .record_candidate_trace_disposition(
347 token,
348 CandidateTraceDisposition::ForagerIgnored,
349 );
350 }
351 continue;
352 }
353 StepInterrupt::TerminatePhase => {
354 if let Some(token) = best_trace_token.take() {
355 step_scope
356 .phase_scope_mut()
357 .record_candidate_trace_disposition(
358 token,
359 CandidateTraceDisposition::ForagerIgnored,
360 );
361 }
362 break;
363 }
364 }
365 }
366
367 if let (Some(selected_id), Some(score)) = (best_move_id, best_score) {
369 if let Some(token) = best_trace_token {
370 step_scope
371 .phase_scope_mut()
372 .record_candidate_trace_disposition(
373 token,
374 CandidateTraceDisposition::Selected,
375 );
376 }
377 step_scope.apply_committed_change(|score_director| {
378 cursor.apply_owned_candidate(selected_id, score_director);
379 });
380 if let Some(token) = best_trace_token {
381 step_scope
382 .phase_scope_mut()
383 .record_candidate_trace_disposition(
384 token,
385 CandidateTraceDisposition::Applied,
386 );
387 }
388 step_scope.set_step_score(score);
389 last_step_score = score;
390 step_scope.phase_scope_mut().update_best_solution();
391 } else {
392 break;
394 }
395
396 step_scope.complete();
397 steps += 1;
398 }
399
400 if phase_scope.solver_scope().best_solution().is_none() {
402 phase_scope.update_best_solution();
403 }
404 }
405
406 fn phase_type_name(&self) -> &'static str {
407 "KOpt"
408 }
409}
410
411impl<S, V, D> PhaseFactory<S, D> for KOptPhaseBuilder<S, V>
412where
413 S: PlanningSolution,
414 V: Clone + Send + Sync + Debug + 'static,
415 D: Director<S>,
416{
417 type Phase = KOptPhase<S, V>;
418
419 fn create(&self) -> Self::Phase {
420 KOptPhase {
421 config: KOptConfig::new(self.k),
422 list_len: self.list_len,
423 list_get: self.list_get,
424 sublist_remove: self.sublist_remove,
425 sublist_insert: self.sublist_insert,
426 variable_name: self.variable_name,
427 descriptor_index: self.descriptor_index,
428 step_limit: self.step_limit,
429 _marker: PhantomData,
430 }
431 }
432}