Skip to main content

pumpkin_core/branching/branchers/
autonomous_search.rs

1use super::independent_variable_value_brancher::IndependentVariableValueBrancher;
2use crate::DefaultBrancher;
3use crate::basic_types::DeletablePredicateIdGenerator;
4use crate::basic_types::PredicateId;
5use crate::basic_types::SolutionReference;
6use crate::branching::Brancher;
7use crate::branching::BrancherEvent;
8use crate::branching::SelectionContext;
9use crate::branching::value_selection::RandomSplitter;
10use crate::branching::variable_selection::RandomSelector;
11use crate::containers::KeyValueHeap;
12use crate::containers::StorageKey;
13use crate::create_statistics_struct;
14use crate::engine::Assignments;
15use crate::engine::predicates::predicate::Predicate;
16use crate::propagation::ReadDomains;
17use crate::results::Solution;
18use crate::statistics::Statistic;
19use crate::statistics::StatisticLogger;
20use crate::statistics::moving_averages::CumulativeMovingAverage;
21use crate::statistics::moving_averages::MovingAverage;
22use crate::variables::DomainId;
23/// A [`Brancher`] that combines [VSIDS \[1\]](https://dl.acm.org/doi/pdf/10.1145/378239.379017)
24/// and [Solution-based phase saving \[2\]](https://people.eng.unimelb.edu.au/pstuckey/papers/lns-restarts.pdf).
25///
26/// There are three components:
27/// 1. Predicate selection
28/// 2. Truth value assignment
29/// 3. Backup Selection
30///
31/// # Predicate selection
32/// The VSIDS algorithm is an adaptation for the CP case. It determines which
33/// [`Predicate`] should be branched on based on how often it appears in conflicts.
34///
35/// Intuitively, the more often a [`Predicate`] appears in *recent* conflicts, the more "important"
36/// it is during the search process. VSIDS is originally from the SAT field (see \[1\]) but we
37/// adapted it for constraint programming by considering [`Predicate`]s from recent conflicts
38/// directly rather than Boolean variables.
39///
40/// # Truth value assignment
41/// The truth value for the [`Predicate`] is selected to be consistent with the
42/// best solution known so far. In this way, the search is directed around this existing solution.
43///
44/// In case where there is no known solution, then the predicate is assigned to true. This resembles
45/// a fail-first strategy with the idea that the given predicate was encountered in conflicts, so
46/// assigning it to true may cause another conflict soon.
47///
48/// # Backup selection
49/// VSIDS relies on [`Predicate`]s appearing in conflicts to discover which [`Predicate`]s are
50/// "important". However, it could be the case that all [`Predicate`]s which VSIDS has discovered
51/// are already assigned.
52///
53/// In this case, [`AutonomousSearch`] defaults either to the backup described in
54/// [`DefaultBrancher`] (when created using [`AutonomousSearch::default_over_all_variables`]) or it
55/// defaults to the [`Brancher`] provided to [`AutonomousSearch::new`].
56///
57/// # Bibliography
58/// \[1\] M. W. Moskewicz, C. F. Madigan, Y. Zhao, L. Zhang, and S. Malik, ‘Chaff: Engineering an
59/// efficient SAT solver’, in Proceedings of the 38th annual Design Automation Conference, 2001.
60///
61/// \[2\] E. Demirović, G. Chu, and P. J. Stuckey, ‘Solution-based phase saving for CP: A
62/// value-selection heuristic to simulate local search behavior in complete solvers’, in the
63/// proceedings of the Principles and Practice of Constraint Programming (CP 2018).
64#[derive(Debug)]
65pub struct AutonomousSearch<BackupBrancher> {
66    /// Predicates are mapped to ids. This is used internally in the heap.
67    predicate_id_info: DeletablePredicateIdGenerator,
68    /// Stores the activities for a predicate, represented with its id.
69    heap: KeyValueHeap<PredicateId, f64>,
70    /// After popping predicates off the heap that current have a truth value, the predicates are
71    /// labelled as dormant because they do not contribute to VSIDS at the moment. When
72    /// backtracking, dormant predicates are examined and readded to the heap. Dormant predicates
73    /// with low activities are removed.
74    dormant_predicates: Vec<Predicate>,
75    /// How much the activity of a predicate is increased when it appears in a conflict.
76    /// This value changes during search (see [`Vsids::decay_activities`]).
77    increment: f64,
78    /// The maximum allowed [`Vsids`] value, if this value is reached then all of the values are
79    /// divided by this value. The increment is constant.
80    max_threshold: f64,
81    /// Whenever a conflict is found, the [`Vsids::increment`] is multiplied by
82    /// 1 / [`Vsids::decay_factor`] (this is synonymous with increasing the
83    /// [`Vsids::increment`] since 0 <= [`Vsids::decay_factor`] <= 1).
84    /// The decay factor is constant.
85    decay_factor: f64,
86    /// Contains the best-known solution or [`None`] if no solution has been found.
87    best_known_solution: Option<Solution>,
88    /// If the heap does not contain any more unfixed predicates then this backup_brancher will be
89    /// used instead.
90    backup_brancher: BackupBrancher,
91    /// The statistics gathered by the autonomous search
92    statistics: AutonomousSearchStatistics,
93    /// Whether synchronisation should take place in the next call to
94    /// [`AutonomousSearch::next_decision`].
95    ///
96    /// This is used to prevent unnecessary work when [`AutonomousSearch::synchronise`] is called
97    /// multiple times in a row without a call to [`AutonomousSearch::next_decision`].
98    should_synchronise: bool,
99}
100
101create_statistics_struct!(AutonomousSearchStatistics {
102    num_backup_called: usize,
103    num_predicates_removed: usize,
104    num_calls: usize,
105    num_predicates_added: usize,
106    average_size_of_heap: CumulativeMovingAverage<usize>,
107    num_assigned_predicates_encountered: usize,
108});
109
110const DEFAULT_VSIDS_INCREMENT: f64 = 1.0;
111const DEFAULT_VSIDS_MAX_THRESHOLD: f64 = 1e100;
112const DEFAULT_VSIDS_DECAY_FACTOR: f64 = 0.95;
113const DEFAULT_VSIDS_VALUE: f64 = 0.0;
114
115impl DefaultBrancher {
116    /// Creates a new instance with default values for
117    /// the parameters (`1.0` for the increment, `1e100` for the max threshold,
118    /// `0.95` for the decay factor and `0.0` for the initial VSIDS value).
119    ///
120    /// If there are no more predicates left to select, this [`Brancher`] switches to
121    /// [`RandomSelector`] with [`RandomSplitter`].
122    pub fn default_over_all_variables(assignments: &Assignments) -> DefaultBrancher {
123        AutonomousSearch {
124            predicate_id_info: DeletablePredicateIdGenerator::default(),
125            heap: KeyValueHeap::default(),
126            dormant_predicates: vec![],
127            increment: DEFAULT_VSIDS_INCREMENT,
128            max_threshold: DEFAULT_VSIDS_MAX_THRESHOLD,
129            decay_factor: DEFAULT_VSIDS_DECAY_FACTOR,
130            best_known_solution: None,
131            should_synchronise: false,
132            backup_brancher: IndependentVariableValueBrancher::new(
133                RandomSelector::new(assignments.get_domains()),
134                RandomSplitter,
135            ),
136            statistics: Default::default(),
137        }
138    }
139
140    pub fn add_domain(&mut self, domain: DomainId) {
141        self.backup_brancher.variable_selector.add_domain(domain);
142    }
143}
144
145impl<BackupSelector> AutonomousSearch<BackupSelector> {
146    /// Creates a new instance with default values for
147    /// the parameters (`1.0` for the increment, `1e100` for the max threshold,
148    /// `0.95` for the decay factor and `0.0` for the initial VSIDS value).
149    ///
150    /// Uses the `backup_brancher` in case there are no more predicates to be selected by VSIDS.
151    pub fn new(backup_brancher: BackupSelector) -> Self {
152        AutonomousSearch {
153            predicate_id_info: DeletablePredicateIdGenerator::default(),
154            heap: KeyValueHeap::default(),
155            dormant_predicates: vec![],
156            increment: DEFAULT_VSIDS_INCREMENT,
157            max_threshold: DEFAULT_VSIDS_MAX_THRESHOLD,
158            decay_factor: DEFAULT_VSIDS_DECAY_FACTOR,
159            best_known_solution: None,
160            should_synchronise: false,
161            backup_brancher,
162            statistics: Default::default(),
163        }
164    }
165
166    /// Resizes the heap to accommodate for the id.
167    /// Recall that the underlying heap uses direct hashing.
168    fn resize_heap(&mut self, id: PredicateId) {
169        while self.heap.len() <= id.index() {
170            self.heap.grow(id, DEFAULT_VSIDS_VALUE);
171        }
172    }
173
174    /// Bumps the activity of a predicate by [`Vsids::increment`].
175    /// Used when a predicate is encountered during a conflict.
176    fn bump_activity(&mut self, predicate: Predicate) {
177        self.statistics.num_predicates_added +=
178            (!self.predicate_id_info.has_id_for_predicate(predicate)) as usize;
179        let id = self.predicate_id_info.get_id(predicate);
180        self.resize_heap(id);
181        self.heap.restore_key(id);
182
183        // Scale the activities if the values are too large.
184        // Also remove predicates that have activities close to zero.
185        let activity = self.heap.get_value(id);
186        if activity + self.increment >= self.max_threshold {
187            // Adjust heap values.
188            self.heap.divide_values(self.max_threshold);
189
190            // Adjust increment. It is important to adjust the increment after the above code.
191            self.increment /= self.max_threshold;
192        }
193        // Now perform the standard bumping
194        self.heap.increment(id, self.increment);
195    }
196
197    /// Decays the activities (i.e. increases the [`Vsids::increment`] by multiplying it
198    /// with 1 / [`Vsids::decay_factor`]) such that future bumps (see
199    /// [`Vsids::bump_activity`]) is more impactful.
200    ///
201    /// Doing it in this manner is cheaper than dividing each activity value eagerly.
202    fn decay_activities(&mut self) {
203        self.increment *= 1.0 / self.decay_factor;
204    }
205
206    fn next_candidate_predicate(&mut self, context: &mut SelectionContext) -> Option<Predicate> {
207        loop {
208            // We peek the next variable, since we do not pop since we do not (yet) want to
209            // remove the value from the heap.
210            if let Some((candidate, _)) = self.heap.peek_max() {
211                let predicate = self
212                    .predicate_id_info
213                    .get_predicate(*candidate)
214                    .expect("Expected predicate id to exist");
215                if context.is_predicate_assigned(predicate) {
216                    self.statistics.num_assigned_predicates_encountered += 1;
217                    let _ = self.heap.pop_max();
218
219                    // We know that this predicate is now dormant
220                    let predicate_id = self.predicate_id_info.get_id(predicate);
221                    self.heap.delete_key(predicate_id);
222                    self.predicate_id_info.delete_id(predicate_id);
223                    self.dormant_predicates.push(predicate);
224                } else {
225                    return Some(predicate);
226                }
227            } else {
228                return None;
229            }
230        }
231    }
232
233    /// Determines whether the provided [`Predicate`] should be returned as is or whether its
234    /// negation should be returned. This is determined based on its assignment in the best-known
235    /// solution.
236    ///
237    /// For example, if we have found the solution `x = 5` then the call `determine_polarity([x >=
238    /// 3])` would return `true`.
239    fn determine_polarity(&self, predicate: Predicate) -> Predicate {
240        if let Some(solution) = &self.best_known_solution {
241            // We have a solution
242            if !solution.contains_domain_id(predicate.get_domain()) {
243                // This can occur if an encoding is used
244                return predicate;
245            }
246            // Match the truth value according to the best solution.
247            if solution.evaluate_predicate(predicate) == Some(true) {
248                predicate
249            } else {
250                !predicate
251            }
252        } else {
253            // We do not have a solution to match against, we simply return the predicate with
254            // positive polarity
255            predicate
256        }
257    }
258
259    fn synchronise_internal(&mut self) {
260        // We drain the dormant predicates and add them back to the heap; we could check here
261        // whether the predicates are already satisfied but this appeared to introduce too much
262        // overhead in some cases.
263        self.dormant_predicates.drain(..).for_each(|predicate| {
264            let id = self.predicate_id_info.get_id(predicate);
265
266            while self.heap.len() <= id.index() {
267                self.heap.grow(id, DEFAULT_VSIDS_VALUE);
268            }
269
270            self.heap.restore_key(id);
271        });
272    }
273}
274
275impl<BackupBrancher: Brancher> Brancher for AutonomousSearch<BackupBrancher> {
276    fn next_decision(&mut self, context: &mut SelectionContext) -> Option<Predicate> {
277        if self.should_synchronise {
278            self.synchronise_internal();
279            self.should_synchronise = false;
280        }
281        self.statistics.num_calls += 1;
282        self.statistics
283            .average_size_of_heap
284            .add_term(self.heap.num_nonremoved_elements());
285        let result = self
286            .next_candidate_predicate(context)
287            .map(|predicate| self.determine_polarity(predicate));
288        if result.is_none() && !context.are_all_variables_assigned() {
289            // There are variables for which we do not have a predicate, rely on the backup
290            self.statistics.num_backup_called += 1;
291            self.backup_brancher.next_decision(context)
292        } else {
293            result
294        }
295    }
296
297    fn log_statistics(&self, statistic_logger: StatisticLogger) {
298        let statistic_logger = statistic_logger.attach_to_prefix("AutonomousSearch");
299        self.statistics.log(statistic_logger);
300    }
301
302    fn on_backtrack(&mut self) {
303        self.backup_brancher.on_backtrack()
304    }
305
306    /// Restores dormant predicates after backtracking.
307    fn synchronise(&mut self, context: &mut SelectionContext) {
308        self.should_synchronise = true;
309        self.backup_brancher.synchronise(context);
310    }
311
312    fn on_conflict(&mut self) {
313        self.decay_activities();
314        self.backup_brancher.on_conflict();
315    }
316
317    fn on_solution(&mut self, solution: SolutionReference) {
318        // We store the best known solution
319        self.best_known_solution = Some(solution.into());
320        self.backup_brancher.on_solution(solution);
321    }
322
323    fn on_appearance_in_conflict_predicate(&mut self, predicate: Predicate) {
324        self.bump_activity(predicate);
325        self.backup_brancher
326            .on_appearance_in_conflict_predicate(predicate);
327    }
328
329    fn on_restart(&mut self) {
330        self.backup_brancher.on_restart();
331    }
332
333    fn on_unassign_integer(&mut self, variable: DomainId, value: i32) {
334        self.backup_brancher.on_unassign_integer(variable, value)
335    }
336
337    fn is_restart_pointless(&mut self) -> bool {
338        false
339    }
340
341    fn subscribe_to_events(&self) -> Vec<BrancherEvent> {
342        [
343            BrancherEvent::Solution,
344            BrancherEvent::Conflict,
345            BrancherEvent::Backtrack,
346            BrancherEvent::Synchronise,
347            BrancherEvent::AppearanceInConflictPredicate,
348        ]
349        .into_iter()
350        .chain(self.backup_brancher.subscribe_to_events())
351        .collect()
352    }
353}
354
355#[cfg(test)]
356mod tests {
357    use super::AutonomousSearch;
358    use crate::basic_types::tests::TestRandom;
359    use crate::branching::Brancher;
360    use crate::branching::SelectionContext;
361    use crate::engine::Assignments;
362    use crate::engine::notifications::NotificationEngine;
363    use crate::predicate;
364    use crate::results::SolutionReference;
365
366    #[test]
367    fn brancher_picks_bumped_values() {
368        let mut assignments = Assignments::default();
369        let x = assignments.grow(0, 10);
370        let y = assignments.grow(-10, 0);
371
372        let mut brancher = AutonomousSearch::default_over_all_variables(&assignments);
373        brancher.on_appearance_in_conflict_predicate(predicate!(x >= 5));
374        brancher.on_appearance_in_conflict_predicate(predicate!(x >= 5));
375        brancher.on_appearance_in_conflict_predicate(predicate!(y >= -5));
376
377        (0..100).for_each(|_| brancher.on_conflict());
378    }
379
380    #[test]
381    fn dormant_values() {
382        let mut notification_engine = NotificationEngine::default();
383        let mut assignments = Assignments::default();
384        let x = assignments.grow(0, 10);
385        notification_engine.grow();
386
387        let mut brancher = AutonomousSearch::default_over_all_variables(&assignments);
388
389        let predicate = predicate!(x >= 5);
390        brancher.on_appearance_in_conflict_predicate(predicate);
391        let decision = brancher.next_decision(&mut SelectionContext::new(
392            &assignments,
393            &mut TestRandom::default(),
394        ));
395        assert_eq!(decision, Some(predicate));
396
397        assignments.new_checkpoint();
398        // Decision Level 1
399        let _ = assignments.post_predicate(predicate!(x >= 5), None, &mut notification_engine);
400
401        assignments.new_checkpoint();
402        // Decision Level 2
403        let _ = assignments.post_predicate(predicate!(x >= 7), None, &mut notification_engine);
404
405        assignments.new_checkpoint();
406        // Decision Level 3
407        let _ = assignments.post_predicate(predicate!(x >= 10), None, &mut notification_engine);
408
409        assignments.new_checkpoint();
410        // We end at decision level 4
411
412        let decision = brancher.next_decision(&mut SelectionContext::new(
413            &assignments,
414            &mut TestRandom::default(),
415        ));
416        assert!(decision.is_none());
417        assert!(brancher.dormant_predicates.contains(&predicate));
418
419        let _ = assignments.synchronise(3, &mut notification_engine);
420
421        let decision = brancher.next_decision(&mut SelectionContext::new(
422            &assignments,
423            &mut TestRandom::default(),
424        ));
425        assert!(decision.is_none());
426        assert!(brancher.dormant_predicates.contains(&predicate));
427
428        let _ = assignments.synchronise(0, &mut notification_engine);
429        brancher.synchronise(&mut SelectionContext::new(
430            &assignments,
431            &mut TestRandom::default(),
432        ));
433
434        let decision = brancher.next_decision(&mut SelectionContext::new(
435            &assignments,
436            &mut TestRandom::default(),
437        ));
438        assert_eq!(decision, Some(predicate));
439        assert!(!brancher.dormant_predicates.contains(&predicate));
440    }
441
442    #[test]
443    fn uses_fallback() {
444        let mut assignments = Assignments::default();
445        let x = assignments.grow(0, 10);
446
447        let mut brancher = AutonomousSearch::default_over_all_variables(&assignments);
448
449        let result = brancher.next_decision(&mut SelectionContext::new(
450            &assignments,
451            &mut TestRandom {
452                integers: vec![2],
453                usizes: vec![0],
454                bools: vec![false],
455                weighted_choice: |_| unreachable!(),
456            },
457        ));
458
459        assert_eq!(result, Some(predicate!(x <= 2)));
460    }
461
462    #[test]
463    fn uses_stored_solution() {
464        let mut notification_engine = NotificationEngine::default();
465        let mut assignments = Assignments::default();
466        let x = assignments.grow(0, 10);
467        notification_engine.grow();
468
469        assignments.new_checkpoint();
470        let _ = assignments.post_predicate(predicate!(x == 7), None, &mut notification_engine);
471
472        let mut brancher = AutonomousSearch::default_over_all_variables(&assignments);
473
474        brancher.on_solution(SolutionReference::new(&assignments));
475
476        let _ = assignments.synchronise(0, &mut notification_engine);
477
478        assert_eq!(
479            predicate!(x >= 5),
480            brancher.determine_polarity(predicate!(x >= 5))
481        );
482        assert_eq!(
483            !predicate!(x >= 10),
484            brancher.determine_polarity(predicate!(x >= 10))
485        );
486        assert_eq!(
487            predicate!(x <= 8),
488            brancher.determine_polarity(predicate!(x <= 8))
489        );
490        assert_eq!(
491            !predicate!(x <= 5),
492            brancher.determine_polarity(predicate!(x <= 5))
493        );
494
495        brancher.on_appearance_in_conflict_predicate(predicate!(x >= 5));
496
497        let result = brancher.next_decision(&mut SelectionContext::new(
498            &assignments,
499            &mut TestRandom::default(),
500        ));
501        assert_eq!(result, Some(predicate!(x >= 5)));
502    }
503}