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 crate::heuristic::selector::precedence_route::{
21    node_index, PrecedenceRouteGraph, PrecedenceRouteHooks,
22};
23
24use super::metadata::{
25    encode_option_debug, encode_usize, hash_str, MoveTabuScope, ScopedValueTabuToken,
26};
27use super::{Move, MoveTabuSignature};
28
29/// A ruin-and-recreate move for Large Neighborhood Search on list variables.
30///
31/// Removes selected elements from a source entity, then reinserts each one
32/// greedily into the best position across all entities (including the source).
33/// The move is self-contained: accepting it leaves the solution valid.
34///
35/// # Type Parameters
36/// * `S` - The planning solution type
37/// * `V` - The list element value type
38///
39/// # Example
40///
41/// ```
42/// use solverforge_solver::heuristic::r#move::ListRuinMove;
43/// use solverforge_core::domain::PlanningSolution;
44/// use solverforge_core::score::SoftScore;
45///
46/// #[derive(Clone, Debug)]
47/// struct Route { stops: Vec<i32>, score: Option<SoftScore> }
48///
49/// impl PlanningSolution for Route {
50///     type Score = SoftScore;
51///     fn score(&self) -> Option<Self::Score> { self.score }
52///     fn set_score(&mut self, score: Option<Self::Score>) { self.score = score; }
53/// }
54///
55/// fn entity_count(s: &Route) -> usize { 1 }
56/// fn list_len(s: &Route, _: usize) -> usize { s.stops.len() }
57/// fn list_get(s: &Route, _: usize, pos: usize) -> Option<i32> { s.stops.get(pos).copied() }
58/// fn list_remove(s: &mut Route, _: usize, idx: usize) -> i32 { s.stops.remove(idx) }
59/// fn list_insert(s: &mut Route, _: usize, idx: usize, v: i32) { s.stops.insert(idx, v); }
60///
61/// // Ruin elements at indices 1 and 3, then recreate greedily
62/// let m = ListRuinMove::<Route, i32>::new(
63///     0,
64///     &[1, 3],
65///     entity_count,
66///     list_len, list_get, list_remove, list_insert,
67///     "stops", 0,
68/// );
69/// ```
70pub struct ListRuinMove<S, V> {
71    entity_index: usize,
72    element_indices: SmallVec<[usize; 8]>,
73    sources: SmallVec<[(usize, SmallVec<[usize; 8]>); 4]>,
74    entity_indices: SmallVec<[usize; 8]>,
75    // Number of entities in solution (for recreate phase)
76    entity_count: fn(&S) -> usize,
77    list_len: fn(&S, usize) -> usize,
78    list_get: fn(&S, usize, usize) -> Option<V>,
79    // Remove element at index, returning it
80    list_remove: fn(&mut S, usize, usize) -> V,
81    // Insert element at index
82    list_insert: fn(&mut S, usize, usize, V),
83    element_owner_fn: Option<fn(&S, &V) -> Option<usize>>,
84    precedence_element_count: Option<fn(&S) -> usize>,
85    precedence_index_to_element: Option<fn(&S, usize) -> V>,
86    precedence_successors_fn: Option<fn(&S, V, &mut Vec<V>)>,
87    skip_empty_destinations: bool,
88    variable_name: &'static str,
89    descriptor_index: usize,
90    _phantom: PhantomData<fn() -> V>,
91}
92
93impl<S, V> Clone for ListRuinMove<S, V> {
94    fn clone(&self) -> Self {
95        Self {
96            entity_index: self.entity_index,
97            element_indices: self.element_indices.clone(),
98            sources: self.sources.clone(),
99            entity_indices: self.entity_indices.clone(),
100            entity_count: self.entity_count,
101            list_len: self.list_len,
102            list_get: self.list_get,
103            list_remove: self.list_remove,
104            list_insert: self.list_insert,
105            element_owner_fn: self.element_owner_fn,
106            precedence_element_count: self.precedence_element_count,
107            precedence_index_to_element: self.precedence_index_to_element,
108            precedence_successors_fn: self.precedence_successors_fn,
109            skip_empty_destinations: self.skip_empty_destinations,
110            variable_name: self.variable_name,
111            descriptor_index: self.descriptor_index,
112            _phantom: PhantomData,
113        }
114    }
115}
116
117impl<S, V: Debug> Debug for ListRuinMove<S, V> {
118    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
119        f.debug_struct("ListRuinMove")
120            .field("sources", &self.sources.as_slice())
121            .field("variable_name", &self.variable_name)
122            .finish()
123    }
124}
125
126impl<S, V> ListRuinMove<S, V> {
127    /* Creates a new list ruin-and-recreate move.
128
129    # Arguments
130    * `entity_index` - Entity index to ruin from
131    * `element_indices` - Indices of elements to remove
132    * `entity_count` - Function returning total entity count
133    * `list_len` - Function to get list length for an entity
134    * `list_remove` - Function to remove element at index
135    * `list_insert` - Function to insert element at index
136    * `variable_name` - Name of the list variable
137    * `descriptor_index` - Entity descriptor index
138    */
139    #[allow(clippy::too_many_arguments)]
140    pub fn new(
141        entity_index: usize,
142        element_indices: &[usize],
143        entity_count: fn(&S) -> usize,
144        list_len: fn(&S, usize) -> usize,
145        list_get: fn(&S, usize, usize) -> Option<V>,
146        list_remove: fn(&mut S, usize, usize) -> V,
147        list_insert: fn(&mut S, usize, usize, V),
148        variable_name: &'static str,
149        descriptor_index: usize,
150    ) -> Self {
151        let mut indices: SmallVec<[usize; 8]> = SmallVec::from_slice(element_indices);
152        indices.sort_unstable();
153        let sources = smallvec![(entity_index, indices.clone())];
154        let entity_indices = smallvec![entity_index];
155        Self {
156            entity_index,
157            element_indices: indices,
158            sources,
159            entity_indices,
160            entity_count,
161            list_len,
162            list_get,
163            list_remove,
164            list_insert,
165            element_owner_fn: None,
166            precedence_element_count: None,
167            precedence_index_to_element: None,
168            precedence_successors_fn: None,
169            skip_empty_destinations: false,
170            variable_name,
171            descriptor_index,
172            _phantom: PhantomData,
173        }
174    }
175
176    #[allow(clippy::too_many_arguments)]
177    pub(crate) fn new_multi_source(
178        sources: &[(usize, SmallVec<[usize; 8]>)],
179        entity_count: fn(&S) -> usize,
180        list_len: fn(&S, usize) -> usize,
181        list_get: fn(&S, usize, usize) -> Option<V>,
182        list_remove: fn(&mut S, usize, usize) -> V,
183        list_insert: fn(&mut S, usize, usize, V),
184        variable_name: &'static str,
185        descriptor_index: usize,
186    ) -> Self {
187        let mut merged: SmallVec<[(usize, SmallVec<[usize; 8]>); 4]> = SmallVec::new();
188        for (entity_index, indices) in sources {
189            let mut sorted = indices.clone();
190            sorted.sort_unstable();
191            sorted.dedup();
192            if sorted.is_empty() {
193                continue;
194            }
195            if let Some((_, existing)) = merged
196                .iter_mut()
197                .find(|(candidate, _)| candidate == entity_index)
198            {
199                existing.extend(sorted);
200                existing.sort_unstable();
201                existing.dedup();
202            } else {
203                merged.push((*entity_index, sorted));
204            }
205        }
206        merged.sort_by_key(|(entity_index, _)| *entity_index);
207        let (entity_index, element_indices) = merged
208            .first()
209            .cloned()
210            .unwrap_or_else(|| (0, SmallVec::new()));
211        let entity_indices = merged
212            .iter()
213            .map(|(entity_index, _)| *entity_index)
214            .collect();
215        Self {
216            entity_index,
217            element_indices,
218            sources: merged,
219            entity_indices,
220            entity_count,
221            list_len,
222            list_get,
223            list_remove,
224            list_insert,
225            element_owner_fn: None,
226            precedence_element_count: None,
227            precedence_index_to_element: None,
228            precedence_successors_fn: None,
229            skip_empty_destinations: false,
230            variable_name,
231            descriptor_index,
232            _phantom: PhantomData,
233        }
234    }
235
236    pub fn with_element_owner_fn(
237        mut self,
238        element_owner_fn: Option<fn(&S, &V) -> Option<usize>>,
239    ) -> Self {
240        self.element_owner_fn = element_owner_fn;
241        self
242    }
243
244    pub(crate) fn with_precedence_hooks(
245        mut self,
246        element_count: Option<fn(&S) -> usize>,
247        index_to_element: Option<fn(&S, usize) -> V>,
248        successors_fn: Option<fn(&S, V, &mut Vec<V>)>,
249    ) -> Self {
250        self.precedence_element_count = element_count;
251        self.precedence_index_to_element = index_to_element;
252        self.precedence_successors_fn = successors_fn;
253        self
254    }
255
256    pub fn with_skip_empty_destinations(mut self, skip_empty_destinations: bool) -> Self {
257        self.skip_empty_destinations = skip_empty_destinations;
258        self
259    }
260
261    pub fn entity_index(&self) -> usize {
262        self.entity_index
263    }
264
265    pub fn element_indices(&self) -> &[usize] {
266        &self.element_indices
267    }
268
269    pub fn ruin_count(&self) -> usize {
270        self.sources.iter().map(|(_, indices)| indices.len()).sum()
271    }
272}
273
274#[cfg(test)]
275pub(crate) fn final_positions_after_insertions(
276    placements: &SmallVec<[(usize, usize); 8]>,
277) -> SmallVec<[usize; 8]> {
278    let mut current_positions: SmallVec<[usize; 8]> = SmallVec::with_capacity(placements.len());
279
280    for i in 0..placements.len() {
281        let (entity_i, insert_pos_i) = placements[i];
282
283        for j in 0..i {
284            let (entity_j, _) = placements[j];
285            if entity_j == entity_i && current_positions[j] >= insert_pos_i {
286                current_positions[j] += 1;
287            }
288        }
289
290        current_positions.push(insert_pos_i);
291    }
292
293    current_positions
294}
295
296fn final_positions_after_ordered_insertions(
297    placements: &SmallVec<[(usize, usize, usize); 8]>,
298) -> SmallVec<[usize; 8]> {
299    let mut current_positions: SmallVec<[usize; 8]> = SmallVec::with_capacity(placements.len());
300
301    for i in 0..placements.len() {
302        let (entity_i, insert_pos_i, _) = placements[i];
303
304        for j in 0..i {
305            let (entity_j, _, _) = placements[j];
306            if entity_j == entity_i && current_positions[j] >= insert_pos_i {
307                current_positions[j] += 1;
308            }
309        }
310
311        current_positions.push(insert_pos_i);
312    }
313
314    current_positions
315}
316
317impl<S, V> Move<S> for ListRuinMove<S, V>
318where
319    S: PlanningSolution,
320    V: Clone + PartialEq + Send + Sync + Debug + 'static,
321{
322    type Undo = SmallVec<[(usize, usize, usize); 8]>;
323
324    fn is_doable<D: Director<S>>(&self, score_director: &D) -> bool {
325        if self.sources.is_empty() || self.sources.iter().all(|(_, indices)| indices.is_empty()) {
326            return false;
327        }
328        let solution = score_director.working_solution();
329
330        let Some(owner_fn) = self.element_owner_fn else {
331            return self.sources.iter().all(|(entity_index, indices)| {
332                let len = (self.list_len)(solution, *entity_index);
333                indices.iter().all(|&idx| idx < len)
334            });
335        };
336
337        let n_entities = (self.entity_count)(solution);
338        self.sources.iter().all(|(entity_index, indices)| {
339            let len = (self.list_len)(solution, *entity_index);
340            indices.iter().all(|&idx| {
341                if idx >= len {
342                    return false;
343                }
344                let Some(element) = (self.list_get)(solution, *entity_index, idx) else {
345                    return false;
346                };
347                crate::list_placement::candidate_entity_indices(
348                    Some(owner_fn),
349                    solution,
350                    n_entities,
351                    &element,
352                )
353                .next()
354                .is_some()
355            })
356        })
357    }
358
359    fn do_move<D: Director<S>>(&self, score_director: &mut D) -> Self::Undo {
360        let list_remove = self.list_remove;
361        let list_insert = self.list_insert;
362        let list_len = self.list_len;
363        let list_get = self.list_get;
364        let entity_count = self.entity_count;
365        let element_owner_fn = self.element_owner_fn;
366        let skip_empty_destinations = self.skip_empty_destinations;
367        let descriptor = self.descriptor_index;
368
369        // --- Ruin phase: remove selected elements from each source entity ---
370        let mut removed: SmallVec<[(usize, usize, V); 8]> = SmallVec::new();
371        for (src, indices) in &self.sources {
372            score_director.before_variable_changed(descriptor, *src);
373            let mut source_removed: SmallVec<[(usize, V); 8]> = SmallVec::new();
374            for &idx in indices.iter().rev() {
375                let value = list_remove(score_director.working_solution_mut(), *src, idx);
376                source_removed.push((idx, value));
377            }
378            source_removed.reverse();
379            removed.extend(
380                source_removed
381                    .into_iter()
382                    .map(|(idx, value)| (*src, idx, value)),
383            );
384            score_director.after_variable_changed(descriptor, *src);
385        }
386
387        // --- Recreate phase: greedily reinsert the best remaining element and position ---
388        // Track where each element ends up for the undo closure.
389        let mut placements: SmallVec<[(usize, usize, usize); 8]> = SmallVec::new();
390        let mut remaining: SmallVec<[(usize, usize, usize, V); 8]> = removed
391            .iter()
392            .cloned()
393            .enumerate()
394            .map(|(idx, (src, original_pos, value))| (idx, src, original_pos, value))
395            .collect();
396
397        let n_entities = entity_count(score_director.working_solution());
398        while !remaining.is_empty() {
399            let precedence_graph =
400                self.recreate_precedence_graph(score_director.working_solution());
401            let mut best_choice: Option<(usize, usize, usize, S::Score)> = None;
402
403            for (remaining_idx, (_, _, _, elem)) in remaining.iter().enumerate() {
404                let candidates = crate::list_placement::candidate_entity_indices(
405                    element_owner_fn,
406                    score_director.working_solution(),
407                    n_entities,
408                    elem,
409                );
410                for e in candidates {
411                    let len = list_len(score_director.working_solution(), e);
412                    if skip_empty_destinations && element_owner_fn.is_none() && len == 0 {
413                        continue;
414                    }
415                    for pos in 0..=len {
416                        if precedence_graph.as_ref().is_some_and(|(elements, graph)| {
417                            let Some(element_node) = node_index(elements, elem) else {
418                                return false;
419                            };
420                            let prev = (pos > 0)
421                                .then(|| list_get(score_director.working_solution(), e, pos - 1))
422                                .flatten();
423                            let next = (pos < len)
424                                .then(|| list_get(score_director.working_solution(), e, pos))
425                                .flatten();
426                            graph.insertion_introduces_cycle(
427                                prev.as_ref().and_then(|value| node_index(elements, value)),
428                                element_node,
429                                next.as_ref().and_then(|value| node_index(elements, value)),
430                            )
431                        }) {
432                            continue;
433                        }
434
435                        score_director.before_variable_changed(descriptor, e);
436                        list_insert(score_director.working_solution_mut(), e, pos, elem.clone());
437                        score_director.after_variable_changed(descriptor, e);
438
439                        let candidate_score = score_director.calculate_score();
440                        if best_choice
441                            .is_none_or(|(_, _, _, best_score)| candidate_score > best_score)
442                        {
443                            best_choice = Some((remaining_idx, e, pos, candidate_score));
444                        }
445
446                        score_director.before_variable_changed(descriptor, e);
447                        list_remove(score_director.working_solution_mut(), e, pos);
448                        score_director.after_variable_changed(descriptor, e);
449                    }
450                }
451            }
452
453            let Some((remaining_idx, best_entity, best_pos, _)) = best_choice else {
454                self.restore_removed_elements(score_director, &placements, &removed);
455                return SmallVec::new();
456            };
457            let (original_removed_idx, _, _, elem) = remaining.remove(remaining_idx);
458
459            // Apply the best insertion permanently
460            score_director.before_variable_changed(descriptor, best_entity);
461            list_insert(
462                score_director.working_solution_mut(),
463                best_entity,
464                best_pos,
465                elem.clone(),
466            );
467            score_director.after_variable_changed(descriptor, best_entity);
468
469            // Store the placement as recorded at insertion time (no adjustment needed;
470            // undo will compute actual current positions accounting for later insertions).
471            placements.push((best_entity, best_pos, original_removed_idx));
472        }
473
474        /* --- Register undo ---
475        placements[i] = (entity, pos, original_removed_idx) at the moment an element was inserted.
476        Later insertions j > i into the same entity at pos <= placements[i].pos
477        shifted element i rightward by 1 for each such j.
478        During undo we process in reverse: remove last-placed first.
479        At that point, only placements[j] with j > i (already removed) have been
480        undone, so the current position of element i is:
481        placements[i].pos + #{j > i : same entity AND placements[j].pos <= placements[i].pos}
482        which we compute on the fly as we iterate in reverse.
483
484        After collecting values, reinsert them by original source entity and
485        original index in ascending order.
486        */
487        placements
488    }
489
490    fn undo_move<D: Director<S>>(&self, score_director: &mut D, placements: Self::Undo) {
491        let n = placements.len();
492        let mut current_pos = final_positions_after_ordered_insertions(&placements);
493        let mut vals: SmallVec<[(usize, usize, V); 8]> = SmallVec::with_capacity(n);
494
495        for i in (0..n).rev() {
496            let (entity_index, _, original_removed_idx) = placements[i];
497            let actual_pos = current_pos[i];
498            score_director.before_variable_changed(self.descriptor_index, entity_index);
499            let val = (self.list_remove)(
500                score_director.working_solution_mut(),
501                entity_index,
502                actual_pos,
503            );
504            let (source_entity, original_pos) =
505                removed_source_entry(&self.sources, original_removed_idx)
506                    .expect("list ruin undo placement index must map to an original source entry");
507            vals.push((source_entity, original_pos, val));
508            score_director.after_variable_changed(self.descriptor_index, entity_index);
509
510            for j in 0..i {
511                let (other_entity, _, _) = placements[j];
512                if other_entity == entity_index && current_pos[j] > actual_pos {
513                    current_pos[j] -= 1;
514                }
515            }
516        }
517        vals.sort_by_key(|(entity_index, original_pos, _)| (*entity_index, *original_pos));
518
519        let mut current_entity = None;
520        for (entity_index, original_pos, val) in vals {
521            if current_entity != Some(entity_index) {
522                if let Some(previous_entity) = current_entity {
523                    score_director.after_variable_changed(self.descriptor_index, previous_entity);
524                }
525                score_director.before_variable_changed(self.descriptor_index, entity_index);
526                current_entity = Some(entity_index);
527            }
528            (self.list_insert)(
529                score_director.working_solution_mut(),
530                entity_index,
531                original_pos,
532                val,
533            );
534        }
535        if let Some(entity_index) = current_entity {
536            score_director.after_variable_changed(self.descriptor_index, entity_index);
537        }
538    }
539
540    fn descriptor_index(&self) -> usize {
541        self.descriptor_index
542    }
543
544    fn entity_indices(&self) -> &[usize] {
545        &self.entity_indices
546    }
547
548    fn variable_name(&self) -> &str {
549        self.variable_name
550    }
551
552    fn telemetry_label(&self) -> &'static str {
553        "list_ruin"
554    }
555
556    fn tabu_signature<D: Director<S>>(&self, score_director: &D) -> MoveTabuSignature {
557        let mut value_ids: SmallVec<[u64; 2]> = SmallVec::new();
558        for (entity_index, indices) in &self.sources {
559            for &idx in indices {
560                let value = (self.list_get)(score_director.working_solution(), *entity_index, idx);
561                value_ids.push(encode_option_debug(value.as_ref()));
562            }
563        }
564        let variable_id = hash_str(self.variable_name);
565        let scope = MoveTabuScope::new(self.descriptor_index, self.variable_name);
566        let destination_value_tokens: SmallVec<[ScopedValueTabuToken; 2]> = value_ids
567            .iter()
568            .copied()
569            .map(|value_id| scope.value_token(value_id))
570            .collect();
571        let mut move_id = smallvec![
572            encode_usize(self.descriptor_index),
573            variable_id,
574            encode_usize(self.sources.len()),
575            encode_usize(self.ruin_count())
576        ];
577        for (entity_index, indices) in &self.sources {
578            move_id.push(encode_usize(*entity_index));
579            move_id.push(encode_usize(indices.len()));
580            move_id.extend(indices.iter().map(|&idx| encode_usize(idx)));
581        }
582        move_id.extend(value_ids.iter().copied());
583
584        MoveTabuSignature::new(scope, move_id.clone(), move_id)
585            .with_entity_tokens(
586                self.entity_indices
587                    .iter()
588                    .copied()
589                    .map(encode_usize)
590                    .map(|entity_id| scope.entity_token(entity_id)),
591            )
592            .with_destination_value_tokens(destination_value_tokens)
593    }
594}
595
596fn removed_source_entry(
597    sources: &SmallVec<[(usize, SmallVec<[usize; 8]>); 4]>,
598    target_index: usize,
599) -> Option<(usize, usize)> {
600    let mut offset = 0usize;
601    for (entity_index, indices) in sources {
602        if target_index < offset + indices.len() {
603            return Some((*entity_index, indices[target_index - offset]));
604        }
605        offset += indices.len();
606    }
607    None
608}
609
610impl<S, V> ListRuinMove<S, V>
611where
612    S: PlanningSolution,
613    V: Clone + PartialEq,
614{
615    fn restore_removed_elements<D: Director<S>>(
616        &self,
617        score_director: &mut D,
618        placements: &SmallVec<[(usize, usize, usize); 8]>,
619        removed: &SmallVec<[(usize, usize, V); 8]>,
620    ) {
621        let mut current_pos = final_positions_after_ordered_insertions(placements);
622        for i in (0..placements.len()).rev() {
623            let (entity_index, _, _) = placements[i];
624            let actual_pos = current_pos[i];
625            score_director.before_variable_changed(self.descriptor_index, entity_index);
626            (self.list_remove)(
627                score_director.working_solution_mut(),
628                entity_index,
629                actual_pos,
630            );
631            score_director.after_variable_changed(self.descriptor_index, entity_index);
632
633            for j in 0..i {
634                let (other_entity, _, _) = placements[j];
635                if other_entity == entity_index && current_pos[j] > actual_pos {
636                    current_pos[j] -= 1;
637                }
638            }
639        }
640
641        let mut removed = removed.clone();
642        removed.sort_by_key(|(entity_index, original_pos, _)| (*entity_index, *original_pos));
643        let mut current_entity = None;
644        for (entity_index, original_pos, value) in removed {
645            if current_entity != Some(entity_index) {
646                if let Some(previous_entity) = current_entity {
647                    score_director.after_variable_changed(self.descriptor_index, previous_entity);
648                }
649                score_director.before_variable_changed(self.descriptor_index, entity_index);
650                current_entity = Some(entity_index);
651            }
652            (self.list_insert)(
653                score_director.working_solution_mut(),
654                entity_index,
655                original_pos,
656                value,
657            );
658        }
659        if let Some(entity_index) = current_entity {
660            score_director.after_variable_changed(self.descriptor_index, entity_index);
661        }
662    }
663
664    fn recreate_precedence_graph(&self, solution: &S) -> Option<(Vec<V>, PrecedenceRouteGraph)> {
665        let element_count = self.precedence_element_count?;
666        let index_to_element = self.precedence_index_to_element?;
667        let fixed_successors = self.precedence_successors_fn?;
668        let elements = (0..element_count(solution))
669            .map(|index| index_to_element(solution, index))
670            .collect::<Vec<_>>();
671        let hooks = PrecedenceRouteHooks::new(
672            element_count,
673            index_to_element,
674            fixed_successors,
675            self.entity_count,
676            self.list_len,
677            self.list_get,
678        );
679        let graph = hooks.build_graph_with_elements(solution, &elements);
680        Some((elements, graph))
681    }
682}