Skip to main content

pumpkin_core/conflict_resolving/
conflict_analysis_context.rs

1use std::fmt::Debug;
2
3use crate::Random;
4use crate::basic_types::StoredConflictInfo;
5use crate::branching::Brancher;
6#[cfg(doc)]
7use crate::branching::branchers::autonomous_search::AutonomousSearch;
8#[cfg(doc)]
9use crate::conflict_resolving::ConflictResolver;
10use crate::conflict_resolving::LearnedNogood;
11use crate::containers::HashMap;
12use crate::engine::Assignments;
13use crate::engine::ConstraintSatisfactionSolver;
14use crate::engine::EmptyDomainConflict;
15use crate::engine::RestartStrategy;
16use crate::engine::State;
17use crate::engine::TrailedValues;
18use crate::engine::constraint_satisfaction_solver::CSPSolverState;
19use crate::engine::constraint_satisfaction_solver::NogoodLabel;
20use crate::engine::predicates::predicate::Predicate;
21use crate::engine::predicates::predicate::PredicateType;
22use crate::predicate;
23use crate::predicates::PropositionalConjunction;
24use crate::proof::ConstraintTag;
25use crate::proof::InferenceCode;
26use crate::proof::ProofLog;
27use crate::proof::RootExplanationContext;
28use crate::proof::explain_root_assignment;
29use crate::propagation::CurrentNogood;
30use crate::propagation::ExplanationContext;
31use crate::propagation::HasAssignments;
32use crate::propagators::nogoods::NogoodChecker;
33use crate::propagators::nogoods::NogoodPropagator;
34use crate::pumpkin_assert_eq_simple;
35use crate::state::EmptyDomain;
36use crate::state::PropagatorHandle;
37
38/// Used during conflict analysis to provide the necessary information.
39///
40/// All fields are made public for the time being for simplicity. In the future that may change.
41pub struct ConflictAnalysisContext<'a> {
42    pub(crate) solver_state: &'a mut CSPSolverState,
43    pub(crate) brancher: &'a mut dyn Brancher,
44
45    pub(crate) proof_log: &'a mut ProofLog,
46
47    pub(crate) unit_nogood_inference_codes: &'a mut HashMap<Predicate, InferenceCode>,
48
49    pub(crate) restart_strategy: &'a mut RestartStrategy,
50    pub(crate) state: &'a mut State,
51
52    pub(crate) nogood_propagator_handle: PropagatorHandle<NogoodPropagator>,
53
54    pub(crate) rng: &'a mut dyn Random,
55}
56
57impl Debug for ConflictAnalysisContext<'_> {
58    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
59        f.debug_struct(std::any::type_name::<Self>()).finish()
60    }
61}
62
63impl ConflictAnalysisContext<'_> {
64    pub fn get_state(&self) -> &State {
65        self.state
66    }
67
68    /// Apply a [`Predicate`] to the [`State`].
69    ///
70    /// Returns `true` if a change to a domain occured, and `false` if the given [`Predicate`] was
71    /// already true.
72    ///
73    /// If a domain becomes empty due to this operation, an [`EmptyDomain`] error is returned.
74    pub fn post(&mut self, predicate: Predicate) -> Result<bool, EmptyDomain> {
75        self.state.post(predicate)
76    }
77
78    /// Restore to the given checkpoint.
79    ///
80    /// If the provided checkpoint is equal to the current checkpoint, this is a no-op. If
81    /// the provided checkpoint is larger than the current checkpoint, this method will
82    /// panic.
83    ///
84    /// See [`State::new_checkpoint`] for an example.
85    pub fn restore_to(&mut self, checkpoint: usize) {
86        ConstraintSatisfactionSolver::backtrack(self.state, checkpoint, self.brancher, self.rng);
87    }
88
89    /// Returns a nogood which led to the conflict, excluding predicates from the root decision
90    /// level.
91    pub fn get_conflict_nogood(&mut self) -> Vec<Predicate> {
92        let conflict_nogood = match self.solver_state.get_conflict_info() {
93            StoredConflictInfo::Propagator(conflict) => {
94                let _ = self.proof_log.log_inference(
95                    &mut self.state.constraint_tags,
96                    conflict.inference_code,
97                    conflict.conjunction.iter().copied(),
98                    None,
99                    &self.state.variable_names,
100                    &self.state.assignments,
101                );
102
103                conflict.conjunction
104            }
105            StoredConflictInfo::EmptyDomain(conflict) => self.compute_conflict_nogood(conflict),
106            StoredConflictInfo::RootLevelConflict(_) => {
107                unreachable!("Should never attempt to learn a nogood from a root level conflict")
108            }
109            StoredConflictInfo::InconsistentAssumptions(predicate) => {
110                vec![predicate, !predicate].into()
111            }
112        };
113
114        for &predicate in conflict_nogood.iter() {
115            let predicate_dl = self
116                .state
117                .get_checkpoint_for_predicate(predicate)
118                .expect("all predicates in the conflict nogood should be assigned to true");
119
120            if predicate_dl == 0 {
121                explain_root_assignment(
122                    &mut RootExplanationContext {
123                        proof_log: self.proof_log,
124                        unit_nogood_inference_codes: self.unit_nogood_inference_codes,
125                        state: self.state,
126                    },
127                    predicate,
128                );
129            }
130        }
131
132        conflict_nogood
133            .into_iter()
134            .filter(|&p| self.state.get_checkpoint_for_predicate(p).unwrap() > 0)
135            .collect()
136    }
137
138    /// Compute the reason for `predicate` being true. The reason will be stored in
139    /// `reason_buffer`.
140    ///
141    /// If `predicate` is not true, or it is a decision, then this function will panic.
142    pub fn get_propagation_reason(
143        &mut self,
144        predicate: Predicate,
145        current_nogood: CurrentNogood<'_>,
146        reason_buffer: &mut (impl Extend<Predicate> + AsRef<[Predicate]>),
147    ) {
148        Self::get_propagation_reason_inner(
149            predicate,
150            current_nogood,
151            self.proof_log,
152            self.unit_nogood_inference_codes,
153            reason_buffer,
154            self.state,
155        );
156    }
157
158    /// Returns the last decision which was made by the solver (if such a decision exists).
159    pub fn find_last_decision(&mut self) -> Option<Predicate> {
160        self.state.assignments.find_last_decision()
161    }
162}
163
164/// Methods used for proof logging
165impl ConflictAnalysisContext<'_> {
166    /// Explains the root assignment of `predicate` in the proof log.
167    pub fn explain_root_assignment(&mut self, predicate: Predicate) {
168        explain_root_assignment(
169            &mut RootExplanationContext {
170                proof_log: self.proof_log,
171                unit_nogood_inference_codes: self.unit_nogood_inference_codes,
172                state: self.state,
173            },
174            predicate,
175        );
176    }
177
178    /// Log a deduction (learned nogood) to the proof.
179    ///
180    /// The inferences and marked propagations are assumed to be recorded in reverse-application
181    /// order.
182    pub fn log_deduction(
183        &mut self,
184        premises: impl IntoIterator<Item = Predicate>,
185    ) -> ConstraintTag {
186        self.proof_log
187            .log_deduction(
188                premises,
189                &self.state.variable_names,
190                &mut self.state.constraint_tags,
191                &self.state.assignments,
192            )
193            .expect("Failed to write proof log")
194    }
195}
196
197/// Methods used for keeping track of statistics.
198impl ConflictAnalysisContext<'_> {
199    /// Informs the used [`Brancher`] that the provided `predicate` appeared during conflict
200    /// analysis.
201    ///
202    /// This is used by [`Brancher`]s such as [`AutonomousSearch`] to guide the search.
203    pub fn predicate_appeared_in_conflict(&mut self, predicate: Predicate) {
204        self.brancher.on_appearance_in_conflict_predicate(predicate);
205    }
206}
207
208impl ConflictAnalysisContext<'_> {
209    /// Backtracks the solver and adds the learned nogood to the database, returning the level to
210    /// which the solver backtracked.
211    pub fn process_learned_nogood(
212        &mut self,
213        learned_nogood_predicates: Vec<Predicate>,
214        lbd: u32,
215    ) -> usize {
216        // important to notify about the conflict _before_ backtracking removes literals from
217        // the trail -> although in the current version this does nothing but notify that a
218        // conflict happened
219        self.restart_strategy
220            .notify_conflict(lbd, self.state.assignments.get_pruned_value_count());
221
222        let learned_nogood = LearnedNogood::create_from_vec(learned_nogood_predicates, self);
223
224        let constraint_tag = self.log_deduction(learned_nogood.predicates.iter().copied());
225        let inference_code = InferenceCode::new(constraint_tag, NogoodLabel);
226
227        self.state.add_inference_checker(
228            inference_code.clone(),
229            Box::new(NogoodChecker {
230                nogood: learned_nogood.predicates.clone().into(),
231            }),
232        );
233
234        self.restore_to(learned_nogood.backtrack_level);
235
236        if learned_nogood.len() == 1 {
237            let _ = self
238                .unit_nogood_inference_codes
239                .insert(!learned_nogood[0], inference_code.clone());
240        }
241
242        #[cfg(feature = "check-propagations")]
243        let trail_len_before_nogood = self.state.trail_len();
244
245        let (nogood_propagator, mut propagation_context) = self
246            .state
247            .get_propagator_mut_with_context(self.nogood_propagator_handle);
248        let nogood_propagator =
249            nogood_propagator.expect("nogood propagator handle should refer to nogood propagator");
250
251        nogood_propagator.add_asserting_nogood(
252            learned_nogood.to_vec(),
253            inference_code,
254            &mut propagation_context,
255        );
256
257        #[cfg(feature = "check-propagations")]
258        self.state.check_propagations(trail_len_before_nogood);
259
260        learned_nogood.backtrack_level
261    }
262
263    /// Compute the reason for `predicate` being true. The reason will be stored in
264    /// `reason_buffer`.
265    ///
266    /// If `predicate` is not true, or it is a decision, then this function will panic.
267    pub(crate) fn get_propagation_reason_inner(
268        predicate: Predicate,
269        current_nogood: CurrentNogood<'_>,
270        proof_log: &mut ProofLog,
271        unit_nogood_inference_codes: &HashMap<Predicate, InferenceCode>,
272        reason_buffer: &mut (impl Extend<Predicate> + AsRef<[Predicate]>),
273        state: &mut State,
274    ) {
275        let trail_index = state.get_propagation_reason(predicate, reason_buffer, current_nogood);
276
277        if let Some(trail_index) = trail_index {
278            let trail_entry = state.assignments.get_trail_entry(trail_index);
279            let (reason_ref, inference_code) = trail_entry
280                .reason
281                .expect("Cannot be a null reason for propagation.");
282
283            let propagator_id = state.reason_store.get_propagator(reason_ref);
284
285            if state
286                .propagators
287                .as_propagator_handle::<NogoodPropagator>(propagator_id)
288                .is_some()
289                && reason_buffer.as_ref().is_empty()
290            {
291                // This means that a unit nogood was propagated, we indicate that this nogood step
292                // was used
293                //
294                // It could be that the predicate is implied by another unit nogood
295
296                let inference_code = unit_nogood_inference_codes
297                    .get(&predicate)
298                    .or_else(|| {
299                        // It could be the case that we attempt to get the reason for the predicate
300                        // [x >= v] but that the corresponding unit nogood idea is the one for the
301                        // predicate [x == v]
302                        let domain_id = predicate.get_domain();
303                        let right_hand_side = predicate.get_right_hand_side();
304
305                        unit_nogood_inference_codes.get(&predicate!(domain_id == right_hand_side))
306                    })
307                    .expect("Expected to be able to retrieve step id for unit nogood");
308
309                let _ = proof_log.log_inference(
310                    &mut state.constraint_tags,
311                    inference_code.clone(),
312                    [],
313                    Some(predicate),
314                    &state.variable_names,
315                    &state.assignments,
316                );
317            } else {
318                // Otherwise we log the inference which was used to derive the nogood
319                let _ = proof_log.log_inference(
320                    &mut state.constraint_tags,
321                    inference_code,
322                    reason_buffer.as_ref().iter().copied(),
323                    Some(predicate),
324                    &state.variable_names,
325                    &state.assignments,
326                );
327            }
328        }
329    }
330
331    fn compute_conflict_nogood(
332        &mut self,
333        conflict: EmptyDomainConflict,
334    ) -> PropositionalConjunction {
335        let conflict_domain = conflict.domain();
336
337        // Look up the reason for the bound that changed.
338        // The reason for changing the bound cannot be a decision, so we can safely unwrap.
339        let mut empty_domain_reason: Vec<Predicate> = vec![];
340        let _ = self.state.reason_store.get_or_compute(
341            conflict.trigger_reason,
342            ExplanationContext::without_working_nogood(
343                &self.state.assignments,
344                self.state.assignments.num_trail_entries(), // Note that we do not do a
345                // `-1` here; the `Assignments` automatically undoes the last trail entry when an
346                // empty domain is created meaning that the `-1` has already been applied.
347                &mut self.state.notification_engine,
348            ),
349            &mut self.state.propagators,
350            &mut empty_domain_reason,
351        );
352
353        // We also need to log this last propagation to the proof log as an inference.
354        let _ = self.proof_log.log_inference(
355            &mut self.state.constraint_tags,
356            conflict.trigger_inference_code,
357            empty_domain_reason.iter().copied(),
358            Some(conflict.trigger_predicate),
359            &self.state.variable_names,
360            &self.state.assignments,
361        );
362
363        let old_lower_bound = self.state.lower_bound(conflict_domain);
364        let old_upper_bound = self.state.upper_bound(conflict_domain);
365
366        match conflict.trigger_predicate.get_predicate_type() {
367            PredicateType::LowerBound => {
368                // The last trail entry was a lower-bound propagation meaning that the empty domain
369                // was caused by the upper-bound
370                //
371                // We lift so that it is the most general upper-bound possible while still causing
372                // the empty domain
373                empty_domain_reason.push(predicate!(
374                    conflict_domain <= conflict.trigger_predicate.get_right_hand_side() - 1
375                ));
376            }
377            PredicateType::UpperBound => {
378                // The last trail entry was an upper-bound propagation meaning that the empty domain
379                // was caused by the lower-bound
380                //
381                // We lift so that it is the most general lower-bound possible while still causing
382                // the empty domain
383                empty_domain_reason.push(predicate!(
384                    conflict_domain >= conflict.trigger_predicate.get_right_hand_side() + 1
385                ));
386            }
387            PredicateType::NotEqual => {
388                // The last trail entry was a not equals propagation meaning that the empty domain
389                // was due to the domain being assigned to the removed value
390                pumpkin_assert_eq_simple!(old_upper_bound, old_lower_bound);
391
392                empty_domain_reason.push(predicate!(conflict_domain == old_lower_bound));
393            }
394            PredicateType::Equal => {
395                // The last trail entry was an equality propagation; we split into three cases.
396                if conflict.trigger_predicate.get_right_hand_side() < old_lower_bound {
397                    // 1) The assigned value was lower than the lower-bound
398                    //
399                    // We lift so that it is the most general lower-bound possible while still
400                    // causing the empty domain
401                    empty_domain_reason.push(predicate!(
402                        conflict_domain >= conflict.trigger_predicate.get_right_hand_side() + 1
403                    ));
404                } else if conflict.trigger_predicate.get_right_hand_side() > old_upper_bound {
405                    // 2) The assigned value was larger than the upper-bound
406                    //
407                    // We lift so that it is the most general upper-bound possible while still
408                    // causing the empty domain
409                    empty_domain_reason.push(predicate!(
410                        conflict_domain <= conflict.trigger_predicate.get_right_hand_side() - 1
411                    ));
412                } else {
413                    // 3) The assigned value was equal to a hole in the domain
414                    empty_domain_reason.push(predicate!(
415                        conflict_domain != conflict.trigger_predicate.get_right_hand_side()
416                    ))
417                }
418            }
419        }
420
421        empty_domain_reason.into()
422    }
423}
424
425impl HasAssignments for ConflictAnalysisContext<'_> {
426    fn assignments(&self) -> &Assignments {
427        &self.state.assignments
428    }
429
430    fn trailed_values(&self) -> &TrailedValues {
431        &self.state.trailed_values
432    }
433
434    fn trailed_values_mut(&mut self) -> &mut TrailedValues {
435        &mut self.state.trailed_values
436    }
437}