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