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