Skip to main content

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}