Skip to main content

solverforge_solver/heuristic/move/
list_ruin.rs

1/* ListRuinMove - ruin-and-recreate move for Large Neighborhood Search on list variables.
2
3Removes selected elements from a list entity, then greedily reinserts each
4one into the best available position across all entities. This makes the move
5self-contained: it can be accepted by a local search acceptor without leaving
6the solution in a degenerate state.
7
8# Zero-Erasure Design
9
10Uses concrete function pointers for list operations. No `dyn Any`, no downcasting.
11*/
12
13use std::fmt::Debug;
14use std::marker::PhantomData;
15
16use smallvec::{smallvec, SmallVec};
17use solverforge_core::domain::PlanningSolution;
18use solverforge_scoring::Director;
19
20use super::metadata::{
21    encode_option_debug, encode_usize, hash_str, MoveTabuScope, ScopedValueTabuToken,
22};
23use super::{Move, MoveTabuSignature};
24
25/// A ruin-and-recreate move for Large Neighborhood Search on list variables.
26///
27/// Removes selected elements from a source entity, then reinserts each one
28/// greedily into the best position across all entities (including the source).
29/// The move is self-contained: accepting it leaves the solution valid.
30///
31/// # Type Parameters
32/// * `S` - The planning solution type
33/// * `V` - The list element value type
34///
35/// # Example
36///
37/// ```
38/// use solverforge_solver::heuristic::r#move::ListRuinMove;
39/// use solverforge_core::domain::PlanningSolution;
40/// use solverforge_core::score::SoftScore;
41///
42/// #[derive(Clone, Debug)]
43/// struct Route { stops: Vec<i32>, score: Option<SoftScore> }
44///
45/// impl PlanningSolution for Route {
46///     type Score = SoftScore;
47///     fn score(&self) -> Option<Self::Score> { self.score }
48///     fn set_score(&mut self, score: Option<Self::Score>) { self.score = score; }
49/// }
50///
51/// fn entity_count(s: &Route) -> usize { 1 }
52/// fn list_len(s: &Route, _: usize) -> usize { s.stops.len() }
53/// fn list_get(s: &Route, _: usize, pos: usize) -> Option<i32> { s.stops.get(pos).copied() }
54/// fn list_remove(s: &mut Route, _: usize, idx: usize) -> i32 { s.stops.remove(idx) }
55/// fn list_insert(s: &mut Route, _: usize, idx: usize, v: i32) { s.stops.insert(idx, v); }
56///
57/// // Ruin elements at indices 1 and 3, then recreate greedily
58/// let m = ListRuinMove::<Route, i32>::new(
59///     0,
60///     &[1, 3],
61///     entity_count,
62///     list_len, list_get, list_remove, list_insert,
63///     "stops", 0,
64/// );
65/// ```
66pub struct ListRuinMove<S, V> {
67    // Entity index to ruin from
68    entity_index: usize,
69    // Indices of elements to remove (sorted ascending)
70    element_indices: SmallVec<[usize; 8]>,
71    // Number of entities in solution (for recreate phase)
72    entity_count: fn(&S) -> usize,
73    list_len: fn(&S, usize) -> usize,
74    list_get: fn(&S, usize, usize) -> Option<V>,
75    // Remove element at index, returning it
76    list_remove: fn(&mut S, usize, usize) -> V,
77    // Insert element at index
78    list_insert: fn(&mut S, usize, usize, V),
79    variable_name: &'static str,
80    descriptor_index: usize,
81    _phantom: PhantomData<fn() -> V>,
82}
83
84impl<S, V> Clone for ListRuinMove<S, V> {
85    fn clone(&self) -> Self {
86        Self {
87            entity_index: self.entity_index,
88            element_indices: self.element_indices.clone(),
89            entity_count: self.entity_count,
90            list_len: self.list_len,
91            list_get: self.list_get,
92            list_remove: self.list_remove,
93            list_insert: self.list_insert,
94            variable_name: self.variable_name,
95            descriptor_index: self.descriptor_index,
96            _phantom: PhantomData,
97        }
98    }
99}
100
101impl<S, V: Debug> Debug for ListRuinMove<S, V> {
102    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
103        f.debug_struct("ListRuinMove")
104            .field("entity", &self.entity_index)
105            .field("elements", &self.element_indices.as_slice())
106            .field("variable_name", &self.variable_name)
107            .finish()
108    }
109}
110
111impl<S, V> ListRuinMove<S, V> {
112    /* Creates a new list ruin-and-recreate move.
113
114    # Arguments
115    * `entity_index` - Entity index to ruin from
116    * `element_indices` - Indices of elements to remove
117    * `entity_count` - Function returning total entity count
118    * `list_len` - Function to get list length for an entity
119    * `list_remove` - Function to remove element at index
120    * `list_insert` - Function to insert element at index
121    * `variable_name` - Name of the list variable
122    * `descriptor_index` - Entity descriptor index
123    */
124    #[allow(clippy::too_many_arguments)]
125    pub fn new(
126        entity_index: usize,
127        element_indices: &[usize],
128        entity_count: fn(&S) -> usize,
129        list_len: fn(&S, usize) -> usize,
130        list_get: fn(&S, usize, usize) -> Option<V>,
131        list_remove: fn(&mut S, usize, usize) -> V,
132        list_insert: fn(&mut S, usize, usize, V),
133        variable_name: &'static str,
134        descriptor_index: usize,
135    ) -> Self {
136        let mut indices: SmallVec<[usize; 8]> = SmallVec::from_slice(element_indices);
137        indices.sort_unstable();
138        Self {
139            entity_index,
140            element_indices: indices,
141            entity_count,
142            list_len,
143            list_get,
144            list_remove,
145            list_insert,
146            variable_name,
147            descriptor_index,
148            _phantom: PhantomData,
149        }
150    }
151
152    pub fn entity_index(&self) -> usize {
153        self.entity_index
154    }
155
156    pub fn element_indices(&self) -> &[usize] {
157        &self.element_indices
158    }
159
160    pub fn ruin_count(&self) -> usize {
161        self.element_indices.len()
162    }
163}
164
165pub(crate) fn final_positions_after_insertions(
166    placements: &SmallVec<[(usize, usize); 8]>,
167) -> SmallVec<[usize; 8]> {
168    let mut current_positions: SmallVec<[usize; 8]> = SmallVec::with_capacity(placements.len());
169
170    for i in 0..placements.len() {
171        let (entity_i, insert_pos_i) = placements[i];
172
173        for j in 0..i {
174            let (entity_j, _) = placements[j];
175            if entity_j == entity_i && current_positions[j] >= insert_pos_i {
176                current_positions[j] += 1;
177            }
178        }
179
180        current_positions.push(insert_pos_i);
181    }
182
183    current_positions
184}
185
186impl<S, V> Move<S> for ListRuinMove<S, V>
187where
188    S: PlanningSolution,
189    V: Clone + Send + Sync + Debug + 'static,
190{
191    type Undo = SmallVec<[(usize, usize); 8]>;
192
193    fn is_doable<D: Director<S>>(&self, score_director: &D) -> bool {
194        if self.element_indices.is_empty() {
195            return false;
196        }
197        let solution = score_director.working_solution();
198        let len = (self.list_len)(solution, self.entity_index);
199        self.element_indices.iter().all(|&idx| idx < len)
200    }
201
202    fn do_move<D: Director<S>>(&self, score_director: &mut D) -> Self::Undo {
203        let list_remove = self.list_remove;
204        let list_insert = self.list_insert;
205        let list_len = self.list_len;
206        let entity_count = self.entity_count;
207        let src = self.entity_index;
208        let descriptor = self.descriptor_index;
209
210        // --- Ruin phase: remove elements from source entity ---
211        score_director.before_variable_changed(descriptor, src);
212        let mut removed: SmallVec<[V; 8]> = SmallVec::new();
213        for &idx in self.element_indices.iter().rev() {
214            let value = list_remove(score_director.working_solution_mut(), src, idx);
215            removed.push(value);
216        }
217        // removed is in reverse removal order; reverse to get original order
218        removed.reverse();
219        score_director.after_variable_changed(descriptor, src);
220
221        // --- Recreate phase: greedily reinsert each element at best position ---
222        // Track where each element ends up for the undo closure.
223        let mut placements: SmallVec<[(usize, usize); 8]> = SmallVec::new();
224
225        let n_entities = entity_count(score_director.working_solution());
226
227        for elem in removed.iter() {
228            let mut best_score: Option<S::Score> = None;
229            let mut best_entity = src;
230            let mut best_pos = list_len(score_director.working_solution(), src);
231
232            for e in 0..n_entities {
233                let len = list_len(score_director.working_solution(), e);
234                for pos in 0..=len {
235                    score_director.before_variable_changed(descriptor, e);
236                    list_insert(score_director.working_solution_mut(), e, pos, elem.clone());
237                    score_director.after_variable_changed(descriptor, e);
238
239                    let candidate_score = score_director.calculate_score();
240                    if best_score.is_none_or(|b| candidate_score > b) {
241                        best_score = Some(candidate_score);
242                        best_entity = e;
243                        best_pos = pos;
244                    }
245
246                    score_director.before_variable_changed(descriptor, e);
247                    list_remove(score_director.working_solution_mut(), e, pos);
248                    score_director.after_variable_changed(descriptor, e);
249                }
250            }
251
252            // Apply the best insertion permanently
253            score_director.before_variable_changed(descriptor, best_entity);
254            list_insert(
255                score_director.working_solution_mut(),
256                best_entity,
257                best_pos,
258                elem.clone(),
259            );
260            score_director.after_variable_changed(descriptor, best_entity);
261
262            // Store the placement as recorded at insertion time (no adjustment needed;
263            // undo will compute actual current positions accounting for later insertions).
264            placements.push((best_entity, best_pos));
265        }
266
267        /* --- Register undo ---
268        placements[i] = (entity, pos) at the moment element i was inserted.
269        Later insertions j > i into the same entity at pos <= placements[i].pos
270        shifted element i rightward by 1 for each such j.
271        During undo we process in reverse: remove last-placed first.
272        At that point, only placements[j] with j > i (already removed) have been
273        undone, so the current position of element i is:
274        placements[i].pos + #{j > i : same entity AND placements[j].pos <= placements[i].pos}
275        which we compute on the fly as we iterate in reverse.
276
277        After collecting values, reinsert at original indices (ascending) in source entity.
278        Reinserting at orig_indices[k] in order k=0,1,... shifts later indices by 1,
279        but orig_indices is sorted ascending so each insertion at idx shifts positions > idx,
280        which are exactly the later orig_indices — so we insert at orig_indices[k] + k
281        to account for the k prior insertions that each shifted by 1.
282        */
283        placements
284    }
285
286    fn undo_move<D: Director<S>>(&self, score_director: &mut D, placements: Self::Undo) {
287        let n = placements.len();
288        let mut current_pos = final_positions_after_insertions(&placements);
289        let mut vals: SmallVec<[V; 8]> = SmallVec::with_capacity(n);
290
291        for i in (0..n).rev() {
292            let (entity_index, _) = placements[i];
293            let actual_pos = current_pos[i];
294            score_director.before_variable_changed(self.descriptor_index, entity_index);
295            vals.push((self.list_remove)(
296                score_director.working_solution_mut(),
297                entity_index,
298                actual_pos,
299            ));
300            score_director.after_variable_changed(self.descriptor_index, entity_index);
301
302            for j in 0..i {
303                let (other_entity, _) = placements[j];
304                if other_entity == entity_index && current_pos[j] > actual_pos {
305                    current_pos[j] -= 1;
306                }
307            }
308        }
309        vals.reverse();
310
311        score_director.before_variable_changed(self.descriptor_index, self.entity_index);
312        for (&idx, val) in self.element_indices.iter().zip(vals) {
313            (self.list_insert)(
314                score_director.working_solution_mut(),
315                self.entity_index,
316                idx,
317                val,
318            );
319        }
320        score_director.after_variable_changed(self.descriptor_index, self.entity_index);
321    }
322
323    fn descriptor_index(&self) -> usize {
324        self.descriptor_index
325    }
326
327    fn entity_indices(&self) -> &[usize] {
328        std::slice::from_ref(&self.entity_index)
329    }
330
331    fn variable_name(&self) -> &str {
332        self.variable_name
333    }
334
335    fn tabu_signature<D: Director<S>>(&self, score_director: &D) -> MoveTabuSignature {
336        let mut value_ids: SmallVec<[u64; 2]> = SmallVec::new();
337        for &idx in &self.element_indices {
338            let value = (self.list_get)(score_director.working_solution(), self.entity_index, idx);
339            value_ids.push(encode_option_debug(value.as_ref()));
340        }
341        let entity_id = encode_usize(self.entity_index);
342        let variable_id = hash_str(self.variable_name);
343        let scope = MoveTabuScope::new(self.descriptor_index, self.variable_name);
344        let destination_value_tokens: SmallVec<[ScopedValueTabuToken; 2]> = value_ids
345            .iter()
346            .copied()
347            .map(|value_id| scope.value_token(value_id))
348            .collect();
349        let mut move_id = smallvec![
350            encode_usize(self.descriptor_index),
351            variable_id,
352            entity_id,
353            encode_usize(self.element_indices.len())
354        ];
355        move_id.extend(self.element_indices.iter().map(|&idx| encode_usize(idx)));
356        move_id.extend(value_ids.iter().copied());
357
358        MoveTabuSignature::new(scope, move_id.clone(), move_id)
359            .with_entity_tokens([scope.entity_token(entity_id)])
360            .with_destination_value_tokens(destination_value_tokens)
361    }
362}