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};
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) -> Option<i32> {
36s.tasks.get(idx).and_then(|t| t.assigned_to)
37}
38fn set_task(s: &mut Schedule, idx: 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 selector = RuinMoveSelector::<Schedule, i32>::new(
442, 3,
45entity_count,
46get_task, set_task,
47"assigned_to", 0,
48);
49
50// Use with a score director
51let solution = Schedule {
52tasks: vec![
53Task { assigned_to: Some(1) },
54Task { assigned_to: Some(2) },
55Task { assigned_to: Some(3) },
56],
57score: None,
58};
59let descriptor = SolutionDescriptor::new("Schedule", TypeId::of::<Schedule>());
60let director = ScoreDirector::simple(
61solution, descriptor, |s, _| s.tasks.len()
62);
63
64let moves: Vec<_> = selector.iter_moves(&director).collect();
65assert!(!moves.is_empty());
66```
67*/
68
69use std::cell::RefCell;
70use std::fmt::Debug;
71use std::marker::PhantomData;
72
73use rand::rngs::SmallRng;
74use rand::{RngExt, SeedableRng};
75use smallvec::SmallVec;
76use solverforge_core::domain::PlanningSolution;
77use solverforge_scoring::Director;
78
79use crate::heuristic::r#move::RuinMove;
80
81use super::MoveSelector;
82
83/// A move selector that generates `RuinMove` instances for Large Neighborhood Search.
84///
85/// Selects random subsets of entities to "ruin" (unassign), enabling a construction
86/// heuristic to reassign them in potentially better configurations.
87///
88/// # Type Parameters
89/// * `S` - The planning solution type
90/// * `V` - The variable value type
91///
92/// # Zero-Erasure
93///
94/// All variable access uses `fn` pointers:
95/// - `getter: fn(&S, usize) -> Option<V>` - gets current value
96/// - `setter: fn(&mut S, usize, Option<V>)` - sets value
97/// - `entity_count: fn(&S) -> usize` - counts entities
98pub struct RuinMoveSelector<S, V> {
99    // Minimum entities to include in each ruin move.
100    min_ruin_count: usize,
101    // Maximum entities to include in each ruin 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 current value.
108    getter: fn(&S, usize) -> Option<V>,
109    // Function to set value.
110    setter: fn(&mut S, usize, Option<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<fn() -> V>,
118}
119
120// SAFETY: RefCell<SmallRng> is only accessed while pre-generating a move batch
121// inside `iter_moves`, and selectors are consumed from a single thread at a time.
122unsafe impl<S, V> Send for RuinMoveSelector<S, V> {}
123
124impl<S, V: Debug> Debug for RuinMoveSelector<S, V> {
125    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
126        f.debug_struct("RuinMoveSelector")
127            .field("min_ruin_count", &self.min_ruin_count)
128            .field("max_ruin_count", &self.max_ruin_count)
129            .field("moves_per_step", &self.moves_per_step)
130            .field("variable_name", &self.variable_name)
131            .field("descriptor_index", &self.descriptor_index)
132            .finish()
133    }
134}
135
136impl<S, V> RuinMoveSelector<S, V> {
137    /// Creates a new ruin move selector with typed function pointers.
138    ///
139    /// # Arguments
140    /// * `min_ruin_count` - Minimum entities to ruin (at least 1)
141    /// * `max_ruin_count` - Maximum entities to ruin
142    /// * `entity_count` - Function to get total entity count
143    /// * `getter` - Function to get current value
144    /// * `setter` - Function to set value
145    /// * `variable_name` - Name of the planning variable
146    /// * `descriptor_index` - Entity descriptor index
147    ///
148    /// # Panics
149    /// Panics if `min_ruin_count` is 0 or `max_ruin_count < min_ruin_count`.
150    pub fn new(
151        min_ruin_count: usize,
152        max_ruin_count: usize,
153        entity_count: fn(&S) -> usize,
154        getter: fn(&S, usize) -> Option<V>,
155        setter: fn(&mut S, usize, Option<V>),
156        variable_name: &'static str,
157        descriptor_index: usize,
158    ) -> Self {
159        assert!(min_ruin_count >= 1, "min_ruin_count must be at least 1");
160        assert!(
161            max_ruin_count >= min_ruin_count,
162            "max_ruin_count must be >= min_ruin_count"
163        );
164
165        Self {
166            min_ruin_count,
167            max_ruin_count,
168            rng: RefCell::new(SmallRng::from_rng(&mut rand::rng())),
169            entity_count,
170            getter,
171            setter,
172            variable_name,
173            descriptor_index,
174            moves_per_step: 10, // Default: generate 10 ruin moves per step
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    pub fn with_seed(mut self, seed: u64) -> Self {
188        self.rng = RefCell::new(SmallRng::seed_from_u64(seed));
189        self
190    }
191}
192
193impl<S, V> MoveSelector<S, RuinMove<S, V>> for RuinMoveSelector<S, V>
194where
195    S: PlanningSolution,
196    V: Clone + Send + Sync + Debug + 'static,
197{
198    fn iter_moves<'a, D: Director<S>>(
199        &'a self,
200        score_director: &'a D,
201    ) -> impl Iterator<Item = RuinMove<S, V>> + 'a {
202        let total_entities = (self.entity_count)(score_director.working_solution());
203        let getter = self.getter;
204        let setter = self.setter;
205        let variable_name = self.variable_name;
206        let descriptor_index = self.descriptor_index;
207
208        let min = self.min_ruin_count.min(total_entities);
209        let max = self.max_ruin_count.min(total_entities);
210        let moves_count = self.moves_per_step;
211
212        // Pre-generate subsets using RNG
213        let mut rng = self.rng.borrow_mut();
214        let subsets: Vec<SmallVec<[usize; 8]>> = (0..moves_count)
215            .map(|_| {
216                if total_entities == 0 {
217                    return SmallVec::new();
218                }
219                let ruin_count = if min == max {
220                    min
221                } else {
222                    rng.random_range(min..=max)
223                };
224                let mut indices: SmallVec<[usize; 8]> = (0..total_entities).collect();
225                for i in 0..ruin_count {
226                    let j = rng.random_range(i..total_entities);
227                    indices.swap(i, j);
228                }
229                indices.truncate(ruin_count);
230                indices
231            })
232            .collect();
233
234        subsets.into_iter().map(move |indices| {
235            RuinMove::new(&indices, getter, setter, variable_name, descriptor_index)
236        })
237    }
238
239    fn size<D: Director<S>>(&self, score_director: &D) -> usize {
240        let total = (self.entity_count)(score_director.working_solution());
241        if total == 0 {
242            return 0;
243        }
244        // Return configured moves per step (not combinatorial count)
245        self.moves_per_step
246    }
247
248    fn is_never_ending(&self) -> bool {
249        // Random selection means we could generate moves indefinitely
250        false
251    }
252}