solverforge_solver/phase/vnd/
phase.rs

1//! Variable Neighborhood Descent phase implementation.
2
3use std::fmt::Debug;
4use std::marker::PhantomData;
5
6use solverforge_core::domain::PlanningSolution;
7use solverforge_scoring::{RecordingScoreDirector, ScoreDirector};
8
9use crate::heuristic::r#move::{Move, MoveArena};
10use crate::heuristic::selector::MoveSelector;
11use crate::phase::Phase;
12use crate::scope::{PhaseScope, SolverScope, StepScope};
13
14/// Variable Neighborhood Descent phase.
15///
16/// Wraps a tuple of move selectors (neighborhoods) and explores them in sequence,
17/// restarting from the first whenever an improvement is found.
18///
19/// Uses macro-generated tuple implementations for zero type erasure.
20///
21/// # Type Parameters
22/// * `T` - Tuple of move selectors
23/// * `M` - The move type produced by all selectors
24///
25/// # Example
26///
27/// ```
28/// use solverforge_solver::phase::vnd::VndPhase;
29/// use solverforge_solver::heuristic::r#move::ChangeMove;
30/// use solverforge_solver::heuristic::selector::ChangeMoveSelector;
31/// use solverforge_core::domain::PlanningSolution;
32/// use solverforge_core::score::SimpleScore;
33///
34/// #[derive(Clone, Debug)]
35/// struct MySolution {
36///     values: Vec<Option<i32>>,
37///     score: Option<SimpleScore>,
38/// }
39///
40/// impl PlanningSolution for MySolution {
41///     type Score = SimpleScore;
42///     fn score(&self) -> Option<Self::Score> { self.score }
43///     fn set_score(&mut self, score: Option<Self::Score>) { self.score = score; }
44/// }
45///
46/// fn get_value(s: &MySolution, idx: usize) -> Option<i32> {
47///     s.values.get(idx).copied().flatten()
48/// }
49/// fn set_value(s: &mut MySolution, idx: usize, v: Option<i32>) {
50///     if let Some(slot) = s.values.get_mut(idx) { *slot = v; }
51/// }
52///
53/// type MyMove = ChangeMove<MySolution, i32>;
54///
55/// let selector = ChangeMoveSelector::simple(
56///     get_value, set_value, 0, "value", vec![1, 2, 3]
57/// );
58///
59/// // Single neighborhood VND
60/// let vnd: VndPhase<_, MyMove> = VndPhase::new((selector,));
61/// ```
62pub struct VndPhase<T, M>(pub T, PhantomData<M>);
63
64impl<T: Debug, M> Debug for VndPhase<T, M> {
65    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
66        f.debug_tuple("VndPhase").field(&self.0).finish()
67    }
68}
69
70impl<T, M> VndPhase<T, M> {
71    /// Creates a new VND phase from a tuple of move selectors.
72    pub fn new(neighborhoods: T) -> Self {
73        Self(neighborhoods, PhantomData)
74    }
75}
76
77/// Generates `Phase` implementations for VndPhase with tuple neighborhoods.
78macro_rules! impl_vnd_phase {
79    // Single neighborhood
80    ($idx:tt: $MS:ident) => {
81        impl<S, D, M, $MS> Phase<S, D> for VndPhase<($MS,), M>
82        where
83            S: PlanningSolution,
84            D: ScoreDirector<S>,
85            M: Move<S>,
86            $MS: MoveSelector<S, M>,
87        {
88            fn solve(&mut self, solver_scope: &mut SolverScope<S, D>) {
89                let mut arena = MoveArena::<M>::new();
90                let mut phase_scope = PhaseScope::new(solver_scope, 0);
91                let mut current_score = phase_scope.calculate_score();
92
93                loop {
94                    let mut step_scope = StepScope::new(&mut phase_scope);
95                    arena.reset();
96                    arena.extend((self.0).$idx.iter_moves(step_scope.score_director()));
97
98                    let best_index = find_best_improving_move_index(&arena, &mut step_scope, &current_score);
99
100                    if let Some((selected_index, selected_score)) = best_index {
101                        let selected_move = arena.take(selected_index);
102                        selected_move.do_move(step_scope.score_director_mut());
103                        step_scope.set_step_score(selected_score);
104                        current_score = selected_score;
105                        step_scope.phase_scope_mut().update_best_solution();
106                        step_scope.complete();
107                    } else {
108                        step_scope.complete();
109                        break;
110                    }
111                }
112            }
113
114            fn phase_type_name(&self) -> &'static str {
115                "VariableNeighborhoodDescent"
116            }
117        }
118    };
119
120    // Multiple neighborhoods
121    ($($idx:tt: $MS:ident),+) => {
122        impl<S, D, M, $($MS),+> Phase<S, D> for VndPhase<($($MS,)+), M>
123        where
124            S: PlanningSolution,
125            D: ScoreDirector<S>,
126            M: Move<S>,
127            $($MS: MoveSelector<S, M>,)+
128        {
129            fn solve(&mut self, solver_scope: &mut SolverScope<S, D>) {
130                const COUNT: usize = impl_vnd_phase!(@count $($idx),+);
131                let mut arena = MoveArena::<M>::new();
132                let mut phase_scope = PhaseScope::new(solver_scope, 0);
133                let mut current_score = phase_scope.calculate_score();
134                let mut k = 0usize;
135
136                while k < COUNT {
137                    let mut step_scope = StepScope::new(&mut phase_scope);
138                    arena.reset();
139
140                    // Populate arena from neighborhood k
141                    match k {
142                        $($idx => arena.extend((self.0).$idx.iter_moves(step_scope.score_director())),)+
143                        _ => {}
144                    }
145
146                    let best_index = find_best_improving_move_index(&arena, &mut step_scope, &current_score);
147
148                    if let Some((selected_index, selected_score)) = best_index {
149                        let selected_move = arena.take(selected_index);
150                        selected_move.do_move(step_scope.score_director_mut());
151                        step_scope.set_step_score(selected_score);
152                        current_score = selected_score;
153                        step_scope.phase_scope_mut().update_best_solution();
154                        step_scope.complete();
155                        k = 0; // Restart from first neighborhood
156                    } else {
157                        step_scope.complete();
158                        k += 1; // Try next neighborhood
159                    }
160                }
161            }
162
163            fn phase_type_name(&self) -> &'static str {
164                "VariableNeighborhoodDescent"
165            }
166        }
167    };
168
169    // Helper: count tuple elements
170    (@count $($idx:tt),+) => {
171        0 $(+ { let _ = $idx; 1 })+
172    };
173}
174
175/// Finds the index of the best improving move in the arena.
176///
177/// Returns `Some((index, score))` if an improving move is found, `None` otherwise.
178fn find_best_improving_move_index<S, D, M>(
179    arena: &MoveArena<M>,
180    step_scope: &mut StepScope<'_, '_, '_, S, D>,
181    current_score: &S::Score,
182) -> Option<(usize, S::Score)>
183where
184    S: PlanningSolution,
185    D: ScoreDirector<S>,
186    M: Move<S>,
187{
188    let mut best: Option<(usize, S::Score)> = None;
189
190    for i in 0..arena.len() {
191        let m = arena.get(i).unwrap();
192
193        if !m.is_doable(step_scope.score_director()) {
194            continue;
195        }
196
197        let mut recording = RecordingScoreDirector::new(step_scope.score_director_mut());
198        m.do_move(&mut recording);
199        let move_score = recording.calculate_score();
200        recording.undo_changes();
201
202        if move_score > *current_score {
203            match &best {
204                Some((_, best_score)) if move_score > *best_score => {
205                    best = Some((i, move_score));
206                }
207                None => {
208                    best = Some((i, move_score));
209                }
210                _ => {}
211            }
212        }
213    }
214
215    best
216}
217
218impl_vnd_phase!(0: MS0);
219impl_vnd_phase!(0: MS0, 1: MS1);
220impl_vnd_phase!(0: MS0, 1: MS1, 2: MS2);
221impl_vnd_phase!(0: MS0, 1: MS1, 2: MS2, 3: MS3);
222impl_vnd_phase!(0: MS0, 1: MS1, 2: MS2, 3: MS3, 4: MS4);
223impl_vnd_phase!(0: MS0, 1: MS1, 2: MS2, 3: MS3, 4: MS4, 5: MS5);
224impl_vnd_phase!(0: MS0, 1: MS1, 2: MS2, 3: MS3, 4: MS4, 5: MS5, 6: MS6);
225impl_vnd_phase!(0: MS0, 1: MS1, 2: MS2, 3: MS3, 4: MS4, 5: MS5, 6: MS6, 7: MS7);
226
227#[cfg(test)]
228mod tests {
229    use super::*;
230    use crate::heuristic::r#move::ChangeMove;
231    use crate::heuristic::selector::ChangeMoveSelector;
232    use solverforge_core::domain::{EntityDescriptor, SolutionDescriptor, TypedEntityExtractor};
233    use solverforge_core::score::SimpleScore;
234    use solverforge_scoring::SimpleScoreDirector;
235    use std::any::TypeId;
236
237    #[derive(Clone, Debug)]
238    struct Queen {
239        column: i32,
240        row: Option<i32>,
241    }
242
243    #[derive(Clone, Debug)]
244    struct NQueensSolution {
245        queens: Vec<Queen>,
246        score: Option<SimpleScore>,
247    }
248
249    impl PlanningSolution for NQueensSolution {
250        type Score = SimpleScore;
251
252        fn score(&self) -> Option<Self::Score> {
253            self.score
254        }
255
256        fn set_score(&mut self, score: Option<Self::Score>) {
257            self.score = score;
258        }
259    }
260
261    fn get_queens(s: &NQueensSolution) -> &Vec<Queen> {
262        &s.queens
263    }
264    fn get_queens_mut(s: &mut NQueensSolution) -> &mut Vec<Queen> {
265        &mut s.queens
266    }
267
268    fn get_queen_row(s: &NQueensSolution, idx: usize) -> Option<i32> {
269        s.queens.get(idx).and_then(|q| q.row)
270    }
271
272    fn set_queen_row(s: &mut NQueensSolution, idx: usize, v: Option<i32>) {
273        if let Some(queen) = s.queens.get_mut(idx) {
274            queen.row = v;
275        }
276    }
277
278    fn calculate_conflicts(solution: &NQueensSolution) -> SimpleScore {
279        let mut conflicts = 0i64;
280
281        for (i, q1) in solution.queens.iter().enumerate() {
282            if let Some(row1) = q1.row {
283                for q2 in solution.queens.iter().skip(i + 1) {
284                    if let Some(row2) = q2.row {
285                        if row1 == row2 {
286                            conflicts += 1;
287                        }
288                        let col_diff = (q2.column - q1.column).abs();
289                        let row_diff = (row2 - row1).abs();
290                        if col_diff == row_diff {
291                            conflicts += 1;
292                        }
293                    }
294                }
295            }
296        }
297
298        SimpleScore::of(-conflicts)
299    }
300
301    fn create_director(
302        rows: &[i32],
303    ) -> SimpleScoreDirector<NQueensSolution, impl Fn(&NQueensSolution) -> SimpleScore> {
304        let queens: Vec<_> = rows
305            .iter()
306            .enumerate()
307            .map(|(col, &row)| Queen {
308                column: col as i32,
309                row: Some(row),
310            })
311            .collect();
312
313        let solution = NQueensSolution {
314            queens,
315            score: None,
316        };
317
318        let extractor = Box::new(TypedEntityExtractor::new(
319            "Queen",
320            "queens",
321            get_queens,
322            get_queens_mut,
323        ));
324        let entity_desc = EntityDescriptor::new("Queen", TypeId::of::<Queen>(), "queens")
325            .with_extractor(extractor);
326
327        let descriptor =
328            SolutionDescriptor::new("NQueensSolution", TypeId::of::<NQueensSolution>())
329                .with_entity(entity_desc);
330
331        SimpleScoreDirector::with_calculator(solution, descriptor, calculate_conflicts)
332    }
333
334    type NQueensMove = ChangeMove<NQueensSolution, i32>;
335
336    fn create_move_selector(
337        values: Vec<i32>,
338    ) -> ChangeMoveSelector<
339        NQueensSolution,
340        i32,
341        crate::heuristic::selector::FromSolutionEntitySelector,
342        crate::heuristic::selector::StaticTypedValueSelector<NQueensSolution, i32>,
343    > {
344        ChangeMoveSelector::simple(get_queen_row, set_queen_row, 0, "row", values)
345    }
346
347    #[test]
348    fn test_vnd_improves_solution() {
349        let director = create_director(&[0, 0, 0, 0]);
350        let mut solver_scope = SolverScope::new(director);
351
352        let initial_score = solver_scope.calculate_score();
353        assert!(initial_score < SimpleScore::of(0));
354
355        let values: Vec<i32> = (0..4).collect();
356        let mut phase: VndPhase<_, NQueensMove> = VndPhase::new((
357            create_move_selector(values.clone()),
358            create_move_selector(values),
359        ));
360
361        phase.solve(&mut solver_scope);
362
363        let final_score = solver_scope.best_score().copied().unwrap_or(initial_score);
364        assert!(final_score >= initial_score);
365    }
366
367    #[test]
368    fn test_vnd_single_neighborhood() {
369        let director = create_director(&[0, 0, 0, 0]);
370        let mut solver_scope = SolverScope::new(director);
371
372        let initial_score = solver_scope.calculate_score();
373
374        let values: Vec<i32> = (0..4).collect();
375        let mut phase: VndPhase<_, NQueensMove> = VndPhase::new((create_move_selector(values),));
376
377        phase.solve(&mut solver_scope);
378
379        let final_score = solver_scope.best_score().copied().unwrap_or(initial_score);
380        assert!(final_score >= initial_score);
381    }
382}