Skip to main content

solverforge_solver/phase/construction/
phase.rs

1// Construction heuristic phase implementation.
2
3use std::any::Any;
4use std::fmt::Debug;
5use std::marker::PhantomData;
6
7use solverforge_core::domain::PlanningSolution;
8use solverforge_core::score::Score;
9use solverforge_scoring::{Director, RecordingDirector};
10use tracing::info;
11
12use crate::heuristic::r#move::Move;
13use crate::phase::construction::{
14    BestFitForager, ConstructionForager, EntityPlacer, FirstFeasibleForager, FirstFitForager,
15};
16use crate::phase::control::{
17    settle_construction_interrupt, should_interrupt_evaluation, StepInterrupt,
18};
19use crate::phase::Phase;
20use crate::scope::ProgressCallback;
21use crate::scope::{PhaseScope, SolverScope, StepScope};
22
23/// Construction heuristic phase that builds an initial solution.
24///
25/// This phase iterates over uninitialized entities and assigns values
26/// to their planning variables using a greedy approach.
27///
28/// # Type Parameters
29/// * `S` - The planning solution type
30/// * `M` - The move type
31/// * `P` - The entity placer type
32/// * `Fo` - The forager type
33pub struct ConstructionHeuristicPhase<S, M, P, Fo>
34where
35    S: PlanningSolution,
36    M: Move<S>,
37    P: EntityPlacer<S, M>,
38    Fo: ConstructionForager<S, M>,
39{
40    placer: P,
41    forager: Fo,
42    _phantom: PhantomData<fn() -> (S, M)>,
43}
44
45impl<S, M, P, Fo> ConstructionHeuristicPhase<S, M, P, Fo>
46where
47    S: PlanningSolution,
48    M: Move<S>,
49    P: EntityPlacer<S, M>,
50    Fo: ConstructionForager<S, M>,
51{
52    pub fn new(placer: P, forager: Fo) -> Self {
53        Self {
54            placer,
55            forager,
56            _phantom: PhantomData,
57        }
58    }
59}
60
61impl<S, M, P, Fo> Debug for ConstructionHeuristicPhase<S, M, P, Fo>
62where
63    S: PlanningSolution,
64    M: Move<S>,
65    P: EntityPlacer<S, M> + Debug,
66    Fo: ConstructionForager<S, M> + Debug,
67{
68    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
69        f.debug_struct("ConstructionHeuristicPhase")
70            .field("placer", &self.placer)
71            .field("forager", &self.forager)
72            .finish()
73    }
74}
75
76impl<S, D, BestCb, M, P, Fo> Phase<S, D, BestCb> for ConstructionHeuristicPhase<S, M, P, Fo>
77where
78    S: PlanningSolution,
79    D: Director<S>,
80    BestCb: ProgressCallback<S>,
81    M: Move<S> + 'static,
82    P: EntityPlacer<S, M>,
83    Fo: ConstructionForager<S, M> + 'static,
84{
85    fn solve(&mut self, solver_scope: &mut SolverScope<S, D, BestCb>) {
86        let mut phase_scope = PhaseScope::new(solver_scope, 0);
87        let phase_index = phase_scope.phase_index();
88
89        info!(
90            event = "phase_start",
91            phase = "Construction Heuristic",
92            phase_index = phase_index,
93        );
94
95        // Get all placements (entities that need values assigned)
96        let mut placements = self
97            .placer
98            .get_placements(phase_scope.score_director())
99            .into_iter();
100        let mut pending_placement = None;
101
102        loop {
103            // Construction must complete — only stop for external flag or time limit,
104            // never for step/move count limits (those are for local search).
105            if phase_scope
106                .solver_scope_mut()
107                .should_terminate_construction()
108            {
109                break;
110            }
111
112            let mut placement = match pending_placement.take().or_else(|| placements.next()) {
113                Some(placement) => placement,
114                None => break,
115            };
116
117            // Record move evaluations at call-site (Option C: maintains trait purity)
118            // BestFitForager evaluates ALL moves in the placement
119            let moves_in_placement = placement.moves.len() as u64;
120
121            let mut step_scope = StepScope::new(&mut phase_scope);
122
123            // Use forager to pick the best move index for this placement
124            let selected_idx = match select_move_index(&self.forager, &placement, &mut step_scope) {
125                ConstructionSelection::Selected(selected_idx) => selected_idx,
126                ConstructionSelection::Interrupted => {
127                    match settle_construction_interrupt(&mut step_scope) {
128                        StepInterrupt::Restart => {
129                            pending_placement = Some(placement);
130                            continue;
131                        }
132                        StepInterrupt::TerminatePhase => break,
133                    }
134                }
135            };
136
137            // Record all moves as evaluated, with one accepted if selection succeeded
138            for i in 0..moves_in_placement {
139                let accepted = selected_idx == Some(i as usize);
140                step_scope
141                    .phase_scope_mut()
142                    .solver_scope_mut()
143                    .stats_mut()
144                    .record_move(accepted);
145            }
146
147            if let Some(idx) = selected_idx {
148                // Take ownership of the move
149                let m = placement.take_move(idx);
150
151                // Execute the move
152                m.do_move(step_scope.score_director_mut());
153
154                // Calculate and record the step score
155                let step_score = step_scope.calculate_score();
156                step_scope.set_step_score(step_score);
157            }
158
159            step_scope.complete();
160        }
161
162        // Update best solution at end of phase
163        phase_scope.update_best_solution();
164
165        let best_score = phase_scope
166            .solver_scope()
167            .best_score()
168            .map(|s| format!("{}", s))
169            .unwrap_or_else(|| "none".to_string());
170
171        let duration = phase_scope.elapsed();
172        let steps = phase_scope.step_count();
173        let speed = if duration.as_secs_f64() > 0.0 {
174            (steps as f64 / duration.as_secs_f64()) as u64
175        } else {
176            0
177        };
178        let stats = phase_scope.solver_scope().stats();
179
180        info!(
181            event = "phase_end",
182            phase = "Construction Heuristic",
183            phase_index = phase_index,
184            duration_ms = duration.as_millis() as u64,
185            steps = steps,
186            moves_evaluated = stats.moves_evaluated,
187            moves_accepted = stats.moves_accepted,
188            score_calculations = stats.score_calculations,
189            speed = speed,
190            score = best_score,
191        );
192    }
193
194    fn phase_type_name(&self) -> &'static str {
195        "ConstructionHeuristic"
196    }
197}
198
199enum ConstructionSelection {
200    Selected(Option<usize>),
201    Interrupted,
202}
203
204fn select_move_index<S, D, BestCb, M, Fo>(
205    forager: &Fo,
206    placement: &crate::phase::construction::Placement<S, M>,
207    step_scope: &mut StepScope<'_, '_, '_, S, D, BestCb>,
208) -> ConstructionSelection
209where
210    S: PlanningSolution,
211    S::Score: Score,
212    D: Director<S>,
213    BestCb: ProgressCallback<S>,
214    M: Move<S> + 'static,
215    Fo: ConstructionForager<S, M> + 'static,
216{
217    let erased = forager as &dyn Any;
218
219    if erased.is::<FirstFitForager<S, M>>() {
220        return select_first_fit_index(placement, step_scope);
221    }
222    if erased.is::<BestFitForager<S, M>>() {
223        return select_best_fit_index(placement, step_scope);
224    }
225    if erased.is::<FirstFeasibleForager<S, M>>() {
226        return select_first_feasible_index(placement, step_scope);
227    }
228
229    ConstructionSelection::Selected(
230        forager.pick_move_index(placement, step_scope.score_director_mut()),
231    )
232}
233
234fn select_first_fit_index<S, D, BestCb, M>(
235    placement: &crate::phase::construction::Placement<S, M>,
236    step_scope: &mut StepScope<'_, '_, '_, S, D, BestCb>,
237) -> ConstructionSelection
238where
239    S: PlanningSolution,
240    D: Director<S>,
241    BestCb: ProgressCallback<S>,
242    M: Move<S> + 'static,
243{
244    for (idx, m) in placement.moves.iter().enumerate() {
245        if should_interrupt_evaluation(step_scope, idx) {
246            return ConstructionSelection::Interrupted;
247        }
248        if m.is_doable(step_scope.score_director()) {
249            return ConstructionSelection::Selected(Some(idx));
250        }
251    }
252    ConstructionSelection::Selected(None)
253}
254
255fn select_best_fit_index<S, D, BestCb, M>(
256    placement: &crate::phase::construction::Placement<S, M>,
257    step_scope: &mut StepScope<'_, '_, '_, S, D, BestCb>,
258) -> ConstructionSelection
259where
260    S: PlanningSolution,
261    S::Score: Score,
262    D: Director<S>,
263    BestCb: ProgressCallback<S>,
264    M: Move<S> + 'static,
265{
266    let mut best_idx: Option<usize> = None;
267    let mut best_score: Option<S::Score> = None;
268
269    for (idx, m) in placement.moves.iter().enumerate() {
270        if should_interrupt_evaluation(step_scope, idx) {
271            return ConstructionSelection::Interrupted;
272        }
273        if !m.is_doable(step_scope.score_director()) {
274            continue;
275        }
276
277        let score = {
278            let mut recording = RecordingDirector::new(step_scope.score_director_mut());
279            m.do_move(&mut recording);
280            let score = recording.calculate_score();
281            recording.undo_changes();
282            score
283        };
284
285        let is_better = match &best_score {
286            None => true,
287            Some(best) => score > *best,
288        };
289        if is_better {
290            best_idx = Some(idx);
291            best_score = Some(score);
292        }
293    }
294
295    ConstructionSelection::Selected(best_idx)
296}
297
298fn select_first_feasible_index<S, D, BestCb, M>(
299    placement: &crate::phase::construction::Placement<S, M>,
300    step_scope: &mut StepScope<'_, '_, '_, S, D, BestCb>,
301) -> ConstructionSelection
302where
303    S: PlanningSolution,
304    S::Score: Score,
305    D: Director<S>,
306    BestCb: ProgressCallback<S>,
307    M: Move<S> + 'static,
308{
309    let mut fallback_idx: Option<usize> = None;
310    let mut fallback_score: Option<S::Score> = None;
311
312    for (idx, m) in placement.moves.iter().enumerate() {
313        if should_interrupt_evaluation(step_scope, idx) {
314            return ConstructionSelection::Interrupted;
315        }
316        if !m.is_doable(step_scope.score_director()) {
317            continue;
318        }
319
320        let score = {
321            let mut recording = RecordingDirector::new(step_scope.score_director_mut());
322            m.do_move(&mut recording);
323            let score = recording.calculate_score();
324            recording.undo_changes();
325            score
326        };
327
328        if score.is_feasible() {
329            return ConstructionSelection::Selected(Some(idx));
330        }
331
332        let is_better = match &fallback_score {
333            None => true,
334            Some(best) => score > *best,
335        };
336        if is_better {
337            fallback_idx = Some(idx);
338            fallback_score = Some(score);
339        }
340    }
341
342    ConstructionSelection::Selected(fallback_idx)
343}
344
345#[cfg(test)]
346#[path = "phase_tests.rs"]
347mod tests;