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 solverforge_core::domain::PlanningSolution;
15use solverforge_scoring::Director;
16
17use super::Move;
18
19/// A move that relocates a contiguous sublist from one position to another.
20///
21/// Supports both intra-list moves (within same entity) and inter-list moves
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::SubListChangeMove;
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/// // Move elements [1..3) from vehicle 0 to vehicle 1 at position 0
64/// let m = SubListChangeMove::<Solution, i32>::new(
65/// 0, 1, 3, // source: entity 0, range [1, 3)
66/// 1, 0, // dest: entity 1, position 0
67/// list_len, sublist_remove, sublist_insert,
68/// "visits", 0,
69/// );
70/// ```
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 list_len: fn(&S, usize) -> usize,
83 // Remove sublist [start, end), returns removed elements
84 sublist_remove: fn(&mut S, usize, usize, usize) -> Vec<V>,
85 // Insert elements at position
86 sublist_insert: fn(&mut S, usize, usize, Vec<V>),
87 variable_name: &'static str,
88 descriptor_index: usize,
89 // Store indices for entity_indices()
90 indices: [usize; 2],
91 _phantom: PhantomData<fn() -> V>,
92}
93
94impl<S, V> Clone for SubListChangeMove<S, V> {
95 fn clone(&self) -> Self {
96 *self
97 }
98}
99
100impl<S, V> Copy for SubListChangeMove<S, V> {}
101
102impl<S, V: Debug> Debug for SubListChangeMove<S, V> {
103 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
104 f.debug_struct("SubListChangeMove")
105 .field("source_entity", &self.source_entity_index)
106 .field("source_range", &(self.source_start..self.source_end))
107 .field("dest_entity", &self.dest_entity_index)
108 .field("dest_position", &self.dest_position)
109 .field("variable_name", &self.variable_name)
110 .finish()
111 }
112}
113
114impl<S, V> SubListChangeMove<S, V> {
115 /* Creates a new sublist change move with typed function pointers.
116
117 # Arguments
118 * `source_entity_index` - Entity index to remove from
119 * `source_start` - Start of range (inclusive)
120 * `source_end` - End of range (exclusive)
121 * `dest_entity_index` - Entity index to insert into
122 * `dest_position` - Position in destination list
123 * `list_len` - Function to get list length
124 * `sublist_remove` - Function to remove range [start, end)
125 * `sublist_insert` - Function to insert elements at position
126 * `variable_name` - Name of the list variable
127 * `descriptor_index` - Entity descriptor index
128 */
129 #[allow(clippy::too_many_arguments)]
130 pub fn new(
131 source_entity_index: usize,
132 source_start: usize,
133 source_end: usize,
134 dest_entity_index: usize,
135 dest_position: usize,
136 list_len: fn(&S, usize) -> usize,
137 sublist_remove: fn(&mut S, usize, usize, usize) -> Vec<V>,
138 sublist_insert: fn(&mut S, usize, usize, Vec<V>),
139 variable_name: &'static str,
140 descriptor_index: usize,
141 ) -> Self {
142 Self {
143 source_entity_index,
144 source_start,
145 source_end,
146 dest_entity_index,
147 dest_position,
148 list_len,
149 sublist_remove,
150 sublist_insert,
151 variable_name,
152 descriptor_index,
153 indices: [source_entity_index, dest_entity_index],
154 _phantom: PhantomData,
155 }
156 }
157
158 pub fn source_entity_index(&self) -> usize {
159 self.source_entity_index
160 }
161
162 pub fn source_start(&self) -> usize {
163 self.source_start
164 }
165
166 pub fn source_end(&self) -> usize {
167 self.source_end
168 }
169
170 pub fn sublist_len(&self) -> usize {
171 self.source_end.saturating_sub(self.source_start)
172 }
173
174 pub fn dest_entity_index(&self) -> usize {
175 self.dest_entity_index
176 }
177
178 pub fn dest_position(&self) -> usize {
179 self.dest_position
180 }
181
182 pub fn is_intra_list(&self) -> bool {
183 self.source_entity_index == self.dest_entity_index
184 }
185}
186
187impl<S, V> Move<S> for SubListChangeMove<S, V>
188where
189 S: PlanningSolution,
190 V: Clone + Send + Sync + Debug + 'static,
191{
192 fn is_doable<D: Director<S>>(&self, score_director: &D) -> bool {
193 let solution = score_director.working_solution();
194
195 // Check range is valid (start < end)
196 if self.source_start >= self.source_end {
197 return false;
198 }
199
200 // Check source range is within bounds
201 let source_len = (self.list_len)(solution, self.source_entity_index);
202 if self.source_end > source_len {
203 return false;
204 }
205
206 // Check destination position is valid
207 let dest_len = (self.list_len)(solution, self.dest_entity_index);
208 let sublist_len = self.sublist_len();
209
210 let max_dest = if self.is_intra_list() {
211 // After removing sublist, list is shorter
212 source_len - sublist_len
213 } else {
214 dest_len
215 };
216
217 if self.dest_position > max_dest {
218 return false;
219 }
220
221 // For intra-list, dest_position is relative to the post-removal list.
222 if self.is_intra_list() {
223 // Re-inserting at the original start position is the only no-op.
224 if self.dest_position == self.source_start {
225 return false;
226 }
227 }
228
229 true
230 }
231
232 fn do_move<D: Director<S>>(&self, score_director: &mut D) {
233 // Notify before changes
234 score_director.before_variable_changed(self.descriptor_index, self.source_entity_index);
235 if !self.is_intra_list() {
236 score_director.before_variable_changed(self.descriptor_index, self.dest_entity_index);
237 }
238
239 // Remove sublist from source
240 let elements = (self.sublist_remove)(
241 score_director.working_solution_mut(),
242 self.source_entity_index,
243 self.source_start,
244 self.source_end,
245 );
246
247 // dest_position is relative to post-removal list, no adjustment needed
248 let dest_pos = self.dest_position;
249
250 // Insert at destination
251 (self.sublist_insert)(
252 score_director.working_solution_mut(),
253 self.dest_entity_index,
254 dest_pos,
255 elements,
256 );
257
258 // Notify after changes
259 score_director.after_variable_changed(self.descriptor_index, self.source_entity_index);
260 if !self.is_intra_list() {
261 score_director.after_variable_changed(self.descriptor_index, self.dest_entity_index);
262 }
263
264 // Register undo
265 let sublist_remove = self.sublist_remove;
266 let sublist_insert = self.sublist_insert;
267 let src_entity = self.source_entity_index;
268 let src_start = self.source_start;
269 let dest_entity = self.dest_entity_index;
270 let sublist_len = self.sublist_len();
271
272 score_director.register_undo(Box::new(move |s: &mut S| {
273 // Remove from where we inserted
274 let removed = sublist_remove(s, dest_entity, dest_pos, dest_pos + sublist_len);
275 // Insert back at original position
276 sublist_insert(s, src_entity, src_start, removed);
277 }));
278 }
279
280 fn descriptor_index(&self) -> usize {
281 self.descriptor_index
282 }
283
284 fn entity_indices(&self) -> &[usize] {
285 if self.is_intra_list() {
286 &self.indices[0..1]
287 } else {
288 &self.indices
289 }
290 }
291
292 fn variable_name(&self) -> &str {
293 self.variable_name
294 }
295}