solverforge_solver/phase/localsearch/acceptor/tabu_search.rs
1// Tabu search acceptor.
2
3use std::fmt::Debug;
4
5use solverforge_core::domain::PlanningSolution;
6
7use super::Acceptor;
8
9/// Tabu search acceptor - maintains a tabu list of recently visited solutions.
10///
11/// Tabu search prevents revisiting recently explored solutions by maintaining
12/// a tabu list. This helps escape local optima and prevents cycling.
13///
14/// This implementation tracks recent scores to identify solutions that should
15/// be forbidden (tabu). A more sophisticated implementation would track the
16/// actual moves or entity changes.
17///
18/// # Example
19///
20/// ```
21/// use solverforge_solver::phase::localsearch::TabuSearchAcceptor;
22/// use solverforge_core::score::SoftScore;
23/// use solverforge_core::domain::PlanningSolution;
24///
25/// #[derive(Clone)]
26/// struct MySolution;
27/// impl PlanningSolution for MySolution {
28/// type Score = SoftScore;
29/// fn score(&self) -> Option<Self::Score> { None }
30/// fn set_score(&mut self, _: Option<Self::Score>) {}
31/// }
32///
33/// let acceptor = TabuSearchAcceptor::<MySolution>::new(7);
34/// ```
35pub struct TabuSearchAcceptor<S: PlanningSolution> {
36 // Maximum size of the tabu list.
37 tabu_size: usize,
38 // List of tabu (forbidden) scores.
39 tabu_list: Vec<S::Score>,
40 // Whether to accept improving moves even if tabu.
41 aspiration_enabled: bool,
42 // Best score seen so far (for aspiration criterion).
43 best_score: Option<S::Score>,
44}
45
46impl<S: PlanningSolution> Debug for TabuSearchAcceptor<S> {
47 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
48 f.debug_struct("TabuSearchAcceptor")
49 .field("tabu_size", &self.tabu_size)
50 .field("tabu_list_len", &self.tabu_list.len())
51 .field("aspiration_enabled", &self.aspiration_enabled)
52 .finish()
53 }
54}
55
56impl<S: PlanningSolution> Clone for TabuSearchAcceptor<S> {
57 fn clone(&self) -> Self {
58 Self {
59 tabu_size: self.tabu_size,
60 tabu_list: self.tabu_list.clone(),
61 aspiration_enabled: self.aspiration_enabled,
62 best_score: self.best_score,
63 }
64 }
65}
66
67impl<S: PlanningSolution> TabuSearchAcceptor<S> {
68 /// Creates a new tabu search acceptor.
69 ///
70 /// # Arguments
71 /// * `tabu_size` - Maximum number of solutions to remember as tabu. Must be > 0.
72 ///
73 /// # Panics
74 ///
75 /// Panics if `tabu_size` is 0.
76 pub fn new(tabu_size: usize) -> Self {
77 assert!(tabu_size > 0, "tabu_size must be > 0, got 0");
78 Self {
79 tabu_size,
80 tabu_list: Vec::with_capacity(tabu_size),
81 aspiration_enabled: true,
82 best_score: None,
83 }
84 }
85
86 /// Creates a tabu search acceptor without aspiration.
87 ///
88 /// Without aspiration, tabu moves are never accepted, even if they
89 /// would lead to a new best solution.
90 ///
91 /// # Panics
92 ///
93 /// Panics if `tabu_size` is 0.
94 pub fn without_aspiration(tabu_size: usize) -> Self {
95 assert!(tabu_size > 0, "tabu_size must be > 0, got 0");
96 Self {
97 tabu_size,
98 tabu_list: Vec::with_capacity(tabu_size),
99 aspiration_enabled: false,
100 best_score: None,
101 }
102 }
103
104 // Returns true if the given score is in the tabu list.
105 fn is_tabu(&self, score: &S::Score) -> bool {
106 self.tabu_list.iter().any(|s| s == score)
107 }
108
109 fn add_to_tabu(&mut self, score: S::Score) {
110 if self.tabu_list.len() >= self.tabu_size {
111 self.tabu_list.remove(0);
112 }
113 self.tabu_list.push(score);
114 }
115}
116
117impl<S: PlanningSolution> Default for TabuSearchAcceptor<S> {
118 fn default() -> Self {
119 Self::new(7) // Default tabu tenure of 7
120 }
121}
122
123impl<S: PlanningSolution> Acceptor<S> for TabuSearchAcceptor<S> {
124 fn is_accepted(&mut self, last_step_score: &S::Score, move_score: &S::Score) -> bool {
125 // Check aspiration criterion: accept if this would be a new best score
126 if self.aspiration_enabled {
127 if let Some(best) = &self.best_score {
128 if move_score > best {
129 return true; // Aspiration: accept new best even if tabu
130 }
131 }
132 }
133
134 // Reject if the move leads to a tabu solution
135 if self.is_tabu(move_score) {
136 return false;
137 }
138
139 // Accept improving moves
140 if move_score > last_step_score {
141 return true;
142 }
143
144 // Accept equal moves (allows exploration on plateaus)
145 if move_score >= last_step_score {
146 return true;
147 }
148
149 // Reject worsening moves that aren't tabu-breaking
150 false
151 }
152
153 fn phase_started(&mut self, initial_score: &S::Score) {
154 self.tabu_list.clear();
155 self.best_score = Some(*initial_score);
156 }
157
158 fn phase_ended(&mut self) {
159 self.tabu_list.clear();
160 }
161
162 fn step_ended(&mut self, step_score: &S::Score) {
163 // Add the step score to the tabu list
164 self.add_to_tabu(*step_score);
165
166 // Update best score
167 if let Some(best) = &self.best_score {
168 if step_score > best {
169 self.best_score = Some(*step_score);
170 }
171 } else {
172 self.best_score = Some(*step_score);
173 }
174 }
175}