Skip to main content

solverforge_solver/heuristic/selector/
list_ruin.rs

1/* List ruin move selector for Large Neighborhood Search on list variables.
2
3Generates `ListRuinMove` instances that remove elements from list variables,
4enabling exploration of distant regions in the solution space.
5
6# Zero-Erasure Design
7
8Uses `fn` pointers for list operations. No `Arc<dyn Fn>`, no trait objects
9in hot paths.
10
11# Example
12
13```
14use solverforge_solver::heuristic::selector::{MoveSelector, ListRuinMoveSelector};
15use solverforge_solver::heuristic::r#move::ListRuinMove;
16use solverforge_core::domain::PlanningSolution;
17use solverforge_core::score::SoftScore;
18use solverforge_scoring::{Director, ScoreDirector};
19use solverforge_core::domain::SolutionDescriptor;
20use std::any::TypeId;
21
22#[derive(Clone, Debug)]
23struct Route { stops: Vec<i32> }
24
25#[derive(Clone, Debug)]
26struct VrpSolution { routes: Vec<Route>, score: Option<SoftScore> }
27
28impl PlanningSolution for VrpSolution {
29type Score = SoftScore;
30fn score(&self) -> Option<Self::Score> { self.score }
31fn set_score(&mut self, score: Option<Self::Score>) { self.score = score; }
32}
33
34fn entity_count(s: &VrpSolution) -> usize { s.routes.len() }
35fn list_len(s: &VrpSolution, idx: usize) -> usize {
36s.routes.get(idx).map_or(0, |r| r.stops.len())
37}
38fn list_remove(s: &mut VrpSolution, entity_idx: usize, idx: usize) -> i32 {
39s.routes.get_mut(entity_idx).map(|r| r.stops.remove(idx)).unwrap_or(0)
40}
41fn list_insert(s: &mut VrpSolution, entity_idx: usize, idx: usize, v: i32) {
42if let Some(r) = s.routes.get_mut(entity_idx) { r.stops.insert(idx, v); }
43}
44
45// Create selector that removes 2-3 elements at a time
46let selector = ListRuinMoveSelector::<VrpSolution, i32>::new(
472, 3,
48entity_count,
49list_len, list_remove, list_insert,
50"stops", 0,
51);
52
53// Use with a score director
54let solution = VrpSolution {
55routes: vec![Route { stops: vec![1, 2, 3, 4, 5] }],
56score: None,
57};
58let descriptor = SolutionDescriptor::new("VrpSolution", TypeId::of::<VrpSolution>());
59let director = ScoreDirector::simple(
60solution, descriptor, |s, _| s.routes.len()
61);
62
63let moves: Vec<_> = selector.iter_moves(&director).collect();
64assert!(!moves.is_empty());
65```
66*/
67
68use std::cell::RefCell;
69use std::fmt::Debug;
70use std::marker::PhantomData;
71
72use rand::rngs::SmallRng;
73use rand::{RngExt, SeedableRng};
74use smallvec::SmallVec;
75use solverforge_core::domain::PlanningSolution;
76use solverforge_scoring::Director;
77
78use crate::heuristic::r#move::ListRuinMove;
79
80use super::move_selector::{ArenaMoveCursor, MoveSelector};
81
82/// A move selector that generates `ListRuinMove` instances for Large Neighborhood Search.
83///
84/// Selects random subsets of elements from list variables to "ruin" (remove),
85/// enabling a construction heuristic to reinsert them in better positions.
86///
87/// # Type Parameters
88/// * `S` - The planning solution type
89/// * `V` - The list element value type
90///
91/// # Zero-Erasure
92///
93/// All list access uses `fn` pointers:
94/// - `list_len: fn(&S, usize) -> usize` - gets list length
95/// - `list_remove: fn(&mut S, usize, usize) -> V` - removes element
96/// - `list_insert: fn(&mut S, usize, usize, V)` - inserts element
97/// - `entity_count: fn(&S) -> usize` - counts entities
98pub struct ListRuinMoveSelector<S, V> {
99    // Minimum elements to remove per move.
100    min_ruin_count: usize,
101    // Maximum elements to remove per move.
102    max_ruin_count: usize,
103    // RNG state for reproducible subset selection.
104    rng: RefCell<SmallRng>,
105    // Function to get entity count from solution.
106    entity_count: fn(&S) -> usize,
107    // Function to get list length for an entity.
108    list_len: fn(&S, usize) -> usize,
109    // Function to read a list element by position for move metadata.
110    list_get: fn(&S, usize, usize) -> Option<V>,
111    // Function to remove element at index, returning it.
112    list_remove: fn(&mut S, usize, usize) -> V,
113    // Function to insert element at index.
114    list_insert: fn(&mut S, usize, usize, V),
115    // Variable name.
116    variable_name: &'static str,
117    // Entity descriptor index.
118    descriptor_index: usize,
119    // Number of ruin moves to generate per iteration.
120    moves_per_step: usize,
121    _phantom: PhantomData<fn() -> V>,
122}
123
124// SAFETY: RefCell<SmallRng> is only accessed while pre-generating a move batch
125// inside `iter_moves`, and selectors are consumed from a single thread at a time.
126unsafe impl<S, V> Send for ListRuinMoveSelector<S, V> {}
127
128impl<S, V: Debug> Debug for ListRuinMoveSelector<S, V> {
129    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
130        f.debug_struct("ListRuinMoveSelector")
131            .field("min_ruin_count", &self.min_ruin_count)
132            .field("max_ruin_count", &self.max_ruin_count)
133            .field("moves_per_step", &self.moves_per_step)
134            .field("variable_name", &self.variable_name)
135            .field("descriptor_index", &self.descriptor_index)
136            .finish()
137    }
138}
139
140impl<S, V> ListRuinMoveSelector<S, V> {
141    /* Creates a new list ruin move selector with concrete function pointers.
142
143    # Arguments
144    * `min_ruin_count` - Minimum elements to remove (at least 1)
145    * `max_ruin_count` - Maximum elements to remove
146    * `entity_count` - Function to get total entity count
147    * `list_len` - Function to get list length for an entity
148    * `list_remove` - Function to remove element at index
149    * `list_insert` - Function to insert element at index
150    * `variable_name` - Name of the list variable
151    * `descriptor_index` - Entity descriptor index
152
153    # Panics
154    Panics if `min_ruin_count` is 0 or `max_ruin_count < min_ruin_count`.
155    */
156    #[allow(clippy::too_many_arguments)]
157    pub fn new(
158        min_ruin_count: usize,
159        max_ruin_count: usize,
160        entity_count: fn(&S) -> usize,
161        list_len: fn(&S, usize) -> usize,
162        list_get: fn(&S, usize, usize) -> Option<V>,
163        list_remove: fn(&mut S, usize, usize) -> V,
164        list_insert: fn(&mut S, usize, usize, V),
165        variable_name: &'static str,
166        descriptor_index: usize,
167    ) -> Self {
168        assert!(min_ruin_count >= 1, "min_ruin_count must be at least 1");
169        assert!(
170            max_ruin_count >= min_ruin_count,
171            "max_ruin_count must be >= min_ruin_count"
172        );
173
174        Self {
175            min_ruin_count,
176            max_ruin_count,
177            rng: RefCell::new(SmallRng::from_rng(&mut rand::rng())),
178            entity_count,
179            list_len,
180            list_get,
181            list_remove,
182            list_insert,
183            variable_name,
184            descriptor_index,
185            moves_per_step: 10,
186            _phantom: PhantomData,
187        }
188    }
189
190    /// Sets the number of ruin moves to generate per iteration.
191    ///
192    /// Default is 10.
193    pub fn with_moves_per_step(mut self, count: usize) -> Self {
194        self.moves_per_step = count;
195        self
196    }
197
198    pub fn with_seed(mut self, seed: u64) -> Self {
199        self.rng = RefCell::new(SmallRng::seed_from_u64(seed));
200        self
201    }
202}
203
204impl<S, V> MoveSelector<S, ListRuinMove<S, V>> for ListRuinMoveSelector<S, V>
205where
206    S: PlanningSolution,
207    V: Clone + Send + Sync + Debug + 'static,
208{
209    type Cursor<'a>
210        = ArenaMoveCursor<S, ListRuinMove<S, V>>
211    where
212        Self: 'a;
213
214    fn open_cursor<'a, D: Director<S>>(&'a self, score_director: &D) -> Self::Cursor<'a> {
215        let solution = score_director.working_solution();
216        let total_entities = (self.entity_count)(solution);
217        let list_len = self.list_len;
218        let list_get = self.list_get;
219        let list_remove = self.list_remove;
220        let list_insert = self.list_insert;
221        let variable_name = self.variable_name;
222        let descriptor_index = self.descriptor_index;
223        let min_ruin = self.min_ruin_count;
224        let max_ruin = self.max_ruin_count;
225        let moves_count = self.moves_per_step;
226
227        let non_empty_entities: Vec<(usize, usize)> = (0..total_entities)
228            .filter_map(|entity_idx| {
229                let len = list_len(solution, entity_idx);
230                (len > 0).then_some((entity_idx, len))
231            })
232            .collect();
233
234        // Pre-generate moves using RNG (empty if no entities)
235        let mut rng = self.rng.borrow_mut();
236        let moves: Vec<ListRuinMove<S, V>> = if non_empty_entities.is_empty() {
237            Vec::new()
238        } else {
239            (0..moves_count)
240                .filter_map(|_| {
241                    // Pick a random entity
242                    let (entity_idx, list_length) =
243                        non_empty_entities[rng.random_range(0..non_empty_entities.len())];
244
245                    if list_length == 0 {
246                        return None;
247                    }
248
249                    // Clamp ruin count to available elements
250                    let min = min_ruin.min(list_length);
251                    let max = max_ruin.min(list_length);
252                    let ruin_count = if min == max {
253                        min
254                    } else {
255                        rng.random_range(min..=max)
256                    };
257
258                    // Fisher-Yates partial shuffle to select random indices
259                    let mut indices: SmallVec<[usize; 8]> = (0..list_length).collect();
260                    for i in 0..ruin_count {
261                        let j = rng.random_range(i..list_length);
262                        indices.swap(i, j);
263                    }
264                    indices.truncate(ruin_count);
265
266                    Some(ListRuinMove::new(
267                        entity_idx,
268                        &indices,
269                        self.entity_count,
270                        list_len,
271                        list_get,
272                        list_remove,
273                        list_insert,
274                        variable_name,
275                        descriptor_index,
276                    ))
277                })
278                .collect()
279        };
280
281        ArenaMoveCursor::from_moves(moves)
282    }
283
284    fn size<D: Director<S>>(&self, score_director: &D) -> usize {
285        let total = (self.entity_count)(score_director.working_solution());
286        if total == 0 {
287            return 0;
288        }
289        self.moves_per_step
290    }
291
292    fn is_never_ending(&self) -> bool {
293        false
294    }
295}