Skip to main content

solverforge_solver/phase/localsearch/acceptor/
diversified_late_acceptance.rs

1// Diversified late acceptance acceptor.
2
3use std::fmt::Debug;
4
5use solverforge_core::domain::PlanningSolution;
6use solverforge_core::Score;
7
8use super::Acceptor;
9use crate::heuristic::r#move::MoveTabuSignature;
10
11/// Diversified late acceptance acceptor - combines late acceptance with best score tracking.
12///
13/// Extends [`LateAcceptanceAcceptor`] by also tracking the best score found.
14/// Accepts a move if it:
15/// 1. Improves the last step score (always accepted), OR
16/// 2. Is at least as good as the score from N steps ago, OR
17/// 3. Is within a tolerance of the best score found so far
18///
19/// The third condition allows escaping from local optima by accepting
20/// moves that don't regress too far from the best known solution.
21///
22/// # Example
23///
24/// ```
25/// use solverforge_solver::phase::localsearch::DiversifiedLateAcceptanceAcceptor;
26/// use solverforge_core::score::SoftScore;
27/// use solverforge_core::domain::PlanningSolution;
28///
29/// #[derive(Clone)]
30/// struct MySolution;
31/// impl PlanningSolution for MySolution {
32///     type Score = SoftScore;
33///     fn score(&self) -> Option<Self::Score> { None }
34///     fn set_score(&mut self, _: Option<Self::Score>) {}
35/// }
36///
37/// // Accept if better than 400-step-old score OR within 5% of best
38/// let acceptor = DiversifiedLateAcceptanceAcceptor::<MySolution>::new(400, 0.05);
39/// ```
40pub struct DiversifiedLateAcceptanceAcceptor<S: PlanningSolution> {
41    // Size of the late acceptance list.
42    late_acceptance_size: usize,
43    // Circular buffer of historical scores.
44    score_history: Vec<Option<S::Score>>,
45    // Current index in the buffer.
46    current_index: usize,
47    // Best score found so far in this phase.
48    best_score: Option<S::Score>,
49    // Tolerance as a fraction (0.05 = 5% worse than best is acceptable).
50    tolerance: f64,
51}
52
53impl<S: PlanningSolution> Debug for DiversifiedLateAcceptanceAcceptor<S> {
54    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
55        f.debug_struct("DiversifiedLateAcceptanceAcceptor")
56            .field("late_acceptance_size", &self.late_acceptance_size)
57            .field("current_index", &self.current_index)
58            .field("tolerance", &self.tolerance)
59            .finish()
60    }
61}
62
63impl<S: PlanningSolution> Clone for DiversifiedLateAcceptanceAcceptor<S> {
64    fn clone(&self) -> Self {
65        Self {
66            late_acceptance_size: self.late_acceptance_size,
67            score_history: self.score_history.clone(),
68            current_index: self.current_index,
69            best_score: self.best_score,
70            tolerance: self.tolerance,
71        }
72    }
73}
74
75impl<S: PlanningSolution> DiversifiedLateAcceptanceAcceptor<S> {
76    /// Creates a new diversified late acceptance acceptor.
77    ///
78    /// # Arguments
79    /// * `late_acceptance_size` - Number of historical scores to keep. Must be > 0.
80    /// * `tolerance` - Fraction of best score to accept (0.05 = 5% tolerance)
81    ///
82    /// # Panics
83    ///
84    /// Panics if `late_acceptance_size` is 0.
85    pub fn new(late_acceptance_size: usize, tolerance: f64) -> Self {
86        assert!(
87            late_acceptance_size > 0,
88            "late_acceptance_size must be > 0, got 0"
89        );
90        Self {
91            late_acceptance_size,
92            score_history: vec![None; late_acceptance_size],
93            current_index: 0,
94            best_score: None,
95            tolerance,
96        }
97    }
98
99    /// Creates with default tolerance of 0.01 (1%).
100    pub fn with_default_tolerance(late_acceptance_size: usize) -> Self {
101        Self::new(late_acceptance_size, 0.01)
102    }
103}
104
105impl<S: PlanningSolution> Default for DiversifiedLateAcceptanceAcceptor<S> {
106    fn default() -> Self {
107        Self::new(400, 0.01)
108    }
109}
110
111impl<S: PlanningSolution> Acceptor<S> for DiversifiedLateAcceptanceAcceptor<S> {
112    fn is_accepted(
113        &mut self,
114        last_step_score: &S::Score,
115        move_score: &S::Score,
116        _move_signature: Option<&MoveTabuSignature>,
117    ) -> bool {
118        // Accept non-worsening moves (consistent with LateAcceptanceAcceptor which uses >=)
119        if move_score >= last_step_score {
120            return true;
121        }
122
123        // Accept if better than or equal to the late score
124        if let Some(late_score) = &self.score_history[self.current_index] {
125            if move_score >= late_score {
126                return true;
127            }
128        } else {
129            // No history yet, accept
130            return true;
131        }
132
133        // Diversification: accept if within tolerance of best score
134        if let Some(best) = &self.best_score {
135            // Calculate threshold: best - tolerance * |best|
136            let abs_best = best.abs();
137            let threshold = *best - abs_best.multiply(self.tolerance);
138            if move_score >= &threshold {
139                return true;
140            }
141        }
142
143        false
144    }
145
146    fn phase_started(&mut self, initial_score: &S::Score) {
147        // Initialize history with the initial score
148        for slot in &mut self.score_history {
149            *slot = Some(*initial_score);
150        }
151        self.current_index = 0;
152        self.best_score = Some(*initial_score);
153    }
154
155    fn step_ended(
156        &mut self,
157        step_score: &S::Score,
158        _accepted_move_signature: Option<&MoveTabuSignature>,
159    ) {
160        // Update best score if improved
161        if let Some(best) = &self.best_score {
162            if step_score > best {
163                self.best_score = Some(*step_score);
164            }
165        } else {
166            self.best_score = Some(*step_score);
167        }
168
169        // Record the step score in the history
170        self.score_history[self.current_index] = Some(*step_score);
171        self.current_index = (self.current_index + 1) % self.late_acceptance_size;
172    }
173}
174
175#[cfg(test)]
176mod tests;