solverforge_solver/heuristic/move/
composite.rs1use std::fmt::Debug;
68
69use smallvec::SmallVec;
70use solverforge_core::domain::PlanningSolution;
71use solverforge_scoring::ScoreDirector;
72
73use super::Move;
74
75#[derive(Clone)]
90pub struct CompositeMove<S, M1, M2>
91where
92 S: PlanningSolution,
93 M1: Move<S>,
94 M2: Move<S>,
95{
96 first: M1,
97 second: M2,
98 combined_indices: SmallVec<[usize; 8]>,
100 _phantom: std::marker::PhantomData<S>,
101}
102
103impl<S, M1, M2> Debug for CompositeMove<S, M1, M2>
104where
105 S: PlanningSolution,
106 M1: Move<S>,
107 M2: Move<S>,
108{
109 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
110 f.debug_struct("CompositeMove")
111 .field("first", &self.first)
112 .field("second", &self.second)
113 .field("combined_indices", &self.combined_indices.as_slice())
114 .finish()
115 }
116}
117
118impl<S, M1, M2> CompositeMove<S, M1, M2>
119where
120 S: PlanningSolution,
121 M1: Move<S>,
122 M2: Move<S>,
123{
124 pub fn new(first: M1, second: M2) -> Self {
132 let mut combined_indices = SmallVec::new();
134 combined_indices.extend_from_slice(first.entity_indices());
135 for idx in second.entity_indices() {
136 if !combined_indices.contains(idx) {
137 combined_indices.push(*idx);
138 }
139 }
140
141 Self {
142 first,
143 second,
144 combined_indices,
145 _phantom: std::marker::PhantomData,
146 }
147 }
148
149 pub fn first(&self) -> &M1 {
151 &self.first
152 }
153
154 pub fn second(&self) -> &M2 {
156 &self.second
157 }
158}
159
160impl<S, M1, M2> Move<S> for CompositeMove<S, M1, M2>
161where
162 S: PlanningSolution,
163 M1: Move<S>,
164 M2: Move<S>,
165{
166 fn is_doable(&self, score_director: &dyn ScoreDirector<S>) -> bool {
167 self.first.is_doable(score_director) && self.second.is_doable(score_director)
169 }
170
171 fn do_move(&self, score_director: &mut dyn ScoreDirector<S>) {
172 self.first.do_move(score_director);
174 self.second.do_move(score_director);
175 }
176
177 fn descriptor_index(&self) -> usize {
178 self.first.descriptor_index()
180 }
181
182 fn entity_indices(&self) -> &[usize] {
183 &self.combined_indices
184 }
185
186 fn variable_name(&self) -> &str {
187 self.first.variable_name()
189 }
190}
191
192#[cfg(test)]
193mod tests {
194 use super::*;
195 use crate::heuristic::r#move::ChangeMove;
196 use solverforge_core::domain::SolutionDescriptor;
197 use solverforge_core::score::SimpleScore;
198 use solverforge_scoring::{RecordingScoreDirector, SimpleScoreDirector};
199 use std::any::TypeId;
200
201 #[derive(Clone, Debug)]
202 struct Task {
203 x: Option<i32>,
204 y: Option<i32>,
205 }
206
207 #[derive(Clone, Debug)]
208 struct Sol {
209 tasks: Vec<Task>,
210 score: Option<SimpleScore>,
211 }
212
213 impl PlanningSolution for Sol {
214 type Score = SimpleScore;
215 fn score(&self) -> Option<Self::Score> {
216 self.score
217 }
218 fn set_score(&mut self, score: Option<Self::Score>) {
219 self.score = score;
220 }
221 }
222
223 fn get_x(s: &Sol, i: usize) -> Option<i32> {
224 s.tasks.get(i).and_then(|t| t.x)
225 }
226 fn set_x(s: &mut Sol, i: usize, v: Option<i32>) {
227 if let Some(t) = s.tasks.get_mut(i) {
228 t.x = v;
229 }
230 }
231 fn get_y(s: &Sol, i: usize) -> Option<i32> {
232 s.tasks.get(i).and_then(|t| t.y)
233 }
234 fn set_y(s: &mut Sol, i: usize, v: Option<i32>) {
235 if let Some(t) = s.tasks.get_mut(i) {
236 t.y = v;
237 }
238 }
239
240 fn create_director(tasks: Vec<Task>) -> SimpleScoreDirector<Sol, impl Fn(&Sol) -> SimpleScore> {
241 let solution = Sol { tasks, score: None };
242 let descriptor = SolutionDescriptor::new("Sol", TypeId::of::<Sol>());
243 SimpleScoreDirector::with_calculator(solution, descriptor, |_| SimpleScore::of(0))
244 }
245
246 #[test]
247 fn composite_applies_both_moves() {
248 let tasks = vec![Task {
249 x: Some(1),
250 y: Some(2),
251 }];
252 let mut director = create_director(tasks);
253
254 let move_x = ChangeMove::new(0, Some(5), get_x, set_x, "x", 0);
255 let move_y = ChangeMove::new(0, Some(10), get_y, set_y, "y", 0);
256 let composite = CompositeMove::new(move_x, move_y);
257
258 assert!(composite.is_doable(&director));
259 composite.do_move(&mut director);
260
261 assert_eq!(get_x(director.working_solution(), 0), Some(5));
262 assert_eq!(get_y(director.working_solution(), 0), Some(10));
263 }
264
265 #[test]
266 fn composite_undo_restores_both() {
267 let tasks = vec![Task {
268 x: Some(1),
269 y: Some(2),
270 }];
271 let mut director = create_director(tasks);
272
273 let move_x = ChangeMove::new(0, Some(5), get_x, set_x, "x", 0);
274 let move_y = ChangeMove::new(0, Some(10), get_y, set_y, "y", 0);
275 let composite = CompositeMove::new(move_x, move_y);
276
277 {
278 let mut recording = RecordingScoreDirector::new(&mut director);
279 composite.do_move(&mut recording);
280
281 assert_eq!(get_x(recording.working_solution(), 0), Some(5));
282 assert_eq!(get_y(recording.working_solution(), 0), Some(10));
283
284 recording.undo_changes();
285 }
286
287 assert_eq!(get_x(director.working_solution(), 0), Some(1));
289 assert_eq!(get_y(director.working_solution(), 0), Some(2));
290 }
291
292 #[test]
293 fn composite_not_doable_if_first_not_doable() {
294 let tasks = vec![Task {
295 x: Some(5),
296 y: Some(2),
297 }];
298 let director = create_director(tasks);
299
300 let move_x = ChangeMove::new(0, Some(5), get_x, set_x, "x", 0);
302 let move_y = ChangeMove::new(0, Some(10), get_y, set_y, "y", 0);
303 let composite = CompositeMove::new(move_x, move_y);
304
305 assert!(!composite.is_doable(&director));
306 }
307
308 #[test]
309 fn composite_not_doable_if_second_not_doable() {
310 let tasks = vec![Task {
311 x: Some(1),
312 y: Some(10),
313 }];
314 let director = create_director(tasks);
315
316 let move_x = ChangeMove::new(0, Some(5), get_x, set_x, "x", 0);
317 let move_y = ChangeMove::new(0, Some(10), get_y, set_y, "y", 0);
319 let composite = CompositeMove::new(move_x, move_y);
320
321 assert!(!composite.is_doable(&director));
322 }
323
324 #[test]
325 fn composite_combines_entity_indices() {
326 let move_x = ChangeMove::new(0, Some(5), get_x, set_x, "x", 0);
328 let move_y = ChangeMove::new(1, Some(10), get_y, set_y, "y", 0);
329 let composite = CompositeMove::new(move_x, move_y);
330
331 assert_eq!(composite.entity_indices(), &[0, 1]);
332 }
333
334 #[test]
335 fn composite_deduplicates_entity_indices() {
336 let move_x = ChangeMove::new(0, Some(5), get_x, set_x, "x", 0);
338 let move_y = ChangeMove::new(0, Some(10), get_y, set_y, "y", 0);
339 let composite = CompositeMove::new(move_x, move_y);
340
341 assert_eq!(composite.entity_indices(), &[0]);
343 }
344
345 #[test]
346 fn composite_uses_first_move_descriptor() {
347 let move_x = ChangeMove::new(0, Some(5), get_x, set_x, "x", 1);
348 let move_y = ChangeMove::new(0, Some(10), get_y, set_y, "y", 2);
349 let composite = CompositeMove::new(move_x, move_y);
350
351 assert_eq!(composite.descriptor_index(), 1);
352 assert_eq!(composite.variable_name(), "x");
353 }
354}