Skip to main content

solverforge_solver/phase/localsearch/
phase.rs

1// Local search phase implementation.
2
3use std::fmt::Debug;
4use std::marker::PhantomData;
5use std::time::Instant;
6
7use solverforge_core::domain::PlanningSolution;
8use solverforge_scoring::{Director, RecordingDirector};
9use tracing::{debug, info, trace};
10
11use crate::heuristic::r#move::Move;
12use crate::heuristic::selector::move_selector::MoveCursor;
13use crate::heuristic::selector::MoveSelector;
14use crate::phase::control::{
15    settle_search_interrupt, should_interrupt_evaluation, should_interrupt_generation,
16    StepInterrupt,
17};
18use crate::phase::localsearch::{Acceptor, LocalSearchForager};
19use crate::phase::Phase;
20use crate::scope::ProgressCallback;
21use crate::scope::{PhaseScope, SolverScope, StepScope};
22use crate::stats::{format_duration, whole_units_per_second};
23
24/// Local search phase that improves an existing solution.
25///
26/// This phase iteratively:
27/// 1. Generates candidate moves into an arena
28/// 2. Evaluates each move by index
29/// 3. Accepts/rejects based on the acceptor
30/// 4. Takes ownership of the best accepted move from arena
31///
32/// # Type Parameters
33/// * `S` - The planning solution type
34/// * `M` - The move type
35/// * `MS` - The move selector type
36/// * `A` - The acceptor type
37/// * `Fo` - The forager type
38///
39/// # Zero-Clone Design
40///
41/// Uses index-based foraging. The forager stores `(usize, Score)` pairs.
42/// When a move is selected, ownership transfers via `arena.take(index)`.
43/// Moves are NEVER cloned.
44pub struct LocalSearchPhase<S, M, MS, A, Fo>
45where
46    S: PlanningSolution,
47    M: Move<S>,
48    MS: MoveSelector<S, M>,
49    A: Acceptor<S>,
50    Fo: LocalSearchForager<S, M>,
51{
52    move_selector: MS,
53    acceptor: A,
54    forager: Fo,
55    step_limit: Option<u64>,
56    _phantom: PhantomData<fn() -> (S, M)>,
57}
58
59impl<S, M, MS, A, Fo> LocalSearchPhase<S, M, MS, A, Fo>
60where
61    S: PlanningSolution,
62    M: Move<S> + 'static,
63    MS: MoveSelector<S, M>,
64    A: Acceptor<S>,
65    Fo: LocalSearchForager<S, M>,
66{
67    pub fn new(move_selector: MS, acceptor: A, forager: Fo, step_limit: Option<u64>) -> Self {
68        Self {
69            move_selector,
70            acceptor,
71            forager,
72            step_limit,
73            _phantom: PhantomData,
74        }
75    }
76}
77
78impl<S, M, MS, A, Fo> Debug for LocalSearchPhase<S, M, MS, A, Fo>
79where
80    S: PlanningSolution,
81    M: Move<S>,
82    MS: MoveSelector<S, M> + Debug,
83    A: Acceptor<S> + Debug,
84    Fo: LocalSearchForager<S, M> + Debug,
85{
86    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
87        f.debug_struct("LocalSearchPhase")
88            .field("move_selector", &self.move_selector)
89            .field("acceptor", &self.acceptor)
90            .field("forager", &self.forager)
91            .field("step_limit", &self.step_limit)
92            .finish()
93    }
94}
95
96impl<S, D, BestCb, M, MS, A, Fo> Phase<S, D, BestCb> for LocalSearchPhase<S, M, MS, A, Fo>
97where
98    S: PlanningSolution,
99    D: Director<S>,
100    BestCb: ProgressCallback<S>,
101    M: Move<S>,
102    MS: MoveSelector<S, M>,
103    A: Acceptor<S>,
104    Fo: LocalSearchForager<S, M>,
105{
106    fn solve(&mut self, solver_scope: &mut SolverScope<S, D, BestCb>) {
107        let mut phase_scope = PhaseScope::new(solver_scope, 0);
108        let phase_index = phase_scope.phase_index();
109
110        // Calculate initial score
111        let mut last_step_score = phase_scope.calculate_score();
112
113        info!(
114            event = "phase_start",
115            phase = "Local Search",
116            phase_index = phase_index,
117            score = %last_step_score,
118        );
119
120        // Notify acceptor of phase start
121        self.acceptor.phase_started(&last_step_score);
122
123        let start_time = Instant::now();
124        let mut local_moves_generated: u64 = 0;
125        let mut local_moves_evaluated: u64 = 0;
126        let mut last_progress_time = Instant::now();
127        let mut last_progress_moves: u64 = 0;
128        loop {
129            // Check early termination
130            if phase_scope.solver_scope_mut().should_terminate() {
131                break;
132            }
133
134            // Check step limit
135            if let Some(limit) = self.step_limit {
136                if phase_scope.step_count() >= limit {
137                    break;
138                }
139            }
140
141            let mut step_scope = StepScope::new(&mut phase_scope);
142
143            /* Reset forager and acceptor for this step.
144            Pass best and last-step scores so foragers that implement
145            pick-early-on-improvement strategies know their reference targets.
146            */
147            let best_score = step_scope
148                .phase_scope()
149                .solver_scope()
150                .best_score()
151                .copied()
152                .unwrap_or(last_step_score);
153            self.forager.step_started(best_score, last_step_score);
154            self.acceptor.step_started();
155            let requires_move_signatures = self.acceptor.requires_move_signatures();
156
157            let mut interrupted_step = false;
158            let mut generated_moves = 0usize;
159            let mut evaluated_moves = 0usize;
160            let generation_started = Instant::now();
161            let mut cursor = self.move_selector.open_cursor(step_scope.score_director());
162            step_scope
163                .phase_scope_mut()
164                .record_generation_time(generation_started.elapsed());
165
166            while !self.forager.is_quit_early() {
167                if should_interrupt_generation(&step_scope, generated_moves) {
168                    interrupted_step = true;
169                    break;
170                }
171
172                let generation_started = Instant::now();
173                let Some((candidate_index, mov)) = cursor.next_candidate() else {
174                    break;
175                };
176                let generation_elapsed = generation_started.elapsed();
177                generated_moves += 1;
178                local_moves_generated += 1;
179                step_scope
180                    .phase_scope_mut()
181                    .record_generated_move(generation_elapsed);
182
183                if should_interrupt_evaluation(&step_scope, evaluated_moves) {
184                    interrupted_step = true;
185                    break;
186                }
187                evaluated_moves += 1;
188                local_moves_evaluated += 1;
189
190                if local_moves_evaluated & 0x1FFF == 0 {
191                    let now = Instant::now();
192                    if now.duration_since(last_progress_time).as_secs() >= 1 {
193                        let current_speed = whole_units_per_second(
194                            local_moves_evaluated - last_progress_moves,
195                            now.duration_since(last_progress_time),
196                        );
197                        debug!(
198                            event = "progress",
199                            steps = step_scope.step_index(),
200                            moves_generated = local_moves_generated,
201                            moves_evaluated = local_moves_evaluated,
202                            moves_accepted = step_scope.phase_scope().solver_scope().stats().moves_accepted,
203                            score_calculations = step_scope.phase_scope().solver_scope().stats().score_calculations,
204                            speed = current_speed,
205                            acceptance_rate = format!(
206                                "{:.1}%",
207                                step_scope.phase_scope().solver_scope().stats().acceptance_rate() * 100.0
208                            ),
209                            current_score = %last_step_score,
210                            best_score = %best_score,
211                        );
212                        step_scope.phase_scope().solver_scope().report_progress();
213                        last_progress_time = now;
214                        last_progress_moves = local_moves_evaluated;
215                    }
216                }
217
218                let evaluation_started = Instant::now();
219                if !mov.is_doable(step_scope.score_director()) {
220                    step_scope
221                        .phase_scope_mut()
222                        .record_evaluated_move(evaluation_started.elapsed());
223                    continue;
224                }
225
226                let move_score = {
227                    let mut recording = RecordingDirector::new(step_scope.score_director_mut());
228                    mov.do_move(&mut recording);
229                    let score = recording.calculate_score();
230                    recording.undo_changes();
231                    score
232                };
233
234                step_scope.phase_scope_mut().record_score_calculation();
235
236                let move_signature = if requires_move_signatures {
237                    Some(mov.tabu_signature(step_scope.score_director()))
238                } else {
239                    None
240                };
241
242                let accepted = self.acceptor.is_accepted(
243                    &last_step_score,
244                    &move_score,
245                    move_signature.as_ref(),
246                );
247
248                step_scope
249                    .phase_scope_mut()
250                    .record_evaluated_move(evaluation_started.elapsed());
251                if accepted {
252                    step_scope.phase_scope_mut().record_move_accepted();
253                }
254
255                trace!(
256                    event = "step",
257                    step = step_scope.step_index(),
258                    move_index = candidate_index,
259                    score = %move_score,
260                    accepted = accepted,
261                );
262
263                if accepted {
264                    self.forager.add_move_index(candidate_index, move_score);
265                }
266            }
267
268            if interrupted_step {
269                match settle_search_interrupt(&mut step_scope) {
270                    StepInterrupt::Restart => continue,
271                    StepInterrupt::TerminatePhase => break,
272                }
273            }
274
275            // Pick the best accepted move index
276            let mut accepted_move_signature = None;
277            if let Some((selected_index, selected_score)) = self.forager.pick_move_index() {
278                let selected_move = cursor.take_candidate(selected_index);
279                if requires_move_signatures {
280                    accepted_move_signature =
281                        Some(selected_move.tabu_signature(step_scope.score_director()));
282                }
283                step_scope.apply_committed_move(&selected_move);
284                step_scope.set_step_score(selected_score);
285
286                // Update last step score
287                last_step_score = selected_score;
288
289                // Update best solution if improved
290                step_scope.phase_scope_mut().update_best_solution();
291            }
292            /* else: no accepted moves this step — that's fine, the acceptor
293            history still needs to advance so Late Acceptance / SA / etc.
294            can eventually escape the local optimum.
295            */
296
297            /* Always notify acceptor that step ended. For stateful acceptors
298            (Late Acceptance, Simulated Annealing, Great Deluge, SCHC),
299            the history must advance every step — even steps where no move
300            was accepted — otherwise the acceptor state stalls.
301            */
302            self.acceptor
303                .step_ended(&last_step_score, accepted_move_signature.as_ref());
304
305            step_scope.complete();
306        }
307
308        // Notify acceptor of phase end
309        self.acceptor.phase_ended();
310
311        let duration = start_time.elapsed();
312        let steps = phase_scope.step_count();
313        let stats = phase_scope.stats();
314        let speed = whole_units_per_second(stats.moves_evaluated, duration);
315        let acceptance_rate = stats.acceptance_rate() * 100.0;
316        let calc_speed = whole_units_per_second(stats.score_calculations, duration);
317
318        let best_score_str = phase_scope
319            .solver_scope()
320            .best_score()
321            .map(|s| format!("{}", s))
322            .unwrap_or_else(|| "none".to_string());
323
324        info!(
325            event = "phase_end",
326            phase = "Local Search",
327            phase_index = phase_index,
328            duration = %format_duration(duration),
329            steps = steps,
330            moves_generated = stats.moves_generated,
331            moves_evaluated = stats.moves_evaluated,
332            moves_accepted = stats.moves_accepted,
333            score_calculations = stats.score_calculations,
334            generation_time = %format_duration(stats.generation_time()),
335            evaluation_time = %format_duration(stats.evaluation_time()),
336            moves_speed = speed,
337            calc_speed = calc_speed,
338            acceptance_rate = format!("{:.1}%", acceptance_rate),
339            score = best_score_str,
340        );
341    }
342
343    fn phase_type_name(&self) -> &'static str {
344        "LocalSearch"
345    }
346}
347
348#[cfg(test)]
349mod tests;