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