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}