Skip to main content

solverforge_solver/heuristic/move/
sublist_swap.rs

1/* SublistSwapMove - swaps two contiguous sublists within or between list variables.
2
3This move exchanges two ranges of elements. Essential for vehicle routing
4where segments need to be swapped between vehicles.
5
6# Zero-Erasure Design
7
8Uses concrete function pointers for list operations. No `dyn Any`, no downcasting.
9*/
10
11use std::fmt::Debug;
12use std::marker::PhantomData;
13
14use smallvec::{smallvec, SmallVec};
15use solverforge_core::domain::PlanningSolution;
16use solverforge_scoring::Director;
17
18use super::metadata::{
19    encode_option_debug, encode_usize, hash_str, MoveTabuScope, ScopedEntityTabuToken,
20    ScopedValueTabuToken,
21};
22use super::segment_layout::derive_segment_swap_layout;
23use super::{Move, MoveTabuSignature};
24
25/// A move that swaps two contiguous sublists.
26///
27/// Supports both intra-list swaps (within same entity) and inter-list swaps
28/// (between different entities). Uses concrete function pointers for zero-erasure.
29///
30/// # Type Parameters
31/// * `S` - The planning solution type
32/// * `V` - The list element value type
33///
34/// # Example
35///
36/// ```
37/// use solverforge_solver::heuristic::r#move::SublistSwapMove;
38/// use solverforge_core::domain::PlanningSolution;
39/// use solverforge_core::score::SoftScore;
40///
41/// #[derive(Clone, Debug)]
42/// struct Vehicle { id: usize, visits: Vec<i32> }
43///
44/// #[derive(Clone, Debug)]
45/// struct Solution { vehicles: Vec<Vehicle>, score: Option<SoftScore> }
46///
47/// impl PlanningSolution for Solution {
48///     type Score = SoftScore;
49///     fn score(&self) -> Option<Self::Score> { self.score }
50///     fn set_score(&mut self, score: Option<Self::Score>) { self.score = score; }
51/// }
52///
53/// fn list_len(s: &Solution, entity_idx: usize) -> usize {
54///     s.vehicles.get(entity_idx).map_or(0, |v| v.visits.len())
55/// }
56/// fn list_get(s: &Solution, entity_idx: usize, pos: usize) -> Option<i32> {
57///     s.vehicles
58///         .get(entity_idx)
59///         .and_then(|v| v.visits.get(pos))
60///         .copied()
61/// }
62/// fn sublist_remove(s: &mut Solution, entity_idx: usize, start: usize, end: usize) -> Vec<i32> {
63///     s.vehicles.get_mut(entity_idx)
64///         .map(|v| v.visits.drain(start..end).collect())
65///         .unwrap_or_default()
66/// }
67/// fn sublist_insert(s: &mut Solution, entity_idx: usize, pos: usize, items: Vec<i32>) {
68///     if let Some(v) = s.vehicles.get_mut(entity_idx) {
69///         for (i, item) in items.into_iter().enumerate() {
70///             v.visits.insert(pos + i, item);
71///         }
72///     }
73/// }
74///
75/// // Swap [1..3) from vehicle 0 with [0..2) from vehicle 1
76/// let m = SublistSwapMove::<Solution, i32>::new(
77///     0, 1, 3,  // first: entity 0, range [1, 3)
78///     1, 0, 2,  // second: entity 1, range [0, 2)
79///     list_len, list_get, sublist_remove, sublist_insert,
80///     "visits", 0,
81/// );
82/// ```
83pub struct SublistSwapMove<S, V> {
84    // First entity index
85    first_entity_index: usize,
86    // Start of first range (inclusive)
87    first_start: usize,
88    // End of first range (exclusive)
89    first_end: usize,
90    // Second entity index
91    second_entity_index: usize,
92    // Start of second range (inclusive)
93    second_start: usize,
94    // End of second range (exclusive)
95    second_end: usize,
96    list_len: fn(&S, usize) -> usize,
97    list_get: fn(&S, usize, usize) -> Option<V>,
98    // Remove sublist [start, end), returns removed elements
99    sublist_remove: fn(&mut S, usize, usize, usize) -> Vec<V>,
100    // Insert elements at position
101    sublist_insert: fn(&mut S, usize, usize, Vec<V>),
102    variable_name: &'static str,
103    descriptor_index: usize,
104    // Store indices for entity_indices()
105    indices: [usize; 2],
106    _phantom: PhantomData<fn() -> V>,
107}
108
109impl<S, V> Clone for SublistSwapMove<S, V> {
110    fn clone(&self) -> Self {
111        *self
112    }
113}
114
115impl<S, V> Copy for SublistSwapMove<S, V> {}
116
117impl<S, V: Debug> Debug for SublistSwapMove<S, V> {
118    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
119        f.debug_struct("SublistSwapMove")
120            .field("first_entity", &self.first_entity_index)
121            .field("first_range", &(self.first_start..self.first_end))
122            .field("second_entity", &self.second_entity_index)
123            .field("second_range", &(self.second_start..self.second_end))
124            .field("variable_name", &self.variable_name)
125            .finish()
126    }
127}
128
129impl<S, V> SublistSwapMove<S, V> {
130    /* Creates a new sublist swap move with concrete function pointers.
131
132    # Arguments
133    * `first_entity_index` - First entity index
134    * `first_start` - Start of first range (inclusive)
135    * `first_end` - End of first range (exclusive)
136    * `second_entity_index` - Second entity index
137    * `second_start` - Start of second range (inclusive)
138    * `second_end` - End of second range (exclusive)
139    * `list_len` - Function to get list length
140    * `sublist_remove` - Function to remove range [start, end)
141    * `sublist_insert` - Function to insert elements at position
142    * `variable_name` - Name of the list variable
143    * `descriptor_index` - Entity descriptor index
144    */
145    #[allow(clippy::too_many_arguments)]
146    pub fn new(
147        first_entity_index: usize,
148        first_start: usize,
149        first_end: usize,
150        second_entity_index: usize,
151        second_start: usize,
152        second_end: usize,
153        list_len: fn(&S, usize) -> usize,
154        list_get: fn(&S, usize, usize) -> Option<V>,
155        sublist_remove: fn(&mut S, usize, usize, usize) -> Vec<V>,
156        sublist_insert: fn(&mut S, usize, usize, Vec<V>),
157        variable_name: &'static str,
158        descriptor_index: usize,
159    ) -> Self {
160        Self {
161            first_entity_index,
162            first_start,
163            first_end,
164            second_entity_index,
165            second_start,
166            second_end,
167            list_len,
168            list_get,
169            sublist_remove,
170            sublist_insert,
171            variable_name,
172            descriptor_index,
173            indices: [first_entity_index, second_entity_index],
174            _phantom: PhantomData,
175        }
176    }
177
178    pub fn first_entity_index(&self) -> usize {
179        self.first_entity_index
180    }
181
182    pub fn first_start(&self) -> usize {
183        self.first_start
184    }
185
186    pub fn first_end(&self) -> usize {
187        self.first_end
188    }
189
190    pub fn first_len(&self) -> usize {
191        self.first_end.saturating_sub(self.first_start)
192    }
193
194    pub fn second_entity_index(&self) -> usize {
195        self.second_entity_index
196    }
197
198    pub fn second_start(&self) -> usize {
199        self.second_start
200    }
201
202    pub fn second_end(&self) -> usize {
203        self.second_end
204    }
205
206    pub fn second_len(&self) -> usize {
207        self.second_end.saturating_sub(self.second_start)
208    }
209
210    pub fn is_intra_list(&self) -> bool {
211        self.first_entity_index == self.second_entity_index
212    }
213}
214
215impl<S, V> Move<S> for SublistSwapMove<S, V>
216where
217    S: PlanningSolution,
218    V: Clone + Send + Sync + Debug + 'static,
219{
220    type Undo = ();
221
222    fn is_doable<D: Director<S>>(&self, score_director: &D) -> bool {
223        let solution = score_director.working_solution();
224
225        // Both ranges must be valid (start < end)
226        if self.first_start >= self.first_end || self.second_start >= self.second_end {
227            return false;
228        }
229
230        // Check first range is within bounds
231        let first_list_len = (self.list_len)(solution, self.first_entity_index);
232        if self.first_end > first_list_len {
233            return false;
234        }
235
236        // Check second range is within bounds
237        let second_list_len = (self.list_len)(solution, self.second_entity_index);
238        if self.second_end > second_list_len {
239            return false;
240        }
241
242        // For intra-list swaps, ranges must not overlap
243        if self.is_intra_list() {
244            // Ranges overlap if one starts before the other ends
245            let overlaps = self.first_start < self.second_end && self.second_start < self.first_end;
246            if overlaps {
247                return false;
248            }
249        }
250
251        true
252    }
253
254    fn do_move<D: Director<S>>(&self, score_director: &mut D) -> Self::Undo {
255        let layout = derive_segment_swap_layout(
256            self.first_entity_index,
257            self.first_start,
258            self.first_end,
259            self.second_entity_index,
260            self.second_start,
261            self.second_end,
262        );
263
264        // Notify before changes
265        score_director.before_variable_changed(self.descriptor_index, self.first_entity_index);
266        if !self.is_intra_list() {
267            score_director.before_variable_changed(self.descriptor_index, self.second_entity_index);
268        }
269
270        if self.is_intra_list() {
271            let (early_range, late_range) = layout.exact.ordered_ranges();
272
273            // Remove later range first
274            let late_elements = (self.sublist_remove)(
275                score_director.working_solution_mut(),
276                self.first_entity_index,
277                late_range.start,
278                late_range.end,
279            );
280
281            // Remove earlier range
282            let early_elements = (self.sublist_remove)(
283                score_director.working_solution_mut(),
284                self.first_entity_index,
285                early_range.start,
286                early_range.end,
287            );
288
289            // Insert late elements at early position
290            let late_len = late_range.len();
291            let early_len = early_range.len();
292            (self.sublist_insert)(
293                score_director.working_solution_mut(),
294                self.first_entity_index,
295                early_range.start,
296                late_elements,
297            );
298
299            /* Insert early elements at adjusted late position
300            After removing early range, late_start shifts by early_len
301            After inserting late elements, it shifts back by late_len
302            */
303            let new_late_pos = late_range.start - early_len + late_len;
304            (self.sublist_insert)(
305                score_director.working_solution_mut(),
306                self.first_entity_index,
307                new_late_pos,
308                early_elements,
309            );
310        } else {
311            // Inter-list swap: simpler, no index interaction between lists
312            let first_elements = (self.sublist_remove)(
313                score_director.working_solution_mut(),
314                self.first_entity_index,
315                self.first_start,
316                self.first_end,
317            );
318
319            let second_elements = (self.sublist_remove)(
320                score_director.working_solution_mut(),
321                self.second_entity_index,
322                self.second_start,
323                self.second_end,
324            );
325
326            // Insert swapped
327            (self.sublist_insert)(
328                score_director.working_solution_mut(),
329                self.first_entity_index,
330                self.first_start,
331                second_elements,
332            );
333
334            (self.sublist_insert)(
335                score_director.working_solution_mut(),
336                self.second_entity_index,
337                self.second_start,
338                first_elements,
339            );
340        }
341
342        // Notify after changes
343        score_director.after_variable_changed(self.descriptor_index, self.first_entity_index);
344        if !self.is_intra_list() {
345            score_director.after_variable_changed(self.descriptor_index, self.second_entity_index);
346        }
347    }
348
349    fn undo_move<D: Director<S>>(&self, score_director: &mut D, (): Self::Undo) {
350        let inverse = derive_segment_swap_layout(
351            self.first_entity_index,
352            self.first_start,
353            self.first_end,
354            self.second_entity_index,
355            self.second_start,
356            self.second_end,
357        )
358        .inverse;
359        score_director.before_variable_changed(self.descriptor_index, inverse.first_entity_index);
360        if inverse.first_entity_index != inverse.second_entity_index {
361            score_director
362                .before_variable_changed(self.descriptor_index, inverse.second_entity_index);
363        }
364
365        if inverse.first_entity_index == inverse.second_entity_index {
366            let (early_range, late_range) = inverse.ordered_ranges();
367            let late_elements = (self.sublist_remove)(
368                score_director.working_solution_mut(),
369                inverse.first_entity_index,
370                late_range.start,
371                late_range.end,
372            );
373            let early_elements = (self.sublist_remove)(
374                score_director.working_solution_mut(),
375                inverse.first_entity_index,
376                early_range.start,
377                early_range.end,
378            );
379            let late_len = late_range.len();
380            let early_len = early_range.len();
381            (self.sublist_insert)(
382                score_director.working_solution_mut(),
383                inverse.first_entity_index,
384                early_range.start,
385                late_elements,
386            );
387            let new_late_pos = late_range.start - early_len + late_len;
388            (self.sublist_insert)(
389                score_director.working_solution_mut(),
390                inverse.first_entity_index,
391                new_late_pos,
392                early_elements,
393            );
394        } else {
395            let first_elements = (self.sublist_remove)(
396                score_director.working_solution_mut(),
397                inverse.first_entity_index,
398                inverse.first_range.start,
399                inverse.first_range.end,
400            );
401            let second_elements = (self.sublist_remove)(
402                score_director.working_solution_mut(),
403                inverse.second_entity_index,
404                inverse.second_range.start,
405                inverse.second_range.end,
406            );
407            (self.sublist_insert)(
408                score_director.working_solution_mut(),
409                inverse.first_entity_index,
410                inverse.first_range.start,
411                second_elements,
412            );
413            (self.sublist_insert)(
414                score_director.working_solution_mut(),
415                inverse.second_entity_index,
416                inverse.second_range.start,
417                first_elements,
418            );
419        }
420
421        score_director.after_variable_changed(self.descriptor_index, inverse.first_entity_index);
422        if inverse.first_entity_index != inverse.second_entity_index {
423            score_director
424                .after_variable_changed(self.descriptor_index, inverse.second_entity_index);
425        }
426    }
427
428    fn descriptor_index(&self) -> usize {
429        self.descriptor_index
430    }
431
432    fn entity_indices(&self) -> &[usize] {
433        if self.is_intra_list() {
434            &self.indices[0..1]
435        } else {
436            &self.indices
437        }
438    }
439
440    fn variable_name(&self) -> &str {
441        self.variable_name
442    }
443
444    fn tabu_signature<D: Director<S>>(&self, score_director: &D) -> MoveTabuSignature {
445        let layout = derive_segment_swap_layout(
446            self.first_entity_index,
447            self.first_start,
448            self.first_end,
449            self.second_entity_index,
450            self.second_start,
451            self.second_end,
452        );
453        let mut first_value_ids: SmallVec<[u64; 2]> = SmallVec::new();
454        for pos in self.first_start..self.first_end {
455            let value = (self.list_get)(
456                score_director.working_solution(),
457                self.first_entity_index,
458                pos,
459            );
460            first_value_ids.push(encode_option_debug(value.as_ref()));
461        }
462        let mut second_value_ids: SmallVec<[u64; 2]> = SmallVec::new();
463        for pos in self.second_start..self.second_end {
464            let value = (self.list_get)(
465                score_director.working_solution(),
466                self.second_entity_index,
467                pos,
468            );
469            second_value_ids.push(encode_option_debug(value.as_ref()));
470        }
471        let first_entity_id = encode_usize(self.first_entity_index);
472        let second_entity_id = encode_usize(self.second_entity_index);
473        let variable_id = hash_str(self.variable_name);
474        let scope = MoveTabuScope::new(self.descriptor_index, self.variable_name);
475        let mut entity_tokens: SmallVec<[ScopedEntityTabuToken; 2]> =
476            smallvec![scope.entity_token(first_entity_id)];
477        if !self.is_intra_list() {
478            entity_tokens.push(scope.entity_token(second_entity_id));
479        }
480        let destination_value_tokens: SmallVec<[ScopedValueTabuToken; 2]> = first_value_ids
481            .iter()
482            .chain(second_value_ids.iter())
483            .copied()
484            .map(|value_id| scope.value_token(value_id))
485            .collect();
486        let mut move_id = smallvec![
487            encode_usize(self.descriptor_index),
488            variable_id,
489            encode_usize(layout.exact.first_entity_index),
490            encode_usize(layout.exact.first_range.start),
491            encode_usize(layout.exact.first_range.end),
492            encode_usize(layout.exact.second_entity_index),
493            encode_usize(layout.exact.second_range.start),
494            encode_usize(layout.exact.second_range.end)
495        ];
496        move_id.extend(first_value_ids.iter().copied());
497        move_id.extend(second_value_ids.iter().copied());
498        let mut undo_move_id = smallvec![
499            encode_usize(self.descriptor_index),
500            variable_id,
501            encode_usize(layout.inverse.first_entity_index),
502            encode_usize(layout.inverse.first_range.start),
503            encode_usize(layout.inverse.first_range.end),
504            encode_usize(layout.inverse.second_entity_index),
505            encode_usize(layout.inverse.second_range.start),
506            encode_usize(layout.inverse.second_range.end)
507        ];
508        undo_move_id.extend(second_value_ids.iter().copied());
509        undo_move_id.extend(first_value_ids.iter().copied());
510
511        MoveTabuSignature::new(scope, move_id, undo_move_id)
512            .with_entity_tokens(entity_tokens)
513            .with_destination_value_tokens(destination_value_tokens)
514    }
515}