Skip to main content

solverforge_solver/heuristic/selector/
ruin.rs

1/* Ruin move selector for Large Neighborhood Search.
2
3Generates `RuinMove` instances that unassign subsets of entities,
4enabling exploration of distant regions in the solution space.
5
6# Zero-Erasure Design
7
8Uses `fn` pointers for variable access and entity counting.
9No `Arc<dyn Fn>`, no trait objects in hot paths.
10
11# Example
12
13```
14use solverforge_solver::heuristic::selector::{MoveSelector, RuinMoveSelector, RuinVariableAccess};
15use solverforge_solver::heuristic::r#move::RuinMove;
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 Task { assigned_to: Option<i32> }
24
25#[derive(Clone, Debug)]
26struct Schedule { tasks: Vec<Task>, score: Option<SoftScore> }
27
28impl PlanningSolution for Schedule {
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: &Schedule) -> usize { s.tasks.len() }
35fn get_task(s: &Schedule, idx: usize, _variable_index: usize) -> Option<i32> {
36s.tasks.get(idx).and_then(|t| t.assigned_to)
37}
38fn set_task(s: &mut Schedule, idx: usize, _variable_index: usize, v: Option<i32>) {
39if 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
43let access = RuinVariableAccess::new(
44entity_count,
45get_task, set_task,
460,
47"assigned_to", 0,
48);
49let selector = RuinMoveSelector::<Schedule, i32>::new(
502, 3,
51access,
52);
53
54// Use with a score director
55let solution = Schedule {
56tasks: vec![
57Task { assigned_to: Some(1) },
58Task { assigned_to: Some(2) },
59Task { assigned_to: Some(3) },
60],
61score: None,
62};
63let descriptor = SolutionDescriptor::new("Schedule", TypeId::of::<Schedule>());
64let director = ScoreDirector::simple(
65solution, descriptor, |s, _| s.tasks.len()
66);
67
68let moves: Vec<_> = selector.iter_moves(&director).collect();
69assert!(!moves.is_empty());
70```
71*/
72
73use std::cell::RefCell;
74use std::fmt::Debug;
75use std::marker::PhantomData;
76
77use rand::rngs::SmallRng;
78use rand::{RngExt, SeedableRng};
79use smallvec::SmallVec;
80use solverforge_core::domain::PlanningSolution;
81use solverforge_scoring::Director;
82
83use crate::heuristic::r#move::RuinMove;
84
85use super::move_selector::{
86    CandidateId, CandidateStore, MoveCandidateRef, MoveCursor, MoveSelector,
87};
88
89pub struct RuinVariableAccess<S, V> {
90    // Function to get entity count from solution.
91    entity_count: fn(&S) -> usize,
92    // Function to get current value.
93    getter: fn(&S, usize, usize) -> Option<V>,
94    // Function to set value.
95    setter: fn(&mut S, usize, usize, Option<V>),
96    variable_index: usize,
97    // Variable name.
98    variable_name: &'static str,
99    // Entity descriptor index.
100    descriptor_index: usize,
101    _phantom: PhantomData<fn() -> V>,
102}
103
104impl<S, V> Clone for RuinVariableAccess<S, V> {
105    fn clone(&self) -> Self {
106        *self
107    }
108}
109
110impl<S, V> Copy for RuinVariableAccess<S, V> {}
111
112impl<S, V> RuinVariableAccess<S, V> {
113    /// Creates scalar-variable access metadata for scalar ruin moves.
114    pub fn new(
115        entity_count: fn(&S) -> usize,
116        getter: fn(&S, usize, usize) -> Option<V>,
117        setter: fn(&mut S, usize, usize, Option<V>),
118        variable_index: usize,
119        variable_name: &'static str,
120        descriptor_index: usize,
121    ) -> Self {
122        Self {
123            entity_count,
124            getter,
125            setter,
126            variable_index,
127            variable_name,
128            descriptor_index,
129            _phantom: PhantomData,
130        }
131    }
132}
133
134/// A move selector that generates `RuinMove` instances for Large Neighborhood Search.
135///
136/// Selects random subsets of entities to "ruin" (unassign), enabling a construction
137/// heuristic to reassign them in potentially better configurations.
138///
139/// # Type Parameters
140/// * `S` - The planning solution type
141/// * `V` - The variable value type
142///
143/// # Zero-Erasure
144///
145/// All variable access uses `fn` pointers:
146/// - `getter: fn(&S, usize, usize) -> Option<V>` - gets current value
147/// - `setter: fn(&mut S, usize, usize, Option<V>)` - sets value
148/// - `entity_count: fn(&S) -> usize` - counts entities
149pub struct RuinMoveSelector<S, V> {
150    // Minimum entities to include in each ruin move.
151    min_ruin_count: usize,
152    // Maximum entities to include in each ruin move.
153    max_ruin_count: usize,
154    // RNG state for reproducible subset selection.
155    rng: RefCell<SmallRng>,
156    access: RuinVariableAccess<S, V>,
157    // Number of ruin moves to generate per iteration.
158    moves_per_step: usize,
159}
160
161pub struct RuinMoveCursor<S, V>
162where
163    S: PlanningSolution,
164    V: Clone + Send + Sync + Debug + 'static,
165{
166    store: CandidateStore<S, RuinMove<S, V>>,
167    subsets: std::vec::IntoIter<SmallVec<[usize; 8]>>,
168    access: RuinVariableAccess<S, V>,
169}
170
171impl<S, V> RuinMoveCursor<S, V>
172where
173    S: PlanningSolution,
174    V: Clone + Send + Sync + Debug + 'static,
175{
176    pub(crate) fn next_subset(&mut self) -> Option<SmallVec<[usize; 8]>> {
177        self.subsets.next()
178    }
179
180    fn next_move(&mut self) -> Option<RuinMove<S, V>> {
181        let indices = self.next_subset()?;
182        Some(RuinMove::from_indices(
183            indices,
184            self.access.getter,
185            self.access.setter,
186            self.access.variable_index,
187            self.access.variable_name,
188            self.access.descriptor_index,
189        ))
190    }
191}
192
193impl<S, V> MoveCursor<S, RuinMove<S, V>> for RuinMoveCursor<S, V>
194where
195    S: PlanningSolution,
196    V: Clone + Send + Sync + Debug + 'static,
197{
198    fn next_candidate(&mut self) -> Option<CandidateId> {
199        let mov = self.next_move()?;
200        Some(self.store.push(mov))
201    }
202
203    fn candidate(&self, id: CandidateId) -> Option<MoveCandidateRef<'_, S, RuinMove<S, V>>> {
204        self.store.candidate(id)
205    }
206
207    fn take_candidate(&mut self, id: CandidateId) -> RuinMove<S, V> {
208        self.store.take_candidate(id)
209    }
210
211    fn next_owned_candidate(&mut self) -> Option<RuinMove<S, V>> {
212        self.next_move()
213    }
214
215    #[inline(always)]
216    fn next_owned_candidate_matching(
217        &mut self,
218        predicate: for<'a> fn(MoveCandidateRef<'a, S, RuinMove<S, V>>) -> bool,
219    ) -> Option<RuinMove<S, V>> {
220        loop {
221            let mov = self.next_move()?;
222            if predicate(MoveCandidateRef::Borrowed(&mov)) {
223                return Some(mov);
224            }
225        }
226    }
227
228    fn release_candidate(&mut self, id: CandidateId) -> bool {
229        self.store.release_candidate(id)
230    }
231}
232
233impl<S, V> Iterator for RuinMoveCursor<S, V>
234where
235    S: PlanningSolution,
236    V: Clone + Send + Sync + Debug + 'static,
237{
238    type Item = RuinMove<S, V>;
239
240    fn next(&mut self) -> Option<Self::Item> {
241        self.next_owned_candidate()
242    }
243}
244
245// SAFETY: RefCell<SmallRng> is only accessed while pre-generating a move batch
246// inside `iter_moves`, and selectors are consumed from a single thread at a time.
247unsafe impl<S, V> Send for RuinMoveSelector<S, V> {}
248
249impl<S, V: Debug> Debug for RuinMoveSelector<S, V> {
250    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
251        f.debug_struct("RuinMoveSelector")
252            .field("min_ruin_count", &self.min_ruin_count)
253            .field("max_ruin_count", &self.max_ruin_count)
254            .field("moves_per_step", &self.moves_per_step)
255            .field("variable_name", &self.access.variable_name)
256            .field("descriptor_index", &self.access.descriptor_index)
257            .finish()
258    }
259}
260
261impl<S, V> RuinMoveSelector<S, V> {
262    /// Creates a new ruin move selector with concrete function pointers.
263    ///
264    /// # Arguments
265    /// * `min_ruin_count` - Minimum entities to ruin (at least 1)
266    /// * `max_ruin_count` - Maximum entities to ruin
267    /// * `access` - Concrete variable access metadata for the scalar variable
268    ///
269    /// # Panics
270    /// Panics if `min_ruin_count` is 0 or `max_ruin_count < min_ruin_count`.
271    pub fn new(
272        min_ruin_count: usize,
273        max_ruin_count: usize,
274        access: RuinVariableAccess<S, V>,
275    ) -> Self {
276        assert!(min_ruin_count >= 1, "min_ruin_count must be at least 1");
277        assert!(
278            max_ruin_count >= min_ruin_count,
279            "max_ruin_count must be >= min_ruin_count"
280        );
281
282        Self {
283            min_ruin_count,
284            max_ruin_count,
285            rng: RefCell::new(SmallRng::from_rng(&mut rand::rng())),
286            access,
287            moves_per_step: 10, // Default: generate 10 ruin moves per step
288        }
289    }
290
291    /// Sets the number of ruin moves to generate per iteration.
292    ///
293    /// Default is 10.
294    pub fn with_moves_per_step(mut self, count: usize) -> Self {
295        self.moves_per_step = count;
296        self
297    }
298
299    pub fn with_seed(mut self, seed: u64) -> Self {
300        self.rng = RefCell::new(SmallRng::seed_from_u64(seed));
301        self
302    }
303}
304
305impl<S, V> MoveSelector<S, RuinMove<S, V>> for RuinMoveSelector<S, V>
306where
307    S: PlanningSolution,
308    V: Clone + Send + Sync + Debug + 'static,
309{
310    type Cursor<'a>
311        = RuinMoveCursor<S, V>
312    where
313        Self: 'a;
314
315    fn open_cursor<'a, D: Director<S>>(&'a self, score_director: &D) -> Self::Cursor<'a> {
316        let access = self.access;
317        let total_entities = (access.entity_count)(score_director.working_solution());
318
319        let min = self.min_ruin_count.min(total_entities);
320        let max = self.max_ruin_count.min(total_entities);
321        let moves_count = self.moves_per_step;
322
323        // Pre-generate subsets using RNG
324        let mut rng = self.rng.borrow_mut();
325        let mut permutation: Vec<usize> = (0..total_entities).collect();
326        let subsets: Vec<SmallVec<[usize; 8]>> = (0..moves_count)
327            .map(|_| {
328                if total_entities == 0 {
329                    return SmallVec::new();
330                }
331                for (index, entity) in permutation.iter_mut().enumerate() {
332                    *entity = index;
333                }
334                let ruin_count = if min == max {
335                    min
336                } else {
337                    rng.random_range(min..=max)
338                };
339                for i in 0..ruin_count {
340                    let j = rng.random_range(i..total_entities);
341                    permutation.swap(i, j);
342                }
343                permutation[..ruin_count].iter().copied().collect()
344            })
345            .collect();
346
347        RuinMoveCursor {
348            store: CandidateStore::new(),
349            subsets: subsets.into_iter(),
350            access,
351        }
352    }
353
354    fn size<D: Director<S>>(&self, score_director: &D) -> usize {
355        let total = (self.access.entity_count)(score_director.working_solution());
356        if total == 0 {
357            return 0;
358        }
359        // Return configured moves per step (not combinatorial count)
360        self.moves_per_step
361    }
362
363    fn is_never_ending(&self) -> bool {
364        // Random selection means we could generate moves indefinitely
365        false
366    }
367}