solverforge_solver/phase/construction/
forager.rs

1//! Foragers for construction heuristic move selection
2//!
3//! Foragers determine which move to select from the candidates
4//! generated for each entity placement.
5
6use std::fmt::Debug;
7use std::marker::PhantomData;
8
9use solverforge_core::domain::PlanningSolution;
10use solverforge_core::score::Score;
11use solverforge_scoring::{RecordingScoreDirector, ScoreDirector};
12
13use crate::heuristic::r#move::Move;
14
15use super::Placement;
16
17/// Trait for selecting a move during construction.
18///
19/// Foragers evaluate candidate moves and pick one based on their strategy.
20///
21/// # Type Parameters
22/// * `S` - The planning solution type
23/// * `M` - The move type
24pub trait ConstructionForager<S, M>: Send + Debug
25where
26    S: PlanningSolution,
27    M: Move<S>,
28{
29    /// Picks a move from the placement's candidates.
30    ///
31    /// Returns None if no suitable move is found.
32    fn pick_move(
33        &self,
34        placement: &Placement<S, M>,
35        score_director: &mut dyn ScoreDirector<S>,
36    ) -> Option<M>;
37}
38
39/// First Fit forager - picks the first feasible move.
40///
41/// This is the fastest forager but may not produce optimal results.
42/// It simply takes the first move that can be executed.
43#[derive(Clone, Default)]
44pub struct FirstFitForager<S, M> {
45    _phantom: PhantomData<(S, M)>,
46}
47
48impl<S, M> Debug for FirstFitForager<S, M> {
49    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
50        f.debug_struct("FirstFitForager").finish()
51    }
52}
53
54impl<S, M> FirstFitForager<S, M> {
55    /// Creates a new First Fit forager.
56    pub fn new() -> Self {
57        Self {
58            _phantom: PhantomData,
59        }
60    }
61}
62
63impl<S, M> ConstructionForager<S, M> for FirstFitForager<S, M>
64where
65    S: PlanningSolution,
66    M: Move<S>,
67{
68    fn pick_move(
69        &self,
70        placement: &Placement<S, M>,
71        score_director: &mut dyn ScoreDirector<S>,
72    ) -> Option<M> {
73        // Return the first doable move
74        for m in &placement.moves {
75            if m.is_doable(score_director) {
76                return Some(m.clone());
77            }
78        }
79        None
80    }
81}
82
83/// Best Fit forager - evaluates all moves and picks the best.
84///
85/// This forager evaluates each candidate move by executing it,
86/// calculating the score, and undoing it. The move with the best
87/// score is selected.
88#[derive(Clone, Default)]
89pub struct BestFitForager<S, M> {
90    _phantom: PhantomData<(S, M)>,
91}
92
93impl<S, M> Debug for BestFitForager<S, M> {
94    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
95        f.debug_struct("BestFitForager").finish()
96    }
97}
98
99impl<S, M> BestFitForager<S, M> {
100    /// Creates a new Best Fit forager.
101    pub fn new() -> Self {
102        Self {
103            _phantom: PhantomData,
104        }
105    }
106}
107
108impl<S, M> ConstructionForager<S, M> for BestFitForager<S, M>
109where
110    S: PlanningSolution,
111    M: Move<S>,
112{
113    fn pick_move(
114        &self,
115        placement: &Placement<S, M>,
116        score_director: &mut dyn ScoreDirector<S>,
117    ) -> Option<M> {
118        let mut best_move: Option<M> = None;
119        let mut best_score: Option<S::Score> = None;
120
121        for m in &placement.moves {
122            if !m.is_doable(score_director) {
123                continue;
124            }
125
126            // Use RecordingScoreDirector for automatic undo
127            let score = {
128                let mut recording = RecordingScoreDirector::new(score_director);
129
130                // Execute move
131                m.do_move(&mut recording);
132
133                // Evaluate
134                let score = recording.calculate_score();
135
136                // Undo move
137                recording.undo_changes();
138
139                score
140            };
141
142            // Check if this is the best so far
143            let is_better = match &best_score {
144                None => true,
145                Some(best) => score > *best,
146            };
147
148            if is_better {
149                best_move = Some(m.clone());
150                best_score = Some(score);
151            }
152        }
153
154        best_move
155    }
156}
157
158/// First Feasible forager - picks the first move that results in a feasible score.
159///
160/// This forager evaluates moves until it finds one that produces a feasible
161/// (non-negative hard score) solution.
162#[derive(Clone, Default)]
163pub struct FirstFeasibleForager<S, M> {
164    _phantom: PhantomData<(S, M)>,
165}
166
167impl<S, M> Debug for FirstFeasibleForager<S, M> {
168    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
169        f.debug_struct("FirstFeasibleForager").finish()
170    }
171}
172
173impl<S, M> FirstFeasibleForager<S, M> {
174    /// Creates a new First Feasible forager.
175    pub fn new() -> Self {
176        Self {
177            _phantom: PhantomData,
178        }
179    }
180}
181
182impl<S, M> ConstructionForager<S, M> for FirstFeasibleForager<S, M>
183where
184    S: PlanningSolution,
185    M: Move<S>,
186{
187    fn pick_move(
188        &self,
189        placement: &Placement<S, M>,
190        score_director: &mut dyn ScoreDirector<S>,
191    ) -> Option<M> {
192        let mut fallback_move: Option<M> = None;
193        let mut fallback_score: Option<S::Score> = None;
194
195        for m in &placement.moves {
196            if !m.is_doable(score_director) {
197                continue;
198            }
199
200            // Use RecordingScoreDirector for automatic undo
201            let score = {
202                let mut recording = RecordingScoreDirector::new(score_director);
203
204                // Execute move
205                m.do_move(&mut recording);
206
207                // Evaluate
208                let score = recording.calculate_score();
209
210                // If feasible, return this move immediately
211                if score.is_feasible() {
212                    recording.undo_changes();
213                    return Some(m.clone());
214                }
215
216                // Undo move
217                recording.undo_changes();
218
219                score
220            };
221
222            // Track best infeasible as fallback
223            let is_better = match &fallback_score {
224                None => true,
225                Some(best) => score > *best,
226            };
227
228            if is_better {
229                fallback_move = Some(m.clone());
230                fallback_score = Some(score);
231            }
232        }
233
234        // No feasible move found, return best infeasible
235        fallback_move
236    }
237}
238
239/// Weakest Fit forager - picks the move with the lowest strength value.
240///
241/// This forager evaluates each candidate move using a strength function
242/// and selects the move with the minimum strength. This is useful for
243/// assigning the "weakest" or least constraining values first.
244///
245/// # Example
246///
247/// ```
248/// use solverforge_solver::phase::construction::{WeakestFitForager, ConstructionForager};
249/// use solverforge_solver::heuristic::r#move::ChangeMove;
250/// use solverforge_core::domain::PlanningSolution;
251/// use solverforge_core::score::SimpleScore;
252///
253/// #[derive(Clone, Debug)]
254/// struct Task { priority: Option<i32> }
255///
256/// #[derive(Clone, Debug)]
257/// struct Solution { tasks: Vec<Task>, score: Option<SimpleScore> }
258///
259/// impl PlanningSolution for Solution {
260///     type Score = SimpleScore;
261///     fn score(&self) -> Option<Self::Score> { self.score }
262///     fn set_score(&mut self, score: Option<Self::Score>) { self.score = score; }
263/// }
264///
265/// // Strength function: priority value (lower = weaker)
266/// fn priority_strength(m: &ChangeMove<Solution, i32>) -> i64 {
267///     m.to_value().map(|&v| v as i64).unwrap_or(0)
268/// }
269///
270/// let forager = WeakestFitForager::<Solution, ChangeMove<Solution, i32>>::new(priority_strength);
271/// ```
272pub struct WeakestFitForager<S, M>
273where
274    S: PlanningSolution,
275    M: Move<S>,
276{
277    /// Function to evaluate strength of a move.
278    strength_fn: fn(&M) -> i64,
279    _phantom: PhantomData<S>,
280}
281
282impl<S, M> Debug for WeakestFitForager<S, M>
283where
284    S: PlanningSolution,
285    M: Move<S>,
286{
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>
293where
294    S: PlanningSolution,
295    M: Move<S>,
296{
297    /// Creates a new Weakest Fit forager with the given strength function.
298    ///
299    /// The strength function evaluates how "strong" a move is. The forager
300    /// picks the move with the minimum strength value.
301    pub fn new(strength_fn: fn(&M) -> i64) -> Self {
302        Self {
303            strength_fn,
304            _phantom: PhantomData,
305        }
306    }
307}
308
309impl<S, M> ConstructionForager<S, M> for WeakestFitForager<S, M>
310where
311    S: PlanningSolution,
312    M: Move<S>,
313{
314    fn pick_move(
315        &self,
316        placement: &Placement<S, M>,
317        score_director: &mut dyn ScoreDirector<S>,
318    ) -> Option<M> {
319        let mut best_move: Option<M> = None;
320        let mut min_strength: Option<i64> = None;
321
322        for m in &placement.moves {
323            if !m.is_doable(score_director) {
324                continue;
325            }
326
327            let strength = (self.strength_fn)(m);
328
329            let is_weaker = match min_strength {
330                None => true,
331                Some(best) => strength < best,
332            };
333
334            if is_weaker {
335                best_move = Some(m.clone());
336                min_strength = Some(strength);
337            }
338        }
339
340        best_move
341    }
342}
343
344/// Strongest Fit forager - picks the move with the highest strength value.
345///
346/// This forager evaluates each candidate move using a strength function
347/// and selects the move with the maximum strength. This is useful for
348/// assigning the "strongest" or most constraining values first.
349///
350/// # Example
351///
352/// ```
353/// use solverforge_solver::phase::construction::{StrongestFitForager, ConstructionForager};
354/// use solverforge_solver::heuristic::r#move::ChangeMove;
355/// use solverforge_core::domain::PlanningSolution;
356/// use solverforge_core::score::SimpleScore;
357///
358/// #[derive(Clone, Debug)]
359/// struct Task { priority: Option<i32> }
360///
361/// #[derive(Clone, Debug)]
362/// struct Solution { tasks: Vec<Task>, score: Option<SimpleScore> }
363///
364/// impl PlanningSolution for Solution {
365///     type Score = SimpleScore;
366///     fn score(&self) -> Option<Self::Score> { self.score }
367///     fn set_score(&mut self, score: Option<Self::Score>) { self.score = score; }
368/// }
369///
370/// // Strength function: priority value (higher = stronger)
371/// fn priority_strength(m: &ChangeMove<Solution, i32>) -> i64 {
372///     m.to_value().map(|&v| v as i64).unwrap_or(0)
373/// }
374///
375/// let forager = StrongestFitForager::<Solution, ChangeMove<Solution, i32>>::new(priority_strength);
376/// ```
377pub struct StrongestFitForager<S, M>
378where
379    S: PlanningSolution,
380    M: Move<S>,
381{
382    /// Function to evaluate strength of a move.
383    strength_fn: fn(&M) -> i64,
384    _phantom: PhantomData<S>,
385}
386
387impl<S, M> Debug for StrongestFitForager<S, M>
388where
389    S: PlanningSolution,
390    M: Move<S>,
391{
392    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
393        f.debug_struct("StrongestFitForager").finish()
394    }
395}
396
397impl<S, M> StrongestFitForager<S, M>
398where
399    S: PlanningSolution,
400    M: Move<S>,
401{
402    /// Creates a new Strongest Fit forager with the given strength function.
403    ///
404    /// The strength function evaluates how "strong" a move is. The forager
405    /// picks the move with the maximum strength value.
406    pub fn new(strength_fn: fn(&M) -> i64) -> Self {
407        Self {
408            strength_fn,
409            _phantom: PhantomData,
410        }
411    }
412}
413
414impl<S, M> ConstructionForager<S, M> for StrongestFitForager<S, M>
415where
416    S: PlanningSolution,
417    M: Move<S>,
418{
419    fn pick_move(
420        &self,
421        placement: &Placement<S, M>,
422        score_director: &mut dyn ScoreDirector<S>,
423    ) -> Option<M> {
424        let mut best_move: Option<M> = None;
425        let mut max_strength: Option<i64> = None;
426
427        for m in &placement.moves {
428            if !m.is_doable(score_director) {
429                continue;
430            }
431
432            let strength = (self.strength_fn)(m);
433
434            let is_stronger = match max_strength {
435                None => true,
436                Some(best) => strength > best,
437            };
438
439            if is_stronger {
440                best_move = Some(m.clone());
441                max_strength = Some(strength);
442            }
443        }
444
445        best_move
446    }
447}
448
449#[cfg(test)]
450mod tests {
451    use super::*;
452    use crate::heuristic::r#move::ChangeMove;
453    use crate::heuristic::selector::EntityReference;
454    use solverforge_core::domain::{EntityDescriptor, SolutionDescriptor, TypedEntityExtractor};
455    use solverforge_core::score::SimpleScore;
456    use solverforge_scoring::SimpleScoreDirector;
457    use std::any::TypeId;
458
459    #[derive(Clone, Debug)]
460    struct Queen {
461        row: Option<i64>,
462    }
463
464    #[derive(Clone, Debug)]
465    struct NQueensSolution {
466        queens: Vec<Queen>,
467        score: Option<SimpleScore>,
468    }
469
470    impl PlanningSolution for NQueensSolution {
471        type Score = SimpleScore;
472
473        fn score(&self) -> Option<Self::Score> {
474            self.score
475        }
476
477        fn set_score(&mut self, score: Option<Self::Score>) {
478            self.score = score;
479        }
480    }
481
482    fn get_queens(s: &NQueensSolution) -> &Vec<Queen> {
483        &s.queens
484    }
485
486    fn get_queens_mut(s: &mut NQueensSolution) -> &mut Vec<Queen> {
487        &mut s.queens
488    }
489
490    // Typed getter - zero erasure
491    fn get_queen_row(s: &NQueensSolution, idx: usize) -> Option<i64> {
492        s.queens.get(idx).and_then(|q| q.row)
493    }
494
495    // Typed setter - zero erasure
496    fn set_queen_row(s: &mut NQueensSolution, idx: usize, v: Option<i64>) {
497        if let Some(queen) = s.queens.get_mut(idx) {
498            queen.row = v;
499        }
500    }
501
502    fn create_test_director(
503    ) -> SimpleScoreDirector<NQueensSolution, impl Fn(&NQueensSolution) -> SimpleScore> {
504        let solution = NQueensSolution {
505            queens: vec![Queen { row: None }],
506            score: None,
507        };
508
509        let extractor = Box::new(TypedEntityExtractor::new(
510            "Queen",
511            "queens",
512            get_queens,
513            get_queens_mut,
514        ));
515        let entity_desc = EntityDescriptor::new("Queen", TypeId::of::<Queen>(), "queens")
516            .with_extractor(extractor);
517
518        let descriptor =
519            SolutionDescriptor::new("NQueensSolution", TypeId::of::<NQueensSolution>())
520                .with_entity(entity_desc);
521
522        // Score function: prefer higher row values
523        SimpleScoreDirector::with_calculator(solution, descriptor, |sol| {
524            let sum: i64 = sol.queens.iter().filter_map(|q| q.row).sum();
525            SimpleScore::of(sum)
526        })
527    }
528
529    type TestMove = ChangeMove<NQueensSolution, i64>;
530
531    fn create_placement() -> Placement<NQueensSolution, TestMove> {
532        let entity_ref = EntityReference::new(0, 0);
533        let moves: Vec<TestMove> = vec![
534            ChangeMove::new(0, Some(1i64), get_queen_row, set_queen_row, "row", 0),
535            ChangeMove::new(0, Some(5i64), get_queen_row, set_queen_row, "row", 0),
536            ChangeMove::new(0, Some(3i64), get_queen_row, set_queen_row, "row", 0),
537        ];
538        Placement::new(entity_ref, moves)
539    }
540
541    #[test]
542    fn test_first_fit_forager() {
543        let mut director = create_test_director();
544        let placement = create_placement();
545
546        let forager = FirstFitForager::<NQueensSolution, TestMove>::new();
547        let selected = forager.pick_move(&placement, &mut director);
548
549        // First Fit should pick the first move (value 1)
550        assert!(selected.is_some());
551    }
552
553    #[test]
554    fn test_best_fit_forager() {
555        let mut director = create_test_director();
556        let placement = create_placement();
557
558        let forager = BestFitForager::<NQueensSolution, TestMove>::new();
559        let selected = forager.pick_move(&placement, &mut director);
560
561        // Best Fit should pick the move with highest score (value 5)
562        assert!(selected.is_some());
563
564        // Execute the selected move and check the score
565        if let Some(m) = selected {
566            m.do_move(&mut director);
567            let score = director.calculate_score();
568            assert_eq!(score, SimpleScore::of(5));
569        }
570    }
571
572    #[test]
573    fn test_empty_placement() {
574        let mut director = create_test_director();
575        let placement = Placement::new(EntityReference::new(0, 0), vec![]);
576
577        let forager = FirstFitForager::<NQueensSolution, TestMove>::new();
578        let selected = forager.pick_move(&placement, &mut director);
579
580        assert!(selected.is_none());
581    }
582
583    fn value_strength(m: &TestMove) -> i64 {
584        m.to_value().copied().unwrap_or(0)
585    }
586
587    #[test]
588    fn test_weakest_fit_forager() {
589        let mut director = create_test_director();
590        let placement = create_placement(); // values: 1, 5, 3
591
592        let forager = WeakestFitForager::<NQueensSolution, TestMove>::new(value_strength);
593        let selected = forager.pick_move(&placement, &mut director);
594
595        // Weakest Fit should pick the move with lowest strength (value 1)
596        assert!(selected.is_some());
597        if let Some(m) = selected {
598            m.do_move(&mut director);
599            let score = director.calculate_score();
600            assert_eq!(score, SimpleScore::of(1));
601        }
602    }
603
604    #[test]
605    fn test_strongest_fit_forager() {
606        let mut director = create_test_director();
607        let placement = create_placement(); // values: 1, 5, 3
608
609        let forager = StrongestFitForager::<NQueensSolution, TestMove>::new(value_strength);
610        let selected = forager.pick_move(&placement, &mut director);
611
612        // Strongest Fit should pick the move with highest strength (value 5)
613        assert!(selected.is_some());
614        if let Some(m) = selected {
615            m.do_move(&mut director);
616            let score = director.calculate_score();
617            assert_eq!(score, SimpleScore::of(5));
618        }
619    }
620}