Skip to main content

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::SimpleScore;
23/// use solverforge_core::domain::PlanningSolution;
24///
25/// #[derive(Clone)]
26/// struct MySolution;
27/// impl PlanningSolution for MySolution {
28///     type Score = SimpleScore;
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    /// Adds a score to the tabu list, removing the oldest if at capacity.
110    fn add_to_tabu(&mut self, score: S::Score) {
111        if self.tabu_list.len() >= self.tabu_size {
112            self.tabu_list.remove(0);
113        }
114        self.tabu_list.push(score);
115    }
116}
117
118impl<S: PlanningSolution> Default for TabuSearchAcceptor<S> {
119    fn default() -> Self {
120        Self::new(7) // Default tabu tenure of 7
121    }
122}
123
124impl<S: PlanningSolution> Acceptor<S> for TabuSearchAcceptor<S> {
125    fn is_accepted(&mut self, last_step_score: &S::Score, move_score: &S::Score) -> bool {
126        // Check aspiration criterion: accept if this would be a new best score
127        if self.aspiration_enabled {
128            if let Some(best) = &self.best_score {
129                if move_score > best {
130                    return true; // Aspiration: accept new best even if tabu
131                }
132            }
133        }
134
135        // Reject if the move leads to a tabu solution
136        if self.is_tabu(move_score) {
137            return false;
138        }
139
140        // Accept improving moves
141        if move_score > last_step_score {
142            return true;
143        }
144
145        // Accept equal moves (allows exploration on plateaus)
146        if move_score >= last_step_score {
147            return true;
148        }
149
150        // Reject worsening moves that aren't tabu-breaking
151        false
152    }
153
154    fn phase_started(&mut self, initial_score: &S::Score) {
155        self.tabu_list.clear();
156        self.best_score = Some(*initial_score);
157    }
158
159    fn phase_ended(&mut self) {
160        self.tabu_list.clear();
161    }
162
163    fn step_ended(&mut self, step_score: &S::Score) {
164        // Add the step score to the tabu list
165        self.add_to_tabu(*step_score);
166
167        // Update best score
168        if let Some(best) = &self.best_score {
169            if step_score > best {
170                self.best_score = Some(*step_score);
171            }
172        } else {
173            self.best_score = Some(*step_score);
174        }
175    }
176}