Skip to main content

solverforge_solver/heuristic/move/
list_multi_swap.rs

1/* ListMultiSwapMove - applies multiple independent intra-list swaps.
2
3This move coordinates several swaps across distinct list entities while keeping
4the same concrete function-pointer access pattern as the existing list moves.
5*/
6
7use std::fmt::Debug;
8
9use smallvec::SmallVec;
10use solverforge_core::domain::PlanningSolution;
11use solverforge_scoring::Director;
12
13use super::metadata::{
14    encode_option_debug, encode_usize, scoped_move_identity, MoveTabuScope, ScopedEntityTabuToken,
15    ScopedValueTabuToken, TABU_OP_LIST_MULTI_SWAP,
16};
17use super::{Move, MoveTabuSignature};
18
19/// A move that applies multiple independent intra-list swaps.
20///
21/// Each swap is encoded as `(entity_index, first_position, second_position)`.
22/// Swaps must target distinct entities; this keeps notification and undo
23/// semantics simple and avoids order-dependent overlap within one list.
24pub struct ListMultiSwapMove<S, V> {
25    swaps: SmallVec<[(usize, usize, usize); 4]>,
26    list_len: fn(&S, usize) -> usize,
27    list_get: fn(&S, usize, usize) -> Option<V>,
28    list_set: fn(&mut S, usize, usize, V),
29    variable_name: &'static str,
30    descriptor_index: usize,
31    indices: SmallVec<[usize; 4]>,
32    require_score_improvement: bool,
33}
34
35impl<S, V> Clone for ListMultiSwapMove<S, V> {
36    fn clone(&self) -> Self {
37        Self {
38            swaps: self.swaps.clone(),
39            list_len: self.list_len,
40            list_get: self.list_get,
41            list_set: self.list_set,
42            variable_name: self.variable_name,
43            descriptor_index: self.descriptor_index,
44            indices: self.indices.clone(),
45            require_score_improvement: self.require_score_improvement,
46        }
47    }
48}
49
50impl<S, V: Debug> Debug for ListMultiSwapMove<S, V> {
51    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
52        f.debug_struct("ListMultiSwapMove")
53            .field("swaps", &self.swaps)
54            .field("variable_name", &self.variable_name)
55            .field("require_score_improvement", &self.require_score_improvement)
56            .finish()
57    }
58}
59
60impl<S, V> ListMultiSwapMove<S, V> {
61    pub fn new(
62        swaps: &[(usize, usize, usize)],
63        list_len: fn(&S, usize) -> usize,
64        list_get: fn(&S, usize, usize) -> Option<V>,
65        list_set: fn(&mut S, usize, usize, V),
66        variable_name: &'static str,
67        descriptor_index: usize,
68    ) -> Self {
69        let mut indices = SmallVec::new();
70        for &(entity, _, _) in swaps {
71            if !indices.contains(&entity) {
72                indices.push(entity);
73            }
74        }
75        Self {
76            swaps: swaps.iter().copied().collect(),
77            list_len,
78            list_get,
79            list_set,
80            variable_name,
81            descriptor_index,
82            indices,
83            require_score_improvement: false,
84        }
85    }
86
87    pub fn with_require_score_improvement(mut self, require_score_improvement: bool) -> Self {
88        self.require_score_improvement = require_score_improvement;
89        self
90    }
91
92    pub fn swaps(&self) -> &[(usize, usize, usize)] {
93        &self.swaps
94    }
95}
96
97impl<S, V> Move<S> for ListMultiSwapMove<S, V>
98where
99    S: PlanningSolution,
100    V: Clone + PartialEq + Send + Sync + Debug + 'static,
101{
102    type Undo = ();
103
104    fn is_doable<D: Director<S>>(&self, score_director: &D) -> bool {
105        if self.swaps.is_empty() {
106            return false;
107        }
108
109        let solution = score_director.working_solution();
110        let mut seen_entities = SmallVec::<[usize; 4]>::new();
111        for &(entity, first, second) in &self.swaps {
112            if first == second || seen_entities.contains(&entity) {
113                return false;
114            }
115            seen_entities.push(entity);
116
117            let len = (self.list_len)(solution, entity);
118            if first >= len || second >= len {
119                return false;
120            }
121
122            let first_val = (self.list_get)(solution, entity, first);
123            let second_val = (self.list_get)(solution, entity, second);
124            if first_val != second_val {
125                continue;
126            }
127            return false;
128        }
129
130        true
131    }
132
133    fn do_move<D: Director<S>>(&self, score_director: &mut D) -> Self::Undo {
134        let mut values = SmallVec::<[(V, V); 4]>::new();
135        for &(entity, first, second) in &self.swaps {
136            let first_val = (self.list_get)(score_director.working_solution(), entity, first)
137                .expect("first position should be valid");
138            let second_val = (self.list_get)(score_director.working_solution(), entity, second)
139                .expect("second position should be valid");
140            values.push((first_val, second_val));
141        }
142
143        for &entity in &self.indices {
144            score_director.before_variable_changed(self.descriptor_index, entity);
145        }
146
147        for (&(entity, first, second), (first_val, second_val)) in
148            self.swaps.iter().zip(values.iter())
149        {
150            (self.list_set)(
151                score_director.working_solution_mut(),
152                entity,
153                first,
154                second_val.clone(),
155            );
156            (self.list_set)(
157                score_director.working_solution_mut(),
158                entity,
159                second,
160                first_val.clone(),
161            );
162        }
163
164        for &entity in &self.indices {
165            score_director.after_variable_changed(self.descriptor_index, entity);
166        }
167    }
168
169    fn undo_move<D: Director<S>>(&self, score_director: &mut D, (): Self::Undo) {
170        self.do_move(score_director);
171    }
172
173    fn descriptor_index(&self) -> usize {
174        self.descriptor_index
175    }
176
177    fn entity_indices(&self) -> &[usize] {
178        &self.indices
179    }
180
181    fn variable_name(&self) -> &str {
182        self.variable_name
183    }
184
185    fn telemetry_label(&self) -> &'static str {
186        "list_multi_swap"
187    }
188
189    fn requires_score_improvement(&self) -> bool {
190        self.require_score_improvement
191    }
192
193    fn tabu_signature<D: Director<S>>(&self, score_director: &D) -> MoveTabuSignature {
194        let scope = MoveTabuScope::new(self.descriptor_index, self.variable_name);
195        let mut entity_tokens = SmallVec::<[ScopedEntityTabuToken; 2]>::new();
196        for &entity in &self.indices {
197            entity_tokens.push(scope.entity_token(encode_usize(entity)));
198        }
199
200        let mut destination_value_tokens = SmallVec::<[ScopedValueTabuToken; 2]>::new();
201        for &(entity, first, second) in &self.swaps {
202            let first_val = (self.list_get)(score_director.working_solution(), entity, first);
203            let second_val = (self.list_get)(score_director.working_solution(), entity, second);
204            destination_value_tokens
205                .push(scope.value_token(encode_option_debug(second_val.as_ref())));
206            destination_value_tokens
207                .push(scope.value_token(encode_option_debug(first_val.as_ref())));
208        }
209
210        let mut canonical = self
211            .swaps
212            .iter()
213            .map(|&(entity, first, second)| {
214                let (left, right) = if first <= second {
215                    (first, second)
216                } else {
217                    (second, first)
218                };
219                (entity, left, right)
220            })
221            .collect::<SmallVec<[(usize, usize, usize); 4]>>();
222        canonical.sort_unstable();
223
224        let mut components = SmallVec::<[u64; 8]>::new();
225        components.push(encode_usize(canonical.len()));
226        for (entity, first, second) in canonical {
227            components.push(encode_usize(entity));
228            components.push(encode_usize(first));
229            components.push(encode_usize(second));
230        }
231        let move_id = scoped_move_identity(scope, TABU_OP_LIST_MULTI_SWAP, components);
232
233        MoveTabuSignature::new(scope, move_id.clone(), move_id)
234            .with_entity_tokens(entity_tokens)
235            .with_destination_value_tokens(destination_value_tokens)
236    }
237}