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::borrow::Cow;
12use std::fmt::Debug;
13use std::marker::PhantomData;
14
15use solverforge_core::domain::PlanningSolution;
16use solverforge_scoring::Director;
17
18use super::list_kernel::{
19    sublist_change_candidate_trace_identity, sublist_change_do_move, sublist_change_is_doable,
20    sublist_change_tabu_signature, sublist_change_undo_move, StaticListWindowAccess,
21};
22use super::segment_layout::SegmentRelocationCoords;
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    fn access(&self) -> StaticListWindowAccess<S, V> {
202        StaticListWindowAccess {
203            list_len: self.list_len,
204            list_get: self.list_get,
205            sublist_remove: self.sublist_remove,
206            sublist_insert: self.sublist_insert,
207            variable_name: self.variable_name,
208            descriptor_index: self.descriptor_index,
209        }
210    }
211
212    fn coordinates(&self) -> SegmentRelocationCoords {
213        SegmentRelocationCoords::new(
214            self.source_entity_index,
215            self.source_start,
216            self.source_end,
217            self.dest_entity_index,
218            self.dest_position,
219        )
220    }
221}
222
223impl<S, V> Move<S> for SublistChangeMove<S, V>
224where
225    S: PlanningSolution,
226    V: Clone + Send + Sync + Debug + 'static,
227{
228    type Undo = ();
229
230    fn is_doable<D: Director<S>>(&self, score_director: &D) -> bool {
231        sublist_change_is_doable(&self.access(), self.coordinates(), score_director)
232    }
233
234    fn do_move<D: Director<S>>(&self, score_director: &mut D) -> Self::Undo {
235        sublist_change_do_move(&self.access(), self.coordinates(), score_director);
236    }
237
238    fn undo_move<D: Director<S>>(&self, score_director: &mut D, (): Self::Undo) {
239        sublist_change_undo_move(&self.access(), self.coordinates(), score_director);
240    }
241
242    fn descriptor_index(&self) -> usize {
243        self.descriptor_index
244    }
245
246    fn entity_indices(&self) -> &[usize] {
247        if self.is_intra_list() {
248            &self.indices[0..1]
249        } else {
250            &self.indices
251        }
252    }
253
254    fn variable_name(&self) -> &str {
255        self.variable_name
256    }
257
258    fn telemetry_label(&self) -> &'static str {
259        "sublist_change"
260    }
261
262    fn tabu_signature<D: Director<S>>(&self, score_director: &D) -> Cow<'_, MoveTabuSignature> {
263        Cow::Owned(sublist_change_tabu_signature(
264            &self.access(),
265            self.coordinates(),
266            score_director,
267        ))
268    }
269
270    fn candidate_trace_identity(&self) -> Option<crate::stats::CandidateTraceIdentity> {
271        Some(sublist_change_candidate_trace_identity(
272            &self.access(),
273            self.coordinates(),
274        ))
275    }
276}