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::{ArenaMoveCursor, MoveSelector};
86
87pub struct RuinVariableAccess<S, V> {
88    // Function to get entity count from solution.
89    entity_count: fn(&S) -> usize,
90    // Function to get current value.
91    getter: fn(&S, usize, usize) -> Option<V>,
92    // Function to set value.
93    setter: fn(&mut S, usize, usize, Option<V>),
94    variable_index: usize,
95    // Variable name.
96    variable_name: &'static str,
97    // Entity descriptor index.
98    descriptor_index: usize,
99    _phantom: PhantomData<fn() -> V>,
100}
101
102impl<S, V> Clone for RuinVariableAccess<S, V> {
103    fn clone(&self) -> Self {
104        *self
105    }
106}
107
108impl<S, V> Copy for RuinVariableAccess<S, V> {}
109
110impl<S, V> RuinVariableAccess<S, V> {
111    /// Creates typed variable access metadata for scalar ruin moves.
112    pub fn new(
113        entity_count: fn(&S) -> usize,
114        getter: fn(&S, usize, usize) -> Option<V>,
115        setter: fn(&mut S, usize, usize, Option<V>),
116        variable_index: usize,
117        variable_name: &'static str,
118        descriptor_index: usize,
119    ) -> Self {
120        Self {
121            entity_count,
122            getter,
123            setter,
124            variable_index,
125            variable_name,
126            descriptor_index,
127            _phantom: PhantomData,
128        }
129    }
130}
131
132/// A move selector that generates `RuinMove` instances for Large Neighborhood Search.
133///
134/// Selects random subsets of entities to "ruin" (unassign), enabling a construction
135/// heuristic to reassign them in potentially better configurations.
136///
137/// # Type Parameters
138/// * `S` - The planning solution type
139/// * `V` - The variable value type
140///
141/// # Zero-Erasure
142///
143/// All variable access uses `fn` pointers:
144/// - `getter: fn(&S, usize, usize) -> Option<V>` - gets current value
145/// - `setter: fn(&mut S, usize, usize, Option<V>)` - sets value
146/// - `entity_count: fn(&S) -> usize` - counts entities
147pub struct RuinMoveSelector<S, V> {
148    // Minimum entities to include in each ruin move.
149    min_ruin_count: usize,
150    // Maximum entities to include in each ruin move.
151    max_ruin_count: usize,
152    // RNG state for reproducible subset selection.
153    rng: RefCell<SmallRng>,
154    access: RuinVariableAccess<S, V>,
155    // Number of ruin moves to generate per iteration.
156    moves_per_step: usize,
157}
158
159// SAFETY: RefCell<SmallRng> is only accessed while pre-generating a move batch
160// inside `iter_moves`, and selectors are consumed from a single thread at a time.
161unsafe impl<S, V> Send for RuinMoveSelector<S, V> {}
162
163impl<S, V: Debug> Debug for RuinMoveSelector<S, V> {
164    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
165        f.debug_struct("RuinMoveSelector")
166            .field("min_ruin_count", &self.min_ruin_count)
167            .field("max_ruin_count", &self.max_ruin_count)
168            .field("moves_per_step", &self.moves_per_step)
169            .field("variable_name", &self.access.variable_name)
170            .field("descriptor_index", &self.access.descriptor_index)
171            .finish()
172    }
173}
174
175impl<S, V> RuinMoveSelector<S, V> {
176    /// Creates a new ruin move selector with typed function pointers.
177    ///
178    /// # Arguments
179    /// * `min_ruin_count` - Minimum entities to ruin (at least 1)
180    /// * `max_ruin_count` - Maximum entities to ruin
181    /// * `access` - Typed variable access metadata for the scalar variable
182    ///
183    /// # Panics
184    /// Panics if `min_ruin_count` is 0 or `max_ruin_count < min_ruin_count`.
185    pub fn new(
186        min_ruin_count: usize,
187        max_ruin_count: usize,
188        access: RuinVariableAccess<S, V>,
189    ) -> Self {
190        assert!(min_ruin_count >= 1, "min_ruin_count must be at least 1");
191        assert!(
192            max_ruin_count >= min_ruin_count,
193            "max_ruin_count must be >= min_ruin_count"
194        );
195
196        Self {
197            min_ruin_count,
198            max_ruin_count,
199            rng: RefCell::new(SmallRng::from_rng(&mut rand::rng())),
200            access,
201            moves_per_step: 10, // Default: generate 10 ruin moves per step
202        }
203    }
204
205    /// Sets the number of ruin moves to generate per iteration.
206    ///
207    /// Default is 10.
208    pub fn with_moves_per_step(mut self, count: usize) -> Self {
209        self.moves_per_step = count;
210        self
211    }
212
213    pub fn with_seed(mut self, seed: u64) -> Self {
214        self.rng = RefCell::new(SmallRng::seed_from_u64(seed));
215        self
216    }
217}
218
219impl<S, V> MoveSelector<S, RuinMove<S, V>> for RuinMoveSelector<S, V>
220where
221    S: PlanningSolution,
222    V: Clone + Send + Sync + Debug + 'static,
223{
224    type Cursor<'a>
225        = ArenaMoveCursor<S, RuinMove<S, V>>
226    where
227        Self: 'a;
228
229    fn open_cursor<'a, D: Director<S>>(&'a self, score_director: &D) -> Self::Cursor<'a> {
230        let access = self.access;
231        let total_entities = (access.entity_count)(score_director.working_solution());
232
233        let min = self.min_ruin_count.min(total_entities);
234        let max = self.max_ruin_count.min(total_entities);
235        let moves_count = self.moves_per_step;
236
237        // Pre-generate subsets using RNG
238        let mut rng = self.rng.borrow_mut();
239        let subsets: Vec<SmallVec<[usize; 8]>> = (0..moves_count)
240            .map(|_| {
241                if total_entities == 0 {
242                    return SmallVec::new();
243                }
244                let ruin_count = if min == max {
245                    min
246                } else {
247                    rng.random_range(min..=max)
248                };
249                let mut indices: SmallVec<[usize; 8]> = (0..total_entities).collect();
250                for i in 0..ruin_count {
251                    let j = rng.random_range(i..total_entities);
252                    indices.swap(i, j);
253                }
254                indices.truncate(ruin_count);
255                indices
256            })
257            .collect();
258
259        ArenaMoveCursor::from_moves(subsets.into_iter().map(move |indices| {
260            RuinMove::new(
261                &indices,
262                access.getter,
263                access.setter,
264                access.variable_index,
265                access.variable_name,
266                access.descriptor_index,
267            )
268        }))
269    }
270
271    fn size<D: Director<S>>(&self, score_director: &D) -> usize {
272        let total = (self.access.entity_count)(score_director.working_solution());
273        if total == 0 {
274            return 0;
275        }
276        // Return configured moves per step (not combinatorial count)
277        self.moves_per_step
278    }
279
280    fn is_never_ending(&self) -> bool {
281        // Random selection means we could generate moves indefinitely
282        false
283    }
284}