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 candidate = cursor
253 .candidate(candidate_id)
254 .expect("k-opt candidate id must remain live during evaluation");
255 let is_doable =
256 !crate::pinning::move_changes_pinned(&candidate, step_scope.score_director())
257 && candidate.is_doable(step_scope.score_director());
258 if !is_doable {
259 if let Some(token) = trace_token {
260 let phase_scope = step_scope.phase_scope_mut();
261 phase_scope.record_candidate_trace_disposition(
262 token,
263 CandidateTraceDisposition::Evaluated,
264 );
265 phase_scope.record_candidate_trace_disposition(
266 token,
267 CandidateTraceDisposition::NotDoable,
268 );
269 }
270 assert!(cursor.release_candidate(candidate_id));
271 if candidate_ordinal.is_multiple_of(GENERATION_POLL_INTERVAL) {
272 step_scope.phase_scope_mut().report_progress_if_due();
273 }
274 continue;
275 }
276
277 let move_score = {
278 let mv = cursor
279 .candidate(candidate_id)
280 .expect("k-opt candidate id must remain live during evaluation");
281 let score_state = step_scope.score_director().snapshot_score_state();
282 let undo = mv.do_move(step_scope.score_director_mut());
283 let move_score = step_scope.calculate_score();
284 mv.undo_move(step_scope.score_director_mut(), undo);
285 step_scope
286 .score_director_mut()
287 .restore_score_state(score_state);
288 move_score
289 };
290 if let Some(token) = trace_token {
291 step_scope
292 .phase_scope_mut()
293 .record_candidate_trace_disposition(
294 token,
295 CandidateTraceDisposition::Evaluated,
296 );
297 }
298
299 if move_score > last_step_score
300 && best_score.as_ref().is_none_or(|b| move_score > *b)
301 {
302 if let Some(previous_best) = best_move_id.replace(candidate_id) {
303 assert!(cursor.release_candidate(previous_best));
304 }
305 if let Some(token) = std::mem::replace(&mut best_trace_token, trace_token) {
306 step_scope
307 .phase_scope_mut()
308 .record_candidate_trace_disposition(
309 token,
310 CandidateTraceDisposition::ForagerIgnored,
311 );
312 }
313 best_score = Some(move_score);
314 } else {
315 assert!(cursor.release_candidate(candidate_id));
316 if let Some(token) = trace_token {
317 step_scope
318 .phase_scope_mut()
319 .record_candidate_trace_disposition(
320 token,
321 CandidateTraceDisposition::ForagerIgnored,
322 );
323 }
324 }
325 if candidate_ordinal.is_multiple_of(GENERATION_POLL_INTERVAL) {
326 step_scope.phase_scope_mut().report_progress_if_due();
327 }
328 }
329
330 if interrupted_step {
331 match settle_search_interrupt(&mut step_scope) {
332 StepInterrupt::Restart => {
333 if let Some(token) = best_trace_token.take() {
334 step_scope
335 .phase_scope_mut()
336 .record_candidate_trace_disposition(
337 token,
338 CandidateTraceDisposition::ForagerIgnored,
339 );
340 }
341 continue;
342 }
343 StepInterrupt::TerminatePhase => {
344 if let Some(token) = best_trace_token.take() {
345 step_scope
346 .phase_scope_mut()
347 .record_candidate_trace_disposition(
348 token,
349 CandidateTraceDisposition::ForagerIgnored,
350 );
351 }
352 break;
353 }
354 }
355 }
356
357 if let (Some(selected_id), Some(score)) = (best_move_id, best_score) {
359 if let Some(token) = best_trace_token {
360 step_scope
361 .phase_scope_mut()
362 .record_candidate_trace_disposition(
363 token,
364 CandidateTraceDisposition::Selected,
365 );
366 }
367 step_scope.apply_committed_change(|score_director| {
368 cursor.apply_owned_candidate(selected_id, score_director);
369 });
370 if let Some(token) = best_trace_token {
371 step_scope
372 .phase_scope_mut()
373 .record_candidate_trace_disposition(
374 token,
375 CandidateTraceDisposition::Applied,
376 );
377 }
378 step_scope.set_step_score(score);
379 last_step_score = score;
380 step_scope.phase_scope_mut().update_best_solution();
381 } else {
382 break;
384 }
385
386 step_scope.complete();
387 steps += 1;
388 }
389
390 if phase_scope.solver_scope().best_solution().is_none() {
392 phase_scope.update_best_solution();
393 }
394 }
395
396 fn phase_type_name(&self) -> &'static str {
397 "KOpt"
398 }
399}
400
401impl<S, V, D> PhaseFactory<S, D> for KOptPhaseBuilder<S, V>
402where
403 S: PlanningSolution,
404 V: Clone + Send + Sync + Debug + 'static,
405 D: Director<S>,
406{
407 type Phase = KOptPhase<S, V>;
408
409 fn create(&self) -> Self::Phase {
410 KOptPhase {
411 config: KOptConfig::new(self.k),
412 list_len: self.list_len,
413 list_get: self.list_get,
414 sublist_remove: self.sublist_remove,
415 sublist_insert: self.sublist_insert,
416 variable_name: self.variable_name,
417 descriptor_index: self.descriptor_index,
418 step_limit: self.step_limit,
419 _marker: PhantomData,
420 }
421 }
422}