Skip to main content

solverforge_solver/heuristic/move/
list_swap.rs

1/* ListSwapMove - swaps two elements within or between list variables.
2
3This move exchanges two elements at different positions.
4Useful for TSP-style improvements and route optimization.
5
6# Zero-Erasure Design
7
8Uses concrete function pointers for list operations. No `dyn Any`, no downcasting.
9*/
10
11use std::fmt::Debug;
12
13use solverforge_core::domain::PlanningSolution;
14use solverforge_scoring::Director;
15
16use super::list_kernel::{
17    swap_candidate_trace_identity, swap_do_move, swap_is_doable, swap_tabu_signature,
18    StaticListSwapAccess, SwapCoordinates,
19};
20use super::{Move, MoveTabuSignature};
21
22/// A move that swaps two elements in list variables.
23///
24/// Supports both intra-list swaps (within same entity) and inter-list swaps
25/// (between different entities). Uses concrete function pointers for zero-erasure.
26///
27/// # Type Parameters
28/// * `S` - The planning solution type
29/// * `V` - The list element value type
30///
31/// # Example
32///
33/// ```
34/// use solverforge_solver::heuristic::r#move::ListSwapMove;
35/// use solverforge_core::domain::PlanningSolution;
36/// use solverforge_core::score::SoftScore;
37///
38/// #[derive(Clone, Debug)]
39/// struct Vehicle { id: usize, visits: Vec<i32> }
40///
41/// #[derive(Clone, Debug)]
42/// struct Solution { vehicles: Vec<Vehicle>, score: Option<SoftScore> }
43///
44/// impl PlanningSolution for Solution {
45///     type Score = SoftScore;
46///     fn score(&self) -> Option<Self::Score> { self.score }
47///     fn set_score(&mut self, score: Option<Self::Score>) { self.score = score; }
48/// }
49///
50/// fn list_len(s: &Solution, entity_idx: usize) -> usize {
51///     s.vehicles.get(entity_idx).map_or(0, |v| v.visits.len())
52/// }
53/// fn list_get(s: &Solution, entity_idx: usize, pos: usize) -> Option<i32> {
54///     s.vehicles.get(entity_idx).and_then(|v| v.visits.get(pos).copied())
55/// }
56/// fn list_set(s: &mut Solution, entity_idx: usize, pos: usize, val: i32) {
57///     if let Some(v) = s.vehicles.get_mut(entity_idx) {
58///         if let Some(elem) = v.visits.get_mut(pos) { *elem = val; }
59///     }
60/// }
61///
62/// // Swap elements at positions 1 and 3 in vehicle 0
63/// let m = ListSwapMove::<Solution, i32>::new(
64///     0, 1, 0, 3,
65///     list_len, list_get, list_set,
66///     "visits", 0,
67/// );
68/// ```
69pub struct ListSwapMove<S, V> {
70    // First entity index
71    first_entity_index: usize,
72    // Position in first entity's list
73    first_position: usize,
74    // Second entity index
75    second_entity_index: usize,
76    // Position in second entity's list
77    second_position: usize,
78    list_len: fn(&S, usize) -> usize,
79    list_get: fn(&S, usize, usize) -> Option<V>,
80    // Set element at position
81    list_set: fn(&mut S, usize, usize, V),
82    variable_name: &'static str,
83    descriptor_index: usize,
84    // Store indices for entity_indices()
85    indices: [usize; 2],
86}
87
88impl<S, V> Clone for ListSwapMove<S, V> {
89    fn clone(&self) -> Self {
90        *self
91    }
92}
93
94impl<S, V> Copy for ListSwapMove<S, V> {}
95
96impl<S, V: Debug> Debug for ListSwapMove<S, V> {
97    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
98        f.debug_struct("ListSwapMove")
99            .field("first_entity", &self.first_entity_index)
100            .field("first_position", &self.first_position)
101            .field("second_entity", &self.second_entity_index)
102            .field("second_position", &self.second_position)
103            .field("variable_name", &self.variable_name)
104            .finish()
105    }
106}
107
108impl<S, V> ListSwapMove<S, V> {
109    /* Creates a new list swap move with concrete function pointers.
110
111    # Arguments
112    * `first_entity_index` - First entity index
113    * `first_position` - Position in first entity's list
114    * `second_entity_index` - Second entity index
115    * `second_position` - Position in second entity's list
116    * `list_len` - Function to get list length
117    * `list_get` - Function to get element at position
118    * `list_set` - Function to set element at position
119    * `variable_name` - Name of the list variable
120    * `descriptor_index` - Entity descriptor index
121    */
122    #[allow(clippy::too_many_arguments)]
123    pub fn new(
124        first_entity_index: usize,
125        first_position: usize,
126        second_entity_index: usize,
127        second_position: usize,
128        list_len: fn(&S, usize) -> usize,
129        list_get: fn(&S, usize, usize) -> Option<V>,
130        list_set: fn(&mut S, usize, usize, V),
131        variable_name: &'static str,
132        descriptor_index: usize,
133    ) -> Self {
134        Self {
135            first_entity_index,
136            first_position,
137            second_entity_index,
138            second_position,
139            list_len,
140            list_get,
141            list_set,
142            variable_name,
143            descriptor_index,
144            indices: [first_entity_index, second_entity_index],
145        }
146    }
147
148    pub fn first_entity_index(&self) -> usize {
149        self.first_entity_index
150    }
151
152    pub fn first_position(&self) -> usize {
153        self.first_position
154    }
155
156    pub fn second_entity_index(&self) -> usize {
157        self.second_entity_index
158    }
159
160    pub fn second_position(&self) -> usize {
161        self.second_position
162    }
163
164    pub fn is_intra_list(&self) -> bool {
165        self.first_entity_index == self.second_entity_index
166    }
167
168    fn access(&self) -> StaticListSwapAccess<S, V> {
169        StaticListSwapAccess {
170            list_len: self.list_len,
171            list_get: self.list_get,
172            list_set: self.list_set,
173            variable_name: self.variable_name,
174            descriptor_index: self.descriptor_index,
175        }
176    }
177
178    fn coordinates(&self) -> SwapCoordinates {
179        SwapCoordinates {
180            first_entity: self.first_entity_index,
181            first_position: self.first_position,
182            second_entity: self.second_entity_index,
183            second_position: self.second_position,
184        }
185    }
186}
187
188impl<S, V> Move<S> for ListSwapMove<S, V>
189where
190    S: PlanningSolution,
191    V: Clone + PartialEq + Send + Sync + Debug + 'static,
192{
193    type Undo = ();
194
195    fn is_doable<D: Director<S>>(&self, score_director: &D) -> bool {
196        swap_is_doable(&self.access(), self.coordinates(), score_director)
197    }
198
199    fn do_move<D: Director<S>>(&self, score_director: &mut D) -> Self::Undo {
200        swap_do_move(&self.access(), self.coordinates(), score_director);
201    }
202
203    fn undo_move<D: Director<S>>(&self, score_director: &mut D, (): Self::Undo) {
204        swap_do_move(&self.access(), self.coordinates(), score_director);
205    }
206
207    fn descriptor_index(&self) -> usize {
208        self.descriptor_index
209    }
210
211    fn entity_indices(&self) -> &[usize] {
212        if self.is_intra_list() {
213            &self.indices[0..1]
214        } else {
215            &self.indices
216        }
217    }
218
219    fn variable_name(&self) -> &str {
220        self.variable_name
221    }
222
223    fn telemetry_label(&self) -> &'static str {
224        "list_swap"
225    }
226
227    fn tabu_signature<D: Director<S>>(&self, score_director: &D) -> MoveTabuSignature {
228        swap_tabu_signature(&self.access(), self.coordinates(), score_director)
229    }
230
231    fn candidate_trace_identity(&self) -> Option<crate::stats::CandidateTraceIdentity> {
232        Some(swap_candidate_trace_identity(
233            &self.access(),
234            self.coordinates(),
235        ))
236    }
237}