solverforge_solver/phase/localsearch/
forager.rs1use std::fmt::Debug;
8use std::marker::PhantomData;
9
10use crate::heuristic::r#move::Move;
11use crate::heuristic::selector::move_selector::CandidateId;
12use solverforge_core::domain::PlanningSolution;
13
14pub trait LocalSearchForager<S, M>: Send + Debug
25where
26 S: PlanningSolution,
27 M: Move<S>,
28{
29 fn step_started(&mut self, best_score: S::Score, last_step_score: S::Score, step_seed: u64);
37
38 fn add_move_index(&mut self, index: CandidateId, score: S::Score) -> ForagerDecision;
43
44 fn is_quit_early(&self) -> bool;
47
48 fn accepted_count_limit(&self) -> Option<usize> {
49 None
50 }
51
52 fn pick_move_index(&mut self) -> Option<(CandidateId, S::Score)>;
57}
58
59#[derive(Clone, Copy, Debug, PartialEq, Eq)]
61pub enum ForagerDecision {
62 Keep,
64 Release,
66 Replace(CandidateId),
68}
69
70pub(super) struct BestCandidate<S>
71where
72 S: PlanningSolution,
73{
74 selected: Option<(CandidateId, S::Score)>,
75 equal_count: u64,
76 step_seed: u64,
77 random_ties: bool,
78}
79
80impl<S> BestCandidate<S>
81where
82 S: PlanningSolution,
83{
84 fn new(random_ties: bool) -> Self {
85 Self {
86 selected: None,
87 equal_count: 0,
88 step_seed: 0,
89 random_ties,
90 }
91 }
92
93 fn reset(&mut self, step_seed: u64) {
94 self.selected = None;
95 self.equal_count = 0;
96 self.step_seed = step_seed;
97 }
98
99 fn consider(&mut self, index: CandidateId, score: S::Score) -> ForagerDecision {
100 let Some((selected, best_score)) = self.selected else {
101 self.selected = Some((index, score));
102 self.equal_count = 1;
103 return ForagerDecision::Keep;
104 };
105
106 match score.cmp(&best_score) {
107 std::cmp::Ordering::Less => ForagerDecision::Release,
108 std::cmp::Ordering::Greater => self.replace(index, score),
109 std::cmp::Ordering::Equal => {
110 self.equal_count += 1;
111 if self.random_ties && reservoir_pick(self.step_seed, self.equal_count) {
112 self.selected = Some((index, score));
113 ForagerDecision::Replace(selected)
114 } else {
115 ForagerDecision::Release
116 }
117 }
118 }
119 }
120
121 pub(super) fn replace(&mut self, index: CandidateId, score: S::Score) -> ForagerDecision {
122 self.equal_count = 1;
123 match self.selected.replace((index, score)) {
124 Some((replaced, _)) => ForagerDecision::Replace(replaced),
125 None => ForagerDecision::Keep,
126 }
127 }
128
129 pub(super) fn take(&mut self) -> Option<(CandidateId, S::Score)> {
130 self.equal_count = 0;
131 self.selected.take()
132 }
133
134 pub(super) fn has_selection(&self) -> bool {
135 self.selected.is_some()
136 }
137
138 fn random_ties(&self) -> bool {
139 self.random_ties
140 }
141}
142
143fn reservoir_pick(step_seed: u64, equal_count: u64) -> bool {
144 let mixed = splitmix64(
145 step_seed ^ equal_count.wrapping_mul(0x9E37_79B9_7F4A_7C15) ^ 0xF04A_63E2_39B7_4D11,
146 );
147 mixed.is_multiple_of(equal_count)
148}
149
150fn splitmix64(mut value: u64) -> u64 {
151 value = value.wrapping_add(0x9E37_79B9_7F4A_7C15);
152 value = (value ^ (value >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
153 value = (value ^ (value >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
154 value ^ (value >> 31)
155}
156
157mod improving;
158
159pub use improving::{FirstBestScoreImprovingForager, FirstLastStepScoreImprovingForager};
160
161pub struct AcceptedCountForager<S>
168where
169 S: PlanningSolution,
170{
171 accepted_count_limit: usize,
173 accepted_count: usize,
174 best_move: BestCandidate<S>,
175 _phantom: PhantomData<fn() -> S>,
176}
177
178impl<S> AcceptedCountForager<S>
179where
180 S: PlanningSolution,
181{
182 pub fn new(accepted_count_limit: usize, random_ties: bool) -> Self {
191 assert!(
192 accepted_count_limit > 0,
193 "AcceptedCountForager: accepted_count_limit must be > 0, got 0"
194 );
195 Self {
196 accepted_count_limit,
197 accepted_count: 0,
198 best_move: BestCandidate::new(random_ties),
199 _phantom: PhantomData,
200 }
201 }
202}
203
204impl<S> Clone for AcceptedCountForager<S>
205where
206 S: PlanningSolution,
207{
208 fn clone(&self) -> Self {
209 Self {
210 accepted_count_limit: self.accepted_count_limit,
211 accepted_count: 0,
212 best_move: BestCandidate::new(self.best_move.random_ties()),
213 _phantom: PhantomData,
214 }
215 }
216}
217
218impl<S> Debug for AcceptedCountForager<S>
219where
220 S: PlanningSolution,
221{
222 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
223 f.debug_struct("AcceptedCountForager")
224 .field("accepted_count_limit", &self.accepted_count_limit)
225 .field("accepted_count", &self.accepted_count)
226 .finish()
227 }
228}
229
230impl<S, M> LocalSearchForager<S, M> for AcceptedCountForager<S>
231where
232 S: PlanningSolution,
233 M: Move<S>,
234{
235 fn step_started(&mut self, _best_score: S::Score, _last_step_score: S::Score, step_seed: u64) {
236 self.accepted_count = 0;
237 self.best_move.reset(step_seed);
238 }
239
240 fn add_move_index(&mut self, index: CandidateId, score: S::Score) -> ForagerDecision {
241 if self.accepted_count >= self.accepted_count_limit {
242 return ForagerDecision::Release;
243 }
244 self.accepted_count += 1;
245 self.best_move.consider(index, score)
246 }
247
248 fn is_quit_early(&self) -> bool {
249 self.accepted_count >= self.accepted_count_limit
250 }
251
252 fn accepted_count_limit(&self) -> Option<usize> {
253 Some(self.accepted_count_limit)
254 }
255
256 fn pick_move_index(&mut self) -> Option<(CandidateId, S::Score)> {
257 self.best_move.take()
258 }
259}
260
261pub struct FirstAcceptedForager<S>
265where
266 S: PlanningSolution,
267{
268 accepted_move: Option<(CandidateId, S::Score)>,
270 _phantom: PhantomData<fn() -> S>,
271}
272
273impl<S> Debug for FirstAcceptedForager<S>
274where
275 S: PlanningSolution,
276{
277 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
278 f.debug_struct("FirstAcceptedForager")
279 .field("has_move", &self.accepted_move.is_some())
280 .finish()
281 }
282}
283
284impl<S> FirstAcceptedForager<S>
285where
286 S: PlanningSolution,
287{
288 pub fn new() -> Self {
289 Self {
290 accepted_move: None,
291 _phantom: PhantomData,
292 }
293 }
294}
295
296impl<S> Clone for FirstAcceptedForager<S>
297where
298 S: PlanningSolution,
299{
300 fn clone(&self) -> Self {
301 Self {
302 accepted_move: None, _phantom: PhantomData,
304 }
305 }
306}
307
308impl<S> Default for FirstAcceptedForager<S>
309where
310 S: PlanningSolution,
311{
312 fn default() -> Self {
313 Self::new()
314 }
315}
316
317impl<S, M> LocalSearchForager<S, M> for FirstAcceptedForager<S>
318where
319 S: PlanningSolution,
320 M: Move<S>,
321{
322 fn step_started(&mut self, _best_score: S::Score, _last_step_score: S::Score, _step_seed: u64) {
323 self.accepted_move = None;
324 }
325
326 fn add_move_index(&mut self, index: CandidateId, score: S::Score) -> ForagerDecision {
327 if self.accepted_move.is_none() {
328 self.accepted_move = Some((index, score));
329 ForagerDecision::Keep
330 } else {
331 ForagerDecision::Release
332 }
333 }
334
335 fn is_quit_early(&self) -> bool {
336 self.accepted_move.is_some()
337 }
338
339 fn accepted_count_limit(&self) -> Option<usize> {
340 Some(1)
341 }
342
343 fn pick_move_index(&mut self) -> Option<(CandidateId, S::Score)> {
344 self.accepted_move.take()
345 }
346}
347
348pub struct BestScoreForager<S>
353where
354 S: PlanningSolution,
355{
356 best_move: BestCandidate<S>,
358 _phantom: PhantomData<fn() -> S>,
359}
360
361impl<S> BestScoreForager<S>
362where
363 S: PlanningSolution,
364{
365 pub fn new(random_ties: bool) -> Self {
366 Self {
367 best_move: BestCandidate::new(random_ties),
368 _phantom: PhantomData,
369 }
370 }
371}
372
373impl<S> Default for BestScoreForager<S>
374where
375 S: PlanningSolution,
376{
377 fn default() -> Self {
378 Self::new(true)
379 }
380}
381
382impl<S> Debug for BestScoreForager<S>
383where
384 S: PlanningSolution,
385{
386 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
387 f.debug_struct("BestScoreForager")
388 .field("has_move", &self.best_move.has_selection())
389 .finish()
390 }
391}
392
393impl<S> Clone for BestScoreForager<S>
394where
395 S: PlanningSolution,
396{
397 fn clone(&self) -> Self {
398 Self {
399 best_move: BestCandidate::new(self.best_move.random_ties()),
400 _phantom: PhantomData,
401 }
402 }
403}
404
405impl<S, M> LocalSearchForager<S, M> for BestScoreForager<S>
406where
407 S: PlanningSolution,
408 M: Move<S>,
409{
410 fn step_started(&mut self, _best_score: S::Score, _last_step_score: S::Score, step_seed: u64) {
411 self.best_move.reset(step_seed);
412 }
413
414 fn add_move_index(&mut self, index: CandidateId, score: S::Score) -> ForagerDecision {
415 self.best_move.consider(index, score)
416 }
417
418 fn is_quit_early(&self) -> bool {
419 false }
421
422 fn pick_move_index(&mut self) -> Option<(CandidateId, S::Score)> {
423 self.best_move.take()
424 }
425}
426
427#[cfg(test)]
428mod any_tests;
429#[cfg(test)]
430mod tests;