Skip to main content

solverforge_solver/heuristic/move/
pillar_change.rs

1/* PillarChangeMove - assigns a value to all entities in a pillar.
2
3A pillar is a group of entities that share the same variable value.
4This move changes all of them to a new value atomically.
5
6# Zero-Erasure Design
7
8PillarChangeMove 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, SmallVec};
16use solverforge_core::domain::PlanningSolution;
17use solverforge_scoring::Director;
18
19use super::metadata::{
20    encode_option_debug, encode_usize, hash_str, MoveTabuScope, ScopedEntityTabuToken,
21};
22use super::{Move, MoveTabuSignature};
23
24/// A move that assigns a value to all entities in a pillar.
25///
26/// Stores entity 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 PillarChangeMove<S, V> {
33    entity_indices: Vec<usize>,
34    descriptor_index: usize,
35    variable_name: &'static str,
36    to_value: Option<V>,
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 PillarChangeMove<S, V> {
45    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
46        f.debug_struct("PillarChangeMove")
47            .field("entity_indices", &self.entity_indices)
48            .field("descriptor_index", &self.descriptor_index)
49            .field("variable_index", &self.variable_index)
50            .field("variable_name", &self.variable_name)
51            .field("to_value", &self.to_value)
52            .finish()
53    }
54}
55
56impl<S, V> PillarChangeMove<S, V> {
57    /// Creates a new pillar change move with concrete function pointers.
58    ///
59    /// # Arguments
60    /// * `entity_indices` - Indices of entities in the pillar
61    /// * `to_value` - The new value to assign to all entities
62    /// * `getter` - Concrete getter function pointer
63    /// * `setter` - Concrete setter function pointer
64    /// * `variable_name` - Name of the variable being changed
65    /// * `descriptor_index` - Index in the entity descriptor
66    pub fn new(
67        entity_indices: Vec<usize>,
68        to_value: Option<V>,
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            entity_indices,
77            descriptor_index,
78            variable_name,
79            to_value,
80            getter,
81            setter,
82            variable_index,
83        }
84    }
85
86    pub fn pillar_size(&self) -> usize {
87        self.entity_indices.len()
88    }
89
90    pub fn to_value(&self) -> Option<&V> {
91        self.to_value.as_ref()
92    }
93}
94
95impl<S, V> Move<S> for PillarChangeMove<S, V>
96where
97    S: PlanningSolution,
98    V: Clone + PartialEq + Send + Sync + Debug + 'static,
99{
100    type Undo = SmallVec<[(usize, Option<V>); 4]>;
101
102    fn is_doable<D: Director<S>>(&self, score_director: &D) -> bool {
103        if self.entity_indices.is_empty() {
104            return false;
105        }
106
107        // Check first entity exists
108        let count = score_director.entity_count(self.descriptor_index);
109        if let Some(&first_idx) = self.entity_indices.first() {
110            if count.is_none_or(|c| first_idx >= c) {
111                return false;
112            }
113
114            // Get current value using concrete getter - zero erasure
115            let current = (self.getter)(
116                score_director.working_solution(),
117                first_idx,
118                self.variable_index,
119            );
120
121            match (&current, &self.to_value) {
122                (None, None) => false,
123                (Some(cur), Some(target)) => cur != target,
124                _ => true,
125            }
126        } else {
127            false
128        }
129    }
130
131    fn do_move<D: Director<S>>(&self, score_director: &mut D) -> Self::Undo {
132        // Capture old values using concrete getter - zero erasure
133        let old_values: SmallVec<[(usize, Option<V>); 4]> = self
134            .entity_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
144        // Notify before changes for all entities
145        for &idx in &self.entity_indices {
146            score_director.before_variable_changed(self.descriptor_index, idx);
147        }
148
149        // Apply new value to all entities using concrete setter - zero erasure
150        for &idx in &self.entity_indices {
151            (self.setter)(
152                score_director.working_solution_mut(),
153                idx,
154                self.variable_index,
155                self.to_value.clone(),
156            );
157        }
158
159        // Notify after changes
160        for &idx in &self.entity_indices {
161            score_director.after_variable_changed(self.descriptor_index, idx);
162        }
163
164        old_values
165    }
166
167    fn undo_move<D: Director<S>>(&self, score_director: &mut D, undo: Self::Undo) {
168        for (idx, _) in &undo {
169            score_director.before_variable_changed(self.descriptor_index, *idx);
170        }
171        for (idx, old_value) in undo {
172            (self.setter)(
173                score_director.working_solution_mut(),
174                idx,
175                self.variable_index,
176                old_value,
177            );
178        }
179        for &idx in &self.entity_indices {
180            score_director.after_variable_changed(self.descriptor_index, idx);
181        }
182    }
183
184    fn descriptor_index(&self) -> usize {
185        self.descriptor_index
186    }
187
188    fn entity_indices(&self) -> &[usize] {
189        &self.entity_indices
190    }
191
192    fn variable_name(&self) -> &str {
193        self.variable_name
194    }
195
196    fn tabu_signature<D: Director<S>>(&self, score_director: &D) -> Cow<'_, MoveTabuSignature> {
197        let from_value = self.entity_indices.first().and_then(|&idx| {
198            (self.getter)(score_director.working_solution(), idx, self.variable_index)
199        });
200        let from_id = encode_option_debug(from_value.as_ref());
201        let to_id = encode_option_debug(self.to_value.as_ref());
202        let variable_id = hash_str(self.variable_name);
203        let scope = MoveTabuScope::new(self.descriptor_index, self.variable_name);
204        let entity_ids: SmallVec<[u64; 2]> = self
205            .entity_indices
206            .iter()
207            .map(|&idx| encode_usize(idx))
208            .collect();
209        let entity_tokens: SmallVec<[ScopedEntityTabuToken; 2]> = entity_ids
210            .iter()
211            .copied()
212            .map(|entity_id| scope.entity_token(entity_id))
213            .collect();
214        let mut move_id = smallvec![
215            encode_usize(self.descriptor_index),
216            variable_id,
217            encode_usize(self.entity_indices.len()),
218            from_id,
219            to_id
220        ];
221        move_id.extend(entity_ids.iter().copied());
222
223        let mut undo_move_id = smallvec![
224            encode_usize(self.descriptor_index),
225            variable_id,
226            encode_usize(self.entity_indices.len()),
227            to_id,
228            from_id
229        ];
230        undo_move_id.extend(entity_ids.iter().copied());
231
232        Cow::Owned(
233            MoveTabuSignature::new(scope, move_id, undo_move_id)
234                .with_entity_tokens(entity_tokens)
235                .with_destination_value_tokens([scope.value_token(to_id)]),
236        )
237    }
238}