solverforge_solver/heuristic/selector/list_ruin.rs
1//! List ruin move selector for Large Neighborhood Search on list variables.
2//!
3//! Generates `ListRuinMove` instances that remove elements from list variables,
4//! enabling exploration of distant regions in the solution space.
5//!
6//! # Zero-Erasure Design
7//!
8//! Uses `fn` pointers for list operations. No `Arc<dyn Fn>`, no trait objects
9//! in hot paths.
10//!
11//! # Example
12//!
13//! ```
14//! use solverforge_solver::heuristic::selector::{MoveSelector, ListRuinMoveSelector};
15//! use solverforge_solver::heuristic::r#move::ListRuinMove;
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 Route { stops: Vec<i32> }
24//!
25//! #[derive(Clone, Debug)]
26//! struct VrpSolution { routes: Vec<Route>, score: Option<SimpleScore> }
27//!
28//! impl PlanningSolution for VrpSolution {
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: &VrpSolution) -> usize { s.routes.len() }
35//! fn list_len(s: &VrpSolution, idx: usize) -> usize {
36//! s.routes.get(idx).map_or(0, |r| r.stops.len())
37//! }
38//! fn list_remove(s: &mut VrpSolution, entity_idx: usize, idx: usize) -> i32 {
39//! s.routes.get_mut(entity_idx).map(|r| r.stops.remove(idx)).unwrap_or(0)
40//! }
41//! fn list_insert(s: &mut VrpSolution, entity_idx: usize, idx: usize, v: i32) {
42//! if let Some(r) = s.routes.get_mut(entity_idx) { r.stops.insert(idx, v); }
43//! }
44//!
45//! // Create selector that removes 2-3 elements at a time
46//! let selector = ListRuinMoveSelector::<VrpSolution, i32>::new(
47//! 2, 3,
48//! entity_count,
49//! list_len, list_remove, list_insert,
50//! "stops", 0,
51//! );
52//!
53//! // Use with a score director
54//! let solution = VrpSolution {
55//! routes: vec![Route { stops: vec![1, 2, 3, 4, 5] }],
56//! score: None,
57//! };
58//! let descriptor = SolutionDescriptor::new("VrpSolution", TypeId::of::<VrpSolution>());
59//! let director = SimpleScoreDirector::with_calculator(
60//! solution, descriptor, |_| SimpleScore::of(0)
61//! );
62//!
63//! let moves: Vec<_> = selector.iter_moves(&director).collect();
64//! assert!(!moves.is_empty());
65//! ```
66
67use std::fmt::Debug;
68use std::marker::PhantomData;
69
70use rand::rngs::StdRng;
71use rand::{Rng, SeedableRng};
72use smallvec::SmallVec;
73use solverforge_core::domain::PlanningSolution;
74use solverforge_scoring::ScoreDirector;
75
76use crate::heuristic::r#move::ListRuinMove;
77
78use super::MoveSelector;
79
80/// A move selector that generates `ListRuinMove` instances for Large Neighborhood Search.
81///
82/// Selects random subsets of elements from list variables to "ruin" (remove),
83/// enabling a construction heuristic to reinsert them in better positions.
84///
85/// # Type Parameters
86/// * `S` - The planning solution type
87/// * `V` - The list element value type
88///
89/// # Zero-Erasure
90///
91/// All list access uses `fn` pointers:
92/// - `list_len: fn(&S, usize) -> usize` - gets list length
93/// - `list_remove: fn(&mut S, usize, usize) -> V` - removes element
94/// - `list_insert: fn(&mut S, usize, usize, V)` - inserts element
95/// - `entity_count: fn(&S) -> usize` - counts entities
96pub struct ListRuinMoveSelector<S, V> {
97 /// Minimum elements to remove per move.
98 min_ruin_count: usize,
99 /// Maximum elements to remove per 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 list length for an entity.
106 list_len: fn(&S, usize) -> usize,
107 /// Function to remove element at index, returning it.
108 list_remove: fn(&mut S, usize, usize) -> V,
109 /// Function to insert element at index.
110 list_insert: fn(&mut S, usize, usize, 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
120impl<S, V: Debug> Debug for ListRuinMoveSelector<S, V> {
121 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
122 f.debug_struct("ListRuinMoveSelector")
123 .field("min_ruin_count", &self.min_ruin_count)
124 .field("max_ruin_count", &self.max_ruin_count)
125 .field("moves_per_step", &self.moves_per_step)
126 .field("variable_name", &self.variable_name)
127 .field("descriptor_index", &self.descriptor_index)
128 .finish()
129 }
130}
131
132impl<S, V> ListRuinMoveSelector<S, V> {
133 /// Creates a new list ruin move selector with typed function pointers.
134 ///
135 /// # Arguments
136 /// * `min_ruin_count` - Minimum elements to remove (at least 1)
137 /// * `max_ruin_count` - Maximum elements to remove
138 /// * `entity_count` - Function to get total entity count
139 /// * `list_len` - Function to get list length for an entity
140 /// * `list_remove` - Function to remove element at index
141 /// * `list_insert` - Function to insert element at index
142 /// * `variable_name` - Name of the list variable
143 /// * `descriptor_index` - Entity descriptor index
144 ///
145 /// # Panics
146 /// Panics if `min_ruin_count` is 0 or `max_ruin_count < min_ruin_count`.
147 #[allow(clippy::too_many_arguments)]
148 pub fn new(
149 min_ruin_count: usize,
150 max_ruin_count: usize,
151 entity_count: fn(&S) -> usize,
152 list_len: fn(&S, usize) -> usize,
153 list_remove: fn(&mut S, usize, usize) -> V,
154 list_insert: fn(&mut S, usize, usize, V),
155 variable_name: &'static str,
156 descriptor_index: usize,
157 ) -> Self {
158 assert!(min_ruin_count >= 1, "min_ruin_count must be at least 1");
159 assert!(
160 max_ruin_count >= min_ruin_count,
161 "max_ruin_count must be >= min_ruin_count"
162 );
163
164 Self {
165 min_ruin_count,
166 max_ruin_count,
167 seed: None,
168 entity_count,
169 list_len,
170 list_remove,
171 list_insert,
172 variable_name,
173 descriptor_index,
174 moves_per_step: 10,
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 /// Sets the random seed for reproducible subset selection.
188 pub fn with_seed(mut self, seed: u64) -> Self {
189 self.seed = Some(seed);
190 self
191 }
192
193 /// Creates a random number generator.
194 fn create_rng(&self) -> StdRng {
195 match self.seed {
196 Some(seed) => StdRng::seed_from_u64(seed),
197 None => StdRng::from_os_rng(),
198 }
199 }
200}
201
202impl<S, V> MoveSelector<S, ListRuinMove<S, V>> for ListRuinMoveSelector<S, V>
203where
204 S: PlanningSolution,
205 V: Clone + Send + Sync + Debug + 'static,
206{
207 fn iter_moves<'a, D: ScoreDirector<S>>(
208 &'a self,
209 score_director: &'a D,
210 ) -> impl Iterator<Item = ListRuinMove<S, V>> + 'a {
211 let solution = score_director.working_solution();
212 let total_entities = (self.entity_count)(solution);
213 let list_len = self.list_len;
214 let list_remove = self.list_remove;
215 let list_insert = self.list_insert;
216 let variable_name = self.variable_name;
217 let descriptor_index = self.descriptor_index;
218 let min_ruin = self.min_ruin_count;
219 let max_ruin = self.max_ruin_count;
220 let moves_count = self.moves_per_step;
221
222 // Pre-generate moves using RNG (empty if no entities)
223 let mut rng = self.create_rng();
224 let moves: Vec<ListRuinMove<S, V>> = if total_entities == 0 {
225 Vec::new()
226 } else {
227 (0..moves_count)
228 .filter_map(|_| {
229 // Pick a random entity
230 let entity_idx = rng.random_range(0..total_entities);
231 let list_length = list_len(solution, entity_idx);
232
233 if list_length == 0 {
234 return None;
235 }
236
237 // Clamp ruin count to available elements
238 let min = min_ruin.min(list_length);
239 let max = max_ruin.min(list_length);
240 let ruin_count = if min == max {
241 min
242 } else {
243 rng.random_range(min..=max)
244 };
245
246 // Fisher-Yates partial shuffle to select random indices
247 let mut indices: SmallVec<[usize; 8]> = (0..list_length).collect();
248 for i in 0..ruin_count {
249 let j = rng.random_range(i..list_length);
250 indices.swap(i, j);
251 }
252 indices.truncate(ruin_count);
253
254 Some(ListRuinMove::new(
255 entity_idx,
256 &indices,
257 self.entity_count,
258 list_len,
259 list_remove,
260 list_insert,
261 variable_name,
262 descriptor_index,
263 ))
264 })
265 .collect()
266 };
267
268 moves.into_iter()
269 }
270
271 fn size<D: ScoreDirector<S>>(&self, score_director: &D) -> usize {
272 let total = (self.entity_count)(score_director.working_solution());
273 if total == 0 {
274 return 0;
275 }
276 self.moves_per_step
277 }
278
279 fn is_never_ending(&self) -> bool {
280 false
281 }
282}