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 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;