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/// Undo is handled by `RecordingDirector`, not by this move.
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    fn is_doable<D: Director<S>>(&self, score_director: &D) -> bool {
115        if self.left_indices.is_empty() || self.right_indices.is_empty() {
116            return false;
117        }
118
119        let count = score_director.entity_count(self.descriptor_index);
120        let max = count.unwrap_or(0);
121
122        // Check all indices valid
123        for &idx in self.left_indices.iter().chain(&self.right_indices) {
124            if idx >= max {
125                return false;
126            }
127        }
128
129        // Get representative values using concrete getter - zero erasure
130        let left_val = self
131            .left_indices
132            .first()
133            .map(|&idx| (self.getter)(score_director.working_solution(), idx, self.variable_index));
134        let right_val = self
135            .right_indices
136            .first()
137            .map(|&idx| (self.getter)(score_director.working_solution(), idx, self.variable_index));
138
139        left_val != right_val
140    }
141
142    fn do_move<D: Director<S>>(&self, score_director: &mut D) {
143        // Capture all old values using concrete getter - zero erasure
144        let left_old: Vec<(usize, Option<V>)> = self
145            .left_indices
146            .iter()
147            .map(|&idx| {
148                (
149                    idx,
150                    (self.getter)(score_director.working_solution(), idx, self.variable_index),
151                )
152            })
153            .collect();
154        let right_old: Vec<(usize, Option<V>)> = self
155            .right_indices
156            .iter()
157            .map(|&idx| {
158                (
159                    idx,
160                    (self.getter)(score_director.working_solution(), idx, self.variable_index),
161                )
162            })
163            .collect();
164
165        // Get representative values for the swap
166        let left_value = left_old.first().and_then(|(_, v)| v.clone());
167        let right_value = right_old.first().and_then(|(_, v)| v.clone());
168
169        // Notify before changes for all entities
170        for &idx in self.left_indices.iter().chain(&self.right_indices) {
171            score_director.before_variable_changed(self.descriptor_index, idx);
172        }
173
174        // Swap: left gets right's value using concrete setter - zero erasure
175        for &idx in &self.left_indices {
176            (self.setter)(
177                score_director.working_solution_mut(),
178                idx,
179                self.variable_index,
180                right_value.clone(),
181            );
182        }
183        // Right gets left's value
184        for &idx in &self.right_indices {
185            (self.setter)(
186                score_director.working_solution_mut(),
187                idx,
188                self.variable_index,
189                left_value.clone(),
190            );
191        }
192
193        // Notify after changes
194        for &idx in self.left_indices.iter().chain(&self.right_indices) {
195            score_director.after_variable_changed(self.descriptor_index, idx);
196        }
197
198        // Register concrete undo closure - restore all original values
199        let setter = self.setter;
200        let variable_index = self.variable_index;
201        score_director.register_undo(Box::new(move |s: &mut S| {
202            for (idx, old_value) in left_old {
203                setter(s, idx, variable_index, old_value);
204            }
205            for (idx, old_value) in right_old {
206                setter(s, idx, variable_index, old_value);
207            }
208        }));
209    }
210
211    fn descriptor_index(&self) -> usize {
212        self.descriptor_index
213    }
214
215    fn entity_indices(&self) -> &[usize] {
216        // Return left indices as primary; caller can use left_indices/right_indices for full info
217        &self.left_indices
218    }
219
220    fn variable_name(&self) -> &str {
221        self.variable_name
222    }
223
224    fn tabu_signature<D: Director<S>>(&self, score_director: &D) -> MoveTabuSignature {
225        let left_value = self.left_indices.first().and_then(|&idx| {
226            (self.getter)(score_director.working_solution(), idx, self.variable_index)
227        });
228        let right_value = self.right_indices.first().and_then(|&idx| {
229            (self.getter)(score_director.working_solution(), idx, self.variable_index)
230        });
231        let left_id = encode_option_debug(left_value.as_ref());
232        let right_id = encode_option_debug(right_value.as_ref());
233        let scope = MoveTabuScope::new(self.descriptor_index, self.variable_name);
234        let mut entity_ids: SmallVec<[u64; 2]> = self
235            .left_indices
236            .iter()
237            .chain(&self.right_indices)
238            .map(|&idx| encode_usize(idx))
239            .collect();
240        entity_ids.sort_unstable();
241        entity_ids.dedup();
242        let entity_tokens: SmallVec<[ScopedEntityTabuToken; 2]> = entity_ids
243            .iter()
244            .copied()
245            .map(|entity_id| scope.entity_token(entity_id))
246            .collect();
247
248        let mut move_id = scoped_move_identity(scope, TABU_OP_PILLAR_SWAP, std::iter::empty());
249        append_canonical_usize_slice_pair(&mut move_id, &self.left_indices, &self.right_indices);
250
251        MoveTabuSignature::new(scope, move_id.clone(), move_id)
252            .with_entity_tokens(entity_tokens)
253            .with_destination_value_tokens([
254                scope.value_token(right_id),
255                scope.value_token(left_id),
256            ])
257    }
258}