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, MoveAffectedEntity, 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: Debug> Debug for PillarSwapMove<S, V> {
45    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
46        f.debug_struct("PillarSwapMove")
47            .field("left_indices", &self.left_indices)
48            .field("right_indices", &self.right_indices)
49            .field("descriptor_index", &self.descriptor_index)
50            .field("variable_index", &self.variable_index)
51            .field("variable_name", &self.variable_name)
52            .finish()
53    }
54}
55
56impl<S, V> PillarSwapMove<S, V> {
57    /// Creates a new pillar swap move with concrete function pointers.
58    ///
59    /// # Arguments
60    /// * `left_indices` - Indices of entities in the left pillar
61    /// * `right_indices` - Indices of entities in the right pillar
62    /// * `getter` - Concrete getter function pointer
63    /// * `setter` - Concrete setter function pointer
64    /// * `variable_name` - Name of the variable being swapped
65    /// * `descriptor_index` - Index in the entity descriptor
66    pub fn new(
67        left_indices: Vec<usize>,
68        right_indices: Vec<usize>,
69        getter: fn(&S, usize, usize) -> Option<V>,
70        setter: fn(&mut S, usize, usize, Option<V>),
71        variable_index: usize,
72        variable_name: &'static str,
73        descriptor_index: usize,
74    ) -> Self {
75        Self {
76            left_indices,
77            right_indices,
78            descriptor_index,
79            variable_name,
80            getter,
81            setter,
82            variable_index,
83        }
84    }
85
86    pub fn left_indices(&self) -> &[usize] {
87        &self.left_indices
88    }
89
90    pub fn right_indices(&self) -> &[usize] {
91        &self.right_indices
92    }
93}
94
95impl<S, V> Move<S> for PillarSwapMove<S, V>
96where
97    S: PlanningSolution,
98    V: Clone + PartialEq + Send + Sync + Debug + 'static,
99{
100    type Undo = (Vec<(usize, Option<V>)>, Vec<(usize, Option<V>)>);
101
102    fn is_doable<D: Director<S>>(&self, score_director: &D) -> bool {
103        if self.left_indices.is_empty() || self.right_indices.is_empty() {
104            return false;
105        }
106
107        let count = score_director.entity_count(self.descriptor_index);
108        let max = count.unwrap_or(0);
109
110        // Check all indices valid
111        for &idx in self.left_indices.iter().chain(&self.right_indices) {
112            if idx >= max {
113                return false;
114            }
115        }
116
117        // Get representative values using concrete getter - zero erasure
118        let left_val = self
119            .left_indices
120            .first()
121            .map(|&idx| (self.getter)(score_director.working_solution(), idx, self.variable_index));
122        let right_val = self
123            .right_indices
124            .first()
125            .map(|&idx| (self.getter)(score_director.working_solution(), idx, self.variable_index));
126
127        left_val != right_val
128    }
129
130    fn do_move<D: Director<S>>(&self, score_director: &mut D) -> Self::Undo {
131        // Capture all old values using concrete getter - zero erasure
132        let left_old: Vec<(usize, Option<V>)> = self
133            .left_indices
134            .iter()
135            .map(|&idx| {
136                (
137                    idx,
138                    (self.getter)(score_director.working_solution(), idx, self.variable_index),
139                )
140            })
141            .collect();
142        let right_old: Vec<(usize, Option<V>)> = self
143            .right_indices
144            .iter()
145            .map(|&idx| {
146                (
147                    idx,
148                    (self.getter)(score_director.working_solution(), idx, self.variable_index),
149                )
150            })
151            .collect();
152
153        // Get representative values for the swap
154        let left_value = left_old.first().and_then(|(_, v)| v.clone());
155        let right_value = right_old.first().and_then(|(_, v)| v.clone());
156
157        // Notify before changes for all entities
158        for &idx in self.left_indices.iter().chain(&self.right_indices) {
159            score_director.before_variable_changed(self.descriptor_index, idx);
160        }
161
162        // Swap: left gets right's value using concrete setter - zero erasure
163        for &idx in &self.left_indices {
164            (self.setter)(
165                score_director.working_solution_mut(),
166                idx,
167                self.variable_index,
168                right_value.clone(),
169            );
170        }
171        // Right gets left's value
172        for &idx in &self.right_indices {
173            (self.setter)(
174                score_director.working_solution_mut(),
175                idx,
176                self.variable_index,
177                left_value.clone(),
178            );
179        }
180
181        // Notify after changes
182        for &idx in self.left_indices.iter().chain(&self.right_indices) {
183            score_director.after_variable_changed(self.descriptor_index, idx);
184        }
185
186        (left_old, right_old)
187    }
188
189    fn undo_move<D: Director<S>>(&self, score_director: &mut D, undo: Self::Undo) {
190        for (idx, _) in undo.0.iter().chain(&undo.1) {
191            score_director.before_variable_changed(self.descriptor_index, *idx);
192        }
193        for (idx, old_value) in undo.0.into_iter().chain(undo.1) {
194            (self.setter)(
195                score_director.working_solution_mut(),
196                idx,
197                self.variable_index,
198                old_value,
199            );
200        }
201        for &idx in self.left_indices.iter().chain(&self.right_indices) {
202            score_director.after_variable_changed(self.descriptor_index, idx);
203        }
204    }
205
206    fn descriptor_index(&self) -> usize {
207        self.descriptor_index
208    }
209
210    fn entity_indices(&self) -> &[usize] {
211        // Return left indices as primary; caller can use left_indices/right_indices for full info
212        &self.left_indices
213    }
214
215    fn variable_name(&self) -> &str {
216        self.variable_name
217    }
218
219    /// A pillar swap mutates BOTH halves. `entity_indices()` exposes the left
220    /// pillar as its primary slice, so the trait default would hide every
221    /// right-pillar entity from pin checks, tabu bookkeeping, and telemetry.
222    fn for_each_affected_entity<W: FnMut(MoveAffectedEntity<'_>)>(&self, visitor: &mut W) {
223        for &entity_index in self.left_indices.iter().chain(&self.right_indices) {
224            visitor(MoveAffectedEntity {
225                descriptor_index: self.descriptor_index,
226                entity_index,
227                variable_name: self.variable_name,
228            });
229        }
230    }
231
232    fn tabu_signature<D: Director<S>>(&self, score_director: &D) -> MoveTabuSignature {
233        let left_value = self.left_indices.first().and_then(|&idx| {
234            (self.getter)(score_director.working_solution(), idx, self.variable_index)
235        });
236        let right_value = self.right_indices.first().and_then(|&idx| {
237            (self.getter)(score_director.working_solution(), idx, self.variable_index)
238        });
239        let left_id = encode_option_debug(left_value.as_ref());
240        let right_id = encode_option_debug(right_value.as_ref());
241        let scope = MoveTabuScope::new(self.descriptor_index, self.variable_name);
242        let mut entity_ids: SmallVec<[u64; 2]> = self
243            .left_indices
244            .iter()
245            .chain(&self.right_indices)
246            .map(|&idx| encode_usize(idx))
247            .collect();
248        entity_ids.sort_unstable();
249        entity_ids.dedup();
250        let entity_tokens: SmallVec<[ScopedEntityTabuToken; 2]> = entity_ids
251            .iter()
252            .copied()
253            .map(|entity_id| scope.entity_token(entity_id))
254            .collect();
255
256        let mut move_id = scoped_move_identity(scope, TABU_OP_PILLAR_SWAP, std::iter::empty());
257        append_canonical_usize_slice_pair(&mut move_id, &self.left_indices, &self.right_indices);
258
259        MoveTabuSignature::new_self_undoing(scope, move_id)
260            .with_entity_tokens(entity_tokens)
261            .with_destination_value_tokens([
262                scope.value_token(right_id),
263                scope.value_token(left_id),
264            ])
265    }
266}