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, RecordingDirector};
12
13use crate::heuristic::r#move::MoveArena;
14use crate::heuristic::selector::k_opt::{KOptConfig, KOptMoveSelector};
15use crate::phase::control::{
16    append_interruptibly, settle_search_interrupt, should_interrupt_evaluation, StepInterrupt,
17};
18use crate::phase::Phase;
19use crate::scope::{PhaseScope, SolverScope, StepScope};
20
21use super::super::PhaseFactory;
22
23/// Builder for creating k-opt local search phases.
24///
25/// # Type Parameters
26///
27/// * `S` - The planning solution type
28/// * `V` - The list element value type (e.g., `usize` for visit indices)
29/// # Example
30///
31/// ```
32/// use solverforge_solver::KOptPhaseBuilder;
33/// use solverforge_core::domain::PlanningSolution;
34/// use solverforge_core::score::SoftScore;
35///
36/// #[derive(Clone, Debug)]
37/// struct Vehicle { visits: Vec<usize> }
38///
39/// #[derive(Clone, Debug)]
40/// struct Plan {
41///     vehicles: Vec<Vehicle>,
42///     score: Option<SoftScore>,
43/// }
44///
45/// impl PlanningSolution for Plan {
46///     type Score = SoftScore;
47///     fn score(&self) -> Option<Self::Score> { self.score }
48///     fn set_score(&mut self, score: Option<Self::Score>) { self.score = score; }
49/// }
50///
51/// fn list_len(s: &Plan, idx: usize) -> usize {
52///     s.vehicles.get(idx).map_or(0, |v| v.visits.len())
53/// }
54///
55/// fn sublist_remove(s: &mut Plan, idx: usize, start: usize, end: usize) -> Vec<usize> {
56///     s.vehicles.get_mut(idx)
57///         .map(|v| v.visits.drain(start..end).collect())
58///         .unwrap_or_default()
59/// }
60///
61/// fn sublist_insert(s: &mut Plan, idx: usize, pos: usize, items: Vec<usize>) {
62///     if let Some(v) = s.vehicles.get_mut(idx) {
63///         for (i, item) in items.into_iter().enumerate() {
64///             v.visits.insert(pos + i, item);
65///         }
66///     }
67/// }
68///
69/// let builder = KOptPhaseBuilder::<Plan, usize>::new(
70///     list_len,
71///     |s, idx, pos| s.vehicles.get(idx).and_then(|v| v.visits.get(pos)).copied(),
72///     sublist_remove,
73///     sublist_insert,
74///     "visits",
75///     0,
76/// );
77/// ```
78pub struct KOptPhaseBuilder<S, V>
79where
80    S: PlanningSolution,
81    V: Clone + Send + Sync + Debug + 'static,
82{
83    list_len: fn(&S, usize) -> usize,
84    list_get: fn(&S, usize, usize) -> Option<V>,
85    sublist_remove: fn(&mut S, usize, usize, usize) -> Vec<V>,
86    sublist_insert: fn(&mut S, usize, usize, Vec<V>),
87    variable_name: &'static str,
88    descriptor_index: usize,
89    k: usize,
90    step_limit: Option<u64>,
91    _marker: PhantomData<(fn() -> S, fn() -> V)>,
92}
93
94impl<S, V> KOptPhaseBuilder<S, V>
95where
96    S: PlanningSolution,
97    V: Clone + Send + Sync + Debug + 'static,
98{
99    /// Creates a new k-opt phase builder.
100    pub fn new(
101        list_len: fn(&S, usize) -> usize,
102        list_get: fn(&S, usize, usize) -> Option<V>,
103        sublist_remove: fn(&mut S, usize, usize, usize) -> Vec<V>,
104        sublist_insert: fn(&mut S, usize, usize, Vec<V>),
105        variable_name: &'static str,
106        descriptor_index: usize,
107    ) -> Self {
108        Self {
109            list_len,
110            list_get,
111            sublist_remove,
112            sublist_insert,
113            variable_name,
114            descriptor_index,
115            k: 3, // Default to 3-opt
116            step_limit: Some(1000),
117            _marker: PhantomData,
118        }
119    }
120
121    pub fn with_k(mut self, k: usize) -> Self {
122        assert!((2..=5).contains(&k), "k must be between 2 and 5");
123        self.k = k;
124        self
125    }
126
127    pub fn with_step_limit(mut self, limit: u64) -> Self {
128        self.step_limit = Some(limit);
129        self
130    }
131
132    /// Removes the step limit.
133    pub fn without_step_limit(mut self) -> Self {
134        self.step_limit = None;
135        self
136    }
137}
138
139impl<S, V> Debug for KOptPhaseBuilder<S, V>
140where
141    S: PlanningSolution,
142    V: Clone + Send + Sync + Debug + 'static,
143{
144    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
145        f.debug_struct("KOptPhaseBuilder")
146            .field("k", &self.k)
147            .field("variable_name", &self.variable_name)
148            .field("descriptor_index", &self.descriptor_index)
149            .field("step_limit", &self.step_limit)
150            .finish()
151    }
152}
153
154/// K-opt local search phase.
155///
156/// Iterates through k-opt moves and accepts improving ones using hill climbing.
157pub struct KOptPhase<S, V>
158where
159    S: PlanningSolution,
160    V: Clone + Send + Sync + Debug + 'static,
161{
162    config: KOptConfig,
163    list_len: fn(&S, usize) -> usize,
164    list_get: fn(&S, usize, usize) -> Option<V>,
165    sublist_remove: fn(&mut S, usize, usize, usize) -> Vec<V>,
166    sublist_insert: fn(&mut S, usize, usize, Vec<V>),
167    variable_name: &'static str,
168    descriptor_index: usize,
169    step_limit: Option<u64>,
170    _marker: PhantomData<(fn() -> S, fn() -> V)>,
171}
172
173impl<S, V> Debug for KOptPhase<S, V>
174where
175    S: PlanningSolution,
176    V: Clone + Send + Sync + Debug + 'static,
177{
178    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
179        f.debug_struct("KOptPhase")
180            .field("k", &self.config.k)
181            .field("variable_name", &self.variable_name)
182            .finish()
183    }
184}
185
186impl<S, V, D> Phase<S, D> for KOptPhase<S, V>
187where
188    S: PlanningSolution,
189    V: Clone + Send + Sync + Debug + 'static,
190    D: Director<S>,
191{
192    fn solve(&mut self, solver_scope: &mut SolverScope<S, D>) {
193        use crate::heuristic::r#move::Move;
194        use crate::heuristic::selector::entity::FromSolutionEntitySelector;
195        use crate::heuristic::selector::move_selector::MoveSelector;
196
197        let mut phase_scope = PhaseScope::new(solver_scope, 0);
198
199        // Calculate initial score
200        let mut last_step_score = phase_scope.calculate_score();
201
202        // Create move selector
203        let entity_selector = FromSolutionEntitySelector::new(self.descriptor_index);
204        let move_selector = KOptMoveSelector::<S, V, _>::new(
205            entity_selector,
206            self.config.clone(),
207            self.list_len,
208            self.list_get,
209            self.sublist_remove,
210            self.sublist_insert,
211            self.variable_name,
212            self.descriptor_index,
213        );
214
215        let step_limit = self.step_limit.unwrap_or(u64::MAX);
216        let mut steps = 0u64;
217        let mut arena = MoveArena::new();
218
219        while steps < step_limit && !phase_scope.solver_scope_mut().should_terminate() {
220            let mut step_scope = StepScope::new(&mut phase_scope);
221
222            arena.reset();
223            let generation_interrupted = {
224                let iter = move_selector.iter_moves(step_scope.score_director());
225                append_interruptibly(&step_scope, &mut arena, iter)
226            };
227            if generation_interrupted {
228                match settle_search_interrupt(&mut step_scope) {
229                    StepInterrupt::Restart => continue,
230                    StepInterrupt::TerminatePhase => break,
231                }
232            }
233
234            let mut best_move_idx = None;
235            let mut best_score = None;
236            let mut interrupted_step = false;
237
238            // Evaluate all moves
239            for idx in 0..arena.len() {
240                if should_interrupt_evaluation(&step_scope, idx) {
241                    interrupted_step = true;
242                    break;
243                }
244
245                let mv = arena.get(idx).unwrap();
246                if !mv.is_doable(step_scope.score_director()) {
247                    continue;
248                }
249
250                // Use RecordingDirector for automatic undo
251                {
252                    let mut recording = RecordingDirector::new(step_scope.score_director_mut());
253                    mv.do_move(&mut recording);
254                    let move_score = recording.calculate_score();
255
256                    // Accept if improving over last step
257                    if move_score > last_step_score
258                        && best_score.as_ref().is_none_or(|b| move_score > *b)
259                    {
260                        best_score = Some(move_score);
261                        best_move_idx = Some(idx);
262                    }
263
264                    // Undo the move
265                    recording.undo_changes();
266                }
267            }
268
269            if interrupted_step {
270                match settle_search_interrupt(&mut step_scope) {
271                    StepInterrupt::Restart => continue,
272                    StepInterrupt::TerminatePhase => break,
273                }
274            }
275
276            // Apply best move if found
277            if let (Some(idx), Some(score)) = (best_move_idx, best_score) {
278                let selected_move = arena.take(idx);
279                step_scope.apply_committed_move(&selected_move);
280                step_scope.set_step_score(score);
281                last_step_score = score;
282                step_scope.phase_scope_mut().update_best_solution();
283            } else {
284                // No improving moves found - phase is done
285                break;
286            }
287
288            step_scope.complete();
289            steps += 1;
290        }
291
292        // Ensure we have a best solution
293        if phase_scope.solver_scope().best_solution().is_none() {
294            phase_scope.update_best_solution();
295        }
296    }
297
298    fn phase_type_name(&self) -> &'static str {
299        "KOpt"
300    }
301}
302
303impl<S, V, D> PhaseFactory<S, D> for KOptPhaseBuilder<S, V>
304where
305    S: PlanningSolution,
306    V: Clone + Send + Sync + Debug + 'static,
307    D: Director<S>,
308{
309    type Phase = KOptPhase<S, V>;
310
311    fn create(&self) -> Self::Phase {
312        KOptPhase {
313            config: KOptConfig::new(self.k),
314            list_len: self.list_len,
315            list_get: self.list_get,
316            sublist_remove: self.sublist_remove,
317            sublist_insert: self.sublist_insert,
318            variable_name: self.variable_name,
319            descriptor_index: self.descriptor_index,
320            step_limit: self.step_limit,
321            _marker: PhantomData,
322        }
323    }
324}