1use 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
31pub 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 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 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 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 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;