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