solverforge_solver/phase/localsearch/acceptor/
tabu_search.rs1use std::fmt::Debug;
2
3use solverforge_core::domain::PlanningSolution;
4
5use super::Acceptor;
6use crate::heuristic::r#move::{
7 metadata::{MoveIdentity, ScopedEntityTabuToken, ScopedValueTabuToken},
8 MoveTabuSignature,
9};
10
11#[derive(Clone, Copy, Debug, PartialEq, Eq)]
12pub(crate) struct TabuSearchPolicy {
13 pub entity_tabu_size: Option<usize>,
14 pub value_tabu_size: Option<usize>,
15 pub move_tabu_size: Option<usize>,
16 pub undo_move_tabu_size: Option<usize>,
17 pub aspiration_enabled: bool,
18}
19
20impl TabuSearchPolicy {
21 pub(crate) fn move_only(move_tabu_size: usize) -> Self {
22 Self {
23 entity_tabu_size: None,
24 value_tabu_size: None,
25 move_tabu_size: Some(move_tabu_size),
26 undo_move_tabu_size: None,
27 aspiration_enabled: true,
28 }
29 .validated()
30 }
31
32 pub(crate) fn validated(self) -> Self {
33 let entity_tabu_size = validate_tabu_size("entity_tabu_size", self.entity_tabu_size);
34 let value_tabu_size = validate_tabu_size("value_tabu_size", self.value_tabu_size);
35 let move_tabu_size = validate_tabu_size("move_tabu_size", self.move_tabu_size);
36 let undo_move_tabu_size =
37 validate_tabu_size("undo_move_tabu_size", self.undo_move_tabu_size);
38
39 assert!(
40 entity_tabu_size.is_some()
41 || value_tabu_size.is_some()
42 || move_tabu_size.is_some()
43 || undo_move_tabu_size.is_some(),
44 "tabu_search requires at least one tabu dimension"
45 );
46
47 Self {
48 entity_tabu_size,
49 value_tabu_size,
50 move_tabu_size,
51 undo_move_tabu_size,
52 aspiration_enabled: self.aspiration_enabled,
53 }
54 }
55}
56
57fn validate_tabu_size(field_name: &str, value: Option<usize>) -> Option<usize> {
58 match value {
59 Some(0) => panic!("tabu_search field `{field_name}` must be greater than 0"),
60 _ => value,
61 }
62}
63
64#[derive(Clone, Debug, Default)]
65struct TabuMemory<T> {
66 tenure: Option<usize>,
67 entries: Vec<T>,
68}
69
70impl<T: Clone + PartialEq> TabuMemory<T> {
71 fn new(tenure: Option<usize>) -> Self {
72 Self {
73 tenure,
74 entries: Vec::new(),
75 }
76 }
77
78 fn contains(&self, entry: &T) -> bool {
79 self.entries.iter().any(|item| item == entry)
80 }
81
82 fn record(&mut self, entry: T) {
83 let Some(tenure) = self.tenure else {
84 return;
85 };
86 if self.entries.len() >= tenure {
87 self.entries.remove(0);
88 }
89 self.entries.push(entry);
90 }
91
92 fn record_many<'a>(&mut self, entries: impl IntoIterator<Item = &'a T>)
93 where
94 T: 'a,
95 {
96 for entry in entries {
97 self.record(entry.clone());
98 }
99 }
100
101 fn clear(&mut self) {
102 self.entries.clear();
103 }
104}
105
106pub struct TabuSearchAcceptor<S: PlanningSolution> {
113 entity_memory: TabuMemory<ScopedEntityTabuToken>,
114 value_memory: TabuMemory<ScopedValueTabuToken>,
115 move_memory: TabuMemory<MoveIdentity>,
116 reverse_move_memory: TabuMemory<MoveIdentity>,
119 aspiration_enabled: bool,
120 best_score: Option<S::Score>,
121}
122
123impl<S: PlanningSolution> Debug for TabuSearchAcceptor<S> {
124 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
125 f.debug_struct("TabuSearchAcceptor")
126 .field("entity_tabu_size", &self.entity_memory.tenure)
127 .field("value_tabu_size", &self.value_memory.tenure)
128 .field("move_tabu_size", &self.move_memory.tenure)
129 .field("undo_move_tabu_size", &self.reverse_move_memory.tenure)
130 .field("aspiration_enabled", &self.aspiration_enabled)
131 .finish()
132 }
133}
134
135impl<S: PlanningSolution> Clone for TabuSearchAcceptor<S> {
136 fn clone(&self) -> Self {
137 Self {
138 entity_memory: self.entity_memory.clone(),
139 value_memory: self.value_memory.clone(),
140 move_memory: self.move_memory.clone(),
141 reverse_move_memory: self.reverse_move_memory.clone(),
142 aspiration_enabled: self.aspiration_enabled,
143 best_score: self.best_score,
144 }
145 }
146}
147
148impl<S: PlanningSolution> TabuSearchAcceptor<S> {
149 pub(crate) fn new(policy: TabuSearchPolicy) -> Self {
150 let policy = policy.validated();
151
152 Self {
153 entity_memory: TabuMemory::new(policy.entity_tabu_size),
154 value_memory: TabuMemory::new(policy.value_tabu_size),
155 move_memory: TabuMemory::new(policy.move_tabu_size),
156 reverse_move_memory: TabuMemory::new(policy.undo_move_tabu_size),
157 aspiration_enabled: policy.aspiration_enabled,
158 best_score: None,
159 }
160 }
161
162 fn is_tabu(&self, signature: &MoveTabuSignature) -> bool {
163 signature
164 .entity_tokens
165 .iter()
166 .any(|entity_token| self.entity_memory.contains(entity_token))
167 || signature
168 .destination_value_tokens
169 .iter()
170 .any(|value_token| self.value_memory.contains(value_token))
171 || self.move_memory.contains(&signature.move_id)
172 || self.reverse_move_memory.contains(&signature.move_id)
173 }
174}
175
176impl<S: PlanningSolution> Acceptor<S> for TabuSearchAcceptor<S> {
177 fn requires_move_signatures(&self) -> bool {
178 true
179 }
180
181 fn is_accepted(
182 &mut self,
183 _last_step_score: &S::Score,
184 move_score: &S::Score,
185 move_signature: Option<&MoveTabuSignature>,
186 ) -> bool {
187 let signature = move_signature.expect("tabu search requires move signatures");
188 let aspirational = self
189 .best_score
190 .as_ref()
191 .is_some_and(|best| self.aspiration_enabled && move_score > best);
192 if !aspirational && self.is_tabu(signature) {
193 return false;
194 }
195 true
196 }
197
198 fn phase_started(&mut self, initial_score: &S::Score) {
199 self.entity_memory.clear();
200 self.value_memory.clear();
201 self.move_memory.clear();
202 self.reverse_move_memory.clear();
203 self.best_score = Some(*initial_score);
204 }
205
206 fn phase_ended(&mut self) {
207 self.entity_memory.clear();
208 self.value_memory.clear();
209 self.move_memory.clear();
210 self.reverse_move_memory.clear();
211 self.best_score = None;
212 }
213
214 fn step_ended(
215 &mut self,
216 step_score: &S::Score,
217 accepted_move_signature: Option<&MoveTabuSignature>,
218 ) {
219 if let Some(signature) = accepted_move_signature {
220 self.entity_memory
221 .record_many(signature.entity_tokens.iter());
222 self.value_memory
223 .record_many(signature.destination_value_tokens.iter());
224 self.move_memory.record(signature.move_id.clone());
225 self.reverse_move_memory
226 .record(signature.undo_move_id.clone());
227 }
228
229 if let Some(best) = &self.best_score {
230 if step_score > best {
231 self.best_score = Some(*step_score);
232 }
233 } else {
234 self.best_score = Some(*step_score);
235 }
236 }
237}