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