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