solverforge_solver/heuristic/selector/
ruin.rs

1//! Ruin move selector for Large Neighborhood Search.
2//!
3//! Generates `RuinMove` instances that unassign subsets of entities,
4//! enabling exploration of distant regions in the solution space.
5//!
6//! # Zero-Erasure Design
7//!
8//! Uses `fn` pointers for variable access and entity counting.
9//! No `Arc<dyn Fn>`, no trait objects in hot paths.
10//!
11//! # Example
12//!
13//! ```
14//! use solverforge_solver::heuristic::selector::{MoveSelector, RuinMoveSelector};
15//! use solverforge_solver::heuristic::r#move::RuinMove;
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 Task { assigned_to: Option<i32> }
24//!
25//! #[derive(Clone, Debug)]
26//! struct Schedule { tasks: Vec<Task>, score: Option<SimpleScore> }
27//!
28//! impl PlanningSolution for Schedule {
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: &Schedule) -> usize { s.tasks.len() }
35//! fn get_task(s: &Schedule, idx: usize) -> Option<i32> {
36//!     s.tasks.get(idx).and_then(|t| t.assigned_to)
37//! }
38//! fn set_task(s: &mut Schedule, idx: usize, v: Option<i32>) {
39//!     if let Some(t) = s.tasks.get_mut(idx) { t.assigned_to = v; }
40//! }
41//!
42//! // Create selector that ruins 2-3 entities at a time
43//! let selector = RuinMoveSelector::<Schedule, i32>::new(
44//!     2, 3,
45//!     entity_count,
46//!     get_task, set_task,
47//!     "assigned_to", 0,
48//! );
49//!
50//! // Use with a score director
51//! let solution = Schedule {
52//!     tasks: vec![
53//!         Task { assigned_to: Some(1) },
54//!         Task { assigned_to: Some(2) },
55//!         Task { assigned_to: Some(3) },
56//!     ],
57//!     score: None,
58//! };
59//! let descriptor = SolutionDescriptor::new("Schedule", TypeId::of::<Schedule>());
60//! let director = SimpleScoreDirector::with_calculator(
61//!     solution, descriptor, |_| SimpleScore::of(0)
62//! );
63//!
64//! let moves: Vec<_> = selector.iter_moves(&director).collect();
65//! assert!(!moves.is_empty());
66//! ```
67
68use std::fmt::Debug;
69use std::marker::PhantomData;
70
71use rand::rngs::StdRng;
72use rand::{Rng, SeedableRng};
73use smallvec::SmallVec;
74use solverforge_core::domain::PlanningSolution;
75use solverforge_scoring::ScoreDirector;
76
77use crate::heuristic::r#move::RuinMove;
78
79use super::MoveSelector;
80
81/// A move selector that generates `RuinMove` instances for Large Neighborhood Search.
82///
83/// Selects random subsets of entities to "ruin" (unassign), enabling a construction
84/// heuristic to reassign them in potentially better configurations.
85///
86/// # Type Parameters
87/// * `S` - The planning solution type
88/// * `V` - The variable value type
89///
90/// # Zero-Erasure
91///
92/// All variable access uses `fn` pointers:
93/// - `getter: fn(&S, usize) -> Option<V>` - gets current value
94/// - `setter: fn(&mut S, usize, Option<V>)` - sets value
95/// - `entity_count: fn(&S) -> usize` - counts entities
96pub struct RuinMoveSelector<S, V> {
97    /// Minimum entities to include in each ruin move.
98    min_ruin_count: usize,
99    /// Maximum entities to include in each ruin 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 current value.
106    getter: fn(&S, usize) -> Option<V>,
107    /// Function to set value.
108    setter: fn(&mut S, usize, Option<V>),
109    /// Variable name.
110    variable_name: &'static str,
111    /// Entity descriptor index.
112    descriptor_index: usize,
113    /// Number of ruin moves to generate per iteration.
114    moves_per_step: usize,
115    _phantom: PhantomData<V>,
116}
117
118impl<S, V: Debug> Debug for RuinMoveSelector<S, V> {
119    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
120        f.debug_struct("RuinMoveSelector")
121            .field("min_ruin_count", &self.min_ruin_count)
122            .field("max_ruin_count", &self.max_ruin_count)
123            .field("moves_per_step", &self.moves_per_step)
124            .field("variable_name", &self.variable_name)
125            .field("descriptor_index", &self.descriptor_index)
126            .finish()
127    }
128}
129
130impl<S, V> RuinMoveSelector<S, V> {
131    /// Creates a new ruin move selector with typed function pointers.
132    ///
133    /// # Arguments
134    /// * `min_ruin_count` - Minimum entities to ruin (at least 1)
135    /// * `max_ruin_count` - Maximum entities to ruin
136    /// * `entity_count` - Function to get total entity count
137    /// * `getter` - Function to get current value
138    /// * `setter` - Function to set value
139    /// * `variable_name` - Name of the planning variable
140    /// * `descriptor_index` - Entity descriptor index
141    ///
142    /// # Panics
143    /// Panics if `min_ruin_count` is 0 or `max_ruin_count < min_ruin_count`.
144    pub fn new(
145        min_ruin_count: usize,
146        max_ruin_count: usize,
147        entity_count: fn(&S) -> usize,
148        getter: fn(&S, usize) -> Option<V>,
149        setter: fn(&mut S, usize, Option<V>),
150        variable_name: &'static str,
151        descriptor_index: usize,
152    ) -> Self {
153        assert!(min_ruin_count >= 1, "min_ruin_count must be at least 1");
154        assert!(
155            max_ruin_count >= min_ruin_count,
156            "max_ruin_count must be >= min_ruin_count"
157        );
158
159        Self {
160            min_ruin_count,
161            max_ruin_count,
162            seed: None,
163            entity_count,
164            getter,
165            setter,
166            variable_name,
167            descriptor_index,
168            moves_per_step: 10, // Default: generate 10 ruin moves per step
169            _phantom: PhantomData,
170        }
171    }
172
173    /// Sets the number of ruin moves to generate per iteration.
174    ///
175    /// Default is 10.
176    pub fn with_moves_per_step(mut self, count: usize) -> Self {
177        self.moves_per_step = count;
178        self
179    }
180
181    /// Sets the random seed for reproducible subset selection.
182    pub fn with_seed(mut self, seed: u64) -> Self {
183        self.seed = Some(seed);
184        self
185    }
186
187    /// Creates a random number generator.
188    fn create_rng(&self) -> StdRng {
189        match self.seed {
190            Some(seed) => StdRng::seed_from_u64(seed),
191            None => StdRng::from_os_rng(),
192        }
193    }
194}
195
196impl<S, V> MoveSelector<S, RuinMove<S, V>> for RuinMoveSelector<S, V>
197where
198    S: PlanningSolution,
199    V: Clone + Send + Sync + Debug + 'static,
200{
201    fn iter_moves<'a, D: ScoreDirector<S>>(
202        &'a self,
203        score_director: &'a D,
204    ) -> Box<dyn Iterator<Item = RuinMove<S, V>> + 'a> {
205        let total_entities = (self.entity_count)(score_director.working_solution());
206        let getter = self.getter;
207        let setter = self.setter;
208        let variable_name = self.variable_name;
209        let descriptor_index = self.descriptor_index;
210
211        let min = self.min_ruin_count.min(total_entities);
212        let max = self.max_ruin_count.min(total_entities);
213        let moves_count = self.moves_per_step;
214
215        // Pre-generate subsets using RNG
216        let mut rng = self.create_rng();
217        let subsets: Vec<SmallVec<[usize; 8]>> = (0..moves_count)
218            .map(|_| {
219                if total_entities == 0 {
220                    return SmallVec::new();
221                }
222                let ruin_count = if min == max {
223                    min
224                } else {
225                    rng.random_range(min..=max)
226                };
227                let mut indices: SmallVec<[usize; 8]> = (0..total_entities).collect();
228                for i in 0..ruin_count {
229                    let j = rng.random_range(i..total_entities);
230                    indices.swap(i, j);
231                }
232                indices.truncate(ruin_count);
233                indices
234            })
235            .collect();
236
237        Box::new(subsets.into_iter().map(move |indices| {
238            RuinMove::new(&indices, getter, setter, variable_name, descriptor_index)
239        }))
240    }
241
242    fn size<D: ScoreDirector<S>>(&self, score_director: &D) -> usize {
243        let total = (self.entity_count)(score_director.working_solution());
244        if total == 0 {
245            return 0;
246        }
247        // Return configured moves per step (not combinatorial count)
248        self.moves_per_step
249    }
250
251    fn is_never_ending(&self) -> bool {
252        // Random selection means we could generate moves indefinitely
253        false
254    }
255}
256
257#[cfg(test)]
258mod tests {
259    use super::*;
260    use solverforge_core::domain::SolutionDescriptor;
261    use solverforge_core::score::SimpleScore;
262    use solverforge_scoring::SimpleScoreDirector;
263    use std::any::TypeId;
264
265    #[derive(Clone, Debug)]
266    struct Task {
267        assigned_to: Option<i32>,
268    }
269
270    #[derive(Clone, Debug)]
271    struct Schedule {
272        tasks: Vec<Task>,
273        score: Option<SimpleScore>,
274    }
275
276    impl PlanningSolution for Schedule {
277        type Score = SimpleScore;
278        fn score(&self) -> Option<Self::Score> {
279            self.score
280        }
281        fn set_score(&mut self, score: Option<Self::Score>) {
282            self.score = score;
283        }
284    }
285
286    fn entity_count(s: &Schedule) -> usize {
287        s.tasks.len()
288    }
289    fn get_assigned(s: &Schedule, idx: usize) -> Option<i32> {
290        s.tasks.get(idx).and_then(|t| t.assigned_to)
291    }
292    fn set_assigned(s: &mut Schedule, idx: usize, v: Option<i32>) {
293        if let Some(t) = s.tasks.get_mut(idx) {
294            t.assigned_to = v;
295        }
296    }
297
298    fn create_director(
299        assignments: &[Option<i32>],
300    ) -> SimpleScoreDirector<Schedule, impl Fn(&Schedule) -> SimpleScore> {
301        let tasks: Vec<Task> = assignments
302            .iter()
303            .map(|&a| Task { assigned_to: a })
304            .collect();
305        let solution = Schedule { tasks, score: None };
306        let descriptor = SolutionDescriptor::new("Schedule", TypeId::of::<Schedule>());
307        SimpleScoreDirector::with_calculator(solution, descriptor, |_| SimpleScore::of(0))
308    }
309
310    #[test]
311    fn generates_ruin_moves() {
312        let director = create_director(&[Some(1), Some(2), Some(3), Some(4), Some(5)]);
313
314        let selector = RuinMoveSelector::<Schedule, i32>::new(
315            2,
316            3,
317            entity_count,
318            get_assigned,
319            set_assigned,
320            "assigned_to",
321            0,
322        )
323        .with_moves_per_step(5);
324
325        let moves: Vec<_> = selector.iter_moves(&director).collect();
326
327        assert_eq!(moves.len(), 5);
328        for m in &moves {
329            let count = m.ruin_count();
330            assert!((2..=3).contains(&count));
331        }
332    }
333
334    #[test]
335    fn clamps_to_available_entities() {
336        let director = create_director(&[Some(1), Some(2)]);
337
338        // Request more entities than available
339        let selector = RuinMoveSelector::<Schedule, i32>::new(
340            5,
341            10,
342            entity_count,
343            get_assigned,
344            set_assigned,
345            "assigned_to",
346            0,
347        )
348        .with_moves_per_step(3);
349
350        let moves: Vec<_> = selector.iter_moves(&director).collect();
351
352        assert_eq!(moves.len(), 3);
353        for m in &moves {
354            // Should clamp to max 2 entities
355            assert!(m.ruin_count() <= 2);
356        }
357    }
358
359    #[test]
360    fn empty_solution_yields_empty_moves() {
361        let director = create_director(&[]);
362
363        let selector = RuinMoveSelector::<Schedule, i32>::new(
364            1,
365            2,
366            entity_count,
367            get_assigned,
368            set_assigned,
369            "assigned_to",
370            0,
371        );
372
373        let moves: Vec<_> = selector.iter_moves(&director).collect();
374
375        // Moves are generated but they're empty (no entities to ruin)
376        for m in &moves {
377            assert_eq!(m.ruin_count(), 0);
378        }
379    }
380
381    #[test]
382    fn size_returns_moves_per_step() {
383        let director = create_director(&[Some(1), Some(2), Some(3)]);
384
385        let selector = RuinMoveSelector::<Schedule, i32>::new(
386            1,
387            2,
388            entity_count,
389            get_assigned,
390            set_assigned,
391            "assigned_to",
392            0,
393        )
394        .with_moves_per_step(7);
395
396        assert_eq!(selector.size(&director), 7);
397    }
398
399    #[test]
400    #[should_panic(expected = "min_ruin_count must be at least 1")]
401    fn panics_on_zero_min() {
402        let _selector = RuinMoveSelector::<Schedule, i32>::new(
403            0,
404            2,
405            entity_count,
406            get_assigned,
407            set_assigned,
408            "assigned_to",
409            0,
410        );
411    }
412
413    #[test]
414    #[should_panic(expected = "max_ruin_count must be >= min_ruin_count")]
415    fn panics_on_invalid_range() {
416        let _selector = RuinMoveSelector::<Schedule, i32>::new(
417            5,
418            2,
419            entity_count,
420            get_assigned,
421            set_assigned,
422            "assigned_to",
423            0,
424        );
425    }
426}