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