Skip to main content

solverforge_solver/heuristic/move/
sublist_change.rs

1/* SublistChangeMove - relocates a contiguous sublist within or between list variables.
2
3This move removes a range of elements from one position and inserts them at another.
4Essential for vehicle routing where multiple consecutive stops need relocation.
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 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_relocation_layout;
23use super::{Move, MoveTabuSignature};
24
25/// A move that relocates a contiguous sublist from one position to another.
26///
27/// Supports both intra-list moves (within same entity) and inter-list moves
28/// (between different entities). Uses typed 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::SublistChangeMove;
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/// // Move elements [1..3) from vehicle 0 to vehicle 1 at position 0
76/// let m = SublistChangeMove::<Solution, i32>::new(
77///     0, 1, 3,  // source: entity 0, range [1, 3)
78///     1, 0,     // dest: entity 1, position 0
79///     list_len, list_get, sublist_remove, sublist_insert,
80///     "visits", 0,
81/// );
82/// ```
83pub struct SublistChangeMove<S, V> {
84    // Source entity index
85    source_entity_index: usize,
86    // Start of range in source list (inclusive)
87    source_start: usize,
88    // End of range in source list (exclusive)
89    source_end: usize,
90    // Destination entity index
91    dest_entity_index: usize,
92    // Position in destination list to insert at
93    dest_position: usize,
94    list_len: fn(&S, usize) -> usize,
95    list_get: fn(&S, usize, usize) -> Option<V>,
96    // Remove sublist [start, end), returns removed elements
97    sublist_remove: fn(&mut S, usize, usize, usize) -> Vec<V>,
98    // Insert elements at position
99    sublist_insert: fn(&mut S, usize, usize, Vec<V>),
100    variable_name: &'static str,
101    descriptor_index: usize,
102    // Store indices for entity_indices()
103    indices: [usize; 2],
104    _phantom: PhantomData<fn() -> V>,
105}
106
107impl<S, V> Clone for SublistChangeMove<S, V> {
108    fn clone(&self) -> Self {
109        *self
110    }
111}
112
113impl<S, V> Copy for SublistChangeMove<S, V> {}
114
115impl<S, V: Debug> Debug for SublistChangeMove<S, V> {
116    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
117        f.debug_struct("SublistChangeMove")
118            .field("source_entity", &self.source_entity_index)
119            .field("source_range", &(self.source_start..self.source_end))
120            .field("dest_entity", &self.dest_entity_index)
121            .field("dest_position", &self.dest_position)
122            .field("variable_name", &self.variable_name)
123            .finish()
124    }
125}
126
127impl<S, V> SublistChangeMove<S, V> {
128    /* Creates a new sublist change move with typed function pointers.
129
130    # Arguments
131    * `source_entity_index` - Entity index to remove from
132    * `source_start` - Start of range (inclusive)
133    * `source_end` - End of range (exclusive)
134    * `dest_entity_index` - Entity index to insert into
135    * `dest_position` - Position in destination list
136    * `list_len` - Function to get list length
137    * `sublist_remove` - Function to remove range [start, end)
138    * `sublist_insert` - Function to insert elements at position
139    * `variable_name` - Name of the list variable
140    * `descriptor_index` - Entity descriptor index
141    */
142    #[allow(clippy::too_many_arguments)]
143    pub fn new(
144        source_entity_index: usize,
145        source_start: usize,
146        source_end: usize,
147        dest_entity_index: usize,
148        dest_position: usize,
149        list_len: fn(&S, usize) -> usize,
150        list_get: fn(&S, usize, usize) -> Option<V>,
151        sublist_remove: fn(&mut S, usize, usize, usize) -> Vec<V>,
152        sublist_insert: fn(&mut S, usize, usize, Vec<V>),
153        variable_name: &'static str,
154        descriptor_index: usize,
155    ) -> Self {
156        Self {
157            source_entity_index,
158            source_start,
159            source_end,
160            dest_entity_index,
161            dest_position,
162            list_len,
163            list_get,
164            sublist_remove,
165            sublist_insert,
166            variable_name,
167            descriptor_index,
168            indices: [source_entity_index, dest_entity_index],
169            _phantom: PhantomData,
170        }
171    }
172
173    pub fn source_entity_index(&self) -> usize {
174        self.source_entity_index
175    }
176
177    pub fn source_start(&self) -> usize {
178        self.source_start
179    }
180
181    pub fn source_end(&self) -> usize {
182        self.source_end
183    }
184
185    pub fn sublist_len(&self) -> usize {
186        self.source_end.saturating_sub(self.source_start)
187    }
188
189    pub fn dest_entity_index(&self) -> usize {
190        self.dest_entity_index
191    }
192
193    pub fn dest_position(&self) -> usize {
194        self.dest_position
195    }
196
197    pub fn is_intra_list(&self) -> bool {
198        self.source_entity_index == self.dest_entity_index
199    }
200}
201
202impl<S, V> Move<S> for SublistChangeMove<S, V>
203where
204    S: PlanningSolution,
205    V: Clone + Send + Sync + Debug + 'static,
206{
207    fn is_doable<D: Director<S>>(&self, score_director: &D) -> bool {
208        let solution = score_director.working_solution();
209
210        // Check range is valid (start < end)
211        if self.source_start >= self.source_end {
212            return false;
213        }
214
215        // Check source range is within bounds
216        let source_len = (self.list_len)(solution, self.source_entity_index);
217        if self.source_end > source_len {
218            return false;
219        }
220
221        // Check destination position is valid
222        let dest_len = (self.list_len)(solution, self.dest_entity_index);
223        let sublist_len = self.sublist_len();
224
225        let max_dest = if self.is_intra_list() {
226            // After removing sublist, list is shorter
227            source_len - sublist_len
228        } else {
229            dest_len
230        };
231
232        if self.dest_position > max_dest {
233            return false;
234        }
235
236        // For intra-list, dest_position is relative to the post-removal list.
237        if self.is_intra_list() {
238            // Re-inserting at the original start position is the only no-op.
239            if self.dest_position == self.source_start {
240                return false;
241            }
242        }
243
244        true
245    }
246
247    fn do_move<D: Director<S>>(&self, score_director: &mut D) {
248        let layout = derive_segment_relocation_layout(
249            self.source_entity_index,
250            self.source_start,
251            self.source_end,
252            self.dest_entity_index,
253            self.dest_position,
254        );
255
256        // Notify before changes
257        score_director.before_variable_changed(self.descriptor_index, self.source_entity_index);
258        if !self.is_intra_list() {
259            score_director.before_variable_changed(self.descriptor_index, self.dest_entity_index);
260        }
261
262        // Remove sublist from source
263        let elements = (self.sublist_remove)(
264            score_director.working_solution_mut(),
265            self.source_entity_index,
266            self.source_start,
267            self.source_end,
268        );
269
270        // dest_position is relative to post-removal list, no adjustment needed
271        let dest_pos = layout.exact.dest_position;
272
273        // Insert at destination
274        (self.sublist_insert)(
275            score_director.working_solution_mut(),
276            self.dest_entity_index,
277            dest_pos,
278            elements,
279        );
280
281        // Notify after changes
282        score_director.after_variable_changed(self.descriptor_index, self.source_entity_index);
283        if !self.is_intra_list() {
284            score_director.after_variable_changed(self.descriptor_index, self.dest_entity_index);
285        }
286
287        // Register undo
288        let sublist_remove = self.sublist_remove;
289        let sublist_insert = self.sublist_insert;
290        let inverse = layout.inverse;
291
292        score_director.register_undo(Box::new(move |s: &mut S| {
293            let removed = sublist_remove(
294                s,
295                inverse.source_entity_index,
296                inverse.source_range.start,
297                inverse.source_range.end,
298            );
299            sublist_insert(s, inverse.dest_entity_index, inverse.dest_position, removed);
300        }));
301    }
302
303    fn descriptor_index(&self) -> usize {
304        self.descriptor_index
305    }
306
307    fn entity_indices(&self) -> &[usize] {
308        if self.is_intra_list() {
309            &self.indices[0..1]
310        } else {
311            &self.indices
312        }
313    }
314
315    fn variable_name(&self) -> &str {
316        self.variable_name
317    }
318
319    fn tabu_signature<D: Director<S>>(&self, score_director: &D) -> MoveTabuSignature {
320        let layout = derive_segment_relocation_layout(
321            self.source_entity_index,
322            self.source_start,
323            self.source_end,
324            self.dest_entity_index,
325            self.dest_position,
326        );
327        let mut moved_ids: SmallVec<[u64; 2]> = SmallVec::new();
328        for pos in self.source_start..self.source_end {
329            let value = (self.list_get)(
330                score_director.working_solution(),
331                self.source_entity_index,
332                pos,
333            );
334            moved_ids.push(encode_option_debug(value.as_ref()));
335        }
336        let source_entity_id = encode_usize(self.source_entity_index);
337        let dest_entity_id = encode_usize(self.dest_entity_index);
338        let variable_id = hash_str(self.variable_name);
339        let scope = MoveTabuScope::new(self.descriptor_index, self.variable_name);
340        let mut entity_tokens: SmallVec<[ScopedEntityTabuToken; 2]> =
341            smallvec![scope.entity_token(source_entity_id)];
342        if !self.is_intra_list() {
343            entity_tokens.push(scope.entity_token(dest_entity_id));
344        }
345        let destination_value_tokens: SmallVec<[ScopedValueTabuToken; 2]> = moved_ids
346            .iter()
347            .copied()
348            .map(|value_id| scope.value_token(value_id))
349            .collect();
350        let mut move_id = smallvec![
351            encode_usize(self.descriptor_index),
352            variable_id,
353            source_entity_id,
354            encode_usize(layout.exact.source_range.start),
355            encode_usize(layout.exact.source_range.end),
356            dest_entity_id,
357            encode_usize(layout.exact.dest_position)
358        ];
359        move_id.extend(moved_ids.iter().copied());
360        let mut undo_move_id = smallvec![
361            encode_usize(self.descriptor_index),
362            variable_id,
363            encode_usize(layout.inverse.source_entity_index),
364            encode_usize(layout.inverse.source_range.start),
365            encode_usize(layout.inverse.source_range.end),
366            encode_usize(layout.inverse.dest_entity_index),
367            encode_usize(layout.inverse.dest_position)
368        ];
369        undo_move_id.extend(moved_ids.iter().copied());
370
371        MoveTabuSignature::new(scope, move_id, undo_move_id)
372            .with_entity_tokens(entity_tokens)
373            .with_destination_value_tokens(destination_value_tokens)
374    }
375}