1use std::any::Any;
4use std::fmt::Debug;
5use std::marker::PhantomData;
6
7use solverforge_core::domain::PlanningSolution;
8use solverforge_core::score::Score;
9use solverforge_scoring::{Director, RecordingDirector};
10use tracing::info;
11
12use crate::heuristic::r#move::Move;
13use crate::phase::construction::{
14 BestFitForager, ConstructionForager, EntityPlacer, FirstFeasibleForager, FirstFitForager,
15};
16use crate::phase::control::{
17 settle_construction_interrupt, should_interrupt_evaluation, StepInterrupt,
18};
19use crate::phase::Phase;
20use crate::scope::ProgressCallback;
21use crate::scope::{PhaseScope, SolverScope, StepScope};
22
23pub struct ConstructionHeuristicPhase<S, M, P, Fo>
34where
35 S: PlanningSolution,
36 M: Move<S>,
37 P: EntityPlacer<S, M>,
38 Fo: ConstructionForager<S, M>,
39{
40 placer: P,
41 forager: Fo,
42 _phantom: PhantomData<fn() -> (S, M)>,
43}
44
45impl<S, M, P, Fo> ConstructionHeuristicPhase<S, M, P, Fo>
46where
47 S: PlanningSolution,
48 M: Move<S>,
49 P: EntityPlacer<S, M>,
50 Fo: ConstructionForager<S, M>,
51{
52 pub fn new(placer: P, forager: Fo) -> Self {
53 Self {
54 placer,
55 forager,
56 _phantom: PhantomData,
57 }
58 }
59}
60
61impl<S, M, P, Fo> Debug for ConstructionHeuristicPhase<S, M, P, Fo>
62where
63 S: PlanningSolution,
64 M: Move<S>,
65 P: EntityPlacer<S, M> + Debug,
66 Fo: ConstructionForager<S, M> + Debug,
67{
68 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
69 f.debug_struct("ConstructionHeuristicPhase")
70 .field("placer", &self.placer)
71 .field("forager", &self.forager)
72 .finish()
73 }
74}
75
76impl<S, D, BestCb, M, P, Fo> Phase<S, D, BestCb> for ConstructionHeuristicPhase<S, M, P, Fo>
77where
78 S: PlanningSolution,
79 D: Director<S>,
80 BestCb: ProgressCallback<S>,
81 M: Move<S> + 'static,
82 P: EntityPlacer<S, M>,
83 Fo: ConstructionForager<S, M> + 'static,
84{
85 fn solve(&mut self, solver_scope: &mut SolverScope<S, D, BestCb>) {
86 let mut phase_scope = PhaseScope::new(solver_scope, 0);
87 let phase_index = phase_scope.phase_index();
88
89 info!(
90 event = "phase_start",
91 phase = "Construction Heuristic",
92 phase_index = phase_index,
93 );
94
95 let mut placements = self
97 .placer
98 .get_placements(phase_scope.score_director())
99 .into_iter();
100 let mut pending_placement = None;
101
102 loop {
103 if phase_scope
106 .solver_scope_mut()
107 .should_terminate_construction()
108 {
109 break;
110 }
111
112 let mut placement = match pending_placement.take().or_else(|| placements.next()) {
113 Some(placement) => placement,
114 None => break,
115 };
116
117 let moves_in_placement = placement.moves.len() as u64;
120
121 let mut step_scope = StepScope::new(&mut phase_scope);
122
123 let selected_idx = match select_move_index(&self.forager, &placement, &mut step_scope) {
125 ConstructionSelection::Selected(selected_idx) => selected_idx,
126 ConstructionSelection::Interrupted => {
127 match settle_construction_interrupt(&mut step_scope) {
128 StepInterrupt::Restart => {
129 pending_placement = Some(placement);
130 continue;
131 }
132 StepInterrupt::TerminatePhase => break,
133 }
134 }
135 };
136
137 for i in 0..moves_in_placement {
139 let accepted = selected_idx == Some(i as usize);
140 step_scope
141 .phase_scope_mut()
142 .solver_scope_mut()
143 .stats_mut()
144 .record_move(accepted);
145 }
146
147 if let Some(idx) = selected_idx {
148 let m = placement.take_move(idx);
150
151 m.do_move(step_scope.score_director_mut());
153
154 let step_score = step_scope.calculate_score();
156 step_scope.set_step_score(step_score);
157 }
158
159 step_scope.complete();
160 }
161
162 phase_scope.update_best_solution();
164
165 let best_score = phase_scope
166 .solver_scope()
167 .best_score()
168 .map(|s| format!("{}", s))
169 .unwrap_or_else(|| "none".to_string());
170
171 let duration = phase_scope.elapsed();
172 let steps = phase_scope.step_count();
173 let speed = if duration.as_secs_f64() > 0.0 {
174 (steps as f64 / duration.as_secs_f64()) as u64
175 } else {
176 0
177 };
178 let stats = phase_scope.solver_scope().stats();
179
180 info!(
181 event = "phase_end",
182 phase = "Construction Heuristic",
183 phase_index = phase_index,
184 duration_ms = duration.as_millis() as u64,
185 steps = steps,
186 moves_evaluated = stats.moves_evaluated,
187 moves_accepted = stats.moves_accepted,
188 score_calculations = stats.score_calculations,
189 speed = speed,
190 score = best_score,
191 );
192 }
193
194 fn phase_type_name(&self) -> &'static str {
195 "ConstructionHeuristic"
196 }
197}
198
199enum ConstructionSelection {
200 Selected(Option<usize>),
201 Interrupted,
202}
203
204fn select_move_index<S, D, BestCb, M, Fo>(
205 forager: &Fo,
206 placement: &crate::phase::construction::Placement<S, M>,
207 step_scope: &mut StepScope<'_, '_, '_, S, D, BestCb>,
208) -> ConstructionSelection
209where
210 S: PlanningSolution,
211 S::Score: Score,
212 D: Director<S>,
213 BestCb: ProgressCallback<S>,
214 M: Move<S> + 'static,
215 Fo: ConstructionForager<S, M> + 'static,
216{
217 let erased = forager as &dyn Any;
218
219 if erased.is::<FirstFitForager<S, M>>() {
220 return select_first_fit_index(placement, step_scope);
221 }
222 if erased.is::<BestFitForager<S, M>>() {
223 return select_best_fit_index(placement, step_scope);
224 }
225 if erased.is::<FirstFeasibleForager<S, M>>() {
226 return select_first_feasible_index(placement, step_scope);
227 }
228
229 ConstructionSelection::Selected(
230 forager.pick_move_index(placement, step_scope.score_director_mut()),
231 )
232}
233
234fn select_first_fit_index<S, D, BestCb, M>(
235 placement: &crate::phase::construction::Placement<S, M>,
236 step_scope: &mut StepScope<'_, '_, '_, S, D, BestCb>,
237) -> ConstructionSelection
238where
239 S: PlanningSolution,
240 D: Director<S>,
241 BestCb: ProgressCallback<S>,
242 M: Move<S> + 'static,
243{
244 for (idx, m) in placement.moves.iter().enumerate() {
245 if should_interrupt_evaluation(step_scope, idx) {
246 return ConstructionSelection::Interrupted;
247 }
248 if m.is_doable(step_scope.score_director()) {
249 return ConstructionSelection::Selected(Some(idx));
250 }
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 if should_interrupt_evaluation(step_scope, idx) {
271 return ConstructionSelection::Interrupted;
272 }
273 if !m.is_doable(step_scope.score_director()) {
274 continue;
275 }
276
277 let score = {
278 let mut recording = RecordingDirector::new(step_scope.score_director_mut());
279 m.do_move(&mut recording);
280 let score = recording.calculate_score();
281 recording.undo_changes();
282 score
283 };
284
285 let is_better = match &best_score {
286 None => true,
287 Some(best) => score > *best,
288 };
289 if is_better {
290 best_idx = Some(idx);
291 best_score = Some(score);
292 }
293 }
294
295 ConstructionSelection::Selected(best_idx)
296}
297
298fn select_first_feasible_index<S, D, BestCb, M>(
299 placement: &crate::phase::construction::Placement<S, M>,
300 step_scope: &mut StepScope<'_, '_, '_, S, D, BestCb>,
301) -> ConstructionSelection
302where
303 S: PlanningSolution,
304 S::Score: Score,
305 D: Director<S>,
306 BestCb: ProgressCallback<S>,
307 M: Move<S> + 'static,
308{
309 let mut fallback_idx: Option<usize> = None;
310 let mut fallback_score: Option<S::Score> = None;
311
312 for (idx, m) in placement.moves.iter().enumerate() {
313 if should_interrupt_evaluation(step_scope, idx) {
314 return ConstructionSelection::Interrupted;
315 }
316 if !m.is_doable(step_scope.score_director()) {
317 continue;
318 }
319
320 let score = {
321 let mut recording = RecordingDirector::new(step_scope.score_director_mut());
322 m.do_move(&mut recording);
323 let score = recording.calculate_score();
324 recording.undo_changes();
325 score
326 };
327
328 if score.is_feasible() {
329 return ConstructionSelection::Selected(Some(idx));
330 }
331
332 let is_better = match &fallback_score {
333 None => true,
334 Some(best) => score > *best,
335 };
336 if is_better {
337 fallback_idx = Some(idx);
338 fallback_score = Some(score);
339 }
340 }
341
342 ConstructionSelection::Selected(fallback_idx)
343}
344
345#[cfg(test)]
346#[path = "phase_tests.rs"]
347mod tests;