pumpkin_core/engine/
constraint_satisfaction_solver.rs

1//! Houses the solver which attempts to find a solution to a Constraint Satisfaction Problem (CSP)
2//! using a Lazy Clause Generation approach.
3use std::cmp::max;
4use std::collections::VecDeque;
5use std::fmt::Debug;
6use std::time::Instant;
7
8use rand::rngs::SmallRng;
9use rand::SeedableRng;
10
11use super::conflict_analysis::AnalysisMode;
12use super::conflict_analysis::ConflictAnalysisContext;
13use super::conflict_analysis::LearnedNogood;
14use super::conflict_analysis::NoLearningResolver;
15use super::conflict_analysis::SemanticMinimiser;
16use super::notifications::NotificationEngine;
17use super::propagation::constructor::PropagatorConstructor;
18use super::propagation::store::PropagatorStore;
19use super::propagation::PropagatorId;
20use super::solver_statistics::SolverStatistics;
21use super::termination::TerminationCondition;
22use super::variables::IntegerVariable;
23use super::variables::Literal;
24use super::Lbd;
25use super::ResolutionResolver;
26use super::TrailedValues;
27use super::VariableNames;
28use crate::basic_types::moving_averages::MovingAverage;
29use crate::basic_types::CSPSolverExecutionFlag;
30use crate::basic_types::ConstraintOperationError;
31use crate::basic_types::Inconsistency;
32use crate::basic_types::PredicateId;
33use crate::basic_types::PropositionalConjunction;
34use crate::basic_types::Random;
35use crate::basic_types::SolutionReference;
36use crate::basic_types::StoredConflictInfo;
37use crate::branching::Brancher;
38use crate::branching::SelectionContext;
39use crate::containers::HashMap;
40use crate::declare_inference_label;
41use crate::engine::conflict_analysis::ConflictResolver as Resolver;
42use crate::engine::cp::PropagatorQueue;
43use crate::engine::predicates::predicate::Predicate;
44use crate::engine::propagation::constructor::PropagatorConstructorContext;
45use crate::engine::propagation::store::PropagatorHandle;
46use crate::engine::propagation::ExplanationContext;
47use crate::engine::propagation::PropagationContext;
48use crate::engine::propagation::PropagationContextMut;
49use crate::engine::propagation::Propagator;
50use crate::engine::reason::ReasonStore;
51use crate::engine::variables::DomainId;
52use crate::engine::Assignments;
53use crate::engine::DebugHelper;
54use crate::engine::RestartOptions;
55use crate::engine::RestartStrategy;
56use crate::predicate;
57use crate::predicates::PredicateType;
58use crate::proof::explain_root_assignment;
59use crate::proof::finalize_proof;
60use crate::proof::ConstraintTag;
61use crate::proof::FinalizingContext;
62use crate::proof::InferenceCode;
63use crate::proof::ProofLog;
64use crate::proof::RootExplanationContext;
65use crate::propagators::nogoods::LearningOptions;
66use crate::propagators::nogoods::NogoodPropagator;
67use crate::pumpkin_assert_advanced;
68use crate::pumpkin_assert_eq_simple;
69use crate::pumpkin_assert_extreme;
70use crate::pumpkin_assert_moderate;
71use crate::pumpkin_assert_simple;
72use crate::statistics::statistic_logger::StatisticLogger;
73use crate::statistics::statistic_logging::should_log_statistics;
74#[cfg(doc)]
75use crate::Solver;
76
77/// A solver which attempts to find a solution to a Constraint Satisfaction Problem (CSP) using
78/// a Lazy Clause Generation (LCG [\[1\]](https://people.eng.unimelb.edu.au/pstuckey/papers/cp09-lc.pdf))
79/// approach.
80///
81/// It requires that all of the propagators which are added, are able to explain the
82/// propagations and conflicts they have made/found. It then uses standard SAT concepts such as
83/// 1UIP (see \[2\]) to learn clauses (also called nogoods in the CP field, see \[3\]) to avoid
84/// unnecessary exploration of the search space while utilizing the search procedure benefits from
85/// constraint programming (e.g. by preventing the exponential blow-up of problem encodings).
86///
87/// # Practical
88/// The [`ConstraintSatisfactionSolver`] makes use of certain options which allow the user to
89/// influence the behaviour of the solver; see for example the [`SatisfactionSolverOptions`].
90///
91/// The solver switches between making decisions using implementations of the [`Brancher`] (which
92/// are passed to the [`ConstraintSatisfactionSolver::solve`] method) and propagation (use
93/// [`ConstraintSatisfactionSolver::add_propagator`] to add a propagator). If a conflict is found by
94/// any of the propagators then the solver will analyse the conflict
95/// using 1UIP reasoning and backtrack if possible.
96///
97/// # Bibliography
98/// \[1\] T. Feydy and P. J. Stuckey, ‘Lazy clause generation reengineered’, in International
99/// Conference on Principles and Practice of Constraint Programming, 2009, pp. 352–366.
100///
101/// \[2\] J. Marques-Silva, I. Lynce, and S. Malik, ‘Conflict-driven clause learning SAT
102/// solvers’, in Handbook of satisfiability, IOS press, 2021
103///
104/// \[3\] F. Rossi, P. Van Beek, and T. Walsh, ‘Constraint programming’, Foundations of Artificial
105/// Intelligence, vol. 3, pp. 181–211, 2008.
106#[derive(Debug)]
107pub struct ConstraintSatisfactionSolver {
108    /// The solver continuously changes states during the search.
109    /// The state helps track additional information and contributes to making the code clearer.
110    pub(crate) state: CSPSolverState,
111    /// The list of propagators. Propagators live here and are queried when events (domain changes)
112    /// happen. The list is only traversed during synchronisation for now.
113    propagators: PropagatorStore,
114    nogood_propagator_handle: PropagatorHandle<NogoodPropagator>,
115
116    /// Tracks information about the restarts. Occassionally the solver will undo all its decisions
117    /// and start the search from the root note. Note that learned clauses and other state
118    /// information is kept after a restart.
119    restart_strategy: RestartStrategy,
120    /// Holds the assumptions when the solver is queried to solve under assumptions.
121    assumptions: Vec<Predicate>,
122    semantic_minimiser: SemanticMinimiser,
123    /// Tracks information related to the assignments of integer variables.
124    pub(crate) assignments: Assignments,
125    /// Dictates the order in which propagators will be called to propagate.
126    propagator_queue: PropagatorQueue,
127    /// Handles storing information about propagation reasons, which are used later to construct
128    /// explanations during conflict analysis
129    pub(crate) reason_store: ReasonStore,
130    /// A set of counters updated during the search.
131    solver_statistics: SolverStatistics,
132    /// Miscellaneous constant parameters used by the solver.
133    internal_parameters: SatisfactionSolverOptions,
134    /// The names of the variables in the solver.
135    variable_names: VariableNames,
136    /// Computes the LBD for nogoods.
137    lbd_helper: Lbd,
138    /// A map from predicates that are propagated at the root to inference codes in the proof.
139    unit_nogood_inference_codes: HashMap<Predicate, InferenceCode>,
140    /// The resolver which is used upon a conflict.
141    conflict_resolver: Box<dyn Resolver>,
142    /// Keep track of trailed values (i.e. values which automatically backtrack)
143    pub(crate) trailed_values: TrailedValues,
144    /// Component responsible for providing notifications for changes to the domains of variables
145    /// and/or the polarity [Predicate]s
146    notification_engine: NotificationEngine,
147}
148
149impl Default for ConstraintSatisfactionSolver {
150    fn default() -> Self {
151        ConstraintSatisfactionSolver::new(SatisfactionSolverOptions::default())
152    }
153}
154
155/// The result of [`ConstraintSatisfactionSolver::extract_clausal_core`]; there are 2 cases:
156/// 1. In the case of [`CoreExtractionResult::ConflictingAssumption`], two assumptions have been
157///    given which directly conflict with one another; e.g. if the assumptions `[x, !x]` have been
158///    given then the result of [`ConstraintSatisfactionSolver::extract_clausal_core`] will be a
159///    [`CoreExtractionResult::ConflictingAssumption`] containing `x`.
160/// 2. The standard case is when a [`CoreExtractionResult::Core`] is returned which contains (a
161///    subset of) the assumptions which led to conflict.
162#[derive(Debug, Clone)]
163pub enum CoreExtractionResult {
164    /// Conflicting assumptions were provided; e.g. in the case of the assumptions `[x, !x]`, this
165    /// result will contain `!x`
166    ConflictingAssumption(Predicate),
167    /// The standard case where this result contains the core consisting of (a subset of) the
168    /// assumptions which led to conflict.
169    Core(Vec<Predicate>),
170}
171
172/// During search, the CP solver will inevitably evaluate partial assignments that violate at
173/// least one constraint. When this happens, conflict resolution is applied to restore the
174/// solver to a state from which it can continue the search.
175///
176/// The manner in which conflict resolution is done greatly impacts the performance of the
177/// solver.
178#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
179#[cfg_attr(feature = "clap", derive(clap::ValueEnum))]
180pub enum ConflictResolver {
181    NoLearning,
182    #[default]
183    UIP,
184}
185
186/// Options for the [`Solver`] which determine how it behaves.
187#[derive(Debug)]
188pub struct SatisfactionSolverOptions {
189    /// The options used by the restart strategy.
190    pub restart_options: RestartOptions,
191    /// Whether learned clause minimisation should take place
192    pub learning_clause_minimisation: bool,
193    /// A random number generator which is used by the [`Solver`] to determine randomised values.
194    pub random_generator: SmallRng,
195    /// The proof log for the solver.
196    pub proof_log: ProofLog,
197    /// The resolver used for conflict analysis
198    pub conflict_resolver: ConflictResolver,
199    /// The options which influence the learning of the solver.
200    pub learning_options: LearningOptions,
201    /// The number of MBs which are preallocated by the nogood propagator.
202    pub memory_preallocated: usize,
203}
204
205impl Default for SatisfactionSolverOptions {
206    fn default() -> Self {
207        SatisfactionSolverOptions {
208            restart_options: RestartOptions::default(),
209            learning_clause_minimisation: true,
210            random_generator: SmallRng::seed_from_u64(42),
211            proof_log: ProofLog::default(),
212            conflict_resolver: ConflictResolver::default(),
213            learning_options: LearningOptions::default(),
214            memory_preallocated: 1000,
215        }
216    }
217}
218
219impl ConstraintSatisfactionSolver {
220    /// This is a temporary accessor to help refactoring.
221    pub fn get_solution_reference(&self) -> SolutionReference<'_> {
222        SolutionReference::new(&self.assignments)
223    }
224
225    /// Conclude the proof with the unsatisfiable claim.
226    ///
227    /// This method will finish the proof. Any new operation will not be logged to the proof.
228    pub fn conclude_proof_unsat(&mut self) -> std::io::Result<()> {
229        let proof = std::mem::take(&mut self.internal_parameters.proof_log);
230        proof.unsat(&self.variable_names)
231    }
232
233    /// Conclude the proof with the optimality claim.
234    ///
235    /// This method will finish the proof. Any new operation will not be logged to the proof.
236    pub fn conclude_proof_optimal(&mut self, bound: Predicate) -> std::io::Result<()> {
237        let proof = std::mem::take(&mut self.internal_parameters.proof_log);
238        proof.optimal(bound, &self.variable_names)
239    }
240
241    fn complete_proof(&mut self) {
242        let conflict = match self.state.get_conflict_info() {
243            StoredConflictInfo::Propagator(conflict) => {
244                let _ = self.internal_parameters.proof_log.log_inference(
245                    conflict.inference_code,
246                    conflict.conjunction.iter().copied(),
247                    None,
248                    &self.variable_names,
249                );
250
251                conflict.conjunction.clone()
252            }
253            StoredConflictInfo::EmptyDomain { conflict_nogood } => conflict_nogood.clone(),
254            StoredConflictInfo::RootLevelConflict(_) => {
255                unreachable!("There should always be a specified conflicting constraint.")
256            }
257        };
258
259        let context = FinalizingContext {
260            conflict,
261            propagators: &mut self.propagators,
262            proof_log: &mut self.internal_parameters.proof_log,
263            unit_nogood_inference_codes: &self.unit_nogood_inference_codes,
264            assignments: &self.assignments,
265            reason_store: &mut self.reason_store,
266            notification_engine: &mut self.notification_engine,
267            variable_names: &self.variable_names,
268        };
269
270        finalize_proof(context);
271    }
272
273    pub(crate) fn is_logging_full_proof(&self) -> bool {
274        self.internal_parameters.proof_log.is_logging_inferences()
275    }
276}
277
278// methods that offer basic functionality
279impl ConstraintSatisfactionSolver {
280    pub fn new(solver_options: SatisfactionSolverOptions) -> Self {
281        let mut propagators = PropagatorStore::default();
282
283        let nogood_propagator_slot = propagators.new_propagator();
284        let nogood_propagator = NogoodPropagator::with_options(
285            nogood_propagator_slot.key(),
286            // 1_000_000 bytes in 1 MB
287            (solver_options.memory_preallocated * 1_000_000) / size_of::<PredicateId>(),
288            solver_options.learning_options,
289        );
290
291        let nogood_propagator_handle = nogood_propagator_slot.populate(nogood_propagator);
292
293        let mut csp_solver = ConstraintSatisfactionSolver {
294            state: CSPSolverState::default(),
295            assumptions: Vec::default(),
296            assignments: Assignments::default(),
297            propagator_queue: PropagatorQueue::new(5),
298            reason_store: ReasonStore::default(),
299            restart_strategy: RestartStrategy::new(solver_options.restart_options),
300            propagators,
301            nogood_propagator_handle,
302            solver_statistics: SolverStatistics::default(),
303            variable_names: VariableNames::default(),
304            semantic_minimiser: SemanticMinimiser::default(),
305            lbd_helper: Lbd::default(),
306            unit_nogood_inference_codes: Default::default(),
307            conflict_resolver: match solver_options.conflict_resolver {
308                ConflictResolver::NoLearning => Box::new(NoLearningResolver),
309                ConflictResolver::UIP => Box::new(ResolutionResolver::default()),
310            },
311            internal_parameters: solver_options,
312            trailed_values: TrailedValues::default(),
313            notification_engine: NotificationEngine::default(),
314        };
315
316        // As a convention, the assignments contain a dummy domain_id=0, which represents a 0-1
317        // variable that is assigned to one. We use it to represent predicates that are
318        // trivially true. We need to adjust other data structures to take this into account.
319        let dummy_id = Predicate::trivially_true().get_domain();
320
321        csp_solver
322            .variable_names
323            .add_integer(dummy_id, "Dummy".to_owned());
324
325        assert!(dummy_id.id() == 0);
326        assert!(csp_solver.assignments.get_lower_bound(dummy_id) == 1);
327        assert!(csp_solver.assignments.get_upper_bound(dummy_id) == 1);
328
329        csp_solver
330    }
331
332    pub fn solve(
333        &mut self,
334        termination: &mut impl TerminationCondition,
335        brancher: &mut impl Brancher,
336    ) -> CSPSolverExecutionFlag {
337        let dummy_assumptions: Vec<Predicate> = vec![];
338        self.solve_under_assumptions(&dummy_assumptions, termination, brancher)
339    }
340
341    pub fn solve_under_assumptions(
342        &mut self,
343        assumptions: &[Predicate],
344        termination: &mut impl TerminationCondition,
345        brancher: &mut impl Brancher,
346    ) -> CSPSolverExecutionFlag {
347        if self.state.is_inconsistent() {
348            return CSPSolverExecutionFlag::Infeasible;
349        }
350
351        let start_time = Instant::now();
352
353        self.initialise(assumptions);
354        let result = self.solve_internal(termination, brancher);
355
356        self.solver_statistics
357            .engine_statistics
358            .time_spent_in_solver += start_time.elapsed();
359
360        result
361    }
362
363    pub fn get_state(&self) -> &CSPSolverState {
364        &self.state
365    }
366
367    pub fn get_random_generator(&mut self) -> &mut impl Random {
368        &mut self.internal_parameters.random_generator
369    }
370
371    pub fn log_statistics(&self, verbose: bool) {
372        // We first check whether the statistics will/should be logged to prevent unnecessarily
373        // going through all the propagators
374        if !should_log_statistics() {
375            return;
376        }
377
378        self.solver_statistics.log(
379            &self.assignments,
380            &self.propagators,
381            StatisticLogger::default(),
382            verbose,
383        );
384        if verbose {
385            for (index, propagator) in self.propagators.iter_propagators().enumerate() {
386                propagator.log_statistics(StatisticLogger::new([
387                    propagator.name(),
388                    "number",
389                    index.to_string().as_str(),
390                ]));
391            }
392        }
393    }
394
395    pub fn create_new_literal(&mut self, name: Option<String>) -> Literal {
396        let domain_id = self.create_new_integer_variable(0, 1, name);
397        Literal::new(domain_id)
398    }
399
400    pub fn create_new_literal_for_predicate(
401        &mut self,
402        predicate: Predicate,
403        name: Option<String>,
404        constraint_tag: ConstraintTag,
405    ) -> Literal {
406        let literal = self.create_new_literal(name);
407
408        self.internal_parameters
409            .proof_log
410            .reify_predicate(literal, predicate);
411
412        // If literal --> predicate
413        let _ = self.add_clause(
414            vec![!literal.get_true_predicate(), predicate],
415            constraint_tag,
416        );
417
418        // If !literal --> !predicate
419        let _ = self.add_clause(
420            vec![!literal.get_false_predicate(), !predicate],
421            constraint_tag,
422        );
423
424        literal
425    }
426
427    /// Create a new integer variable. Its domain will have the given lower and upper bounds.
428    pub fn create_new_integer_variable(
429        &mut self,
430        lower_bound: i32,
431        upper_bound: i32,
432        name: Option<String>,
433    ) -> DomainId {
434        assert!(
435            !self.state.is_inconsistent(),
436            "Variables cannot be created in an inconsistent state"
437        );
438
439        let domain_id = self.assignments.grow(lower_bound, upper_bound);
440        self.notification_engine.grow();
441
442        if let Some(name) = name {
443            self.variable_names.add_integer(domain_id, name);
444        }
445
446        domain_id
447    }
448
449    /// Creates an integer variable with a domain containing only the values in `values`
450    pub fn create_new_integer_variable_sparse(
451        &mut self,
452        values: Vec<i32>,
453        name: Option<String>,
454    ) -> DomainId {
455        let domain_id = self.assignments.create_new_integer_variable_sparse(values);
456
457        self.notification_engine.grow();
458
459        if let Some(name) = name {
460            self.variable_names.add_integer(domain_id, name);
461        }
462
463        domain_id
464    }
465
466    /// Create a new [`ConstraintTag`].
467    pub fn new_constraint_tag(&mut self) -> ConstraintTag {
468        self.internal_parameters.proof_log.new_constraint_tag()
469    }
470
471    /// Returns an unsatisfiable core or an [`Err`] if the provided assumptions were conflicting
472    /// with one another ([`Err`] then contain the [`Literal`] which was conflicting).
473    ///
474    /// We define an unsatisfiable core as a clause containing only negated assumption literals,
475    /// which is implied by the formula. Alternatively, it is the negation of a conjunction of
476    /// assumptions which cannot be satisfied together with the rest of the formula. The clause is
477    /// not necessarily unique or minimal.
478    ///
479    /// The unsatisfiable core can be verified with reverse unit propagation (RUP).
480    ///
481    /// *Notes:*
482    ///   - If the solver is not in an unsatisfied state, this method will panic.
483    ///   - If the solver is in an unsatisfied state, but solving was done without assumptions, this
484    ///     will return an empty vector.
485    ///   - If the assumptions are inconsistent, i.e. both literal x and !x are assumed, an error is
486    ///     returned, with the literal being one of the inconsistent assumptions.
487    ///
488    /// # Example usage
489    /// ```rust
490    /// // We construct the following SAT instance:
491    /// //   (x0 \/ x1 \/ x2) /\ (x0 \/ !x1 \/ x2)
492    /// // And solve under the assumptions:
493    /// //   !x0 /\ x1 /\ !x2
494    /// # use pumpkin_core::Solver;
495    /// # use pumpkin_core::termination::Indefinite;
496    /// # use pumpkin_core::results::SatisfactionResultUnderAssumptions;
497    /// let mut solver = Solver::default();
498    ///
499    /// // We use a dummy constraint tag for this example.
500    /// let constraint_tag = solver.new_constraint_tag();
501    ///
502    /// let x = vec![
503    ///     solver.new_literal().get_true_predicate(),
504    ///     solver.new_literal().get_true_predicate(),
505    ///     solver.new_literal().get_true_predicate(),
506    /// ];
507    ///
508    /// solver.add_clause([x[0], x[1], x[2]], constraint_tag);
509    /// solver.add_clause([x[0], !x[1], x[2]], constraint_tag);
510    ///
511    /// let assumptions = [!x[0], x[1], !x[2]];
512    /// let mut termination = Indefinite;
513    /// let mut brancher = solver.default_brancher();
514    /// let result = solver.satisfy_under_assumptions(&mut brancher, &mut termination, &assumptions);
515    ///
516    /// if let SatisfactionResultUnderAssumptions::UnsatisfiableUnderAssumptions(mut unsatisfiable) =
517    ///     result
518    /// {
519    ///     {
520    ///         let core = unsatisfiable.extract_core();
521    ///
522    ///         // The order of the literals in the core is undefined, so we check for unordered
523    ///         // equality.
524    ///         assert_eq!(
525    ///             core.len(),
526    ///             assumptions.len(),
527    ///             "The core has the length of the number of assumptions"
528    ///         );
529    ///         assert!(
530    ///             core.iter().all(|&lit| assumptions.contains(&lit)),
531    ///             "All literals in the core are assumptions"
532    ///         );
533    ///     }
534    /// }
535    /// ```
536    pub fn extract_clausal_core(&mut self, brancher: &mut impl Brancher) -> CoreExtractionResult {
537        if self.state.is_infeasible() {
538            return CoreExtractionResult::Core(vec![]);
539        }
540
541        self.assumptions
542            .iter()
543            .enumerate()
544            .find(|(index, assumption)| {
545                self.assumptions
546                    .iter()
547                    .skip(index + 1)
548                    .any(|other_assumptiion| {
549                        assumption.is_mutually_exclusive_with(*other_assumptiion)
550                    })
551            })
552            .map(|(_, conflicting_assumption)| {
553                CoreExtractionResult::ConflictingAssumption(*conflicting_assumption)
554            })
555            .unwrap_or_else(|| {
556                let mut conflict_analysis_context = ConflictAnalysisContext {
557                    assignments: &mut self.assignments,
558                    counters: &mut self.solver_statistics,
559                    solver_state: &mut self.state,
560                    reason_store: &mut self.reason_store,
561                    brancher,
562                    semantic_minimiser: &mut self.semantic_minimiser,
563                    propagators: &mut self.propagators,
564                    notification_engine: &mut self.notification_engine,
565                    propagator_queue: &mut self.propagator_queue,
566                    should_minimise: self.internal_parameters.learning_clause_minimisation,
567                    proof_log: &mut self.internal_parameters.proof_log,
568                    unit_nogood_inference_codes: &self.unit_nogood_inference_codes,
569                    trailed_values: &mut self.trailed_values,
570                    variable_names: &self.variable_names,
571                    rng: &mut self.internal_parameters.random_generator,
572                };
573
574                let mut resolver = ResolutionResolver::with_mode(AnalysisMode::AllDecision);
575                let learned_nogood = resolver
576                    .resolve_conflict(&mut conflict_analysis_context)
577                    .expect("Expected core extraction to be able to extract a core");
578
579                CoreExtractionResult::Core(learned_nogood.predicates.clone())
580            })
581    }
582
583    pub fn get_literal_value(&self, literal: Literal) -> Option<bool> {
584        let literal_is_true = self
585            .assignments
586            .is_predicate_satisfied(literal.get_true_predicate());
587        let opposite_literal_is_true = self
588            .assignments
589            .is_predicate_satisfied((!literal).get_true_predicate());
590
591        pumpkin_assert_moderate!(!(literal_is_true && opposite_literal_is_true));
592
593        // If both the literal is not true and its negation is not true then the literal is
594        // unassigned
595        if !literal_is_true && !opposite_literal_is_true {
596            None
597        } else {
598            Some(literal_is_true)
599        }
600    }
601
602    /// Get the lower bound for the given variable.
603    pub fn get_lower_bound(&self, variable: &impl IntegerVariable) -> i32 {
604        variable.lower_bound(&self.assignments)
605    }
606
607    /// Get the upper bound for the given variable.
608    pub fn get_upper_bound(&self, variable: &impl IntegerVariable) -> i32 {
609        variable.upper_bound(&self.assignments)
610    }
611
612    /// Determine whether `value` is in the domain of `variable`.
613    pub fn integer_variable_contains(&self, variable: &impl IntegerVariable, value: i32) -> bool {
614        variable.contains(&self.assignments, value)
615    }
616
617    /// Get the assigned integer for the given variable. If it is not assigned, `None` is returned.
618    pub fn get_assigned_integer_value(&self, variable: &impl IntegerVariable) -> Option<i32> {
619        let lb = self.get_lower_bound(variable);
620        let ub = self.get_upper_bound(variable);
621
622        if lb == ub {
623            Some(lb)
624        } else {
625            None
626        }
627    }
628
629    pub fn restore_state_at_root(&mut self, brancher: &mut impl Brancher) {
630        if self.assignments.get_decision_level() != 0 {
631            ConstraintSatisfactionSolver::backtrack(
632                &mut self.notification_engine,
633                &mut self.assignments,
634                &mut self.reason_store,
635                &mut self.propagator_queue,
636                &mut self.propagators,
637                0,
638                brancher,
639                &mut self.trailed_values,
640                &mut self.internal_parameters.random_generator,
641            );
642            self.state.declare_ready();
643        } else if self.state.internal_state == CSPSolverStateInternal::ContainsSolution {
644            self.state.declare_ready();
645        }
646    }
647}
648
649// methods that serve as the main building blocks
650impl ConstraintSatisfactionSolver {
651    fn initialise(&mut self, assumptions: &[Predicate]) {
652        pumpkin_assert_simple!(
653            !self.state.is_infeasible_under_assumptions(),
654            "Solver is not expected to be in the infeasible under assumptions state when initialising.
655             Missed extracting the core?"
656        );
657        self.state.declare_solving();
658        assumptions.clone_into(&mut self.assumptions);
659    }
660
661    fn solve_internal(
662        &mut self,
663        termination: &mut impl TerminationCondition,
664        brancher: &mut impl Brancher,
665    ) -> CSPSolverExecutionFlag {
666        loop {
667            if termination.should_stop() {
668                self.state.declare_timeout();
669                return CSPSolverExecutionFlag::Timeout;
670            }
671
672            self.propagate();
673
674            if self.state.no_conflict() {
675                // Restarts should only occur after a new decision level has been declared to
676                // account for the fact that all assumptions should be assigned when restarts take
677                // place. Since one assumption is posted per decision level, all assumptions are
678                // assigned when the decision level is strictly larger than the number of
679                // assumptions.
680                if self.get_decision_level() > self.assumptions.len()
681                    && self.restart_strategy.should_restart()
682                {
683                    self.restart_during_search(brancher);
684                }
685
686                let branching_result = self.make_next_decision(brancher);
687
688                self.solver_statistics.engine_statistics.peak_depth = max(
689                    self.solver_statistics.engine_statistics.peak_depth,
690                    self.assignments.get_decision_level() as u64,
691                );
692
693                match branching_result {
694                    Err(CSPSolverExecutionFlag::Infeasible) => {
695                        // Can happen when the branching decision was an assumption
696                        // that is inconsistent with the current assignment. We do not
697                        // have to declare a new state, as it will be done inside the
698                        // `make_next_decision` function.
699                        pumpkin_assert_simple!(self.state.is_infeasible_under_assumptions());
700
701                        self.complete_proof();
702                        return CSPSolverExecutionFlag::Infeasible;
703                    }
704
705                    Err(flag) => return flag,
706                    Ok(()) => {}
707                }
708
709                if let Err(flag) = branching_result {
710                    return flag;
711                }
712            } else {
713                if self.get_decision_level() == 0 {
714                    self.complete_proof();
715                    self.state.declare_infeasible();
716
717                    return CSPSolverExecutionFlag::Infeasible;
718                }
719
720                self.resolve_conflict_with_nogood(brancher);
721
722                brancher.on_conflict();
723                self.decay_nogood_activities();
724            }
725        }
726    }
727
728    fn decay_nogood_activities(&mut self) {
729        match self
730            .propagators
731            .get_propagator_mut(self.nogood_propagator_handle)
732        {
733            Some(nogood_propagator) => {
734                nogood_propagator.decay_nogood_activities();
735            }
736            None => panic!("Provided propagator should be the nogood propagator"),
737        }
738    }
739
740    fn make_next_decision(
741        &mut self,
742        brancher: &mut impl Brancher,
743    ) -> Result<(), CSPSolverExecutionFlag> {
744        // Set the next decision to be an assumption, if there are assumptions left.
745        // Currently assumptions are implemented by adding an assumption predicate
746        // at separate decision levels.
747        if let Some(assumption_literal) = self.peek_next_assumption_predicate() {
748            self.declare_new_decision_level();
749
750            let _ = self
751                .assignments
752                .post_predicate(assumption_literal, None, &mut self.notification_engine)
753                .map_err(|_| {
754                    self.state
755                        .declare_infeasible_under_assumptions(assumption_literal);
756                    CSPSolverExecutionFlag::Infeasible
757                })?;
758
759            return Ok(());
760        }
761
762        // Otherwise proceed with standard branching.
763        let context = &mut SelectionContext::new(
764            &self.assignments,
765            &mut self.internal_parameters.random_generator,
766        );
767
768        // If there is a next decision, make the decision.
769        let Some(decision_predicate) = brancher.next_decision(context) else {
770            // Otherwise there are no more decisions to be made,
771            // all predicates have been applied without a conflict,
772            // meaning the problem is feasible.
773            self.state.declare_solution_found();
774            return Err(CSPSolverExecutionFlag::Feasible);
775        };
776
777        self.declare_new_decision_level();
778
779        // Note: This also checks that the decision predicate is not already true. That is a
780        // stronger check than the `.expect(...)` used later on when handling the result of
781        // `Assignments::post_predicate`.
782        pumpkin_assert_moderate!(
783            !self.assignments.is_predicate_satisfied(decision_predicate),
784            "Decision should not already be assigned; double check the brancher"
785        );
786
787        self.solver_statistics.engine_statistics.num_decisions += 1;
788        let update_occurred = self
789            .assignments
790            .post_predicate(decision_predicate, None, &mut self.notification_engine)
791            .expect("Decisions are expected not to fail.");
792        pumpkin_assert_simple!(update_occurred);
793
794        Ok(())
795    }
796
797    pub(crate) fn declare_new_decision_level(&mut self) {
798        self.assignments.increase_decision_level();
799        self.notification_engine.increase_decision_level();
800        self.trailed_values.increase_decision_level();
801        self.reason_store.increase_decision_level();
802    }
803
804    /// Changes the state based on the conflict analysis. It performs the following:
805    /// - Derives a nogood using our CP version of the 1UIP scheme.
806    /// - Adds the learned nogood to the database.
807    /// - Performs backtracking.
808    /// - Enqueues the propagated [`Predicate`] of the learned nogood.
809    /// - Todo: Updates the internal data structures (e.g. for the restart strategy or the learned
810    ///   clause manager)
811    ///
812    /// # Note
813    /// This method performs no propagation, this is left up to the solver afterwards.
814    fn resolve_conflict_with_nogood(&mut self, brancher: &mut impl Brancher) {
815        pumpkin_assert_moderate!(self.state.is_conflicting());
816
817        let current_decision_level = self.get_decision_level();
818
819        let mut conflict_analysis_context = ConflictAnalysisContext {
820            assignments: &mut self.assignments,
821            counters: &mut self.solver_statistics,
822            solver_state: &mut self.state,
823            reason_store: &mut self.reason_store,
824            brancher,
825            semantic_minimiser: &mut self.semantic_minimiser,
826            notification_engine: &mut self.notification_engine,
827            propagators: &mut self.propagators,
828            propagator_queue: &mut self.propagator_queue,
829            should_minimise: self.internal_parameters.learning_clause_minimisation,
830            proof_log: &mut self.internal_parameters.proof_log,
831            unit_nogood_inference_codes: &self.unit_nogood_inference_codes,
832            trailed_values: &mut self.trailed_values,
833            variable_names: &self.variable_names,
834            rng: &mut self.internal_parameters.random_generator,
835        };
836
837        let learned_nogood = self
838            .conflict_resolver
839            .resolve_conflict(&mut conflict_analysis_context);
840
841        // important to notify about the conflict _before_ backtracking removes literals from
842        // the trail -> although in the current version this does nothing but notify that a
843        // conflict happened
844        if let Some(learned_nogood) = learned_nogood.as_ref() {
845            conflict_analysis_context
846                .counters
847                .learned_clause_statistics
848                .average_backtrack_amount
849                .add_term((current_decision_level - learned_nogood.backjump_level) as u64);
850
851            conflict_analysis_context
852                .counters
853                .engine_statistics
854                .sum_of_backjumps +=
855                current_decision_level as u64 - 1 - learned_nogood.backjump_level as u64;
856            if current_decision_level - learned_nogood.backjump_level > 1 {
857                conflict_analysis_context
858                    .counters
859                    .engine_statistics
860                    .num_backjumps += 1;
861            }
862
863            self.restart_strategy.notify_conflict(
864                self.lbd_helper.compute_lbd(
865                    &learned_nogood.predicates,
866                    conflict_analysis_context.assignments,
867                ),
868                conflict_analysis_context
869                    .assignments
870                    .get_pruned_value_count(),
871            );
872        }
873
874        let result = self
875            .conflict_resolver
876            .process(&mut conflict_analysis_context, &learned_nogood);
877        if result.is_err() {
878            unreachable!("Cannot resolve nogood and reach error")
879        }
880
881        if let Some(learned_nogood) = learned_nogood {
882            let constraint_tag = self
883                .internal_parameters
884                .proof_log
885                .log_deduction(
886                    learned_nogood.predicates.iter().copied(),
887                    &self.variable_names,
888                )
889                .expect("Failed to write proof log");
890
891            let inference_code = self
892                .internal_parameters
893                .proof_log
894                .create_inference_code(constraint_tag, NogoodLabel);
895
896            if learned_nogood.predicates.len() == 1 {
897                let _ = self
898                    .unit_nogood_inference_codes
899                    .insert(!learned_nogood.predicates[0], inference_code);
900            }
901
902            self.solver_statistics
903                .learned_clause_statistics
904                .num_unit_nogoods_learned += (learned_nogood.predicates.len() == 1) as u64;
905
906            self.solver_statistics
907                .learned_clause_statistics
908                .average_learned_nogood_length
909                .add_term(learned_nogood.predicates.len() as u64);
910
911            self.add_learned_nogood(learned_nogood, inference_code);
912        }
913
914        self.state.declare_solving();
915    }
916
917    fn add_learned_nogood(&mut self, learned_nogood: LearnedNogood, inference_code: InferenceCode) {
918        let mut context = PropagationContextMut::new(
919            &mut self.trailed_values,
920            &mut self.assignments,
921            &mut self.reason_store,
922            &mut self.semantic_minimiser,
923            &mut self.notification_engine,
924            self.nogood_propagator_handle.propagator_id(),
925        );
926
927        let nogood_propagator = self
928            .propagators
929            .get_propagator_mut(self.nogood_propagator_handle)
930            .expect("nogood propagator handle should refer to nogood propagator");
931
932        nogood_propagator.add_asserting_nogood(
933            learned_nogood.predicates,
934            inference_code,
935            &mut context,
936            &mut self.solver_statistics,
937        );
938    }
939
940    /// Performs a restart during the search process; it is only called when it has been determined
941    /// to be necessary by the [`ConstraintSatisfactionSolver::restart_strategy`]. A 'restart'
942    /// differs from backtracking to level zero in that a restart backtracks to decision level
943    /// zero and then performs additional operations, e.g., clean up learned clauses, adjust
944    /// restart frequency, etc.
945    ///
946    /// This method will also increase the decision level after backtracking.
947    ///
948    /// Returns true if a restart took place and false otherwise.
949    fn restart_during_search(&mut self, brancher: &mut impl Brancher) {
950        pumpkin_assert_simple!(
951            self.get_decision_level() > self.assumptions.len(),
952            "Sanity check: restarts should not trigger whilst assigning assumptions"
953        );
954
955        // no point backtracking past the assumption level
956        if self.get_decision_level() <= self.assumptions.len() {
957            return;
958        }
959
960        if brancher.is_restart_pointless() {
961            // If the brancher is static then there is no point in restarting as it would make the
962            // exact same decision
963            return;
964        }
965
966        self.solver_statistics.engine_statistics.num_restarts += 1;
967
968        ConstraintSatisfactionSolver::backtrack(
969            &mut self.notification_engine,
970            &mut self.assignments,
971            &mut self.reason_store,
972            &mut self.propagator_queue,
973            &mut self.propagators,
974            0,
975            brancher,
976            &mut self.trailed_values,
977            &mut self.internal_parameters.random_generator,
978        );
979
980        self.restart_strategy.notify_restart();
981    }
982
983    #[allow(
984        clippy::too_many_arguments,
985        reason = "This method requires this many arguments, though a backtracking context could be considered; for now this function needs to be used by conflict analysis"
986    )]
987    pub(crate) fn backtrack<BrancherType: Brancher + ?Sized>(
988        notification_engine: &mut NotificationEngine,
989        assignments: &mut Assignments,
990        reason_store: &mut ReasonStore,
991        propagator_queue: &mut PropagatorQueue,
992        propagators: &mut PropagatorStore,
993        backtrack_level: usize,
994        brancher: &mut BrancherType,
995        trailed_values: &mut TrailedValues,
996        rng: &mut dyn Random,
997    ) {
998        pumpkin_assert_simple!(backtrack_level < assignments.get_decision_level());
999
1000        brancher.on_backtrack();
1001
1002        assignments
1003            .synchronise(backtrack_level, notification_engine)
1004            .iter()
1005            .for_each(|(domain_id, previous_value)| {
1006                brancher.on_unassign_integer(*domain_id, *previous_value)
1007            });
1008
1009        trailed_values.synchronise(backtrack_level);
1010
1011        reason_store.synchronise(backtrack_level);
1012        propagator_queue.clear();
1013        // For now all propagators are called to synchronise, in the future this will be improved in
1014        // two ways:
1015        //      + allow incremental synchronisation
1016        //      + only call the subset of propagators that were notified since last backtrack
1017        for propagator in propagators.iter_propagators_mut() {
1018            let context = PropagationContext::new(assignments);
1019            propagator.synchronise(context);
1020        }
1021
1022        brancher.synchronise(&mut SelectionContext::new(assignments, rng));
1023
1024        let _ = notification_engine.process_backtrack_events(assignments, propagators);
1025        notification_engine.clear_event_drain();
1026
1027        notification_engine.update_last_notified_index(assignments);
1028        // Should be done after the assignments and trailed values have been synchronised
1029        notification_engine.synchronise(backtrack_level, assignments, trailed_values);
1030    }
1031
1032    pub(crate) fn compute_reason_for_empty_domain(&mut self) -> PropositionalConjunction {
1033        // The empty domain happened after posting the last predicate on the trail.
1034        // The reason for this empty domain is computed as the reason for the bounds before the last
1035        // trail predicate was posted, plus the reason for the last trail predicate.
1036
1037        // The last predicate on the trail reveals the domain id that has resulted
1038        // in an empty domain.
1039        let entry = self.assignments.get_last_entry_on_trail();
1040        let (entry_reason, entry_inference_code) = entry
1041            .reason
1042            .expect("Cannot cause an empty domain using a decision.");
1043        let conflict_domain = entry.predicate.get_domain();
1044        assert!(
1045            entry.old_lower_bound != self.assignments.get_lower_bound(conflict_domain)
1046                || entry.old_upper_bound != self.assignments.get_upper_bound(conflict_domain),
1047            "One of the two bounds had to change."
1048        );
1049
1050        // Look up the reason for the bound that changed.
1051        // The reason for changing the bound cannot be a decision, so we can safely unwrap.
1052        let mut empty_domain_reason: Vec<Predicate> = vec![];
1053        let _ = self.reason_store.get_or_compute(
1054            entry_reason,
1055            ExplanationContext::without_working_nogood(
1056                &self.assignments,
1057                self.assignments.num_trail_entries() - 1,
1058                &mut self.notification_engine,
1059            ),
1060            &mut self.propagators,
1061            &mut empty_domain_reason,
1062        );
1063
1064        // We also need to log this last propagation to the proof log as an inference.
1065        let _ = self.internal_parameters.proof_log.log_inference(
1066            entry_inference_code,
1067            empty_domain_reason.iter().copied(),
1068            Some(entry.predicate),
1069            &self.variable_names,
1070        );
1071
1072        // Now we get the explanation for the bound which was conflicting with the last trail entry
1073        match entry.predicate.get_predicate_type() {
1074            PredicateType::LowerBound => {
1075                // The last trail entry was a lower-bound propagation meaning that the empty domain
1076                // was caused by the upper-bound
1077                //
1078                // We lift so that it is the most general upper-bound possible while still causing
1079                // the empty domain
1080                empty_domain_reason.push(predicate!(
1081                    conflict_domain <= entry.predicate.get_right_hand_side() - 1
1082                ));
1083            }
1084            PredicateType::UpperBound => {
1085                // The last trail entry was an upper-bound propagation meaning that the empty domain
1086                // was caused by the lower-bound
1087                //
1088                // We lift so that it is the most general lower-bound possible while still causing
1089                // the empty domain
1090                empty_domain_reason.push(predicate!(
1091                    conflict_domain >= entry.predicate.get_right_hand_side() + 1
1092                ));
1093            }
1094            PredicateType::NotEqual => {
1095                // The last trail entry was a not equals propagation meaning that the empty domain
1096                // was due to the domain being assigned to the removed value
1097                pumpkin_assert_eq_simple!(entry.old_upper_bound, entry.old_lower_bound);
1098
1099                empty_domain_reason.push(predicate!(conflict_domain == entry.old_lower_bound));
1100            }
1101            PredicateType::Equal => {
1102                // The last trail entry was an equality propagation; we split into three cases.
1103                if entry.predicate.get_right_hand_side() < entry.old_lower_bound {
1104                    // 1) The assigned value was lower than the lower-bound
1105                    //
1106                    // We lift so that it is the most general lower-bound possible while still
1107                    // causing the empty domain
1108                    empty_domain_reason.push(predicate!(
1109                        conflict_domain >= entry.predicate.get_right_hand_side() + 1
1110                    ));
1111                } else if entry.predicate.get_right_hand_side() > entry.old_upper_bound {
1112                    // 2) The assigned value was larger than the upper-bound
1113                    //
1114                    // We lift so that it is the most general upper-bound possible while still
1115                    // causing the empty domain
1116                    empty_domain_reason.push(predicate!(
1117                        conflict_domain <= entry.predicate.get_right_hand_side() - 1
1118                    ));
1119                } else {
1120                    // 3) The assigned value was equal to a hole in the domain
1121                    empty_domain_reason.push(predicate!(
1122                        conflict_domain != entry.predicate.get_right_hand_side()
1123                    ))
1124                }
1125            }
1126        }
1127
1128        empty_domain_reason.into()
1129    }
1130
1131    /// Main propagation loop.
1132    pub(crate) fn propagate(&mut self) {
1133        // Record the number of predicates on the trail for statistics purposes.
1134        let num_assigned_variables_old = self.assignments.num_trail_entries();
1135        // The initial domain events are due to the decision predicate.
1136        self.notification_engine
1137            .notify_propagators_about_domain_events(
1138                &mut self.assignments,
1139                &mut self.trailed_values,
1140                &mut self.propagators,
1141                self.nogood_propagator_handle,
1142                &mut self.propagator_queue,
1143            );
1144        // Keep propagating until there are unprocessed propagators, or a conflict is detected.
1145        while let Some(propagator_id) = self.propagator_queue.pop() {
1146            self.solver_statistics
1147                .engine_statistics
1148                .num_propagators_called += 1;
1149
1150            let num_trail_entries_before = self.assignments.num_trail_entries();
1151
1152            let propagation_status = {
1153                let propagator = &mut self.propagators[propagator_id];
1154                let context = PropagationContextMut::new(
1155                    &mut self.trailed_values,
1156                    &mut self.assignments,
1157                    &mut self.reason_store,
1158                    &mut self.semantic_minimiser,
1159                    &mut self.notification_engine,
1160                    propagator_id,
1161                );
1162                propagator.propagate(context)
1163            };
1164
1165            if self.assignments.get_decision_level() == 0 {
1166                self.handle_root_propagation(num_trail_entries_before);
1167            }
1168            match propagation_status {
1169                Ok(_) => {
1170                    // Notify other propagators of the propagations and continue.
1171                    self.notification_engine
1172                        .notify_propagators_about_domain_events(
1173                            &mut self.assignments,
1174                            &mut self.trailed_values,
1175                            &mut self.propagators,
1176                            self.nogood_propagator_handle,
1177                            &mut self.propagator_queue,
1178                        );
1179                }
1180                Err(inconsistency) => match inconsistency {
1181                    // A propagator did a change that resulted in an empty domain.
1182                    Inconsistency::EmptyDomain => {
1183                        self.prepare_for_conflict_resolution();
1184                        break;
1185                    }
1186                    // A propagator-specific reason for the current conflict.
1187                    Inconsistency::Conflict(conflict) => {
1188                        pumpkin_assert_advanced!(DebugHelper::debug_reported_failure(
1189                            &self.trailed_values,
1190                            &self.assignments,
1191                            &conflict.conjunction,
1192                            &self.propagators[propagator_id],
1193                            propagator_id,
1194                            &self.notification_engine
1195                        ));
1196
1197                        let stored_conflict_info = StoredConflictInfo::Propagator(conflict);
1198                        self.state.declare_conflict(stored_conflict_info);
1199                        break;
1200                    }
1201                },
1202            }
1203            pumpkin_assert_extreme!(
1204                DebugHelper::debug_check_propagations(
1205                    num_trail_entries_before,
1206                    propagator_id,
1207                    &self.trailed_values,
1208                    &self.assignments,
1209                    &mut self.reason_store,
1210                    &mut self.propagators,
1211                    &self.notification_engine
1212                ),
1213                "Checking the propagations performed by the propagator led to inconsistencies!"
1214            );
1215        }
1216        // Record statistics.
1217        self.solver_statistics.engine_statistics.num_conflicts +=
1218            self.state.is_conflicting() as u64;
1219        self.solver_statistics.engine_statistics.num_propagations +=
1220            self.assignments.num_trail_entries() as u64 - num_assigned_variables_old as u64;
1221        // Only check fixed point propagation if there was no reported conflict,
1222        // since otherwise the state may be inconsistent.
1223        pumpkin_assert_extreme!(
1224            self.state.is_conflicting()
1225                || DebugHelper::debug_fixed_point_propagation(
1226                    &self.trailed_values,
1227                    &self.assignments,
1228                    &self.propagators,
1229                    &self.notification_engine
1230                )
1231        );
1232    }
1233
1234    /// Introduces any root-level propagations to the proof by introducing them as
1235    /// nogoods.
1236    ///
1237    /// The inference `R -> l` is logged to the proof as follows:
1238    /// 1. Infernce `R /\ ~l -> false`
1239    /// 2. Nogood (clause) `l`
1240    fn handle_root_propagation(&mut self, start_trail_index: usize) {
1241        pumpkin_assert_eq_simple!(self.get_decision_level(), 0);
1242
1243        for trail_idx in start_trail_index..self.assignments.num_trail_entries() {
1244            let entry = self.assignments.get_trail_entry(trail_idx);
1245            let (reason_ref, inference_code) = entry
1246                .reason
1247                .expect("Added by a propagator and must therefore have a reason");
1248
1249            if !self.internal_parameters.proof_log.is_logging_inferences() {
1250                // In case we are not logging inferences, we only need to keep track
1251                // of the root-level inferences to allow us to correctly finalize the
1252                // proof.
1253                let _ = self
1254                    .unit_nogood_inference_codes
1255                    .insert(entry.predicate, inference_code);
1256                continue;
1257            }
1258
1259            // Get the conjunction of predicates explaining the propagation.
1260            let mut reason = vec![];
1261            let _ = self.reason_store.get_or_compute(
1262                reason_ref,
1263                ExplanationContext::without_working_nogood(
1264                    &self.assignments,
1265                    trail_idx,
1266                    &mut self.notification_engine,
1267                ),
1268                &mut self.propagators,
1269                &mut reason,
1270            );
1271
1272            let propagated = entry.predicate;
1273
1274            // The proof inference for the propagation `R -> l` is `R /\ ~l -> false`.
1275            let inference_premises = reason.iter().copied().chain(std::iter::once(!propagated));
1276            let _ = self.internal_parameters.proof_log.log_inference(
1277                inference_code,
1278                inference_premises,
1279                None,
1280                &self.variable_names,
1281            );
1282
1283            // Since inference steps are only related to the nogood they directly precede,
1284            // facts derived at the root are also logged as nogoods so they can be used in the
1285            // derivation of other nogoods.
1286            //
1287            // In case we are logging hints, we must therefore identify what proof steps contribute
1288            // to the derivation of the current nogood, and therefore are in the premise of the
1289            // previously logged inference. These proof steps are necessarily unit nogoods, and
1290            // therefore we recursively look up which unit nogoods are involved in the premise of
1291            // the inference.
1292
1293            let mut to_explain: VecDeque<Predicate> = reason.iter().copied().collect();
1294
1295            while let Some(premise) = to_explain.pop_front() {
1296                pumpkin_assert_simple!(self.assignments.is_predicate_satisfied(premise));
1297
1298                let mut context = RootExplanationContext {
1299                    propagators: &mut self.propagators,
1300                    proof_log: &mut self.internal_parameters.proof_log,
1301                    unit_nogood_inference_codes: &self.unit_nogood_inference_codes,
1302                    assignments: &self.assignments,
1303                    reason_store: &mut self.reason_store,
1304                    notification_engine: &mut self.notification_engine,
1305                    variable_names: &self.variable_names,
1306                };
1307
1308                explain_root_assignment(&mut context, premise);
1309            }
1310
1311            // Log the nogood which adds the root-level knowledge to the proof.
1312            let constraint_tag = self
1313                .internal_parameters
1314                .proof_log
1315                .log_deduction([!propagated], &self.variable_names);
1316
1317            if let Ok(constraint_tag) = constraint_tag {
1318                let inference_code = self
1319                    .internal_parameters
1320                    .proof_log
1321                    .create_inference_code(constraint_tag, NogoodLabel);
1322
1323                let _ = self
1324                    .unit_nogood_inference_codes
1325                    .insert(propagated, inference_code);
1326            }
1327        }
1328    }
1329
1330    fn peek_next_assumption_predicate(&self) -> Option<Predicate> {
1331        // The convention is that at decision level i, the (i-1)th assumption is posted.
1332        // Note that decisions start being posted start at 1, hence the minus one.
1333        let next_assumption_index = self.get_decision_level();
1334        self.assumptions.get(next_assumption_index).copied()
1335    }
1336}
1337
1338// methods for adding constraints (propagators and clauses)
1339impl ConstraintSatisfactionSolver {
1340    /// See [`crate::Solver::add_propagator`] for documentation.
1341    pub(crate) fn add_propagator<Constructor>(
1342        &mut self,
1343        constructor: Constructor,
1344    ) -> Result<PropagatorHandle<Constructor::PropagatorImpl>, ConstraintOperationError>
1345    where
1346        Constructor: PropagatorConstructor,
1347        Constructor::PropagatorImpl: 'static,
1348    {
1349        if self.state.is_inconsistent() {
1350            return Err(ConstraintOperationError::InfeasiblePropagator);
1351        }
1352
1353        let propagator_slot = self.propagators.new_propagator();
1354
1355        let constructor_context = PropagatorConstructorContext::new(
1356            &mut self.notification_engine,
1357            &mut self.trailed_values,
1358            &mut self.internal_parameters.proof_log,
1359            propagator_slot.key().propagator_id(),
1360            &mut self.assignments,
1361        );
1362
1363        let propagator = constructor.create(constructor_context);
1364
1365        pumpkin_assert_simple!(
1366            propagator.priority() <= 3,
1367            "The propagator priority exceeds 3.
1368             Currently we only support values up to 3,
1369             but this can easily be changed if there is a good reason."
1370        );
1371        let new_propagator_id = propagator_slot.populate(propagator);
1372
1373        let new_propagator = &mut self.propagators[new_propagator_id.propagator_id()];
1374
1375        self.propagator_queue
1376            .enqueue_propagator(new_propagator_id.propagator_id(), new_propagator.priority());
1377
1378        self.propagate();
1379
1380        if self.state.no_conflict() {
1381            Ok(new_propagator_id)
1382        } else {
1383            self.complete_proof();
1384            let _ = self.conclude_proof_unsat();
1385            Err(ConstraintOperationError::InfeasiblePropagator)
1386        }
1387    }
1388
1389    pub fn post_predicate(&mut self, predicate: Predicate) -> Result<(), ConstraintOperationError> {
1390        assert!(
1391            self.get_decision_level() == 0,
1392            "Can only post predicates at the root level."
1393        );
1394
1395        if self.state.is_infeasible() {
1396            Err(ConstraintOperationError::InfeasibleState)
1397        } else {
1398            match self
1399                .assignments
1400                .post_predicate(predicate, None, &mut self.notification_engine)
1401            {
1402                Ok(_) => Ok(()),
1403                Err(_) => Err(ConstraintOperationError::InfeasibleNogood),
1404            }
1405        }
1406    }
1407
1408    fn add_nogood(
1409        &mut self,
1410        nogood: Vec<Predicate>,
1411        inference_code: InferenceCode,
1412    ) -> Result<(), ConstraintOperationError> {
1413        pumpkin_assert_eq_simple!(self.get_decision_level(), 0);
1414        let num_trail_entries = self.assignments.num_trail_entries();
1415
1416        let mut propagation_context = PropagationContextMut::new(
1417            &mut self.trailed_values,
1418            &mut self.assignments,
1419            &mut self.reason_store,
1420            &mut self.semantic_minimiser,
1421            &mut self.notification_engine,
1422            self.nogood_propagator_handle.propagator_id(),
1423        );
1424
1425        let nogood_propagator = self
1426            .propagators
1427            .get_propagator_mut(self.nogood_propagator_handle)
1428            .expect("nogood propagator handle should refer to nogood propagator");
1429
1430        let propagation_status =
1431            nogood_propagator.add_nogood(nogood, inference_code, &mut propagation_context);
1432
1433        if propagation_status.is_err() || self.state.is_conflicting() {
1434            self.prepare_for_conflict_resolution();
1435            self.handle_root_propagation(num_trail_entries);
1436            self.complete_proof();
1437            return Err(ConstraintOperationError::InfeasibleNogood);
1438        }
1439
1440        self.handle_root_propagation(num_trail_entries);
1441
1442        // temporary hack for the nogood propagator that does propagation from scratch
1443        self.propagator_queue.enqueue_propagator(PropagatorId(0), 0);
1444        self.propagate();
1445
1446        self.handle_root_propagation(num_trail_entries);
1447
1448        if self.state.is_infeasible() {
1449            self.prepare_for_conflict_resolution();
1450            self.complete_proof();
1451            Err(ConstraintOperationError::InfeasibleState)
1452        } else {
1453            Ok(())
1454        }
1455    }
1456
1457    fn prepare_for_conflict_resolution(&mut self) {
1458        if self.state.is_conflicting() {
1459            return;
1460        }
1461
1462        let empty_domain_reason = self.compute_reason_for_empty_domain();
1463
1464        // TODO: As a temporary solution, we remove the last trail element.
1465        // This way we guarantee that the assignment is consistent, which is needed
1466        // for the conflict analysis data structures. The proper alternative would
1467        // be to forbid the assignments from getting into an inconsistent state.
1468        self.assignments.remove_last_trail_element();
1469
1470        let stored_conflict_info = StoredConflictInfo::EmptyDomain {
1471            conflict_nogood: empty_domain_reason,
1472        };
1473        self.state.declare_conflict(stored_conflict_info);
1474    }
1475
1476    /// Creates a clause from `literals` and adds it to the current formula.
1477    ///
1478    /// If the formula becomes trivially unsatisfiable, a [`ConstraintOperationError`] will be
1479    /// returned. Subsequent calls to this m\Zethod will always return an error, and no
1480    /// modification of the solver will take place.
1481    pub fn add_clause(
1482        &mut self,
1483        predicates: impl IntoIterator<Item = Predicate>,
1484        constraint_tag: ConstraintTag,
1485    ) -> Result<(), ConstraintOperationError> {
1486        pumpkin_assert_simple!(
1487            self.get_decision_level() == 0,
1488            "Clauses can only be added in the root"
1489        );
1490
1491        if self.state.is_inconsistent() {
1492            return Err(ConstraintOperationError::InfeasiblePropagator);
1493        }
1494
1495        // We can simply negate the clause and retrieve a nogood, e.g. if we have the
1496        // clause `[x1 >= 5] \/ [x2 != 3] \/ [x3 <= 5]`, then it **cannot** be the case that `[x1 <
1497        // 5] /\ [x2 = 3] /\ [x3 > 5]`
1498
1499        let mut are_all_falsified_at_root = true;
1500        let predicates = predicates
1501            .into_iter()
1502            .map(|predicate| {
1503                are_all_falsified_at_root &= self.assignments.is_predicate_falsified(predicate);
1504                !predicate
1505            })
1506            .collect::<Vec<_>>();
1507
1508        if predicates.is_empty() {
1509            // This breaks the proof. If it occurs, we should fix up the proof logging.
1510            // The main issue is that nogoods are not tagged. In the proof that is problematic.
1511            self.state
1512                .declare_conflict(StoredConflictInfo::RootLevelConflict(
1513                    ConstraintOperationError::InfeasibleClause,
1514                ));
1515            return Err(ConstraintOperationError::InfeasibleClause);
1516        }
1517
1518        if are_all_falsified_at_root {
1519            finalize_proof(FinalizingContext {
1520                conflict: predicates.into(),
1521                propagators: &mut self.propagators,
1522                proof_log: &mut self.internal_parameters.proof_log,
1523                unit_nogood_inference_codes: &self.unit_nogood_inference_codes,
1524                assignments: &self.assignments,
1525                reason_store: &mut self.reason_store,
1526                notification_engine: &mut self.notification_engine,
1527                variable_names: &self.variable_names,
1528            });
1529            self.state
1530                .declare_conflict(StoredConflictInfo::RootLevelConflict(
1531                    ConstraintOperationError::InfeasibleClause,
1532                ));
1533            return Err(ConstraintOperationError::InfeasibleClause);
1534        }
1535
1536        let inference_code = self
1537            .internal_parameters
1538            .proof_log
1539            .create_inference_code(constraint_tag, NogoodLabel);
1540        if let Err(constraint_operation_error) = self.add_nogood(predicates, inference_code) {
1541            let _ = self.conclude_proof_unsat();
1542
1543            self.state
1544                .declare_conflict(StoredConflictInfo::RootLevelConflict(
1545                    constraint_operation_error,
1546                ));
1547            return Err(constraint_operation_error);
1548        }
1549        Ok(())
1550    }
1551
1552    pub(crate) fn get_decision_level(&self) -> usize {
1553        self.assignments.get_decision_level()
1554    }
1555}
1556
1557#[derive(Default, Debug, PartialEq, Eq)]
1558enum CSPSolverStateInternal {
1559    #[default]
1560    Ready,
1561    Solving,
1562    ContainsSolution,
1563    Conflict {
1564        conflict_info: StoredConflictInfo,
1565    },
1566    Infeasible,
1567    InfeasibleUnderAssumptions {
1568        violated_assumption: Predicate,
1569    },
1570    Timeout,
1571}
1572
1573#[derive(Default, Debug)]
1574pub struct CSPSolverState {
1575    internal_state: CSPSolverStateInternal,
1576}
1577
1578impl CSPSolverState {
1579    pub fn is_ready(&self) -> bool {
1580        matches!(self.internal_state, CSPSolverStateInternal::Ready)
1581    }
1582
1583    pub fn no_conflict(&self) -> bool {
1584        !self.is_conflicting()
1585    }
1586
1587    pub fn is_conflicting(&self) -> bool {
1588        matches!(
1589            self.internal_state,
1590            CSPSolverStateInternal::Conflict { conflict_info: _ }
1591        )
1592    }
1593
1594    pub fn is_infeasible(&self) -> bool {
1595        matches!(self.internal_state, CSPSolverStateInternal::Infeasible)
1596    }
1597
1598    /// Determines whether the current state is inconsistent; i.e. whether it is conflicting,
1599    /// infeasible or infeasible under assumptions
1600    pub fn is_inconsistent(&self) -> bool {
1601        self.is_conflicting() || self.is_infeasible() || self.is_infeasible_under_assumptions()
1602    }
1603
1604    pub fn is_infeasible_under_assumptions(&self) -> bool {
1605        matches!(
1606            self.internal_state,
1607            CSPSolverStateInternal::InfeasibleUnderAssumptions {
1608                violated_assumption: _
1609            }
1610        )
1611    }
1612
1613    pub fn get_violated_assumption(&self) -> Predicate {
1614        if let CSPSolverStateInternal::InfeasibleUnderAssumptions {
1615            violated_assumption,
1616        } = self.internal_state
1617        {
1618            violated_assumption
1619        } else {
1620            panic!(
1621                "Cannot extract violated assumption without getting the solver into the infeasible
1622                 under assumptions state."
1623            );
1624        }
1625    }
1626
1627    pub(crate) fn get_conflict_info(&self) -> StoredConflictInfo {
1628        match &self.internal_state {
1629            CSPSolverStateInternal::Conflict { conflict_info } => conflict_info.clone(),
1630            CSPSolverStateInternal::InfeasibleUnderAssumptions {
1631                violated_assumption,
1632            } => StoredConflictInfo::EmptyDomain {
1633                conflict_nogood: vec![*violated_assumption, !(*violated_assumption)].into(),
1634            },
1635            _ => {
1636                panic!("Cannot extract conflict clause if solver is not in a conflict.");
1637            }
1638        }
1639    }
1640
1641    pub fn timeout(&self) -> bool {
1642        matches!(self.internal_state, CSPSolverStateInternal::Timeout)
1643    }
1644
1645    pub fn has_solution(&self) -> bool {
1646        matches!(
1647            self.internal_state,
1648            CSPSolverStateInternal::ContainsSolution
1649        )
1650    }
1651
1652    pub(crate) fn declare_ready(&mut self) {
1653        self.internal_state = CSPSolverStateInternal::Ready;
1654    }
1655
1656    pub fn declare_solving(&mut self) {
1657        pumpkin_assert_simple!((self.is_ready() || self.is_conflicting()) && !self.is_infeasible());
1658        self.internal_state = CSPSolverStateInternal::Solving;
1659    }
1660
1661    fn declare_infeasible(&mut self) {
1662        self.internal_state = CSPSolverStateInternal::Infeasible;
1663    }
1664
1665    fn declare_conflict(&mut self, conflict_info: StoredConflictInfo) {
1666        self.internal_state = CSPSolverStateInternal::Conflict { conflict_info };
1667    }
1668
1669    fn declare_solution_found(&mut self) {
1670        pumpkin_assert_simple!(!self.is_infeasible());
1671        self.internal_state = CSPSolverStateInternal::ContainsSolution;
1672    }
1673
1674    fn declare_timeout(&mut self) {
1675        pumpkin_assert_simple!(!self.is_infeasible());
1676        self.internal_state = CSPSolverStateInternal::Timeout;
1677    }
1678
1679    fn declare_infeasible_under_assumptions(&mut self, violated_assumption: Predicate) {
1680        pumpkin_assert_simple!(!self.is_infeasible());
1681        self.internal_state = CSPSolverStateInternal::InfeasibleUnderAssumptions {
1682            violated_assumption,
1683        }
1684    }
1685}
1686
1687declare_inference_label!(NogoodLabel, "nogood");
1688
1689#[cfg(test)]
1690mod tests {
1691    use super::ConstraintSatisfactionSolver;
1692    use super::CoreExtractionResult;
1693    use crate::basic_types::CSPSolverExecutionFlag;
1694    use crate::predicate;
1695    use crate::predicates::Predicate;
1696    use crate::propagators::linear_not_equal::LinearNotEqualPropagatorArgs;
1697    use crate::termination::Indefinite;
1698    use crate::variables::TransformableVariable;
1699    use crate::DefaultBrancher;
1700
1701    fn is_same_core(core1: &[Predicate], core2: &[Predicate]) -> bool {
1702        core1.len() == core2.len() && core2.iter().all(|lit| core1.contains(lit))
1703    }
1704
1705    fn is_result_the_same(res1: &CoreExtractionResult, res2: &CoreExtractionResult) -> bool {
1706        match (res1, res2) {
1707            (
1708                CoreExtractionResult::ConflictingAssumption(assumption1),
1709                CoreExtractionResult::ConflictingAssumption(assumption2),
1710            ) => assumption1 == assumption2,
1711            (CoreExtractionResult::Core(core1), CoreExtractionResult::Core(core2)) => {
1712                is_same_core(core1, core2)
1713            }
1714            _ => false,
1715        }
1716    }
1717
1718    fn run_test(
1719        mut solver: ConstraintSatisfactionSolver,
1720        assumptions: Vec<Predicate>,
1721        expected_flag: CSPSolverExecutionFlag,
1722        expected_result: CoreExtractionResult,
1723    ) {
1724        let mut brancher = DefaultBrancher::default_over_all_variables(&solver.assignments);
1725        let flag = solver.solve_under_assumptions(&assumptions, &mut Indefinite, &mut brancher);
1726        assert_eq!(flag, expected_flag, "The flags do not match.");
1727
1728        if matches!(flag, CSPSolverExecutionFlag::Infeasible) {
1729            assert!(
1730                is_result_the_same(
1731                    &solver.extract_clausal_core(&mut brancher),
1732                    &expected_result
1733                ),
1734                "The result is not the same"
1735            );
1736        }
1737    }
1738
1739    fn create_instance1() -> (ConstraintSatisfactionSolver, Vec<Predicate>) {
1740        let mut solver = ConstraintSatisfactionSolver::default();
1741        let constraint_tag = solver.new_constraint_tag();
1742        let lit1 = solver.create_new_literal(None).get_true_predicate();
1743        let lit2 = solver.create_new_literal(None).get_true_predicate();
1744
1745        let _ = solver.add_clause([lit1, lit2], constraint_tag);
1746        let _ = solver.add_clause([lit1, !lit2], constraint_tag);
1747        let _ = solver.add_clause([!lit1, lit2], constraint_tag);
1748        (solver, vec![lit1, lit2])
1749    }
1750
1751    #[test]
1752    fn core_extraction_unit_core() {
1753        let mut solver = ConstraintSatisfactionSolver::default();
1754        let constraint_tag = solver.new_constraint_tag();
1755        let lit1 = solver.create_new_literal(None).get_true_predicate();
1756        let _ = solver.add_clause(vec![lit1], constraint_tag);
1757
1758        run_test(
1759            solver,
1760            vec![!lit1],
1761            CSPSolverExecutionFlag::Infeasible,
1762            CoreExtractionResult::Core(vec![!lit1]),
1763        )
1764    }
1765
1766    #[test]
1767    fn simple_core_extraction_1_1() {
1768        let (solver, lits) = create_instance1();
1769        run_test(
1770            solver,
1771            vec![!lits[0], !lits[1]],
1772            CSPSolverExecutionFlag::Infeasible,
1773            CoreExtractionResult::Core(vec![!lits[0]]),
1774        )
1775    }
1776
1777    #[test]
1778    fn simple_core_extraction_1_2() {
1779        let (solver, lits) = create_instance1();
1780        run_test(
1781            solver,
1782            vec![!lits[1], !lits[0]],
1783            CSPSolverExecutionFlag::Infeasible,
1784            CoreExtractionResult::Core(vec![!lits[1]]),
1785        );
1786    }
1787
1788    #[test]
1789    fn simple_core_extraction_1_infeasible() {
1790        let (mut solver, lits) = create_instance1();
1791        let constraint_tag = solver.new_constraint_tag();
1792        let _ = solver.add_clause([!lits[0], !lits[1]], constraint_tag);
1793        run_test(
1794            solver,
1795            vec![!lits[1], !lits[0]],
1796            CSPSolverExecutionFlag::Infeasible,
1797            CoreExtractionResult::Core(vec![]),
1798        );
1799    }
1800
1801    #[test]
1802    fn simple_core_extraction_1_core_conflicting() {
1803        let (solver, lits) = create_instance1();
1804        run_test(
1805            solver,
1806            vec![!lits[1], lits[1]],
1807            CSPSolverExecutionFlag::Infeasible,
1808            CoreExtractionResult::ConflictingAssumption(!lits[1]),
1809        );
1810    }
1811    fn create_instance2() -> (ConstraintSatisfactionSolver, Vec<Predicate>) {
1812        let mut solver = ConstraintSatisfactionSolver::default();
1813        let constraint_tag = solver.new_constraint_tag();
1814        let lit1 = solver.create_new_literal(None).get_true_predicate();
1815        let lit2 = solver.create_new_literal(None).get_true_predicate();
1816        let lit3 = solver.create_new_literal(None).get_true_predicate();
1817
1818        let _ = solver.add_clause([lit1, lit2, lit3], constraint_tag);
1819        let _ = solver.add_clause([lit1, !lit2, lit3], constraint_tag);
1820        (solver, vec![lit1, lit2, lit3])
1821    }
1822
1823    #[test]
1824    fn simple_core_extraction_2_1() {
1825        let (solver, lits) = create_instance2();
1826        run_test(
1827            solver,
1828            vec![!lits[0], lits[1], !lits[2]],
1829            CSPSolverExecutionFlag::Infeasible,
1830            CoreExtractionResult::Core(vec![!lits[0], lits[1], !lits[2]]),
1831        );
1832    }
1833
1834    #[test]
1835    fn simple_core_extraction_2_long_assumptions_with_inconsistency_at_the_end() {
1836        let (solver, lits) = create_instance2();
1837        run_test(
1838            solver,
1839            vec![!lits[0], lits[1], !lits[2], lits[0]],
1840            CSPSolverExecutionFlag::Infeasible,
1841            CoreExtractionResult::ConflictingAssumption(!lits[0]),
1842        );
1843    }
1844
1845    #[test]
1846    fn simple_core_extraction_2_inconsistent_long_assumptions() {
1847        let (solver, lits) = create_instance2();
1848        run_test(
1849            solver,
1850            vec![!lits[0], !lits[0], !lits[1], !lits[1], lits[0]],
1851            CSPSolverExecutionFlag::Infeasible,
1852            CoreExtractionResult::ConflictingAssumption(!lits[0]),
1853        );
1854    }
1855    fn create_instance3() -> (ConstraintSatisfactionSolver, Vec<Predicate>) {
1856        let mut solver = ConstraintSatisfactionSolver::default();
1857        let constraint_tag = solver.new_constraint_tag();
1858
1859        let lit1 = solver.create_new_literal(None).get_true_predicate();
1860        let lit2 = solver.create_new_literal(None).get_true_predicate();
1861        let lit3 = solver.create_new_literal(None).get_true_predicate();
1862
1863        let _ = solver.add_clause([lit1, lit2, lit3], constraint_tag);
1864        (solver, vec![lit1, lit2, lit3])
1865    }
1866
1867    #[test]
1868    fn simple_core_extraction_3_1() {
1869        let (solver, lits) = create_instance3();
1870        run_test(
1871            solver,
1872            vec![!lits[0], !lits[1], !lits[2]],
1873            CSPSolverExecutionFlag::Infeasible,
1874            CoreExtractionResult::Core(vec![!lits[0], !lits[1], !lits[2]]),
1875        );
1876    }
1877
1878    #[test]
1879    fn simple_core_extraction_3_2() {
1880        let (solver, lits) = create_instance3();
1881        run_test(
1882            solver,
1883            vec![!lits[0], !lits[1]],
1884            CSPSolverExecutionFlag::Feasible,
1885            CoreExtractionResult::Core(vec![]), // will be ignored in the test
1886        );
1887    }
1888
1889    #[test]
1890    fn core_extraction_equality_assumption() {
1891        let mut solver = ConstraintSatisfactionSolver::default();
1892
1893        let x = solver.create_new_integer_variable(0, 10, None);
1894        let y = solver.create_new_integer_variable(0, 10, None);
1895        let z = solver.create_new_integer_variable(0, 10, None);
1896
1897        let constraint_tag = solver.new_constraint_tag();
1898
1899        let result = solver.add_propagator(LinearNotEqualPropagatorArgs {
1900            terms: [x.scaled(1), y.scaled(-1)].into(),
1901            rhs: 0,
1902            constraint_tag,
1903        });
1904        assert!(result.is_ok());
1905        run_test(
1906            solver,
1907            vec![
1908                predicate!(x >= 5),
1909                predicate!(z != 10),
1910                predicate!(y == 5),
1911                predicate!(x <= 5),
1912            ],
1913            CSPSolverExecutionFlag::Infeasible,
1914            CoreExtractionResult::Core(vec![predicate!(x == 5), predicate!(y == 5)]),
1915        )
1916    }
1917
1918    #[test]
1919    fn new_domain_with_negative_lower_bound() {
1920        let lb = -2;
1921        let ub = 2;
1922
1923        let mut solver = ConstraintSatisfactionSolver::default();
1924        let domain_id = solver.create_new_integer_variable(lb, ub, None);
1925
1926        assert_eq!(lb, solver.assignments.get_lower_bound(domain_id));
1927
1928        assert_eq!(ub, solver.assignments.get_upper_bound(domain_id));
1929
1930        assert!(!solver
1931            .assignments
1932            .is_predicate_satisfied(predicate![domain_id == lb]));
1933
1934        for value in (lb + 1)..ub {
1935            let predicate = predicate![domain_id >= value];
1936
1937            assert!(!solver.assignments.is_predicate_satisfied(predicate));
1938
1939            assert!(!solver
1940                .assignments
1941                .is_predicate_satisfied(predicate![domain_id == value]));
1942        }
1943
1944        assert!(!solver
1945            .assignments
1946            .is_predicate_satisfied(predicate![domain_id == ub]));
1947    }
1948
1949    #[test]
1950    fn check_can_compute_1uip_with_propagator_initialisation_conflict() {
1951        let mut solver = ConstraintSatisfactionSolver::default();
1952
1953        let x = solver.create_new_integer_variable(1, 1, None);
1954        let y = solver.create_new_integer_variable(2, 2, None);
1955
1956        let constraint_tag = solver.new_constraint_tag();
1957
1958        let propagator = LinearNotEqualPropagatorArgs {
1959            terms: vec![x, y].into(),
1960            rhs: 3,
1961            constraint_tag,
1962        };
1963        let result = solver.add_propagator(propagator);
1964        assert!(result.is_err());
1965    }
1966}