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