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