Skip to main content

solverforge_solver/phase/construction/
forager.rs

1/* Foragers for construction heuristic move selection
2
3Foragers determine which move to select from the candidates
4generated for each entity placement.
5
6# Zero-Erasure Design
7
8Foragers return indices into the placement's move Vec, not cloned moves.
9The caller takes ownership via the index.
10*/
11
12use std::fmt::Debug;
13use std::marker::PhantomData;
14
15use solverforge_core::domain::PlanningSolution;
16use solverforge_core::score::Score;
17use solverforge_scoring::{Director, RecordingDirector};
18
19use crate::heuristic::r#move::Move;
20
21use super::Placement;
22
23/// Trait for selecting a move during construction.
24///
25/// Foragers evaluate candidate moves and pick one based on their strategy.
26// Returns the index of the selected move, not a cloned move.
27///
28/// # Type Parameters
29/// * `S` - The planning solution type
30/// * `M` - The move type
31pub trait ConstructionForager<S, M>: Send + Debug
32where
33    S: PlanningSolution,
34    M: Move<S>,
35{
36    /* Picks a move index from the placement's candidates.
37
38    Returns None if no suitable move is found.
39    */
40    fn pick_move_index<D: Director<S>>(
41        &self,
42        placement: &Placement<S, M>,
43        score_director: &mut D,
44    ) -> Option<usize>;
45}
46
47/// First Fit forager - picks the first feasible move.
48///
49/// This is the fastest forager but may not produce optimal results.
50/// It simply takes the first move that can be executed.
51pub struct FirstFitForager<S, M> {
52    _phantom: PhantomData<fn() -> (S, M)>,
53}
54
55impl<S, M> Clone for FirstFitForager<S, M> {
56    fn clone(&self) -> Self {
57        *self
58    }
59}
60
61impl<S, M> Copy for FirstFitForager<S, M> {}
62
63impl<S, M> Default for FirstFitForager<S, M> {
64    fn default() -> Self {
65        Self::new()
66    }
67}
68
69impl<S, M> Debug for FirstFitForager<S, M> {
70    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
71        f.debug_struct("FirstFitForager").finish()
72    }
73}
74
75impl<S, M> FirstFitForager<S, M> {
76    pub fn new() -> Self {
77        Self {
78            _phantom: PhantomData,
79        }
80    }
81}
82
83impl<S, M> ConstructionForager<S, M> for FirstFitForager<S, M>
84where
85    S: PlanningSolution,
86    M: Move<S>,
87{
88    fn pick_move_index<D: Director<S>>(
89        &self,
90        placement: &Placement<S, M>,
91        score_director: &mut D,
92    ) -> Option<usize> {
93        for (idx, m) in placement.moves.iter().enumerate() {
94            if m.is_doable(score_director) {
95                return Some(idx);
96            }
97        }
98        None
99    }
100}
101
102/// Best Fit forager - evaluates all moves and picks the best.
103///
104/// This forager evaluates each candidate move by executing it,
105/// calculating the score, and undoing it. The move with the best
106/// score is selected.
107pub struct BestFitForager<S, M> {
108    _phantom: PhantomData<fn() -> (S, M)>,
109}
110
111impl<S, M> Clone for BestFitForager<S, M> {
112    fn clone(&self) -> Self {
113        *self
114    }
115}
116
117impl<S, M> Copy for BestFitForager<S, M> {}
118
119impl<S, M> Default for BestFitForager<S, M> {
120    fn default() -> Self {
121        Self::new()
122    }
123}
124
125impl<S, M> Debug for BestFitForager<S, M> {
126    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
127        f.debug_struct("BestFitForager").finish()
128    }
129}
130
131impl<S, M> BestFitForager<S, M> {
132    pub fn new() -> Self {
133        Self {
134            _phantom: PhantomData,
135        }
136    }
137}
138
139impl<S, M> ConstructionForager<S, M> for BestFitForager<S, M>
140where
141    S: PlanningSolution,
142    M: Move<S>,
143{
144    fn pick_move_index<D: Director<S>>(
145        &self,
146        placement: &Placement<S, M>,
147        score_director: &mut D,
148    ) -> Option<usize> {
149        let mut best_idx: Option<usize> = None;
150        let mut best_score: Option<S::Score> = None;
151
152        for (idx, m) in placement.moves.iter().enumerate() {
153            if !m.is_doable(score_director) {
154                continue;
155            }
156
157            // Use RecordingDirector for automatic undo
158            let score = {
159                let mut recording = RecordingDirector::new(score_director);
160
161                // Execute move
162                m.do_move(&mut recording);
163
164                // Evaluate
165                let score = recording.calculate_score();
166
167                // Undo move
168                recording.undo_changes();
169
170                score
171            };
172
173            // Check if this is the best so far
174            let is_better = match &best_score {
175                None => true,
176                Some(best) => score > *best,
177            };
178
179            if is_better {
180                best_idx = Some(idx);
181                best_score = Some(score);
182            }
183        }
184
185        best_idx
186    }
187}
188
189/// First Feasible forager - picks the first move that results in a feasible score.
190///
191/// This forager evaluates moves until it finds one that produces a feasible
192/// (non-negative hard score) solution.
193pub struct FirstFeasibleForager<S, M> {
194    _phantom: PhantomData<fn() -> (S, M)>,
195}
196
197impl<S, M> Clone for FirstFeasibleForager<S, M> {
198    fn clone(&self) -> Self {
199        *self
200    }
201}
202
203impl<S, M> Copy for FirstFeasibleForager<S, M> {}
204
205impl<S, M> Default for FirstFeasibleForager<S, M> {
206    fn default() -> Self {
207        Self::new()
208    }
209}
210
211impl<S, M> Debug for FirstFeasibleForager<S, M> {
212    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
213        f.debug_struct("FirstFeasibleForager").finish()
214    }
215}
216
217impl<S, M> FirstFeasibleForager<S, M> {
218    pub fn new() -> Self {
219        Self {
220            _phantom: PhantomData,
221        }
222    }
223}
224
225impl<S, M> ConstructionForager<S, M> for FirstFeasibleForager<S, M>
226where
227    S: PlanningSolution,
228    M: Move<S>,
229{
230    fn pick_move_index<D: Director<S>>(
231        &self,
232        placement: &Placement<S, M>,
233        score_director: &mut D,
234    ) -> Option<usize> {
235        let mut fallback_idx: Option<usize> = None;
236        let mut fallback_score: Option<S::Score> = None;
237
238        for (idx, m) in placement.moves.iter().enumerate() {
239            if !m.is_doable(score_director) {
240                continue;
241            }
242
243            // Use RecordingDirector for automatic undo
244            let score = {
245                let mut recording = RecordingDirector::new(score_director);
246
247                // Execute move
248                m.do_move(&mut recording);
249
250                // Evaluate
251                let score = recording.calculate_score();
252
253                // If feasible, return this move index immediately
254                if score.is_feasible() {
255                    recording.undo_changes();
256                    return Some(idx);
257                }
258
259                // Undo move
260                recording.undo_changes();
261
262                score
263            };
264
265            // Track best infeasible as fallback
266            let is_better = match &fallback_score {
267                None => true,
268                Some(best) => score > *best,
269            };
270
271            if is_better {
272                fallback_idx = Some(idx);
273                fallback_score = Some(score);
274            }
275        }
276
277        // No feasible move found, return best infeasible
278        fallback_idx
279    }
280}
281
282/// Weakest Fit forager - picks the move with the lowest strength value.
283///
284/// This forager evaluates each candidate move using a strength function
285/// and selects the move with the minimum strength. This is useful for
286/// assigning the "weakest" or least constraining values first.
287pub struct WeakestFitForager<S, M> {
288    // Function to evaluate strength of a move.
289    strength_fn: fn(&M) -> i64,
290    _phantom: PhantomData<fn() -> S>,
291}
292
293impl<S, M> Clone for WeakestFitForager<S, M> {
294    fn clone(&self) -> Self {
295        *self
296    }
297}
298
299impl<S, M> Copy for WeakestFitForager<S, M> {}
300
301impl<S, M> Debug for WeakestFitForager<S, M> {
302    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
303        f.debug_struct("WeakestFitForager").finish()
304    }
305}
306
307impl<S, M> WeakestFitForager<S, M> {
308    /// Creates a new Weakest Fit forager with the given strength function.
309    ///
310    /// The strength function evaluates how "strong" a move is. The forager
311    /// picks the move with the minimum strength value.
312    pub fn new(strength_fn: fn(&M) -> i64) -> Self {
313        Self {
314            strength_fn,
315            _phantom: PhantomData,
316        }
317    }
318}
319
320impl<S, M> ConstructionForager<S, M> for WeakestFitForager<S, M>
321where
322    S: PlanningSolution,
323    M: Move<S>,
324{
325    fn pick_move_index<D: Director<S>>(
326        &self,
327        placement: &Placement<S, M>,
328        score_director: &mut D,
329    ) -> Option<usize> {
330        let mut best_idx: Option<usize> = None;
331        let mut min_strength: Option<i64> = None;
332
333        for (idx, m) in placement.moves.iter().enumerate() {
334            if !m.is_doable(score_director) {
335                continue;
336            }
337
338            let strength = (self.strength_fn)(m);
339
340            let is_weaker = match min_strength {
341                None => true,
342                Some(best) => strength < best,
343            };
344
345            if is_weaker {
346                best_idx = Some(idx);
347                min_strength = Some(strength);
348            }
349        }
350
351        best_idx
352    }
353}
354
355/// Strongest Fit forager - picks the move with the highest strength value.
356///
357/// This forager evaluates each candidate move using a strength function
358/// and selects the move with the maximum strength. This is useful for
359/// assigning the "strongest" or most constraining values first.
360pub struct StrongestFitForager<S, M> {
361    // Function to evaluate strength of a move.
362    strength_fn: fn(&M) -> i64,
363    _phantom: PhantomData<fn() -> S>,
364}
365
366impl<S, M> Clone for StrongestFitForager<S, M> {
367    fn clone(&self) -> Self {
368        *self
369    }
370}
371
372impl<S, M> Copy for StrongestFitForager<S, M> {}
373
374impl<S, M> Debug for StrongestFitForager<S, M> {
375    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
376        f.debug_struct("StrongestFitForager").finish()
377    }
378}
379
380impl<S, M> StrongestFitForager<S, M> {
381    /// Creates a new Strongest Fit forager with the given strength function.
382    ///
383    /// The strength function evaluates how "strong" a move is. The forager
384    /// picks the move with the maximum strength value.
385    pub fn new(strength_fn: fn(&M) -> i64) -> Self {
386        Self {
387            strength_fn,
388            _phantom: PhantomData,
389        }
390    }
391}
392
393impl<S, M> ConstructionForager<S, M> for StrongestFitForager<S, M>
394where
395    S: PlanningSolution,
396    M: Move<S>,
397{
398    fn pick_move_index<D: Director<S>>(
399        &self,
400        placement: &Placement<S, M>,
401        score_director: &mut D,
402    ) -> Option<usize> {
403        let mut best_idx: Option<usize> = None;
404        let mut max_strength: Option<i64> = None;
405
406        for (idx, m) in placement.moves.iter().enumerate() {
407            if !m.is_doable(score_director) {
408                continue;
409            }
410
411            let strength = (self.strength_fn)(m);
412
413            let is_stronger = match max_strength {
414                None => true,
415                Some(best) => strength > best,
416            };
417
418            if is_stronger {
419                best_idx = Some(idx);
420                max_strength = Some(strength);
421            }
422        }
423
424        best_idx
425    }
426}
427
428#[cfg(test)]
429#[path = "forager_tests.rs"]
430mod tests;