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