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, &S) -> 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, &S) -> i64) -> Self {
298        Self {
299            strength_fn,
300            _phantom: PhantomData,
301        }
302    }
303
304    pub(crate) fn strength(&self, mov: &M, solution: &S) -> i64 {
305        (self.strength_fn)(mov, solution)
306    }
307}
308
309impl<S, M> ConstructionForager<S, M> for WeakestFitForager<S, M>
310where
311    S: PlanningSolution,
312    S::Score: Score,
313    M: Move<S>,
314{
315    fn pick_move_index<D: Director<S>>(
316        &self,
317        placement: &Placement<S, M>,
318        score_director: &mut D,
319    ) -> ConstructionChoice {
320        let mut best_idx: Option<usize> = None;
321        let mut min_strength: Option<i64> = None;
322
323        for (idx, m) in placement.moves.iter().enumerate() {
324            if !m.is_doable(score_director) {
325                continue;
326            }
327
328            let strength = self.strength(m, score_director.working_solution());
329
330            let is_weaker = match min_strength {
331                None => true,
332                Some(best) => strength < best,
333            };
334
335            if is_weaker {
336                best_idx = Some(idx);
337                min_strength = Some(strength);
338            }
339        }
340
341        let Some(best_idx) = best_idx else {
342            return ConstructionChoice::KeepCurrent;
343        };
344
345        if !placement.keep_current_legal() {
346            return ConstructionChoice::Select(best_idx);
347        }
348
349        let baseline_score = score_director.calculate_score();
350        let candidate_score = evaluate_trial_move(score_director, &placement.moves[best_idx]);
351        if candidate_score > baseline_score {
352            ConstructionChoice::Select(best_idx)
353        } else {
354            ConstructionChoice::KeepCurrent
355        }
356    }
357}
358
359/// Strongest Fit forager - picks the move with the highest strength value.
360///
361/// This forager evaluates each candidate move using a strength function
362/// and selects the move with the maximum strength. This is useful for
363/// assigning the "strongest" or most constraining values first.
364pub struct StrongestFitForager<S, M> {
365    // Function to evaluate strength of a move.
366    strength_fn: fn(&M, &S) -> i64,
367    _phantom: PhantomData<fn() -> S>,
368}
369
370impl<S, M> Clone for StrongestFitForager<S, M> {
371    fn clone(&self) -> Self {
372        *self
373    }
374}
375
376impl<S, M> Copy for StrongestFitForager<S, M> {}
377
378impl<S, M> Debug for StrongestFitForager<S, M> {
379    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
380        f.debug_struct("StrongestFitForager").finish()
381    }
382}
383
384impl<S, M> StrongestFitForager<S, M> {
385    /// Creates a new Strongest Fit forager with the given strength function.
386    ///
387    /// The strength function evaluates how "strong" a move is. The forager
388    /// picks the move with the maximum strength value.
389    pub fn new(strength_fn: fn(&M, &S) -> i64) -> Self {
390        Self {
391            strength_fn,
392            _phantom: PhantomData,
393        }
394    }
395
396    pub(crate) fn strength(&self, mov: &M, solution: &S) -> i64 {
397        (self.strength_fn)(mov, solution)
398    }
399}
400
401impl<S, M> ConstructionForager<S, M> for StrongestFitForager<S, M>
402where
403    S: PlanningSolution,
404    S::Score: Score,
405    M: Move<S>,
406{
407    fn pick_move_index<D: Director<S>>(
408        &self,
409        placement: &Placement<S, M>,
410        score_director: &mut D,
411    ) -> ConstructionChoice {
412        let mut best_idx: Option<usize> = None;
413        let mut max_strength: Option<i64> = None;
414
415        for (idx, m) in placement.moves.iter().enumerate() {
416            if !m.is_doable(score_director) {
417                continue;
418            }
419
420            let strength = self.strength(m, score_director.working_solution());
421
422            let is_stronger = match max_strength {
423                None => true,
424                Some(best) => strength > best,
425            };
426
427            if is_stronger {
428                best_idx = Some(idx);
429                max_strength = Some(strength);
430            }
431        }
432
433        let Some(best_idx) = best_idx else {
434            return ConstructionChoice::KeepCurrent;
435        };
436
437        if !placement.keep_current_legal() {
438            return ConstructionChoice::Select(best_idx);
439        }
440
441        let baseline_score = score_director.calculate_score();
442        let candidate_score = evaluate_trial_move(score_director, &placement.moves[best_idx]);
443        if candidate_score > baseline_score {
444            ConstructionChoice::Select(best_idx)
445        } else {
446            ConstructionChoice::KeepCurrent
447        }
448    }
449}
450
451#[cfg(test)]
452mod tests;