solverforge_solver/heuristic/selector/
list_ruin.rs

1//! List ruin move selector for Large Neighborhood Search on list variables.
2//!
3//! Generates `ListRuinMove` instances that remove elements from list variables,
4//! enabling exploration of distant regions in the solution space.
5//!
6//! # Zero-Erasure Design
7//!
8//! Uses `fn` pointers for list operations. No `Arc<dyn Fn>`, no trait objects
9//! in hot paths.
10//!
11//! # Example
12//!
13//! ```
14//! use solverforge_solver::heuristic::selector::{MoveSelector, ListRuinMoveSelector};
15//! use solverforge_solver::heuristic::r#move::ListRuinMove;
16//! use solverforge_core::domain::PlanningSolution;
17//! use solverforge_core::score::SimpleScore;
18//! use solverforge_scoring::{ScoreDirector, SimpleScoreDirector};
19//! use solverforge_core::domain::SolutionDescriptor;
20//! use std::any::TypeId;
21//!
22//! #[derive(Clone, Debug)]
23//! struct Route { stops: Vec<i32> }
24//!
25//! #[derive(Clone, Debug)]
26//! struct VrpSolution { routes: Vec<Route>, score: Option<SimpleScore> }
27//!
28//! impl PlanningSolution for VrpSolution {
29//!     type Score = SimpleScore;
30//!     fn score(&self) -> Option<Self::Score> { self.score }
31//!     fn set_score(&mut self, score: Option<Self::Score>) { self.score = score; }
32//! }
33//!
34//! fn entity_count(s: &VrpSolution) -> usize { s.routes.len() }
35//! fn list_len(s: &VrpSolution, idx: usize) -> usize {
36//!     s.routes.get(idx).map_or(0, |r| r.stops.len())
37//! }
38//! fn list_remove(s: &mut VrpSolution, entity_idx: usize, idx: usize) -> i32 {
39//!     s.routes.get_mut(entity_idx).map(|r| r.stops.remove(idx)).unwrap_or(0)
40//! }
41//! fn list_insert(s: &mut VrpSolution, entity_idx: usize, idx: usize, v: i32) {
42//!     if 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
46//! let selector = ListRuinMoveSelector::<VrpSolution, i32>::new(
47//!     2, 3,
48//!     entity_count,
49//!     list_len, list_remove, list_insert,
50//!     "stops", 0,
51//! );
52//!
53//! // Use with a score director
54//! let solution = VrpSolution {
55//!     routes: vec![Route { stops: vec![1, 2, 3, 4, 5] }],
56//!     score: None,
57//! };
58//! let descriptor = SolutionDescriptor::new("VrpSolution", TypeId::of::<VrpSolution>());
59//! let director = SimpleScoreDirector::with_calculator(
60//!     solution, descriptor, |_| SimpleScore::of(0)
61//! );
62//!
63//! let moves: Vec<_> = selector.iter_moves(&director).collect();
64//! assert!(!moves.is_empty());
65//! ```
66
67use std::fmt::Debug;
68use std::marker::PhantomData;
69
70use rand::rngs::StdRng;
71use rand::{Rng, SeedableRng};
72use smallvec::SmallVec;
73use solverforge_core::domain::PlanningSolution;
74use solverforge_scoring::ScoreDirector;
75
76use crate::heuristic::r#move::ListRuinMove;
77
78use super::MoveSelector;
79
80/// A move selector that generates `ListRuinMove` instances for Large Neighborhood Search.
81///
82/// Selects random subsets of elements from list variables to "ruin" (remove),
83/// enabling a construction heuristic to reinsert them in better positions.
84///
85/// # Type Parameters
86/// * `S` - The planning solution type
87/// * `V` - The list element value type
88///
89/// # Zero-Erasure
90///
91/// All list access uses `fn` pointers:
92/// - `list_len: fn(&S, usize) -> usize` - gets list length
93/// - `list_remove: fn(&mut S, usize, usize) -> V` - removes element
94/// - `list_insert: fn(&mut S, usize, usize, V)` - inserts element
95/// - `entity_count: fn(&S) -> usize` - counts entities
96pub struct ListRuinMoveSelector<S, V> {
97    /// Minimum elements to remove per move.
98    min_ruin_count: usize,
99    /// Maximum elements to remove per move.
100    max_ruin_count: usize,
101    /// Random seed for reproducible subset selection.
102    seed: Option<u64>,
103    /// Function to get entity count from solution.
104    entity_count: fn(&S) -> usize,
105    /// Function to get list length for an entity.
106    list_len: fn(&S, usize) -> usize,
107    /// Function to remove element at index, returning it.
108    list_remove: fn(&mut S, usize, usize) -> V,
109    /// Function to insert element at index.
110    list_insert: fn(&mut S, usize, usize, V),
111    /// Variable name.
112    variable_name: &'static str,
113    /// Entity descriptor index.
114    descriptor_index: usize,
115    /// Number of ruin moves to generate per iteration.
116    moves_per_step: usize,
117    _phantom: PhantomData<V>,
118}
119
120impl<S, V: Debug> Debug for ListRuinMoveSelector<S, V> {
121    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
122        f.debug_struct("ListRuinMoveSelector")
123            .field("min_ruin_count", &self.min_ruin_count)
124            .field("max_ruin_count", &self.max_ruin_count)
125            .field("moves_per_step", &self.moves_per_step)
126            .field("variable_name", &self.variable_name)
127            .field("descriptor_index", &self.descriptor_index)
128            .finish()
129    }
130}
131
132impl<S, V> ListRuinMoveSelector<S, V> {
133    /// Creates a new list ruin move selector with typed function pointers.
134    ///
135    /// # Arguments
136    /// * `min_ruin_count` - Minimum elements to remove (at least 1)
137    /// * `max_ruin_count` - Maximum elements to remove
138    /// * `entity_count` - Function to get total entity count
139    /// * `list_len` - Function to get list length for an entity
140    /// * `list_remove` - Function to remove element at index
141    /// * `list_insert` - Function to insert element at index
142    /// * `variable_name` - Name of the list variable
143    /// * `descriptor_index` - Entity descriptor index
144    ///
145    /// # Panics
146    /// Panics if `min_ruin_count` is 0 or `max_ruin_count < min_ruin_count`.
147    #[allow(clippy::too_many_arguments)]
148    pub fn new(
149        min_ruin_count: usize,
150        max_ruin_count: usize,
151        entity_count: fn(&S) -> usize,
152        list_len: fn(&S, usize) -> usize,
153        list_remove: fn(&mut S, usize, usize) -> V,
154        list_insert: fn(&mut S, usize, usize, V),
155        variable_name: &'static str,
156        descriptor_index: usize,
157    ) -> Self {
158        assert!(min_ruin_count >= 1, "min_ruin_count must be at least 1");
159        assert!(
160            max_ruin_count >= min_ruin_count,
161            "max_ruin_count must be >= min_ruin_count"
162        );
163
164        Self {
165            min_ruin_count,
166            max_ruin_count,
167            seed: None,
168            entity_count,
169            list_len,
170            list_remove,
171            list_insert,
172            variable_name,
173            descriptor_index,
174            moves_per_step: 10,
175            _phantom: PhantomData,
176        }
177    }
178
179    /// Sets the number of ruin moves to generate per iteration.
180    ///
181    /// Default is 10.
182    pub fn with_moves_per_step(mut self, count: usize) -> Self {
183        self.moves_per_step = count;
184        self
185    }
186
187    /// Sets the random seed for reproducible subset selection.
188    pub fn with_seed(mut self, seed: u64) -> Self {
189        self.seed = Some(seed);
190        self
191    }
192
193    /// Creates a random number generator.
194    fn create_rng(&self) -> StdRng {
195        match self.seed {
196            Some(seed) => StdRng::seed_from_u64(seed),
197            None => StdRng::from_os_rng(),
198        }
199    }
200}
201
202impl<S, V> MoveSelector<S, ListRuinMove<S, V>> for ListRuinMoveSelector<S, V>
203where
204    S: PlanningSolution,
205    V: Clone + Send + Sync + Debug + 'static,
206{
207    fn iter_moves<'a, D: ScoreDirector<S>>(
208        &'a self,
209        score_director: &'a D,
210    ) -> Box<dyn Iterator<Item = ListRuinMove<S, V>> + 'a> {
211        let solution = score_director.working_solution();
212        let total_entities = (self.entity_count)(solution);
213        let list_len = self.list_len;
214        let list_remove = self.list_remove;
215        let list_insert = self.list_insert;
216        let variable_name = self.variable_name;
217        let descriptor_index = self.descriptor_index;
218        let min_ruin = self.min_ruin_count;
219        let max_ruin = self.max_ruin_count;
220        let moves_count = self.moves_per_step;
221
222        if total_entities == 0 {
223            return Box::new(std::iter::empty());
224        }
225
226        // Pre-generate moves using RNG
227        let mut rng = self.create_rng();
228        let moves: Vec<ListRuinMove<S, V>> = (0..moves_count)
229            .filter_map(|_| {
230                // Pick a random entity
231                let entity_idx = rng.random_range(0..total_entities);
232                let list_length = list_len(solution, entity_idx);
233
234                if list_length == 0 {
235                    return None;
236                }
237
238                // Clamp ruin count to available elements
239                let min = min_ruin.min(list_length);
240                let max = max_ruin.min(list_length);
241                let ruin_count = if min == max {
242                    min
243                } else {
244                    rng.random_range(min..=max)
245                };
246
247                // Fisher-Yates partial shuffle to select random indices
248                let mut indices: SmallVec<[usize; 8]> = (0..list_length).collect();
249                for i in 0..ruin_count {
250                    let j = rng.random_range(i..list_length);
251                    indices.swap(i, j);
252                }
253                indices.truncate(ruin_count);
254
255                Some(ListRuinMove::new(
256                    entity_idx,
257                    &indices,
258                    list_len,
259                    list_remove,
260                    list_insert,
261                    variable_name,
262                    descriptor_index,
263                ))
264            })
265            .collect();
266
267        Box::new(moves.into_iter())
268    }
269
270    fn size<D: ScoreDirector<S>>(&self, score_director: &D) -> usize {
271        let total = (self.entity_count)(score_director.working_solution());
272        if total == 0 {
273            return 0;
274        }
275        self.moves_per_step
276    }
277
278    fn is_never_ending(&self) -> bool {
279        false
280    }
281}
282
283#[cfg(test)]
284mod tests {
285    use super::*;
286    use solverforge_core::domain::SolutionDescriptor;
287    use solverforge_core::score::SimpleScore;
288    use solverforge_scoring::SimpleScoreDirector;
289    use std::any::TypeId;
290
291    #[derive(Clone, Debug)]
292    struct Route {
293        stops: Vec<i32>,
294    }
295
296    #[derive(Clone, Debug)]
297    struct VrpSolution {
298        routes: Vec<Route>,
299        score: Option<SimpleScore>,
300    }
301
302    impl PlanningSolution for VrpSolution {
303        type Score = SimpleScore;
304        fn score(&self) -> Option<Self::Score> {
305            self.score
306        }
307        fn set_score(&mut self, score: Option<Self::Score>) {
308            self.score = score;
309        }
310    }
311
312    fn entity_count(s: &VrpSolution) -> usize {
313        s.routes.len()
314    }
315    fn list_len(s: &VrpSolution, entity_idx: usize) -> usize {
316        s.routes.get(entity_idx).map_or(0, |r| r.stops.len())
317    }
318    fn list_remove(s: &mut VrpSolution, entity_idx: usize, idx: usize) -> i32 {
319        s.routes
320            .get_mut(entity_idx)
321            .map(|r| r.stops.remove(idx))
322            .unwrap_or(0)
323    }
324    fn list_insert(s: &mut VrpSolution, entity_idx: usize, idx: usize, v: i32) {
325        if let Some(r) = s.routes.get_mut(entity_idx) {
326            r.stops.insert(idx, v);
327        }
328    }
329
330    fn create_director(
331        routes: Vec<Vec<i32>>,
332    ) -> SimpleScoreDirector<VrpSolution, impl Fn(&VrpSolution) -> SimpleScore> {
333        let routes = routes.into_iter().map(|stops| Route { stops }).collect();
334        let solution = VrpSolution {
335            routes,
336            score: None,
337        };
338        let descriptor = SolutionDescriptor::new("VrpSolution", TypeId::of::<VrpSolution>());
339        SimpleScoreDirector::with_calculator(solution, descriptor, |_| SimpleScore::of(0))
340    }
341
342    #[test]
343    fn generates_list_ruin_moves() {
344        let director = create_director(vec![vec![1, 2, 3, 4, 5]]);
345
346        let selector = ListRuinMoveSelector::<VrpSolution, i32>::new(
347            2,
348            3,
349            entity_count,
350            list_len,
351            list_remove,
352            list_insert,
353            "stops",
354            0,
355        )
356        .with_moves_per_step(5);
357
358        let moves: Vec<_> = selector.iter_moves(&director).collect();
359
360        assert_eq!(moves.len(), 5);
361        for m in &moves {
362            let count = m.ruin_count();
363            assert!((2..=3).contains(&count));
364        }
365    }
366
367    #[test]
368    fn clamps_to_available_elements() {
369        let director = create_director(vec![vec![1, 2]]);
370
371        // Request more elements than available
372        let selector = ListRuinMoveSelector::<VrpSolution, i32>::new(
373            5,
374            10,
375            entity_count,
376            list_len,
377            list_remove,
378            list_insert,
379            "stops",
380            0,
381        )
382        .with_moves_per_step(3);
383
384        let moves: Vec<_> = selector.iter_moves(&director).collect();
385
386        assert_eq!(moves.len(), 3);
387        for m in &moves {
388            assert!(m.ruin_count() <= 2);
389        }
390    }
391
392    #[test]
393    fn empty_solution_yields_no_moves() {
394        let director = create_director(vec![]);
395
396        let selector = ListRuinMoveSelector::<VrpSolution, i32>::new(
397            1,
398            2,
399            entity_count,
400            list_len,
401            list_remove,
402            list_insert,
403            "stops",
404            0,
405        );
406
407        let moves: Vec<_> = selector.iter_moves(&director).collect();
408        assert!(moves.is_empty());
409    }
410
411    #[test]
412    fn empty_list_yields_no_moves_for_that_entity() {
413        let director = create_director(vec![vec![], vec![1, 2, 3]]);
414
415        let selector = ListRuinMoveSelector::<VrpSolution, i32>::new(
416            1,
417            2,
418            entity_count,
419            list_len,
420            list_remove,
421            list_insert,
422            "stops",
423            0,
424        )
425        .with_moves_per_step(10)
426        .with_seed(42);
427
428        let moves: Vec<_> = selector.iter_moves(&director).collect();
429
430        // Some moves may be None due to empty list selection
431        // All returned moves should be valid
432        for m in &moves {
433            assert!(m.ruin_count() >= 1);
434        }
435    }
436
437    #[test]
438    fn size_returns_moves_per_step() {
439        let director = create_director(vec![vec![1, 2, 3]]);
440
441        let selector = ListRuinMoveSelector::<VrpSolution, i32>::new(
442            1,
443            2,
444            entity_count,
445            list_len,
446            list_remove,
447            list_insert,
448            "stops",
449            0,
450        )
451        .with_moves_per_step(7);
452
453        assert_eq!(selector.size(&director), 7);
454    }
455
456    #[test]
457    #[should_panic(expected = "min_ruin_count must be at least 1")]
458    fn panics_on_zero_min() {
459        let _selector = ListRuinMoveSelector::<VrpSolution, i32>::new(
460            0,
461            2,
462            entity_count,
463            list_len,
464            list_remove,
465            list_insert,
466            "stops",
467            0,
468        );
469    }
470
471    #[test]
472    #[should_panic(expected = "max_ruin_count must be >= min_ruin_count")]
473    fn panics_on_invalid_range() {
474        let _selector = ListRuinMoveSelector::<VrpSolution, i32>::new(
475            5,
476            2,
477            entity_count,
478            list_len,
479            list_remove,
480            list_insert,
481            "stops",
482            0,
483        );
484    }
485}