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<fn() -> 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 ) -> impl 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 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}