solverforge_solver/heuristic/move/
list_swap.rs

1//! ListSwapMove - swaps two elements within or between list variables.
2//!
3//! This move exchanges two elements at different positions.
4//! Useful for TSP-style improvements and route optimization.
5//!
6//! # Zero-Erasure Design
7//!
8//! Uses typed function pointers for list operations. No `dyn Any`, no downcasting.
9
10use std::fmt::Debug;
11
12use solverforge_core::domain::PlanningSolution;
13use solverforge_scoring::ScoreDirector;
14
15use super::Move;
16
17/// A move that swaps two elements in list variables.
18///
19/// Supports both intra-list swaps (within same entity) and inter-list swaps
20/// (between different entities). Uses typed function pointers for zero-erasure.
21///
22/// # Type Parameters
23/// * `S` - The planning solution type
24/// * `V` - The list element value type
25///
26/// # Example
27///
28/// ```
29/// use solverforge_solver::heuristic::r#move::ListSwapMove;
30/// use solverforge_core::domain::PlanningSolution;
31/// use solverforge_core::score::SimpleScore;
32///
33/// #[derive(Clone, Debug)]
34/// struct Vehicle { id: usize, visits: Vec<i32> }
35///
36/// #[derive(Clone, Debug)]
37/// struct Solution { vehicles: Vec<Vehicle>, score: Option<SimpleScore> }
38///
39/// impl PlanningSolution for Solution {
40///     type Score = SimpleScore;
41///     fn score(&self) -> Option<Self::Score> { self.score }
42///     fn set_score(&mut self, score: Option<Self::Score>) { self.score = score; }
43/// }
44///
45/// fn list_len(s: &Solution, entity_idx: usize) -> usize {
46///     s.vehicles.get(entity_idx).map_or(0, |v| v.visits.len())
47/// }
48/// fn list_get(s: &Solution, entity_idx: usize, pos: usize) -> Option<i32> {
49///     s.vehicles.get(entity_idx).and_then(|v| v.visits.get(pos).copied())
50/// }
51/// fn list_set(s: &mut Solution, entity_idx: usize, pos: usize, val: i32) {
52///     if let Some(v) = s.vehicles.get_mut(entity_idx) {
53///         if let Some(elem) = v.visits.get_mut(pos) { *elem = val; }
54///     }
55/// }
56///
57/// // Swap elements at positions 1 and 3 in vehicle 0
58/// let m = ListSwapMove::<Solution, i32>::new(
59///     0, 1, 0, 3,
60///     list_len, list_get, list_set,
61///     "visits", 0,
62/// );
63/// ```
64pub struct ListSwapMove<S, V> {
65    /// First entity index
66    first_entity_index: usize,
67    /// Position in first entity's list
68    first_position: usize,
69    /// Second entity index
70    second_entity_index: usize,
71    /// Position in second entity's list
72    second_position: usize,
73    /// Get list length for an entity
74    list_len: fn(&S, usize) -> usize,
75    /// Get element at position
76    list_get: fn(&S, usize, usize) -> Option<V>,
77    /// Set element at position
78    list_set: fn(&mut S, usize, usize, V),
79    variable_name: &'static str,
80    descriptor_index: usize,
81    /// Store indices for entity_indices()
82    indices: [usize; 2],
83}
84
85impl<S, V> Clone for ListSwapMove<S, V> {
86    fn clone(&self) -> Self {
87        *self
88    }
89}
90
91impl<S, V> Copy for ListSwapMove<S, V> {}
92
93impl<S, V: Debug> Debug for ListSwapMove<S, V> {
94    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
95        f.debug_struct("ListSwapMove")
96            .field("first_entity", &self.first_entity_index)
97            .field("first_position", &self.first_position)
98            .field("second_entity", &self.second_entity_index)
99            .field("second_position", &self.second_position)
100            .field("variable_name", &self.variable_name)
101            .finish()
102    }
103}
104
105impl<S, V> ListSwapMove<S, V> {
106    /// Creates a new list swap move with typed function pointers.
107    ///
108    /// # Arguments
109    /// * `first_entity_index` - First entity index
110    /// * `first_position` - Position in first entity's list
111    /// * `second_entity_index` - Second entity index
112    /// * `second_position` - Position in second entity's list
113    /// * `list_len` - Function to get list length
114    /// * `list_get` - Function to get element at position
115    /// * `list_set` - Function to set element at position
116    /// * `variable_name` - Name of the list variable
117    /// * `descriptor_index` - Entity descriptor index
118    #[allow(clippy::too_many_arguments)]
119    pub fn new(
120        first_entity_index: usize,
121        first_position: usize,
122        second_entity_index: usize,
123        second_position: usize,
124        list_len: fn(&S, usize) -> usize,
125        list_get: fn(&S, usize, usize) -> Option<V>,
126        list_set: fn(&mut S, usize, usize, V),
127        variable_name: &'static str,
128        descriptor_index: usize,
129    ) -> Self {
130        Self {
131            first_entity_index,
132            first_position,
133            second_entity_index,
134            second_position,
135            list_len,
136            list_get,
137            list_set,
138            variable_name,
139            descriptor_index,
140            indices: [first_entity_index, second_entity_index],
141        }
142    }
143
144    /// Returns the first entity index.
145    pub fn first_entity_index(&self) -> usize {
146        self.first_entity_index
147    }
148
149    /// Returns the first position.
150    pub fn first_position(&self) -> usize {
151        self.first_position
152    }
153
154    /// Returns the second entity index.
155    pub fn second_entity_index(&self) -> usize {
156        self.second_entity_index
157    }
158
159    /// Returns the second position.
160    pub fn second_position(&self) -> usize {
161        self.second_position
162    }
163
164    /// Returns true if this is an intra-list swap (same entity).
165    pub fn is_intra_list(&self) -> bool {
166        self.first_entity_index == self.second_entity_index
167    }
168}
169
170impl<S, V> Move<S> for ListSwapMove<S, V>
171where
172    S: PlanningSolution,
173    V: Clone + PartialEq + Send + Sync + Debug + 'static,
174{
175    fn is_doable<D: ScoreDirector<S>>(&self, score_director: &D) -> bool {
176        let solution = score_director.working_solution();
177
178        // Check first position is valid
179        let first_len = (self.list_len)(solution, self.first_entity_index);
180        if self.first_position >= first_len {
181            return false;
182        }
183
184        // Check second position is valid
185        let second_len = (self.list_len)(solution, self.second_entity_index);
186        if self.second_position >= second_len {
187            return false;
188        }
189
190        // For intra-list, can't swap with self
191        if self.is_intra_list() && self.first_position == self.second_position {
192            return false;
193        }
194
195        // Get values and check they're different
196        let first_val = (self.list_get)(solution, self.first_entity_index, self.first_position);
197        let second_val = (self.list_get)(solution, self.second_entity_index, self.second_position);
198
199        first_val != second_val
200    }
201
202    fn do_move<D: ScoreDirector<S>>(&self, score_director: &mut D) {
203        // Get both values
204        let first_val = (self.list_get)(
205            score_director.working_solution(),
206            self.first_entity_index,
207            self.first_position,
208        )
209        .expect("first position should be valid");
210
211        let second_val = (self.list_get)(
212            score_director.working_solution(),
213            self.second_entity_index,
214            self.second_position,
215        )
216        .expect("second position should be valid");
217
218        // Notify before changes
219        score_director.before_variable_changed(
220            self.descriptor_index,
221            self.first_entity_index,
222            self.variable_name,
223        );
224        if !self.is_intra_list() {
225            score_director.before_variable_changed(
226                self.descriptor_index,
227                self.second_entity_index,
228                self.variable_name,
229            );
230        }
231
232        // Swap: first gets second's value, second gets first's value
233        (self.list_set)(
234            score_director.working_solution_mut(),
235            self.first_entity_index,
236            self.first_position,
237            second_val.clone(),
238        );
239        (self.list_set)(
240            score_director.working_solution_mut(),
241            self.second_entity_index,
242            self.second_position,
243            first_val.clone(),
244        );
245
246        // Notify after changes
247        score_director.after_variable_changed(
248            self.descriptor_index,
249            self.first_entity_index,
250            self.variable_name,
251        );
252        if !self.is_intra_list() {
253            score_director.after_variable_changed(
254                self.descriptor_index,
255                self.second_entity_index,
256                self.variable_name,
257            );
258        }
259
260        // Register undo - swap back
261        let list_set = self.list_set;
262        let first_entity = self.first_entity_index;
263        let first_pos = self.first_position;
264        let second_entity = self.second_entity_index;
265        let second_pos = self.second_position;
266
267        score_director.register_undo(Box::new(move |s: &mut S| {
268            // Restore original values
269            list_set(s, first_entity, first_pos, first_val);
270            list_set(s, second_entity, second_pos, second_val);
271        }));
272    }
273
274    fn descriptor_index(&self) -> usize {
275        self.descriptor_index
276    }
277
278    fn entity_indices(&self) -> &[usize] {
279        if self.is_intra_list() {
280            &self.indices[0..1]
281        } else {
282            &self.indices
283        }
284    }
285
286    fn variable_name(&self) -> &str {
287        self.variable_name
288    }
289}
290
291#[cfg(test)]
292mod tests {
293    use super::*;
294    use solverforge_core::domain::{EntityDescriptor, SolutionDescriptor, TypedEntityExtractor};
295    use solverforge_core::score::SimpleScore;
296    use solverforge_scoring::{RecordingScoreDirector, SimpleScoreDirector};
297    use std::any::TypeId;
298
299    #[derive(Clone, Debug)]
300    struct Vehicle {
301        visits: Vec<i32>,
302    }
303
304    #[derive(Clone, Debug)]
305    struct RoutingSolution {
306        vehicles: Vec<Vehicle>,
307        score: Option<SimpleScore>,
308    }
309
310    impl PlanningSolution for RoutingSolution {
311        type Score = SimpleScore;
312        fn score(&self) -> Option<Self::Score> {
313            self.score
314        }
315        fn set_score(&mut self, score: Option<Self::Score>) {
316            self.score = score;
317        }
318    }
319
320    fn get_vehicles(s: &RoutingSolution) -> &Vec<Vehicle> {
321        &s.vehicles
322    }
323    fn get_vehicles_mut(s: &mut RoutingSolution) -> &mut Vec<Vehicle> {
324        &mut s.vehicles
325    }
326
327    fn list_len(s: &RoutingSolution, entity_idx: usize) -> usize {
328        s.vehicles.get(entity_idx).map_or(0, |v| v.visits.len())
329    }
330    fn list_get(s: &RoutingSolution, entity_idx: usize, pos: usize) -> Option<i32> {
331        s.vehicles
332            .get(entity_idx)
333            .and_then(|v| v.visits.get(pos).copied())
334    }
335    fn list_set(s: &mut RoutingSolution, entity_idx: usize, pos: usize, val: i32) {
336        if let Some(v) = s.vehicles.get_mut(entity_idx) {
337            if let Some(elem) = v.visits.get_mut(pos) {
338                *elem = val;
339            }
340        }
341    }
342
343    fn create_director(
344        vehicles: Vec<Vehicle>,
345    ) -> SimpleScoreDirector<RoutingSolution, impl Fn(&RoutingSolution) -> SimpleScore> {
346        let solution = RoutingSolution {
347            vehicles,
348            score: None,
349        };
350        let extractor = Box::new(TypedEntityExtractor::new(
351            "Vehicle",
352            "vehicles",
353            get_vehicles,
354            get_vehicles_mut,
355        ));
356        let entity_desc = EntityDescriptor::new("Vehicle", TypeId::of::<Vehicle>(), "vehicles")
357            .with_extractor(extractor);
358        let descriptor =
359            SolutionDescriptor::new("RoutingSolution", TypeId::of::<RoutingSolution>())
360                .with_entity(entity_desc);
361        SimpleScoreDirector::with_calculator(solution, descriptor, |_| SimpleScore::of(0))
362    }
363
364    #[test]
365    fn intra_list_swap() {
366        let vehicles = vec![Vehicle {
367            visits: vec![1, 2, 3, 4, 5],
368        }];
369        let mut director = create_director(vehicles);
370
371        // Swap positions 1 and 3 (values 2 and 4)
372        let m = ListSwapMove::<RoutingSolution, i32>::new(
373            0, 1, 0, 3, list_len, list_get, list_set, "visits", 0,
374        );
375
376        assert!(m.is_doable(&director));
377
378        {
379            let mut recording = RecordingScoreDirector::new(&mut director);
380            m.do_move(&mut recording);
381
382            let visits = &recording.working_solution().vehicles[0].visits;
383            assert_eq!(visits, &[1, 4, 3, 2, 5]);
384
385            recording.undo_changes();
386        }
387
388        let visits = &director.working_solution().vehicles[0].visits;
389        assert_eq!(visits, &[1, 2, 3, 4, 5]);
390    }
391
392    #[test]
393    fn inter_list_swap() {
394        let vehicles = vec![
395            Vehicle {
396                visits: vec![1, 2, 3],
397            },
398            Vehicle {
399                visits: vec![10, 20, 30],
400            },
401        ];
402        let mut director = create_director(vehicles);
403
404        // Swap vehicle 0 position 1 (value=2) with vehicle 1 position 2 (value=30)
405        let m = ListSwapMove::<RoutingSolution, i32>::new(
406            0, 1, 1, 2, list_len, list_get, list_set, "visits", 0,
407        );
408
409        assert!(m.is_doable(&director));
410
411        {
412            let mut recording = RecordingScoreDirector::new(&mut director);
413            m.do_move(&mut recording);
414
415            let sol = recording.working_solution();
416            assert_eq!(sol.vehicles[0].visits, vec![1, 30, 3]);
417            assert_eq!(sol.vehicles[1].visits, vec![10, 20, 2]);
418
419            recording.undo_changes();
420        }
421
422        let sol = director.working_solution();
423        assert_eq!(sol.vehicles[0].visits, vec![1, 2, 3]);
424        assert_eq!(sol.vehicles[1].visits, vec![10, 20, 30]);
425    }
426
427    #[test]
428    fn same_position_not_doable() {
429        let vehicles = vec![Vehicle {
430            visits: vec![1, 2, 3],
431        }];
432        let director = create_director(vehicles);
433
434        let m = ListSwapMove::<RoutingSolution, i32>::new(
435            0, 1, 0, 1, list_len, list_get, list_set, "visits", 0,
436        );
437
438        assert!(!m.is_doable(&director));
439    }
440
441    #[test]
442    fn same_values_not_doable() {
443        let vehicles = vec![Vehicle {
444            visits: vec![5, 5, 5],
445        }];
446        let director = create_director(vehicles);
447
448        let m = ListSwapMove::<RoutingSolution, i32>::new(
449            0, 0, 0, 2, list_len, list_get, list_set, "visits", 0,
450        );
451
452        assert!(!m.is_doable(&director));
453    }
454
455    #[test]
456    fn invalid_position_not_doable() {
457        let vehicles = vec![Vehicle {
458            visits: vec![1, 2, 3],
459        }];
460        let director = create_director(vehicles);
461
462        let m = ListSwapMove::<RoutingSolution, i32>::new(
463            0, 1, 0, 10, list_len, list_get, list_set, "visits", 0,
464        );
465
466        assert!(!m.is_doable(&director));
467    }
468}