solverforge_solver/heuristic/move/
pillar_swap.rs

1//! PillarSwapMove - exchanges values between two pillars.
2//!
3//! A pillar is a group of entities that share the same variable value.
4//! This move swaps the values between two pillars atomically.
5//!
6//! # Zero-Erasure Design
7//!
8//! PillarSwapMove 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 pillars.
20///
21/// Stores pillar 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#[derive(Clone)]
28pub struct PillarSwapMove<S, V> {
29    left_indices: Vec<usize>,
30    right_indices: Vec<usize>,
31    descriptor_index: usize,
32    variable_name: &'static str,
33    /// Typed getter function pointer - zero erasure.
34    getter: fn(&S, usize) -> Option<V>,
35    /// Typed setter function pointer - zero erasure.
36    setter: fn(&mut S, usize, Option<V>),
37    _phantom: PhantomData<S>,
38}
39
40impl<S, V: Debug> Debug for PillarSwapMove<S, V> {
41    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
42        f.debug_struct("PillarSwapMove")
43            .field("left_indices", &self.left_indices)
44            .field("right_indices", &self.right_indices)
45            .field("descriptor_index", &self.descriptor_index)
46            .field("variable_name", &self.variable_name)
47            .finish()
48    }
49}
50
51impl<S, V> PillarSwapMove<S, V> {
52    /// Creates a new pillar swap move with typed function pointers.
53    ///
54    /// # Arguments
55    /// * `left_indices` - Indices of entities in the left pillar
56    /// * `right_indices` - Indices of entities in the right pillar
57    /// * `getter` - Typed getter function pointer
58    /// * `setter` - Typed setter function pointer
59    /// * `variable_name` - Name of the variable being swapped
60    /// * `descriptor_index` - Index in the entity descriptor
61    pub fn new(
62        left_indices: Vec<usize>,
63        right_indices: Vec<usize>,
64        getter: fn(&S, usize) -> Option<V>,
65        setter: fn(&mut S, usize, Option<V>),
66        variable_name: &'static str,
67        descriptor_index: usize,
68    ) -> Self {
69        Self {
70            left_indices,
71            right_indices,
72            descriptor_index,
73            variable_name,
74            getter,
75            setter,
76            _phantom: PhantomData,
77        }
78    }
79
80    /// Returns the left pillar indices.
81    pub fn left_indices(&self) -> &[usize] {
82        &self.left_indices
83    }
84
85    /// Returns the right pillar indices.
86    pub fn right_indices(&self) -> &[usize] {
87        &self.right_indices
88    }
89}
90
91impl<S, V> Move<S> for PillarSwapMove<S, V>
92where
93    S: PlanningSolution,
94    V: Clone + PartialEq + Send + Sync + Debug + 'static,
95{
96    fn is_doable(&self, score_director: &dyn ScoreDirector<S>) -> bool {
97        if self.left_indices.is_empty() || self.right_indices.is_empty() {
98            return false;
99        }
100
101        let count = score_director.entity_count(self.descriptor_index);
102        let max = count.unwrap_or(0);
103
104        // Check all indices valid
105        for &idx in self.left_indices.iter().chain(&self.right_indices) {
106            if idx >= max {
107                return false;
108            }
109        }
110
111        // Get representative values using typed getter - zero erasure
112        let left_val = self
113            .left_indices
114            .first()
115            .map(|&idx| (self.getter)(score_director.working_solution(), idx));
116        let right_val = self
117            .right_indices
118            .first()
119            .map(|&idx| (self.getter)(score_director.working_solution(), idx));
120
121        left_val != right_val
122    }
123
124    fn do_move(&self, score_director: &mut dyn ScoreDirector<S>) {
125        // Capture all old values using typed getter - zero erasure
126        let left_old: Vec<(usize, Option<V>)> = self
127            .left_indices
128            .iter()
129            .map(|&idx| (idx, (self.getter)(score_director.working_solution(), idx)))
130            .collect();
131        let right_old: Vec<(usize, Option<V>)> = self
132            .right_indices
133            .iter()
134            .map(|&idx| (idx, (self.getter)(score_director.working_solution(), idx)))
135            .collect();
136
137        // Get representative values for the swap
138        let left_value = left_old.first().and_then(|(_, v)| v.clone());
139        let right_value = right_old.first().and_then(|(_, v)| v.clone());
140
141        // Notify before changes for all entities
142        for &idx in self.left_indices.iter().chain(&self.right_indices) {
143            score_director.before_variable_changed(self.descriptor_index, idx, self.variable_name);
144        }
145
146        // Swap: left gets right's value using typed setter - zero erasure
147        for &idx in &self.left_indices {
148            (self.setter)(
149                score_director.working_solution_mut(),
150                idx,
151                right_value.clone(),
152            );
153        }
154        // Right gets left's value
155        for &idx in &self.right_indices {
156            (self.setter)(
157                score_director.working_solution_mut(),
158                idx,
159                left_value.clone(),
160            );
161        }
162
163        // Notify after changes
164        for &idx in self.left_indices.iter().chain(&self.right_indices) {
165            score_director.after_variable_changed(self.descriptor_index, idx, self.variable_name);
166        }
167
168        // Register typed undo closure - restore all original values
169        let setter = self.setter;
170        score_director.register_undo(Box::new(move |s: &mut S| {
171            for (idx, old_value) in left_old {
172                setter(s, idx, old_value);
173            }
174            for (idx, old_value) in right_old {
175                setter(s, idx, old_value);
176            }
177        }));
178    }
179
180    fn descriptor_index(&self) -> usize {
181        self.descriptor_index
182    }
183
184    fn entity_indices(&self) -> &[usize] {
185        // Return left indices as primary; caller can use left_indices/right_indices for full info
186        &self.left_indices
187    }
188
189    fn variable_name(&self) -> &str {
190        self.variable_name
191    }
192}
193
194#[cfg(test)]
195mod tests {
196    use super::*;
197    use solverforge_core::domain::{EntityDescriptor, SolutionDescriptor, TypedEntityExtractor};
198    use solverforge_core::score::SimpleScore;
199    use solverforge_scoring::{RecordingScoreDirector, SimpleScoreDirector};
200    use std::any::TypeId;
201
202    #[derive(Clone, Debug)]
203    struct Employee {
204        id: usize,
205        shift: Option<i32>,
206    }
207
208    #[derive(Clone, Debug)]
209    struct Solution {
210        employees: Vec<Employee>,
211        score: Option<SimpleScore>,
212    }
213
214    impl PlanningSolution for Solution {
215        type Score = SimpleScore;
216        fn score(&self) -> Option<Self::Score> {
217            self.score
218        }
219        fn set_score(&mut self, score: Option<Self::Score>) {
220            self.score = score;
221        }
222    }
223
224    // Typed getter - zero erasure
225    fn get_shift(s: &Solution, idx: usize) -> Option<i32> {
226        s.employees.get(idx).and_then(|e| e.shift)
227    }
228
229    // Typed setter - zero erasure
230    fn set_shift(s: &mut Solution, idx: usize, v: Option<i32>) {
231        if let Some(e) = s.employees.get_mut(idx) {
232            e.shift = v;
233        }
234    }
235
236    fn create_director(
237        employees: Vec<Employee>,
238    ) -> SimpleScoreDirector<Solution, impl Fn(&Solution) -> SimpleScore> {
239        let solution = Solution {
240            employees,
241            score: None,
242        };
243        let extractor = Box::new(TypedEntityExtractor::new(
244            "Employee",
245            "employees",
246            |s: &Solution| &s.employees,
247            |s: &mut Solution| &mut s.employees,
248        ));
249        let entity_desc = EntityDescriptor::new("Employee", TypeId::of::<Employee>(), "employees")
250            .with_extractor(extractor);
251        let descriptor =
252            SolutionDescriptor::new("Solution", TypeId::of::<Solution>()).with_entity(entity_desc);
253        SimpleScoreDirector::with_calculator(solution, descriptor, |_| SimpleScore::of(0))
254    }
255
256    #[test]
257    fn test_pillar_swap_all_entities() {
258        let mut director = create_director(vec![
259            Employee {
260                id: 0,
261                shift: Some(1),
262            },
263            Employee {
264                id: 1,
265                shift: Some(1),
266            },
267            Employee {
268                id: 2,
269                shift: Some(2),
270            },
271            Employee {
272                id: 3,
273                shift: Some(2),
274            },
275        ]);
276
277        let m = PillarSwapMove::<Solution, i32>::new(
278            vec![0, 1],
279            vec![2, 3],
280            get_shift,
281            set_shift,
282            "shift",
283            0,
284        );
285        assert!(m.is_doable(&director));
286
287        {
288            let mut recording = RecordingScoreDirector::new(&mut director);
289            m.do_move(&mut recording);
290
291            // Verify swap using typed getter
292            assert_eq!(get_shift(recording.working_solution(), 0), Some(2));
293            assert_eq!(get_shift(recording.working_solution(), 1), Some(2));
294            assert_eq!(get_shift(recording.working_solution(), 2), Some(1));
295            assert_eq!(get_shift(recording.working_solution(), 3), Some(1));
296
297            recording.undo_changes();
298        }
299
300        assert_eq!(get_shift(director.working_solution(), 0), Some(1));
301        assert_eq!(get_shift(director.working_solution(), 1), Some(1));
302        assert_eq!(get_shift(director.working_solution(), 2), Some(2));
303        assert_eq!(get_shift(director.working_solution(), 3), Some(2));
304
305        // Verify entity identity preserved
306        let solution = director.working_solution();
307        assert_eq!(solution.employees[0].id, 0);
308        assert_eq!(solution.employees[1].id, 1);
309        assert_eq!(solution.employees[2].id, 2);
310        assert_eq!(solution.employees[3].id, 3);
311    }
312
313    #[test]
314    fn test_pillar_swap_same_value_not_doable() {
315        let director = create_director(vec![
316            Employee {
317                id: 0,
318                shift: Some(1),
319            },
320            Employee {
321                id: 1,
322                shift: Some(1),
323            },
324        ]);
325        let m = PillarSwapMove::<Solution, i32>::new(
326            vec![0],
327            vec![1],
328            get_shift,
329            set_shift,
330            "shift",
331            0,
332        );
333        assert!(!m.is_doable(&director));
334    }
335
336    #[test]
337    fn test_pillar_swap_empty_pillar_not_doable() {
338        let director = create_director(vec![Employee {
339            id: 0,
340            shift: Some(1),
341        }]);
342        let m =
343            PillarSwapMove::<Solution, i32>::new(vec![], vec![0], get_shift, set_shift, "shift", 0);
344        assert!(!m.is_doable(&director));
345    }
346}