pumpkin_core/engine/
restart_strategy.rs

1use std::fmt::Debug;
2
3use crate::basic_types::moving_averages::CumulativeMovingAverage;
4use crate::basic_types::moving_averages::MovingAverage;
5use crate::basic_types::moving_averages::WindowedMovingAverage;
6use crate::basic_types::sequence_generators::ConstantSequence;
7use crate::basic_types::sequence_generators::GeometricSequence;
8use crate::basic_types::sequence_generators::LubySequence;
9use crate::basic_types::sequence_generators::SequenceGenerator;
10use crate::basic_types::sequence_generators::SequenceGeneratorType;
11use crate::pumpkin_assert_simple;
12
13/// The options which are used by the solver to determine when a restart should occur.
14///
15/// An implementation of a restart strategy based on the specfication of [Section 4 of \[1\]](https://fmv.jku.at/papers/BiereFroehlich-POS15.pdf)
16/// (for more information about the Glucose restart strategies see [\[2\]](https://www.cril.univ-artois.fr/articles/xxmain.pdf) and
17/// [\[3\]](http://www.pragmaticsofsat.org/2012/slides-glucose.pdf)). The idea is to restart when the
18/// the quality of the learned clauses (indicated by the LBD [\[4\]](https://www.ijcai.org/Proceedings/09/Papers/074.pdf)
19/// which is the number of different decision levels present in a learned clause, lower is generally
20/// better) is poor. It takes this into account by keeping track of the overall average LBD and the
21/// short-term average LBD and comparing these with one another.
22///
23/// The strategy also takes into account that if a solver is "close" to finding a solution that it
24/// would be better to not restart at that moment and it can then decide to skip a restart.
25///
26/// It generalises the aforementioned Glucose restart strategy by using adjustable
27/// [`RestartOptions`] which allows the user to also specify Luby restarts
28/// (see [\[5\]](https://www.sciencedirect.com/science/article/pii/0020019093900299)) and
29/// constant restarts (see [Section 3 of \[1\]](https://fmv.jku.at/papers/BiereFroehlich-POS15.pdf)).
30///
31/// # Bibliography
32/// \[1\] A. Biere and A. Fröhlich, ‘Evaluating CDCL restart schemes’, Proceedings of Pragmatics of
33/// SAT, pp. 1–17, 2015.
34///
35/// \[2\] G. Audemard and L. Simon, ‘Refining restarts strategies for SAT and UNSAT’, in Principles
36/// and Practice of Constraint Programming: 18th International Conference, CP 2012, Québec City, QC,
37/// Canada, October 8-12, 2012. Proceedings, 2012, pp. 118–126.
38///
39/// \[3\] G. Audemard and L. Simon, ‘Glucose 2.1: Aggressive, but reactive, clause database
40/// management, dynamic restarts (system description)’, in Pragmatics of SAT 2012 (POS’12), 2012.
41///
42/// \[4\] G. Audemard and L. Simon, ‘Predicting learnt clauses quality in modern SAT solvers’, in
43/// Twenty-first international joint conference on artificial intelligence, 2009.
44///
45/// \[5\] M. Luby, A. Sinclair, and D. Zuckerman, ‘Optimal speedup of Las Vegas algorithms’,
46/// Information Processing Letters, vol. 47, no. 4, pp. 173–180, 1993.
47#[derive(Debug, Clone, Copy)]
48pub struct RestartOptions {
49    /// Decides the sequence based on which the restarts are performed.
50    /// To be used in combination with [`RestartOptions::base_interval`]
51    pub sequence_generator_type: SequenceGeneratorType,
52    /// The base interval length is used as a multiplier to the restart sequence.
53    /// For example, constant restarts with base interval 100 means a retart is triggered every 100
54    /// conflicts.
55    pub base_interval: u64,
56    /// The minimum number of conflicts to be reached before the first restart is considered
57    pub min_num_conflicts_before_first_restart: u64,
58    /// Used to determine if a restart should be forced.
59    /// The state is "bad" if the current LBD value is much greater than the global LBD average A
60    /// greater/lower value for lbd-coef means a less/more frequent restart policy
61    pub lbd_coef: f64,
62    /// Used to determine if a restart should be blocked.
63    /// To be used in combination with
64    /// [`RestartOptions::num_assigned_window`].
65    /// A restart is blocked if the number of assigned propositional variables is must greater than
66    /// the average number of assigned variables in the recent past A greater/lower value for
67    /// [`RestartOptions::num_assigned_coef`] means fewer/more blocked restarts
68    pub num_assigned_coef: f64,
69    /// Used to determine the length of the recent past that should be considered when deciding on
70    /// blocking restarts. The solver considers the last
71    /// [`RestartOptions::num_assigned_window`] conflicts as the reference point for the
72    /// number of assigned variables
73    pub num_assigned_window: u64,
74    /// The coefficient in the geometric sequence `x_i = x_{i-1} * geometric-coef` where `x_1 =
75    /// `[`RestartOptions::base_interval`]. Used only if
76    /// [`RestartOptions::sequence_generator_type`] is assigned to
77    /// [`SequenceGeneratorType::Geometric`].
78    pub geometric_coef: Option<f64>,
79    /// Determines whether restarts should be able to occur
80    pub no_restarts: bool,
81}
82
83impl Default for RestartOptions {
84    fn default() -> Self {
85        // The values which are used are based on [Glucose](https://github.com/audemard/glucose).
86        Self {
87            sequence_generator_type: SequenceGeneratorType::Constant,
88            base_interval: 50,
89            min_num_conflicts_before_first_restart: 10000,
90            lbd_coef: 1.25,
91            num_assigned_coef: 1.4,
92            num_assigned_window: 5000,
93            geometric_coef: None,
94            no_restarts: false,
95        }
96    }
97}
98
99#[derive(Debug)]
100pub(crate) struct RestartStrategy {
101    /// A generator for determining how many conflicts should be found before the next restart is
102    /// able to take place (one example of such a generator is [`LubySequence`]).
103    sequence_generator: Box<dyn SequenceGenerator>,
104    /// The number of conflicts encountered since the last restart took place
105    number_of_conflicts_encountered_since_restart: u64,
106    /// The minimum number of conflicts until the next restart is able to take place (note that if
107    /// [`RestartStrategy::number_of_conflicts_encountered_since_restart`] >
108    /// [`RestartStrategy::number_of_conflicts_until_restart`] it does not necessarily mean
109    /// that a restart is guaranteed to take place in the next call to
110    /// [`RestartStrategy::should_restart`]).
111    number_of_conflicts_until_restart: u64,
112    /// The minimum number of conflicts until the first restart is able to take place
113    minimum_number_of_conflicts_before_first_restart: u64,
114    /// The recent average of LBD values, used in [`RestartStrategy::should_restart`].
115    lbd_short_term_moving_average: Box<dyn MovingAverage<u64>>,
116    /// A coefficient which influences the decision whether a restart should take place in
117    /// [`RestartStrategy::should_restart`], the higher this value, the fewer restarts are
118    /// performed.
119    lbd_coefficient: f64,
120    /// The long-term average of LBD values, used in [`RestartStrategy::should_restart`].
121    lbd_long_term_moving_average: Box<dyn MovingAverage<u64>>,
122    /// A coefficient influencing whether a restart will be blocked in
123    /// [`RestartStrategy::notify_conflict`], the higher the value, the fewer restarts are
124    /// performed.
125    number_of_variables_coefficient: f64,
126    /// The average number of variables which are assigned, used in
127    /// [`RestartStrategy::notify_conflict`].
128    number_of_assigned_variables_moving_average: Box<dyn MovingAverage<u64>>,
129    /// The number of restarts which have been performed.
130    number_of_restarts: u64,
131    /// The number of restarts which have been blocked.
132    number_of_blocked_restarts: u64,
133    /// Determines whether restarts should be able to occur
134    no_restarts: bool,
135}
136
137impl Default for RestartStrategy {
138    fn default() -> Self {
139        RestartStrategy::new(RestartOptions::default())
140    }
141}
142
143impl RestartStrategy {
144    pub(crate) fn new(options: RestartOptions) -> Self {
145        let mut sequence_generator: Box<dyn SequenceGenerator> =
146            match options.sequence_generator_type {
147                SequenceGeneratorType::Constant => Box::new(ConstantSequence::new(
148                    options.base_interval as i64,
149                )),
150                SequenceGeneratorType::Geometric => Box::new(GeometricSequence::new(
151                    options.base_interval as i64,
152                    options.geometric_coef.expect(
153                        "Using the geometric sequence for restarts, but the parameter restarts-geometric-coef is not defined.",
154                    ),
155                )),
156                SequenceGeneratorType::Luby => Box::new(LubySequence::new(options.base_interval as i64)),
157            };
158
159        let number_of_conflicts_until_restart = sequence_generator.next().try_into().expect("Expected restart generator to generate a positive value but it generated a negative one");
160
161        RestartStrategy {
162            sequence_generator,
163            number_of_conflicts_encountered_since_restart: 0,
164            number_of_conflicts_until_restart,
165            minimum_number_of_conflicts_before_first_restart: options
166                .min_num_conflicts_before_first_restart,
167            lbd_short_term_moving_average: Box::new(WindowedMovingAverage::new(
168                options.base_interval,
169            )),
170            lbd_coefficient: options.lbd_coef,
171            lbd_long_term_moving_average: Box::<CumulativeMovingAverage<u64>>::default(),
172            number_of_variables_coefficient: options.num_assigned_coef,
173            number_of_assigned_variables_moving_average: Box::new(WindowedMovingAverage::new(
174                options.num_assigned_window,
175            )),
176            number_of_restarts: 0,
177            number_of_blocked_restarts: 0,
178            no_restarts: options.no_restarts,
179        }
180    }
181
182    /// Determines whether the restart strategy indicates that a restart should take place; the
183    /// strategy considers three conditions (in this order):
184    /// - If no restarts have taken place yet then a restart can only take place if the number of
185    ///   conflicts encountered since the last restart is larger or equal to
186    ///   [`RestartOptions::min_num_conflicts_before_first_restart`]
187    /// - If the previous condition is met then a restart can only take place if the number of
188    ///   conflicts encountered since the last restart is larger or equal to the number of conflicts
189    ///   until the next restart as indicated by the restart sequence generator specified in
190    ///   [`RestartOptions::sequence_generator_type`]
191    /// - If both of the previous conditions hold then it is determined whether the short-term
192    ///   average LBD is lower than the long-term average LBD multiplied by
193    ///   [`RestartOptions::lbd_coef`], this condition determines whether the solver is learning
194    ///   "bad" clauses based on the LBD; if it is learning "sufficiently bad" clauses then a
195    ///   restart will be performed.
196    pub(crate) fn should_restart(&self) -> bool {
197        if self.no_restarts {
198            return false;
199        }
200
201        // Do not restart until a certain number of conflicts take place before the first restart
202        // this is done to collect some early runtime statistics for the restart strategy
203        if self.should_restart_first_time() {
204            return false;
205        }
206
207        // Do not restart until a minimum number of conflicts took place after the last restart
208        if !self.should_trigger_later_restart() {
209            return false;
210        }
211
212        // Restarts can now be considered!
213        // Only restart if the solver is learning "bad" clauses, this is the case if the long-term
214        // average lbd multiplied by the `lbd_coefficient` is lower than the short-term average lbd
215        self.lbd_long_term_moving_average.value() * self.lbd_coefficient
216            <= self.lbd_short_term_moving_average.value()
217    }
218
219    fn should_restart_first_time(&self) -> bool {
220        self.number_of_restarts == 0
221            && self.number_of_conflicts_encountered_since_restart
222                < self.minimum_number_of_conflicts_before_first_restart
223    }
224
225    /// Notifies the restart strategy that a conflict has taken place so that it can adjust its
226    /// internal values, this method has the additional responsibility of checking whether a restart
227    /// should be blocked based on whether the solver is "sufficiently close" to finding a solution.
228    pub(crate) fn notify_conflict(&mut self, lbd: u32, number_of_pruned_values: u64) {
229        if self.no_restarts {
230            return;
231        }
232        // Update moving averages
233        self.number_of_assigned_variables_moving_average
234            .add_term(number_of_pruned_values);
235        self.lbd_short_term_moving_average.add_term(lbd as u64);
236        self.lbd_long_term_moving_average.add_term(lbd as u64);
237
238        // Increase the number of conflicts encountered since the last restart
239        self.number_of_conflicts_encountered_since_restart += 1;
240
241        if self.should_block_restart(number_of_pruned_values) {
242            // Restart has been blocked
243            self.number_of_blocked_restarts += 1;
244            self.reset_values()
245        }
246    }
247
248    fn should_block_restart(&self, number_of_pruned_values: u64) -> bool {
249        // If the solver has more variables assigned now than in the recent past, then block the
250        // restart. The idea is that the solver is 'closer' to finding a solution and restarting
251        // could be harmful to the performance
252
253        if self.should_restart_first_time() {
254            // Don't block the first restart.
255            return false;
256        }
257
258        let close_to_solution = number_of_pruned_values as f64
259            > self.number_of_assigned_variables_moving_average.value()
260                * self.number_of_variables_coefficient;
261
262        self.should_trigger_later_restart() && close_to_solution
263    }
264
265    fn should_trigger_later_restart(&self) -> bool {
266        self.number_of_conflicts_until_restart <= self.number_of_conflicts_encountered_since_restart
267    }
268
269    /// Notifies the restart strategy that a restart has taken place so that it can adjust its
270    /// internal values
271    pub(crate) fn notify_restart(&mut self) {
272        pumpkin_assert_simple!(!self.no_restarts);
273
274        self.number_of_restarts += 1;
275        self.reset_values()
276    }
277
278    /// Resets the values related to determining whether a restart takes place; this method should
279    /// be called whenever a restart has taken place or should have taken place and was blocked.
280    fn reset_values(&mut self) {
281        pumpkin_assert_simple!(!self.no_restarts);
282
283        self.number_of_conflicts_until_restart =
284            self.sequence_generator.next().try_into().expect("Expected restart generator to generate a positive value but it generated a negative one");
285        self.number_of_conflicts_encountered_since_restart = 0;
286        self.lbd_short_term_moving_average
287            .adapt(self.number_of_conflicts_until_restart);
288    }
289}