1use std::fmt::Debug;
4use std::marker::PhantomData;
5use std::time::Instant;
6
7use solverforge_core::domain::PlanningSolution;
8use solverforge_scoring::Director;
9use tracing::info;
10
11use crate::heuristic::r#move::Move;
12use crate::heuristic::selector::move_selector::{CandidateId, MoveCandidateRef};
13use crate::phase::localsearch::{
14 Acceptor, LocalSearchForager, MoveCursorSource, SelectorCursorSource,
15};
16use crate::phase::Phase;
17use crate::scope::{PhaseScope, ProgressCallback, SolverScope};
18use crate::stats::{format_duration, whole_units_per_second, CandidateTracePullToken};
19
20mod candidates;
21mod step;
22
23use step::{execute_step, StepOutcome};
24
25const STEP_ACCEPTED_LABEL_LIMIT: usize = 32;
26
27#[derive(Clone, Copy)]
28struct StepMoveLabelCount {
29 label: &'static str,
30 count: u64,
31}
32
33struct StepMoveLabelCounts {
34 entries: [StepMoveLabelCount; STEP_ACCEPTED_LABEL_LIMIT],
35 overflow: u64,
36}
37
38impl StepMoveLabelCounts {
39 const EMPTY_ENTRY: StepMoveLabelCount = StepMoveLabelCount {
40 label: "",
41 count: 0,
42 };
43
44 fn new() -> Self {
45 Self {
46 entries: [Self::EMPTY_ENTRY; STEP_ACCEPTED_LABEL_LIMIT],
47 overflow: 0,
48 }
49 }
50
51 fn record(&mut self, label: &'static str) {
52 for entry in &mut self.entries {
53 if entry.count > 0 && entry.label == label {
54 entry.count += 1;
55 return;
56 }
57 }
58 for entry in &mut self.entries {
59 if entry.count == 0 {
60 entry.label = label;
61 entry.count = 1;
62 return;
63 }
64 }
65 self.overflow += 1;
66 }
67
68 fn for_each_ignored_except_selected(
69 &self,
70 selected_label: Option<&'static str>,
71 mut visitor: impl FnMut(&'static str, u64),
72 ) {
73 let mut selected_remaining = selected_label;
74 for entry in &self.entries {
75 if entry.count == 0 {
76 continue;
77 }
78 let ignored = if selected_remaining == Some(entry.label) {
79 selected_remaining = None;
80 entry.count.saturating_sub(1)
81 } else {
82 entry.count
83 };
84 if ignored > 0 {
85 visitor(entry.label, ignored);
86 }
87 }
88 if self.overflow > 0 {
89 visitor("move", self.overflow);
90 }
91 }
92}
93
94fn take_trace_token(
95 tokens: &mut Vec<(CandidateId, CandidateTracePullToken)>,
96 candidate_id: CandidateId,
97) -> Option<CandidateTracePullToken> {
98 tokens
99 .iter()
100 .position(|(recorded_id, _)| *recorded_id == candidate_id)
101 .map(|index| tokens.swap_remove(index).1)
102}
103
104pub struct LocalSearchPhase<S, M, Source, A, Fo>
125where
126 S: PlanningSolution,
127 M: Move<S>,
128 Source: MoveCursorSource<S, M>,
129 A: Acceptor<S>,
130 Fo: LocalSearchForager<S, M>,
131{
132 move_source: Source,
133 resources: Source::Resources,
134 acceptor: A,
135 forager: Fo,
136 step_limit: Option<u64>,
137 _phantom: PhantomData<fn() -> (S, M)>,
138}
139
140fn candidate_selector_label<S, M>(mov: &MoveCandidateRef<'_, S, M>) -> String
141where
142 S: PlanningSolution,
143 M: Move<S>,
144{
145 let move_label = mov.telemetry_label();
146 if mov.variable_name() == "compound_scalar" || mov.variable_name() == "conflict_repair" {
147 return format!("{}:{move_label}", mov.variable_name());
148 }
149 let mut label = None;
150 mov.for_each_affected_entity(&mut |affected| {
151 if label.is_none() {
152 label = Some(affected.variable_name.to_string());
153 }
154 });
155 label
156 .map(|variable| format!("{variable}:{move_label}"))
157 .unwrap_or_else(|| format!("move:{move_label}"))
158}
159
160impl<S, M, Source, A, Fo> LocalSearchPhase<S, M, Source, A, Fo>
161where
162 S: PlanningSolution,
163 M: Move<S> + 'static,
164 Source: MoveCursorSource<S, M>,
165 A: Acceptor<S>,
166 Fo: LocalSearchForager<S, M>,
167{
168 pub(crate) fn with_cursor_source(
175 move_source: Source,
176 resources: Source::Resources,
177 acceptor: A,
178 forager: Fo,
179 step_limit: Option<u64>,
180 ) -> Self {
181 Self {
182 move_source,
183 resources,
184 acceptor,
185 forager,
186 step_limit,
187 _phantom: PhantomData,
188 }
189 }
190}
191
192impl<S, M, MS, A, Fo> LocalSearchPhase<S, M, SelectorCursorSource<MS>, A, Fo>
193where
194 S: PlanningSolution,
195 M: Move<S> + 'static,
196 MS: crate::heuristic::selector::MoveSelector<S, M>,
197 A: Acceptor<S>,
198 Fo: LocalSearchForager<S, M>,
199{
200 pub fn new(move_selector: MS, acceptor: A, forager: Fo, step_limit: Option<u64>) -> Self {
205 Self::with_cursor_source(
206 SelectorCursorSource::new(move_selector),
207 (),
208 acceptor,
209 forager,
210 step_limit,
211 )
212 }
213}
214
215impl<S, M, Source, A, Fo> Debug for LocalSearchPhase<S, M, Source, A, Fo>
216where
217 S: PlanningSolution,
218 M: Move<S>,
219 Source: MoveCursorSource<S, M> + Debug,
220 A: Acceptor<S> + Debug,
221 Fo: LocalSearchForager<S, M> + Debug,
222{
223 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
224 f.debug_struct("LocalSearchPhase")
225 .field("move_source", &self.move_source)
226 .field("acceptor", &self.acceptor)
227 .field("forager", &self.forager)
228 .field("step_limit", &self.step_limit)
229 .finish()
230 }
231}
232
233#[allow(clippy::too_many_arguments)]
237pub(crate) fn solve_local_search_with_resources<S, D, BestCb, M, Source, A, Fo>(
238 move_source: &mut Source,
239 resources: &mut Source::Resources,
240 acceptor: &mut A,
241 forager: &mut Fo,
242 step_limit: Option<u64>,
243 solver_scope: &mut SolverScope<S, D, BestCb>,
244) where
245 S: PlanningSolution,
246 D: Director<S>,
247 BestCb: ProgressCallback<S>,
248 M: Move<S>,
249 Source: MoveCursorSource<S, M> + Debug + Send,
250 A: Acceptor<S>,
251 Fo: LocalSearchForager<S, M>,
252{
253 let mut phase_scope = PhaseScope::with_phase_type(solver_scope, 0, "Local Search");
254 let phase_index = phase_scope.phase_index();
255 let mut last_step_score = phase_scope.calculate_score();
256
257 info!(
258 event = "phase_start",
259 phase = "Local Search",
260 phase_index = phase_index,
261 score = %last_step_score,
262 );
263 acceptor.phase_started(&last_step_score);
264
265 let start_time = Instant::now();
266 loop {
267 if phase_scope.solver_scope_mut().should_terminate() {
268 break;
269 }
270 if step_limit.is_some_and(|limit| phase_scope.step_count() >= limit) {
271 break;
272 }
273
274 match execute_step(
275 move_source,
276 resources,
277 acceptor,
278 forager,
279 &mut phase_scope,
280 &mut last_step_score,
281 ) {
282 StepOutcome::Continue | StepOutcome::Restart => continue,
283 StepOutcome::Terminate => break,
284 }
285 }
286
287 acceptor.phase_ended();
288
289 let duration = start_time.elapsed();
290 let steps = phase_scope.step_count();
291 let stats = phase_scope.stats();
292 let speed = whole_units_per_second(stats.moves_evaluated, duration);
293 let acceptance_rate = stats.acceptance_rate() * 100.0;
294 let calc_speed = whole_units_per_second(stats.score_calculations, duration);
295 let best_score_str = phase_scope
296 .solver_scope()
297 .best_score()
298 .map(|s| format!("{s}"))
299 .unwrap_or_else(|| "none".to_string());
300
301 info!(
302 event = "phase_end",
303 phase = "Local Search",
304 phase_index = phase_index,
305 duration = %format_duration(duration),
306 steps = steps,
307 moves_generated = stats.moves_generated,
308 moves_evaluated = stats.moves_evaluated,
309 moves_accepted = stats.moves_accepted,
310 moves_score_improving = stats.moves_score_improving(),
311 moves_applied_improving = stats.moves_applied_improving(),
312 score_calculations = stats.score_calculations,
313 generation_time = %format_duration(stats.generation_time()),
314 evaluation_time = %format_duration(stats.evaluation_time()),
315 moves_speed = speed,
316 calc_speed = calc_speed,
317 acceptance_rate = format!("{acceptance_rate:.1}%"),
318 score = best_score_str,
319 );
320}
321
322impl<S, D, BestCb, M, Source, A, Fo> Phase<S, D, BestCb> for LocalSearchPhase<S, M, Source, A, Fo>
323where
324 S: PlanningSolution,
325 D: Director<S>,
326 BestCb: ProgressCallback<S>,
327 M: Move<S>,
328 Source: MoveCursorSource<S, M> + Debug + Send,
329 A: Acceptor<S>,
330 Fo: LocalSearchForager<S, M>,
331{
332 fn solve(&mut self, solver_scope: &mut SolverScope<S, D, BestCb>) {
333 let Self {
334 move_source,
335 resources,
336 acceptor,
337 forager,
338 step_limit,
339 ..
340 } = self;
341 solve_local_search_with_resources(
342 move_source,
343 resources,
344 acceptor,
345 forager,
346 *step_limit,
347 solver_scope,
348 );
349 }
350
351 fn phase_type_name(&self) -> &'static str {
352 "LocalSearch"
353 }
354}
355
356#[cfg(test)]
357mod tests;