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, 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
25pub 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 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 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 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 let m = placement.take_move(idx);
144
145 m.do_move(step_scope.score_director_mut());
147
148 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 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;