Skip to main content

solverforge_solver/heuristic/move/
pillar_swap.rs

1/* PillarSwapMove - exchanges values between two pillars.
2
3A pillar is a group of entities that share the same variable value.
4This move swaps the values between two pillars atomically.
5
6# Zero-Erasure Design
7
8PillarSwapMove uses concrete function pointers instead of `dyn Any` for complete
9compile-time type safety. No runtime type checks or downcasting.
10*/
11
12use std::fmt::Debug;
13
14use smallvec::SmallVec;
15use solverforge_core::domain::PlanningSolution;
16use solverforge_scoring::Director;
17
18use super::metadata::{
19    append_canonical_usize_slice_pair, encode_option_debug, encode_usize, scoped_move_identity,
20    MoveTabuScope, ScopedEntityTabuToken, TABU_OP_PILLAR_SWAP,
21};
22use super::{Move, MoveTabuSignature};
23
24/// A move that swaps values between two pillars.
25///
26/// Stores pillar indices and concrete function pointers for zero-erasure access.
27/// `do_move` returns the previous pillar values as typed undo data.
28///
29/// # Type Parameters
30/// * `S` - The planning solution type
31/// * `V` - The variable value type
32pub struct PillarSwapMove<S, V> {
33    left_indices: Vec<usize>,
34    right_indices: Vec<usize>,
35    descriptor_index: usize,
36    variable_name: &'static str,
37    // Concrete getter function pointer - zero erasure.
38    getter: fn(&S, usize, usize) -> Option<V>,
39    // Concrete setter function pointer - zero erasure.
40    setter: fn(&mut S, usize, usize, Option<V>),
41    variable_index: usize,
42}
43
44impl<S, V: Clone> Clone for PillarSwapMove<S, V> {
45    fn clone(&self) -> Self {
46        Self {
47            left_indices: self.left_indices.clone(),
48            right_indices: self.right_indices.clone(),
49            descriptor_index: self.descriptor_index,
50            variable_name: self.variable_name,
51            getter: self.getter,
52            setter: self.setter,
53            variable_index: self.variable_index,
54        }
55    }
56}
57
58impl<S, V: Debug> Debug for PillarSwapMove<S, V> {
59    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
60        f.debug_struct("PillarSwapMove")
61            .field("left_indices", &self.left_indices)
62            .field("right_indices", &self.right_indices)
63            .field("descriptor_index", &self.descriptor_index)
64            .field("variable_index", &self.variable_index)
65            .field("variable_name", &self.variable_name)
66            .finish()
67    }
68}
69
70impl<S, V> PillarSwapMove<S, V> {
71    /// Creates a new pillar swap move with concrete function pointers.
72    ///
73    /// # Arguments
74    /// * `left_indices` - Indices of entities in the left pillar
75    /// * `right_indices` - Indices of entities in the right pillar
76    /// * `getter` - Concrete getter function pointer
77    /// * `setter` - Concrete setter function pointer
78    /// * `variable_name` - Name of the variable being swapped
79    /// * `descriptor_index` - Index in the entity descriptor
80    pub fn new(
81        left_indices: Vec<usize>,
82        right_indices: Vec<usize>,
83        getter: fn(&S, usize, usize) -> Option<V>,
84        setter: fn(&mut S, usize, usize, Option<V>),
85        variable_index: usize,
86        variable_name: &'static str,
87        descriptor_index: usize,
88    ) -> Self {
89        Self {
90            left_indices,
91            right_indices,
92            descriptor_index,
93            variable_name,
94            getter,
95            setter,
96            variable_index,
97        }
98    }
99
100    pub fn left_indices(&self) -> &[usize] {
101        &self.left_indices
102    }
103
104    pub fn right_indices(&self) -> &[usize] {
105        &self.right_indices
106    }
107}
108
109impl<S, V> Move<S> for PillarSwapMove<S, V>
110where
111    S: PlanningSolution,
112    V: Clone + PartialEq + Send + Sync + Debug + 'static,
113{
114    type Undo = (Vec<(usize, Option<V>)>, Vec<(usize, Option<V>)>);
115
116    fn is_doable<D: Director<S>>(&self, score_director: &D) -> bool {
117        if self.left_indices.is_empty() || self.right_indices.is_empty() {
118            return false;
119        }
120
121        let count = score_director.entity_count(self.descriptor_index);
122        let max = count.unwrap_or(0);
123
124        // Check all indices valid
125        for &idx in self.left_indices.iter().chain(&self.right_indices) {
126            if idx >= max {
127                return false;
128            }
129        }
130
131        // Get representative values using concrete getter - zero erasure
132        let left_val = self
133            .left_indices
134            .first()
135            .map(|&idx| (self.getter)(score_director.working_solution(), idx, self.variable_index));
136        let right_val = self
137            .right_indices
138            .first()
139            .map(|&idx| (self.getter)(score_director.working_solution(), idx, self.variable_index));
140
141        left_val != right_val
142    }
143
144    fn do_move<D: Director<S>>(&self, score_director: &mut D) -> Self::Undo {
145        // Capture all old values using concrete getter - zero erasure
146        let left_old: Vec<(usize, Option<V>)> = self
147            .left_indices
148            .iter()
149            .map(|&idx| {
150                (
151                    idx,
152                    (self.getter)(score_director.working_solution(), idx, self.variable_index),
153                )
154            })
155            .collect();
156        let right_old: Vec<(usize, Option<V>)> = self
157            .right_indices
158            .iter()
159            .map(|&idx| {
160                (
161                    idx,
162                    (self.getter)(score_director.working_solution(), idx, self.variable_index),
163                )
164            })
165            .collect();
166
167        // Get representative values for the swap
168        let left_value = left_old.first().and_then(|(_, v)| v.clone());
169        let right_value = right_old.first().and_then(|(_, v)| v.clone());
170
171        // Notify before changes for all entities
172        for &idx in self.left_indices.iter().chain(&self.right_indices) {
173            score_director.before_variable_changed(self.descriptor_index, idx);
174        }
175
176        // Swap: left gets right's value using concrete setter - zero erasure
177        for &idx in &self.left_indices {
178            (self.setter)(
179                score_director.working_solution_mut(),
180                idx,
181                self.variable_index,
182                right_value.clone(),
183            );
184        }
185        // Right gets left's value
186        for &idx in &self.right_indices {
187            (self.setter)(
188                score_director.working_solution_mut(),
189                idx,
190                self.variable_index,
191                left_value.clone(),
192            );
193        }
194
195        // Notify after changes
196        for &idx in self.left_indices.iter().chain(&self.right_indices) {
197            score_director.after_variable_changed(self.descriptor_index, idx);
198        }
199
200        (left_old, right_old)
201    }
202
203    fn undo_move<D: Director<S>>(&self, score_director: &mut D, undo: Self::Undo) {
204        for (idx, _) in undo.0.iter().chain(&undo.1) {
205            score_director.before_variable_changed(self.descriptor_index, *idx);
206        }
207        for (idx, old_value) in undo.0.into_iter().chain(undo.1) {
208            (self.setter)(
209                score_director.working_solution_mut(),
210                idx,
211                self.variable_index,
212                old_value,
213            );
214        }
215        for &idx in self.left_indices.iter().chain(&self.right_indices) {
216            score_director.after_variable_changed(self.descriptor_index, idx);
217        }
218    }
219
220    fn descriptor_index(&self) -> usize {
221        self.descriptor_index
222    }
223
224    fn entity_indices(&self) -> &[usize] {
225        // Return left indices as primary; caller can use left_indices/right_indices for full info
226        &self.left_indices
227    }
228
229    fn variable_name(&self) -> &str {
230        self.variable_name
231    }
232
233    fn tabu_signature<D: Director<S>>(&self, score_director: &D) -> MoveTabuSignature {
234        let left_value = self.left_indices.first().and_then(|&idx| {
235            (self.getter)(score_director.working_solution(), idx, self.variable_index)
236        });
237        let right_value = self.right_indices.first().and_then(|&idx| {
238            (self.getter)(score_director.working_solution(), idx, self.variable_index)
239        });
240        let left_id = encode_option_debug(left_value.as_ref());
241        let right_id = encode_option_debug(right_value.as_ref());
242        let scope = MoveTabuScope::new(self.descriptor_index, self.variable_name);
243        let mut entity_ids: SmallVec<[u64; 2]> = self
244            .left_indices
245            .iter()
246            .chain(&self.right_indices)
247            .map(|&idx| encode_usize(idx))
248            .collect();
249        entity_ids.sort_unstable();
250        entity_ids.dedup();
251        let entity_tokens: SmallVec<[ScopedEntityTabuToken; 2]> = entity_ids
252            .iter()
253            .copied()
254            .map(|entity_id| scope.entity_token(entity_id))
255            .collect();
256
257        let mut move_id = scoped_move_identity(scope, TABU_OP_PILLAR_SWAP, std::iter::empty());
258        append_canonical_usize_slice_pair(&mut move_id, &self.left_indices, &self.right_indices);
259
260        MoveTabuSignature::new(scope, move_id.clone(), move_id)
261            .with_entity_tokens(entity_tokens)
262            .with_destination_value_tokens([
263                scope.value_token(right_id),
264                scope.value_token(left_id),
265            ])
266    }
267}