solverforge_solver/heuristic/move/
sublist_swap.rs

1//! SubListSwapMove - swaps two contiguous sublists within or between list variables.
2//!
3//! This move exchanges two ranges of elements. Essential for vehicle routing
4//! where segments need to be swapped between vehicles.
5//!
6//! # Zero-Erasure Design
7//!
8//! Uses typed function pointers for list operations. No `dyn Any`, no downcasting.
9
10use std::fmt::Debug;
11use std::marker::PhantomData;
12
13use solverforge_core::domain::PlanningSolution;
14use solverforge_scoring::ScoreDirector;
15
16use super::Move;
17
18/// A move that swaps two contiguous sublists.
19///
20/// Supports both intra-list swaps (within same entity) and inter-list swaps
21/// (between different entities). Uses typed function pointers for zero-erasure.
22///
23/// # Type Parameters
24/// * `S` - The planning solution type
25/// * `V` - The list element value type
26///
27/// # Example
28///
29/// ```
30/// use solverforge_solver::heuristic::r#move::SubListSwapMove;
31/// use solverforge_core::domain::PlanningSolution;
32/// use solverforge_core::score::SimpleScore;
33///
34/// #[derive(Clone, Debug)]
35/// struct Vehicle { id: usize, visits: Vec<i32> }
36///
37/// #[derive(Clone, Debug)]
38/// struct Solution { vehicles: Vec<Vehicle>, score: Option<SimpleScore> }
39///
40/// impl PlanningSolution for Solution {
41///     type Score = SimpleScore;
42///     fn score(&self) -> Option<Self::Score> { self.score }
43///     fn set_score(&mut self, score: Option<Self::Score>) { self.score = score; }
44/// }
45///
46/// fn list_len(s: &Solution, entity_idx: usize) -> usize {
47///     s.vehicles.get(entity_idx).map_or(0, |v| v.visits.len())
48/// }
49/// fn sublist_remove(s: &mut Solution, entity_idx: usize, start: usize, end: usize) -> Vec<i32> {
50///     s.vehicles.get_mut(entity_idx)
51///         .map(|v| v.visits.drain(start..end).collect())
52///         .unwrap_or_default()
53/// }
54/// fn sublist_insert(s: &mut Solution, entity_idx: usize, pos: usize, items: Vec<i32>) {
55///     if let Some(v) = s.vehicles.get_mut(entity_idx) {
56///         for (i, item) in items.into_iter().enumerate() {
57///             v.visits.insert(pos + i, item);
58///         }
59///     }
60/// }
61///
62/// // Swap [1..3) from vehicle 0 with [0..2) from vehicle 1
63/// let m = SubListSwapMove::<Solution, i32>::new(
64///     0, 1, 3,  // first: entity 0, range [1, 3)
65///     1, 0, 2,  // second: entity 1, range [0, 2)
66///     list_len, sublist_remove, sublist_insert,
67///     "visits", 0,
68/// );
69/// ```
70#[derive(Clone)]
71pub struct SubListSwapMove<S, V> {
72    /// First entity index
73    first_entity_index: usize,
74    /// Start of first range (inclusive)
75    first_start: usize,
76    /// End of first range (exclusive)
77    first_end: usize,
78    /// Second entity index
79    second_entity_index: usize,
80    /// Start of second range (inclusive)
81    second_start: usize,
82    /// End of second range (exclusive)
83    second_end: usize,
84    /// Get list length for an entity
85    list_len: fn(&S, usize) -> usize,
86    /// Remove sublist [start, end), returns removed elements
87    sublist_remove: fn(&mut S, usize, usize, usize) -> Vec<V>,
88    /// Insert elements at position
89    sublist_insert: fn(&mut S, usize, usize, Vec<V>),
90    variable_name: &'static str,
91    descriptor_index: usize,
92    /// Store indices for entity_indices()
93    indices: [usize; 2],
94    _phantom: PhantomData<V>,
95}
96
97impl<S, V: Debug> Debug for SubListSwapMove<S, V> {
98    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
99        f.debug_struct("SubListSwapMove")
100            .field("first_entity", &self.first_entity_index)
101            .field("first_range", &(self.first_start..self.first_end))
102            .field("second_entity", &self.second_entity_index)
103            .field("second_range", &(self.second_start..self.second_end))
104            .field("variable_name", &self.variable_name)
105            .finish()
106    }
107}
108
109impl<S, V> SubListSwapMove<S, V> {
110    /// Creates a new sublist swap move with typed function pointers.
111    ///
112    /// # Arguments
113    /// * `first_entity_index` - First entity index
114    /// * `first_start` - Start of first range (inclusive)
115    /// * `first_end` - End of first range (exclusive)
116    /// * `second_entity_index` - Second entity index
117    /// * `second_start` - Start of second range (inclusive)
118    /// * `second_end` - End of second range (exclusive)
119    /// * `list_len` - Function to get list length
120    /// * `sublist_remove` - Function to remove range [start, end)
121    /// * `sublist_insert` - Function to insert elements at position
122    /// * `variable_name` - Name of the list variable
123    /// * `descriptor_index` - Entity descriptor index
124    #[allow(clippy::too_many_arguments)]
125    pub fn new(
126        first_entity_index: usize,
127        first_start: usize,
128        first_end: usize,
129        second_entity_index: usize,
130        second_start: usize,
131        second_end: usize,
132        list_len: fn(&S, usize) -> usize,
133        sublist_remove: fn(&mut S, usize, usize, usize) -> Vec<V>,
134        sublist_insert: fn(&mut S, usize, usize, Vec<V>),
135        variable_name: &'static str,
136        descriptor_index: usize,
137    ) -> Self {
138        Self {
139            first_entity_index,
140            first_start,
141            first_end,
142            second_entity_index,
143            second_start,
144            second_end,
145            list_len,
146            sublist_remove,
147            sublist_insert,
148            variable_name,
149            descriptor_index,
150            indices: [first_entity_index, second_entity_index],
151            _phantom: PhantomData,
152        }
153    }
154
155    /// Returns the first entity index.
156    pub fn first_entity_index(&self) -> usize {
157        self.first_entity_index
158    }
159
160    /// Returns the first range start.
161    pub fn first_start(&self) -> usize {
162        self.first_start
163    }
164
165    /// Returns the first range end.
166    pub fn first_end(&self) -> usize {
167        self.first_end
168    }
169
170    /// Returns the first sublist length.
171    pub fn first_len(&self) -> usize {
172        self.first_end.saturating_sub(self.first_start)
173    }
174
175    /// Returns the second entity index.
176    pub fn second_entity_index(&self) -> usize {
177        self.second_entity_index
178    }
179
180    /// Returns the second range start.
181    pub fn second_start(&self) -> usize {
182        self.second_start
183    }
184
185    /// Returns the second range end.
186    pub fn second_end(&self) -> usize {
187        self.second_end
188    }
189
190    /// Returns the second sublist length.
191    pub fn second_len(&self) -> usize {
192        self.second_end.saturating_sub(self.second_start)
193    }
194
195    /// Returns true if this is an intra-list swap (same entity).
196    pub fn is_intra_list(&self) -> bool {
197        self.first_entity_index == self.second_entity_index
198    }
199}
200
201impl<S, V> Move<S> for SubListSwapMove<S, V>
202where
203    S: PlanningSolution,
204    V: Clone + Send + Sync + Debug + 'static,
205{
206    fn is_doable(&self, score_director: &dyn ScoreDirector<S>) -> bool {
207        let solution = score_director.working_solution();
208
209        // Both ranges must be valid (start < end)
210        if self.first_start >= self.first_end || self.second_start >= self.second_end {
211            return false;
212        }
213
214        // Check first range is within bounds
215        let first_list_len = (self.list_len)(solution, self.first_entity_index);
216        if self.first_end > first_list_len {
217            return false;
218        }
219
220        // Check second range is within bounds
221        let second_list_len = (self.list_len)(solution, self.second_entity_index);
222        if self.second_end > second_list_len {
223            return false;
224        }
225
226        // For intra-list swaps, ranges must not overlap
227        if self.is_intra_list() {
228            // Ranges overlap if one starts before the other ends
229            let overlaps = self.first_start < self.second_end && self.second_start < self.first_end;
230            if overlaps {
231                return false;
232            }
233        }
234
235        true
236    }
237
238    fn do_move(&self, score_director: &mut dyn ScoreDirector<S>) {
239        // Notify before changes
240        score_director.before_variable_changed(
241            self.descriptor_index,
242            self.first_entity_index,
243            self.variable_name,
244        );
245        if !self.is_intra_list() {
246            score_director.before_variable_changed(
247                self.descriptor_index,
248                self.second_entity_index,
249                self.variable_name,
250            );
251        }
252
253        if self.is_intra_list() {
254            // Intra-list swap: need to handle carefully due to index shifts
255            // Always process the later range first to avoid index shifts affecting the earlier range
256            let (early_start, early_end, late_start, late_end) =
257                if self.first_start < self.second_start {
258                    (
259                        self.first_start,
260                        self.first_end,
261                        self.second_start,
262                        self.second_end,
263                    )
264                } else {
265                    (
266                        self.second_start,
267                        self.second_end,
268                        self.first_start,
269                        self.first_end,
270                    )
271                };
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_start,
278                late_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_start,
286                early_end,
287            );
288
289            // Insert late elements at early position
290            (self.sublist_insert)(
291                score_director.working_solution_mut(),
292                self.first_entity_index,
293                early_start,
294                late_elements.clone(),
295            );
296
297            // Insert early elements at adjusted late position
298            // After removing early range, late_start shifts by early_len
299            // After inserting late elements, it shifts back by late_len
300            let late_len = late_end - late_start;
301            let early_len = early_end - early_start;
302            let new_late_pos = late_start - early_len + late_len;
303            (self.sublist_insert)(
304                score_director.working_solution_mut(),
305                self.first_entity_index,
306                new_late_pos,
307                early_elements.clone(),
308            );
309
310            // Register undo - swap back
311            let sublist_remove = self.sublist_remove;
312            let sublist_insert = self.sublist_insert;
313            let entity = self.first_entity_index;
314
315            score_director.register_undo(Box::new(move |s: &mut S| {
316                // Remove late elements (now at early position with late_len)
317                let late_at_early = sublist_remove(s, entity, early_start, early_start + late_len);
318                // Remove early elements (now at new_late_pos with early_len)
319                let early_at_late = sublist_remove(
320                    s,
321                    entity,
322                    new_late_pos - late_len,
323                    new_late_pos - late_len + early_len,
324                );
325                // Insert early back at early
326                sublist_insert(s, entity, early_start, early_at_late);
327                // Insert late back at late
328                sublist_insert(s, entity, late_start, late_at_early);
329            }));
330        } else {
331            // Inter-list swap: simpler, no index interaction between lists
332            let first_elements = (self.sublist_remove)(
333                score_director.working_solution_mut(),
334                self.first_entity_index,
335                self.first_start,
336                self.first_end,
337            );
338
339            let second_elements = (self.sublist_remove)(
340                score_director.working_solution_mut(),
341                self.second_entity_index,
342                self.second_start,
343                self.second_end,
344            );
345
346            // Insert swapped
347            (self.sublist_insert)(
348                score_director.working_solution_mut(),
349                self.first_entity_index,
350                self.first_start,
351                second_elements.clone(),
352            );
353
354            (self.sublist_insert)(
355                score_director.working_solution_mut(),
356                self.second_entity_index,
357                self.second_start,
358                first_elements.clone(),
359            );
360
361            // Register undo
362            let sublist_remove = self.sublist_remove;
363            let sublist_insert = self.sublist_insert;
364            let first_entity = self.first_entity_index;
365            let first_start = self.first_start;
366            let second_entity = self.second_entity_index;
367            let second_start = self.second_start;
368            let first_len = self.first_len();
369            let second_len = self.second_len();
370
371            score_director.register_undo(Box::new(move |s: &mut S| {
372                // Remove second elements from first list
373                let second_at_first =
374                    sublist_remove(s, first_entity, first_start, first_start + second_len);
375                // Remove first elements from second list
376                let first_at_second =
377                    sublist_remove(s, second_entity, second_start, second_start + first_len);
378                // Restore originals
379                sublist_insert(s, first_entity, first_start, first_at_second);
380                sublist_insert(s, second_entity, second_start, second_at_first);
381            }));
382        }
383
384        // Notify after changes
385        score_director.after_variable_changed(
386            self.descriptor_index,
387            self.first_entity_index,
388            self.variable_name,
389        );
390        if !self.is_intra_list() {
391            score_director.after_variable_changed(
392                self.descriptor_index,
393                self.second_entity_index,
394                self.variable_name,
395            );
396        }
397    }
398
399    fn descriptor_index(&self) -> usize {
400        self.descriptor_index
401    }
402
403    fn entity_indices(&self) -> &[usize] {
404        if self.is_intra_list() {
405            &self.indices[0..1]
406        } else {
407            &self.indices
408        }
409    }
410
411    fn variable_name(&self) -> &str {
412        self.variable_name
413    }
414}
415
416#[cfg(test)]
417mod tests {
418    use super::*;
419    use solverforge_core::domain::{EntityDescriptor, SolutionDescriptor, TypedEntityExtractor};
420    use solverforge_core::score::SimpleScore;
421    use solverforge_scoring::{RecordingScoreDirector, SimpleScoreDirector};
422    use std::any::TypeId;
423
424    #[derive(Clone, Debug)]
425    struct Vehicle {
426        visits: Vec<i32>,
427    }
428
429    #[derive(Clone, Debug)]
430    struct RoutingSolution {
431        vehicles: Vec<Vehicle>,
432        score: Option<SimpleScore>,
433    }
434
435    impl PlanningSolution for RoutingSolution {
436        type Score = SimpleScore;
437        fn score(&self) -> Option<Self::Score> {
438            self.score
439        }
440        fn set_score(&mut self, score: Option<Self::Score>) {
441            self.score = score;
442        }
443    }
444
445    fn get_vehicles(s: &RoutingSolution) -> &Vec<Vehicle> {
446        &s.vehicles
447    }
448    fn get_vehicles_mut(s: &mut RoutingSolution) -> &mut Vec<Vehicle> {
449        &mut s.vehicles
450    }
451
452    fn list_len(s: &RoutingSolution, entity_idx: usize) -> usize {
453        s.vehicles.get(entity_idx).map_or(0, |v| v.visits.len())
454    }
455    fn sublist_remove(
456        s: &mut RoutingSolution,
457        entity_idx: usize,
458        start: usize,
459        end: usize,
460    ) -> Vec<i32> {
461        s.vehicles
462            .get_mut(entity_idx)
463            .map(|v| v.visits.drain(start..end).collect())
464            .unwrap_or_default()
465    }
466    fn sublist_insert(s: &mut RoutingSolution, entity_idx: usize, pos: usize, items: Vec<i32>) {
467        if let Some(v) = s.vehicles.get_mut(entity_idx) {
468            for (i, item) in items.into_iter().enumerate() {
469                v.visits.insert(pos + i, item);
470            }
471        }
472    }
473
474    fn create_director(
475        vehicles: Vec<Vehicle>,
476    ) -> SimpleScoreDirector<RoutingSolution, impl Fn(&RoutingSolution) -> SimpleScore> {
477        let solution = RoutingSolution {
478            vehicles,
479            score: None,
480        };
481        let extractor = Box::new(TypedEntityExtractor::new(
482            "Vehicle",
483            "vehicles",
484            get_vehicles,
485            get_vehicles_mut,
486        ));
487        let entity_desc = EntityDescriptor::new("Vehicle", TypeId::of::<Vehicle>(), "vehicles")
488            .with_extractor(extractor);
489        let descriptor =
490            SolutionDescriptor::new("RoutingSolution", TypeId::of::<RoutingSolution>())
491                .with_entity(entity_desc);
492        SimpleScoreDirector::with_calculator(solution, descriptor, |_| SimpleScore::of(0))
493    }
494
495    #[test]
496    fn inter_list_swap() {
497        let vehicles = vec![
498            Vehicle {
499                visits: vec![1, 2, 3, 4],
500            },
501            Vehicle {
502                visits: vec![10, 20, 30],
503            },
504        ];
505        let mut director = create_director(vehicles);
506
507        // Swap [1..3) from vehicle 0 with [0..2) from vehicle 1
508        // [1, 2, 3, 4] swapping [2, 3] with [10, 20] from [10, 20, 30]
509        let m = SubListSwapMove::<RoutingSolution, i32>::new(
510            0,
511            1,
512            3,
513            1,
514            0,
515            2,
516            list_len,
517            sublist_remove,
518            sublist_insert,
519            "visits",
520            0,
521        );
522
523        assert!(m.is_doable(&director));
524
525        {
526            let mut recording = RecordingScoreDirector::new(&mut director);
527            m.do_move(&mut recording);
528
529            let sol = recording.working_solution();
530            assert_eq!(sol.vehicles[0].visits, vec![1, 10, 20, 4]);
531            assert_eq!(sol.vehicles[1].visits, vec![2, 3, 30]);
532
533            recording.undo_changes();
534        }
535
536        let sol = director.working_solution();
537        assert_eq!(sol.vehicles[0].visits, vec![1, 2, 3, 4]);
538        assert_eq!(sol.vehicles[1].visits, vec![10, 20, 30]);
539    }
540
541    #[test]
542    fn intra_list_swap() {
543        let vehicles = vec![Vehicle {
544            visits: vec![1, 2, 3, 4, 5, 6, 7, 8],
545        }];
546        let mut director = create_director(vehicles);
547
548        // Swap [1..3) with [5..7) in same list
549        // [1, 2, 3, 4, 5, 6, 7, 8] -> swap [2, 3] with [6, 7]
550        let m = SubListSwapMove::<RoutingSolution, i32>::new(
551            0,
552            1,
553            3,
554            0,
555            5,
556            7,
557            list_len,
558            sublist_remove,
559            sublist_insert,
560            "visits",
561            0,
562        );
563
564        assert!(m.is_doable(&director));
565
566        {
567            let mut recording = RecordingScoreDirector::new(&mut director);
568            m.do_move(&mut recording);
569
570            // After: [1, 6, 7, 4, 5, 2, 3, 8]
571            let visits = &recording.working_solution().vehicles[0].visits;
572            assert_eq!(visits, &[1, 6, 7, 4, 5, 2, 3, 8]);
573
574            recording.undo_changes();
575        }
576
577        let visits = &director.working_solution().vehicles[0].visits;
578        assert_eq!(visits, &[1, 2, 3, 4, 5, 6, 7, 8]);
579    }
580
581    #[test]
582    fn overlapping_ranges_not_doable() {
583        let vehicles = vec![Vehicle {
584            visits: vec![1, 2, 3, 4, 5],
585        }];
586        let director = create_director(vehicles);
587
588        // Ranges [1..4) and [2..5) overlap
589        let m = SubListSwapMove::<RoutingSolution, i32>::new(
590            0,
591            1,
592            4,
593            0,
594            2,
595            5,
596            list_len,
597            sublist_remove,
598            sublist_insert,
599            "visits",
600            0,
601        );
602
603        assert!(!m.is_doable(&director));
604    }
605
606    #[test]
607    fn empty_range_not_doable() {
608        let vehicles = vec![Vehicle {
609            visits: vec![1, 2, 3],
610        }];
611        let director = create_director(vehicles);
612
613        let m = SubListSwapMove::<RoutingSolution, i32>::new(
614            0,
615            1,
616            1,
617            0,
618            2,
619            3,
620            list_len,
621            sublist_remove,
622            sublist_insert,
623            "visits",
624            0,
625        );
626
627        assert!(!m.is_doable(&director));
628    }
629
630    #[test]
631    fn out_of_bounds_not_doable() {
632        let vehicles = vec![Vehicle {
633            visits: vec![1, 2, 3],
634        }];
635        let director = create_director(vehicles);
636
637        let m = SubListSwapMove::<RoutingSolution, i32>::new(
638            0,
639            0,
640            2,
641            0,
642            2,
643            10,
644            list_len,
645            sublist_remove,
646            sublist_insert,
647            "visits",
648            0,
649        );
650
651        assert!(!m.is_doable(&director));
652    }
653}