solverforge_solver/heuristic/selector/
ruin.rs1use 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
81pub struct RuinMoveSelector<S, V> {
97 min_ruin_count: usize,
99 max_ruin_count: usize,
101 seed: Option<u64>,
103 entity_count: fn(&S) -> usize,
105 getter: fn(&S, usize) -> Option<V>,
107 setter: fn(&mut S, usize, Option<V>),
109 variable_name: &'static str,
111 descriptor_index: usize,
113 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 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, _phantom: PhantomData,
170 }
171 }
172
173 pub fn with_moves_per_step(mut self, count: usize) -> Self {
177 self.moves_per_step = count;
178 self
179 }
180
181 pub fn with_seed(mut self, seed: u64) -> Self {
183 self.seed = Some(seed);
184 self
185 }
186
187 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>(
202 &'a self,
203 score_director: &'a dyn ScoreDirector<S>,
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 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(&self, score_director: &dyn ScoreDirector<S>) -> usize {
243 let total = (self.entity_count)(score_director.working_solution());
244 if total == 0 {
245 return 0;
246 }
247 self.moves_per_step
249 }
250
251 fn is_never_ending(&self) -> bool {
252 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 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 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 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}