Skip to main content

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}