Skip to main content

solverforge_solver/heuristic/move/
sublist_swap.rs

1/* SublistSwapMove - swaps two contiguous sublists within or between list variables.
2
3This move exchanges two ranges of elements. Essential for vehicle routing
4where segments need to be swapped between vehicles.
5
6# Zero-Erasure Design
7
8Uses concrete function pointers for list operations. No `dyn Any`, no downcasting.
9*/
10
11use std::fmt::Debug;
12use std::marker::PhantomData;
13
14use solverforge_core::domain::PlanningSolution;
15use solverforge_scoring::Director;
16
17use super::list_kernel::{
18    sublist_swap_candidate_trace_identity, sublist_swap_do_move, sublist_swap_is_doable,
19    sublist_swap_tabu_signature, sublist_swap_undo_move, StaticListWindowAccess,
20};
21use super::segment_layout::SegmentSwapCoords;
22use super::{Move, MoveTabuSignature};
23
24/// A move that swaps two contiguous sublists.
25///
26/// Supports both intra-list swaps (within same entity) and inter-list swaps
27/// (between different entities). Uses concrete function pointers for zero-erasure.
28///
29/// # Type Parameters
30/// * `S` - The planning solution type
31/// * `V` - The list element value type
32///
33/// # Example
34///
35/// ```
36/// use solverforge_solver::heuristic::r#move::SublistSwapMove;
37/// use solverforge_core::domain::PlanningSolution;
38/// use solverforge_core::score::SoftScore;
39///
40/// #[derive(Clone, Debug)]
41/// struct Vehicle { id: usize, visits: Vec<i32> }
42///
43/// #[derive(Clone, Debug)]
44/// struct Solution { vehicles: Vec<Vehicle>, score: Option<SoftScore> }
45///
46/// impl PlanningSolution for Solution {
47///     type Score = SoftScore;
48///     fn score(&self) -> Option<Self::Score> { self.score }
49///     fn set_score(&mut self, score: Option<Self::Score>) { self.score = score; }
50/// }
51///
52/// fn list_len(s: &Solution, entity_idx: usize) -> usize {
53///     s.vehicles.get(entity_idx).map_or(0, |v| v.visits.len())
54/// }
55/// fn list_get(s: &Solution, entity_idx: usize, pos: usize) -> Option<i32> {
56///     s.vehicles
57///         .get(entity_idx)
58///         .and_then(|v| v.visits.get(pos))
59///         .copied()
60/// }
61/// fn sublist_remove(s: &mut Solution, entity_idx: usize, start: usize, end: usize) -> Vec<i32> {
62///     s.vehicles.get_mut(entity_idx)
63///         .map(|v| v.visits.drain(start..end).collect())
64///         .unwrap_or_default()
65/// }
66/// fn sublist_insert(s: &mut Solution, entity_idx: usize, pos: usize, items: Vec<i32>) {
67///     if let Some(v) = s.vehicles.get_mut(entity_idx) {
68///         for (i, item) in items.into_iter().enumerate() {
69///             v.visits.insert(pos + i, item);
70///         }
71///     }
72/// }
73///
74/// // Swap [1..3) from vehicle 0 with [0..2) from vehicle 1
75/// let m = SublistSwapMove::<Solution, i32>::new(
76///     0, 1, 3,  // first: entity 0, range [1, 3)
77///     1, 0, 2,  // second: entity 1, range [0, 2)
78///     list_len, list_get, sublist_remove, sublist_insert,
79///     "visits", 0,
80/// );
81/// ```
82pub struct SublistSwapMove<S, V> {
83    // First entity index
84    first_entity_index: usize,
85    // Start of first range (inclusive)
86    first_start: usize,
87    // End of first range (exclusive)
88    first_end: usize,
89    // Second entity index
90    second_entity_index: usize,
91    // Start of second range (inclusive)
92    second_start: usize,
93    // End of second range (exclusive)
94    second_end: usize,
95    list_len: fn(&S, usize) -> usize,
96    list_get: fn(&S, usize, usize) -> Option<V>,
97    // Remove sublist [start, end), returns removed elements
98    sublist_remove: fn(&mut S, usize, usize, usize) -> Vec<V>,
99    // Insert elements at position
100    sublist_insert: fn(&mut S, usize, usize, Vec<V>),
101    variable_name: &'static str,
102    descriptor_index: usize,
103    // Store indices for entity_indices()
104    indices: [usize; 2],
105    _phantom: PhantomData<fn() -> V>,
106}
107
108impl<S, V> Clone for SublistSwapMove<S, V> {
109    fn clone(&self) -> Self {
110        *self
111    }
112}
113
114impl<S, V> Copy for SublistSwapMove<S, V> {}
115
116impl<S, V: Debug> Debug for SublistSwapMove<S, V> {
117    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
118        f.debug_struct("SublistSwapMove")
119            .field("first_entity", &self.first_entity_index)
120            .field("first_range", &(self.first_start..self.first_end))
121            .field("second_entity", &self.second_entity_index)
122            .field("second_range", &(self.second_start..self.second_end))
123            .field("variable_name", &self.variable_name)
124            .finish()
125    }
126}
127
128impl<S, V> SublistSwapMove<S, V> {
129    /* Creates a new sublist swap move with concrete function pointers.
130
131    # Arguments
132    * `first_entity_index` - First entity index
133    * `first_start` - Start of first range (inclusive)
134    * `first_end` - End of first range (exclusive)
135    * `second_entity_index` - Second entity index
136    * `second_start` - Start of second range (inclusive)
137    * `second_end` - End of second range (exclusive)
138    * `list_len` - Function to get list length
139    * `sublist_remove` - Function to remove range [start, end)
140    * `sublist_insert` - Function to insert elements at position
141    * `variable_name` - Name of the list variable
142    * `descriptor_index` - Entity descriptor index
143    */
144    #[allow(clippy::too_many_arguments)]
145    pub fn new(
146        first_entity_index: usize,
147        first_start: usize,
148        first_end: usize,
149        second_entity_index: usize,
150        second_start: usize,
151        second_end: usize,
152        list_len: fn(&S, usize) -> usize,
153        list_get: fn(&S, usize, usize) -> Option<V>,
154        sublist_remove: fn(&mut S, usize, usize, usize) -> Vec<V>,
155        sublist_insert: fn(&mut S, usize, usize, Vec<V>),
156        variable_name: &'static str,
157        descriptor_index: usize,
158    ) -> Self {
159        Self {
160            first_entity_index,
161            first_start,
162            first_end,
163            second_entity_index,
164            second_start,
165            second_end,
166            list_len,
167            list_get,
168            sublist_remove,
169            sublist_insert,
170            variable_name,
171            descriptor_index,
172            indices: [first_entity_index, second_entity_index],
173            _phantom: PhantomData,
174        }
175    }
176
177    pub fn first_entity_index(&self) -> usize {
178        self.first_entity_index
179    }
180
181    pub fn first_start(&self) -> usize {
182        self.first_start
183    }
184
185    pub fn first_end(&self) -> usize {
186        self.first_end
187    }
188
189    pub fn first_len(&self) -> usize {
190        self.first_end.saturating_sub(self.first_start)
191    }
192
193    pub fn second_entity_index(&self) -> usize {
194        self.second_entity_index
195    }
196
197    pub fn second_start(&self) -> usize {
198        self.second_start
199    }
200
201    pub fn second_end(&self) -> usize {
202        self.second_end
203    }
204
205    pub fn second_len(&self) -> usize {
206        self.second_end.saturating_sub(self.second_start)
207    }
208
209    pub fn is_intra_list(&self) -> bool {
210        self.first_entity_index == self.second_entity_index
211    }
212
213    fn access(&self) -> StaticListWindowAccess<S, V> {
214        StaticListWindowAccess {
215            list_len: self.list_len,
216            list_get: self.list_get,
217            sublist_remove: self.sublist_remove,
218            sublist_insert: self.sublist_insert,
219            variable_name: self.variable_name,
220            descriptor_index: self.descriptor_index,
221        }
222    }
223
224    fn coordinates(&self) -> SegmentSwapCoords {
225        SegmentSwapCoords::new(
226            self.first_entity_index,
227            self.first_start,
228            self.first_end,
229            self.second_entity_index,
230            self.second_start,
231            self.second_end,
232        )
233    }
234}
235
236impl<S, V> Move<S> for SublistSwapMove<S, V>
237where
238    S: PlanningSolution,
239    V: Clone + Send + Sync + Debug + 'static,
240{
241    type Undo = ();
242
243    fn is_doable<D: Director<S>>(&self, score_director: &D) -> bool {
244        sublist_swap_is_doable(&self.access(), self.coordinates(), score_director)
245    }
246
247    fn do_move<D: Director<S>>(&self, score_director: &mut D) -> Self::Undo {
248        sublist_swap_do_move(&self.access(), self.coordinates(), score_director);
249    }
250
251    fn undo_move<D: Director<S>>(&self, score_director: &mut D, (): Self::Undo) {
252        sublist_swap_undo_move(&self.access(), self.coordinates(), score_director);
253    }
254
255    fn descriptor_index(&self) -> usize {
256        self.descriptor_index
257    }
258
259    fn entity_indices(&self) -> &[usize] {
260        if self.is_intra_list() {
261            &self.indices[0..1]
262        } else {
263            &self.indices
264        }
265    }
266
267    fn variable_name(&self) -> &str {
268        self.variable_name
269    }
270
271    fn telemetry_label(&self) -> &'static str {
272        "sublist_swap"
273    }
274
275    fn tabu_signature<D: Director<S>>(&self, score_director: &D) -> MoveTabuSignature {
276        sublist_swap_tabu_signature(&self.access(), self.coordinates(), score_director)
277    }
278
279    fn candidate_trace_identity(&self) -> Option<crate::stats::CandidateTraceIdentity> {
280        Some(sublist_swap_candidate_trace_identity(
281            &self.access(),
282            self.coordinates(),
283        ))
284    }
285}