Skip to main content

solverforge_solver/phase/localsearch/acceptor/
value_tabu.rs

1//! Value tabu acceptor.
2
3use std::fmt::Debug;
4
5use solverforge_core::domain::PlanningSolution;
6
7use super::Acceptor;
8
9/// Value tabu acceptor - maintains a tabu list based on assigned values.
10///
11/// Unlike entity tabu which forbids recently moved entities, value tabu
12/// forbids recently used values. This is useful when the problem has
13/// expensive values that shouldn't be over-utilized.
14///
15/// # Example
16///
17/// ```
18/// use solverforge_solver::phase::localsearch::ValueTabuAcceptor;
19///
20/// let acceptor = ValueTabuAcceptor::new(7);
21/// assert!(!acceptor.is_value_tabu(42));
22/// ```
23pub struct ValueTabuAcceptor {
24    /// Maximum number of values to remember.
25    value_tabu_size: usize,
26    /// List of tabu value hashes.
27    value_tabu_list: Vec<u64>,
28    /// Current step's assigned values.
29    current_step_values: Vec<u64>,
30}
31
32impl Debug for ValueTabuAcceptor {
33    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
34        f.debug_struct("ValueTabuAcceptor")
35            .field("value_tabu_size", &self.value_tabu_size)
36            .field("tabu_list_len", &self.value_tabu_list.len())
37            .finish()
38    }
39}
40
41impl Clone for ValueTabuAcceptor {
42    fn clone(&self) -> Self {
43        Self {
44            value_tabu_size: self.value_tabu_size,
45            value_tabu_list: self.value_tabu_list.clone(),
46            current_step_values: self.current_step_values.clone(),
47        }
48    }
49}
50
51impl ValueTabuAcceptor {
52    /// Creates a new value tabu acceptor.
53    ///
54    /// # Arguments
55    /// * `value_tabu_size` - Maximum number of values to remember as tabu. Must be > 0.
56    ///
57    /// # Panics
58    ///
59    /// Panics if `value_tabu_size` is 0.
60    pub fn new(value_tabu_size: usize) -> Self {
61        assert!(value_tabu_size > 0, "value_tabu_size must be > 0, got 0");
62        Self {
63            value_tabu_size,
64            value_tabu_list: Vec::with_capacity(value_tabu_size),
65            current_step_values: Vec::new(),
66        }
67    }
68
69    /// Records that a value was assigned in the current step.
70    ///
71    /// Call this with the hash of the assigned value before accepting the move.
72    pub fn record_value_assignment(&mut self, value_hash: u64) {
73        self.current_step_values.push(value_hash);
74    }
75
76    /// Returns true if the given value hash is in the tabu list.
77    pub fn is_value_tabu(&self, value_hash: u64) -> bool {
78        self.value_tabu_list.contains(&value_hash)
79    }
80}
81
82impl Default for ValueTabuAcceptor {
83    fn default() -> Self {
84        Self::new(7)
85    }
86}
87
88impl<S: PlanningSolution> Acceptor<S> for ValueTabuAcceptor {
89    fn is_accepted(&mut self, last_step_score: &S::Score, move_score: &S::Score) -> bool {
90        // Accept improving moves
91        if move_score > last_step_score {
92            return true;
93        }
94
95        // Accept equal moves for plateau exploration
96        if move_score >= last_step_score {
97            return true;
98        }
99
100        false
101    }
102
103    fn phase_started(&mut self, _initial_score: &S::Score) {
104        self.value_tabu_list.clear();
105        self.current_step_values.clear();
106    }
107
108    fn phase_ended(&mut self) {
109        self.value_tabu_list.clear();
110    }
111
112    fn step_started(&mut self) {
113        self.current_step_values.clear();
114    }
115
116    fn step_ended(&mut self, _step_score: &S::Score) {
117        // Add current step's values to tabu list
118        for value_hash in &self.current_step_values {
119            if self.value_tabu_list.len() >= self.value_tabu_size {
120                self.value_tabu_list.remove(0);
121            }
122            self.value_tabu_list.push(*value_hash);
123        }
124        self.current_step_values.clear();
125    }
126}
127
128#[cfg(test)]
129mod tests {
130    use super::*;
131    use solverforge_core::score::SimpleScore;
132
133    #[derive(Clone)]
134    struct TestSolution;
135    impl PlanningSolution for TestSolution {
136        type Score = SimpleScore;
137        fn score(&self) -> Option<Self::Score> {
138            None
139        }
140        fn set_score(&mut self, _: Option<Self::Score>) {}
141    }
142
143    #[test]
144    fn test_new_acceptor() {
145        let acceptor = ValueTabuAcceptor::new(5);
146        assert!(!acceptor.is_value_tabu(42));
147    }
148
149    #[test]
150    fn test_record_and_check() {
151        let mut acceptor = ValueTabuAcceptor::new(5);
152        Acceptor::<TestSolution>::phase_started(&mut acceptor, &SimpleScore::of(0));
153
154        acceptor.record_value_assignment(42);
155        Acceptor::<TestSolution>::step_ended(&mut acceptor, &SimpleScore::of(0));
156
157        assert!(acceptor.is_value_tabu(42));
158        assert!(!acceptor.is_value_tabu(99));
159    }
160
161    #[test]
162    fn test_tabu_expiration() {
163        let mut acceptor = ValueTabuAcceptor::new(2);
164        Acceptor::<TestSolution>::phase_started(&mut acceptor, &SimpleScore::of(0));
165
166        // First step: add value 1
167        acceptor.record_value_assignment(1);
168        Acceptor::<TestSolution>::step_ended(&mut acceptor, &SimpleScore::of(0));
169        assert!(acceptor.is_value_tabu(1));
170
171        // Second step: add value 2
172        Acceptor::<TestSolution>::step_started(&mut acceptor);
173        acceptor.record_value_assignment(2);
174        Acceptor::<TestSolution>::step_ended(&mut acceptor, &SimpleScore::of(0));
175        assert!(acceptor.is_value_tabu(1));
176        assert!(acceptor.is_value_tabu(2));
177
178        // Third step: add value 3 - this should evict value 1
179        Acceptor::<TestSolution>::step_started(&mut acceptor);
180        acceptor.record_value_assignment(3);
181        Acceptor::<TestSolution>::step_ended(&mut acceptor, &SimpleScore::of(0));
182
183        assert!(!acceptor.is_value_tabu(1)); // Expired
184        assert!(acceptor.is_value_tabu(2));
185        assert!(acceptor.is_value_tabu(3));
186    }
187
188    #[test]
189    fn test_accepts_improving_move() {
190        let mut acceptor = ValueTabuAcceptor::new(5);
191        let last_score = SimpleScore::of(-10);
192        let move_score = SimpleScore::of(-5);
193        assert!(Acceptor::<TestSolution>::is_accepted(
194            &mut acceptor,
195            &last_score,
196            &move_score
197        ));
198    }
199
200    #[test]
201    fn test_accepts_equal_move() {
202        let mut acceptor = ValueTabuAcceptor::new(5);
203        let score = SimpleScore::of(-10);
204        assert!(Acceptor::<TestSolution>::is_accepted(
205            &mut acceptor,
206            &score,
207            &score
208        ));
209    }
210
211    #[test]
212    fn test_rejects_worsening_move() {
213        let mut acceptor = ValueTabuAcceptor::new(5);
214        let last_score = SimpleScore::of(-5);
215        let move_score = SimpleScore::of(-10);
216        assert!(!Acceptor::<TestSolution>::is_accepted(
217            &mut acceptor,
218            &last_score,
219            &move_score
220        ));
221    }
222
223    #[test]
224    fn test_phase_clears_tabu() {
225        let mut acceptor = ValueTabuAcceptor::new(5);
226        Acceptor::<TestSolution>::phase_started(&mut acceptor, &SimpleScore::of(0));
227
228        acceptor.record_value_assignment(42);
229        Acceptor::<TestSolution>::step_ended(&mut acceptor, &SimpleScore::of(0));
230        assert!(acceptor.is_value_tabu(42));
231
232        Acceptor::<TestSolution>::phase_ended(&mut acceptor);
233        assert!(!acceptor.is_value_tabu(42));
234    }
235}