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