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        // Update best solution at end of phase
157        phase_scope.update_best_solution();
158
159        let best_score = phase_scope
160            .solver_scope()
161            .best_score()
162            .map(|s| format!("{}", s))
163            .unwrap_or_else(|| "none".to_string());
164
165        let duration = phase_scope.elapsed();
166        let steps = phase_scope.step_count();
167        let speed = whole_units_per_second(steps, duration);
168        let stats = phase_scope.stats();
169
170        info!(
171            event = "phase_end",
172            phase = "Construction Heuristic",
173            phase_index = phase_index,
174            duration = %format_duration(duration),
175            steps = steps,
176            moves_generated = stats.moves_generated,
177            moves_evaluated = stats.moves_evaluated,
178            moves_accepted = stats.moves_accepted,
179            score_calculations = stats.score_calculations,
180            generation_time = %format_duration(stats.generation_time()),
181            evaluation_time = %format_duration(stats.evaluation_time()),
182            speed = speed,
183            score = best_score,
184        );
185    }
186
187    fn phase_type_name(&self) -> &'static str {
188        "ConstructionHeuristic"
189    }
190}
191
192enum ConstructionSelection {
193    Selected(Option<usize>),
194    Interrupted,
195}
196
197fn select_move_index<S, D, BestCb, M, Fo>(
198    forager: &Fo,
199    placement: &crate::phase::construction::Placement<S, M>,
200    step_scope: &mut StepScope<'_, '_, '_, S, D, BestCb>,
201) -> ConstructionSelection
202where
203    S: PlanningSolution,
204    S::Score: Score,
205    D: Director<S>,
206    BestCb: ProgressCallback<S>,
207    M: Move<S> + 'static,
208    Fo: ConstructionForager<S, M> + 'static,
209{
210    let erased = forager as &dyn Any;
211
212    if erased.is::<FirstFitForager<S, M>>() {
213        return select_first_fit_index(placement, step_scope);
214    }
215    if erased.is::<BestFitForager<S, M>>() {
216        return select_best_fit_index(placement, step_scope);
217    }
218    if erased.is::<FirstFeasibleForager<S, M>>() {
219        return select_first_feasible_index(placement, step_scope);
220    }
221
222    ConstructionSelection::Selected(
223        forager.pick_move_index(placement, step_scope.score_director_mut()),
224    )
225}
226
227fn select_first_fit_index<S, D, BestCb, M>(
228    placement: &crate::phase::construction::Placement<S, M>,
229    step_scope: &mut StepScope<'_, '_, '_, S, D, BestCb>,
230) -> ConstructionSelection
231where
232    S: PlanningSolution,
233    D: Director<S>,
234    BestCb: ProgressCallback<S>,
235    M: Move<S> + 'static,
236{
237    for (idx, m) in placement.moves.iter().enumerate() {
238        let evaluation_started = Instant::now();
239        if should_interrupt_evaluation(step_scope, idx) {
240            return ConstructionSelection::Interrupted;
241        }
242        if m.is_doable(step_scope.score_director()) {
243            step_scope
244                .phase_scope_mut()
245                .record_evaluated_move(evaluation_started.elapsed());
246            return ConstructionSelection::Selected(Some(idx));
247        }
248        step_scope
249            .phase_scope_mut()
250            .record_evaluated_move(evaluation_started.elapsed());
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        let evaluation_started = Instant::now();
271        if should_interrupt_evaluation(step_scope, idx) {
272            return ConstructionSelection::Interrupted;
273        }
274        if !m.is_doable(step_scope.score_director()) {
275            step_scope
276                .phase_scope_mut()
277                .record_evaluated_move(evaluation_started.elapsed());
278            continue;
279        }
280
281        let score = {
282            let mut recording = RecordingDirector::new(step_scope.score_director_mut());
283            m.do_move(&mut recording);
284            let score = recording.calculate_score();
285            recording.undo_changes();
286            score
287        };
288        step_scope.phase_scope_mut().record_score_calculation();
289        step_scope
290            .phase_scope_mut()
291            .record_evaluated_move(evaluation_started.elapsed());
292
293        let is_better = match &best_score {
294            None => true,
295            Some(best) => score > *best,
296        };
297        if is_better {
298            best_idx = Some(idx);
299            best_score = Some(score);
300        }
301    }
302
303    ConstructionSelection::Selected(best_idx)
304}
305
306fn select_first_feasible_index<S, D, BestCb, M>(
307    placement: &crate::phase::construction::Placement<S, M>,
308    step_scope: &mut StepScope<'_, '_, '_, S, D, BestCb>,
309) -> ConstructionSelection
310where
311    S: PlanningSolution,
312    S::Score: Score,
313    D: Director<S>,
314    BestCb: ProgressCallback<S>,
315    M: Move<S> + 'static,
316{
317    let mut fallback_idx: Option<usize> = None;
318    let mut fallback_score: Option<S::Score> = None;
319
320    for (idx, m) in placement.moves.iter().enumerate() {
321        let evaluation_started = Instant::now();
322        if should_interrupt_evaluation(step_scope, idx) {
323            return ConstructionSelection::Interrupted;
324        }
325        if !m.is_doable(step_scope.score_director()) {
326            step_scope
327                .phase_scope_mut()
328                .record_evaluated_move(evaluation_started.elapsed());
329            continue;
330        }
331
332        let score = {
333            let mut recording = RecordingDirector::new(step_scope.score_director_mut());
334            m.do_move(&mut recording);
335            let score = recording.calculate_score();
336            recording.undo_changes();
337            score
338        };
339        step_scope.phase_scope_mut().record_score_calculation();
340        step_scope
341            .phase_scope_mut()
342            .record_evaluated_move(evaluation_started.elapsed());
343
344        if score.is_feasible() {
345            return ConstructionSelection::Selected(Some(idx));
346        }
347
348        let is_better = match &fallback_score {
349            None => true,
350            Some(best) => score > *best,
351        };
352        if is_better {
353            fallback_idx = Some(idx);
354            fallback_score = Some(score);
355        }
356    }
357
358    ConstructionSelection::Selected(fallback_idx)
359}
360
361#[cfg(test)]
362#[path = "phase_tests.rs"]
363mod tests;