Skip to main content

solverforge_solver/heuristic/move/
k_opt.rs

1/* K-opt move for tour optimization.
2
3K-opt removes k edges from a tour and reconnects the resulting segments
4in a different order, potentially reversing some segments. This is a
5fundamental move for TSP and VRP optimization.
6
7# Zero-Erasure Design
8
9- Fixed arrays for cut points (no SmallVec for static data)
10- Reconnection pattern stored by value (`KOptReconnection` is `Copy`)
11- Concrete function pointers for all list operations
12
13# Example
14
15```
16use solverforge_solver::heuristic::r#move::{KOptMove, CutPoint};
17use solverforge_solver::heuristic::r#move::k_opt_reconnection::THREE_OPT_RECONNECTIONS;
18use solverforge_core::domain::PlanningSolution;
19use solverforge_core::score::SoftScore;
20
21#[derive(Clone, Debug)]
22struct Tour { cities: Vec<i32>, score: Option<SoftScore> }
23
24impl PlanningSolution for Tour {
25type Score = SoftScore;
26fn score(&self) -> Option<Self::Score> { self.score }
27fn set_score(&mut self, score: Option<Self::Score>) { self.score = score; }
28}
29
30fn list_len(s: &Tour, _: usize) -> usize { s.cities.len() }
31fn sublist_remove(s: &mut Tour, _: usize, start: usize, end: usize) -> Vec<i32> {
32s.cities.drain(start..end).collect()
33}
34fn sublist_insert(s: &mut Tour, _: usize, pos: usize, items: Vec<i32>) {
35for (i, item) in items.into_iter().enumerate() {
36s.cities.insert(pos + i, item);
37}
38}
39
40// Create a 3-opt move with cuts at positions 2, 4, 6
41// This creates 4 segments: [0..2), [2..4), [4..6), [6..)
42let cuts = [
43CutPoint::new(0, 2),
44CutPoint::new(0, 4),
45CutPoint::new(0, 6),
46];
47let reconnection = &THREE_OPT_RECONNECTIONS[3]; // Swap middle segments
48
49let m = KOptMove::<Tour, i32>::new(
50&cuts,
51reconnection,
52list_len,
53sublist_remove,
54sublist_insert,
55"cities",
560,
57);
58```
59*/
60
61use std::fmt::Debug;
62use std::marker::PhantomData;
63
64use solverforge_core::domain::PlanningSolution;
65use solverforge_scoring::Director;
66
67use super::k_opt_reconnection::KOptReconnection;
68use super::list_kernel::{
69    k_opt_do_move, k_opt_is_doable, k_opt_tabu_signature, k_opt_undo_move, StaticListWindowAccess,
70};
71use super::metadata::hash_str;
72use super::{Move, MoveTabuSignature};
73
74/* A cut point in a route, defining where an edge is removed.
75
76For k-opt, we have k cut points which divide the route into k+1 segments.
77
78# Example
79
80```
81use solverforge_solver::heuristic::r#move::CutPoint;
82
83// Cut at position 5 in entity 0
84let cut = CutPoint::new(0, 5);
85assert_eq!(cut.entity_index(), 0);
86assert_eq!(cut.position(), 5);
87```
88*/
89#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
90pub struct CutPoint {
91    // Entity (route/vehicle) index.
92    entity_index: usize,
93    // Position in the route where the cut occurs.
94    // The edge between position-1 and position is removed.
95    position: usize,
96}
97
98impl CutPoint {
99    #[inline]
100    pub const fn new(entity_index: usize, position: usize) -> Self {
101        Self {
102            entity_index,
103            position,
104        }
105    }
106
107    #[inline]
108    pub const fn entity_index(&self) -> usize {
109        self.entity_index
110    }
111
112    #[inline]
113    pub const fn position(&self) -> usize {
114        self.position
115    }
116}
117
118/// A k-opt move that removes k edges and reconnects segments.
119///
120/// This is the generalized k-opt move supporting k=2,3,4,5.
121/// For k=2, this is equivalent to a 2-opt (segment reversal).
122///
123/// # Zero-Erasure Design
124///
125/// - Fixed array `[CutPoint; 5]` for up to 5 cuts (5-opt)
126/// - `KOptReconnection` stored by value (`Copy` type, no heap allocation)
127/// - Concrete function pointers for list operations
128///
129/// # Type Parameters
130///
131/// * `S` - The planning solution type
132/// * `V` - The list element value type
133pub struct KOptMove<S, V> {
134    // Cut points (up to 5 for 5-opt).
135    cuts: [CutPoint; 5],
136    // Number of actual cuts (k value).
137    cut_count: u8,
138    // Reconnection pattern to apply (stored by value — `KOptReconnection` is `Copy`).
139    reconnection: KOptReconnection,
140    list_len: fn(&S, usize) -> usize,
141    list_get: fn(&S, usize, usize) -> Option<V>,
142    // Remove sublist [start, end), returns removed elements.
143    sublist_remove: fn(&mut S, usize, usize, usize) -> Vec<V>,
144    // Insert elements at position.
145    sublist_insert: fn(&mut S, usize, usize, Vec<V>),
146    // Variable name.
147    variable_name: &'static str,
148    variable_id: u64,
149    // Descriptor index.
150    descriptor_index: usize,
151    // Entity index (for intra-route moves).
152    entity_index: usize,
153    _phantom: PhantomData<fn() -> V>,
154}
155
156impl<S, V> Clone for KOptMove<S, V> {
157    fn clone(&self) -> Self {
158        Self {
159            cuts: self.cuts,
160            cut_count: self.cut_count,
161            reconnection: self.reconnection,
162            list_len: self.list_len,
163            list_get: self.list_get,
164            sublist_remove: self.sublist_remove,
165            sublist_insert: self.sublist_insert,
166            variable_name: self.variable_name,
167            variable_id: self.variable_id,
168            descriptor_index: self.descriptor_index,
169            entity_index: self.entity_index,
170            _phantom: PhantomData,
171        }
172    }
173}
174
175impl<S, V: Debug> Debug for KOptMove<S, V> {
176    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
177        let cuts: Vec<_> = self.cuts[..self.cut_count as usize]
178            .iter()
179            .map(|c| c.position)
180            .collect();
181        f.debug_struct("KOptMove")
182            .field("k", &self.cut_count)
183            .field("entity", &self.entity_index)
184            .field("cuts", &cuts)
185            .field("reconnection", &self.reconnection)
186            .field("variable_name", &self.variable_name)
187            .finish()
188    }
189}
190
191impl<S, V> KOptMove<S, V> {
192    /* Creates a new k-opt move.
193
194    # Arguments
195
196    * `cuts` - Slice of cut points (must be sorted by position for intra-route)
197    * `reconnection` - How to reconnect the segments
198    * `list_len` - Function to get list length
199    * `sublist_remove` - Function to remove a range
200    * `sublist_insert` - Function to insert elements
201    * `variable_name` - Name of the list variable
202    * `descriptor_index` - Entity descriptor index
203
204    # Panics
205
206    Panics if cuts is empty or has more than 5 elements.
207    */
208    #[allow(clippy::too_many_arguments)]
209    pub fn new(
210        cuts: &[CutPoint],
211        reconnection: &KOptReconnection,
212        list_len: fn(&S, usize) -> usize,
213        list_get: fn(&S, usize, usize) -> Option<V>,
214        sublist_remove: fn(&mut S, usize, usize, usize) -> Vec<V>,
215        sublist_insert: fn(&mut S, usize, usize, Vec<V>),
216        variable_name: &'static str,
217        descriptor_index: usize,
218    ) -> Self {
219        Self::new_with_variable_id(
220            cuts,
221            reconnection,
222            list_len,
223            list_get,
224            sublist_remove,
225            sublist_insert,
226            variable_name,
227            hash_str(variable_name),
228            descriptor_index,
229        )
230    }
231
232    #[allow(clippy::too_many_arguments)]
233    pub(crate) fn new_with_variable_id(
234        cuts: &[CutPoint],
235        reconnection: &KOptReconnection,
236        list_len: fn(&S, usize) -> usize,
237        list_get: fn(&S, usize, usize) -> Option<V>,
238        sublist_remove: fn(&mut S, usize, usize, usize) -> Vec<V>,
239        sublist_insert: fn(&mut S, usize, usize, Vec<V>),
240        variable_name: &'static str,
241        variable_id: u64,
242        descriptor_index: usize,
243    ) -> Self {
244        assert!(!cuts.is_empty() && cuts.len() <= 5, "k must be 1-5");
245
246        let mut cut_array = [CutPoint::default(); 5];
247        for (i, cut) in cuts.iter().enumerate() {
248            cut_array[i] = *cut;
249        }
250
251        // For now, assume intra-route (all cuts on same entity)
252        let entity_index = cuts[0].entity_index;
253
254        Self {
255            cuts: cut_array,
256            cut_count: cuts.len() as u8,
257            reconnection: *reconnection,
258            list_len,
259            list_get,
260            sublist_remove,
261            sublist_insert,
262            variable_name,
263            variable_id,
264            descriptor_index,
265            entity_index,
266            _phantom: PhantomData,
267        }
268    }
269
270    #[inline]
271    pub fn k(&self) -> usize {
272        self.cut_count as usize
273    }
274
275    #[inline]
276    pub fn cuts(&self) -> &[CutPoint] {
277        &self.cuts[..self.cut_count as usize]
278    }
279
280    pub fn is_intra_route(&self) -> bool {
281        let first = self.cuts[0].entity_index;
282        self.cuts[..self.cut_count as usize]
283            .iter()
284            .all(|c| c.entity_index == first)
285    }
286
287    fn access(&self) -> StaticListWindowAccess<S, V> {
288        StaticListWindowAccess {
289            list_len: self.list_len,
290            list_get: self.list_get,
291            sublist_remove: self.sublist_remove,
292            sublist_insert: self.sublist_insert,
293            variable_name: self.variable_name,
294            descriptor_index: self.descriptor_index,
295        }
296    }
297}
298
299impl<S, V> Move<S> for KOptMove<S, V>
300where
301    S: PlanningSolution,
302    V: Clone + Send + Sync + Debug + 'static,
303{
304    type Undo = Vec<V>;
305
306    fn is_doable<D: Director<S>>(&self, score_director: &D) -> bool {
307        k_opt_is_doable(
308            &self.access(),
309            self.cuts(),
310            &self.reconnection,
311            self.entity_index,
312            score_director,
313        )
314    }
315
316    fn do_move<D: Director<S>>(&self, score_director: &mut D) -> Self::Undo {
317        k_opt_do_move(
318            &self.access(),
319            self.cuts(),
320            &self.reconnection,
321            self.entity_index,
322            score_director,
323        )
324    }
325
326    fn undo_move<D: Director<S>>(&self, score_director: &mut D, undo: Self::Undo) {
327        k_opt_undo_move(&self.access(), self.entity_index, undo, score_director);
328    }
329
330    fn descriptor_index(&self) -> usize {
331        self.descriptor_index
332    }
333
334    fn entity_indices(&self) -> &[usize] {
335        std::slice::from_ref(&self.entity_index)
336    }
337
338    fn variable_name(&self) -> &str {
339        self.variable_name
340    }
341
342    fn telemetry_label(&self) -> &'static str {
343        "k_opt"
344    }
345
346    fn tabu_signature<D: Director<S>>(&self, score_director: &D) -> MoveTabuSignature {
347        k_opt_tabu_signature(
348            &self.access(),
349            self.cuts(),
350            &self.reconnection,
351            self.variable_id,
352            self.entity_index,
353            score_director,
354        )
355    }
356}