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::selector::k_opt::{KOptConfig, KOptMoveSelector};
14use crate::phase::Phase;
15use crate::scope::{PhaseScope, SolverScope, StepScope};
16
17use super::super::PhaseFactory;
18
19/// Builder for creating k-opt local search phases.
20///
21/// # Type Parameters
22///
23/// * `S` - The planning solution type
24/// * `V` - The list element value type (e.g., `usize` for visit indices)
25/// * `DM` - The distance meter type (for nearby selection, stored for future use)
26/// * `ESF` - Entity selector factory type (for nearby selection, stored for future use)
27///
28/// # Example
29///
30/// ```
31/// use solverforge_solver::KOptPhaseBuilder;
32/// use solverforge_solver::heuristic::selector::{DefaultDistanceMeter, FromSolutionEntitySelector};
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///     DefaultDistanceMeter,
71///     || FromSolutionEntitySelector::new(0),
72///     list_len,
73///     sublist_remove,
74///     sublist_insert,
75///     "visits",
76///     0,
77/// );
78/// ```
79pub struct KOptPhaseBuilder<S, V, DM, ESF>
80where
81    S: PlanningSolution,
82    V: Clone + Send + Sync + Debug + 'static,
83{
84    list_len: fn(&S, usize) -> usize,
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, fn() -> DM, fn() -> ESF)>,
92}
93
94impl<S, V, DM, ESF> KOptPhaseBuilder<S, V, DM, ESF>
95where
96    S: PlanningSolution,
97    V: Clone + Send + Sync + Debug + 'static,
98{
99    /// Creates a new k-opt phase builder.
100    ///
101    /// The `distance_meter` and `entity_selector_factory` parameters are accepted
102    /// for API compatibility but not currently used (reserved for nearby selection).
103    pub fn new(
104        _distance_meter: DM,
105        _entity_selector_factory: ESF,
106        list_len: fn(&S, usize) -> usize,
107        sublist_remove: fn(&mut S, usize, usize, usize) -> Vec<V>,
108        sublist_insert: fn(&mut S, usize, usize, Vec<V>),
109        variable_name: &'static str,
110        descriptor_index: usize,
111    ) -> Self {
112        Self {
113            list_len,
114            sublist_remove,
115            sublist_insert,
116            variable_name,
117            descriptor_index,
118            k: 3, // Default to 3-opt
119            step_limit: Some(1000),
120            _marker: PhantomData,
121        }
122    }
123
124    pub fn with_k(mut self, k: usize) -> Self {
125        assert!((2..=5).contains(&k), "k must be between 2 and 5");
126        self.k = k;
127        self
128    }
129
130    pub fn with_step_limit(mut self, limit: u64) -> Self {
131        self.step_limit = Some(limit);
132        self
133    }
134
135    /// Removes the step limit.
136    pub fn without_step_limit(mut self) -> Self {
137        self.step_limit = None;
138        self
139    }
140}
141
142impl<S, V, DM, ESF> Debug for KOptPhaseBuilder<S, V, DM, ESF>
143where
144    S: PlanningSolution,
145    V: Clone + Send + Sync + Debug + 'static,
146{
147    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
148        f.debug_struct("KOptPhaseBuilder")
149            .field("k", &self.k)
150            .field("variable_name", &self.variable_name)
151            .field("descriptor_index", &self.descriptor_index)
152            .field("step_limit", &self.step_limit)
153            .finish()
154    }
155}
156
157/// K-opt local search phase.
158///
159/// Iterates through k-opt moves and accepts improving ones using hill climbing.
160pub struct KOptPhase<S, V>
161where
162    S: PlanningSolution,
163    V: Clone + Send + Sync + Debug + 'static,
164{
165    config: KOptConfig,
166    list_len: fn(&S, usize) -> usize,
167    sublist_remove: fn(&mut S, usize, usize, usize) -> Vec<V>,
168    sublist_insert: fn(&mut S, usize, usize, Vec<V>),
169    variable_name: &'static str,
170    descriptor_index: usize,
171    step_limit: Option<u64>,
172    _marker: PhantomData<(fn() -> S, fn() -> V)>,
173}
174
175impl<S, V> Debug for KOptPhase<S, V>
176where
177    S: PlanningSolution,
178    V: Clone + Send + Sync + Debug + 'static,
179{
180    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
181        f.debug_struct("KOptPhase")
182            .field("k", &self.config.k)
183            .field("variable_name", &self.variable_name)
184            .finish()
185    }
186}
187
188impl<S, V, D> Phase<S, D> for KOptPhase<S, V>
189where
190    S: PlanningSolution,
191    V: Clone + Send + Sync + Debug + 'static,
192    D: Director<S>,
193{
194    fn solve(&mut self, solver_scope: &mut SolverScope<S, D>) {
195        use crate::heuristic::r#move::Move;
196        use crate::heuristic::selector::entity::FromSolutionEntitySelector;
197        use crate::heuristic::selector::typed_move_selector::MoveSelector;
198
199        let mut phase_scope = PhaseScope::new(solver_scope, 0);
200
201        // Calculate initial score
202        let mut last_step_score = phase_scope.calculate_score();
203
204        // Create move selector
205        let entity_selector = FromSolutionEntitySelector::new(self.descriptor_index);
206        let move_selector = KOptMoveSelector::<S, V, _>::new(
207            entity_selector,
208            self.config.clone(),
209            self.list_len,
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
219        while steps < step_limit && !phase_scope.solver_scope().should_terminate() {
220            let mut step_scope = StepScope::new(&mut phase_scope);
221
222            // Collect moves first to avoid borrow conflicts
223            let moves: Vec<_> = move_selector
224                .iter_moves(step_scope.score_director())
225                .collect();
226
227            let mut best_move_idx = None;
228            let mut best_score = None;
229
230            // Evaluate all moves
231            for (idx, mv) in moves.iter().enumerate() {
232                if !mv.is_doable(step_scope.score_director()) {
233                    continue;
234                }
235
236                // Use RecordingDirector for automatic undo
237                {
238                    let mut recording = RecordingDirector::new(step_scope.score_director_mut());
239                    mv.do_move(&mut recording);
240                    let move_score = recording.calculate_score();
241
242                    // Accept if improving over last step
243                    if move_score > last_step_score
244                        && best_score.as_ref().is_none_or(|b| move_score > *b)
245                    {
246                        best_score = Some(move_score);
247                        best_move_idx = Some(idx);
248                    }
249
250                    // Undo the move
251                    recording.undo_changes();
252                }
253            }
254
255            // Apply best move if found
256            if let (Some(idx), Some(score)) = (best_move_idx, best_score) {
257                moves[idx].do_move(step_scope.score_director_mut());
258                step_scope.set_step_score(score);
259                last_step_score = score;
260                step_scope.phase_scope_mut().update_best_solution();
261            } else {
262                // No improving moves found - phase is done
263                break;
264            }
265
266            step_scope.complete();
267            steps += 1;
268        }
269
270        // Ensure we have a best solution
271        if phase_scope.solver_scope().best_solution().is_none() {
272            phase_scope.update_best_solution();
273        }
274    }
275
276    fn phase_type_name(&self) -> &'static str {
277        "KOpt"
278    }
279}
280
281impl<S, V, DM, ESF, D> PhaseFactory<S, D> for KOptPhaseBuilder<S, V, DM, ESF>
282where
283    S: PlanningSolution,
284    V: Clone + Send + Sync + Debug + 'static,
285    DM: Send + Sync,
286    ESF: Send + Sync,
287    D: Director<S>,
288{
289    type Phase = KOptPhase<S, V>;
290
291    fn create(&self) -> Self::Phase {
292        KOptPhase {
293            config: KOptConfig::new(self.k),
294            list_len: self.list_len,
295            sublist_remove: self.sublist_remove,
296            sublist_insert: self.sublist_insert,
297            variable_name: self.variable_name,
298            descriptor_index: self.descriptor_index,
299            step_limit: self.step_limit,
300            _marker: PhantomData,
301        }
302    }
303}