solverforge_solver/heuristic/move/
list_ruin.rs

1//! ListRuinMove - removes elements from a list variable for LNS.
2//!
3//! This move removes selected elements from a list, allowing a construction
4//! heuristic to reinsert them in potentially better positions.
5//!
6//! # Zero-Erasure Design
7//!
8//! Uses typed function pointers for list operations. No `dyn Any`, no downcasting.
9
10use std::fmt::Debug;
11use std::marker::PhantomData;
12
13use smallvec::SmallVec;
14use solverforge_core::domain::PlanningSolution;
15use solverforge_scoring::ScoreDirector;
16
17use super::Move;
18
19/// A move that removes elements from a list for Large Neighborhood Search.
20///
21/// Elements are removed by index and stored for reinsertion by a construction
22/// heuristic. This is the list-variable equivalent of `RuinMove`.
23///
24/// # Type Parameters
25/// * `S` - The planning solution type
26/// * `V` - The list element value type
27///
28/// # Example
29///
30/// ```
31/// use solverforge_solver::heuristic::r#move::ListRuinMove;
32/// use solverforge_core::domain::PlanningSolution;
33/// use solverforge_core::score::SimpleScore;
34///
35/// #[derive(Clone, Debug)]
36/// struct Route { stops: Vec<i32>, score: Option<SimpleScore> }
37///
38/// impl PlanningSolution for Route {
39///     type Score = SimpleScore;
40///     fn score(&self) -> Option<Self::Score> { self.score }
41///     fn set_score(&mut self, score: Option<Self::Score>) { self.score = score; }
42/// }
43///
44/// fn list_len(s: &Route, _: usize) -> usize { s.stops.len() }
45/// fn list_remove(s: &mut Route, _: usize, idx: usize) -> i32 { s.stops.remove(idx) }
46/// fn list_insert(s: &mut Route, _: usize, idx: usize, v: i32) { s.stops.insert(idx, v); }
47///
48/// // Remove elements at indices 1 and 3 from the route
49/// let m = ListRuinMove::<Route, i32>::new(
50///     0,
51///     &[1, 3],
52///     list_len, list_remove, list_insert,
53///     "stops", 0,
54/// );
55/// ```
56pub struct ListRuinMove<S, V> {
57    /// Entity index
58    entity_index: usize,
59    /// Indices of elements to remove (in ascending order for correct removal)
60    element_indices: SmallVec<[usize; 8]>,
61    /// Get list length
62    list_len: fn(&S, usize) -> usize,
63    /// Remove element at index, returning it
64    list_remove: fn(&mut S, usize, usize) -> V,
65    /// Insert element at index
66    list_insert: fn(&mut S, usize, usize, V),
67    variable_name: &'static str,
68    descriptor_index: usize,
69    _phantom: PhantomData<V>,
70}
71
72impl<S, V> Clone for ListRuinMove<S, V> {
73    fn clone(&self) -> Self {
74        Self {
75            entity_index: self.entity_index,
76            element_indices: self.element_indices.clone(),
77            list_len: self.list_len,
78            list_remove: self.list_remove,
79            list_insert: self.list_insert,
80            variable_name: self.variable_name,
81            descriptor_index: self.descriptor_index,
82            _phantom: PhantomData,
83        }
84    }
85}
86
87impl<S, V: Debug> Debug for ListRuinMove<S, V> {
88    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
89        f.debug_struct("ListRuinMove")
90            .field("entity", &self.entity_index)
91            .field("elements", &self.element_indices.as_slice())
92            .field("variable_name", &self.variable_name)
93            .finish()
94    }
95}
96
97impl<S, V> ListRuinMove<S, V> {
98    /// Creates a new list ruin move with typed function pointers.
99    ///
100    /// # Arguments
101    /// * `entity_index` - Entity index
102    /// * `element_indices` - Indices of elements to remove
103    /// * `list_len` - Function to get list length
104    /// * `list_remove` - Function to remove element at index
105    /// * `list_insert` - Function to insert element at index
106    /// * `variable_name` - Name of the list variable
107    /// * `descriptor_index` - Entity descriptor index
108    ///
109    /// # Note
110    /// Indices are sorted internally for correct removal order.
111    #[allow(clippy::too_many_arguments)]
112    pub fn new(
113        entity_index: usize,
114        element_indices: &[usize],
115        list_len: fn(&S, usize) -> usize,
116        list_remove: fn(&mut S, usize, usize) -> V,
117        list_insert: fn(&mut S, usize, usize, V),
118        variable_name: &'static str,
119        descriptor_index: usize,
120    ) -> Self {
121        let mut indices: SmallVec<[usize; 8]> = SmallVec::from_slice(element_indices);
122        indices.sort_unstable();
123        Self {
124            entity_index,
125            element_indices: indices,
126            list_len,
127            list_remove,
128            list_insert,
129            variable_name,
130            descriptor_index,
131            _phantom: PhantomData,
132        }
133    }
134
135    /// Returns the entity index.
136    pub fn entity_index(&self) -> usize {
137        self.entity_index
138    }
139
140    /// Returns the element indices being removed.
141    pub fn element_indices(&self) -> &[usize] {
142        &self.element_indices
143    }
144
145    /// Returns the number of elements being removed.
146    pub fn ruin_count(&self) -> usize {
147        self.element_indices.len()
148    }
149}
150
151impl<S, V> Move<S> for ListRuinMove<S, V>
152where
153    S: PlanningSolution,
154    V: Clone + Send + Sync + Debug + 'static,
155{
156    fn is_doable<D: ScoreDirector<S>>(&self, score_director: &D) -> bool {
157        if self.element_indices.is_empty() {
158            return false;
159        }
160
161        let solution = score_director.working_solution();
162        let len = (self.list_len)(solution, self.entity_index);
163
164        // All indices must be within bounds
165        self.element_indices.iter().all(|&idx| idx < len)
166    }
167
168    fn do_move<D: ScoreDirector<S>>(&self, score_director: &mut D) {
169        let list_remove = self.list_remove;
170        let list_insert = self.list_insert;
171        let entity = self.entity_index;
172        let descriptor = self.descriptor_index;
173        let variable_name = self.variable_name;
174
175        // Notify before change
176        score_director.before_variable_changed(descriptor, entity, variable_name);
177
178        // Remove elements in reverse order (highest index first) to preserve indices
179        let mut removed: SmallVec<[(usize, V); 8]> = SmallVec::new();
180        for &idx in self.element_indices.iter().rev() {
181            let value = list_remove(score_director.working_solution_mut(), entity, idx);
182            removed.push((idx, value));
183        }
184
185        // Notify after change
186        score_director.after_variable_changed(descriptor, entity, variable_name);
187
188        // Register undo - reinsert in original order (lowest index first)
189        score_director.register_undo(Box::new(move |s: &mut S| {
190            for (idx, value) in removed.into_iter().rev() {
191                list_insert(s, entity, idx, value);
192            }
193        }));
194    }
195
196    fn descriptor_index(&self) -> usize {
197        self.descriptor_index
198    }
199
200    fn entity_indices(&self) -> &[usize] {
201        std::slice::from_ref(&self.entity_index)
202    }
203
204    fn variable_name(&self) -> &str {
205        self.variable_name
206    }
207}
208
209#[cfg(test)]
210mod tests {
211    use super::*;
212    use solverforge_core::domain::{EntityDescriptor, SolutionDescriptor, TypedEntityExtractor};
213    use solverforge_core::score::SimpleScore;
214    use solverforge_scoring::{RecordingScoreDirector, SimpleScoreDirector};
215    use std::any::TypeId;
216
217    #[derive(Clone, Debug)]
218    struct Route {
219        stops: Vec<i32>,
220    }
221
222    #[derive(Clone, Debug)]
223    struct VrpSolution {
224        routes: Vec<Route>,
225        score: Option<SimpleScore>,
226    }
227
228    impl PlanningSolution for VrpSolution {
229        type Score = SimpleScore;
230        fn score(&self) -> Option<Self::Score> {
231            self.score
232        }
233        fn set_score(&mut self, score: Option<Self::Score>) {
234            self.score = score;
235        }
236    }
237
238    fn get_routes(s: &VrpSolution) -> &Vec<Route> {
239        &s.routes
240    }
241    fn get_routes_mut(s: &mut VrpSolution) -> &mut Vec<Route> {
242        &mut s.routes
243    }
244
245    fn list_len(s: &VrpSolution, entity_idx: usize) -> usize {
246        s.routes.get(entity_idx).map_or(0, |r| r.stops.len())
247    }
248    fn list_remove(s: &mut VrpSolution, entity_idx: usize, idx: usize) -> i32 {
249        s.routes
250            .get_mut(entity_idx)
251            .map(|r| r.stops.remove(idx))
252            .unwrap_or(0)
253    }
254    fn list_insert(s: &mut VrpSolution, entity_idx: usize, idx: usize, v: i32) {
255        if let Some(r) = s.routes.get_mut(entity_idx) {
256            r.stops.insert(idx, v);
257        }
258    }
259
260    fn create_director(
261        stops: Vec<i32>,
262    ) -> SimpleScoreDirector<VrpSolution, impl Fn(&VrpSolution) -> SimpleScore> {
263        let routes = vec![Route { stops }];
264        let solution = VrpSolution {
265            routes,
266            score: None,
267        };
268        let extractor = Box::new(TypedEntityExtractor::new(
269            "Route",
270            "routes",
271            get_routes,
272            get_routes_mut,
273        ));
274        let entity_desc = EntityDescriptor::new("Route", TypeId::of::<Route>(), "routes")
275            .with_extractor(extractor);
276        let descriptor = SolutionDescriptor::new("VrpSolution", TypeId::of::<VrpSolution>())
277            .with_entity(entity_desc);
278        SimpleScoreDirector::with_calculator(solution, descriptor, |_| SimpleScore::of(0))
279    }
280
281    #[test]
282    fn ruin_single_element() {
283        let mut director = create_director(vec![1, 2, 3, 4, 5]);
284
285        let m = ListRuinMove::<VrpSolution, i32>::new(
286            0,
287            &[2],
288            list_len,
289            list_remove,
290            list_insert,
291            "stops",
292            0,
293        );
294
295        assert!(m.is_doable(&director));
296        assert_eq!(m.ruin_count(), 1);
297
298        {
299            let mut recording = RecordingScoreDirector::new(&mut director);
300            m.do_move(&mut recording);
301
302            let stops = &recording.working_solution().routes[0].stops;
303            assert_eq!(stops, &[1, 2, 4, 5]);
304
305            recording.undo_changes();
306        }
307
308        let stops = &director.working_solution().routes[0].stops;
309        assert_eq!(stops, &[1, 2, 3, 4, 5]);
310    }
311
312    #[test]
313    fn ruin_multiple_elements() {
314        let mut director = create_director(vec![1, 2, 3, 4, 5]);
315
316        // Remove indices 1, 3 (values 2, 4)
317        let m = ListRuinMove::<VrpSolution, i32>::new(
318            0,
319            &[1, 3],
320            list_len,
321            list_remove,
322            list_insert,
323            "stops",
324            0,
325        );
326
327        assert!(m.is_doable(&director));
328        assert_eq!(m.ruin_count(), 2);
329
330        {
331            let mut recording = RecordingScoreDirector::new(&mut director);
332            m.do_move(&mut recording);
333
334            let stops = &recording.working_solution().routes[0].stops;
335            assert_eq!(stops, &[1, 3, 5]);
336
337            recording.undo_changes();
338        }
339
340        let stops = &director.working_solution().routes[0].stops;
341        assert_eq!(stops, &[1, 2, 3, 4, 5]);
342    }
343
344    #[test]
345    fn ruin_unordered_indices() {
346        let mut director = create_director(vec![1, 2, 3, 4, 5]);
347
348        // Indices provided in reverse order - should still work
349        let m = ListRuinMove::<VrpSolution, i32>::new(
350            0,
351            &[3, 1],
352            list_len,
353            list_remove,
354            list_insert,
355            "stops",
356            0,
357        );
358
359        {
360            let mut recording = RecordingScoreDirector::new(&mut director);
361            m.do_move(&mut recording);
362
363            let stops = &recording.working_solution().routes[0].stops;
364            assert_eq!(stops, &[1, 3, 5]);
365
366            recording.undo_changes();
367        }
368
369        let stops = &director.working_solution().routes[0].stops;
370        assert_eq!(stops, &[1, 2, 3, 4, 5]);
371    }
372
373    #[test]
374    fn empty_indices_not_doable() {
375        let director = create_director(vec![1, 2, 3]);
376
377        let m = ListRuinMove::<VrpSolution, i32>::new(
378            0,
379            &[],
380            list_len,
381            list_remove,
382            list_insert,
383            "stops",
384            0,
385        );
386
387        assert!(!m.is_doable(&director));
388    }
389
390    #[test]
391    fn out_of_bounds_not_doable() {
392        let director = create_director(vec![1, 2, 3]);
393
394        let m = ListRuinMove::<VrpSolution, i32>::new(
395            0,
396            &[0, 10],
397            list_len,
398            list_remove,
399            list_insert,
400            "stops",
401            0,
402        );
403
404        assert!(!m.is_doable(&director));
405    }
406}