solverforge_solver/heuristic/move/
list_reverse.rs

1//! ListReverseMove - reverses a segment within a list variable.
2//!
3//! This move reverses the order of elements in a range. Essential for
4//! TSP 2-opt optimization where reversing a tour segment can reduce distance.
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 solverforge_core::domain::PlanningSolution;
14use solverforge_scoring::ScoreDirector;
15
16use super::Move;
17
18/// A move that reverses a segment within a list.
19///
20/// This is the fundamental 2-opt move for TSP. Reversing a segment of the tour
21/// can significantly reduce total distance by eliminating crossing edges.
22///
23/// # Type Parameters
24/// * `S` - The planning solution type
25/// * `V` - The list element value type
26///
27/// # Example
28///
29/// ```
30/// use solverforge_solver::heuristic::r#move::ListReverseMove;
31/// use solverforge_core::domain::PlanningSolution;
32/// use solverforge_core::score::SimpleScore;
33///
34/// #[derive(Clone, Debug)]
35/// struct Tour { cities: Vec<i32>, score: Option<SimpleScore> }
36///
37/// impl PlanningSolution for Tour {
38///     type Score = SimpleScore;
39///     fn score(&self) -> Option<Self::Score> { self.score }
40///     fn set_score(&mut self, score: Option<Self::Score>) { self.score = score; }
41/// }
42///
43/// fn list_len(s: &Tour, _: usize) -> usize { s.cities.len() }
44/// fn list_reverse(s: &mut Tour, _: usize, start: usize, end: usize) {
45///     s.cities[start..end].reverse();
46/// }
47///
48/// // Reverse segment [1..4) in tour: [A, B, C, D, E] -> [A, D, C, B, E]
49/// let m = ListReverseMove::<Tour, i32>::new(
50///     0, 1, 4,
51///     list_len, list_reverse,
52///     "cities", 0,
53/// );
54/// ```
55#[derive(Clone, Copy)]
56pub struct ListReverseMove<S, V> {
57    /// Entity index
58    entity_index: usize,
59    /// Start of range to reverse (inclusive)
60    start: usize,
61    /// End of range to reverse (exclusive)
62    end: usize,
63    /// Get list length for an entity
64    list_len: fn(&S, usize) -> usize,
65    /// Reverse elements in range [start, end)
66    list_reverse: fn(&mut S, usize, usize, usize),
67    variable_name: &'static str,
68    descriptor_index: usize,
69    _phantom: PhantomData<V>,
70}
71
72impl<S, V: Debug> Debug for ListReverseMove<S, V> {
73    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
74        f.debug_struct("ListReverseMove")
75            .field("entity", &self.entity_index)
76            .field("range", &(self.start..self.end))
77            .field("variable_name", &self.variable_name)
78            .finish()
79    }
80}
81
82impl<S, V> ListReverseMove<S, V> {
83    /// Creates a new list reverse move with typed function pointers.
84    ///
85    /// # Arguments
86    /// * `entity_index` - Entity index
87    /// * `start` - Start of range (inclusive)
88    /// * `end` - End of range (exclusive)
89    /// * `list_len` - Function to get list length
90    /// * `list_reverse` - Function to reverse elements in range
91    /// * `variable_name` - Name of the list variable
92    /// * `descriptor_index` - Entity descriptor index
93    #[allow(clippy::too_many_arguments)]
94    pub fn new(
95        entity_index: usize,
96        start: usize,
97        end: usize,
98        list_len: fn(&S, usize) -> usize,
99        list_reverse: fn(&mut S, usize, usize, usize),
100        variable_name: &'static str,
101        descriptor_index: usize,
102    ) -> Self {
103        Self {
104            entity_index,
105            start,
106            end,
107            list_len,
108            list_reverse,
109            variable_name,
110            descriptor_index,
111            _phantom: PhantomData,
112        }
113    }
114
115    /// Returns the entity index.
116    pub fn entity_index(&self) -> usize {
117        self.entity_index
118    }
119
120    /// Returns the range start.
121    pub fn start(&self) -> usize {
122        self.start
123    }
124
125    /// Returns the range end.
126    pub fn end(&self) -> usize {
127        self.end
128    }
129
130    /// Returns the segment length.
131    pub fn segment_len(&self) -> usize {
132        self.end.saturating_sub(self.start)
133    }
134}
135
136impl<S, V> Move<S> for ListReverseMove<S, V>
137where
138    S: PlanningSolution,
139    V: Clone + Send + Sync + Debug + 'static,
140{
141    fn is_doable(&self, score_director: &dyn ScoreDirector<S>) -> bool {
142        let solution = score_director.working_solution();
143
144        // Range must have at least 2 elements to be meaningful
145        if self.end <= self.start + 1 {
146            return false;
147        }
148
149        // Check range is within bounds
150        let len = (self.list_len)(solution, self.entity_index);
151        if self.end > len {
152            return false;
153        }
154
155        true
156    }
157
158    fn do_move(&self, score_director: &mut dyn ScoreDirector<S>) {
159        // Notify before change
160        score_director.before_variable_changed(
161            self.descriptor_index,
162            self.entity_index,
163            self.variable_name,
164        );
165
166        // Reverse the segment
167        (self.list_reverse)(
168            score_director.working_solution_mut(),
169            self.entity_index,
170            self.start,
171            self.end,
172        );
173
174        // Notify after change
175        score_director.after_variable_changed(
176            self.descriptor_index,
177            self.entity_index,
178            self.variable_name,
179        );
180
181        // Register undo - reversing twice restores original
182        let list_reverse = self.list_reverse;
183        let entity = self.entity_index;
184        let start = self.start;
185        let end = self.end;
186
187        score_director.register_undo(Box::new(move |s: &mut S| {
188            list_reverse(s, entity, start, end);
189        }));
190    }
191
192    fn descriptor_index(&self) -> usize {
193        self.descriptor_index
194    }
195
196    fn entity_indices(&self) -> &[usize] {
197        std::slice::from_ref(&self.entity_index)
198    }
199
200    fn variable_name(&self) -> &str {
201        self.variable_name
202    }
203}
204
205#[cfg(test)]
206mod tests {
207    use super::*;
208    use solverforge_core::domain::{EntityDescriptor, SolutionDescriptor, TypedEntityExtractor};
209    use solverforge_core::score::SimpleScore;
210    use solverforge_scoring::{RecordingScoreDirector, SimpleScoreDirector};
211    use std::any::TypeId;
212
213    #[derive(Clone, Debug)]
214    struct Tour {
215        cities: Vec<i32>,
216    }
217
218    #[derive(Clone, Debug)]
219    struct TspSolution {
220        tours: Vec<Tour>,
221        score: Option<SimpleScore>,
222    }
223
224    impl PlanningSolution for TspSolution {
225        type Score = SimpleScore;
226        fn score(&self) -> Option<Self::Score> {
227            self.score
228        }
229        fn set_score(&mut self, score: Option<Self::Score>) {
230            self.score = score;
231        }
232    }
233
234    fn get_tours(s: &TspSolution) -> &Vec<Tour> {
235        &s.tours
236    }
237    fn get_tours_mut(s: &mut TspSolution) -> &mut Vec<Tour> {
238        &mut s.tours
239    }
240
241    fn list_len(s: &TspSolution, entity_idx: usize) -> usize {
242        s.tours.get(entity_idx).map_or(0, |t| t.cities.len())
243    }
244    fn list_reverse(s: &mut TspSolution, entity_idx: usize, start: usize, end: usize) {
245        if let Some(t) = s.tours.get_mut(entity_idx) {
246            t.cities[start..end].reverse();
247        }
248    }
249
250    fn create_director(
251        tours: Vec<Tour>,
252    ) -> SimpleScoreDirector<TspSolution, impl Fn(&TspSolution) -> SimpleScore> {
253        let solution = TspSolution { tours, score: None };
254        let extractor = Box::new(TypedEntityExtractor::new(
255            "Tour",
256            "tours",
257            get_tours,
258            get_tours_mut,
259        ));
260        let entity_desc =
261            EntityDescriptor::new("Tour", TypeId::of::<Tour>(), "tours").with_extractor(extractor);
262        let descriptor = SolutionDescriptor::new("TspSolution", TypeId::of::<TspSolution>())
263            .with_entity(entity_desc);
264        SimpleScoreDirector::with_calculator(solution, descriptor, |_| SimpleScore::of(0))
265    }
266
267    #[test]
268    fn reverse_segment() {
269        let tours = vec![Tour {
270            cities: vec![1, 2, 3, 4, 5],
271        }];
272        let mut director = create_director(tours);
273
274        // Reverse [1..4): [1, 2, 3, 4, 5] -> [1, 4, 3, 2, 5]
275        let m =
276            ListReverseMove::<TspSolution, i32>::new(0, 1, 4, list_len, list_reverse, "cities", 0);
277
278        assert!(m.is_doable(&director));
279
280        {
281            let mut recording = RecordingScoreDirector::new(&mut director);
282            m.do_move(&mut recording);
283
284            let cities = &recording.working_solution().tours[0].cities;
285            assert_eq!(cities, &[1, 4, 3, 2, 5]);
286
287            recording.undo_changes();
288        }
289
290        let cities = &director.working_solution().tours[0].cities;
291        assert_eq!(cities, &[1, 2, 3, 4, 5]);
292    }
293
294    #[test]
295    fn reverse_entire_list() {
296        let tours = vec![Tour {
297            cities: vec![1, 2, 3, 4],
298        }];
299        let mut director = create_director(tours);
300
301        let m =
302            ListReverseMove::<TspSolution, i32>::new(0, 0, 4, list_len, list_reverse, "cities", 0);
303
304        assert!(m.is_doable(&director));
305
306        {
307            let mut recording = RecordingScoreDirector::new(&mut director);
308            m.do_move(&mut recording);
309
310            let cities = &recording.working_solution().tours[0].cities;
311            assert_eq!(cities, &[4, 3, 2, 1]);
312
313            recording.undo_changes();
314        }
315
316        let cities = &director.working_solution().tours[0].cities;
317        assert_eq!(cities, &[1, 2, 3, 4]);
318    }
319
320    #[test]
321    fn single_element_not_doable() {
322        let tours = vec![Tour {
323            cities: vec![1, 2, 3],
324        }];
325        let director = create_director(tours);
326
327        // Reversing a single element is a no-op
328        let m =
329            ListReverseMove::<TspSolution, i32>::new(0, 1, 2, list_len, list_reverse, "cities", 0);
330
331        assert!(!m.is_doable(&director));
332    }
333
334    #[test]
335    fn out_of_bounds_not_doable() {
336        let tours = vec![Tour {
337            cities: vec![1, 2, 3],
338        }];
339        let director = create_director(tours);
340
341        let m =
342            ListReverseMove::<TspSolution, i32>::new(0, 1, 10, list_len, list_reverse, "cities", 0);
343
344        assert!(!m.is_doable(&director));
345    }
346}