solverforge_solver/heuristic/move/
swap.rs

1//! SwapMove - exchanges values between two entities.
2//!
3//! This move swaps the values of a planning variable between two entities.
4//! Useful for permutation-based problems.
5//!
6//! # Zero-Erasure Design
7//!
8//! SwapMove uses typed function pointers instead of `dyn Any` for complete
9//! compile-time type safety. No runtime type checks or downcasting.
10
11use std::fmt::Debug;
12use std::marker::PhantomData;
13
14use solverforge_core::domain::PlanningSolution;
15use solverforge_scoring::ScoreDirector;
16
17use super::Move;
18
19/// A move that swaps values between two entities.
20///
21/// Stores entity indices and typed function pointers for zero-erasure access.
22/// Undo is handled by `RecordingScoreDirector`, not by this move.
23///
24/// # Type Parameters
25/// * `S` - The planning solution type
26/// * `V` - The variable value type
27///
28/// # Example
29/// ```
30/// use solverforge_solver::heuristic::r#move::SwapMove;
31/// use solverforge_core::domain::PlanningSolution;
32/// use solverforge_core::score::SimpleScore;
33///
34/// #[derive(Clone)]
35/// struct Sol { values: Vec<Option<i32>>, score: Option<SimpleScore> }
36///
37/// impl PlanningSolution for Sol {
38///     type Score = SimpleScore;
39///     fn score(&self) -> Option<Self::Score> { self.score }
40///     fn set_score(&mut self, score: Option<Self::Score>) { self.score = score; }
41/// }
42///
43/// // Typed getter/setter with zero erasure
44/// fn get_v(s: &Sol, idx: usize) -> Option<i32> { s.values.get(idx).copied().flatten() }
45/// fn set_v(s: &mut Sol, idx: usize, v: Option<i32>) { if let Some(x) = s.values.get_mut(idx) { *x = v; } }
46///
47/// // Swap values between entities 0 and 1
48/// let swap = SwapMove::<Sol, i32>::new(0, 1, get_v, set_v, "value", 0);
49/// ```
50#[derive(Clone, Copy)]
51pub struct SwapMove<S, V> {
52    left_entity_index: usize,
53    right_entity_index: usize,
54    /// Typed getter function pointer - zero erasure.
55    getter: fn(&S, usize) -> Option<V>,
56    /// Typed setter function pointer - zero erasure.
57    setter: fn(&mut S, usize, Option<V>),
58    variable_name: &'static str,
59    descriptor_index: usize,
60    /// Store indices inline for entity_indices() to return a slice.
61    indices: [usize; 2],
62    _phantom: PhantomData<V>,
63}
64
65impl<S, V: Debug> Debug for SwapMove<S, V> {
66    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
67        f.debug_struct("SwapMove")
68            .field("left_entity_index", &self.left_entity_index)
69            .field("right_entity_index", &self.right_entity_index)
70            .field("descriptor_index", &self.descriptor_index)
71            .field("variable_name", &self.variable_name)
72            .finish()
73    }
74}
75
76impl<S, V> SwapMove<S, V> {
77    /// Creates a new swap move with typed function pointers.
78    ///
79    /// # Arguments
80    /// * `left_entity_index` - Index of the first entity
81    /// * `right_entity_index` - Index of the second entity
82    /// * `getter` - Typed getter function pointer
83    /// * `setter` - Typed setter function pointer
84    /// * `variable_name` - Name of the variable being swapped
85    /// * `descriptor_index` - Index in the entity descriptor
86    pub fn new(
87        left_entity_index: usize,
88        right_entity_index: usize,
89        getter: fn(&S, usize) -> Option<V>,
90        setter: fn(&mut S, usize, Option<V>),
91        variable_name: &'static str,
92        descriptor_index: usize,
93    ) -> Self {
94        Self {
95            left_entity_index,
96            right_entity_index,
97            getter,
98            setter,
99            variable_name,
100            descriptor_index,
101            indices: [left_entity_index, right_entity_index],
102            _phantom: PhantomData,
103        }
104    }
105
106    /// Returns the left entity index.
107    pub fn left_entity_index(&self) -> usize {
108        self.left_entity_index
109    }
110
111    /// Returns the right entity index.
112    pub fn right_entity_index(&self) -> usize {
113        self.right_entity_index
114    }
115}
116
117impl<S, V> Move<S> for SwapMove<S, V>
118where
119    S: PlanningSolution,
120    V: Clone + PartialEq + Send + Sync + Debug + 'static,
121{
122    fn is_doable(&self, score_director: &dyn ScoreDirector<S>) -> bool {
123        // Can't swap with self
124        if self.left_entity_index == self.right_entity_index {
125            return false;
126        }
127
128        // Get current values using typed getter - zero erasure
129        let left_val = (self.getter)(score_director.working_solution(), self.left_entity_index);
130        let right_val = (self.getter)(score_director.working_solution(), self.right_entity_index);
131
132        // Swap only makes sense if values differ
133        left_val != right_val
134    }
135
136    fn do_move(&self, score_director: &mut dyn ScoreDirector<S>) {
137        // Get both values using typed getter - zero erasure
138        let left_value = (self.getter)(score_director.working_solution(), self.left_entity_index);
139        let right_value = (self.getter)(score_director.working_solution(), self.right_entity_index);
140
141        // Notify before changes
142        score_director.before_variable_changed(
143            self.descriptor_index,
144            self.left_entity_index,
145            self.variable_name,
146        );
147        score_director.before_variable_changed(
148            self.descriptor_index,
149            self.right_entity_index,
150            self.variable_name,
151        );
152
153        // Swap: left gets right's value, right gets left's value
154        (self.setter)(
155            score_director.working_solution_mut(),
156            self.left_entity_index,
157            right_value.clone(),
158        );
159        (self.setter)(
160            score_director.working_solution_mut(),
161            self.right_entity_index,
162            left_value.clone(),
163        );
164
165        // Notify after changes
166        score_director.after_variable_changed(
167            self.descriptor_index,
168            self.left_entity_index,
169            self.variable_name,
170        );
171        score_director.after_variable_changed(
172            self.descriptor_index,
173            self.right_entity_index,
174            self.variable_name,
175        );
176
177        // Register typed undo closure - swap back
178        let setter = self.setter;
179        let left_idx = self.left_entity_index;
180        let right_idx = self.right_entity_index;
181        score_director.register_undo(Box::new(move |s: &mut S| {
182            // Restore original values
183            setter(s, left_idx, left_value);
184            setter(s, right_idx, right_value);
185        }));
186    }
187
188    fn descriptor_index(&self) -> usize {
189        self.descriptor_index
190    }
191
192    fn entity_indices(&self) -> &[usize] {
193        &self.indices
194    }
195
196    fn variable_name(&self) -> &str {
197        self.variable_name
198    }
199}
200
201#[cfg(test)]
202mod tests {
203    use super::*;
204    use solverforge_core::domain::{EntityDescriptor, SolutionDescriptor, TypedEntityExtractor};
205    use solverforge_core::score::SimpleScore;
206    use solverforge_scoring::{RecordingScoreDirector, SimpleScoreDirector};
207    use std::any::TypeId;
208
209    #[derive(Clone, Debug)]
210    struct Task {
211        id: usize,
212        priority: Option<i32>,
213    }
214
215    #[derive(Clone, Debug)]
216    struct TaskSolution {
217        tasks: Vec<Task>,
218        score: Option<SimpleScore>,
219    }
220
221    impl PlanningSolution for TaskSolution {
222        type Score = SimpleScore;
223        fn score(&self) -> Option<Self::Score> {
224            self.score
225        }
226        fn set_score(&mut self, score: Option<Self::Score>) {
227            self.score = score;
228        }
229    }
230
231    fn get_tasks(s: &TaskSolution) -> &Vec<Task> {
232        &s.tasks
233    }
234
235    fn get_tasks_mut(s: &mut TaskSolution) -> &mut Vec<Task> {
236        &mut s.tasks
237    }
238
239    // Typed getter - zero erasure
240    fn get_priority(s: &TaskSolution, idx: usize) -> Option<i32> {
241        s.tasks.get(idx).and_then(|t| t.priority)
242    }
243
244    // Typed setter - zero erasure
245    fn set_priority(s: &mut TaskSolution, idx: usize, v: Option<i32>) {
246        if let Some(task) = s.tasks.get_mut(idx) {
247            task.priority = v;
248        }
249    }
250
251    fn create_director(
252        tasks: Vec<Task>,
253    ) -> SimpleScoreDirector<TaskSolution, impl Fn(&TaskSolution) -> SimpleScore> {
254        let solution = TaskSolution { tasks, score: None };
255
256        let extractor = Box::new(TypedEntityExtractor::new(
257            "Task",
258            "tasks",
259            get_tasks,
260            get_tasks_mut,
261        ));
262        let entity_desc =
263            EntityDescriptor::new("Task", TypeId::of::<Task>(), "tasks").with_extractor(extractor);
264
265        let descriptor = SolutionDescriptor::new("TaskSolution", TypeId::of::<TaskSolution>())
266            .with_entity(entity_desc);
267
268        SimpleScoreDirector::with_calculator(solution, descriptor, |_| SimpleScore::of(0))
269    }
270
271    #[test]
272    fn test_swap_move_do_and_undo() {
273        let tasks = vec![
274            Task {
275                id: 0,
276                priority: Some(1),
277            },
278            Task {
279                id: 1,
280                priority: Some(5),
281            },
282        ];
283        let mut director = create_director(tasks);
284
285        let m = SwapMove::<TaskSolution, i32>::new(0, 1, get_priority, set_priority, "priority", 0);
286        assert!(m.is_doable(&director));
287
288        {
289            let mut recording = RecordingScoreDirector::new(&mut director);
290            m.do_move(&mut recording);
291
292            // Verify swap using typed getter
293            assert_eq!(get_priority(recording.working_solution(), 0), Some(5));
294            assert_eq!(get_priority(recording.working_solution(), 1), Some(1));
295
296            // Undo via recording
297            recording.undo_changes();
298        }
299
300        // Verify restored using typed getter
301        assert_eq!(get_priority(director.working_solution(), 0), Some(1));
302        assert_eq!(get_priority(director.working_solution(), 1), Some(5));
303
304        // Verify entity identity preserved
305        let solution = director.working_solution();
306        assert_eq!(solution.tasks[0].id, 0);
307        assert_eq!(solution.tasks[1].id, 1);
308    }
309
310    #[test]
311    fn test_swap_same_value_not_doable() {
312        let tasks = vec![
313            Task {
314                id: 0,
315                priority: Some(5),
316            },
317            Task {
318                id: 1,
319                priority: Some(5),
320            },
321        ];
322        let director = create_director(tasks);
323
324        let m = SwapMove::<TaskSolution, i32>::new(0, 1, get_priority, set_priority, "priority", 0);
325        assert!(
326            !m.is_doable(&director),
327            "swapping same values should not be doable"
328        );
329    }
330
331    #[test]
332    fn test_swap_self_not_doable() {
333        let tasks = vec![Task {
334            id: 0,
335            priority: Some(1),
336        }];
337        let director = create_director(tasks);
338
339        let m = SwapMove::<TaskSolution, i32>::new(0, 0, get_priority, set_priority, "priority", 0);
340        assert!(!m.is_doable(&director), "self-swap should not be doable");
341    }
342
343    #[test]
344    fn test_swap_entity_indices() {
345        let m = SwapMove::<TaskSolution, i32>::new(2, 5, get_priority, set_priority, "priority", 0);
346        assert_eq!(m.entity_indices(), &[2, 5]);
347    }
348}