solverforge_solver/phase/localsearch/
forager.rs

1//! Foragers for local search move selection
2//!
3//! Foragers collect accepted moves during a step and select the
4//! best one to apply.
5
6use std::fmt::Debug;
7use std::marker::PhantomData;
8
9use solverforge_core::domain::PlanningSolution;
10
11use crate::heuristic::r#move::Move;
12
13/// Trait for collecting and selecting moves in local search.
14///
15/// Foragers are responsible for:
16/// - Collecting accepted moves during move evaluation
17/// - Deciding when to quit evaluating early
18/// - Selecting the best move to apply
19///
20/// # Type Parameters
21/// * `S` - The planning solution type
22/// * `M` - The move type
23pub trait LocalSearchForager<S: PlanningSolution, M: Move<S>>: Send + Debug {
24    /// Called at the start of each step to reset state.
25    fn step_started(&mut self);
26
27    /// Adds an accepted move to the forager.
28    fn add_move(&mut self, m: M, score: S::Score);
29
30    /// Returns true if the forager has collected enough moves and
31    /// wants to stop evaluating more.
32    fn is_quit_early(&self) -> bool;
33
34    /// Picks the best move from those collected.
35    ///
36    /// Returns None if no moves were accepted.
37    fn pick_move(&mut self) -> Option<(M, S::Score)>;
38}
39
40/// A forager that collects a limited number of accepted moves.
41///
42/// Once the limit is reached, it quits early. It picks the best
43/// move among those collected.
44pub struct AcceptedCountForager<S: PlanningSolution, M> {
45    /// Maximum number of accepted moves to collect.
46    accepted_count_limit: usize,
47    /// Collected moves with their scores.
48    accepted_moves: Vec<(M, S::Score)>,
49    _phantom: PhantomData<S>,
50}
51
52impl<S: PlanningSolution, M> AcceptedCountForager<S, M> {
53    /// Creates a new forager with the given limit.
54    ///
55    /// # Arguments
56    /// * `accepted_count_limit` - Stop after collecting this many accepted moves
57    pub fn new(accepted_count_limit: usize) -> Self {
58        Self {
59            accepted_count_limit,
60            accepted_moves: Vec::new(),
61            _phantom: PhantomData,
62        }
63    }
64}
65
66impl<S: PlanningSolution, M> Debug for AcceptedCountForager<S, M> {
67    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
68        f.debug_struct("AcceptedCountForager")
69            .field("accepted_count_limit", &self.accepted_count_limit)
70            .field("accepted_count", &self.accepted_moves.len())
71            .finish()
72    }
73}
74
75impl<S: PlanningSolution, M: Move<S>> LocalSearchForager<S, M> for AcceptedCountForager<S, M> {
76    fn step_started(&mut self) {
77        self.accepted_moves.clear();
78    }
79
80    fn add_move(&mut self, m: M, score: S::Score) {
81        self.accepted_moves.push((m, score));
82    }
83
84    fn is_quit_early(&self) -> bool {
85        self.accepted_moves.len() >= self.accepted_count_limit
86    }
87
88    fn pick_move(&mut self) -> Option<(M, S::Score)> {
89        if self.accepted_moves.is_empty() {
90            return None;
91        }
92
93        // Find the best move (highest score)
94        let mut best_index = 0;
95        let mut best_score = &self.accepted_moves[0].1;
96
97        for (i, (_, score)) in self.accepted_moves.iter().enumerate().skip(1) {
98            if score > best_score {
99                best_index = i;
100                best_score = score;
101            }
102        }
103
104        // Remove and return the best move
105        Some(self.accepted_moves.swap_remove(best_index))
106    }
107}
108
109/// A forager that picks the first accepted move.
110///
111/// This is the simplest forager - it quits after the first accepted move.
112pub struct FirstAcceptedForager<S: PlanningSolution, M> {
113    /// The first accepted move.
114    accepted_move: Option<(M, S::Score)>,
115    _phantom: PhantomData<S>,
116}
117
118impl<S: PlanningSolution, M> Debug for FirstAcceptedForager<S, M> {
119    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
120        f.debug_struct("FirstAcceptedForager")
121            .field("has_move", &self.accepted_move.is_some())
122            .finish()
123    }
124}
125
126impl<S: PlanningSolution, M> FirstAcceptedForager<S, M> {
127    /// Creates a new first-accepted forager.
128    pub fn new() -> Self {
129        Self {
130            accepted_move: None,
131            _phantom: PhantomData,
132        }
133    }
134}
135
136impl<S: PlanningSolution, M> Default for FirstAcceptedForager<S, M> {
137    fn default() -> Self {
138        Self::new()
139    }
140}
141
142impl<S: PlanningSolution, M: Move<S>> LocalSearchForager<S, M> for FirstAcceptedForager<S, M> {
143    fn step_started(&mut self) {
144        self.accepted_move = None;
145    }
146
147    fn add_move(&mut self, m: M, score: S::Score) {
148        if self.accepted_move.is_none() {
149            self.accepted_move = Some((m, score));
150        }
151    }
152
153    fn is_quit_early(&self) -> bool {
154        self.accepted_move.is_some()
155    }
156
157    fn pick_move(&mut self) -> Option<(M, S::Score)> {
158        self.accepted_move.take()
159    }
160}
161
162#[cfg(test)]
163mod tests {
164    use super::*;
165    use crate::heuristic::r#move::ChangeMove;
166    use solverforge_core::score::SimpleScore;
167
168    #[derive(Clone, Debug)]
169    struct DummySolution {
170        values: Vec<Option<i32>>,
171        score: Option<SimpleScore>,
172    }
173
174    impl PlanningSolution for DummySolution {
175        type Score = SimpleScore;
176
177        fn score(&self) -> Option<Self::Score> {
178            self.score
179        }
180
181        fn set_score(&mut self, score: Option<Self::Score>) {
182            self.score = score;
183        }
184    }
185
186    // Typed getter - zero erasure
187    fn get_value(s: &DummySolution, idx: usize) -> Option<i32> {
188        s.values.get(idx).copied().flatten()
189    }
190
191    // Typed setter - zero erasure
192    fn set_value(s: &mut DummySolution, idx: usize, v: Option<i32>) {
193        if let Some(val) = s.values.get_mut(idx) {
194            *val = v;
195        }
196    }
197
198    type TestMove = ChangeMove<DummySolution, i32>;
199
200    fn create_move(value: i32) -> TestMove {
201        ChangeMove::new(0, Some(value), get_value, set_value, "test", 0)
202    }
203
204    #[test]
205    fn test_accepted_count_forager_collects_moves() {
206        let mut forager = AcceptedCountForager::<DummySolution, TestMove>::new(3);
207        forager.step_started();
208
209        forager.add_move(create_move(1), SimpleScore::of(-10));
210        assert!(!forager.is_quit_early());
211
212        forager.add_move(create_move(2), SimpleScore::of(-5));
213        assert!(!forager.is_quit_early());
214
215        forager.add_move(create_move(3), SimpleScore::of(-8));
216        assert!(forager.is_quit_early());
217    }
218
219    #[test]
220    fn test_accepted_count_forager_picks_best() {
221        let mut forager = AcceptedCountForager::<DummySolution, TestMove>::new(10);
222        forager.step_started();
223
224        forager.add_move(create_move(1), SimpleScore::of(-10));
225        forager.add_move(create_move(2), SimpleScore::of(-5)); // Best
226        forager.add_move(create_move(3), SimpleScore::of(-8));
227
228        let (_, score) = forager.pick_move().unwrap();
229        assert_eq!(score, SimpleScore::of(-5));
230    }
231
232    #[test]
233    fn test_accepted_count_forager_empty() {
234        let mut forager = AcceptedCountForager::<DummySolution, TestMove>::new(3);
235        forager.step_started();
236
237        assert!(forager.pick_move().is_none());
238    }
239
240    #[test]
241    fn test_first_accepted_forager() {
242        let mut forager = FirstAcceptedForager::<DummySolution, TestMove>::new();
243        forager.step_started();
244
245        assert!(!forager.is_quit_early());
246
247        forager.add_move(create_move(1), SimpleScore::of(-10));
248        assert!(forager.is_quit_early());
249
250        // Second move should be ignored
251        forager.add_move(create_move(2), SimpleScore::of(-5));
252
253        let (_, score) = forager.pick_move().unwrap();
254        // Should get the first one, not the better second one
255        assert_eq!(score, SimpleScore::of(-10));
256    }
257
258    #[test]
259    fn test_forager_resets_on_step() {
260        let mut forager = AcceptedCountForager::<DummySolution, TestMove>::new(3);
261
262        forager.step_started();
263        forager.add_move(create_move(1), SimpleScore::of(-10));
264
265        forager.step_started();
266        // After reset, should be empty
267        assert!(forager.pick_move().is_none());
268    }
269}