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 typed function pointers for list operations. No `dyn Any`, no downcasting.
9*/
10
11use std::fmt::Debug;
12use std::marker::PhantomData;
13
14use solverforge_core::domain::PlanningSolution;
15use solverforge_scoring::Director;
16
17use super::Move;
18
19/// A move that swaps two contiguous sublists.
20///
21/// Supports both intra-list swaps (within same entity) and inter-list swaps
22/// (between different entities). Uses typed function pointers for zero-erasure.
23///
24/// # Type Parameters
25/// * `S` - The planning solution type
26/// * `V` - The list element value type
27///
28/// # Example
29///
30/// ```
31/// use solverforge_solver::heuristic::r#move::SubListSwapMove;
32/// use solverforge_core::domain::PlanningSolution;
33/// use solverforge_core::score::SoftScore;
34///
35/// #[derive(Clone, Debug)]
36/// struct Vehicle { id: usize, visits: Vec<i32> }
37///
38/// #[derive(Clone, Debug)]
39/// struct Solution { vehicles: Vec<Vehicle>, score: Option<SoftScore> }
40///
41/// impl PlanningSolution for Solution {
42///     type Score = SoftScore;
43///     fn score(&self) -> Option<Self::Score> { self.score }
44///     fn set_score(&mut self, score: Option<Self::Score>) { self.score = score; }
45/// }
46///
47/// fn list_len(s: &Solution, entity_idx: usize) -> usize {
48///     s.vehicles.get(entity_idx).map_or(0, |v| v.visits.len())
49/// }
50/// fn sublist_remove(s: &mut Solution, entity_idx: usize, start: usize, end: usize) -> Vec<i32> {
51///     s.vehicles.get_mut(entity_idx)
52///         .map(|v| v.visits.drain(start..end).collect())
53///         .unwrap_or_default()
54/// }
55/// fn sublist_insert(s: &mut Solution, entity_idx: usize, pos: usize, items: Vec<i32>) {
56///     if let Some(v) = s.vehicles.get_mut(entity_idx) {
57///         for (i, item) in items.into_iter().enumerate() {
58///             v.visits.insert(pos + i, item);
59///         }
60///     }
61/// }
62///
63/// // Swap [1..3) from vehicle 0 with [0..2) from vehicle 1
64/// let m = SubListSwapMove::<Solution, i32>::new(
65///     0, 1, 3,  // first: entity 0, range [1, 3)
66///     1, 0, 2,  // second: entity 1, range [0, 2)
67///     list_len, sublist_remove, sublist_insert,
68///     "visits", 0,
69/// );
70/// ```
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    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<fn() -> 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    */
132    #[allow(clippy::too_many_arguments)]
133    pub fn new(
134        first_entity_index: usize,
135        first_start: usize,
136        first_end: usize,
137        second_entity_index: usize,
138        second_start: usize,
139        second_end: usize,
140        list_len: fn(&S, usize) -> usize,
141        sublist_remove: fn(&mut S, usize, usize, usize) -> Vec<V>,
142        sublist_insert: fn(&mut S, usize, usize, Vec<V>),
143        variable_name: &'static str,
144        descriptor_index: usize,
145    ) -> Self {
146        Self {
147            first_entity_index,
148            first_start,
149            first_end,
150            second_entity_index,
151            second_start,
152            second_end,
153            list_len,
154            sublist_remove,
155            sublist_insert,
156            variable_name,
157            descriptor_index,
158            indices: [first_entity_index, second_entity_index],
159            _phantom: PhantomData,
160        }
161    }
162
163    pub fn first_entity_index(&self) -> usize {
164        self.first_entity_index
165    }
166
167    pub fn first_start(&self) -> usize {
168        self.first_start
169    }
170
171    pub fn first_end(&self) -> usize {
172        self.first_end
173    }
174
175    pub fn first_len(&self) -> usize {
176        self.first_end.saturating_sub(self.first_start)
177    }
178
179    pub fn second_entity_index(&self) -> usize {
180        self.second_entity_index
181    }
182
183    pub fn second_start(&self) -> usize {
184        self.second_start
185    }
186
187    pub fn second_end(&self) -> usize {
188        self.second_end
189    }
190
191    pub fn second_len(&self) -> usize {
192        self.second_end.saturating_sub(self.second_start)
193    }
194
195    pub fn is_intra_list(&self) -> bool {
196        self.first_entity_index == self.second_entity_index
197    }
198}
199
200impl<S, V> Move<S> for SubListSwapMove<S, V>
201where
202    S: PlanningSolution,
203    V: Clone + Send + Sync + Debug + 'static,
204{
205    fn is_doable<D: Director<S>>(&self, score_director: &D) -> bool {
206        let solution = score_director.working_solution();
207
208        // Both ranges must be valid (start < end)
209        if self.first_start >= self.first_end || self.second_start >= self.second_end {
210            return false;
211        }
212
213        // Check first range is within bounds
214        let first_list_len = (self.list_len)(solution, self.first_entity_index);
215        if self.first_end > first_list_len {
216            return false;
217        }
218
219        // Check second range is within bounds
220        let second_list_len = (self.list_len)(solution, self.second_entity_index);
221        if self.second_end > second_list_len {
222            return false;
223        }
224
225        // For intra-list swaps, ranges must not overlap
226        if self.is_intra_list() {
227            // Ranges overlap if one starts before the other ends
228            let overlaps = self.first_start < self.second_end && self.second_start < self.first_end;
229            if overlaps {
230                return false;
231            }
232        }
233
234        true
235    }
236
237    fn do_move<D: Director<S>>(&self, score_director: &mut D) {
238        // Notify before changes
239        score_director.before_variable_changed(self.descriptor_index, self.first_entity_index);
240        if !self.is_intra_list() {
241            score_director.before_variable_changed(self.descriptor_index, self.second_entity_index);
242        }
243
244        if self.is_intra_list() {
245            // Intra-list swap: need to handle carefully due to index shifts
246            // Always process the later range first to avoid index shifts affecting the earlier range
247            let (early_start, early_end, late_start, late_end) =
248                if self.first_start < self.second_start {
249                    (
250                        self.first_start,
251                        self.first_end,
252                        self.second_start,
253                        self.second_end,
254                    )
255                } else {
256                    (
257                        self.second_start,
258                        self.second_end,
259                        self.first_start,
260                        self.first_end,
261                    )
262                };
263
264            // Remove later range first
265            let late_elements = (self.sublist_remove)(
266                score_director.working_solution_mut(),
267                self.first_entity_index,
268                late_start,
269                late_end,
270            );
271
272            // Remove earlier range
273            let early_elements = (self.sublist_remove)(
274                score_director.working_solution_mut(),
275                self.first_entity_index,
276                early_start,
277                early_end,
278            );
279
280            // Insert late elements at early position
281            let late_len = late_end - late_start;
282            let early_len = early_end - early_start;
283            (self.sublist_insert)(
284                score_director.working_solution_mut(),
285                self.first_entity_index,
286                early_start,
287                late_elements,
288            );
289
290            /* Insert early elements at adjusted late position
291            After removing early range, late_start shifts by early_len
292            After inserting late elements, it shifts back by late_len
293            */
294            let new_late_pos = late_start - early_len + late_len;
295            (self.sublist_insert)(
296                score_director.working_solution_mut(),
297                self.first_entity_index,
298                new_late_pos,
299                early_elements,
300            );
301
302            // Register undo - swap back
303            let sublist_remove = self.sublist_remove;
304            let sublist_insert = self.sublist_insert;
305            let entity = self.first_entity_index;
306
307            score_director.register_undo(Box::new(move |s: &mut S| {
308                // Remove late elements (now at early position with late_len)
309                let late_at_early = sublist_remove(s, entity, early_start, early_start + late_len);
310                // Remove early elements (now at new_late_pos with early_len)
311                let early_at_late = sublist_remove(
312                    s,
313                    entity,
314                    new_late_pos - late_len,
315                    new_late_pos - late_len + early_len,
316                );
317                // Insert early back at early
318                sublist_insert(s, entity, early_start, early_at_late);
319                // Insert late back at late
320                sublist_insert(s, entity, late_start, late_at_early);
321            }));
322        } else {
323            // Inter-list swap: simpler, no index interaction between lists
324            let first_elements = (self.sublist_remove)(
325                score_director.working_solution_mut(),
326                self.first_entity_index,
327                self.first_start,
328                self.first_end,
329            );
330
331            let second_elements = (self.sublist_remove)(
332                score_director.working_solution_mut(),
333                self.second_entity_index,
334                self.second_start,
335                self.second_end,
336            );
337
338            // Insert swapped
339            (self.sublist_insert)(
340                score_director.working_solution_mut(),
341                self.first_entity_index,
342                self.first_start,
343                second_elements,
344            );
345
346            (self.sublist_insert)(
347                score_director.working_solution_mut(),
348                self.second_entity_index,
349                self.second_start,
350                first_elements,
351            );
352
353            // Register undo
354            let sublist_remove = self.sublist_remove;
355            let sublist_insert = self.sublist_insert;
356            let first_entity = self.first_entity_index;
357            let first_start = self.first_start;
358            let second_entity = self.second_entity_index;
359            let second_start = self.second_start;
360            let first_len = self.first_len();
361            let second_len = self.second_len();
362
363            score_director.register_undo(Box::new(move |s: &mut S| {
364                // Remove second elements from first list
365                let second_at_first =
366                    sublist_remove(s, first_entity, first_start, first_start + second_len);
367                // Remove first elements from second list
368                let first_at_second =
369                    sublist_remove(s, second_entity, second_start, second_start + first_len);
370                // Restore originals
371                sublist_insert(s, first_entity, first_start, first_at_second);
372                sublist_insert(s, second_entity, second_start, second_at_first);
373            }));
374        }
375
376        // Notify after changes
377        score_director.after_variable_changed(self.descriptor_index, self.first_entity_index);
378        if !self.is_intra_list() {
379            score_director.after_variable_changed(self.descriptor_index, self.second_entity_index);
380        }
381    }
382
383    fn descriptor_index(&self) -> usize {
384        self.descriptor_index
385    }
386
387    fn entity_indices(&self) -> &[usize] {
388        if self.is_intra_list() {
389            &self.indices[0..1]
390        } else {
391            &self.indices
392        }
393    }
394
395    fn variable_name(&self) -> &str {
396        self.variable_name
397    }
398}
399
400#[cfg(test)]
401#[path = "sublist_swap_tests.rs"]
402mod tests;