Skip to main content

solverforge_solver/phase/construction/
phase.rs

1// Construction heuristic phase implementation.
2
3use std::fmt::Debug;
4use std::marker::PhantomData;
5
6use solverforge_core::domain::PlanningSolution;
7use solverforge_scoring::Director;
8use tracing::info;
9
10use crate::heuristic::r#move::Move;
11use crate::phase::construction::{ConstructionForager, EntityPlacer};
12use crate::phase::Phase;
13use crate::scope::ProgressCallback;
14use crate::scope::{PhaseScope, SolverScope, StepScope};
15
16/// Construction heuristic phase that builds an initial solution.
17///
18/// This phase iterates over uninitialized entities and assigns values
19/// to their planning variables using a greedy approach.
20///
21/// # Type Parameters
22/// * `S` - The planning solution type
23/// * `M` - The move type
24/// * `P` - The entity placer type
25/// * `Fo` - The forager type
26pub struct ConstructionHeuristicPhase<S, M, P, Fo>
27where
28    S: PlanningSolution,
29    M: Move<S>,
30    P: EntityPlacer<S, M>,
31    Fo: ConstructionForager<S, M>,
32{
33    placer: P,
34    forager: Fo,
35    _phantom: PhantomData<fn() -> (S, M)>,
36}
37
38impl<S, M, P, Fo> ConstructionHeuristicPhase<S, M, P, Fo>
39where
40    S: PlanningSolution,
41    M: Move<S>,
42    P: EntityPlacer<S, M>,
43    Fo: ConstructionForager<S, M>,
44{
45    pub fn new(placer: P, forager: Fo) -> Self {
46        Self {
47            placer,
48            forager,
49            _phantom: PhantomData,
50        }
51    }
52}
53
54impl<S, M, P, Fo> Debug for ConstructionHeuristicPhase<S, M, P, Fo>
55where
56    S: PlanningSolution,
57    M: Move<S>,
58    P: EntityPlacer<S, M> + Debug,
59    Fo: ConstructionForager<S, M> + Debug,
60{
61    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
62        f.debug_struct("ConstructionHeuristicPhase")
63            .field("placer", &self.placer)
64            .field("forager", &self.forager)
65            .finish()
66    }
67}
68
69impl<S, D, BestCb, M, P, Fo> Phase<S, D, BestCb> for ConstructionHeuristicPhase<S, M, P, Fo>
70where
71    S: PlanningSolution,
72    D: Director<S>,
73    BestCb: ProgressCallback<S>,
74    M: Move<S>,
75    P: EntityPlacer<S, M>,
76    Fo: ConstructionForager<S, M>,
77{
78    fn solve(&mut self, solver_scope: &mut SolverScope<S, D, BestCb>) {
79        let mut phase_scope = PhaseScope::new(solver_scope, 0);
80        let phase_index = phase_scope.phase_index();
81
82        info!(
83            event = "phase_start",
84            phase = "Construction Heuristic",
85            phase_index = phase_index,
86        );
87
88        // Get all placements (entities that need values assigned)
89        let placements = self.placer.get_placements(phase_scope.score_director());
90
91        for mut placement in placements {
92            // Construction must complete — only stop for external flag or time limit,
93            // never for step/move count limits (those are for local search).
94            if phase_scope
95                .solver_scope_mut()
96                .should_terminate_construction()
97            {
98                break;
99            }
100
101            // Record move evaluations at call-site (Option C: maintains trait purity)
102            // BestFitForager evaluates ALL moves in the placement
103            let moves_in_placement = placement.moves.len() as u64;
104
105            let mut step_scope = StepScope::new(&mut phase_scope);
106
107            // Use forager to pick the best move index for this placement
108            let selected_idx = self
109                .forager
110                .pick_move_index(&placement, step_scope.score_director_mut());
111
112            // Record all moves as evaluated, with one accepted if selection succeeded
113            for i in 0..moves_in_placement {
114                let accepted = selected_idx == Some(i as usize);
115                step_scope
116                    .phase_scope_mut()
117                    .solver_scope_mut()
118                    .stats_mut()
119                    .record_move(accepted);
120            }
121
122            if let Some(idx) = selected_idx {
123                // Take ownership of the move
124                let m = placement.take_move(idx);
125
126                // Execute the move
127                m.do_move(step_scope.score_director_mut());
128
129                // Calculate and record the step score
130                let step_score = step_scope.calculate_score();
131                step_scope.set_step_score(step_score);
132            }
133
134            step_scope.complete();
135        }
136
137        // Update best solution at end of phase
138        phase_scope.update_best_solution();
139
140        let best_score = phase_scope
141            .solver_scope()
142            .best_score()
143            .map(|s| format!("{}", s))
144            .unwrap_or_else(|| "none".to_string());
145
146        let duration = phase_scope.elapsed();
147        let steps = phase_scope.step_count();
148        let speed = if duration.as_secs_f64() > 0.0 {
149            (steps as f64 / duration.as_secs_f64()) as u64
150        } else {
151            0
152        };
153        let stats = phase_scope.solver_scope().stats();
154
155        info!(
156            event = "phase_end",
157            phase = "Construction Heuristic",
158            phase_index = phase_index,
159            duration_ms = duration.as_millis() as u64,
160            steps = steps,
161            moves_evaluated = stats.moves_evaluated,
162            moves_accepted = stats.moves_accepted,
163            score_calculations = stats.score_calculations,
164            speed = speed,
165            score = best_score,
166        );
167    }
168
169    fn phase_type_name(&self) -> &'static str {
170        "ConstructionHeuristic"
171    }
172}
173
174#[cfg(test)]
175mod tests {
176    use super::*;
177    use crate::heuristic::selector::{FromSolutionEntitySelector, StaticValueSelector};
178    use crate::phase::construction::{BestFitForager, FirstFitForager, QueuedEntityPlacer};
179    use crate::test_utils::{
180        create_simple_nqueens_director, get_queen_row, set_queen_row, NQueensSolution,
181    };
182
183    fn create_placer(
184        values: Vec<i64>,
185    ) -> QueuedEntityPlacer<
186        NQueensSolution,
187        i64,
188        FromSolutionEntitySelector,
189        StaticValueSelector<NQueensSolution, i64>,
190    > {
191        let es = FromSolutionEntitySelector::new(0);
192        let vs = StaticValueSelector::new(values);
193        QueuedEntityPlacer::new(es, vs, get_queen_row, set_queen_row, 0, "row")
194    }
195
196    #[test]
197    fn test_construction_first_fit() {
198        let director = create_simple_nqueens_director(4);
199        let mut solver_scope = SolverScope::new(director);
200        solver_scope.start_solving();
201
202        let values: Vec<i64> = (0..4).collect();
203        let placer = create_placer(values);
204        let forager = FirstFitForager::new();
205        let mut phase = ConstructionHeuristicPhase::new(placer, forager);
206
207        phase.solve(&mut solver_scope);
208
209        let solution = solver_scope.working_solution();
210        assert_eq!(solution.queens.len(), 4);
211        for queen in &solution.queens {
212            assert!(queen.row.is_some(), "Queen should have a row assigned");
213        }
214
215        assert!(solver_scope.best_solution().is_some());
216        // Verify stats were recorded
217        assert!(solver_scope.stats().moves_evaluated > 0);
218    }
219
220    #[test]
221    fn test_construction_best_fit() {
222        let director = create_simple_nqueens_director(4);
223        let mut solver_scope = SolverScope::new(director);
224        solver_scope.start_solving();
225
226        let values: Vec<i64> = (0..4).collect();
227        let placer = create_placer(values);
228        let forager = BestFitForager::new();
229        let mut phase = ConstructionHeuristicPhase::new(placer, forager);
230
231        phase.solve(&mut solver_scope);
232
233        let solution = solver_scope.working_solution();
234        for queen in &solution.queens {
235            assert!(queen.row.is_some(), "Queen should have a row assigned");
236        }
237
238        assert!(solver_scope.best_solution().is_some());
239        assert!(solver_scope.best_score().is_some());
240
241        // BestFitForager evaluates all moves: 4 entities * 4 values = 16 moves
242        assert_eq!(solver_scope.stats().moves_evaluated, 16);
243    }
244
245    #[test]
246    fn test_construction_empty_solution() {
247        let director = create_simple_nqueens_director(0);
248        let mut solver_scope = SolverScope::new(director);
249        solver_scope.start_solving();
250
251        let values: Vec<i64> = vec![];
252        let placer = create_placer(values);
253        let forager = FirstFitForager::new();
254        let mut phase = ConstructionHeuristicPhase::new(placer, forager);
255
256        phase.solve(&mut solver_scope);
257
258        // No moves should be evaluated for empty solution
259        assert_eq!(solver_scope.stats().moves_evaluated, 0);
260    }
261}