solverforge_solver/phase/
basic.rs

1//! Basic variable phases for simple assignment problems.
2//!
3//! Provides construction and local search phases for solutions using
4//! `#[basic_variable_config]`, where each entity has a single planning
5//! variable assignable from a fixed value range.
6
7use std::fmt::{self, Debug};
8use std::marker::PhantomData;
9
10use rand::Rng;
11use solverforge_core::domain::PlanningSolution;
12use solverforge_core::score::Score;
13use solverforge_scoring::ScoreDirector;
14use tokio::sync::mpsc;
15use tracing::{debug, info, trace};
16
17use super::Phase;
18use crate::scope::{PhaseScope, SolverScope};
19
20/// Late acceptance history size.
21const LATE_ACCEPTANCE_SIZE: usize = 400;
22
23/// Construction phase for basic variable problems.
24///
25/// Randomly assigns values to uninitialized entities.
26///
27/// # Type Parameters
28/// * `S` - Solution type
29/// * `G` - Get variable function type
30/// * `T` - Set variable function type
31/// * `E` - Entity count function type
32/// * `V` - Value count function type
33pub struct BasicConstructionPhase<S, G, T, E, V>
34where
35    S: PlanningSolution,
36    G: Fn(&S, usize) -> Option<usize> + Send,
37    T: Fn(&mut S, usize, Option<usize>) + Send,
38    E: Fn(&S) -> usize + Send,
39    V: Fn(&S) -> usize + Send,
40{
41    get_variable: G,
42    set_variable: T,
43    entity_count: E,
44    value_count: V,
45    _phantom: PhantomData<fn() -> S>,
46}
47
48impl<S, G, T, E, V> Debug for BasicConstructionPhase<S, G, T, E, V>
49where
50    S: PlanningSolution,
51    G: Fn(&S, usize) -> Option<usize> + Send,
52    T: Fn(&mut S, usize, Option<usize>) + Send,
53    E: Fn(&S) -> usize + Send,
54    V: Fn(&S) -> usize + Send,
55{
56    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
57        f.debug_struct("BasicConstructionPhase").finish()
58    }
59}
60
61impl<S, G, T, E, V> BasicConstructionPhase<S, G, T, E, V>
62where
63    S: PlanningSolution,
64    G: Fn(&S, usize) -> Option<usize> + Send,
65    T: Fn(&mut S, usize, Option<usize>) + Send,
66    E: Fn(&S) -> usize + Send,
67    V: Fn(&S) -> usize + Send,
68{
69    /// Creates a new construction phase.
70    pub fn new(get_variable: G, set_variable: T, entity_count: E, value_count: V) -> Self {
71        Self {
72            get_variable,
73            set_variable,
74            entity_count,
75            value_count,
76            _phantom: PhantomData,
77        }
78    }
79}
80
81impl<S, D, G, T, E, V> Phase<S, D> for BasicConstructionPhase<S, G, T, E, V>
82where
83    S: PlanningSolution,
84    S::Score: Score,
85    D: ScoreDirector<S>,
86    G: Fn(&S, usize) -> Option<usize> + Send,
87    T: Fn(&mut S, usize, Option<usize>) + Send,
88    E: Fn(&S) -> usize + Send,
89    V: Fn(&S) -> usize + Send,
90{
91    fn solve(&mut self, solver_scope: &mut SolverScope<S, D>) {
92        let mut phase_scope = PhaseScope::new(solver_scope, 0);
93        let mut rng = rand::rng();
94
95        let n_entities = (self.entity_count)(phase_scope.solver_scope().working_solution());
96        let n_values = (self.value_count)(phase_scope.solver_scope().working_solution());
97
98        info!(
99            event = "phase_start",
100            phase = "Construction Heuristic",
101            phase_index = 0,
102        );
103
104        if n_entities == 0 || n_values == 0 {
105            phase_scope.update_best_solution();
106            info!(
107                event = "phase_end",
108                phase = "Construction Heuristic",
109                phase_index = 0,
110                duration_ms = phase_scope.elapsed().as_millis() as u64,
111                steps = 0u64,
112                speed = 0u64,
113                score = "N/A",
114            );
115            return;
116        }
117
118        for entity_idx in 0..n_entities {
119            if phase_scope.solver_scope().should_terminate() {
120                break;
121            }
122
123            if (self.get_variable)(phase_scope.solver_scope().working_solution(), entity_idx)
124                .is_none()
125            {
126                let value = rng.random_range(0..n_values);
127                (self.set_variable)(
128                    phase_scope.solver_scope_mut().working_solution_mut(),
129                    entity_idx,
130                    Some(value),
131                );
132            }
133            phase_scope.increment_step_count();
134        }
135
136        phase_scope.update_best_solution();
137
138        let best_score = phase_scope
139            .solver_scope()
140            .best_score()
141            .map(|s| format!("{s}"))
142            .unwrap_or_else(|| "none".to_string());
143
144        let duration = phase_scope.elapsed();
145        let steps = phase_scope.step_count();
146        let speed = if duration.as_secs_f64() > 0.0 {
147            (steps as f64 / duration.as_secs_f64()) as u64
148        } else {
149            0
150        };
151
152        info!(
153            event = "phase_end",
154            phase = "Construction Heuristic",
155            phase_index = 0,
156            duration_ms = duration.as_millis() as u64,
157            steps = steps,
158            speed = speed,
159            score = best_score,
160        );
161    }
162
163    fn phase_type_name(&self) -> &'static str {
164        "BasicConstruction"
165    }
166}
167
168/// Late acceptance local search phase for basic variable problems.
169///
170/// # Type Parameters
171/// * `S` - Solution type
172/// * `G` - Get variable function type
173/// * `T` - Set variable function type
174/// * `E` - Entity count function type
175/// * `V` - Value count function type
176pub struct BasicLocalSearchPhase<S, G, T, E, V>
177where
178    S: PlanningSolution,
179    G: Fn(&S, usize) -> Option<usize> + Send,
180    T: Fn(&mut S, usize, Option<usize>) + Send,
181    E: Fn(&S) -> usize + Send,
182    V: Fn(&S) -> usize + Send,
183{
184    get_variable: G,
185    set_variable: T,
186    entity_count: E,
187    value_count: V,
188    sender: mpsc::UnboundedSender<(S, S::Score)>,
189    _phantom: PhantomData<fn() -> S>,
190}
191
192impl<S, G, T, E, V> Debug for BasicLocalSearchPhase<S, G, T, E, V>
193where
194    S: PlanningSolution,
195    G: Fn(&S, usize) -> Option<usize> + Send,
196    T: Fn(&mut S, usize, Option<usize>) + Send,
197    E: Fn(&S) -> usize + Send,
198    V: Fn(&S) -> usize + Send,
199{
200    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
201        f.debug_struct("BasicLocalSearchPhase").finish()
202    }
203}
204
205impl<S, G, T, E, V> BasicLocalSearchPhase<S, G, T, E, V>
206where
207    S: PlanningSolution,
208    G: Fn(&S, usize) -> Option<usize> + Send,
209    T: Fn(&mut S, usize, Option<usize>) + Send,
210    E: Fn(&S) -> usize + Send,
211    V: Fn(&S) -> usize + Send,
212{
213    /// Creates a new local search phase with a channel sender.
214    pub fn new(
215        get_variable: G,
216        set_variable: T,
217        entity_count: E,
218        value_count: V,
219        sender: mpsc::UnboundedSender<(S, S::Score)>,
220    ) -> Self {
221        Self {
222            get_variable,
223            set_variable,
224            entity_count,
225            value_count,
226            sender,
227            _phantom: PhantomData,
228        }
229    }
230}
231
232impl<S, D, G, T, E, V> Phase<S, D> for BasicLocalSearchPhase<S, G, T, E, V>
233where
234    S: PlanningSolution,
235    S::Score: Score,
236    D: ScoreDirector<S>,
237    G: Fn(&S, usize) -> Option<usize> + Send,
238    T: Fn(&mut S, usize, Option<usize>) + Send,
239    E: Fn(&S) -> usize + Send,
240    V: Fn(&S) -> usize + Send,
241{
242    fn solve(&mut self, solver_scope: &mut SolverScope<S, D>) {
243        let mut phase_scope = PhaseScope::new(solver_scope, 1);
244        let mut rng = rand::rng();
245
246        let n_entities = (self.entity_count)(phase_scope.solver_scope().working_solution());
247        let n_values = (self.value_count)(phase_scope.solver_scope().working_solution());
248
249        info!(
250            event = "phase_start",
251            phase = "Late Acceptance",
252            phase_index = 1,
253        );
254
255        if n_entities == 0 || n_values == 0 {
256            info!(
257                event = "phase_end",
258                phase = "Late Acceptance",
259                phase_index = 1,
260                duration_ms = phase_scope.elapsed().as_millis() as u64,
261                steps = 0u64,
262                speed = 0u64,
263                score = "N/A",
264            );
265            return;
266        }
267
268        // Initialize current score
269        let initial_score = phase_scope.calculate_score();
270        let mut current_score = initial_score;
271        let mut best_score = initial_score;
272
273        // Late acceptance history
274        let mut late_scores = [initial_score; LATE_ACCEPTANCE_SIZE];
275        let mut moves_evaluated: u64 = 0;
276        let mut last_progress_time = std::time::Instant::now();
277        let mut last_progress_moves: u64 = 0;
278
279        // Send initial best through channel
280        {
281            let solution = phase_scope.solver_scope().working_solution().clone();
282            let _ = self.sender.send((solution, best_score));
283        }
284
285        loop {
286            if phase_scope.solver_scope().should_terminate() {
287                break;
288            }
289
290            let entity_idx = rng.random_range(0..n_entities);
291            let old_value =
292                (self.get_variable)(phase_scope.solver_scope().working_solution(), entity_idx);
293            let new_value = Some(rng.random_range(0..n_values));
294
295            if old_value == new_value {
296                continue;
297            }
298
299            moves_evaluated += 1;
300
301            // Log progress every second
302            let now = std::time::Instant::now();
303            if now.duration_since(last_progress_time).as_secs() >= 1 {
304                let moves_delta = moves_evaluated - last_progress_moves;
305                let elapsed_secs = now.duration_since(last_progress_time).as_secs_f64();
306                let current_speed = (moves_delta as f64 / elapsed_secs) as u64;
307                debug!(
308                    event = "progress",
309                    steps = phase_scope.step_count(),
310                    speed = current_speed,
311                    score = %best_score,
312                );
313                last_progress_time = now;
314                last_progress_moves = moves_evaluated;
315            }
316
317            // Apply move
318            let director = phase_scope.score_director_mut();
319            director.before_entity_changed(entity_idx);
320            (self.set_variable)(director.working_solution_mut(), entity_idx, new_value);
321            director.after_entity_changed(entity_idx);
322            let new_score = director.calculate_score();
323
324            let step = phase_scope.step_count();
325            let late_idx = (step as usize) % LATE_ACCEPTANCE_SIZE;
326            let late_score = late_scores[late_idx];
327
328            if new_score >= current_score || new_score >= late_score {
329                late_scores[late_idx] = new_score;
330                current_score = new_score;
331                let new_step = phase_scope.increment_step_count();
332
333                trace!(
334                    event = "step",
335                    step = new_step,
336                    entity = entity_idx,
337                    score = %new_score,
338                    accepted = true,
339                );
340
341                if new_score > best_score {
342                    best_score = new_score;
343                    phase_scope.update_best_solution();
344
345                    let solution = phase_scope.solver_scope().working_solution().clone();
346                    let _ = self.sender.send((solution, best_score));
347                }
348            } else {
349                trace!(
350                    event = "step",
351                    step = moves_evaluated,
352                    entity = entity_idx,
353                    score = %new_score,
354                    accepted = false,
355                );
356                // Undo move
357                let director = phase_scope.score_director_mut();
358                director.before_entity_changed(entity_idx);
359                (self.set_variable)(director.working_solution_mut(), entity_idx, old_value);
360                director.after_entity_changed(entity_idx);
361                director.calculate_score();
362            }
363        }
364
365        let duration = phase_scope.elapsed();
366        let speed = if duration.as_secs_f64() > 0.0 {
367            (moves_evaluated as f64 / duration.as_secs_f64()) as u64
368        } else {
369            0
370        };
371
372        let best_score_str = format!("{best_score}");
373        info!(
374            event = "phase_end",
375            phase = "Late Acceptance",
376            phase_index = 1,
377            duration_ms = duration.as_millis() as u64,
378            steps = phase_scope.step_count(),
379            speed = speed,
380            score = best_score_str,
381        );
382    }
383
384    fn phase_type_name(&self) -> &'static str {
385        "BasicLocalSearch"
386    }
387}