solverforge_solver/heuristic/move/
sublist_change.rs

1//! SubListChangeMove - relocates a contiguous sublist within or between list variables.
2//!
3//! This move removes a range of elements from one position and inserts them at another.
4//! Essential for vehicle routing where multiple consecutive stops need relocation.
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 relocates a contiguous sublist from one position to another.
19///
20/// Supports both intra-list moves (within same entity) and inter-list moves
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::SubListChangeMove;
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/// // Move elements [1..3) from vehicle 0 to vehicle 1 at position 0
63/// let m = SubListChangeMove::<Solution, i32>::new(
64///     0, 1, 3,  // source: entity 0, range [1, 3)
65///     1, 0,     // dest: entity 1, position 0
66///     list_len, sublist_remove, sublist_insert,
67///     "visits", 0,
68/// );
69/// ```
70#[derive(Clone)]
71pub struct SubListChangeMove<S, V> {
72    /// Source entity index
73    source_entity_index: usize,
74    /// Start of range in source list (inclusive)
75    source_start: usize,
76    /// End of range in source list (exclusive)
77    source_end: usize,
78    /// Destination entity index
79    dest_entity_index: usize,
80    /// Position in destination list to insert at
81    dest_position: usize,
82    /// Get list length for an entity
83    list_len: fn(&S, usize) -> usize,
84    /// Remove sublist [start, end), returns removed elements
85    sublist_remove: fn(&mut S, usize, usize, usize) -> Vec<V>,
86    /// Insert elements at position
87    sublist_insert: fn(&mut S, usize, usize, Vec<V>),
88    variable_name: &'static str,
89    descriptor_index: usize,
90    /// Store indices for entity_indices()
91    indices: [usize; 2],
92    _phantom: PhantomData<V>,
93}
94
95impl<S, V: Debug> Debug for SubListChangeMove<S, V> {
96    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
97        f.debug_struct("SubListChangeMove")
98            .field("source_entity", &self.source_entity_index)
99            .field("source_range", &(self.source_start..self.source_end))
100            .field("dest_entity", &self.dest_entity_index)
101            .field("dest_position", &self.dest_position)
102            .field("variable_name", &self.variable_name)
103            .finish()
104    }
105}
106
107impl<S, V> SubListChangeMove<S, V> {
108    /// Creates a new sublist change move with typed function pointers.
109    ///
110    /// # Arguments
111    /// * `source_entity_index` - Entity index to remove from
112    /// * `source_start` - Start of range (inclusive)
113    /// * `source_end` - End of range (exclusive)
114    /// * `dest_entity_index` - Entity index to insert into
115    /// * `dest_position` - Position in destination list
116    /// * `list_len` - Function to get list length
117    /// * `sublist_remove` - Function to remove range [start, end)
118    /// * `sublist_insert` - Function to insert elements at position
119    /// * `variable_name` - Name of the list variable
120    /// * `descriptor_index` - Entity descriptor index
121    #[allow(clippy::too_many_arguments)]
122    pub fn new(
123        source_entity_index: usize,
124        source_start: usize,
125        source_end: usize,
126        dest_entity_index: usize,
127        dest_position: usize,
128        list_len: fn(&S, usize) -> usize,
129        sublist_remove: fn(&mut S, usize, usize, usize) -> Vec<V>,
130        sublist_insert: fn(&mut S, usize, usize, Vec<V>),
131        variable_name: &'static str,
132        descriptor_index: usize,
133    ) -> Self {
134        Self {
135            source_entity_index,
136            source_start,
137            source_end,
138            dest_entity_index,
139            dest_position,
140            list_len,
141            sublist_remove,
142            sublist_insert,
143            variable_name,
144            descriptor_index,
145            indices: [source_entity_index, dest_entity_index],
146            _phantom: PhantomData,
147        }
148    }
149
150    /// Returns the source entity index.
151    pub fn source_entity_index(&self) -> usize {
152        self.source_entity_index
153    }
154
155    /// Returns the source range start (inclusive).
156    pub fn source_start(&self) -> usize {
157        self.source_start
158    }
159
160    /// Returns the source range end (exclusive).
161    pub fn source_end(&self) -> usize {
162        self.source_end
163    }
164
165    /// Returns the sublist length.
166    pub fn sublist_len(&self) -> usize {
167        self.source_end.saturating_sub(self.source_start)
168    }
169
170    /// Returns the destination entity index.
171    pub fn dest_entity_index(&self) -> usize {
172        self.dest_entity_index
173    }
174
175    /// Returns the destination position.
176    pub fn dest_position(&self) -> usize {
177        self.dest_position
178    }
179
180    /// Returns true if this is an intra-list move (same entity).
181    pub fn is_intra_list(&self) -> bool {
182        self.source_entity_index == self.dest_entity_index
183    }
184}
185
186impl<S, V> Move<S> for SubListChangeMove<S, V>
187where
188    S: PlanningSolution,
189    V: Clone + Send + Sync + Debug + 'static,
190{
191    fn is_doable(&self, score_director: &dyn ScoreDirector<S>) -> bool {
192        let solution = score_director.working_solution();
193
194        // Check range is valid (start < end)
195        if self.source_start >= self.source_end {
196            return false;
197        }
198
199        // Check source range is within bounds
200        let source_len = (self.list_len)(solution, self.source_entity_index);
201        if self.source_end > source_len {
202            return false;
203        }
204
205        // Check destination position is valid
206        let dest_len = (self.list_len)(solution, self.dest_entity_index);
207        let sublist_len = self.sublist_len();
208
209        let max_dest = if self.is_intra_list() {
210            // After removing sublist, list is shorter
211            source_len - sublist_len
212        } else {
213            dest_len
214        };
215
216        if self.dest_position > max_dest {
217            return false;
218        }
219
220        // For intra-list, check if move would actually change anything
221        if self.is_intra_list() {
222            // If dest is within the source range, it's a no-op
223            if self.dest_position >= self.source_start && self.dest_position <= self.source_end {
224                return false;
225            }
226        }
227
228        true
229    }
230
231    fn do_move(&self, score_director: &mut dyn ScoreDirector<S>) {
232        // Notify before changes
233        score_director.before_variable_changed(
234            self.descriptor_index,
235            self.source_entity_index,
236            self.variable_name,
237        );
238        if !self.is_intra_list() {
239            score_director.before_variable_changed(
240                self.descriptor_index,
241                self.dest_entity_index,
242                self.variable_name,
243            );
244        }
245
246        // Remove sublist from source
247        let elements = (self.sublist_remove)(
248            score_director.working_solution_mut(),
249            self.source_entity_index,
250            self.source_start,
251            self.source_end,
252        );
253
254        // dest_position is relative to post-removal list, no adjustment needed
255        let dest_pos = self.dest_position;
256
257        // Insert at destination
258        (self.sublist_insert)(
259            score_director.working_solution_mut(),
260            self.dest_entity_index,
261            dest_pos,
262            elements.clone(),
263        );
264
265        // Notify after changes
266        score_director.after_variable_changed(
267            self.descriptor_index,
268            self.source_entity_index,
269            self.variable_name,
270        );
271        if !self.is_intra_list() {
272            score_director.after_variable_changed(
273                self.descriptor_index,
274                self.dest_entity_index,
275                self.variable_name,
276            );
277        }
278
279        // Register undo
280        let sublist_remove = self.sublist_remove;
281        let sublist_insert = self.sublist_insert;
282        let src_entity = self.source_entity_index;
283        let src_start = self.source_start;
284        let dest_entity = self.dest_entity_index;
285        let sublist_len = self.sublist_len();
286
287        score_director.register_undo(Box::new(move |s: &mut S| {
288            // Remove from where we inserted
289            let removed = sublist_remove(s, dest_entity, dest_pos, dest_pos + sublist_len);
290            // Insert back at original position
291            sublist_insert(s, src_entity, src_start, removed);
292        }));
293    }
294
295    fn descriptor_index(&self) -> usize {
296        self.descriptor_index
297    }
298
299    fn entity_indices(&self) -> &[usize] {
300        if self.is_intra_list() {
301            &self.indices[0..1]
302        } else {
303            &self.indices
304        }
305    }
306
307    fn variable_name(&self) -> &str {
308        self.variable_name
309    }
310}
311
312#[cfg(test)]
313mod tests {
314    use super::*;
315    use solverforge_core::domain::{EntityDescriptor, SolutionDescriptor, TypedEntityExtractor};
316    use solverforge_core::score::SimpleScore;
317    use solverforge_scoring::{RecordingScoreDirector, SimpleScoreDirector};
318    use std::any::TypeId;
319
320    #[derive(Clone, Debug)]
321    struct Vehicle {
322        visits: Vec<i32>,
323    }
324
325    #[derive(Clone, Debug)]
326    struct RoutingSolution {
327        vehicles: Vec<Vehicle>,
328        score: Option<SimpleScore>,
329    }
330
331    impl PlanningSolution for RoutingSolution {
332        type Score = SimpleScore;
333        fn score(&self) -> Option<Self::Score> {
334            self.score
335        }
336        fn set_score(&mut self, score: Option<Self::Score>) {
337            self.score = score;
338        }
339    }
340
341    fn get_vehicles(s: &RoutingSolution) -> &Vec<Vehicle> {
342        &s.vehicles
343    }
344    fn get_vehicles_mut(s: &mut RoutingSolution) -> &mut Vec<Vehicle> {
345        &mut s.vehicles
346    }
347
348    fn list_len(s: &RoutingSolution, entity_idx: usize) -> usize {
349        s.vehicles.get(entity_idx).map_or(0, |v| v.visits.len())
350    }
351    fn sublist_remove(
352        s: &mut RoutingSolution,
353        entity_idx: usize,
354        start: usize,
355        end: usize,
356    ) -> Vec<i32> {
357        s.vehicles
358            .get_mut(entity_idx)
359            .map(|v| v.visits.drain(start..end).collect())
360            .unwrap_or_default()
361    }
362    fn sublist_insert(s: &mut RoutingSolution, entity_idx: usize, pos: usize, items: Vec<i32>) {
363        if let Some(v) = s.vehicles.get_mut(entity_idx) {
364            for (i, item) in items.into_iter().enumerate() {
365                v.visits.insert(pos + i, item);
366            }
367        }
368    }
369
370    fn create_director(
371        vehicles: Vec<Vehicle>,
372    ) -> SimpleScoreDirector<RoutingSolution, impl Fn(&RoutingSolution) -> SimpleScore> {
373        let solution = RoutingSolution {
374            vehicles,
375            score: None,
376        };
377        let extractor = Box::new(TypedEntityExtractor::new(
378            "Vehicle",
379            "vehicles",
380            get_vehicles,
381            get_vehicles_mut,
382        ));
383        let entity_desc = EntityDescriptor::new("Vehicle", TypeId::of::<Vehicle>(), "vehicles")
384            .with_extractor(extractor);
385        let descriptor =
386            SolutionDescriptor::new("RoutingSolution", TypeId::of::<RoutingSolution>())
387                .with_entity(entity_desc);
388        SimpleScoreDirector::with_calculator(solution, descriptor, |_| SimpleScore::of(0))
389    }
390
391    #[test]
392    fn intra_list_move_forward() {
393        let vehicles = vec![Vehicle {
394            visits: vec![1, 2, 3, 4, 5, 6],
395        }];
396        let mut director = create_director(vehicles);
397
398        // Move elements [1..3) (values 2, 3) to end of list
399        // After removing [1..3), list is [1, 4, 5, 6], insert at position 4
400        let m = SubListChangeMove::<RoutingSolution, i32>::new(
401            0,
402            1,
403            3,
404            0,
405            4, // Position in post-removal list
406            list_len,
407            sublist_remove,
408            sublist_insert,
409            "visits",
410            0,
411        );
412
413        assert!(m.is_doable(&director));
414
415        {
416            let mut recording = RecordingScoreDirector::new(&mut director);
417            m.do_move(&mut recording);
418
419            // After: [1, 4, 5, 6, 2, 3]
420            let visits = &recording.working_solution().vehicles[0].visits;
421            assert_eq!(visits, &[1, 4, 5, 6, 2, 3]);
422
423            recording.undo_changes();
424        }
425
426        let visits = &director.working_solution().vehicles[0].visits;
427        assert_eq!(visits, &[1, 2, 3, 4, 5, 6]);
428    }
429
430    #[test]
431    fn intra_list_move_backward() {
432        let vehicles = vec![Vehicle {
433            visits: vec![1, 2, 3, 4, 5, 6],
434        }];
435        let mut director = create_director(vehicles);
436
437        // Move elements [3..5) (values 4, 5) to position 1
438        let m = SubListChangeMove::<RoutingSolution, i32>::new(
439            0,
440            3,
441            5,
442            0,
443            1,
444            list_len,
445            sublist_remove,
446            sublist_insert,
447            "visits",
448            0,
449        );
450
451        assert!(m.is_doable(&director));
452
453        {
454            let mut recording = RecordingScoreDirector::new(&mut director);
455            m.do_move(&mut recording);
456
457            // After: [1, 4, 5, 2, 3, 6]
458            let visits = &recording.working_solution().vehicles[0].visits;
459            assert_eq!(visits, &[1, 4, 5, 2, 3, 6]);
460
461            recording.undo_changes();
462        }
463
464        let visits = &director.working_solution().vehicles[0].visits;
465        assert_eq!(visits, &[1, 2, 3, 4, 5, 6]);
466    }
467
468    #[test]
469    fn inter_list_move() {
470        let vehicles = vec![
471            Vehicle {
472                visits: vec![1, 2, 3, 4],
473            },
474            Vehicle {
475                visits: vec![10, 20],
476            },
477        ];
478        let mut director = create_director(vehicles);
479
480        // Move elements [1..3) (values 2, 3) from vehicle 0 to vehicle 1 at position 1
481        let m = SubListChangeMove::<RoutingSolution, i32>::new(
482            0,
483            1,
484            3,
485            1,
486            1,
487            list_len,
488            sublist_remove,
489            sublist_insert,
490            "visits",
491            0,
492        );
493
494        assert!(m.is_doable(&director));
495
496        {
497            let mut recording = RecordingScoreDirector::new(&mut director);
498            m.do_move(&mut recording);
499
500            let sol = recording.working_solution();
501            assert_eq!(sol.vehicles[0].visits, vec![1, 4]);
502            assert_eq!(sol.vehicles[1].visits, vec![10, 2, 3, 20]);
503
504            recording.undo_changes();
505        }
506
507        let sol = director.working_solution();
508        assert_eq!(sol.vehicles[0].visits, vec![1, 2, 3, 4]);
509        assert_eq!(sol.vehicles[1].visits, vec![10, 20]);
510    }
511
512    #[test]
513    fn empty_range_not_doable() {
514        let vehicles = vec![Vehicle {
515            visits: vec![1, 2, 3],
516        }];
517        let director = create_director(vehicles);
518
519        // start >= end is not doable
520        let m = SubListChangeMove::<RoutingSolution, i32>::new(
521            0,
522            2,
523            2,
524            0,
525            0,
526            list_len,
527            sublist_remove,
528            sublist_insert,
529            "visits",
530            0,
531        );
532
533        assert!(!m.is_doable(&director));
534    }
535
536    #[test]
537    fn out_of_bounds_not_doable() {
538        let vehicles = vec![Vehicle {
539            visits: vec![1, 2, 3],
540        }];
541        let director = create_director(vehicles);
542
543        let m = SubListChangeMove::<RoutingSolution, i32>::new(
544            0,
545            1,
546            10,
547            0,
548            0,
549            list_len,
550            sublist_remove,
551            sublist_insert,
552            "visits",
553            0,
554        );
555
556        assert!(!m.is_doable(&director));
557    }
558
559    #[test]
560    fn dest_within_source_not_doable() {
561        let vehicles = vec![Vehicle {
562            visits: vec![1, 2, 3, 4, 5],
563        }];
564        let director = create_director(vehicles);
565
566        // Moving [1..4) to position 2 (within the range) is a no-op
567        let m = SubListChangeMove::<RoutingSolution, i32>::new(
568            0,
569            1,
570            4,
571            0,
572            2,
573            list_len,
574            sublist_remove,
575            sublist_insert,
576            "visits",
577            0,
578        );
579
580        assert!(!m.is_doable(&director));
581    }
582}