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
56    pub fn new(value_tabu_size: usize) -> Self {
57        Self {
58            value_tabu_size,
59            value_tabu_list: Vec::with_capacity(value_tabu_size),
60            current_step_values: Vec::new(),
61        }
62    }
63
64    /// Records that a value was assigned in the current step.
65    ///
66    /// Call this with the hash of the assigned value before accepting the move.
67    pub fn record_value_assignment(&mut self, value_hash: u64) {
68        self.current_step_values.push(value_hash);
69    }
70
71    /// Returns true if the given value hash is in the tabu list.
72    pub fn is_value_tabu(&self, value_hash: u64) -> bool {
73        self.value_tabu_list.contains(&value_hash)
74    }
75}
76
77impl Default for ValueTabuAcceptor {
78    fn default() -> Self {
79        Self::new(7)
80    }
81}
82
83impl<S: PlanningSolution> Acceptor<S> for ValueTabuAcceptor {
84    fn is_accepted(&self, last_step_score: &S::Score, move_score: &S::Score) -> bool {
85        // Accept improving moves
86        if move_score > last_step_score {
87            return true;
88        }
89
90        // Accept equal moves for plateau exploration
91        if move_score >= last_step_score {
92            return true;
93        }
94
95        false
96    }
97
98    fn phase_started(&mut self, _initial_score: &S::Score) {
99        self.value_tabu_list.clear();
100        self.current_step_values.clear();
101    }
102
103    fn phase_ended(&mut self) {
104        self.value_tabu_list.clear();
105    }
106
107    fn step_started(&mut self) {
108        self.current_step_values.clear();
109    }
110
111    fn step_ended(&mut self, _step_score: &S::Score) {
112        // Add current step's values to tabu list
113        for value_hash in &self.current_step_values {
114            if self.value_tabu_list.len() >= self.value_tabu_size {
115                self.value_tabu_list.remove(0);
116            }
117            self.value_tabu_list.push(*value_hash);
118        }
119        self.current_step_values.clear();
120    }
121}
122
123#[cfg(test)]
124mod tests {
125    use super::*;
126    use solverforge_core::score::SimpleScore;
127
128    #[derive(Clone)]
129    struct TestSolution;
130    impl PlanningSolution for TestSolution {
131        type Score = SimpleScore;
132        fn score(&self) -> Option<Self::Score> {
133            None
134        }
135        fn set_score(&mut self, _: Option<Self::Score>) {}
136    }
137
138    #[test]
139    fn test_new_acceptor() {
140        let acceptor = ValueTabuAcceptor::new(5);
141        assert!(!acceptor.is_value_tabu(42));
142    }
143
144    #[test]
145    fn test_record_and_check() {
146        let mut acceptor = ValueTabuAcceptor::new(5);
147        Acceptor::<TestSolution>::phase_started(&mut acceptor, &SimpleScore::of(0));
148
149        acceptor.record_value_assignment(42);
150        Acceptor::<TestSolution>::step_ended(&mut acceptor, &SimpleScore::of(0));
151
152        assert!(acceptor.is_value_tabu(42));
153        assert!(!acceptor.is_value_tabu(99));
154    }
155
156    #[test]
157    fn test_tabu_expiration() {
158        let mut acceptor = ValueTabuAcceptor::new(2);
159        Acceptor::<TestSolution>::phase_started(&mut acceptor, &SimpleScore::of(0));
160
161        // First step: add value 1
162        acceptor.record_value_assignment(1);
163        Acceptor::<TestSolution>::step_ended(&mut acceptor, &SimpleScore::of(0));
164        assert!(acceptor.is_value_tabu(1));
165
166        // Second step: add value 2
167        Acceptor::<TestSolution>::step_started(&mut acceptor);
168        acceptor.record_value_assignment(2);
169        Acceptor::<TestSolution>::step_ended(&mut acceptor, &SimpleScore::of(0));
170        assert!(acceptor.is_value_tabu(1));
171        assert!(acceptor.is_value_tabu(2));
172
173        // Third step: add value 3 - this should evict value 1
174        Acceptor::<TestSolution>::step_started(&mut acceptor);
175        acceptor.record_value_assignment(3);
176        Acceptor::<TestSolution>::step_ended(&mut acceptor, &SimpleScore::of(0));
177
178        assert!(!acceptor.is_value_tabu(1)); // Expired
179        assert!(acceptor.is_value_tabu(2));
180        assert!(acceptor.is_value_tabu(3));
181    }
182
183    #[test]
184    fn test_accepts_improving_move() {
185        let acceptor = ValueTabuAcceptor::new(5);
186        let last_score = SimpleScore::of(-10);
187        let move_score = SimpleScore::of(-5);
188        assert!(Acceptor::<TestSolution>::is_accepted(
189            &acceptor,
190            &last_score,
191            &move_score
192        ));
193    }
194
195    #[test]
196    fn test_accepts_equal_move() {
197        let acceptor = ValueTabuAcceptor::new(5);
198        let score = SimpleScore::of(-10);
199        assert!(Acceptor::<TestSolution>::is_accepted(
200            &acceptor, &score, &score
201        ));
202    }
203
204    #[test]
205    fn test_rejects_worsening_move() {
206        let acceptor = ValueTabuAcceptor::new(5);
207        let last_score = SimpleScore::of(-5);
208        let move_score = SimpleScore::of(-10);
209        assert!(!Acceptor::<TestSolution>::is_accepted(
210            &acceptor,
211            &last_score,
212            &move_score
213        ));
214    }
215
216    #[test]
217    fn test_phase_clears_tabu() {
218        let mut acceptor = ValueTabuAcceptor::new(5);
219        Acceptor::<TestSolution>::phase_started(&mut acceptor, &SimpleScore::of(0));
220
221        acceptor.record_value_assignment(42);
222        Acceptor::<TestSolution>::step_ended(&mut acceptor, &SimpleScore::of(0));
223        assert!(acceptor.is_value_tabu(42));
224
225        Acceptor::<TestSolution>::phase_ended(&mut acceptor);
226        assert!(!acceptor.is_value_tabu(42));
227    }
228}