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