Skip to main content

solverforge_solver/manager/phase_factory/
k_opt.rs

1/* K-opt phase builder for tour optimization.
2
3Creates a local search phase that uses k-opt moves to improve solutions.
4This is commonly used for vehicle routing and traveling salesman problems.
5*/
6
7use 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
24/// Builder for creating k-opt local search phases.
25///
26/// # Type Parameters
27///
28/// * `S` - The planning solution type
29/// * `V` - The list element value type (e.g., `usize` for visit indices)
30/// # Example
31///
32/// ```
33/// use solverforge_solver::KOptPhaseBuilder;
34/// use solverforge_core::domain::PlanningSolution;
35/// use solverforge_core::score::SoftScore;
36///
37/// #[derive(Clone, Debug)]
38/// struct Vehicle { visits: Vec<usize> }
39///
40/// #[derive(Clone, Debug)]
41/// struct Plan {
42///     vehicles: Vec<Vehicle>,
43///     score: Option<SoftScore>,
44/// }
45///
46/// impl PlanningSolution for Plan {
47///     type Score = SoftScore;
48///     fn score(&self) -> Option<Self::Score> { self.score }
49///     fn set_score(&mut self, score: Option<Self::Score>) { self.score = score; }
50/// }
51///
52/// fn list_len(s: &Plan, idx: usize) -> usize {
53///     s.vehicles.get(idx).map_or(0, |v| v.visits.len())
54/// }
55///
56/// fn sublist_remove(s: &mut Plan, idx: usize, start: usize, end: usize) -> Vec<usize> {
57///     s.vehicles.get_mut(idx)
58///         .map(|v| v.visits.drain(start..end).collect())
59///         .unwrap_or_default()
60/// }
61///
62/// fn sublist_insert(s: &mut Plan, idx: usize, pos: usize, items: Vec<usize>) {
63///     if let Some(v) = s.vehicles.get_mut(idx) {
64///         for (i, item) in items.into_iter().enumerate() {
65///             v.visits.insert(pos + i, item);
66///         }
67///     }
68/// }
69///
70/// let builder = KOptPhaseBuilder::<Plan, usize>::new(
71///     list_len,
72///     |s, idx, pos| s.vehicles.get(idx).and_then(|v| v.visits.get(pos)).copied(),
73///     sublist_remove,
74///     sublist_insert,
75///     "visits",
76///     0,
77/// );
78/// ```
79pub 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    /// Creates a new k-opt phase builder.
101    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, // Default to 3-opt
117            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    /// Removes the step limit.
134    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
155/// K-opt local search phase.
156///
157/// Iterates through k-opt moves and accepts improving ones using hill climbing.
158pub 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::MoveSelector;
197
198        let mut phase_scope = PhaseScope::with_phase_type(solver_scope, 0, "K-Opt");
199        // Retired K-opt journals recycle across the phase's trials; arbitrary
200        // route lengths do not grow the heap per trial.
201        let mut journal_pool: Vec<ListUndoJournal<V>> = Vec::new();
202
203        // Calculate initial score
204        let mut last_step_score = phase_scope.calculate_score();
205
206        // Create move selector
207        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_recycling(step_scope.score_director_mut(), &mut journal_pool);
289                    let move_score = step_scope.calculate_score();
290                    mv.undo_move_recycling(
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            // Apply best move if found
368            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                // No improving moves found - phase is done
393                break;
394            }
395
396            step_scope.complete();
397            steps += 1;
398        }
399
400        // Ensure we have a best solution
401        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}