solverforge_solver/manager/phase_factory/
local_search.rs

1//! Local search phase factory.
2
3use std::marker::PhantomData;
4
5use solverforge_core::domain::PlanningSolution;
6
7use crate::heuristic::{Move, MoveSelector};
8use crate::phase::localsearch::{
9    AcceptedCountForager, Acceptor, HillClimbingAcceptor, LateAcceptanceAcceptor,
10    LocalSearchForager, LocalSearchPhase, MoveTabuAcceptor, SimulatedAnnealingAcceptor,
11    TabuSearchAcceptor, ValueTabuAcceptor,
12};
13use crate::phase::Phase;
14
15use super::super::config::LocalSearchType;
16use super::super::SolverPhaseFactory;
17
18/// Factory for creating local search phases.
19///
20/// Local search phases improve an existing solution by exploring neighboring
21/// solutions. The factory provides fresh phase instances for each solve,
22/// ensuring that internal state (like tabu lists or temperature) is reset.
23///
24/// # Type Parameters
25///
26/// * `S` - The planning solution type
27/// * `M` - The move type (e.g., [`ChangeMove`](crate::heuristic::ChangeMove),
28///   [`SwapMove`](crate::heuristic::SwapMove))
29/// * `F` - The closure type that creates move selectors
30///
31/// # Available Acceptors
32///
33/// - [`hill_climbing`](Self::hill_climbing): Only accept improving moves
34/// - [`tabu_search`](Self::tabu_search): Avoid recently visited states
35/// - [`simulated_annealing`](Self::simulated_annealing): Probabilistic acceptance
36/// - [`late_acceptance`](Self::late_acceptance): Compare against historical scores
37///
38/// # Example
39///
40/// ```
41/// use solverforge_solver::manager::{LocalSearchPhaseFactory, SolverPhaseFactory};
42/// use solverforge_solver::heuristic::r#move::ChangeMove;
43/// use solverforge_solver::heuristic::selector::ChangeMoveSelector;
44/// use solverforge_core::domain::PlanningSolution;
45/// use solverforge_core::score::SimpleScore;
46///
47/// #[derive(Clone)]
48/// struct S { values: Vec<Option<i32>>, score: Option<SimpleScore> }
49/// impl PlanningSolution for S {
50///     type Score = SimpleScore;
51///     fn score(&self) -> Option<Self::Score> { self.score }
52///     fn set_score(&mut self, score: Option<Self::Score>) { self.score = score; }
53/// }
54///
55/// fn get_v(s: &S, idx: usize) -> Option<i32> { s.values.get(idx).copied().flatten() }
56/// fn set_v(s: &mut S, idx: usize, v: Option<i32>) { if let Some(x) = s.values.get_mut(idx) { *x = v; } }
57///
58/// type M = ChangeMove<S, i32>;
59///
60/// // Hill climbing with step limit
61/// let factory = LocalSearchPhaseFactory::<S, M, _>::hill_climbing(|| {
62///     Box::new(ChangeMoveSelector::<S, i32>::simple(get_v, set_v, 0, "value", vec![1, 2, 3]))
63/// }).with_step_limit(1000);
64///
65/// // Tabu search
66/// let tabu = LocalSearchPhaseFactory::<S, M, _>::tabu_search(7, || {
67///     Box::new(ChangeMoveSelector::<S, i32>::simple(get_v, set_v, 0, "value", vec![1, 2, 3]))
68/// });
69///
70/// // Simulated annealing
71/// let sa = LocalSearchPhaseFactory::<S, M, _>::simulated_annealing(1.0, 0.999, || {
72///     Box::new(ChangeMoveSelector::<S, i32>::simple(get_v, set_v, 0, "value", vec![1, 2, 3]))
73/// });
74/// ```
75pub struct LocalSearchPhaseFactory<S, M, F>
76where
77    S: PlanningSolution,
78    M: Move<S> + Clone + Send + Sync + 'static,
79    F: Fn() -> Box<dyn MoveSelector<S, M>> + Send + Sync,
80{
81    search_type: LocalSearchType,
82    step_limit: Option<u64>,
83    move_selector_factory: F,
84    _marker: PhantomData<(S, M)>,
85}
86
87impl<S, M, F> LocalSearchPhaseFactory<S, M, F>
88where
89    S: PlanningSolution,
90    M: Move<S> + Clone + Send + Sync + 'static,
91    F: Fn() -> Box<dyn MoveSelector<S, M>> + Send + Sync,
92{
93    /// Creates a new local search phase factory with the specified search type.
94    ///
95    /// # Arguments
96    ///
97    /// * `search_type` - The type of local search algorithm
98    /// * `move_selector_factory` - A closure that creates move selectors
99    ///
100    /// # Example
101    ///
102    /// ```
103    /// use solverforge_solver::manager::{LocalSearchType, LocalSearchPhaseFactory};
104    /// use solverforge_solver::heuristic::r#move::ChangeMove;
105    /// use solverforge_solver::heuristic::selector::ChangeMoveSelector;
106    /// use solverforge_core::domain::PlanningSolution;
107    /// use solverforge_core::score::SimpleScore;
108    ///
109    /// # #[derive(Clone)] struct S { values: Vec<Option<i32>>, score: Option<SimpleScore> }
110    /// # impl PlanningSolution for S {
111    /// #     type Score = SimpleScore;
112    /// #     fn score(&self) -> Option<Self::Score> { self.score }
113    /// #     fn set_score(&mut self, score: Option<Self::Score>) { self.score = score; }
114    /// # }
115    /// # fn get_v(s: &S, idx: usize) -> Option<i32> { s.values.get(idx).copied().flatten() }
116    /// # fn set_v(s: &mut S, idx: usize, v: Option<i32>) { if let Some(x) = s.values.get_mut(idx) { *x = v; } }
117    /// type M = ChangeMove<S, i32>;
118    ///
119    /// let factory = LocalSearchPhaseFactory::<S, M, _>::new(
120    ///     LocalSearchType::TabuSearch { tabu_size: 10 },
121    ///     || Box::new(ChangeMoveSelector::<S, i32>::simple(get_v, set_v, 0, "v", vec![1, 2])),
122    /// );
123    /// ```
124    pub fn new(search_type: LocalSearchType, move_selector_factory: F) -> Self {
125        Self {
126            search_type,
127            step_limit: None,
128            move_selector_factory,
129            _marker: PhantomData,
130        }
131    }
132
133    /// Sets the step limit for this phase.
134    ///
135    /// The phase will terminate after executing this many steps. This is useful
136    /// for multi-phase configurations where you want to limit time spent in each phase.
137    ///
138    /// # Example
139    ///
140    /// ```
141    /// use solverforge_solver::manager::LocalSearchPhaseFactory;
142    /// use solverforge_solver::heuristic::r#move::ChangeMove;
143    /// use solverforge_solver::heuristic::selector::ChangeMoveSelector;
144    /// use solverforge_core::domain::PlanningSolution;
145    /// use solverforge_core::score::SimpleScore;
146    ///
147    /// # #[derive(Clone)] struct S { values: Vec<Option<i32>>, score: Option<SimpleScore> }
148    /// # impl PlanningSolution for S {
149    /// #     type Score = SimpleScore;
150    /// #     fn score(&self) -> Option<Self::Score> { self.score }
151    /// #     fn set_score(&mut self, score: Option<Self::Score>) { self.score = score; }
152    /// # }
153    /// # fn get_v(s: &S, idx: usize) -> Option<i32> { s.values.get(idx).copied().flatten() }
154    /// # fn set_v(s: &mut S, idx: usize, v: Option<i32>) { if let Some(x) = s.values.get_mut(idx) { *x = v; } }
155    /// type M = ChangeMove<S, i32>;
156    ///
157    /// let factory = LocalSearchPhaseFactory::<S, M, _>::hill_climbing(|| {
158    ///     Box::new(ChangeMoveSelector::<S, i32>::simple(get_v, set_v, 0, "v", vec![1, 2]))
159    /// }).with_step_limit(500);
160    /// ```
161    pub fn with_step_limit(mut self, limit: u64) -> Self {
162        self.step_limit = Some(limit);
163        self
164    }
165
166    /// Creates a factory with hill climbing acceptor.
167    ///
168    /// Hill climbing only accepts moves that improve the score. It is simple
169    /// and fast, but can easily get stuck in local optima.
170    ///
171    /// # Example
172    ///
173    /// ```
174    /// use solverforge_solver::manager::LocalSearchPhaseFactory;
175    /// use solverforge_solver::heuristic::r#move::ChangeMove;
176    /// use solverforge_solver::heuristic::selector::ChangeMoveSelector;
177    /// use solverforge_core::domain::PlanningSolution;
178    /// use solverforge_core::score::SimpleScore;
179    ///
180    /// # #[derive(Clone)] struct S { values: Vec<Option<i32>>, score: Option<SimpleScore> }
181    /// # impl PlanningSolution for S {
182    /// #     type Score = SimpleScore;
183    /// #     fn score(&self) -> Option<Self::Score> { self.score }
184    /// #     fn set_score(&mut self, score: Option<Self::Score>) { self.score = score; }
185    /// # }
186    /// # fn get_v(s: &S, idx: usize) -> Option<i32> { s.values.get(idx).copied().flatten() }
187    /// # fn set_v(s: &mut S, idx: usize, v: Option<i32>) { if let Some(x) = s.values.get_mut(idx) { *x = v; } }
188    /// type M = ChangeMove<S, i32>;
189    ///
190    /// let factory = LocalSearchPhaseFactory::<S, M, _>::hill_climbing(|| {
191    ///     Box::new(ChangeMoveSelector::<S, i32>::simple(get_v, set_v, 0, "value", vec![1, 2, 3]))
192    /// });
193    /// ```
194    pub fn hill_climbing(move_selector_factory: F) -> Self {
195        Self::new(LocalSearchType::HillClimbing, move_selector_factory)
196    }
197
198    /// Creates a factory with tabu search acceptor.
199    ///
200    /// Tabu search maintains a list of recently made moves and forbids
201    /// reversing them. This helps escape local optima by forcing exploration.
202    ///
203    /// # Arguments
204    ///
205    /// * `tabu_size` - Number of recent moves to remember
206    /// * `move_selector_factory` - Closure that creates move selectors
207    ///
208    /// # Example
209    ///
210    /// ```
211    /// use solverforge_solver::manager::LocalSearchPhaseFactory;
212    /// use solverforge_solver::heuristic::r#move::ChangeMove;
213    /// use solverforge_solver::heuristic::selector::ChangeMoveSelector;
214    /// use solverforge_core::domain::PlanningSolution;
215    /// use solverforge_core::score::SimpleScore;
216    ///
217    /// # #[derive(Clone)] struct S { values: Vec<Option<i32>>, score: Option<SimpleScore> }
218    /// # impl PlanningSolution for S {
219    /// #     type Score = SimpleScore;
220    /// #     fn score(&self) -> Option<Self::Score> { self.score }
221    /// #     fn set_score(&mut self, score: Option<Self::Score>) { self.score = score; }
222    /// # }
223    /// # fn get_v(s: &S, idx: usize) -> Option<i32> { s.values.get(idx).copied().flatten() }
224    /// # fn set_v(s: &mut S, idx: usize, v: Option<i32>) { if let Some(x) = s.values.get_mut(idx) { *x = v; } }
225    /// type M = ChangeMove<S, i32>;
226    ///
227    /// // Remember last 7 moves
228    /// let factory = LocalSearchPhaseFactory::<S, M, _>::tabu_search(7, || {
229    ///     Box::new(ChangeMoveSelector::<S, i32>::simple(get_v, set_v, 0, "v", vec![1, 2, 3]))
230    /// });
231    /// ```
232    pub fn tabu_search(tabu_size: usize, move_selector_factory: F) -> Self {
233        Self::new(
234            LocalSearchType::TabuSearch { tabu_size },
235            move_selector_factory,
236        )
237    }
238
239    /// Creates a factory with simulated annealing acceptor.
240    ///
241    /// Simulated annealing accepts worse moves with a probability that decreases
242    /// over time. Initially, it explores widely; as the "temperature" cools,
243    /// it becomes more selective.
244    ///
245    /// # Arguments
246    ///
247    /// * `starting_temp` - Initial temperature (higher = more exploration)
248    /// * `decay_rate` - Rate at which temperature decreases (0.0 to 1.0)
249    /// * `move_selector_factory` - Closure that creates move selectors
250    ///
251    /// # Example
252    ///
253    /// ```
254    /// use solverforge_solver::manager::LocalSearchPhaseFactory;
255    /// use solverforge_solver::heuristic::r#move::ChangeMove;
256    /// use solverforge_solver::heuristic::selector::ChangeMoveSelector;
257    /// use solverforge_core::domain::PlanningSolution;
258    /// use solverforge_core::score::SimpleScore;
259    ///
260    /// # #[derive(Clone)] struct S { values: Vec<Option<i32>>, score: Option<SimpleScore> }
261    /// # impl PlanningSolution for S {
262    /// #     type Score = SimpleScore;
263    /// #     fn score(&self) -> Option<Self::Score> { self.score }
264    /// #     fn set_score(&mut self, score: Option<Self::Score>) { self.score = score; }
265    /// # }
266    /// # fn get_v(s: &S, idx: usize) -> Option<i32> { s.values.get(idx).copied().flatten() }
267    /// # fn set_v(s: &mut S, idx: usize, v: Option<i32>) { if let Some(x) = s.values.get_mut(idx) { *x = v; } }
268    /// type M = ChangeMove<S, i32>;
269    ///
270    /// // Start at temperature 1.0, decay by 0.1% per step
271    /// let factory = LocalSearchPhaseFactory::<S, M, _>::simulated_annealing(
272    ///     1.0,
273    ///     0.999,
274    ///     || Box::new(ChangeMoveSelector::<S, i32>::simple(get_v, set_v, 0, "v", vec![1, 2, 3])),
275    /// );
276    /// ```
277    pub fn simulated_annealing(
278        starting_temp: f64,
279        decay_rate: f64,
280        move_selector_factory: F,
281    ) -> Self {
282        Self::new(
283            LocalSearchType::SimulatedAnnealing {
284                starting_temp,
285                decay_rate,
286            },
287            move_selector_factory,
288        )
289    }
290
291    /// Creates a factory with late acceptance acceptor.
292    ///
293    /// Late acceptance compares the new score against the score from N steps ago.
294    /// If the new score is better than or equal to that historical score, the move
295    /// is accepted. This provides a balance between exploration and exploitation.
296    ///
297    /// # Arguments
298    ///
299    /// * `size` - Number of steps to look back
300    /// * `move_selector_factory` - Closure that creates move selectors
301    ///
302    /// # Example
303    ///
304    /// ```
305    /// use solverforge_solver::manager::LocalSearchPhaseFactory;
306    /// use solverforge_solver::heuristic::r#move::ChangeMove;
307    /// use solverforge_solver::heuristic::selector::ChangeMoveSelector;
308    /// use solverforge_core::domain::PlanningSolution;
309    /// use solverforge_core::score::SimpleScore;
310    ///
311    /// # #[derive(Clone)] struct S { values: Vec<Option<i32>>, score: Option<SimpleScore> }
312    /// # impl PlanningSolution for S {
313    /// #     type Score = SimpleScore;
314    /// #     fn score(&self) -> Option<Self::Score> { self.score }
315    /// #     fn set_score(&mut self, score: Option<Self::Score>) { self.score = score; }
316    /// # }
317    /// # fn get_v(s: &S, idx: usize) -> Option<i32> { s.values.get(idx).copied().flatten() }
318    /// # fn set_v(s: &mut S, idx: usize, v: Option<i32>) { if let Some(x) = s.values.get_mut(idx) { *x = v; } }
319    /// type M = ChangeMove<S, i32>;
320    ///
321    /// // Compare against score from 400 steps ago
322    /// let factory = LocalSearchPhaseFactory::<S, M, _>::late_acceptance(400, || {
323    ///     Box::new(ChangeMoveSelector::<S, i32>::simple(get_v, set_v, 0, "v", vec![1, 2, 3]))
324    /// });
325    /// ```
326    pub fn late_acceptance(size: usize, move_selector_factory: F) -> Self {
327        Self::new(
328            LocalSearchType::LateAcceptance { size },
329            move_selector_factory,
330        )
331    }
332
333    /// Creates a factory with value tabu acceptor.
334    ///
335    /// Value tabu remembers recently assigned values and forbids reassigning them.
336    /// This is different from entity tabu in that it tracks values, not entity-variable
337    /// combinations.
338    ///
339    /// # Arguments
340    ///
341    /// * `value_tabu_size` - Number of recent values to forbid
342    /// * `move_selector_factory` - Closure that creates move selectors
343    ///
344    /// # Example
345    ///
346    /// ```
347    /// use solverforge_solver::manager::LocalSearchPhaseFactory;
348    /// use solverforge_solver::heuristic::r#move::ChangeMove;
349    /// use solverforge_solver::heuristic::selector::ChangeMoveSelector;
350    /// use solverforge_core::domain::PlanningSolution;
351    /// use solverforge_core::score::SimpleScore;
352    ///
353    /// # #[derive(Clone)] struct S { values: Vec<Option<i32>>, score: Option<SimpleScore> }
354    /// # impl PlanningSolution for S {
355    /// #     type Score = SimpleScore;
356    /// #     fn score(&self) -> Option<Self::Score> { self.score }
357    /// #     fn set_score(&mut self, score: Option<Self::Score>) { self.score = score; }
358    /// # }
359    /// # fn get_v(s: &S, idx: usize) -> Option<i32> { s.values.get(idx).copied().flatten() }
360    /// # fn set_v(s: &mut S, idx: usize, v: Option<i32>) { if let Some(x) = s.values.get_mut(idx) { *x = v; } }
361    /// type M = ChangeMove<S, i32>;
362    ///
363    /// // Forbid last 5 assigned values
364    /// let factory = LocalSearchPhaseFactory::<S, M, _>::value_tabu_search(5, || {
365    ///     Box::new(ChangeMoveSelector::<S, i32>::simple(get_v, set_v, 0, "v", vec![1, 2, 3]))
366    /// });
367    /// ```
368    pub fn value_tabu_search(value_tabu_size: usize, move_selector_factory: F) -> Self {
369        Self::new(
370            LocalSearchType::ValueTabuSearch { value_tabu_size },
371            move_selector_factory,
372        )
373    }
374
375    /// Creates a factory with move tabu acceptor.
376    ///
377    /// Move tabu remembers recently made moves (by hash) and forbids making the same
378    /// move again. Supports aspiration criterion: tabu moves can be accepted if they
379    /// lead to a new best solution.
380    ///
381    /// # Arguments
382    ///
383    /// * `move_tabu_size` - Number of recent moves to forbid
384    /// * `aspiration_enabled` - Whether to allow tabu moves that reach new best score
385    /// * `move_selector_factory` - Closure that creates move selectors
386    ///
387    /// # Example
388    ///
389    /// ```
390    /// use solverforge_solver::manager::LocalSearchPhaseFactory;
391    /// use solverforge_solver::heuristic::r#move::ChangeMove;
392    /// use solverforge_solver::heuristic::selector::ChangeMoveSelector;
393    /// use solverforge_core::domain::PlanningSolution;
394    /// use solverforge_core::score::SimpleScore;
395    ///
396    /// # #[derive(Clone)] struct S { values: Vec<Option<i32>>, score: Option<SimpleScore> }
397    /// # impl PlanningSolution for S {
398    /// #     type Score = SimpleScore;
399    /// #     fn score(&self) -> Option<Self::Score> { self.score }
400    /// #     fn set_score(&mut self, score: Option<Self::Score>) { self.score = score; }
401    /// # }
402    /// # fn get_v(s: &S, idx: usize) -> Option<i32> { s.values.get(idx).copied().flatten() }
403    /// # fn set_v(s: &mut S, idx: usize, v: Option<i32>) { if let Some(x) = s.values.get_mut(idx) { *x = v; } }
404    /// type M = ChangeMove<S, i32>;
405    ///
406    /// // Forbid last 10 moves, with aspiration enabled
407    /// let factory = LocalSearchPhaseFactory::<S, M, _>::move_tabu_search(10, true, || {
408    ///     Box::new(ChangeMoveSelector::<S, i32>::simple(get_v, set_v, 0, "v", vec![1, 2, 3]))
409    /// });
410    /// ```
411    pub fn move_tabu_search(
412        move_tabu_size: usize,
413        aspiration_enabled: bool,
414        move_selector_factory: F,
415    ) -> Self {
416        Self::new(
417            LocalSearchType::MoveTabuSearch {
418                move_tabu_size,
419                aspiration_enabled,
420            },
421            move_selector_factory,
422        )
423    }
424
425    fn create_acceptor(&self) -> Box<dyn Acceptor<S>> {
426        match self.search_type {
427            LocalSearchType::HillClimbing => Box::new(HillClimbingAcceptor::new()),
428            LocalSearchType::TabuSearch { tabu_size } => {
429                Box::new(TabuSearchAcceptor::new(tabu_size))
430            }
431            LocalSearchType::SimulatedAnnealing {
432                starting_temp,
433                decay_rate,
434            } => Box::new(SimulatedAnnealingAcceptor::new(starting_temp, decay_rate)),
435            LocalSearchType::LateAcceptance { size } => Box::new(LateAcceptanceAcceptor::new(size)),
436            LocalSearchType::ValueTabuSearch { value_tabu_size } => {
437                Box::new(ValueTabuAcceptor::new(value_tabu_size))
438            }
439            LocalSearchType::MoveTabuSearch {
440                move_tabu_size,
441                aspiration_enabled,
442            } => {
443                if aspiration_enabled {
444                    Box::new(MoveTabuAcceptor::new(move_tabu_size))
445                } else {
446                    Box::new(MoveTabuAcceptor::without_aspiration(move_tabu_size))
447                }
448            }
449        }
450    }
451}
452
453impl<S, M, F> SolverPhaseFactory<S> for LocalSearchPhaseFactory<S, M, F>
454where
455    S: PlanningSolution + 'static,
456    M: Move<S> + Clone + Send + Sync + 'static,
457    F: Fn() -> Box<dyn MoveSelector<S, M>> + Send + Sync,
458{
459    fn create_phase(&self) -> Box<dyn Phase<S>> {
460        let move_selector = (self.move_selector_factory)();
461        let acceptor = self.create_acceptor();
462        let forager: Box<dyn LocalSearchForager<S, M>> = Box::new(AcceptedCountForager::new(1));
463
464        Box::new(LocalSearchPhase::new(
465            move_selector,
466            acceptor,
467            forager,
468            self.step_limit,
469        ))
470    }
471}