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 concrete 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 concrete 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 concrete 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    type Undo = ();
208
209    fn is_doable<D: Director<S>>(&self, score_director: &D) -> bool {
210        let solution = score_director.working_solution();
211
212        // Check range is valid (start < end)
213        if self.source_start >= self.source_end {
214            return false;
215        }
216
217        // Check source range is within bounds
218        let source_len = (self.list_len)(solution, self.source_entity_index);
219        if self.source_end > source_len {
220            return false;
221        }
222
223        // Check destination position is valid
224        let dest_len = (self.list_len)(solution, self.dest_entity_index);
225        let sublist_len = self.sublist_len();
226
227        let max_dest = if self.is_intra_list() {
228            // After removing sublist, list is shorter
229            source_len - sublist_len
230        } else {
231            dest_len
232        };
233
234        if self.dest_position > max_dest {
235            return false;
236        }
237
238        // For intra-list, dest_position is relative to the post-removal list.
239        if self.is_intra_list() {
240            // Re-inserting at the original start position is the only no-op.
241            if self.dest_position == self.source_start {
242                return false;
243            }
244        }
245
246        true
247    }
248
249    fn do_move<D: Director<S>>(&self, score_director: &mut D) -> Self::Undo {
250        let layout = derive_segment_relocation_layout(
251            self.source_entity_index,
252            self.source_start,
253            self.source_end,
254            self.dest_entity_index,
255            self.dest_position,
256        );
257
258        // Notify before changes
259        score_director.before_variable_changed(self.descriptor_index, self.source_entity_index);
260        if !self.is_intra_list() {
261            score_director.before_variable_changed(self.descriptor_index, self.dest_entity_index);
262        }
263
264        // Remove sublist from source
265        let elements = (self.sublist_remove)(
266            score_director.working_solution_mut(),
267            self.source_entity_index,
268            self.source_start,
269            self.source_end,
270        );
271
272        // dest_position is relative to post-removal list, no adjustment needed
273        let dest_pos = layout.exact.dest_position;
274
275        // Insert at destination
276        (self.sublist_insert)(
277            score_director.working_solution_mut(),
278            self.dest_entity_index,
279            dest_pos,
280            elements,
281        );
282
283        // Notify after changes
284        score_director.after_variable_changed(self.descriptor_index, self.source_entity_index);
285        if !self.is_intra_list() {
286            score_director.after_variable_changed(self.descriptor_index, self.dest_entity_index);
287        }
288    }
289
290    fn undo_move<D: Director<S>>(&self, score_director: &mut D, (): Self::Undo) {
291        let inverse = derive_segment_relocation_layout(
292            self.source_entity_index,
293            self.source_start,
294            self.source_end,
295            self.dest_entity_index,
296            self.dest_position,
297        )
298        .inverse;
299        score_director.before_variable_changed(self.descriptor_index, inverse.source_entity_index);
300        if inverse.source_entity_index != inverse.dest_entity_index {
301            score_director
302                .before_variable_changed(self.descriptor_index, inverse.dest_entity_index);
303        }
304        let removed = (self.sublist_remove)(
305            score_director.working_solution_mut(),
306            inverse.source_entity_index,
307            inverse.source_range.start,
308            inverse.source_range.end,
309        );
310        (self.sublist_insert)(
311            score_director.working_solution_mut(),
312            inverse.dest_entity_index,
313            inverse.dest_position,
314            removed,
315        );
316        score_director.after_variable_changed(self.descriptor_index, inverse.source_entity_index);
317        if inverse.source_entity_index != inverse.dest_entity_index {
318            score_director.after_variable_changed(self.descriptor_index, inverse.dest_entity_index);
319        }
320    }
321
322    fn descriptor_index(&self) -> usize {
323        self.descriptor_index
324    }
325
326    fn entity_indices(&self) -> &[usize] {
327        if self.is_intra_list() {
328            &self.indices[0..1]
329        } else {
330            &self.indices
331        }
332    }
333
334    fn variable_name(&self) -> &str {
335        self.variable_name
336    }
337
338    fn tabu_signature<D: Director<S>>(&self, score_director: &D) -> MoveTabuSignature {
339        let layout = derive_segment_relocation_layout(
340            self.source_entity_index,
341            self.source_start,
342            self.source_end,
343            self.dest_entity_index,
344            self.dest_position,
345        );
346        let mut moved_ids: SmallVec<[u64; 2]> = SmallVec::new();
347        for pos in self.source_start..self.source_end {
348            let value = (self.list_get)(
349                score_director.working_solution(),
350                self.source_entity_index,
351                pos,
352            );
353            moved_ids.push(encode_option_debug(value.as_ref()));
354        }
355        let source_entity_id = encode_usize(self.source_entity_index);
356        let dest_entity_id = encode_usize(self.dest_entity_index);
357        let variable_id = hash_str(self.variable_name);
358        let scope = MoveTabuScope::new(self.descriptor_index, self.variable_name);
359        let mut entity_tokens: SmallVec<[ScopedEntityTabuToken; 2]> =
360            smallvec![scope.entity_token(source_entity_id)];
361        if !self.is_intra_list() {
362            entity_tokens.push(scope.entity_token(dest_entity_id));
363        }
364        let destination_value_tokens: SmallVec<[ScopedValueTabuToken; 2]> = moved_ids
365            .iter()
366            .copied()
367            .map(|value_id| scope.value_token(value_id))
368            .collect();
369        let mut move_id = smallvec![
370            encode_usize(self.descriptor_index),
371            variable_id,
372            source_entity_id,
373            encode_usize(layout.exact.source_range.start),
374            encode_usize(layout.exact.source_range.end),
375            dest_entity_id,
376            encode_usize(layout.exact.dest_position)
377        ];
378        move_id.extend(moved_ids.iter().copied());
379        let mut undo_move_id = smallvec![
380            encode_usize(self.descriptor_index),
381            variable_id,
382            encode_usize(layout.inverse.source_entity_index),
383            encode_usize(layout.inverse.source_range.start),
384            encode_usize(layout.inverse.source_range.end),
385            encode_usize(layout.inverse.dest_entity_index),
386            encode_usize(layout.inverse.dest_position)
387        ];
388        undo_move_id.extend(moved_ids.iter().copied());
389
390        MoveTabuSignature::new(scope, move_id, undo_move_id)
391            .with_entity_tokens(entity_tokens)
392            .with_destination_value_tokens(destination_value_tokens)
393    }
394}