From 14afd5ac49a667c2c498c4e2f5ca9ae08ce10b5a Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Fri, 9 Jan 2026 14:04:42 +0100 Subject: [PATCH 01/48] refactor(pumpkin-core): InferenceCode directly stores the tag and label --- .../conflict_analysis_context.rs | 9 +++----- .../resolvers/resolution_resolver.rs | 6 ++---- .../engine/constraint_satisfaction_solver.rs | 9 ++------ .../core/src/engine/cp/assignments.rs | 10 +++++---- .../core/src/engine/cp/test_solver.rs | 11 ---------- pumpkin-crates/core/src/engine/state.rs | 16 +------------- pumpkin-crates/core/src/proof/finalizer.rs | 3 +-- .../core/src/proof/inference_code.rs | 21 ++++++++++++------- pumpkin-crates/core/src/proof/mod.rs | 7 ++----- .../core/src/propagation/constructor.rs | 4 ++-- .../propagators/nogoods/nogood_propagator.rs | 7 ++++--- 11 files changed, 37 insertions(+), 66 deletions(-) diff --git a/pumpkin-crates/core/src/engine/conflict_analysis/conflict_analysis_context.rs b/pumpkin-crates/core/src/engine/conflict_analysis/conflict_analysis_context.rs index 14f4643a1..2bd60ae85 100644 --- a/pumpkin-crates/core/src/engine/conflict_analysis/conflict_analysis_context.rs +++ b/pumpkin-crates/core/src/engine/conflict_analysis/conflict_analysis_context.rs @@ -86,7 +86,6 @@ impl ConflictAnalysisContext<'_> { let conflict_nogood = match self.solver_state.get_conflict_info() { StoredConflictInfo::Propagator(conflict) => { let _ = self.proof_log.log_inference( - &self.state.inference_codes, &mut self.state.constraint_tags, conflict.inference_code, conflict.conjunction.iter().copied(), @@ -147,7 +146,8 @@ impl ConflictAnalysisContext<'_> { let trail_entry = state.assignments.get_trail_entry(trail_index); let (reason_ref, inference_code) = trail_entry .reason - .expect("Cannot be a null reason for propagation."); + .expect("Cannot be a null reason for propagation.") + .clone(); let propagator_id = state.reason_store.get_propagator(reason_ref); @@ -176,9 +176,8 @@ impl ConflictAnalysisContext<'_> { .expect("Expected to be able to retrieve step id for unit nogood"); let _ = proof_log.log_inference( - &state.inference_codes, &mut state.constraint_tags, - *inference_code, + inference_code.clone(), [], Some(predicate), &state.variable_names, @@ -186,7 +185,6 @@ impl ConflictAnalysisContext<'_> { } else { // Otherwise we log the inference which was used to derive the nogood let _ = proof_log.log_inference( - &state.inference_codes, &mut state.constraint_tags, inference_code, reason_buffer.as_ref().iter().copied(), @@ -221,7 +219,6 @@ impl ConflictAnalysisContext<'_> { // We also need to log this last propagation to the proof log as an inference. let _ = self.proof_log.log_inference( - &self.state.inference_codes, &mut self.state.constraint_tags, conflict.trigger_inference_code, empty_domain_reason.iter().copied(), diff --git a/pumpkin-crates/core/src/engine/conflict_analysis/resolvers/resolution_resolver.rs b/pumpkin-crates/core/src/engine/conflict_analysis/resolvers/resolution_resolver.rs index 69ef22095..3c9a77e57 100644 --- a/pumpkin-crates/core/src/engine/conflict_analysis/resolvers/resolution_resolver.rs +++ b/pumpkin-crates/core/src/engine/conflict_analysis/resolvers/resolution_resolver.rs @@ -106,14 +106,12 @@ impl ConflictResolver for ResolutionResolver { ) .expect("Failed to write proof log"); - let inference_code = context - .state - .create_inference_code(constraint_tag, NogoodLabel); + let inference_code = InferenceCode::new(constraint_tag, NogoodLabel); if learned_nogood.predicates.len() == 1 { let _ = context .unit_nogood_inference_codes - .insert(!learned_nogood.predicates[0], inference_code); + .insert(!learned_nogood.predicates[0], inference_code.clone()); } context diff --git a/pumpkin-crates/core/src/engine/constraint_satisfaction_solver.rs b/pumpkin-crates/core/src/engine/constraint_satisfaction_solver.rs index 6fcffa158..828ed1de7 100644 --- a/pumpkin-crates/core/src/engine/constraint_satisfaction_solver.rs +++ b/pumpkin-crates/core/src/engine/constraint_satisfaction_solver.rs @@ -833,7 +833,6 @@ impl ConstraintSatisfactionSolver { // The proof inference for the propagation `R -> l` is `R /\ ~l -> false`. let inference_premises = reason.iter().copied().chain(std::iter::once(!propagated)); let _ = self.internal_parameters.proof_log.log_inference( - &self.state.inference_codes, &mut self.state.constraint_tags, inference_code, inference_premises, @@ -877,9 +876,7 @@ impl ConstraintSatisfactionSolver { ); if let Ok(constraint_tag) = constraint_tag { - let inference_code = self - .state - .create_inference_code(constraint_tag, NogoodLabel); + let inference_code = InferenceCode::new(constraint_tag, NogoodLabel); let _ = self .unit_nogood_inference_codes @@ -1045,9 +1042,7 @@ impl ConstraintSatisfactionSolver { return Err(ConstraintOperationError::InfeasibleClause); } - let inference_code = self - .state - .create_inference_code(constraint_tag, NogoodLabel); + let inference_code = InferenceCode::new(constraint_tag, NogoodLabel); if let Err(constraint_operation_error) = self.add_nogood(predicates, inference_code) { let _ = self.conclude_proof_unsat(); diff --git a/pumpkin-crates/core/src/engine/cp/assignments.rs b/pumpkin-crates/core/src/engine/cp/assignments.rs index 94fcccc40..8c130849d 100644 --- a/pumpkin-crates/core/src/engine/cp/assignments.rs +++ b/pumpkin-crates/core/src/engine/cp/assignments.rs @@ -82,7 +82,7 @@ impl Assignments { } else { let values_at_current_checkpoint = self.trail.values_at_checkpoint(self.get_checkpoint()); - let entry = values_at_current_checkpoint[0]; + let entry = &values_at_current_checkpoint[0]; pumpkin_assert_eq_simple!(None, entry.reason); Some(entry.predicate) @@ -108,7 +108,9 @@ impl Assignments { } pub(crate) fn get_trail_entry(&self, index: usize) -> ConstraintProgrammingTrailEntry { - self.trail[index] + // The clone is required because of InferenceCode is not copy. However, it is a + // reference-counted type, so cloning is cheap. + self.trail[index].clone() } // registers the domain of a new integer variable @@ -804,7 +806,7 @@ impl Assignments { .iter() .find_map(|entry| { if entry.predicate == predicate { - entry.reason.map(|(reason_ref, _)| reason_ref) + entry.reason.as_ref().map(|(reason_ref, _)| *reason_ref) } else { None } @@ -813,7 +815,7 @@ impl Assignments { } } -#[derive(Clone, Copy, Debug)] +#[derive(Clone, Debug)] pub(crate) struct ConstraintProgrammingTrailEntry { pub predicate: Predicate, /// Explicitly store the bound before the predicate was applied so that it is easier later on diff --git a/pumpkin-crates/core/src/engine/cp/test_solver.rs b/pumpkin-crates/core/src/engine/cp/test_solver.rs index 8507c3c26..f7bfaeee1 100644 --- a/pumpkin-crates/core/src/engine/cp/test_solver.rs +++ b/pumpkin-crates/core/src/engine/cp/test_solver.rs @@ -1,7 +1,6 @@ //! This module exposes helpers that aid testing of CP propagators. The [`TestSolver`] allows //! setting up specific scenarios under which to test the various operations of a propagator. use std::fmt::Debug; -use std::num::NonZero; use super::PropagatorQueue; use crate::containers::KeyGenerator; @@ -15,7 +14,6 @@ use crate::options::LearningOptions; use crate::predicate; use crate::predicates::PropositionalConjunction; use crate::proof::ConstraintTag; -use crate::proof::InferenceCode; use crate::propagation::Domains; use crate::propagation::EnqueueDecision; use crate::propagation::ExplanationContext; @@ -311,15 +309,6 @@ impl TestSolver { self.constraint_tags.next_key() } - pub fn new_inference_code(&mut self) -> InferenceCode { - self.state.inference_codes.push(( - ConstraintTag::from_non_zero( - NonZero::try_from(1 + self.state.inference_codes.len() as u32).unwrap(), - ), - "label".into(), - )) - } - pub fn new_checkpoint(&mut self) { self.state.new_checkpoint(); } diff --git a/pumpkin-crates/core/src/engine/state.rs b/pumpkin-crates/core/src/engine/state.rs index 11b67ab76..dbc8c77cc 100644 --- a/pumpkin-crates/core/src/engine/state.rs +++ b/pumpkin-crates/core/src/engine/state.rs @@ -67,7 +67,6 @@ pub struct State { /// and/or the polarity [Predicate]s pub(crate) notification_engine: NotificationEngine, - pub(crate) inference_codes: KeyedVec)>, /// The [`ConstraintTag`]s generated for this proof. pub(crate) constraint_tags: KeyGenerator, @@ -106,7 +105,7 @@ impl From for Conflict { } /// A conflict because a domain became empty. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[derive(Clone, Debug, PartialEq, Eq)] pub struct EmptyDomainConflict { /// The predicate that caused a domain to become empty. pub trigger_predicate: Predicate, @@ -154,7 +153,6 @@ impl Default for State { propagators: PropagatorStore::default(), reason_store: ReasonStore::default(), notification_engine: NotificationEngine::default(), - inference_codes: KeyedVec::default(), statistics: StateStatistics::default(), constraint_tags: KeyGenerator::default(), }; @@ -197,18 +195,6 @@ impl State { /// Operations to create . impl State { - /// Create a new [`InferenceCode`] for a [`ConstraintTag`] and [`InferenceLabel`] combination. - /// - /// The inference codes are required to log inferences with [`ProofLog::log_inference`]. - pub(crate) fn create_inference_code( - &mut self, - constraint_tag: ConstraintTag, - inference_label: impl InferenceLabel, - ) -> InferenceCode { - self.inference_codes - .push((constraint_tag, inference_label.to_str())) - } - /// Create a new [`ConstraintTag`]. pub fn new_constraint_tag(&mut self) -> ConstraintTag { self.constraint_tags.next_key() diff --git a/pumpkin-crates/core/src/proof/finalizer.rs b/pumpkin-crates/core/src/proof/finalizer.rs index 1006aabd8..9f4687e39 100644 --- a/pumpkin-crates/core/src/proof/finalizer.rs +++ b/pumpkin-crates/core/src/proof/finalizer.rs @@ -99,9 +99,8 @@ fn get_required_assumptions( // If the predicate is a unit-nogood, we explain the root-level assignment. if let Some(inference_code) = context.unit_nogood_inference_codes.get(&predicate) { let _ = context.proof_log.log_inference( - &context.state.inference_codes, &mut context.state.constraint_tags, - *inference_code, + inference_code.clone(), [], Some(predicate), &context.state.variable_names, diff --git a/pumpkin-crates/core/src/proof/inference_code.rs b/pumpkin-crates/core/src/proof/inference_code.rs index 89eef8409..f14053dea 100644 --- a/pumpkin-crates/core/src/proof/inference_code.rs +++ b/pumpkin-crates/core/src/proof/inference_code.rs @@ -46,16 +46,23 @@ impl StorageKey for ConstraintTag { /// An inference code is a combination of a constraint tag with an inference label. Propagators /// associate an inference code with every propagation to identify why that propagation happened /// in terms of the constraint and inference that identified it. -#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] -pub struct InferenceCode(NonZero); +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub struct InferenceCode(ConstraintTag, Arc); -impl StorageKey for InferenceCode { - fn index(&self) -> usize { - self.0.get() as usize - 1 +impl InferenceCode { + /// Create a new inference code from a [`ConstraintTag`] and [`InferenceLabel`]. + pub fn new(tag: ConstraintTag, label: impl InferenceLabel) -> Self { + InferenceCode(tag, label.to_str()) } - fn create_from_index(index: usize) -> Self { - Self(NonZero::new(index as u32 + 1).expect("the '+ 1' ensures the value is non-zero")) + /// Get the constraint tag. + pub fn tag(&self) -> ConstraintTag { + self.0 + } + + /// Get the inference label. + pub fn label(&self) -> &str { + self.1.as_ref() } } diff --git a/pumpkin-crates/core/src/proof/mod.rs b/pumpkin-crates/core/src/proof/mod.rs index 091a660c0..e324d7538 100644 --- a/pumpkin-crates/core/src/proof/mod.rs +++ b/pumpkin-crates/core/src/proof/mod.rs @@ -80,7 +80,6 @@ impl ProofLog { /// Log an inference to the proof. pub(crate) fn log_inference( &mut self, - inference_codes: &KeyedVec)>, constraint_tags: &mut KeyGenerator, inference_code: InferenceCode, premises: impl IntoIterator, @@ -97,8 +96,6 @@ impl ProofLog { return Ok(ConstraintTag::create_from_index(0)); }; - let (tag, label) = inference_codes[inference_code].clone(); - let inference_tag = constraint_tags.next_key(); let inference = Inference { @@ -110,8 +107,8 @@ impl ProofLog { consequent: propagated.map(|predicate| { proof_atomics.map_predicate_to_proof_atomic(predicate, variable_names) }), - generated_by: Some(tag.into()), - label: Some(label), + generated_by: Some(inference_code.tag().into()), + label: Some(inference_code.label()), }; writer.log_inference(inference)?; diff --git a/pumpkin-crates/core/src/propagation/constructor.rs b/pumpkin-crates/core/src/propagation/constructor.rs index 13b4e0c1f..6b070de94 100644 --- a/pumpkin-crates/core/src/propagation/constructor.rs +++ b/pumpkin-crates/core/src/propagation/constructor.rs @@ -159,13 +159,13 @@ impl PropagatorConstructorContext<'_> { /// Create a new [`InferenceCode`]. These codes are required to identify specific propagations /// in the solver and the proof. + #[deprecated = "construct inference codes with InferenceCode::new"] pub fn create_inference_code( &mut self, constraint_tag: ConstraintTag, inference_label: impl InferenceLabel, ) -> InferenceCode { - self.state - .create_inference_code(constraint_tag, inference_label) + InferenceCode::new(constraint_tag, inference_label) } /// Get a new [`LocalId`] which is guaranteed to be unused. diff --git a/pumpkin-crates/core/src/propagators/nogoods/nogood_propagator.rs b/pumpkin-crates/core/src/propagators/nogoods/nogood_propagator.rs index 674296ad5..8da14b713 100644 --- a/pumpkin-crates/core/src/propagators/nogoods/nogood_propagator.rs +++ b/pumpkin-crates/core/src/propagators/nogoods/nogood_propagator.rs @@ -317,7 +317,8 @@ impl Propagator for NogoodPropagator { predicate, reason, self.inference_codes - [self.nogood_predicates.get_nogood_index(&watcher.nogood_id)], + [self.nogood_predicates.get_nogood_index(&watcher.nogood_id)] + .clone(), ); // If the propagation lead to a conflict. if let Err(e) = result { @@ -491,7 +492,7 @@ impl NogoodPropagator { // asserting nogood such that we can re-create the reason when asked for it let reason = Reason::DynamicLazy(nogood_id.id as u64); let inference_code = - self.inference_codes[self.nogood_predicates.get_nogood_index(&nogood_id)]; + self.inference_codes[self.nogood_predicates.get_nogood_index(&nogood_id)].clone(); let predicate = !context .notification_engine @@ -1154,7 +1155,7 @@ impl NogoodPropagator { // This is an inefficient implementation for testing purposes let nogood = &self.nogood_predicates[nogood_id]; let info_id = self.nogood_predicates.get_nogood_index(&nogood_id); - let inference_code = self.inference_codes[info_id]; + let inference_code = self.inference_codes[info_id].clone(); if self.nogood_info[info_id].is_deleted { // The nogood has already been deleted, meaning that it could be that the call to From 87719f94133afcefcc86ece666e3b86ce4ed9caf Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Fri, 9 Jan 2026 15:06:53 +0100 Subject: [PATCH 02/48] Pass InferenceCode by reference since it is not copy anymore --- .../conflict_analysis_context.rs | 3 +- pumpkin-crates/core/src/engine/cp/mod.rs | 7 +++-- pumpkin-crates/core/src/engine/state.rs | 2 -- .../core/src/proof/inference_code.rs | 15 ++++++++-- pumpkin-crates/core/src/proof/mod.rs | 2 -- .../contexts/propagation_context.rs | 4 +-- .../propagators/nogoods/nogood_propagator.rs | 20 +++++++------ .../src/propagators/reified_propagator.rs | 21 ++++++++------ .../propagators/arithmetic/absolute_value.rs | 18 ++++++------ .../arithmetic/binary/binary_equals.rs | 8 ++--- .../arithmetic/binary/binary_not_equals.rs | 12 ++++---- .../arithmetic/integer_division.rs | 12 ++++---- .../arithmetic/integer_multiplication.rs | 10 +++---- .../arithmetic/linear_less_or_equal.rs | 8 ++--- .../arithmetic/linear_not_equal.rs | 10 +++---- .../src/propagators/arithmetic/maximum.rs | 10 +++---- .../time_table/explanations/pointwise.rs | 4 +-- .../debug.rs | 2 +- .../synchronisation.rs | 4 +-- .../time_table_over_interval_incremental.rs | 22 +++++++------- .../synchronisation.rs | 4 +-- .../time_table_per_point_incremental.rs | 24 +++++++-------- .../time_table/propagation_handler.rs | 29 ++++++++++--------- .../time_table/time_table_over_interval.rs | 16 +++++----- .../time_table/time_table_per_point.rs | 14 ++++----- .../cumulative/time_table/time_table_util.rs | 18 +++++++----- .../disjunctive/disjunctive_propagator.rs | 11 ++++--- .../propagators/src/propagators/element.rs | 16 +++++----- 28 files changed, 171 insertions(+), 155 deletions(-) diff --git a/pumpkin-crates/core/src/engine/conflict_analysis/conflict_analysis_context.rs b/pumpkin-crates/core/src/engine/conflict_analysis/conflict_analysis_context.rs index 2bd60ae85..ba47bb7b3 100644 --- a/pumpkin-crates/core/src/engine/conflict_analysis/conflict_analysis_context.rs +++ b/pumpkin-crates/core/src/engine/conflict_analysis/conflict_analysis_context.rs @@ -146,8 +146,7 @@ impl ConflictAnalysisContext<'_> { let trail_entry = state.assignments.get_trail_entry(trail_index); let (reason_ref, inference_code) = trail_entry .reason - .expect("Cannot be a null reason for propagation.") - .clone(); + .expect("Cannot be a null reason for propagation."); let propagator_id = state.reason_store.get_propagator(reason_ref); diff --git a/pumpkin-crates/core/src/engine/cp/mod.rs b/pumpkin-crates/core/src/engine/cp/mod.rs index e987703a0..2be12ab01 100644 --- a/pumpkin-crates/core/src/engine/cp/mod.rs +++ b/pumpkin-crates/core/src/engine/cp/mod.rs @@ -21,6 +21,7 @@ mod tests { use crate::engine::notifications::NotificationEngine; use crate::engine::reason::ReasonStore; use crate::predicate; + use crate::proof::ConstraintTag; use crate::proof::InferenceCode; use crate::propagation::PropagationContext; use crate::propagation::PropagatorId; @@ -46,7 +47,7 @@ mod tests { let result = context.post( predicate![domain >= 2], conjunction!(), - InferenceCode::create_from_index(0), + &InferenceCode::unknown_label(ConstraintTag::create_from_index(0)), ); assert!(result.is_ok()); } @@ -75,7 +76,7 @@ mod tests { let result = context.post( predicate![domain <= 15], conjunction!(), - InferenceCode::create_from_index(0), + &InferenceCode::unknown_label(ConstraintTag::create_from_index(0)), ); assert!(result.is_ok()); } @@ -104,7 +105,7 @@ mod tests { let result = context.post( predicate![domain != 15], conjunction!(), - InferenceCode::create_from_index(0), + &InferenceCode::unknown_label(ConstraintTag::create_from_index(0)), ); assert!(result.is_ok()); } diff --git a/pumpkin-crates/core/src/engine/state.rs b/pumpkin-crates/core/src/engine/state.rs index dbc8c77cc..2e5b6b936 100644 --- a/pumpkin-crates/core/src/engine/state.rs +++ b/pumpkin-crates/core/src/engine/state.rs @@ -2,7 +2,6 @@ use std::sync::Arc; use crate::basic_types::PropagatorConflict; use crate::containers::KeyGenerator; -use crate::containers::KeyedVec; use crate::create_statistics_struct; use crate::engine::Assignments; use crate::engine::ConstraintProgrammingTrailEntry; @@ -19,7 +18,6 @@ use crate::predicates::Predicate; use crate::predicates::PredicateType; use crate::proof::ConstraintTag; use crate::proof::InferenceCode; -use crate::proof::InferenceLabel; #[cfg(doc)] use crate::proof::ProofLog; use crate::propagation::CurrentNogood; diff --git a/pumpkin-crates/core/src/proof/inference_code.rs b/pumpkin-crates/core/src/proof/inference_code.rs index f14053dea..a856405ca 100644 --- a/pumpkin-crates/core/src/proof/inference_code.rs +++ b/pumpkin-crates/core/src/proof/inference_code.rs @@ -55,6 +55,14 @@ impl InferenceCode { InferenceCode(tag, label.to_str()) } + /// Create an inference label with the [`Unknown`] inference label. + /// + /// This should be avoided as much as possible. This is likely only useful for writing unit + /// tests. + pub fn unknown_label(tag: ConstraintTag) -> Self { + InferenceCode::new(tag, Unknown) + } + /// Get the constraint tag. pub fn tag(&self) -> ConstraintTag { self.0 @@ -82,8 +90,8 @@ impl InferenceCode { /// case. #[macro_export] macro_rules! declare_inference_label { - ($name:ident) => { - declare_inference_label!($name, { + ($v:vis $name:ident) => { + declare_inference_label!($v $name, { let ident_str = stringify!($name); <&str as convert_case::Casing<&str>>::to_case( &ident_str, @@ -132,3 +140,6 @@ pub trait InferenceLabel { /// `Arc`. fn to_str(&self) -> Arc; } + +/// An inference label used when no more specific label is known. +declare_inference_label!(pub Unknown); diff --git a/pumpkin-crates/core/src/proof/mod.rs b/pumpkin-crates/core/src/proof/mod.rs index e324d7538..284ac9617 100644 --- a/pumpkin-crates/core/src/proof/mod.rs +++ b/pumpkin-crates/core/src/proof/mod.rs @@ -12,7 +12,6 @@ mod proof_atomics; use std::fs::File; use std::io::Write; use std::path::Path; -use std::sync::Arc; use dimacs::DimacsProof; use drcp_format::Deduction; @@ -26,7 +25,6 @@ use proof_atomics::ProofAtomics; use crate::Solver; use crate::containers::HashMap; use crate::containers::KeyGenerator; -use crate::containers::KeyedVec; use crate::containers::StorageKey; use crate::engine::variable_names::VariableNames; use crate::predicates::Predicate; diff --git a/pumpkin-crates/core/src/propagation/contexts/propagation_context.rs b/pumpkin-crates/core/src/propagation/contexts/propagation_context.rs index dc9d6571a..c272742e3 100644 --- a/pumpkin-crates/core/src/propagation/contexts/propagation_context.rs +++ b/pumpkin-crates/core/src/propagation/contexts/propagation_context.rs @@ -187,13 +187,13 @@ impl PropagationContext<'_> { &mut self, predicate: Predicate, reason: impl Into, - inference_code: InferenceCode, + inference_code: &InferenceCode, ) -> Result<(), EmptyDomainConflict> { let slot = self.reason_store.new_slot(); let modification_result = self.assignments.post_predicate( predicate, - Some((slot.reason_ref(), inference_code)), + Some((slot.reason_ref(), inference_code.clone())), self.notification_engine, ); diff --git a/pumpkin-crates/core/src/propagators/nogoods/nogood_propagator.rs b/pumpkin-crates/core/src/propagators/nogoods/nogood_propagator.rs index 8da14b713..9bec2334e 100644 --- a/pumpkin-crates/core/src/propagators/nogoods/nogood_propagator.rs +++ b/pumpkin-crates/core/src/propagators/nogoods/nogood_propagator.rs @@ -316,9 +316,8 @@ impl Propagator for NogoodPropagator { let result = context.post( predicate, reason, - self.inference_codes - [self.nogood_predicates.get_nogood_index(&watcher.nogood_id)] - .clone(), + &self.inference_codes + [self.nogood_predicates.get_nogood_index(&watcher.nogood_id)], ); // If the propagation lead to a conflict. if let Err(e) = result { @@ -492,7 +491,7 @@ impl NogoodPropagator { // asserting nogood such that we can re-create the reason when asked for it let reason = Reason::DynamicLazy(nogood_id.id as u64); let inference_code = - self.inference_codes[self.nogood_predicates.get_nogood_index(&nogood_id)].clone(); + &self.inference_codes[self.nogood_predicates.get_nogood_index(&nogood_id)]; let predicate = !context .notification_engine @@ -614,7 +613,7 @@ impl NogoodPropagator { context.post( !nogood[0], PropositionalConjunction::from(input_nogood), - inference_code, + &inference_code, )?; Ok(()) } @@ -1155,7 +1154,7 @@ impl NogoodPropagator { // This is an inefficient implementation for testing purposes let nogood = &self.nogood_predicates[nogood_id]; let info_id = self.nogood_predicates.get_nogood_index(&nogood_id); - let inference_code = self.inference_codes[info_id].clone(); + let inference_code = &self.inference_codes[info_id]; if self.nogood_info[info_id].is_deleted { // The nogood has already been deleted, meaning that it could be that the call to @@ -1192,7 +1191,7 @@ impl NogoodPropagator { .iter() .map(|predicate_id| context.get_predicate(*predicate_id)) .collect::(), - inference_code, + inference_code: inference_code.clone(), } .into()); } @@ -1287,13 +1286,16 @@ impl NogoodPropagator { mod tests { use super::NogoodPropagator; use crate::conjunction; + use crate::containers::StorageKey; use crate::engine::test_solver::TestSolver; use crate::predicate; + use crate::proof::ConstraintTag; + use crate::proof::InferenceCode; #[test] fn ternary_nogood_propagate() { let mut solver = TestSolver::default(); - let inference_code = solver.new_inference_code(); + let inference_code = InferenceCode::unknown_label(ConstraintTag::create_from_index(0)); let dummy = solver.new_variable(0, 1); let a = solver.new_variable(1, 3); let b = solver.new_variable(-4, 4); @@ -1333,7 +1335,7 @@ mod tests { #[test] fn unsat() { let mut solver = TestSolver::default(); - let inference_code = solver.new_inference_code(); + let inference_code = InferenceCode::unknown_label(ConstraintTag::create_from_index(0)); let a = solver.new_variable(1, 3); let b = solver.new_variable(-4, 4); let c = solver.new_variable(-10, 20); diff --git a/pumpkin-crates/core/src/propagators/reified_propagator.rs b/pumpkin-crates/core/src/propagators/reified_propagator.rs index 2cecbc1a8..8047addb9 100644 --- a/pumpkin-crates/core/src/propagators/reified_propagator.rs +++ b/pumpkin-crates/core/src/propagators/reified_propagator.rs @@ -183,7 +183,7 @@ impl ReifiedPropagator { context.post( self.reification_literal.get_false_predicate(), conflict.conjunction, - conflict.inference_code, + &conflict.inference_code, )?; } @@ -231,6 +231,7 @@ mod tests { use crate::engine::test_solver::TestSolver; use crate::predicate; use crate::predicates::PropositionalConjunction; + use crate::proof::ConstraintTag; use crate::proof::InferenceCode; use crate::variables::DomainId; @@ -246,7 +247,9 @@ mod tests { let t1 = triggered_conflict.clone(); let t2 = triggered_conflict.clone(); - let inference_code = solver.new_inference_code(); + let inference_code = InferenceCode::unknown_label(ConstraintTag::create_from_index(0)); + let i1 = inference_code.clone(); + let i2 = inference_code.clone(); let _ = solver .new_propagator(ReifiedPropagatorArgs { @@ -254,14 +257,14 @@ mod tests { move |_: PropagationContext| { Err(PropagatorConflict { conjunction: t1.clone(), - inference_code, + inference_code: i1.clone(), } .into()) }, move |_: Domains| { Some(PropagatorConflict { conjunction: t2.clone(), - inference_code, + inference_code: i2.clone(), }) }, ), @@ -289,7 +292,7 @@ mod tests { ctx.post( predicate![var >= 3], conjunction!(), - InferenceCode::create_from_index(0), + &InferenceCode::unknown_label(ConstraintTag::create_from_index(0)), )?; Ok(()) }, @@ -320,7 +323,7 @@ mod tests { let _ = solver.set_literal(reification_literal, true); let var = solver.new_variable(1, 1); - let inference_code = solver.new_inference_code(); + let inference_code = InferenceCode::unknown_label(ConstraintTag::create_from_index(0)); let inconsistency = solver .new_propagator(ReifiedPropagatorArgs { @@ -328,7 +331,7 @@ mod tests { move |_: PropagationContext| { Err(PropagatorConflict { conjunction: conjunction!([var >= 1]), - inference_code, + inference_code: inference_code.clone(), } .into()) }, @@ -360,7 +363,7 @@ mod tests { let reification_literal = solver.new_literal(); let var = solver.new_variable(1, 5); - let inference_code = solver.new_inference_code(); + let inference_code = InferenceCode::unknown_label(ConstraintTag::create_from_index(0)); let propagator = solver .new_propagator(ReifiedPropagatorArgs { @@ -370,7 +373,7 @@ mod tests { if context.is_fixed(&var) { Some(PropagatorConflict { conjunction: conjunction!([var == 5]), - inference_code, + inference_code: inference_code.clone(), }) } else { None diff --git a/pumpkin-crates/propagators/src/propagators/arithmetic/absolute_value.rs b/pumpkin-crates/propagators/src/propagators/arithmetic/absolute_value.rs index 529bfedfc..eac1df128 100644 --- a/pumpkin-crates/propagators/src/propagators/arithmetic/absolute_value.rs +++ b/pumpkin-crates/propagators/src/propagators/arithmetic/absolute_value.rs @@ -40,7 +40,7 @@ where context.register(signed.clone(), DomainEvents::BOUNDS, LocalId::from(0)); context.register(absolute.clone(), DomainEvents::BOUNDS, LocalId::from(1)); - let inference_code = context.create_inference_code(constraint_tag, AbsoluteValue); + let inference_code = InferenceCode::new(constraint_tag, AbsoluteValue); AbsoluteValuePropagator { signed, @@ -80,7 +80,7 @@ where context.post( predicate![self.absolute >= 0], conjunction!(), - self.inference_code, + &self.inference_code, )?; // Propagating absolute value can be broken into a few cases: @@ -98,20 +98,20 @@ where context.post( predicate![self.absolute <= signed_absolute_ub], conjunction!([self.signed >= signed_lb] & [self.signed <= signed_ub]), - self.inference_code, + &self.inference_code, )?; if signed_lb > 0 { context.post( predicate![self.absolute >= signed_lb], conjunction!([self.signed >= signed_lb]), - self.inference_code, + &self.inference_code, )?; } else if signed_ub < 0 { context.post( predicate![self.absolute >= signed_ub.abs()], conjunction!([self.signed <= signed_ub]), - self.inference_code, + &self.inference_code, )?; } @@ -120,25 +120,25 @@ where context.post( predicate![self.signed >= -absolute_ub], conjunction!([self.absolute <= absolute_ub]), - self.inference_code, + &self.inference_code, )?; context.post( predicate![self.signed <= absolute_ub], conjunction!([self.absolute <= absolute_ub]), - self.inference_code, + &self.inference_code, )?; if signed_ub <= 0 { context.post( predicate![self.signed <= -absolute_lb], conjunction!([self.signed <= 0] & [self.absolute >= absolute_lb]), - self.inference_code, + &self.inference_code, )?; } else if signed_lb >= 0 { context.post( predicate![self.signed >= absolute_lb], conjunction!([self.signed >= 0] & [self.absolute >= absolute_lb]), - self.inference_code, + &self.inference_code, )?; } diff --git a/pumpkin-crates/propagators/src/propagators/arithmetic/binary/binary_equals.rs b/pumpkin-crates/propagators/src/propagators/arithmetic/binary/binary_equals.rs index e3abb42b3..56ee42373 100644 --- a/pumpkin-crates/propagators/src/propagators/arithmetic/binary/binary_equals.rs +++ b/pumpkin-crates/propagators/src/propagators/arithmetic/binary/binary_equals.rs @@ -65,7 +65,7 @@ where a_removed_values: HashSet::default(), b_removed_values: HashSet::default(), - inference_code: context.create_inference_code(constraint_tag, BinaryEquals), + inference_code: InferenceCode::new(constraint_tag, BinaryEquals), has_backtracked: false, first_propagation_loop: true, @@ -141,7 +141,7 @@ where .with_predicate_type(predicate_type) .with_value(value) .into_bits(), - self.inference_code, + &self.inference_code, ) } } @@ -164,7 +164,7 @@ where // Note that we lift the conflict Some(PropagatorConflict { conjunction: conjunction!([self.a <= b_lb - 1] & [self.b >= b_lb]), - inference_code: self.inference_code, + inference_code: self.inference_code.clone(), }) } else if b_ub < a_lb { // If `b` is fully before `a` then we report a conflict @@ -172,7 +172,7 @@ where // Note that we lift the conflict Some(PropagatorConflict { conjunction: conjunction!([self.b <= a_lb - 1] & [self.a >= a_lb]), - inference_code: self.inference_code, + inference_code: self.inference_code.clone(), }) } else { None diff --git a/pumpkin-crates/propagators/src/propagators/arithmetic/binary/binary_not_equals.rs b/pumpkin-crates/propagators/src/propagators/arithmetic/binary/binary_not_equals.rs index c10b6820f..e99bccac9 100644 --- a/pumpkin-crates/propagators/src/propagators/arithmetic/binary/binary_not_equals.rs +++ b/pumpkin-crates/propagators/src/propagators/arithmetic/binary/binary_not_equals.rs @@ -48,7 +48,7 @@ where a, b, - inference_code: context.create_inference_code(constraint_tag, BinaryNotEquals), + inference_code: InferenceCode::new(constraint_tag, BinaryNotEquals), } } } @@ -78,7 +78,7 @@ where // If this is the case then we have detected a conflict Some(PropagatorConflict { conjunction: conjunction!([self.a == lb_a] & [self.b == lb_a]), - inference_code: self.inference_code, + inference_code: self.inference_code.clone(), }) } else { None @@ -117,7 +117,7 @@ where context.post( predicate!(self.b != a_lb), conjunction!([self.a == a_lb]), - self.inference_code, + &self.inference_code, )?; } @@ -126,7 +126,7 @@ where context.post( predicate!(self.a != b_lb), conjunction!([self.b == b_lb]), - self.inference_code, + &self.inference_code, )?; } @@ -152,7 +152,7 @@ where context.post( predicate!(self.b != a_lb), conjunction!([self.a == a_lb]), - self.inference_code, + &self.inference_code, )?; } @@ -160,7 +160,7 @@ where context.post( predicate!(self.a != b_lb), conjunction!([self.b == b_lb]), - self.inference_code, + &self.inference_code, )?; } diff --git a/pumpkin-crates/propagators/src/propagators/arithmetic/integer_division.rs b/pumpkin-crates/propagators/src/propagators/arithmetic/integer_division.rs index 5dd5365c0..4155a532a 100644 --- a/pumpkin-crates/propagators/src/propagators/arithmetic/integer_division.rs +++ b/pumpkin-crates/propagators/src/propagators/arithmetic/integer_division.rs @@ -55,7 +55,7 @@ where context.register(denominator.clone(), DomainEvents::BOUNDS, ID_DENOMINATOR); context.register(rhs.clone(), DomainEvents::BOUNDS, ID_RHS); - let inference_code = context.create_inference_code(constraint_tag, Division); + let inference_code = InferenceCode::new(constraint_tag, Division); DivisionPropagator { numerator, @@ -100,7 +100,7 @@ where &self.numerator, &self.denominator, &self.rhs, - self.inference_code, + &self.inference_code, ) } } @@ -110,7 +110,7 @@ fn perform_propagation PropagationStatusCP { if context.lower_bound(denominator) < 0 && context.upper_bound(denominator) > 0 { // For now we don't do anything in this case, note that this will not lead to incorrect @@ -194,7 +194,7 @@ fn propagate_positive_domains PropagationStatusCP { let rhs_min = context.lower_bound(rhs); let rhs_max = context.upper_bound(rhs); @@ -284,7 +284,7 @@ fn propagate_upper_bounds PropagationStatusCP { let rhs_max = context.upper_bound(rhs); let numerator_max = context.upper_bound(numerator); @@ -330,7 +330,7 @@ fn propagate_signs PropagationStatusCP { let rhs_min = context.lower_bound(rhs); let rhs_max = context.upper_bound(rhs); diff --git a/pumpkin-crates/propagators/src/propagators/arithmetic/integer_multiplication.rs b/pumpkin-crates/propagators/src/propagators/arithmetic/integer_multiplication.rs index cfa34f2c4..d6cb8a456 100644 --- a/pumpkin-crates/propagators/src/propagators/arithmetic/integer_multiplication.rs +++ b/pumpkin-crates/propagators/src/propagators/arithmetic/integer_multiplication.rs @@ -51,7 +51,7 @@ where a, b, c, - inference_code: context.create_inference_code(constraint_tag, IntegerMultiplication), + inference_code: InferenceCode::new(constraint_tag, IntegerMultiplication), } } } @@ -87,7 +87,7 @@ where } fn propagate_from_scratch(&self, context: PropagationContext) -> PropagationStatusCP { - perform_propagation(context, &self.a, &self.b, &self.c, self.inference_code) + perform_propagation(context, &self.a, &self.b, &self.c, &self.inference_code) } } @@ -96,7 +96,7 @@ fn perform_propagation PropagationStatusCP { // First we propagate the signs propagate_signs(&mut context, a, b, c, inference_code)?; @@ -184,7 +184,7 @@ fn perform_propagation PropagationStatusCP { let a_min = context.lower_bound(a); let a_max = context.upper_bound(a); diff --git a/pumpkin-crates/propagators/src/propagators/arithmetic/linear_less_or_equal.rs b/pumpkin-crates/propagators/src/propagators/arithmetic/linear_less_or_equal.rs index d42f37866..b2537cc02 100644 --- a/pumpkin-crates/propagators/src/propagators/arithmetic/linear_less_or_equal.rs +++ b/pumpkin-crates/propagators/src/propagators/arithmetic/linear_less_or_equal.rs @@ -66,7 +66,7 @@ where c, lower_bound_left_hand_side, current_bounds: current_bounds.into(), - inference_code: context.create_inference_code(constraint_tag, LinearBounds), + inference_code: InferenceCode::new(constraint_tag, LinearBounds), reason_buffer: Vec::default(), } } @@ -99,7 +99,7 @@ where .iter() .map(|var| predicate![var >= context.lower_bound(var)]) .collect(), - inference_code: self.inference_code, + inference_code: self.inference_code.clone(), } } } @@ -206,7 +206,7 @@ where let bound = self.c - (lower_bound_left_hand_side - context.lower_bound(x_i)); if context.upper_bound(x_i) > bound { - context.post(predicate![x_i <= bound], i, self.inference_code)?; + context.post(predicate![x_i <= bound], i, &self.inference_code)?; } } @@ -263,7 +263,7 @@ where }) .collect(); - context.post(predicate![x_i <= bound], reason, self.inference_code)?; + context.post(predicate![x_i <= bound], reason, &self.inference_code)?; } } diff --git a/pumpkin-crates/propagators/src/propagators/arithmetic/linear_not_equal.rs b/pumpkin-crates/propagators/src/propagators/arithmetic/linear_not_equal.rs index 522a97ae4..3b01e76d2 100644 --- a/pumpkin-crates/propagators/src/propagators/arithmetic/linear_not_equal.rs +++ b/pumpkin-crates/propagators/src/propagators/arithmetic/linear_not_equal.rs @@ -67,7 +67,7 @@ where fixed_lhs: 0, unfixed_variable_has_been_updated: false, should_recalculate_lhs: false, - inference_code: context.create_inference_code(constraint_tag, LinearNotEquals), + inference_code: InferenceCode::new(constraint_tag, LinearNotEquals), }; propagator.recalculate_fixed_variables(context.domains()); @@ -208,7 +208,7 @@ where .filter(|&(i, _)| i != unfixed_x_i) .map(|(_, x_i)| predicate![x_i == context.lower_bound(x_i)]) .collect::(), - self.inference_code, + &self.inference_code, )?; } } else if self.number_of_fixed_terms == self.terms.len() { @@ -266,7 +266,7 @@ where .expect("Expected to be able to fit i64 into i32") ], reason, - self.inference_code, + &self.inference_code, )?; } else if num_fixed == self.terms.len() && lhs == self.rhs as i64 { let conjunction = self @@ -277,7 +277,7 @@ where return Err(PropagatorConflict { conjunction, - inference_code: self.inference_code, + inference_code: self.inference_code.clone(), } .into()); } @@ -322,7 +322,7 @@ impl LinearNotEqualPropagator { return Err(PropagatorConflict { conjunction, - inference_code: self.inference_code, + inference_code: self.inference_code.clone(), }); } Ok(()) diff --git a/pumpkin-crates/propagators/src/propagators/arithmetic/maximum.rs b/pumpkin-crates/propagators/src/propagators/arithmetic/maximum.rs index 33d3ff38a..22c8b86a1 100644 --- a/pumpkin-crates/propagators/src/propagators/arithmetic/maximum.rs +++ b/pumpkin-crates/propagators/src/propagators/arithmetic/maximum.rs @@ -48,7 +48,7 @@ where LocalId::from(array.len() as u32), ); - let inference_code = context.create_inference_code(constraint_tag, Maximum); + let inference_code = InferenceCode::new(constraint_tag, Maximum); MaximumPropagator { array, @@ -92,7 +92,7 @@ impl Prop context.post( predicate![var <= rhs_ub], conjunction!([self.rhs <= rhs_ub]), - self.inference_code, + &self.inference_code, )?; let var_lb = context.lower_bound(var); @@ -112,7 +112,7 @@ impl Prop context.post( predicate![self.rhs >= max_lb], PropositionalConjunction::from(lb_reason), - self.inference_code, + &self.inference_code, )?; // Rule 3. @@ -128,7 +128,7 @@ impl Prop context.post( predicate![self.rhs <= max_ub], ub_reason, - self.inference_code, + &self.inference_code, )?; } @@ -161,7 +161,7 @@ impl Prop context.post( predicate![propagating_variable >= rhs_lb], propagation_reason, - self.inference_code, + &self.inference_code, )?; } } diff --git a/pumpkin-crates/propagators/src/propagators/cumulative/time_table/explanations/pointwise.rs b/pumpkin-crates/propagators/src/propagators/cumulative/time_table/explanations/pointwise.rs index cdaec708b..b3e97857f 100644 --- a/pumpkin-crates/propagators/src/propagators/cumulative/time_table/explanations/pointwise.rs +++ b/pumpkin-crates/propagators/src/propagators/cumulative/time_table/explanations/pointwise.rs @@ -21,7 +21,7 @@ pub(crate) fn propagate_lower_bounds_with_pointwise_explanations], propagating_task: &Rc>, - inference_code: InferenceCode, + inference_code: &InferenceCode, ) -> Result<(), EmptyDomainConflict> { // The time points should follow the following properties (based on `Improving // scheduling by learning - Andreas Schutt`): @@ -130,7 +130,7 @@ pub(crate) fn propagate_upper_bounds_with_pointwise_explanations], propagating_task: &Rc>, - inference_code: InferenceCode, + inference_code: &InferenceCode, ) -> Result<(), EmptyDomainConflict> { // The time points should follow the following properties (based on `Improving // scheduling by learning - Andreas Schutt`): diff --git a/pumpkin-crates/propagators/src/propagators/cumulative/time_table/over_interval_incremental_propagator/debug.rs b/pumpkin-crates/propagators/src/propagators/cumulative/time_table/over_interval_incremental_propagator/debug.rs index 84f9ed367..29e51c434 100644 --- a/pumpkin-crates/propagators/src/propagators/cumulative/time_table/over_interval_incremental_propagator/debug.rs +++ b/pumpkin-crates/propagators/src/propagators/cumulative/time_table/over_interval_incremental_propagator/debug.rs @@ -27,7 +27,7 @@ pub(crate) fn time_tables_are_the_same_interval< const SYNCHRONISE: bool, >( mut context: Domains, - inference_code: InferenceCode, + inference_code: &InferenceCode, time_table: &OverIntervalTimeTableType, parameters: &CumulativeParameters, ) -> bool { diff --git a/pumpkin-crates/propagators/src/propagators/cumulative/time_table/over_interval_incremental_propagator/synchronisation.rs b/pumpkin-crates/propagators/src/propagators/cumulative/time_table/over_interval_incremental_propagator/synchronisation.rs index 3b53051b3..63002e4dc 100644 --- a/pumpkin-crates/propagators/src/propagators/cumulative/time_table/over_interval_incremental_propagator/synchronisation.rs +++ b/pumpkin-crates/propagators/src/propagators/cumulative/time_table/over_interval_incremental_propagator/synchronisation.rs @@ -58,7 +58,7 @@ pub(crate) fn check_synchronisation_conflict_explanation_over_interval< synchronised_conflict_explanation: &PropagationStatusCP, context: Domains, parameters: &CumulativeParameters, - inference_code: InferenceCode, + inference_code: &InferenceCode, ) -> bool { let error_from_scratch = create_time_table_over_interval_from_scratch(context, parameters, inference_code); @@ -81,7 +81,7 @@ pub(crate) fn check_synchronisation_conflict_explanation_over_interval< /// included in the profile and sorting them in the same order. pub(crate) fn create_synchronised_conflict_explanation( mut context: Domains, - inference_code: InferenceCode, + inference_code: &InferenceCode, conflicting_profile: &mut ResourceProfile, parameters: &CumulativeParameters, ) -> PropagationStatusCP { diff --git a/pumpkin-crates/propagators/src/propagators/cumulative/time_table/over_interval_incremental_propagator/time_table_over_interval_incremental.rs b/pumpkin-crates/propagators/src/propagators/cumulative/time_table/over_interval_incremental_propagator/time_table_over_interval_incremental.rs index a426e0a16..fc6ff7f9c 100644 --- a/pumpkin-crates/propagators/src/propagators/cumulative/time_table/over_interval_incremental_propagator/time_table_over_interval_incremental.rs +++ b/pumpkin-crates/propagators/src/propagators/cumulative/time_table/over_interval_incremental_propagator/time_table_over_interval_incremental.rs @@ -123,7 +123,7 @@ impl PropagatorConstruc self.is_time_table_outdated = true; - self.inference_code = Some(context.create_inference_code(self.constraint_tag, TimeTable)); + self.inference_code = Some(InferenceCode::new(self.constraint_tag, TimeTable)); self } @@ -182,7 +182,7 @@ impl { conflict = Some(Err(create_conflict_explanation( context.reborrow(), - self.inference_code.unwrap(), + self.inference_code.as_ref().unwrap(), &conflict_tasks, self.parameters.options.explanation_type, ) @@ -243,7 +243,7 @@ impl self.time_table = create_time_table_over_interval_from_scratch( context.domains(), &self.parameters, - self.inference_code.unwrap(), + self.inference_code.as_ref().unwrap(), )?; // Then we note that the time-table is not outdated anymore @@ -304,7 +304,7 @@ impl let synchronised_conflict_explanation = create_synchronised_conflict_explanation( context.domains(), - self.inference_code.unwrap(), + self.inference_code.as_ref().unwrap(), &mut conflicting_profile, &self.parameters, ); @@ -313,7 +313,7 @@ impl &synchronised_conflict_explanation, context.domains(), &self.parameters, - self.inference_code.unwrap(), + self.inference_code.as_ref().unwrap(), ), "The conflict explanation was not the same as the conflict explanation from scratch!" ); @@ -335,7 +335,7 @@ impl create_time_table_over_interval_from_scratch( context.domains(), &self.parameters, - self.inference_code.unwrap(), + self.inference_code.as_ref().unwrap(), ) .is_err(), "Time-table from scratch could not find conflict" @@ -345,7 +345,7 @@ impl return Err(create_conflict_explanation( context.domains(), - self.inference_code.unwrap(), + self.inference_code.as_ref().unwrap(), conflicting_profile, self.parameters.options.explanation_type, ) @@ -390,7 +390,7 @@ impl Propagator if self.parameters.is_infeasible { return Err(Conflict::Propagator(PropagatorConflict { conjunction: conjunction!(), - inference_code: self.inference_code.unwrap(), + inference_code: self.inference_code.clone().unwrap(), })); } @@ -399,7 +399,7 @@ impl Propagator pumpkin_assert_extreme!( debug::time_tables_are_the_same_interval::( context.domains(), - self.inference_code.unwrap(), + self.inference_code.as_ref().unwrap(), &self.time_table, &self.parameters, ), @@ -412,7 +412,7 @@ impl Propagator // could cause another propagation by a profile which has not been updated propagate_based_on_timetable( &mut context, - self.inference_code.unwrap(), + self.inference_code.as_ref().unwrap(), self.time_table.iter(), &self.parameters, &mut self.updatable_structures, @@ -523,7 +523,7 @@ impl Propagator &mut context, &self.parameters, &self.updatable_structures, - self.inference_code.unwrap(), + self.inference_code.as_ref().unwrap(), ) } } diff --git a/pumpkin-crates/propagators/src/propagators/cumulative/time_table/per_point_incremental_propagator/synchronisation.rs b/pumpkin-crates/propagators/src/propagators/cumulative/time_table/per_point_incremental_propagator/synchronisation.rs index 320abd00b..8d1a0e978 100644 --- a/pumpkin-crates/propagators/src/propagators/cumulative/time_table/per_point_incremental_propagator/synchronisation.rs +++ b/pumpkin-crates/propagators/src/propagators/cumulative/time_table/per_point_incremental_propagator/synchronisation.rs @@ -22,7 +22,7 @@ pub(crate) fn check_synchronisation_conflict_explanation_per_point< >( synchronised_conflict_explanation: &PropagationStatusCP, context: Domains, - inference_code: InferenceCode, + inference_code: &InferenceCode, parameters: &CumulativeParameters, ) -> bool { let error_from_scratch = @@ -112,7 +112,7 @@ fn get_minimum_set_of_tasks_which_overflow_capacity<'a, Var: IntegerVariable + ' /// profile and sorting them in the same order. pub(crate) fn create_synchronised_conflict_explanation( context: Domains, - inference_code: InferenceCode, + inference_code: &InferenceCode, conflicting_profile: &mut ResourceProfile, parameters: &CumulativeParameters, ) -> PropagationStatusCP { diff --git a/pumpkin-crates/propagators/src/propagators/cumulative/time_table/per_point_incremental_propagator/time_table_per_point_incremental.rs b/pumpkin-crates/propagators/src/propagators/cumulative/time_table/per_point_incremental_propagator/time_table_per_point_incremental.rs index ae860f402..db3a00614 100644 --- a/pumpkin-crates/propagators/src/propagators/cumulative/time_table/per_point_incremental_propagator/time_table_per_point_incremental.rs +++ b/pumpkin-crates/propagators/src/propagators/cumulative/time_table/per_point_incremental_propagator/time_table_per_point_incremental.rs @@ -113,7 +113,7 @@ impl Propagator // Then we do normal propagation self.is_time_table_outdated = true; - self.inference_code = Some(context.create_inference_code(self.constraint_tag, TimeTable)); + self.inference_code = Some(InferenceCode::new(self.constraint_tag, TimeTable)); self } @@ -183,7 +183,7 @@ impl // The newly introduced mandatory part(s) caused an overflow of the resource conflict = Some(Err(create_conflict_explanation( context.reborrow(), - self.inference_code.unwrap(), + self.inference_code.as_ref().unwrap(), current_profile, self.parameters.options.explanation_type, ) @@ -260,7 +260,7 @@ impl // We create the time-table from scratch (and return an error if it overflows) self.time_table = create_time_table_per_point_from_scratch( context.domains(), - self.inference_code.unwrap(), + self.inference_code.as_ref().unwrap(), &self.parameters, )?; @@ -328,7 +328,7 @@ impl let synchronised_conflict_explanation = create_synchronised_conflict_explanation( context.domains(), - self.inference_code.unwrap(), + self.inference_code.as_ref().unwrap(), conflicting_profile, &self.parameters, ); @@ -337,7 +337,7 @@ impl check_synchronisation_conflict_explanation_per_point( &synchronised_conflict_explanation, context.domains(), - self.inference_code.unwrap(), + self.inference_code.as_ref().unwrap(), &self.parameters, ), "The conflict explanation was not the same as the conflict explanation from scratch!" @@ -363,7 +363,7 @@ impl pumpkin_assert_extreme!( create_time_table_per_point_from_scratch( context.domains(), - self.inference_code.unwrap(), + self.inference_code.as_ref().unwrap(), &self.parameters ) .is_err(), @@ -374,7 +374,7 @@ impl return Err(create_conflict_explanation( context.domains(), - self.inference_code.unwrap(), + self.inference_code.as_ref().unwrap(), conflicting_profile, self.parameters.options.explanation_type, ) @@ -419,7 +419,7 @@ impl Propagator if self.parameters.is_infeasible { return Err(Conflict::Propagator(PropagatorConflict { conjunction: conjunction!(), - inference_code: self.inference_code.unwrap(), + inference_code: self.inference_code.clone().unwrap(), })); } @@ -428,7 +428,7 @@ impl Propagator pumpkin_assert_extreme!(debug::time_tables_are_the_same_point::( context.domains(), - self.inference_code.unwrap(), + self.inference_code.as_ref().unwrap(), &self.time_table, &self.parameters )); @@ -439,7 +439,7 @@ impl Propagator // could cause another propagation by a profile which has not been updated propagate_based_on_timetable( &mut context, - self.inference_code.unwrap(), + self.inference_code.as_ref().unwrap(), self.time_table.values(), &self.parameters, &mut self.updatable_structures, @@ -546,7 +546,7 @@ impl Propagator // Use the same debug propagator from `TimeTablePerPoint` propagate_from_scratch_time_table_point( &mut context, - self.inference_code.unwrap(), + self.inference_code.as_ref().unwrap(), &self.parameters, &self.updatable_structures, ) @@ -577,7 +577,7 @@ mod debug { const SYNCHRONISE: bool, >( context: Domains, - inference_code: InferenceCode, + inference_code: &InferenceCode, time_table: &PerPointTimeTableType, parameters: &CumulativeParameters, ) -> bool { diff --git a/pumpkin-crates/propagators/src/propagators/cumulative/time_table/propagation_handler.rs b/pumpkin-crates/propagators/src/propagators/cumulative/time_table/propagation_handler.rs index d1da49e9e..61fcc0203 100644 --- a/pumpkin-crates/propagators/src/propagators/cumulative/time_table/propagation_handler.rs +++ b/pumpkin-crates/propagators/src/propagators/cumulative/time_table/propagation_handler.rs @@ -132,14 +132,14 @@ impl CumulativePropagationHandler { &full_explanation, context.domains() )); - context.post(predicate, full_explanation, self.inference_code) + context.post(predicate, full_explanation, &self.inference_code) } CumulativeExplanationType::Pointwise => { pointwise::propagate_lower_bounds_with_pointwise_explanations( context, profiles, propagating_task, - self.inference_code, + &self.inference_code, ) } } @@ -198,14 +198,14 @@ impl CumulativePropagationHandler { &full_explanation, context.domains() )); - context.post(predicate, full_explanation, self.inference_code) + context.post(predicate, full_explanation, &self.inference_code) } CumulativeExplanationType::Pointwise => { pointwise::propagate_upper_bounds_with_pointwise_explanations( context, profiles, propagating_task, - self.inference_code, + &self.inference_code, ) } } @@ -249,14 +249,14 @@ impl CumulativePropagationHandler { let mut reason = (*explanation).clone(); reason.push(lower_bound_predicate_propagating_task); - context.post(predicate, reason, self.inference_code) + context.post(predicate, reason, &self.inference_code) } CumulativeExplanationType::Pointwise => { pointwise::propagate_lower_bounds_with_pointwise_explanations( context, &[profile], propagating_task, - self.inference_code, + &self.inference_code, ) } } @@ -304,14 +304,14 @@ impl CumulativePropagationHandler { let mut reason = (*explanation).clone(); reason.push(upper_bound_predicate_propagating_task); - context.post(predicate, reason, self.inference_code) + context.post(predicate, reason, &self.inference_code) } CumulativeExplanationType::Pointwise => { pointwise::propagate_upper_bounds_with_pointwise_explanations( context, &[profile], propagating_task, - self.inference_code, + &self.inference_code, ) } } @@ -370,7 +370,7 @@ impl CumulativePropagationHandler { &explanation, context.domains() )); - context.post(predicate, (*explanation).clone(), self.inference_code)?; + context.post(predicate, (*explanation).clone(), &self.inference_code)?; } CumulativeExplanationType::Pointwise => { // We split into two cases when determining the explanation of the profile @@ -403,7 +403,7 @@ impl CumulativePropagationHandler { &explanation, context.domains() )); - context.post(predicate, explanation, self.inference_code)?; + context.post(predicate, explanation, &self.inference_code)?; } } } @@ -448,7 +448,7 @@ impl CumulativePropagationHandler { /// `explanation_type`. pub(crate) fn create_conflict_explanation( context: Context, - inference_code: InferenceCode, + inference_code: &InferenceCode, conflict_profile: &ResourceProfile, explanation_type: CumulativeExplanationType, ) -> PropagatorConflict @@ -469,7 +469,7 @@ where PropagatorConflict { conjunction, - inference_code, + inference_code: inference_code.clone(), } } @@ -481,6 +481,7 @@ pub(crate) mod test_propagation_handler { use pumpkin_core::predicate; use pumpkin_core::predicates::Predicate; use pumpkin_core::predicates::PropositionalConjunction; + use pumpkin_core::proof::ConstraintTag; use pumpkin_core::proof::InferenceCode; use pumpkin_core::propagation::LocalId; use pumpkin_core::state::CurrentNogood; @@ -503,7 +504,7 @@ pub(crate) mod test_propagation_handler { pub(crate) fn new(explanation_type: CumulativeExplanationType) -> Self { let propagation_handler = CumulativePropagationHandler::new( explanation_type, - InferenceCode::create_from_index(0), + InferenceCode::unknown_label(ConstraintTag::create_from_index(0)), ); let state = State::default(); @@ -533,7 +534,7 @@ pub(crate) mod test_propagation_handler { let reason = create_conflict_explanation( self.state.get_domains(), - self.propagation_handler.inference_code, + &self.propagation_handler.inference_code, &profile, self.propagation_handler.explanation_type, ); diff --git a/pumpkin-crates/propagators/src/propagators/cumulative/time_table/time_table_over_interval.rs b/pumpkin-crates/propagators/src/propagators/cumulative/time_table/time_table_over_interval.rs index fdbdb14cc..6bdf00a08 100644 --- a/pumpkin-crates/propagators/src/propagators/cumulative/time_table/time_table_over_interval.rs +++ b/pumpkin-crates/propagators/src/propagators/cumulative/time_table/time_table_over_interval.rs @@ -112,7 +112,7 @@ impl PropagatorConstructor .initialise_bounds_and_remove_fixed(context.domains(), &self.parameters); register_tasks(&self.parameters.tasks, context.reborrow(), false); - self.inference_code = Some(context.create_inference_code(self.constraint_tag, TimeTable)); + self.inference_code = Some(InferenceCode::new(self.constraint_tag, TimeTable)); self } @@ -123,21 +123,21 @@ impl Propagator for TimeTableOverIntervalPropaga if self.parameters.is_infeasible { return Err(Conflict::Propagator(PropagatorConflict { conjunction: conjunction!(), - inference_code: self.inference_code.unwrap(), + inference_code: self.inference_code.clone().unwrap(), })); } let time_table = create_time_table_over_interval_from_scratch( context.domains(), &self.parameters, - self.inference_code.unwrap(), + self.inference_code.as_ref().unwrap(), )?; self.is_time_table_empty = time_table.is_empty(); // No error has been found -> Check for updates (i.e. go over all profiles and all tasks and // check whether an update can take place) propagate_based_on_timetable( &mut context, - self.inference_code.unwrap(), + self.inference_code.as_ref().unwrap(), time_table.iter(), &self.parameters, &mut self.updatable_structures, @@ -198,7 +198,7 @@ impl Propagator for TimeTableOverIntervalPropaga &mut context, &self.parameters, &self.updatable_structures, - self.inference_code.unwrap(), + self.inference_code.as_ref().unwrap(), ) } } @@ -216,7 +216,7 @@ impl Propagator for TimeTableOverIntervalPropaga pub(crate) fn create_time_table_over_interval_from_scratch( mut context: Domains, parameters: &CumulativeParameters, - inference_code: InferenceCode, + inference_code: &InferenceCode, ) -> Result, PropagatorConflict> { // First we create a list of all the events (i.e. start and ends of mandatory parts) let events = create_events(context.reborrow(), parameters); @@ -295,7 +295,7 @@ fn create_events( fn create_time_table_from_events( events: Vec>, context: Context, - inference_code: InferenceCode, + inference_code: &InferenceCode, parameters: &CumulativeParameters, ) -> Result, PropagatorConflict> { pumpkin_assert_extreme!( @@ -449,7 +449,7 @@ pub(crate) fn propagate_from_scratch_time_table_interval, updatable_structures: &UpdatableStructures, - inference_code: InferenceCode, + inference_code: &InferenceCode, ) -> PropagationStatusCP { // We first create a time-table over interval and return an error if there was // an overflow of the resource capacity while building the time-table diff --git a/pumpkin-crates/propagators/src/propagators/cumulative/time_table/time_table_per_point.rs b/pumpkin-crates/propagators/src/propagators/cumulative/time_table/time_table_per_point.rs index 30890b922..1edfda6be 100644 --- a/pumpkin-crates/propagators/src/propagators/cumulative/time_table/time_table_per_point.rs +++ b/pumpkin-crates/propagators/src/propagators/cumulative/time_table/time_table_per_point.rs @@ -103,7 +103,7 @@ impl PropagatorConstructor for TimeTablePerPoint .initialise_bounds_and_remove_fixed(context.domains(), &self.parameters); register_tasks(&self.parameters.tasks, context.reborrow(), false); - self.inference_code = Some(context.create_inference_code(self.constraint_tag, TimeTable)); + self.inference_code = Some(InferenceCode::new(self.constraint_tag, TimeTable)); self } @@ -114,13 +114,13 @@ impl Propagator for TimeTablePerPointPropagator< if self.parameters.is_infeasible { return Err(Conflict::Propagator(PropagatorConflict { conjunction: conjunction!(), - inference_code: self.inference_code.unwrap(), + inference_code: self.inference_code.clone().unwrap(), })); } let time_table = create_time_table_per_point_from_scratch( context.domains(), - self.inference_code.unwrap(), + self.inference_code.as_ref().unwrap(), &self.parameters, )?; self.is_time_table_empty = time_table.is_empty(); @@ -128,7 +128,7 @@ impl Propagator for TimeTablePerPointPropagator< // check whether an update can take place) propagate_based_on_timetable( &mut context, - self.inference_code.unwrap(), + self.inference_code.as_ref().unwrap(), time_table.values(), &self.parameters, &mut self.updatable_structures, @@ -189,7 +189,7 @@ impl Propagator for TimeTablePerPointPropagator< fn propagate_from_scratch(&self, mut context: PropagationContext) -> PropagationStatusCP { propagate_from_scratch_time_table_point( &mut context, - self.inference_code.unwrap(), + self.inference_code.as_ref().unwrap(), &self.parameters, &self.updatable_structures, ) @@ -209,7 +209,7 @@ pub(crate) fn create_time_table_per_point_from_scratch< Context: ReadDomains, >( context: Context, - inference_code: InferenceCode, + inference_code: &InferenceCode, parameters: &CumulativeParameters, ) -> Result, PropagatorConflict> { let mut time_table: PerPointTimeTableType = PerPointTimeTableType::new(); @@ -254,7 +254,7 @@ pub(crate) fn create_time_table_per_point_from_scratch< pub(crate) fn propagate_from_scratch_time_table_point( context: &mut PropagationContext, - inference_code: InferenceCode, + inference_code: &InferenceCode, parameters: &CumulativeParameters, updatable_structures: &UpdatableStructures, ) -> PropagationStatusCP { diff --git a/pumpkin-crates/propagators/src/propagators/cumulative/time_table/time_table_util.rs b/pumpkin-crates/propagators/src/propagators/cumulative/time_table/time_table_util.rs index 3f387fc9b..7da67427b 100644 --- a/pumpkin-crates/propagators/src/propagators/cumulative/time_table/time_table_util.rs +++ b/pumpkin-crates/propagators/src/propagators/cumulative/time_table/time_table_util.rs @@ -200,7 +200,7 @@ fn debug_check_whether_profiles_are_maximal_and_sorted<'a, Var: IntegerVariable /// cannot be increased or decreased, respectively). pub(crate) fn propagate_based_on_timetable<'a, Var: IntegerVariable + 'static>( context: &mut PropagationContext, - inference_code: InferenceCode, + inference_code: &InferenceCode, time_table: impl Iterator> + Clone, parameters: &CumulativeParameters, updatable_structures: &mut UpdatableStructures, @@ -255,14 +255,16 @@ pub(crate) fn propagate_based_on_timetable<'a, Var: IntegerVariable + 'static>( /// [`CumulativeExplanationType::Pointwise`]. fn propagate_single_profiles<'a, Var: IntegerVariable + 'static>( context: &mut PropagationContext, - inference_code: InferenceCode, + inference_code: &InferenceCode, time_table: impl Iterator> + Clone, updatable_structures: &mut UpdatableStructures, parameters: &CumulativeParameters, ) -> PropagationStatusCP { // We create the structure responsible for propagations and explanations - let mut propagation_handler = - CumulativePropagationHandler::new(parameters.options.explanation_type, inference_code); + let mut propagation_handler = CumulativePropagationHandler::new( + parameters.options.explanation_type, + inference_code.clone(), + ); // Then we go over all of the profiles in the time-table 'profile_loop: for profile in time_table { @@ -358,7 +360,7 @@ fn propagate_single_profiles<'a, Var: IntegerVariable + 'static>( /// beneficial. fn propagate_sequence_of_profiles<'a, Var: IntegerVariable + 'static>( context: &mut PropagationContext, - inference_code: InferenceCode, + inference_code: &InferenceCode, time_table: impl Iterator> + Clone, updatable_structures: &mut UpdatableStructures, parameters: &CumulativeParameters, @@ -373,8 +375,10 @@ fn propagate_sequence_of_profiles<'a, Var: IntegerVariable + 'static>( } // We create the structure responsible for propagations and explanations - let mut propagation_handler = - CumulativePropagationHandler::new(parameters.options.explanation_type, inference_code); + let mut propagation_handler = CumulativePropagationHandler::new( + parameters.options.explanation_type, + inference_code.clone(), + ); // Then we go over all the possible tasks for task in updatable_structures.get_unfixed_tasks() { diff --git a/pumpkin-crates/propagators/src/propagators/disjunctive/disjunctive_propagator.rs b/pumpkin-crates/propagators/src/propagators/disjunctive/disjunctive_propagator.rs index 27519eaa1..b34cf84fa 100644 --- a/pumpkin-crates/propagators/src/propagators/disjunctive/disjunctive_propagator.rs +++ b/pumpkin-crates/propagators/src/propagators/disjunctive/disjunctive_propagator.rs @@ -91,8 +91,7 @@ impl PropagatorConstructor for DisjunctiveConstr .collect::>(); let theta_lambda_tree = ThetaLambdaTree::new(&tasks); - let inference_code = - context.create_inference_code(self.constraint_tag, DisjunctiveEdgeFinding); + let inference_code = InferenceCode::new(self.constraint_tag, DisjunctiveEdgeFinding); tasks.iter().for_each(|task| { context.register(task.start_time.clone(), DomainEvents::BOUNDS, task.id); @@ -119,7 +118,7 @@ impl Propagator for DisjunctivePropagator { &mut context, &self.tasks, &mut self.sorted_tasks, - self.inference_code, + &self.inference_code, ) } @@ -131,7 +130,7 @@ impl Propagator for DisjunctivePropagator { &mut context, &self.tasks, &mut sorted_tasks, - self.inference_code, + &self.inference_code, ) } } @@ -143,7 +142,7 @@ fn edge_finding( context: &mut PropagationContext, tasks: &[DisjunctiveTask], sorted_tasks: &mut [DisjunctiveTask], - inference_code: InferenceCode, + inference_code: &InferenceCode, ) -> PropagationStatusCP { // First we create our Theta-Lambda tree and add all of the tasks to Theta (Lambda is empty at // this point) @@ -169,7 +168,7 @@ fn edge_finding( if theta_lambda_tree.ect() > lct_j { return Err(Conflict::Propagator(PropagatorConflict { conjunction: create_conflict_explanation(theta_lambda_tree, context, lct_j), - inference_code, + inference_code: inference_code.clone(), })); } diff --git a/pumpkin-crates/propagators/src/propagators/element.rs b/pumpkin-crates/propagators/src/propagators/element.rs index 1f5e84838..074726a5b 100644 --- a/pumpkin-crates/propagators/src/propagators/element.rs +++ b/pumpkin-crates/propagators/src/propagators/element.rs @@ -59,7 +59,7 @@ where context.register(index.clone(), DomainEvents::ANY_INT, ID_INDEX); context.register(rhs.clone(), DomainEvents::ANY_INT, ID_RHS); - let inference_code = context.create_inference_code(constraint_tag, Element); + let inference_code = InferenceCode::new(constraint_tag, Element); ElementPropagator { array, @@ -158,12 +158,12 @@ where context.post( predicate![self.index >= 0], conjunction!(), - self.inference_code, + &self.inference_code, )?; context.post( predicate![self.index <= self.array.len() as i32 - 1], conjunction!(), - self.inference_code, + &self.inference_code, )?; Ok(()) } @@ -194,7 +194,7 @@ where .with_value(rhs_lb) .into_bits(), ), - self.inference_code, + &self.inference_code, )?; context.post( predicate![self.rhs <= rhs_ub], @@ -204,7 +204,7 @@ where .with_value(rhs_ub) .into_bits(), ), - self.inference_code, + &self.inference_code, )?; Ok(()) @@ -237,7 +237,7 @@ where } for (idx, reason) in to_remove.drain(..) { - context.post(predicate![self.index != idx], reason, self.inference_code)?; + context.post(predicate![self.index != idx], reason, &self.inference_code)?; } Ok(()) @@ -257,12 +257,12 @@ where context.post( predicate![lhs >= rhs_lb], conjunction!([self.rhs >= rhs_lb] & [self.index == index]), - self.inference_code, + &self.inference_code, )?; context.post( predicate![lhs <= rhs_ub], conjunction!([self.rhs <= rhs_ub] & [self.index == index]), - self.inference_code, + &self.inference_code, )?; Ok(()) } From d7690b03697958d890145c90df4ebd23e87b592e Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Fri, 9 Jan 2026 15:46:39 +0100 Subject: [PATCH 03/48] Remove unused documentation --- pumpkin-crates/core/src/proof/inference_code.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/pumpkin-crates/core/src/proof/inference_code.rs b/pumpkin-crates/core/src/proof/inference_code.rs index a856405ca..c0f75fdc6 100644 --- a/pumpkin-crates/core/src/proof/inference_code.rs +++ b/pumpkin-crates/core/src/proof/inference_code.rs @@ -141,5 +141,4 @@ pub trait InferenceLabel { fn to_str(&self) -> Arc; } -/// An inference label used when no more specific label is known. declare_inference_label!(pub Unknown); From f694a483fe890332475935c2ac0f89fefa6e6222 Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Fri, 9 Jan 2026 16:11:17 +0100 Subject: [PATCH 04/48] Remove redundant method --- pumpkin-crates/core/src/propagation/constructor.rs | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/pumpkin-crates/core/src/propagation/constructor.rs b/pumpkin-crates/core/src/propagation/constructor.rs index 6b070de94..b6e6652c5 100644 --- a/pumpkin-crates/core/src/propagation/constructor.rs +++ b/pumpkin-crates/core/src/propagation/constructor.rs @@ -18,9 +18,6 @@ use crate::engine::variables::AffineView; #[cfg(doc)] use crate::engine::variables::DomainId; use crate::predicates::Predicate; -use crate::proof::ConstraintTag; -use crate::proof::InferenceCode; -use crate::proof::InferenceLabel; #[cfg(doc)] use crate::propagation::DomainEvent; use crate::propagation::DomainEvents; @@ -157,17 +154,6 @@ impl PropagatorConstructorContext<'_> { var.watch_all_backtrack(&mut watchers, domain_events.events()); } - /// Create a new [`InferenceCode`]. These codes are required to identify specific propagations - /// in the solver and the proof. - #[deprecated = "construct inference codes with InferenceCode::new"] - pub fn create_inference_code( - &mut self, - constraint_tag: ConstraintTag, - inference_label: impl InferenceLabel, - ) -> InferenceCode { - InferenceCode::new(constraint_tag, inference_label) - } - /// Get a new [`LocalId`] which is guaranteed to be unused. pub(crate) fn get_next_local_id(&self) -> LocalId { *self.next_local_id.deref() From 5b1b1e47eea8302e5f47738d61b080135f1aa176 Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Fri, 9 Jan 2026 16:27:16 +0100 Subject: [PATCH 05/48] Add the checking library --- Cargo.lock | 4 ++++ Cargo.toml | 14 +++++++++++++- pumpkin-crates/checking/Cargo.toml | 12 ++++++++++++ pumpkin-crates/checking/src/lib.rs | 14 ++++++++++++++ 4 files changed, 43 insertions(+), 1 deletion(-) create mode 100644 pumpkin-crates/checking/Cargo.toml create mode 100644 pumpkin-crates/checking/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index d667a5e7b..f43ab1c2e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -735,6 +735,10 @@ dependencies = [ "thiserror", ] +[[package]] +name = "pumpkin-checking" +version = "0.1.0" + [[package]] name = "pumpkin-constraints" version = "0.2.2" diff --git a/Cargo.toml b/Cargo.toml index 1c1b84625..3077fad18 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,18 @@ [workspace] -members = ["./pumpkin-solver", "./pumpkin-checker", "./drcp-format", "./pumpkin-solver-py", "./pumpkin-macros", "./drcp-debugger", "./pumpkin-crates/*", "./fzn-rs", "./fzn-rs-derive"] resolver = "2" +members = [ + # Libraries used by Pumpkin but in principle are independent + "./drcp-format", + "./drcp-debugger", + "./fzn-rs", + "./fzn-rs-derive", + + "./pumpkin-crates/*", # Core libraries of the solver + "./pumpkin-solver", # The solver binary + "./pumpkin-checker", # The uncertified proof checker + "./pumpkin-solver-py", # The python interface + "./pumpkin-macros", # Proc-macros used by the pumpkin source (unpublished) +] [workspace.package] repository = "https://github.com/consol-lab/pumpkin" diff --git a/pumpkin-crates/checking/Cargo.toml b/pumpkin-crates/checking/Cargo.toml new file mode 100644 index 000000000..354307497 --- /dev/null +++ b/pumpkin-crates/checking/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "pumpkin-checking" +version = "0.1.0" +repository.workspace = true +edition.workspace = true +license.workspace = true +authors.workspace = true + +[dependencies] + +[lints] +workspace = true diff --git a/pumpkin-crates/checking/src/lib.rs b/pumpkin-crates/checking/src/lib.rs new file mode 100644 index 000000000..b93cf3ffd --- /dev/null +++ b/pumpkin-crates/checking/src/lib.rs @@ -0,0 +1,14 @@ +pub fn add(left: u64, right: u64) -> u64 { + left + right +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn it_works() { + let result = add(2, 2); + assert_eq!(result, 4); + } +} From e43680c6df381d25dd368f1df2060aca573a5376 Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Fri, 9 Jan 2026 16:59:20 +0100 Subject: [PATCH 06/48] Run inference checkers in propagation loop --- Cargo.lock | 6 +++- pumpkin-crates/checking/Cargo.toml | 3 +- pumpkin-crates/checking/src/lib.rs | 19 +++++------- pumpkin-crates/core/Cargo.toml | 3 ++ pumpkin-crates/core/src/engine/state.rs | 39 +++++++++++++++++++++++++ 5 files changed, 57 insertions(+), 13 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f43ab1c2e..d2a35142d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -737,7 +737,10 @@ dependencies = [ [[package]] name = "pumpkin-checking" -version = "0.1.0" +version = "0.2.2" +dependencies = [ + "dyn-clone", +] [[package]] name = "pumpkin-constraints" @@ -768,6 +771,7 @@ dependencies = [ "log", "num", "once_cell", + "pumpkin-checking", "pumpkin-constraints", "rand", "thiserror", diff --git a/pumpkin-crates/checking/Cargo.toml b/pumpkin-crates/checking/Cargo.toml index 354307497..ba8c8c976 100644 --- a/pumpkin-crates/checking/Cargo.toml +++ b/pumpkin-crates/checking/Cargo.toml @@ -1,12 +1,13 @@ [package] name = "pumpkin-checking" -version = "0.1.0" +version = "0.2.2" repository.workspace = true edition.workspace = true license.workspace = true authors.workspace = true [dependencies] +dyn-clone = "1.0.20" [lints] workspace = true diff --git a/pumpkin-crates/checking/src/lib.rs b/pumpkin-crates/checking/src/lib.rs index b93cf3ffd..139a99dac 100644 --- a/pumpkin-crates/checking/src/lib.rs +++ b/pumpkin-crates/checking/src/lib.rs @@ -1,14 +1,11 @@ -pub fn add(left: u64, right: u64) -> u64 { - left + right -} +use std::fmt::Debug; -#[cfg(test)] -mod tests { - use super::*; +use dyn_clone::DynClone; +use dyn_clone::clone_trait_object; - #[test] - fn it_works() { - let result = add(2, 2); - assert_eq!(result, 4); - } +pub trait InferenceChecker: Debug + DynClone { + fn check(&self) -> bool; } + +// Allow Box to be cloned +clone_trait_object!(InferenceChecker); diff --git a/pumpkin-crates/core/Cargo.toml b/pumpkin-crates/core/Cargo.toml index 94d304d59..6f0ac629e 100644 --- a/pumpkin-crates/core/Cargo.toml +++ b/pumpkin-crates/core/Cargo.toml @@ -11,6 +11,7 @@ description = "The core of the Pumpkin constraint programming solver." workspace = true [dependencies] +pumpkin-checking = { version = "0.2.2", path = "../checking", optional = true } thiserror = "2.0.12" log = "0.4.17" bitfield = "0.14.0" @@ -30,6 +31,7 @@ indexmap = "2.10.0" dyn-clone = "1.0.20" flate2 = { version = "1.1.2" } + [dev-dependencies] pumpkin-constraints = { version = "0.2.2", path = "../constraints", features=["clap"] } @@ -41,5 +43,6 @@ getrandom = { version = "0.2", features = ["js"] } wasm-bindgen-test = "0.3" [features] +check-propagations = ["dep:pumpkin-checking"] debug-checks = [] clap = ["dep:clap"] diff --git a/pumpkin-crates/core/src/engine/state.rs b/pumpkin-crates/core/src/engine/state.rs index 2e5b6b936..185d48485 100644 --- a/pumpkin-crates/core/src/engine/state.rs +++ b/pumpkin-crates/core/src/engine/state.rs @@ -1,6 +1,11 @@ use std::sync::Arc; +#[cfg(feature = "check-propagations")] +use pumpkin_checking::InferenceChecker; + use crate::basic_types::PropagatorConflict; +#[cfg(feature = "check-propagations")] +use crate::containers::HashMap; use crate::containers::KeyGenerator; use crate::create_statistics_struct; use crate::engine::Assignments; @@ -69,6 +74,9 @@ pub struct State { pub(crate) constraint_tags: KeyGenerator, statistics: StateStatistics, + + #[cfg(feature = "check-propagations")] + checkers: HashMap>, } create_statistics_struct!(StateStatistics { @@ -153,6 +161,8 @@ impl Default for State { notification_engine: NotificationEngine::default(), statistics: StateStatistics::default(), constraint_tags: KeyGenerator::default(), + #[cfg(feature = "check-propagations")] + checkers: HashMap::default(), }; // As a convention, the assignments contain a dummy domain_id=0, which represents a 0-1 // variable that is assigned to one. We use it to represent predicates that are @@ -551,6 +561,8 @@ impl State { propagator.propagate(context) }; + self.check_propagations(num_trail_entries_before); + match propagation_status { Ok(_) => { // Notify other propagators of the propagations and continue. @@ -593,6 +605,33 @@ impl State { Ok(()) } + #[cfg(not(feature = "check-propagations"))] + fn check_propagations(&self, _: usize) { + // If the feature is disabled, nothing happens here. The compiler will remove the method + // call. + } + + /// For every propagation on the trail, run the inference checker for it. + /// + /// If the checker rejects the inference, this method panics. + #[cfg(feature = "check-propagations")] + fn check_propagations(&self, first_propagation_index: usize) { + for trail_index in first_propagation_index..self.assignments.num_trail_entries() { + let entry = self.assignments.get_trail_entry(trail_index); + + let (_, inference_code) = entry + .reason + .expect("propagations should only be checked after propagations"); + + let checker = self + .checkers + .get(&inference_code) + .unwrap_or_else(|| panic!("missing checker for inference code {inference_code:?}")); + + assert!(checker.check()); + } + } + /// Performs fixed-point propagation using the propagators defined in the [`State`]. /// /// The posted [`Predicate`]s (using [`State::post`]) and added propagators (using From 5fc11cfa430a5a0084cdce0ccc319b14b0426f3c Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Sun, 11 Jan 2026 13:30:09 +0100 Subject: [PATCH 07/48] Get the linear bounds inference checker in the new infrastructure --- Cargo.lock | 3 + pumpkin-checker/Cargo.toml | 2 + pumpkin-checker/src/deductions.rs | 30 +- .../src/inferences/all_different.rs | 15 +- pumpkin-checker/src/inferences/arithmetic.rs | 85 +-- pumpkin-checker/src/inferences/linear.rs | 55 +- pumpkin-checker/src/inferences/mod.rs | 27 +- pumpkin-checker/src/inferences/nogood.rs | 15 +- pumpkin-checker/src/inferences/time_table.rs | 15 +- pumpkin-checker/src/lib.rs | 26 +- pumpkin-checker/src/model.rs | 245 +++++++- pumpkin-checker/src/state.rs | 514 --------------- pumpkin-crates/checking/src/lib.rs | 590 +++++++++++++++++- .../core/src/engine/predicates/predicate.rs | 29 + pumpkin-crates/core/src/engine/state.rs | 102 ++- .../core/src/engine/variables/affine_view.rs | 100 +++ .../core/src/engine/variables/domain_id.rs | 72 +++ .../src/engine/variables/integer_variable.rs | 17 +- .../core/src/engine/variables/literal.rs | 55 ++ .../core/src/propagation/constructor.rs | 49 ++ pumpkin-crates/propagators/Cargo.toml | 2 + .../arithmetic/linear_less_or_equal.rs | 61 ++ pumpkin-solver/Cargo.toml | 1 + 23 files changed, 1429 insertions(+), 681 deletions(-) delete mode 100644 pumpkin-checker/src/state.rs diff --git a/Cargo.lock b/Cargo.lock index d2a35142d..dc94dfc1f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -732,6 +732,8 @@ dependencies = [ "drcp-format", "flate2", "fzn-rs", + "pumpkin-checking", + "pumpkin-propagators", "thiserror", ] @@ -798,6 +800,7 @@ dependencies = [ "clap", "convert_case 0.6.0", "enumset", + "pumpkin-checking", "pumpkin-constraints", "pumpkin-core", ] diff --git a/pumpkin-checker/Cargo.toml b/pumpkin-checker/Cargo.toml index 21f817aa5..1bbfc3a4e 100644 --- a/pumpkin-checker/Cargo.toml +++ b/pumpkin-checker/Cargo.toml @@ -7,6 +7,8 @@ license.workspace = true authors.workspace = true [dependencies] +pumpkin-checking = { version = "0.2.2", path = "../pumpkin-crates/checking/" } +pumpkin-propagators = { version = "0.2.2", features = ["include-checkers"], path = "../pumpkin-crates/propagators/" } anyhow = "1.0.99" clap = { version = "4.5.47", features = ["derive"] } drcp-format = { version = "0.3.0", path = "../drcp-format" } diff --git a/pumpkin-checker/src/deductions.rs b/pumpkin-checker/src/deductions.rs index d49217e3c..0a2d3a7b1 100644 --- a/pumpkin-checker/src/deductions.rs +++ b/pumpkin-checker/src/deductions.rs @@ -3,10 +3,11 @@ use std::rc::Rc; use drcp_format::ConstraintId; use drcp_format::IntAtomic; +use pumpkin_checking::AtomicConstraint; +use pumpkin_checking::VariableState; use crate::inferences::Fact; use crate::model::Nogood; -use crate::state::VariableState; /// An inference that was ignored when checking a deduction. #[derive(Clone, Debug)] @@ -50,9 +51,11 @@ pub fn verify_deduction( // At some point, this should either reach a fact without a consequent or derive an // inconsistent domain. - let mut variable_state = - VariableState::prepare_for_conflict_check(&Fact::nogood(deduction.premises.clone())) - .ok_or(InvalidDeduction::InconsistentPremises)?; + let mut variable_state = VariableState::prepare_for_conflict_check( + deduction.premises.iter().cloned().map(Into::into), + None, + ) + .ok_or(InvalidDeduction::InconsistentPremises)?; let mut unused_inferences = Vec::new(); @@ -75,9 +78,22 @@ pub fn verify_deduction( // `String`. The former does not implement `Send`, but that is // required for our error type to be used with anyhow. Some(IntAtomic { - name: String::from(premise.name.as_ref()), - comparison: premise.comparison, - value: premise.value, + name: String::from(premise.identifier().as_ref()), + comparison: match premise.comparison() { + pumpkin_checking::Comparison::GreaterEqual => { + drcp_format::IntComparison::GreaterEqual + } + pumpkin_checking::Comparison::LessEqual => { + drcp_format::IntComparison::LessEqual + } + pumpkin_checking::Comparison::Equal => { + drcp_format::IntComparison::Equal + } + pumpkin_checking::Comparison::NotEqual => { + drcp_format::IntComparison::NotEqual + } + }, + value: premise.value(), }) } }) diff --git a/pumpkin-checker/src/inferences/all_different.rs b/pumpkin-checker/src/inferences/all_different.rs index d1dd3681c..6887cb250 100644 --- a/pumpkin-checker/src/inferences/all_different.rs +++ b/pumpkin-checker/src/inferences/all_different.rs @@ -1,9 +1,12 @@ use std::collections::HashSet; +use pumpkin_checking::CheckerVariable; +use pumpkin_checking::VariableState; + use super::Fact; use crate::inferences::InvalidInference; +use crate::model::Atomic; use crate::model::Constraint; -use crate::state::VariableState; /// Verify an `all_different` inference. /// @@ -12,8 +15,9 @@ use crate::state::VariableState; /// /// The checker will reject inferences with redundant atomic constraints. pub(crate) fn verify_all_different( - fact: &Fact, + _: &Fact, constraint: &Constraint, + state: VariableState, ) -> Result<(), InvalidInference> { // This checker takes the union of the domains of the variables in the constraint. If there // are fewer values in the union of the domain than there are variables, then there is a @@ -23,14 +27,11 @@ pub(crate) fn verify_all_different( return Err(InvalidInference::ConstraintLabelMismatch); }; - let variable_state = VariableState::prepare_for_conflict_check(fact) - .ok_or(InvalidInference::InconsistentPremises)?; - // Collect all values present in at least one of the domains. let union_of_domains = all_different .variables .iter() - .filter_map(|variable| variable_state.iter_domain(variable)) + .filter_map(|variable| variable.iter_induced_domain(&state)) .flatten() .collect::>(); @@ -40,7 +41,7 @@ pub(crate) fn verify_all_different( let num_variables = all_different .variables .iter() - .filter(|variable| variable_state.iter_domain(variable).is_some()) + .filter(|variable| variable.iter_induced_domain(&state).is_some()) .count(); if union_of_domains.len() < num_variables { diff --git a/pumpkin-checker/src/inferences/arithmetic.rs b/pumpkin-checker/src/inferences/arithmetic.rs index fb3ff9a51..02573104d 100644 --- a/pumpkin-checker/src/inferences/arithmetic.rs +++ b/pumpkin-checker/src/inferences/arithmetic.rs @@ -1,16 +1,14 @@ use std::collections::BTreeSet; -use std::rc::Rc; -use drcp_format::IntComparison; -use fzn_rs::VariableExpr; +use pumpkin_checking::CheckerVariable; +use pumpkin_checking::I32Ext; +use pumpkin_checking::VariableState; use super::Fact; use crate::inferences::InvalidInference; use crate::model::AllDifferent; use crate::model::Atomic; use crate::model::Constraint; -use crate::state::I32Ext; -use crate::state::VariableState; /// Verify a `binary_equals` inference. /// @@ -18,8 +16,9 @@ use crate::state::VariableState; /// `linear_bounds` inference is that in the binary case, we can certify holes in the domain as /// well. pub(crate) fn verify_binary_equals( - fact: &Fact, + _: &Fact, constraint: &Constraint, + mut state: VariableState, ) -> Result<(), InvalidInference> { // To check this inference we expect the intersection of both domains to be empty. @@ -32,8 +31,11 @@ pub(crate) fn verify_binary_equals( return Err(InvalidInference::Unsound); } - let (weight_a, variable_a) = &linear.terms[0]; - let (weight_b, variable_b) = &linear.terms[1]; + let term_a = &linear.terms[0]; + let term_b = &linear.terms[1]; + + let weight_a = term_a.weight.get(); + let weight_b = term_b.weight.get(); // TODO: Generalize this rule to work with non-unit weights. // At the moment we expect one term to have weight `-1` and the other term to have weight @@ -42,53 +44,26 @@ pub(crate) fn verify_binary_equals( return Err(InvalidInference::Unsound); } - let mut variable_state = VariableState::prepare_for_conflict_check(fact) - .ok_or(InvalidInference::InconsistentPremises)?; - // We apply the domain of variable 2 to variable 1. If the state remains consistent, then // the step is unsound! - let state_is_consistent = match variable_a { - VariableExpr::Identifier(var1) => { - let mut consistent = true; - - if let I32Ext::I32(value) = variable_state.upper_bound(variable_b) { - consistent &= variable_state.apply(&Atomic { - name: Rc::clone(var1), - comparison: IntComparison::LessEqual, - value: linear.bound + value, - }); - } - - if let I32Ext::I32(value) = variable_state.lower_bound(variable_b) { - consistent &= variable_state.apply(&Atomic { - name: Rc::clone(var1), - comparison: IntComparison::GreaterEqual, - value: linear.bound + value, - }); - } - - for value in variable_state.holes(variable_b).collect::>() { - consistent &= variable_state.apply(&Atomic { - name: Rc::clone(var1), - comparison: IntComparison::NotEqual, - value: linear.bound + value, - }); - } - - consistent - } + let mut consistent = true; - VariableExpr::Constant(value) => match variable_b { - VariableExpr::Identifier(var2) => variable_state.apply(&Atomic { - name: Rc::clone(var2), - comparison: IntComparison::NotEqual, - value: linear.bound + *value, - }), - VariableExpr::Constant(_) => panic!("Binary equals over two constants is unexpected."), - }, - }; + if let I32Ext::I32(value) = term_b.induced_upper_bound(&state) { + let atomic = term_a.atomic_less_than(linear.bound + value); + consistent &= state.apply(&atomic); + } - if state_is_consistent { + if let I32Ext::I32(value) = term_b.induced_lower_bound(&state) { + let atomic = term_a.atomic_greater_than(linear.bound + value); + consistent &= state.apply(&atomic); + } + + for value in term_b.induced_holes(&state).collect::>() { + let atomic = term_a.atomic_not_equal(linear.bound + value); + consistent &= state.apply(&atomic); + } + + if consistent { // The intersection of the domains should yield an inconsistent state for the // inference to be sound. return Err(InvalidInference::Unsound); @@ -102,19 +77,17 @@ pub(crate) fn verify_binary_equals( /// Tests that the premise of the inference and the negation of the consequent force the linear sum /// to equal the right-hand side of the not equals constraint. pub(crate) fn verify_binary_not_equals( - fact: &Fact, + _: &Fact, constraint: &Constraint, + state: VariableState, ) -> Result<(), InvalidInference> { let Constraint::AllDifferent(AllDifferent { variables }) = constraint else { return Err(InvalidInference::ConstraintLabelMismatch); }; - let variable_state = VariableState::prepare_for_conflict_check(fact) - .ok_or(InvalidInference::InconsistentPremises)?; - let mut values = BTreeSet::new(); for variable in variables { - let Some(value) = variable_state.fixed_value(variable) else { + let Some(value) = variable.induced_fixed_value(&state) else { continue; }; diff --git a/pumpkin-checker/src/inferences/linear.rs b/pumpkin-checker/src/inferences/linear.rs index 44996d80c..8812cb334 100644 --- a/pumpkin-checker/src/inferences/linear.rs +++ b/pumpkin-checker/src/inferences/linear.rs @@ -1,9 +1,13 @@ +use pumpkin_checking::InferenceChecker; +use pumpkin_checking::VariableState; +use pumpkin_propagators::arithmetic::LinearLessOrEqualInferenceChecker; + use crate::inferences::Fact; use crate::inferences::InvalidInference; +use crate::model::Atomic; use crate::model::Constraint; use crate::model::Linear; -use crate::state::I32Ext; -use crate::state::VariableState; +use crate::model::Term; /// Verify a `linear_bounds` inference. /// @@ -11,22 +15,26 @@ use crate::state::VariableState; pub(super) fn verify_linear_bounds( fact: &Fact, generated_by: &Constraint, + state: VariableState, ) -> Result<(), InvalidInference> { match generated_by { - Constraint::LinearLeq(linear) => verify_linear_inference(linear, fact), + Constraint::LinearLeq(linear) => verify_linear_inference(linear, fact, state), Constraint::LinearEq(linear) => { - let try_upper_bound = verify_linear_inference(linear, fact); + let try_upper_bound = verify_linear_inference(linear, fact, state.clone()); let inverted_linear = Linear { terms: linear .terms .iter() - .map(|(weight, variable)| (-weight, variable.clone())) + .map(|term| Term { + weight: -term.weight, + variable: term.variable.clone(), + }) .collect(), bound: -linear.bound, }; - let try_lower_bound = verify_linear_inference(&inverted_linear, fact); + let try_lower_bound = verify_linear_inference(&inverted_linear, fact, state); match (try_lower_bound, try_upper_bound) { (Ok(_), Ok(_)) => panic!("This should not happen."), @@ -39,35 +47,14 @@ pub(super) fn verify_linear_bounds( } } -fn verify_linear_inference(linear: &Linear, fact: &Fact) -> Result<(), InvalidInference> { - let variable_state = VariableState::prepare_for_conflict_check(fact) - .ok_or(InvalidInference::InconsistentPremises)?; - - // Next, we evaluate the linear inequality. The lower bound of the - // left-hand side must exceed the bound in the constraint. - let left_hand_side = linear.terms.iter().fold(None, |acc, (weight, variable)| { - let lower_bound = if *weight >= 0 { - variable_state.lower_bound(variable) - } else { - variable_state.upper_bound(variable) - }; - - match acc { - None => match lower_bound { - I32Ext::I32(value) => Some(weight * value), - I32Ext::NegativeInf => None, - I32Ext::PositiveInf => None, - }, - - Some(v1) => match lower_bound { - I32Ext::I32(v2) => Some(v1 + weight * v2), - I32Ext::NegativeInf => Some(v1), - I32Ext::PositiveInf => Some(v1), - }, - } - }); +fn verify_linear_inference( + linear: &Linear, + _: &Fact, + state: VariableState, +) -> Result<(), InvalidInference> { + let checker = LinearLessOrEqualInferenceChecker::new(linear.terms.clone().into(), linear.bound); - if left_hand_side.is_some_and(|value| value > linear.bound) { + if InferenceChecker::::check(&checker, state) { Ok(()) } else { Err(InvalidInference::Unsound) diff --git a/pumpkin-checker/src/inferences/mod.rs b/pumpkin-checker/src/inferences/mod.rs index 61e5416a0..41d5887a2 100644 --- a/pumpkin-checker/src/inferences/mod.rs +++ b/pumpkin-checker/src/inferences/mod.rs @@ -4,6 +4,8 @@ mod linear; mod nogood; mod time_table; +use pumpkin_checking::VariableState; + use crate::model::Atomic; use crate::model::Model; @@ -61,8 +63,8 @@ pub(crate) fn verify_inference( inference: &drcp_format::Inference, i32, std::rc::Rc>, ) -> Result { let fact = Fact { - premises: inference.premises.clone(), - consequent: inference.consequent.clone(), + premises: inference.premises.iter().cloned().map(Into::into).collect(), + consequent: inference.consequent.clone().map(Into::into), }; let label = inference @@ -79,7 +81,9 @@ pub(crate) fn verify_inference( return Err(InvalidInference::Unsound); }; - if !model.is_trivially_true(atomic.clone()) { + let atomic: Atomic = atomic.into(); + + if !model.is_trivially_true(&atomic) { // If the consequent is not trivially true in the model then the inference // is unsound. return Err(InvalidInference::Unsound); @@ -88,6 +92,11 @@ pub(crate) fn verify_inference( return Ok(fact); } + // Setup the state for a conflict check. + let variable_state = + VariableState::prepare_for_conflict_check(fact.premises.clone(), fact.consequent.clone()) + .ok_or(InvalidInference::InconsistentPremises)?; + // Get the constraint that generated the inference from the model. let generated_by_constraint_id = inference .generated_by @@ -98,27 +107,27 @@ pub(crate) fn verify_inference( match label { "linear_bounds" => { - linear::verify_linear_bounds(&fact, generated_by)?; + linear::verify_linear_bounds(&fact, generated_by, variable_state)?; } "nogood" => { - nogood::verify_nogood(&fact, generated_by)?; + nogood::verify_nogood(&fact, generated_by, variable_state)?; } "time_table" => { - time_table::verify_time_table(&fact, generated_by)?; + time_table::verify_time_table(&fact, generated_by, variable_state)?; } "all_different" => { - all_different::verify_all_different(&fact, generated_by)?; + all_different::verify_all_different(&fact, generated_by, variable_state)?; } "binary_equals" => { - arithmetic::verify_binary_equals(&fact, generated_by)?; + arithmetic::verify_binary_equals(&fact, generated_by, variable_state)?; } "binary_not_equals" => { - arithmetic::verify_binary_not_equals(&fact, generated_by)?; + arithmetic::verify_binary_not_equals(&fact, generated_by, variable_state)?; } _ => return Err(InvalidInference::UnsupportedLabel), diff --git a/pumpkin-checker/src/inferences/nogood.rs b/pumpkin-checker/src/inferences/nogood.rs index c715ad162..93fe4164b 100644 --- a/pumpkin-checker/src/inferences/nogood.rs +++ b/pumpkin-checker/src/inferences/nogood.rs @@ -1,20 +1,23 @@ +use pumpkin_checking::VariableState; + use crate::inferences::Fact; use crate::inferences::InvalidInference; +use crate::model::Atomic; use crate::model::Constraint; -use crate::state::VariableState; /// Verifies a `nogood` inference. /// /// This inference is used to rewrite a nogood `L /\ p -> false` to `L -> not p`. -pub(crate) fn verify_nogood(fact: &Fact, constraint: &Constraint) -> Result<(), InvalidInference> { +pub(crate) fn verify_nogood( + _: &Fact, + constraint: &Constraint, + state: VariableState, +) -> Result<(), InvalidInference> { let Constraint::Nogood(nogood) = constraint else { return Err(InvalidInference::ConstraintLabelMismatch); }; - let variable_state = VariableState::prepare_for_conflict_check(fact) - .ok_or(InvalidInference::InconsistentPremises)?; - - let is_implied_by_nogood = nogood.iter().all(|atomic| variable_state.is_true(atomic)); + let is_implied_by_nogood = nogood.iter().all(|atomic| state.is_true(atomic)); if is_implied_by_nogood { Ok(()) diff --git a/pumpkin-checker/src/inferences/time_table.rs b/pumpkin-checker/src/inferences/time_table.rs index 4a5c87842..b58cac94c 100644 --- a/pumpkin-checker/src/inferences/time_table.rs +++ b/pumpkin-checker/src/inferences/time_table.rs @@ -1,33 +1,34 @@ use std::collections::BTreeMap; +use pumpkin_checking::CheckerVariable; +use pumpkin_checking::VariableState; + use super::Fact; use crate::inferences::InvalidInference; +use crate::model::Atomic; use crate::model::Constraint; -use crate::state::VariableState; /// Verifies a `time_table` inference for the cumulative constraint. /// /// The premises and negation of the consequent should lead to an overflow of the resource /// capacity. pub(crate) fn verify_time_table( - fact: &Fact, + _: &Fact, constraint: &Constraint, + state: VariableState, ) -> Result<(), InvalidInference> { let Constraint::Cumulative(cumulative) = constraint else { return Err(InvalidInference::ConstraintLabelMismatch); }; - let variable_state = VariableState::prepare_for_conflict_check(fact) - .ok_or(InvalidInference::InconsistentPremises)?; - // The profile is a key-value store. The keys correspond to time-points, and the values to the // relative change in resource consumption. A BTreeMap is used to maintain a sorted order of // the time points. let mut profile = BTreeMap::new(); for task in cumulative.tasks.iter() { - let lst = variable_state.upper_bound(&task.start_time); - let ect = variable_state.lower_bound(&task.start_time) + task.duration; + let lst = task.start_time.induced_upper_bound(&state); + let ect = task.start_time.induced_lower_bound(&state) + task.duration; if ect <= lst { *profile.entry(ect).or_insert(0) += task.resource_usage; diff --git a/pumpkin-checker/src/lib.rs b/pumpkin-checker/src/lib.rs index cac7b8fc9..863a4a7c6 100644 --- a/pumpkin-checker/src/lib.rs +++ b/pumpkin-checker/src/lib.rs @@ -12,7 +12,6 @@ use drcp_format::reader::ProofReader; pub mod deductions; pub mod inferences; -mod state; pub mod model; @@ -150,11 +149,11 @@ fn parse_model(path: impl AsRef) -> anyhow::Result { fzn_rs::Method::Optimize { direction: fzn_rs::ast::OptimizationDirection::Minimize, objective, - } => Some(Objective::Minimize(objective.clone())), + } => Some(Objective::Minimize(objective.clone().into())), fzn_rs::Method::Optimize { direction: fzn_rs::ast::OptimizationDirection::Maximize, objective, - } => Some(Objective::Maximize(objective.clone())), + } => Some(Objective::Maximize(objective.clone().into())), }; for (name, variable) in fzn_model.variables.iter() { @@ -181,7 +180,12 @@ fn parse_model(path: impl AsRef) -> anyhow::Result { let weight = weight?; let variable = variable?; - terms.push((weight, variable)); + terms.push(Term { + weight: weight + .try_into() + .expect("flatzinc does not have 0-weight terms"), + variable: variable.into(), + }); } Constraint::LinearLeq(Linear { @@ -204,7 +208,12 @@ fn parse_model(path: impl AsRef) -> anyhow::Result { let weight = weight?; let variable = variable?; - terms.push((weight, variable)); + terms.push(Term { + weight: weight + .try_into() + .expect("flatzinc does not have 0-weight terms"), + variable: variable.into(), + }); } Constraint::LinearEq(Linear { @@ -233,7 +242,7 @@ fn parse_model(path: impl AsRef) -> anyhow::Result { let resource_usage = maybe_resource_usage?; Ok(Task { - start_time, + start_time: start_time.into(), duration, resource_usage, }) @@ -250,6 +259,7 @@ fn parse_model(path: impl AsRef) -> anyhow::Result { FlatZincConstraints::AllDifferent(variables) => { let variables = fzn_model .resolve_array(variables)? + .map(|maybe_variable| maybe_variable.map(Variable::from)) .collect::, _>>()?; Constraint::AllDifferent(AllDifferent { variables }) @@ -367,7 +377,9 @@ fn verify_conclusion(model: &Model, conclusion: &drcp_format::Conclusion match conclusion { drcp_format::Conclusion::Unsat => nogood.as_ref().is_empty(), - drcp_format::Conclusion::DualBound(atomic) => nogood.as_ref() == [!atomic.clone()], + drcp_format::Conclusion::DualBound(atomic) => { + nogood.as_ref() == [Atomic::from(!atomic.clone())] + } } }) } diff --git a/pumpkin-checker/src/model.rs b/pumpkin-checker/src/model.rs index 722e1d9da..4e191c41a 100644 --- a/pumpkin-checker/src/model.rs +++ b/pumpkin-checker/src/model.rs @@ -3,6 +3,7 @@ //! The main component of the model are the constraints that the checker supports. use std::collections::BTreeMap; +use std::num::NonZero; use std::ops::Deref; use std::rc::Rc; @@ -10,6 +11,11 @@ use drcp_format::ConstraintId; use drcp_format::IntAtomic; use fzn_rs::VariableExpr; use fzn_rs::ast::Domain; +use pumpkin_checking::AtomicConstraint; +use pumpkin_checking::CheckerVariable; +use pumpkin_checking::Comparison; +use pumpkin_checking::I32Ext; +use pumpkin_checking::VariableState; #[derive(Clone, Debug)] pub enum Constraint { @@ -20,17 +26,51 @@ pub enum Constraint { AllDifferent(AllDifferent), } -pub type Atomic = IntAtomic, i32>; +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct Atomic(IntAtomic, i32>); + +impl From, i32>> for Atomic { + fn from(value: IntAtomic, i32>) -> Self { + Atomic(value) + } +} + +impl AtomicConstraint for Atomic { + type Identifier = Rc; + + fn identifier(&self) -> Self::Identifier { + Rc::clone(&self.0.name) + } + + fn comparison(&self) -> Comparison { + match self.0.comparison { + drcp_format::IntComparison::GreaterEqual => Comparison::GreaterEqual, + drcp_format::IntComparison::LessEqual => Comparison::LessEqual, + drcp_format::IntComparison::Equal => Comparison::Equal, + drcp_format::IntComparison::NotEqual => Comparison::NotEqual, + } + } + + fn value(&self) -> i32 { + self.0.value + } + + fn negate(&self) -> Self { + let owned = self.0.clone(); + Self(!owned) + } +} #[derive(Clone, Debug)] pub struct Nogood(Vec); -impl From for Nogood +impl From for Nogood where - T: IntoIterator, + T: IntoIterator, + A: Into, { fn from(value: T) -> Self { - Nogood(value.into_iter().collect()) + Nogood(value.into_iter().map(Into::into).collect()) } } @@ -42,15 +82,176 @@ impl Deref for Nogood { } } +/// A checker variable that can be used with [`pumpkin_checking::VariableState`]. +#[derive(Clone, Debug)] +pub struct Variable(VariableExpr); + +impl From> for Variable { + fn from(value: VariableExpr) -> Self { + Variable(value) + } +} + +impl CheckerVariable for Variable { + fn atomic_less_than(&self, value: i32) -> Atomic { + todo!() + } + + fn atomic_greater_than(&self, value: i32) -> Atomic { + todo!() + } + + fn atomic_equal(&self, value: i32) -> Atomic { + todo!() + } + + fn atomic_not_equal(&self, value: i32) -> Atomic { + todo!() + } + + fn induced_lower_bound(&self, variable_state: &VariableState) -> I32Ext { + match self.0 { + VariableExpr::Identifier(ref ident) => variable_state.lower_bound(ident), + VariableExpr::Constant(value) => value.into(), + } + } + + fn induced_upper_bound(&self, variable_state: &VariableState) -> I32Ext { + match self.0 { + VariableExpr::Identifier(ref ident) => variable_state.upper_bound(ident), + VariableExpr::Constant(value) => value.into(), + } + } + + fn induced_fixed_value(&self, variable_state: &VariableState) -> Option { + match self.0 { + VariableExpr::Identifier(ref ident) => variable_state.fixed_value(ident), + VariableExpr::Constant(value) => value.into(), + } + } + + fn induced_holes<'this, 'state>( + &'this self, + variable_state: &'state VariableState, + ) -> impl Iterator + 'state + where + 'this: 'state, + { + match self.0 { + #[allow( + trivial_casts, + reason = "without it the compiler does not coerce to Box" + )] + VariableExpr::Identifier(ref ident) => { + Box::new(variable_state.holes(ident)) as Box> + } + VariableExpr::Constant(_) => Box::new(std::iter::empty()), + } + } + + fn iter_induced_domain<'this, 'state>( + &'this self, + variable_state: &'state VariableState, + ) -> Option + 'state> + where + 'this: 'state, + { + match self.0 { + #[allow( + trivial_casts, + reason = "without it the compiler does not coerce to Box" + )] + VariableExpr::Identifier(ref ident) => variable_state + .iter_domain(ident) + .map(|iter| Box::new(iter) as Box>), + VariableExpr::Constant(value) => Some(Box::new(std::iter::once(value))), + } + } +} + #[derive(Clone, Debug)] pub struct Linear { - pub terms: Vec<(i32, VariableExpr)>, + pub terms: Vec, pub bound: i32, } +#[derive(Clone, Debug)] +pub struct Term { + pub weight: NonZero, + pub variable: Variable, +} + +impl CheckerVariable for Term { + fn atomic_less_than(&self, value: i32) -> Atomic { + todo!() + } + + fn atomic_greater_than(&self, value: i32) -> Atomic { + todo!() + } + + fn atomic_equal(&self, value: i32) -> Atomic { + todo!() + } + + fn atomic_not_equal(&self, value: i32) -> Atomic { + todo!() + } + + fn induced_lower_bound(&self, variable_state: &VariableState) -> I32Ext { + if self.weight.is_positive() { + self.variable.induced_lower_bound(variable_state) * self.weight.get() + } else { + self.variable.induced_upper_bound(variable_state) * self.weight.get() + } + } + + fn induced_upper_bound(&self, variable_state: &VariableState) -> I32Ext { + if self.weight.is_positive() { + self.variable.induced_upper_bound(variable_state) * self.weight.get() + } else { + self.variable.induced_lower_bound(variable_state) * self.weight.get() + } + } + + fn induced_fixed_value(&self, variable_state: &VariableState) -> Option { + self.variable + .induced_fixed_value(variable_state) + .map(|value| value * self.weight.get()) + } + + fn induced_holes<'this, 'state>( + &'this self, + _variable_state: &'state VariableState, + ) -> impl Iterator + 'state + where + 'this: 'state, + { + todo!("how to compute holes in a scaled domain?"); + + #[allow( + unreachable_code, + reason = "otherwise the function does not return an impl Iterator" + )] + std::iter::empty() + } + + fn iter_induced_domain<'this, 'state>( + &'this self, + variable_state: &'state VariableState, + ) -> Option + 'state> + where + 'this: 'state, + { + self.variable + .iter_induced_domain(variable_state) + .map(|iter| iter.map(|value| value * self.weight.get())) + } +} + #[derive(Clone, Debug)] pub struct Task { - pub start_time: VariableExpr, + pub start_time: Variable, pub duration: i32, pub resource_usage: i32, } @@ -63,13 +264,13 @@ pub struct Cumulative { #[derive(Clone, Debug)] pub struct AllDifferent { - pub variables: Vec>, + pub variables: Vec, } #[derive(Clone, Debug)] pub enum Objective { - Maximize(VariableExpr), - Minimize(VariableExpr), + Maximize(Variable), + Minimize(Variable), } #[derive(Clone, Debug, Default)] @@ -108,28 +309,26 @@ impl Model { /// Test whether the atomic is true in the initial domains of the variables. /// /// Returns false if the atomic is over a variable that is not in the model. - pub fn is_trivially_true(&self, atomic: Atomic) -> bool { - let Some(domain) = self.variables.get(&atomic.name) else { + pub fn is_trivially_true(&self, atomic: &Atomic) -> bool { + let Some(domain) = self.variables.get(&atomic.identifier()) else { return false; }; match domain { Domain::UnboundedInt => false, - Domain::Int(dom) => match atomic.comparison { - drcp_format::IntComparison::GreaterEqual => { - *dom.lower_bound() >= atomic.value as i64 - } - drcp_format::IntComparison::LessEqual => *dom.upper_bound() <= atomic.value as i64, - drcp_format::IntComparison::Equal => { - *dom.lower_bound() >= atomic.value as i64 - && *dom.upper_bound() <= atomic.value as i64 + Domain::Int(dom) => match atomic.comparison() { + Comparison::GreaterEqual => *dom.lower_bound() >= atomic.value() as i64, + Comparison::LessEqual => *dom.upper_bound() <= atomic.value() as i64, + Comparison::Equal => { + *dom.lower_bound() >= atomic.value() as i64 + && *dom.upper_bound() <= atomic.value() as i64 } - drcp_format::IntComparison::NotEqual => { - if *dom.lower_bound() >= atomic.value as i64 { + Comparison::NotEqual => { + if *dom.lower_bound() >= atomic.value() as i64 { return true; } - if *dom.upper_bound() <= atomic.value as i64 { + if *dom.upper_bound() <= atomic.value() as i64 { return true; } @@ -137,7 +336,7 @@ impl Model { return false; } - dom.into_iter().all(|value| value != atomic.value as i64) + dom.into_iter().all(|value| value != atomic.value() as i64) } }, Domain::Bool => todo!("boolean variables are not yet supported"), diff --git a/pumpkin-checker/src/state.rs b/pumpkin-checker/src/state.rs deleted file mode 100644 index a37c801ed..000000000 --- a/pumpkin-checker/src/state.rs +++ /dev/null @@ -1,514 +0,0 @@ -use std::cmp::Ordering; -use std::collections::BTreeMap; -use std::collections::BTreeSet; -use std::ops::Add; -use std::rc::Rc; - -use crate::inferences::Fact; -use crate::model::Atomic; - -/// The domains of all variables in the problem. -/// -/// Domains can be reduced through [`VariableState::apply`]. By default, the domain of every -/// variable is infinite. -#[derive(Clone, Debug, Default)] -pub(crate) struct VariableState { - domains: BTreeMap, Domain>, -} - -impl VariableState { - /// Create a variable state that applies all the premises and, if present, the negation of the - /// consequent. - /// - /// Used by inference checkers if they want to identify a conflict by negating the consequent. - pub(crate) fn prepare_for_conflict_check(fact: &Fact) -> Option { - let mut variable_state = VariableState::default(); - - let negated_consequent = fact - .consequent - .as_ref() - .map(|consequent| !consequent.clone()); - - // Apply all the premises and the negation of the consequent to the state. - if !fact - .premises - .iter() - .chain(negated_consequent.as_ref()) - .all(|premise| variable_state.apply(premise)) - { - return None; - } - - Some(variable_state) - } - - /// Get the lower bound of a variable. - pub(crate) fn lower_bound(&self, variable: &fzn_rs::VariableExpr) -> I32Ext { - let name = match variable { - fzn_rs::VariableExpr::Identifier(name) => name, - fzn_rs::VariableExpr::Constant(value) => return I32Ext::I32(*value), - }; - - self.domains - .get(name) - .map(|domain| domain.lower_bound) - .unwrap_or(I32Ext::NegativeInf) - } - - /// Get the upper bound of a variable. - pub(crate) fn upper_bound(&self, variable: &fzn_rs::VariableExpr) -> I32Ext { - let name = match variable { - fzn_rs::VariableExpr::Identifier(name) => name, - fzn_rs::VariableExpr::Constant(value) => return I32Ext::I32(*value), - }; - - self.domains - .get(name) - .map(|domain| domain.upper_bound) - .unwrap_or(I32Ext::PositiveInf) - } - - /// Get the holes within the lower and upper bound of the variable expression. - pub(crate) fn holes( - &self, - variable: &fzn_rs::VariableExpr, - ) -> impl Iterator + '_ { - #[allow(trivial_casts, reason = "without it we get a type error")] - let name = match variable { - fzn_rs::VariableExpr::Identifier(name) => name, - fzn_rs::VariableExpr::Constant(_) => { - return Box::new(std::iter::empty()) as Box>; - } - }; - - #[allow(trivial_casts, reason = "without it we get a type error")] - self.domains - .get(name) - .map(|domain| Box::new(domain.holes.iter().copied()) as Box>) - .unwrap_or_else(|| Box::new(std::iter::empty())) - } - - /// Get the fixed value of this variable, if it is fixed. - pub(crate) fn fixed_value(&self, variable: &fzn_rs::VariableExpr) -> Option { - let name = match variable { - fzn_rs::VariableExpr::Identifier(name) => name, - fzn_rs::VariableExpr::Constant(value) => return Some(*value), - }; - - let domain = self.domains.get(name)?; - - if domain.lower_bound == domain.upper_bound { - let I32Ext::I32(value) = domain.lower_bound else { - panic!( - "lower can only equal upper if they are integers, otherwise the sign of infinity makes them different" - ); - }; - - Some(value) - } else { - None - } - } - - /// Obtain an iterator over the domain of the variable. - /// - /// If the domain is unbounded, then `None` is returned. - pub(crate) fn iter_domain( - &self, - variable: &fzn_rs::VariableExpr, - ) -> Option> { - match variable { - fzn_rs::VariableExpr::Identifier(name) => { - let domain = self.domains.get(name)?; - - let I32Ext::I32(lower_bound) = domain.lower_bound else { - // If there is no lower bound, then the domain is unbounded. - return None; - }; - - // Ensure there is also an upper bound. - if !matches!(domain.upper_bound, I32Ext::I32(_)) { - return None; - } - - Some(DomainIterator(DomainIteratorImpl::Domain { - domain, - next_value: lower_bound, - })) - } - - fzn_rs::VariableExpr::Constant(value) => { - Some(DomainIterator(DomainIteratorImpl::Constant { - value: *value, - finished: false, - })) - } - } - } - - /// Apply the given [`Atomic`] to the state. - /// - /// Returns true if the state remains consistent, or false if the atomic cannot be true in - /// conjunction with previously applied atomics. - pub(crate) fn apply(&mut self, atomic: &Atomic) -> bool { - let domain = self - .domains - .entry(Rc::clone(&atomic.name)) - .or_insert(Domain::new()); - - match atomic.comparison { - drcp_format::IntComparison::GreaterEqual => { - domain.tighten_lower_bound(atomic.value); - } - - drcp_format::IntComparison::LessEqual => { - domain.tighten_upper_bound(atomic.value); - } - - drcp_format::IntComparison::Equal => { - domain.tighten_lower_bound(atomic.value); - domain.tighten_upper_bound(atomic.value); - } - - drcp_format::IntComparison::NotEqual => { - if domain.lower_bound == atomic.value { - domain.tighten_lower_bound(atomic.value + 1); - } - - if domain.upper_bound == atomic.value { - domain.tighten_upper_bound(atomic.value - 1); - } - - if domain.lower_bound < atomic.value && domain.upper_bound > atomic.value { - let _ = domain.holes.insert(atomic.value); - } - } - } - - domain.is_consistent() - } - - /// Is the given atomic true in the current state. - pub(crate) fn is_true(&self, atomic: &Atomic) -> bool { - let Some(domain) = self.domains.get(&atomic.name) else { - return false; - }; - - match atomic.comparison { - drcp_format::IntComparison::GreaterEqual => domain.lower_bound >= atomic.value, - - drcp_format::IntComparison::LessEqual => domain.upper_bound <= atomic.value, - - drcp_format::IntComparison::Equal => { - domain.lower_bound >= atomic.value && domain.upper_bound <= atomic.value - } - - drcp_format::IntComparison::NotEqual => { - if domain.lower_bound >= atomic.value { - return true; - } - - if domain.upper_bound <= atomic.value { - return true; - } - - if domain.holes.contains(&atomic.value) { - return true; - } - - false - } - } - } -} - -#[derive(Clone, Debug)] -struct Domain { - lower_bound: I32Ext, - upper_bound: I32Ext, - holes: BTreeSet, -} - -impl Domain { - fn new() -> Domain { - Domain { - lower_bound: I32Ext::NegativeInf, - upper_bound: I32Ext::PositiveInf, - holes: BTreeSet::default(), - } - } - - fn tighten_lower_bound(&mut self, bound: i32) { - if self.lower_bound >= bound { - return; - } - - self.lower_bound = I32Ext::I32(bound); - self.holes = self.holes.split_off(&bound); - - // Take care of the condition where the new bound is already a hole in the domain. - if self.holes.contains(&bound) { - self.tighten_lower_bound(bound + 1); - } - } - - fn tighten_upper_bound(&mut self, bound: i32) { - if self.upper_bound <= bound { - return; - } - - self.upper_bound = I32Ext::I32(bound); - - // Note the '+ 1' to keep the elements <= the upper bound instead of < - // the upper bound. - let _ = self.holes.split_off(&(bound + 1)); - - // Take care of the condition where the new bound is already a hole in the domain. - if self.holes.contains(&bound) { - self.tighten_upper_bound(bound - 1); - } - } - - fn is_consistent(&self) -> bool { - // No need to check holes, as the invariant of `Domain` specifies the bounds are as tight - // as possible, taking holes into account. - - self.lower_bound <= self.upper_bound - } -} - -/// An `i32` or infinity. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub(crate) enum I32Ext { - I32(i32), - NegativeInf, - PositiveInf, -} - -impl PartialEq for I32Ext { - fn eq(&self, other: &i32) -> bool { - match self { - I32Ext::I32(v1) => v1 == other, - I32Ext::NegativeInf | I32Ext::PositiveInf => false, - } - } -} - -impl PartialOrd for I32Ext { - fn partial_cmp(&self, other: &I32Ext) -> Option { - Some(self.cmp(other)) - } -} - -impl Ord for I32Ext { - fn cmp(&self, other: &Self) -> Ordering { - match self { - I32Ext::I32(v1) => match other { - I32Ext::I32(v2) => v1.cmp(v2), - I32Ext::NegativeInf => Ordering::Greater, - I32Ext::PositiveInf => Ordering::Less, - }, - I32Ext::NegativeInf => match other { - I32Ext::I32(_) => Ordering::Less, - I32Ext::PositiveInf => Ordering::Less, - I32Ext::NegativeInf => Ordering::Equal, - }, - I32Ext::PositiveInf => match other { - I32Ext::I32(_) => Ordering::Greater, - I32Ext::NegativeInf => Ordering::Greater, - I32Ext::PositiveInf => Ordering::Greater, - }, - } - } -} - -impl PartialOrd for I32Ext { - fn partial_cmp(&self, other: &i32) -> Option { - match self { - I32Ext::I32(v1) => v1.partial_cmp(other), - I32Ext::NegativeInf => Some(Ordering::Less), - I32Ext::PositiveInf => Some(Ordering::Greater), - } - } -} - -impl Add for I32Ext { - type Output = I32Ext; - - fn add(self, rhs: i32) -> Self::Output { - match self { - I32Ext::I32(lhs) => I32Ext::I32(lhs + rhs), - I32Ext::NegativeInf => I32Ext::NegativeInf, - I32Ext::PositiveInf => I32Ext::PositiveInf, - } - } -} - -/// An iterator over the values in the domain of a variable. -pub(crate) struct DomainIterator<'a>(DomainIteratorImpl<'a>); - -enum DomainIteratorImpl<'a> { - Constant { value: i32, finished: bool }, - Domain { domain: &'a Domain, next_value: i32 }, -} - -impl Iterator for DomainIterator<'_> { - type Item = i32; - - fn next(&mut self) -> Option { - match self.0 { - // Iterating over a contant means only yielding the value once, and then - // never again. - DomainIteratorImpl::Constant { - value, - ref mut finished, - } => { - if *finished { - None - } else { - *finished = true; - Some(value) - } - } - - DomainIteratorImpl::Domain { - domain, - ref mut next_value, - } => { - let I32Ext::I32(upper_bound) = domain.upper_bound else { - panic!("Only finite domains can be iterated.") - }; - - loop { - // We have completed iterating the domain. - if *next_value > upper_bound { - return None; - } - - let value = *next_value; - *next_value += 1; - - // The next value is not part of the domain. - if domain.holes.contains(&value) { - continue; - } - - // Here the value is part of the domain, so we yield it. - return Some(value); - } - } - } - } -} - -#[cfg(test)] -mod tests { - use drcp_format::IntAtomic; - use drcp_format::IntComparison; - - use super::*; - - #[test] - fn domain_iterator_unbounded() { - let state = VariableState::default(); - let iterator = state.iter_domain(&fzn_rs::VariableExpr::Identifier(Rc::from("x1"))); - - assert!(iterator.is_none()); - } - - #[test] - fn domain_iterator_unbounded_lower_bound() { - let mut state = VariableState::default(); - - let variable_name = Rc::from("x1"); - let variable = fzn_rs::VariableExpr::Identifier(Rc::clone(&variable_name)); - - let _ = state.apply(&IntAtomic { - name: variable_name, - comparison: IntComparison::LessEqual, - value: 5, - }); - - let iterator = state.iter_domain(&variable); - - assert!(iterator.is_none()); - } - - #[test] - fn domain_iterator_unbounded_upper_bound() { - let mut state = VariableState::default(); - - let variable_name = Rc::from("x1"); - let variable = fzn_rs::VariableExpr::Identifier(Rc::clone(&variable_name)); - - let _ = state.apply(&IntAtomic { - name: variable_name, - comparison: IntComparison::GreaterEqual, - value: 5, - }); - - let iterator = state.iter_domain(&variable); - - assert!(iterator.is_none()); - } - - #[test] - fn domain_iterator_bounded_no_holes() { - let mut state = VariableState::default(); - - let variable_name = Rc::from("x1"); - let variable = fzn_rs::VariableExpr::Identifier(Rc::clone(&variable_name)); - - let _ = state.apply(&IntAtomic { - name: Rc::clone(&variable_name), - comparison: IntComparison::GreaterEqual, - value: 5, - }); - - let _ = state.apply(&IntAtomic { - name: variable_name, - comparison: IntComparison::LessEqual, - value: 10, - }); - - let values = state - .iter_domain(&variable) - .expect("the domain is bounded") - .collect::>(); - - assert_eq!(values, vec![5, 6, 7, 8, 9, 10]); - } - - #[test] - fn domain_iterator_bounded_with_holes() { - let mut state = VariableState::default(); - - let variable_name = Rc::from("x1"); - let variable = fzn_rs::VariableExpr::Identifier(Rc::clone(&variable_name)); - - let _ = state.apply(&IntAtomic { - name: Rc::clone(&variable_name), - comparison: IntComparison::GreaterEqual, - value: 5, - }); - - let _ = state.apply(&IntAtomic { - name: Rc::clone(&variable_name), - comparison: IntComparison::NotEqual, - value: 7, - }); - - let _ = state.apply(&IntAtomic { - name: variable_name, - comparison: IntComparison::LessEqual, - value: 10, - }); - - let values = state - .iter_domain(&variable) - .expect("the domain is bounded") - .collect::>(); - - assert_eq!(values, vec![5, 6, 8, 9, 10]); - } -} diff --git a/pumpkin-crates/checking/src/lib.rs b/pumpkin-crates/checking/src/lib.rs index 139a99dac..bdfb92188 100644 --- a/pumpkin-crates/checking/src/lib.rs +++ b/pumpkin-crates/checking/src/lib.rs @@ -1,11 +1,591 @@ +use std::cmp::Ordering; +use std::collections::BTreeSet; +use std::collections::HashMap; use std::fmt::Debug; +use std::fmt::Display; +use std::hash::Hash; +use std::ops::Add; +use std::ops::Mul; use dyn_clone::DynClone; -use dyn_clone::clone_trait_object; -pub trait InferenceChecker: Debug + DynClone { - fn check(&self) -> bool; +pub trait InferenceChecker: Debug + DynClone { + fn check(&self, state: VariableState) -> bool; } -// Allow Box to be cloned -clone_trait_object!(InferenceChecker); +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum Comparison { + GreaterEqual, + LessEqual, + Equal, + NotEqual, +} + +impl Display for Comparison { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let s = match self { + Comparison::GreaterEqual => ">=", + Comparison::LessEqual => "<=", + Comparison::Equal => "==", + Comparison::NotEqual => "!=", + }; + + write!(f, "{s}") + } +} + +/// A variable. +pub trait CheckerVariable: Debug + Clone { + fn atomic_less_than(&self, value: i32) -> Atomic; + fn atomic_greater_than(&self, value: i32) -> Atomic; + fn atomic_equal(&self, value: i32) -> Atomic; + fn atomic_not_equal(&self, value: i32) -> Atomic; + + /// Get the lower bound of the domain. + fn induced_lower_bound(&self, variable_state: &VariableState) -> I32Ext; + + /// Get the upper bound of the domain. + fn induced_upper_bound(&self, variable_state: &VariableState) -> I32Ext; + + /// Get the value the variable is fixed to, if the variable is fixed. + fn induced_fixed_value(&self, variable_state: &VariableState) -> Option; + + /// Get the holes in the domain. + fn induced_holes<'this, 'state>( + &'this self, + variable_state: &'state VariableState, + ) -> impl Iterator + 'state + where + 'this: 'state; + + /// Iterate the domain of the variable. + /// + /// The order of the values is unspecified. + fn iter_induced_domain<'this, 'state>( + &'this self, + variable_state: &'state VariableState, + ) -> Option + 'state> + where + 'this: 'state; +} + +/// The domains of all variables in the problem. +/// +/// Domains can be reduced through [`VariableState::apply`]. By default, the domain of every +/// variable is infinite. +#[derive(Clone, Debug)] +pub struct VariableState { + domains: HashMap, +} + +impl Default for VariableState { + fn default() -> Self { + Self { + domains: Default::default(), + } + } +} + +pub trait AtomicConstraint: Sized { + type Identifier; + + fn identifier(&self) -> Self::Identifier; + fn comparison(&self) -> Comparison; + fn value(&self) -> i32; + + fn negate(&self) -> Self; +} + +impl VariableState +where + Ident: Hash + Eq, + Atomic: AtomicConstraint, +{ + /// Create a variable state that applies all the premises and, if present, the negation of the + /// consequent. + /// + /// An [`InferenceChecker`] will receive a [`VariableState`] that conforms to this description. + pub fn prepare_for_conflict_check( + premises: impl IntoIterator, + consequent: Option, + ) -> Option { + let mut variable_state = VariableState::default(); + + let negated_consequent = consequent.as_ref().map(AtomicConstraint::negate); + + // Apply all the premises and the negation of the consequent to the state. + if !premises + .into_iter() + .chain(negated_consequent) + .all(|premise| variable_state.apply(&premise)) + { + return None; + } + + Some(variable_state) + } + + /// Get the lower bound of a variable. + pub fn lower_bound(&self, identifier: &Ident) -> I32Ext { + self.domains + .get(identifier) + .map(|domain| domain.lower_bound) + .unwrap_or(I32Ext::NegativeInf) + } + + /// Get the upper bound of a variable. + pub fn upper_bound(&self, identifier: &Ident) -> I32Ext { + self.domains + .get(identifier) + .map(|domain| domain.upper_bound) + .unwrap_or(I32Ext::PositiveInf) + } + + /// Get the holes within the lower and upper bound of the variable expression. + pub fn holes<'a>(&'a self, identifier: &Ident) -> impl Iterator + 'a + where + Ident: 'a, + { + self.domains + .get(identifier) + .map(|domain| domain.holes.iter().copied()) + .into_iter() + .flatten() + } + + /// Get the fixed value of this variable, if it is fixed. + pub fn fixed_value(&self, identifier: &Ident) -> Option { + let domain = self.domains.get(identifier)?; + + if domain.lower_bound == domain.upper_bound { + let I32Ext::I32(value) = domain.lower_bound else { + panic!( + "lower can only equal upper if they are integers, otherwise the sign of infinity makes them different" + ); + }; + + Some(value) + } else { + None + } + } + + /// Obtain an iterator over the domain of the variable. + /// + /// If the domain is unbounded, then `None` is returned. + pub fn iter_domain<'a>(&'a self, identifier: &Ident) -> Option> + where + Ident: 'a, + { + let domain = self.domains.get(identifier)?; + + let I32Ext::I32(lower_bound) = domain.lower_bound else { + // If there is no lower bound, then the domain is unbounded. + return None; + }; + + // Ensure there is also an upper bound. + if !matches!(domain.upper_bound, I32Ext::I32(_)) { + return None; + } + + Some(DomainIterator(DomainIteratorImpl::Domain { + domain, + next_value: lower_bound, + })) + } + + /// Apply the given [`Atomic`] to the state. + /// + /// Returns true if the state remains consistent, or false if the atomic cannot be true in + /// conjunction with previously applied atomics. + pub fn apply(&mut self, atomic: &Atomic) -> bool { + let identifier = atomic.identifier(); + let domain = self.domains.entry(identifier).or_insert(Domain::new()); + + match atomic.comparison() { + Comparison::GreaterEqual => { + domain.tighten_lower_bound(atomic.value()); + } + + Comparison::LessEqual => { + domain.tighten_upper_bound(atomic.value()); + } + + Comparison::Equal => { + domain.tighten_lower_bound(atomic.value()); + domain.tighten_upper_bound(atomic.value()); + } + + Comparison::NotEqual => { + if domain.lower_bound == atomic.value() { + domain.tighten_lower_bound(atomic.value() + 1); + } + + if domain.upper_bound == atomic.value() { + domain.tighten_upper_bound(atomic.value() - 1); + } + + if domain.lower_bound < atomic.value() && domain.upper_bound > atomic.value() { + let _ = domain.holes.insert(atomic.value()); + } + } + } + + domain.is_consistent() + } + + /// Is the given atomic true in the current state. + pub fn is_true(&self, atomic: &Atomic) -> bool { + let Some(domain) = self.domains.get(&atomic.identifier()) else { + return false; + }; + + match atomic.comparison() { + Comparison::GreaterEqual => domain.lower_bound >= atomic.value(), + + Comparison::LessEqual => domain.upper_bound <= atomic.value(), + + Comparison::Equal => { + domain.lower_bound >= atomic.value() && domain.upper_bound <= atomic.value() + } + + Comparison::NotEqual => { + if domain.lower_bound >= atomic.value() { + return true; + } + + if domain.upper_bound <= atomic.value() { + return true; + } + + if domain.holes.contains(&atomic.value()) { + return true; + } + + false + } + } + } +} + +#[derive(Clone, Debug)] +struct Domain { + lower_bound: I32Ext, + upper_bound: I32Ext, + holes: BTreeSet, +} + +impl Domain { + fn new() -> Domain { + Domain { + lower_bound: I32Ext::NegativeInf, + upper_bound: I32Ext::PositiveInf, + holes: BTreeSet::default(), + } + } + + fn tighten_lower_bound(&mut self, bound: i32) { + if self.lower_bound >= bound { + return; + } + + self.lower_bound = I32Ext::I32(bound); + self.holes = self.holes.split_off(&bound); + + // Take care of the condition where the new bound is already a hole in the domain. + if self.holes.contains(&bound) { + self.tighten_lower_bound(bound + 1); + } + } + + fn tighten_upper_bound(&mut self, bound: i32) { + if self.upper_bound <= bound { + return; + } + + self.upper_bound = I32Ext::I32(bound); + + // Note the '+ 1' to keep the elements <= the upper bound instead of < + // the upper bound. + let _ = self.holes.split_off(&(bound + 1)); + + // Take care of the condition where the new bound is already a hole in the domain. + if self.holes.contains(&bound) { + self.tighten_upper_bound(bound - 1); + } + } + + fn is_consistent(&self) -> bool { + // No need to check holes, as the invariant of `Domain` specifies the bounds are as tight + // as possible, taking holes into account. + + self.lower_bound <= self.upper_bound + } +} + +/// An `i32` or infinity. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum I32Ext { + I32(i32), + NegativeInf, + PositiveInf, +} + +impl From for I32Ext { + fn from(value: i32) -> Self { + I32Ext::I32(value) + } +} + +impl PartialEq for I32Ext { + fn eq(&self, other: &i32) -> bool { + match self { + I32Ext::I32(v1) => v1 == other, + I32Ext::NegativeInf | I32Ext::PositiveInf => false, + } + } +} + +impl PartialOrd for I32Ext { + fn partial_cmp(&self, other: &I32Ext) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for I32Ext { + fn cmp(&self, other: &Self) -> Ordering { + match self { + I32Ext::I32(v1) => match other { + I32Ext::I32(v2) => v1.cmp(v2), + I32Ext::NegativeInf => Ordering::Greater, + I32Ext::PositiveInf => Ordering::Less, + }, + I32Ext::NegativeInf => match other { + I32Ext::I32(_) => Ordering::Less, + I32Ext::PositiveInf => Ordering::Less, + I32Ext::NegativeInf => Ordering::Equal, + }, + I32Ext::PositiveInf => match other { + I32Ext::I32(_) => Ordering::Greater, + I32Ext::NegativeInf => Ordering::Greater, + I32Ext::PositiveInf => Ordering::Greater, + }, + } + } +} + +impl PartialOrd for I32Ext { + fn partial_cmp(&self, other: &i32) -> Option { + match self { + I32Ext::I32(v1) => v1.partial_cmp(other), + I32Ext::NegativeInf => Some(Ordering::Less), + I32Ext::PositiveInf => Some(Ordering::Greater), + } + } +} + +impl Add for I32Ext { + type Output = I32Ext; + + fn add(self, rhs: I32Ext) -> Self::Output { + match (self, rhs) { + (I32Ext::I32(lhs), I32Ext::I32(rhs)) => I32Ext::I32(lhs + rhs), + (I32Ext::NegativeInf, I32Ext::NegativeInf) => I32Ext::NegativeInf, + (I32Ext::PositiveInf, I32Ext::PositiveInf) => I32Ext::PositiveInf, + (lhs, rhs) => panic!("the result of {lhs:?} + {rhs:?} is indeterminate"), + } + } +} + +impl Add for I32Ext { + type Output = I32Ext; + + fn add(self, rhs: i32) -> Self::Output { + match self { + I32Ext::I32(lhs) => I32Ext::I32(lhs + rhs), + I32Ext::NegativeInf => I32Ext::NegativeInf, + I32Ext::PositiveInf => I32Ext::PositiveInf, + } + } +} + +impl Mul for I32Ext { + type Output = I32Ext; + + fn mul(self, rhs: i32) -> Self::Output { + match self { + I32Ext::I32(lhs) => I32Ext::I32(lhs * rhs), + I32Ext::NegativeInf => { + if rhs >= 0 { + I32Ext::NegativeInf + } else { + I32Ext::PositiveInf + } + } + I32Ext::PositiveInf => { + if rhs >= 0 { + I32Ext::PositiveInf + } else { + I32Ext::NegativeInf + } + } + } + } +} + +/// An iterator over the values in the domain of a variable. +#[derive(Debug)] +pub struct DomainIterator<'a>(DomainIteratorImpl<'a>); + +#[derive(Debug)] +enum DomainIteratorImpl<'a> { + Domain { domain: &'a Domain, next_value: i32 }, +} + +impl Iterator for DomainIterator<'_> { + type Item = i32; + + fn next(&mut self) -> Option { + match self.0 { + DomainIteratorImpl::Domain { + domain, + ref mut next_value, + } => { + let I32Ext::I32(upper_bound) = domain.upper_bound else { + panic!("Only finite domains can be iterated.") + }; + + loop { + // We have completed iterating the domain. + if *next_value > upper_bound { + return None; + } + + let value = *next_value; + *next_value += 1; + + // The next value is not part of the domain. + if domain.holes.contains(&value) { + continue; + } + + // Here the value is part of the domain, so we yield it. + return Some(value); + } + } + } + } +} + +#[cfg(test)] +mod tests { + use drcp_format::IntAtomic; + use drcp_format::IntComparison; + + use super::*; + + #[test] + fn domain_iterator_unbounded() { + let state = VariableState::default(); + let iterator = state.iter_domain(&fzn_rs::VariableExpr::Identifier(Rc::from("x1"))); + + assert!(iterator.is_none()); + } + + #[test] + fn domain_iterator_unbounded_lower_bound() { + let mut state = VariableState::default(); + + let variable_name = Rc::from("x1"); + let variable = fzn_rs::VariableExpr::Identifier(Rc::clone(&variable_name)); + + let _ = state.apply(&IntAtomic { + name: variable_name, + comparison: Comparison::LessEqual, + value: 5, + }); + + let iterator = state.iter_domain(&variable); + + assert!(iterator.is_none()); + } + + #[test] + fn domain_iterator_unbounded_upper_bound() { + let mut state = VariableState::default(); + + let variable_name = Rc::from("x1"); + let variable = fzn_rs::VariableExpr::Identifier(Rc::clone(&variable_name)); + + let _ = state.apply(&IntAtomic { + name: variable_name, + comparison: Comparison::GreaterEqual, + value: 5, + }); + + let iterator = state.iter_domain(&variable); + + assert!(iterator.is_none()); + } + + #[test] + fn domain_iterator_bounded_no_holes() { + let mut state = VariableState::default(); + + let variable_name = Rc::from("x1"); + let variable = fzn_rs::VariableExpr::Identifier(Rc::clone(&variable_name)); + + let _ = state.apply(&IntAtomic { + name: Rc::clone(&variable_name), + comparison: Comparison::GreaterEqual, + value: 5, + }); + + let _ = state.apply(&IntAtomic { + name: variable_name, + comparison: Comparison::LessEqual, + value: 10, + }); + + let values = state + .iter_domain(&variable) + .expect("the domain is bounded") + .collect::>(); + + assert_eq!(values, vec![5, 6, 7, 8, 9, 10]); + } + + #[test] + fn domain_iterator_bounded_with_holes() { + let mut state = VariableState::default(); + + let variable_name = Rc::from("x1"); + let variable = fzn_rs::VariableExpr::Identifier(Rc::clone(&variable_name)); + + let _ = state.apply(&IntAtomic { + name: Rc::clone(&variable_name), + comparison: Comparison::GreaterEqual, + value: 5, + }); + + let _ = state.apply(&IntAtomic { + name: Rc::clone(&variable_name), + comparison: Comparison::NotEqual, + value: 7, + }); + + let _ = state.apply(&IntAtomic { + name: variable_name, + comparison: Comparison::LessEqual, + value: 10, + }); + + let values = state + .iter_domain(&variable) + .expect("the domain is bounded") + .collect::>(); + + assert_eq!(values, vec![5, 6, 8, 9, 10]); + } +} diff --git a/pumpkin-crates/core/src/engine/predicates/predicate.rs b/pumpkin-crates/core/src/engine/predicates/predicate.rs index 5f3acec88..1a8a067d2 100644 --- a/pumpkin-crates/core/src/engine/predicates/predicate.rs +++ b/pumpkin-crates/core/src/engine/predicates/predicate.rs @@ -1,3 +1,6 @@ +#[cfg(feature = "check-propagations")] +use pumpkin_checking::AtomicConstraint; + use crate::engine::Assignments; use crate::engine::variables::DomainId; use crate::predicate; @@ -231,6 +234,32 @@ impl std::fmt::Debug for Predicate { } } +#[cfg(feature = "check-propagations")] +impl AtomicConstraint for Predicate { + type Identifier = DomainId; + + fn identifier(&self) -> Self::Identifier { + self.get_domain() + } + + fn comparison(&self) -> pumpkin_checking::Comparison { + match self.get_predicate_type() { + PredicateType::LowerBound => pumpkin_checking::Comparison::GreaterEqual, + PredicateType::UpperBound => pumpkin_checking::Comparison::LessEqual, + PredicateType::NotEqual => pumpkin_checking::Comparison::NotEqual, + PredicateType::Equal => pumpkin_checking::Comparison::Equal, + } + } + + fn value(&self) -> i32 { + self.get_right_hand_side() + } + + fn negate(&self) -> Self { + !*self + } +} + #[cfg(test)] mod test { use super::Predicate; diff --git a/pumpkin-crates/core/src/engine/state.rs b/pumpkin-crates/core/src/engine/state.rs index 185d48485..61bd4c608 100644 --- a/pumpkin-crates/core/src/engine/state.rs +++ b/pumpkin-crates/core/src/engine/state.rs @@ -2,6 +2,8 @@ use std::sync::Arc; #[cfg(feature = "check-propagations")] use pumpkin_checking::InferenceChecker; +#[cfg(feature = "check-propagations")] +use pumpkin_checking::VariableState; use crate::basic_types::PropagatorConflict; #[cfg(feature = "check-propagations")] @@ -28,6 +30,7 @@ use crate::proof::ProofLog; use crate::propagation::CurrentNogood; use crate::propagation::Domains; use crate::propagation::ExplanationContext; +use crate::propagation::InferenceCheckers; use crate::propagation::PropagationContext; use crate::propagation::Propagator; use crate::propagation::PropagatorConstructor; @@ -76,7 +79,26 @@ pub struct State { statistics: StateStatistics, #[cfg(feature = "check-propagations")] - checkers: HashMap>, + checkers: HashMap, +} + +/// Wrapper around `Box>` that implements [`Clone`]. +#[cfg(feature = "check-propagations")] +#[derive(Debug)] +struct BoxedChecker(Box>); + +#[cfg(feature = "check-propagations")] +impl Clone for BoxedChecker { + fn clone(&self) -> Self { + BoxedChecker(dyn_clone::clone_box(&*self.0)) + } +} + +#[cfg(feature = "check-propagations")] +impl BoxedChecker { + fn check(&self, variable_state: VariableState) -> bool { + self.0.check(variable_state) + } } create_statistics_struct!(StateStatistics { @@ -357,6 +379,8 @@ impl State { Constructor: PropagatorConstructor, Constructor::PropagatorImpl: 'static, { + constructor.add_inference_checkers(InferenceCheckers::new(self)); + let original_handle: PropagatorHandle = self.propagators.new_propagator().key(); let constructor_context = @@ -380,6 +404,28 @@ impl State { handle } + + /// Add an inference checker to the state. + /// + /// The inference checker will be used to check propagations performed during + /// [`Self::propagate_to_fixed_point`]. + /// + /// If an inference checker already exists for the given inference code, a panic is triggered. + #[cfg(feature = "check-propagations")] + pub fn add_inference_checker( + &mut self, + inference_code: InferenceCode, + checker: impl Into>>, + ) { + let previous_checker = self + .checkers + .insert(inference_code, BoxedChecker(checker.into())); + + assert!( + previous_checker.is_none(), + "cannot add multiple checkers for the same inference code" + ); + } } /// Operations for retrieving propagators. @@ -589,6 +635,29 @@ impl State { Err(conflict) => { self.statistics.num_conflicts += 1; if let Conflict::Propagator(inner) = &conflict { + #[cfg(feature = "check-propagations")] + { + let checker = + self.checkers.get(&inner.inference_code).unwrap_or_else(|| { + panic!( + "missing checker for inference code {:?}", + inner.inference_code + ) + }); + + let variable_state = VariableState::prepare_for_conflict_check( + inner.conjunction.clone(), + None, + ) + .unwrap_or_else(|| { + panic!( + "inconsistent atomics in inference by {:?}", + inner.inference_code, + ) + }); + + assert!(checker.check(variable_state)); + } pumpkin_assert_advanced!(DebugHelper::debug_reported_failure( &self.trailed_values, &self.assignments, @@ -606,7 +675,7 @@ impl State { } #[cfg(not(feature = "check-propagations"))] - fn check_propagations(&self, _: usize) { + fn check_propagations(&mut self, _: usize) { // If the feature is disabled, nothing happens here. The compiler will remove the method // call. } @@ -615,20 +684,43 @@ impl State { /// /// If the checker rejects the inference, this method panics. #[cfg(feature = "check-propagations")] - fn check_propagations(&self, first_propagation_index: usize) { + fn check_propagations(&mut self, first_propagation_index: usize) { + let mut reason_buffer = vec![]; + for trail_index in first_propagation_index..self.assignments.num_trail_entries() { + use pumpkin_checking::VariableState; + let entry = self.assignments.get_trail_entry(trail_index); - let (_, inference_code) = entry + let (reason_ref, inference_code) = entry .reason .expect("propagations should only be checked after propagations"); + reason_buffer.clear(); + let reason_exists = self.reason_store.get_or_compute( + reason_ref, + ExplanationContext::without_working_nogood( + &self.assignments, + trail_index, + &mut self.notification_engine, + ), + &mut self.propagators, + &mut reason_buffer, + ); + assert!(reason_exists, "all propagations have reasons"); + let checker = self .checkers .get(&inference_code) .unwrap_or_else(|| panic!("missing checker for inference code {inference_code:?}")); - assert!(checker.check()); + let variable_state = VariableState::prepare_for_conflict_check( + reason_buffer.drain(..), + Some(entry.predicate), + ) + .unwrap_or_else(|| panic!("inconsistent atomics in inference by {inference_code:?}")); + + assert!(checker.check(variable_state)); } } diff --git a/pumpkin-crates/core/src/engine/variables/affine_view.rs b/pumpkin-crates/core/src/engine/variables/affine_view.rs index aa60c4836..b16dfb7a3 100644 --- a/pumpkin-crates/core/src/engine/variables/affine_view.rs +++ b/pumpkin-crates/core/src/engine/variables/affine_view.rs @@ -1,6 +1,10 @@ use std::cmp::Ordering; use enumset::EnumSet; +#[cfg(feature = "check-propagations")] +use pumpkin_checking::CheckerVariable; +#[cfg(feature = "check-propagations")] +use pumpkin_checking::I32Ext; use super::TransformableVariable; use crate::engine::Assignments; @@ -48,6 +52,102 @@ impl AffineView { } } +#[cfg(feature = "check-propagations")] +impl CheckerVariable for AffineView { + fn atomic_less_than(&self, value: i32) -> Predicate { + use crate::predicate; + + predicate![self <= value] + } + + fn atomic_greater_than(&self, value: i32) -> Predicate { + use crate::predicate; + + predicate![self >= value] + } + + fn atomic_equal(&self, value: i32) -> Predicate { + use crate::predicate; + + predicate![self == value] + } + + fn atomic_not_equal(&self, value: i32) -> Predicate { + use crate::predicate; + + predicate![self != value] + } + + fn induced_lower_bound( + &self, + variable_state: &pumpkin_checking::VariableState, + ) -> I32Ext { + if self.scale.is_positive() { + match self.inner.induced_lower_bound(variable_state) { + I32Ext::I32(value) => I32Ext::I32(self.map(value)), + bound => bound, + } + } else { + match self.inner.induced_upper_bound(variable_state) { + I32Ext::I32(value) => I32Ext::I32(self.map(value)), + I32Ext::NegativeInf => I32Ext::PositiveInf, + I32Ext::PositiveInf => I32Ext::NegativeInf, + } + } + } + + fn induced_upper_bound( + &self, + variable_state: &pumpkin_checking::VariableState, + ) -> I32Ext { + if self.scale.is_positive() { + match self.inner.induced_upper_bound(variable_state) { + I32Ext::I32(value) => I32Ext::I32(self.map(value)), + bound => bound, + } + } else { + match self.inner.induced_lower_bound(variable_state) { + I32Ext::I32(value) => I32Ext::I32(self.map(value)), + I32Ext::NegativeInf => I32Ext::PositiveInf, + I32Ext::PositiveInf => I32Ext::NegativeInf, + } + } + } + + fn induced_fixed_value( + &self, + variable_state: &pumpkin_checking::VariableState, + ) -> Option { + self.inner + .induced_fixed_value(variable_state) + .map(|value| self.map(value)) + } + + fn induced_holes<'this, 'state>( + &'this self, + _variable_state: &'state pumpkin_checking::VariableState, + ) -> impl Iterator + 'state + where + 'this: 'state, + { + todo!("how to iterate holes of a scaled domain"); + #[allow(unreachable_code, reason = "todo does not compile to impl Iterator")] + std::iter::empty() + } + + fn iter_induced_domain<'this, 'state>( + &'this self, + variable_state: &'state pumpkin_checking::VariableState, + ) -> Option + 'state> + where + 'this: 'state, + { + self.inner + .iter_induced_domain(variable_state) + .map(|iter| iter.map(|value| self.map(value))) + } +} + impl IntegerVariable for AffineView where View: IntegerVariable, diff --git a/pumpkin-crates/core/src/engine/variables/domain_id.rs b/pumpkin-crates/core/src/engine/variables/domain_id.rs index 53014e6d2..cc8dfdebe 100644 --- a/pumpkin-crates/core/src/engine/variables/domain_id.rs +++ b/pumpkin-crates/core/src/engine/variables/domain_id.rs @@ -1,4 +1,6 @@ use enumset::EnumSet; +#[cfg(feature = "check-propagations")] +use pumpkin_checking::CheckerVariable; use super::TransformableVariable; use crate::containers::StorageKey; @@ -8,6 +10,8 @@ use crate::engine::notifications::OpaqueDomainEvent; use crate::engine::notifications::Watchers; use crate::engine::variables::AffineView; use crate::engine::variables::IntegerVariable; +#[cfg(feature = "check-propagations")] +use crate::predicates::Predicate; use crate::pumpkin_assert_simple; /// A structure which represents the most basic [`IntegerVariable`]; it is simply the id which links @@ -28,6 +32,74 @@ impl DomainId { } } +#[cfg(feature = "check-propagations")] +impl CheckerVariable for DomainId { + fn atomic_less_than(&self, value: i32) -> Predicate { + use crate::predicate; + + predicate![self <= value] + } + + fn atomic_greater_than(&self, value: i32) -> Predicate { + use crate::predicate; + + predicate![self >= value] + } + + fn atomic_equal(&self, value: i32) -> Predicate { + use crate::predicate; + + predicate![self == value] + } + + fn atomic_not_equal(&self, value: i32) -> Predicate { + use crate::predicate; + + predicate![self != value] + } + + fn induced_lower_bound( + &self, + variable_state: &pumpkin_checking::VariableState, + ) -> pumpkin_checking::I32Ext { + variable_state.lower_bound(self) + } + + fn induced_upper_bound( + &self, + variable_state: &pumpkin_checking::VariableState, + ) -> pumpkin_checking::I32Ext { + variable_state.upper_bound(self) + } + + fn induced_fixed_value( + &self, + variable_state: &pumpkin_checking::VariableState, + ) -> Option { + variable_state.fixed_value(self) + } + + fn induced_holes<'this, 'state>( + &'this self, + variable_state: &'state pumpkin_checking::VariableState, + ) -> impl Iterator + 'state + where + 'this: 'state, + { + variable_state.holes(self) + } + + fn iter_induced_domain<'this, 'state>( + &'this self, + variable_state: &'state pumpkin_checking::VariableState, + ) -> Option + 'state> + where + 'this: 'state, + { + variable_state.iter_domain(self) + } +} + impl IntegerVariable for DomainId { type AffineView = AffineView; diff --git a/pumpkin-crates/core/src/engine/variables/integer_variable.rs b/pumpkin-crates/core/src/engine/variables/integer_variable.rs index 61f4791d7..35922ffed 100644 --- a/pumpkin-crates/core/src/engine/variables/integer_variable.rs +++ b/pumpkin-crates/core/src/engine/variables/integer_variable.rs @@ -1,6 +1,16 @@ use std::fmt::Debug; use enumset::EnumSet; +// When the `check-propagations` feature is enabled, all variables should be `CheckerVariable`. +// However, it is not possible to conditionally impose a supertrait bound. So we define a trait +// `CheckerVariable` that does nothing if the feature is disabled, and implement that trait for +// every type. +#[cfg(feature = "check-propagations")] +use pumpkin_checking::CheckerVariable; +#[cfg(not(feature = "check-propagations"))] +pub trait CheckerVariable {} +#[cfg(not(feature = "check-propagations"))] +impl CheckerVariable for T {} use super::TransformableVariable; use crate::engine::Assignments; @@ -8,11 +18,16 @@ use crate::engine::notifications::DomainEvent; use crate::engine::notifications::OpaqueDomainEvent; use crate::engine::notifications::Watchers; use crate::engine::predicates::predicate_constructor::PredicateConstructor; +use crate::predicates::Predicate; /// A trait specifying the required behaviour of an integer variable such as retrieving a /// lower-bound ([`IntegerVariable::lower_bound`]). pub trait IntegerVariable: - Clone + PredicateConstructor + TransformableVariable + Debug + Clone + + PredicateConstructor + + TransformableVariable + + Debug + + CheckerVariable { type AffineView: IntegerVariable; diff --git a/pumpkin-crates/core/src/engine/variables/literal.rs b/pumpkin-crates/core/src/engine/variables/literal.rs index 6ecee24b9..514b2ea12 100644 --- a/pumpkin-crates/core/src/engine/variables/literal.rs +++ b/pumpkin-crates/core/src/engine/variables/literal.rs @@ -1,6 +1,12 @@ use std::ops::Not; use enumset::EnumSet; +#[cfg(feature = "check-propagations")] +use pumpkin_checking::CheckerVariable; +#[cfg(feature = "check-propagations")] +use pumpkin_checking::I32Ext; +#[cfg(feature = "check-propagations")] +use pumpkin_checking::VariableState; use super::DomainId; use super::IntegerVariable; @@ -51,6 +57,55 @@ impl Not for Literal { } } +/// Forwards a function implementation to the field on self. +macro_rules! forward { + ( + $field:ident, + fn $(<$($lt:lifetime),+>)? $name:ident( + & $($lt_self:lifetime)? self, + $($param_name:ident : $param_type:ty),* + ) -> $return_type:ty + $(where $($where_clause:tt)*)? + ) => { + fn $name$(<$($lt),+>)?( + & $($lt_self)? self, + $($param_name: $param_type),* + ) -> $return_type $(where $($where_clause)*)? { + self.$field.$name($($param_name),*) + } + } +} + +#[cfg(feature = "check-propagations")] +impl CheckerVariable for Literal { + forward!(integer_variable, fn atomic_less_than(&self, value: i32) -> Predicate); + forward!(integer_variable, fn atomic_greater_than(&self, value: i32) -> Predicate); + forward!(integer_variable, fn atomic_not_equal(&self, value: i32) -> Predicate); + forward!(integer_variable, fn atomic_equal(&self, value: i32) -> Predicate); + + forward!(integer_variable, fn induced_lower_bound(&self, variable_state: &VariableState) -> I32Ext); + forward!(integer_variable, fn induced_upper_bound(&self, variable_state: &VariableState) -> I32Ext); + forward!(integer_variable, fn induced_fixed_value(&self, variable_state: &VariableState) -> Option); + forward!( + integer_variable, + fn <'this, 'state> induced_holes( + &'this self, + variable_state: &'state VariableState + ) -> impl Iterator + 'state + where + 'this: 'state, + ); + forward!( + integer_variable, + fn <'this, 'state> iter_induced_domain( + &'this self, + variable_state: &'state VariableState + ) -> Option + 'state> + where + 'this: 'state, + ); +} + impl IntegerVariable for Literal { type AffineView = AffineView; diff --git a/pumpkin-crates/core/src/propagation/constructor.rs b/pumpkin-crates/core/src/propagation/constructor.rs index b6e6652c5..cf69e2b70 100644 --- a/pumpkin-crates/core/src/propagation/constructor.rs +++ b/pumpkin-crates/core/src/propagation/constructor.rs @@ -1,6 +1,9 @@ use std::ops::Deref; use std::ops::DerefMut; +#[cfg(feature = "check-propagations")] +use pumpkin_checking::InferenceChecker; + use super::Domains; use super::LocalId; use super::Propagator; @@ -18,6 +21,7 @@ use crate::engine::variables::AffineView; #[cfg(doc)] use crate::engine::variables::DomainId; use crate::predicates::Predicate; +use crate::proof::InferenceCode; #[cfg(doc)] use crate::propagation::DomainEvent; use crate::propagation::DomainEvents; @@ -33,10 +37,42 @@ pub trait PropagatorConstructor { /// The propagator that is produced by this constructor. type PropagatorImpl: Propagator + Clone; + /// Add inference checkers to the solver if applicable. + /// + /// By default this does nothing, and should only be implemented when `check-propagations` is + /// turned on. + fn add_inference_checkers(&self, _checkers: InferenceCheckers<'_>) {} + /// Create the propagator instance from `Self`. fn create(self, context: PropagatorConstructorContext) -> Self::PropagatorImpl; } +/// Holds all inference checkers in the solver. +/// +/// Only useful if the `check-propagations` feature is enabled. +#[derive(Debug)] +pub struct InferenceCheckers<'state> { + state: &'state mut State, +} + +impl<'state> InferenceCheckers<'state> { + pub(crate) fn new(state: &'state mut State) -> Self { + InferenceCheckers { state } + } +} + +#[cfg(feature = "check-propagations")] +impl InferenceCheckers<'_> { + /// Forwards to [`State::add_inference_checker`]. + pub fn add_inference_checker( + &mut self, + inference_code: InferenceCode, + checker: Box>, + ) { + self.state.add_inference_checker(inference_code, checker); + } +} + /// [`PropagatorConstructorContext`] is used when [`Propagator`]s are initialised after creation. /// /// It represents a communication point between the [`Solver`] and the [`Propagator`]. @@ -177,6 +213,19 @@ impl PropagatorConstructorContext<'_> { } } + /// Add an inference checker for inferences produced by the propagator. + /// + /// If the `check-propagations` feature is enabled, this forwards to + /// [`State::add_inference_checker`]. Otherwise, nothing happens. + #[cfg(feature = "check-propagations")] + pub fn add_inference_checker( + &mut self, + inference_code: InferenceCode, + checker: Box>, + ) { + self.state.add_inference_checker(inference_code, checker); + } + /// Set the next local id to be at least one more than the largest encountered local id. fn update_next_local_id(&mut self, local_id: LocalId) { let next_local_id = (*self.next_local_id.deref()).max(LocalId::from(local_id.unpack() + 1)); diff --git a/pumpkin-crates/propagators/Cargo.toml b/pumpkin-crates/propagators/Cargo.toml index 4c7e40f4a..3b58b1a1f 100644 --- a/pumpkin-crates/propagators/Cargo.toml +++ b/pumpkin-crates/propagators/Cargo.toml @@ -12,6 +12,7 @@ workspace = true [dependencies] pumpkin-core = { version = "0.2.2", path = "../core" } +pumpkin-checking = { version = "0.2.2", path = "../checking", optional = true } enumset = "1.1.2" bitfield-struct = "0.9.2" convert_case = "0.6.0" @@ -22,3 +23,4 @@ pumpkin-constraints = { version = "0.2.2", path = "../constraints" } [features] clap = ["dep:clap", "pumpkin-core/clap"] +include-checkers = ["dep:pumpkin-checking", "pumpkin-core/check-propagations"] diff --git a/pumpkin-crates/propagators/src/propagators/arithmetic/linear_less_or_equal.rs b/pumpkin-crates/propagators/src/propagators/arithmetic/linear_less_or_equal.rs index b2537cc02..4d3c4a778 100644 --- a/pumpkin-crates/propagators/src/propagators/arithmetic/linear_less_or_equal.rs +++ b/pumpkin-crates/propagators/src/propagators/arithmetic/linear_less_or_equal.rs @@ -1,3 +1,13 @@ +#[cfg(feature = "include-checkers")] +use pumpkin_checking::AtomicConstraint; +#[cfg(feature = "include-checkers")] +use pumpkin_checking::CheckerVariable; +#[cfg(feature = "include-checkers")] +use pumpkin_checking::I32Ext; +#[cfg(feature = "include-checkers")] +use pumpkin_checking::InferenceChecker; +#[cfg(feature = "include-checkers")] +use pumpkin_checking::VariableState; use pumpkin_core::asserts::pumpkin_assert_simple; use pumpkin_core::declare_inference_label; use pumpkin_core::predicate; @@ -9,6 +19,8 @@ use pumpkin_core::propagation::DomainEvents; use pumpkin_core::propagation::Domains; use pumpkin_core::propagation::EnqueueDecision; use pumpkin_core::propagation::ExplanationContext; +#[cfg(feature = "include-checkers")] +use pumpkin_core::propagation::InferenceCheckers; use pumpkin_core::propagation::LocalId; use pumpkin_core::propagation::NotificationContext; use pumpkin_core::propagation::OpaqueDomainEvent; @@ -39,6 +51,17 @@ where { type PropagatorImpl = LinearLessOrEqualPropagator; + #[cfg(feature = "include-checkers")] + fn add_inference_checkers(&self, mut checkers: InferenceCheckers<'_>) { + checkers.add_inference_checker( + InferenceCode::new(self.constraint_tag, LinearBounds), + Box::new(LinearLessOrEqualInferenceChecker::new( + self.x.clone(), + self.c, + )), + ); + } + fn create(self, mut context: PropagatorConstructorContext) -> Self::PropagatorImpl { let LinearLessOrEqualPropagatorArgs { x, @@ -271,6 +294,44 @@ where } } +#[derive(Debug, Clone)] +#[cfg(feature = "include-checkers")] +pub struct LinearLessOrEqualInferenceChecker { + terms: Box<[Var]>, + bound: I32Ext, +} + +#[cfg(feature = "include-checkers")] +impl LinearLessOrEqualInferenceChecker { + pub fn new(terms: Box<[Var]>, bound: i32) -> Self { + LinearLessOrEqualInferenceChecker { + terms, + bound: I32Ext::I32(bound), + } + } +} + +#[cfg(feature = "include-checkers")] +impl InferenceChecker for LinearLessOrEqualInferenceChecker +where + Var: CheckerVariable, + Atomic: AtomicConstraint, +{ + fn check(&self, variable_state: VariableState) -> bool { + // Next, we evaluate the linear inequality. The lower bound of the + // left-hand side must exceed the bound in the constraint. Note that the accumulator is an + // I32Ext, and if the lower bound of one of the terms is -infty, then the left-hand side + // will be -infty regardless of the other terms. + let left_hand_side = self.terms.iter().fold(I32Ext::I32(0), |acc, variable| { + let lower_bound = variable.induced_lower_bound(&variable_state); + + acc + lower_bound + }); + + left_hand_side > self.bound + } +} + #[allow(deprecated, reason = "Will be refactored")] #[cfg(test)] mod tests { diff --git a/pumpkin-solver/Cargo.toml b/pumpkin-solver/Cargo.toml index e31497273..6beaa78d6 100644 --- a/pumpkin-solver/Cargo.toml +++ b/pumpkin-solver/Cargo.toml @@ -33,6 +33,7 @@ workspace = true [features] debug-checks = ["pumpkin-core/debug-checks"] +check-propagations = ["pumpkin-core/check-propagations", "pumpkin-propagators/include-checkers"] [build-dependencies] cc = "1.1.30" From cd9e2081b8fafc4f2e4a88205bb122662629c2f7 Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Sun, 11 Jan 2026 13:52:37 +0100 Subject: [PATCH 08/48] Update documentation for pumpkin-checking --- .../checking/src/atomic_constraint.rs | 46 ++ pumpkin-crates/checking/src/i32_ext.rs | 118 ++++ pumpkin-crates/checking/src/lib.rs | 602 +----------------- pumpkin-crates/checking/src/variable.rs | 45 ++ pumpkin-crates/checking/src/variable_state.rs | 401 ++++++++++++ 5 files changed, 628 insertions(+), 584 deletions(-) create mode 100644 pumpkin-crates/checking/src/atomic_constraint.rs create mode 100644 pumpkin-crates/checking/src/i32_ext.rs create mode 100644 pumpkin-crates/checking/src/variable.rs create mode 100644 pumpkin-crates/checking/src/variable_state.rs diff --git a/pumpkin-crates/checking/src/atomic_constraint.rs b/pumpkin-crates/checking/src/atomic_constraint.rs new file mode 100644 index 000000000..0f1f7b2fa --- /dev/null +++ b/pumpkin-crates/checking/src/atomic_constraint.rs @@ -0,0 +1,46 @@ +use std::fmt::Display; + +/// Captures the data associated with an atomic constraint. +/// +/// An atomic constraint has the form `[identifier op value]`, where: +/// - `identifier` identifies a variable, +/// - `op` is a [`Comparison`], +/// - and `value` is an integer. +pub trait AtomicConstraint: Sized { + /// The type of identifier used for variables. + type Identifier; + + /// The identifier of this atomic constraint. + fn identifier(&self) -> Self::Identifier; + + /// The [`Comparison`] used for this atomic constraint. + fn comparison(&self) -> Comparison; + + /// The value on the right-hand side of this atomic constraint. + fn value(&self) -> i32; + + /// The strongest atomic constraint that is mutually exclusive with self. + fn negate(&self) -> Self; +} + +/// An arithmetic comparison between two integers. +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum Comparison { + GreaterEqual, + LessEqual, + Equal, + NotEqual, +} + +impl Display for Comparison { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let s = match self { + Comparison::GreaterEqual => ">=", + Comparison::LessEqual => "<=", + Comparison::Equal => "==", + Comparison::NotEqual => "!=", + }; + + write!(f, "{s}") + } +} diff --git a/pumpkin-crates/checking/src/i32_ext.rs b/pumpkin-crates/checking/src/i32_ext.rs new file mode 100644 index 000000000..13a43e05d --- /dev/null +++ b/pumpkin-crates/checking/src/i32_ext.rs @@ -0,0 +1,118 @@ +use std::{ + cmp::Ordering, + ops::{Add, Mul}, +}; + +/// An [`i32`] or positive/negative infinity. +/// +/// # Note +/// The result of the operation `infty + -infty` is undetermined, and if evaluated will cause a +/// panic. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum I32Ext { + I32(i32), + NegativeInf, + PositiveInf, +} + +impl From for I32Ext { + fn from(value: i32) -> Self { + I32Ext::I32(value) + } +} + +impl PartialEq for I32Ext { + fn eq(&self, other: &i32) -> bool { + match self { + I32Ext::I32(v1) => v1 == other, + I32Ext::NegativeInf | I32Ext::PositiveInf => false, + } + } +} + +impl PartialOrd for I32Ext { + fn partial_cmp(&self, other: &I32Ext) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for I32Ext { + fn cmp(&self, other: &Self) -> Ordering { + match self { + I32Ext::I32(v1) => match other { + I32Ext::I32(v2) => v1.cmp(v2), + I32Ext::NegativeInf => Ordering::Greater, + I32Ext::PositiveInf => Ordering::Less, + }, + I32Ext::NegativeInf => match other { + I32Ext::I32(_) => Ordering::Less, + I32Ext::PositiveInf => Ordering::Less, + I32Ext::NegativeInf => Ordering::Equal, + }, + I32Ext::PositiveInf => match other { + I32Ext::I32(_) => Ordering::Greater, + I32Ext::NegativeInf => Ordering::Greater, + I32Ext::PositiveInf => Ordering::Greater, + }, + } + } +} + +impl PartialOrd for I32Ext { + fn partial_cmp(&self, other: &i32) -> Option { + match self { + I32Ext::I32(v1) => v1.partial_cmp(other), + I32Ext::NegativeInf => Some(Ordering::Less), + I32Ext::PositiveInf => Some(Ordering::Greater), + } + } +} + +impl Add for I32Ext { + type Output = I32Ext; + + fn add(self, rhs: I32Ext) -> Self::Output { + match (self, rhs) { + (I32Ext::I32(lhs), I32Ext::I32(rhs)) => I32Ext::I32(lhs + rhs), + (I32Ext::NegativeInf, I32Ext::NegativeInf) => I32Ext::NegativeInf, + (I32Ext::PositiveInf, I32Ext::PositiveInf) => I32Ext::PositiveInf, + (lhs, rhs) => panic!("the result of {lhs:?} + {rhs:?} is indeterminate"), + } + } +} + +impl Add for I32Ext { + type Output = I32Ext; + + fn add(self, rhs: i32) -> Self::Output { + match self { + I32Ext::I32(lhs) => I32Ext::I32(lhs + rhs), + I32Ext::NegativeInf => I32Ext::NegativeInf, + I32Ext::PositiveInf => I32Ext::PositiveInf, + } + } +} + +impl Mul for I32Ext { + type Output = I32Ext; + + fn mul(self, rhs: i32) -> Self::Output { + match self { + I32Ext::I32(lhs) => I32Ext::I32(lhs * rhs), + I32Ext::NegativeInf => { + if rhs >= 0 { + I32Ext::NegativeInf + } else { + I32Ext::PositiveInf + } + } + I32Ext::PositiveInf => { + if rhs >= 0 { + I32Ext::PositiveInf + } else { + I32Ext::NegativeInf + } + } + } + } +} diff --git a/pumpkin-crates/checking/src/lib.rs b/pumpkin-crates/checking/src/lib.rs index bdfb92188..05013e603 100644 --- a/pumpkin-crates/checking/src/lib.rs +++ b/pumpkin-crates/checking/src/lib.rs @@ -1,591 +1,25 @@ -use std::cmp::Ordering; -use std::collections::BTreeSet; -use std::collections::HashMap; +//! Exposes a common interface used to check inferences. +//! +//! The main exposed type is the [`InferenceChecker`], which can be implemented to verify whether +//! inferences are sound w.r.t. an inference rule. + +mod atomic_constraint; +mod i32_ext; +mod variable; +mod variable_state; + use std::fmt::Debug; -use std::fmt::Display; -use std::hash::Hash; -use std::ops::Add; -use std::ops::Mul; use dyn_clone::DynClone; +pub use atomic_constraint::*; +pub use i32_ext::*; +pub use variable::*; +pub use variable_state::*; + +/// An inference checker tests whether the given state is a conflict under the sematics of an +/// inference rule. pub trait InferenceChecker: Debug + DynClone { + /// Returns `true` if `state` is a conflict, and `false` if not. fn check(&self, state: VariableState) -> bool; } - -#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub enum Comparison { - GreaterEqual, - LessEqual, - Equal, - NotEqual, -} - -impl Display for Comparison { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let s = match self { - Comparison::GreaterEqual => ">=", - Comparison::LessEqual => "<=", - Comparison::Equal => "==", - Comparison::NotEqual => "!=", - }; - - write!(f, "{s}") - } -} - -/// A variable. -pub trait CheckerVariable: Debug + Clone { - fn atomic_less_than(&self, value: i32) -> Atomic; - fn atomic_greater_than(&self, value: i32) -> Atomic; - fn atomic_equal(&self, value: i32) -> Atomic; - fn atomic_not_equal(&self, value: i32) -> Atomic; - - /// Get the lower bound of the domain. - fn induced_lower_bound(&self, variable_state: &VariableState) -> I32Ext; - - /// Get the upper bound of the domain. - fn induced_upper_bound(&self, variable_state: &VariableState) -> I32Ext; - - /// Get the value the variable is fixed to, if the variable is fixed. - fn induced_fixed_value(&self, variable_state: &VariableState) -> Option; - - /// Get the holes in the domain. - fn induced_holes<'this, 'state>( - &'this self, - variable_state: &'state VariableState, - ) -> impl Iterator + 'state - where - 'this: 'state; - - /// Iterate the domain of the variable. - /// - /// The order of the values is unspecified. - fn iter_induced_domain<'this, 'state>( - &'this self, - variable_state: &'state VariableState, - ) -> Option + 'state> - where - 'this: 'state; -} - -/// The domains of all variables in the problem. -/// -/// Domains can be reduced through [`VariableState::apply`]. By default, the domain of every -/// variable is infinite. -#[derive(Clone, Debug)] -pub struct VariableState { - domains: HashMap, -} - -impl Default for VariableState { - fn default() -> Self { - Self { - domains: Default::default(), - } - } -} - -pub trait AtomicConstraint: Sized { - type Identifier; - - fn identifier(&self) -> Self::Identifier; - fn comparison(&self) -> Comparison; - fn value(&self) -> i32; - - fn negate(&self) -> Self; -} - -impl VariableState -where - Ident: Hash + Eq, - Atomic: AtomicConstraint, -{ - /// Create a variable state that applies all the premises and, if present, the negation of the - /// consequent. - /// - /// An [`InferenceChecker`] will receive a [`VariableState`] that conforms to this description. - pub fn prepare_for_conflict_check( - premises: impl IntoIterator, - consequent: Option, - ) -> Option { - let mut variable_state = VariableState::default(); - - let negated_consequent = consequent.as_ref().map(AtomicConstraint::negate); - - // Apply all the premises and the negation of the consequent to the state. - if !premises - .into_iter() - .chain(negated_consequent) - .all(|premise| variable_state.apply(&premise)) - { - return None; - } - - Some(variable_state) - } - - /// Get the lower bound of a variable. - pub fn lower_bound(&self, identifier: &Ident) -> I32Ext { - self.domains - .get(identifier) - .map(|domain| domain.lower_bound) - .unwrap_or(I32Ext::NegativeInf) - } - - /// Get the upper bound of a variable. - pub fn upper_bound(&self, identifier: &Ident) -> I32Ext { - self.domains - .get(identifier) - .map(|domain| domain.upper_bound) - .unwrap_or(I32Ext::PositiveInf) - } - - /// Get the holes within the lower and upper bound of the variable expression. - pub fn holes<'a>(&'a self, identifier: &Ident) -> impl Iterator + 'a - where - Ident: 'a, - { - self.domains - .get(identifier) - .map(|domain| domain.holes.iter().copied()) - .into_iter() - .flatten() - } - - /// Get the fixed value of this variable, if it is fixed. - pub fn fixed_value(&self, identifier: &Ident) -> Option { - let domain = self.domains.get(identifier)?; - - if domain.lower_bound == domain.upper_bound { - let I32Ext::I32(value) = domain.lower_bound else { - panic!( - "lower can only equal upper if they are integers, otherwise the sign of infinity makes them different" - ); - }; - - Some(value) - } else { - None - } - } - - /// Obtain an iterator over the domain of the variable. - /// - /// If the domain is unbounded, then `None` is returned. - pub fn iter_domain<'a>(&'a self, identifier: &Ident) -> Option> - where - Ident: 'a, - { - let domain = self.domains.get(identifier)?; - - let I32Ext::I32(lower_bound) = domain.lower_bound else { - // If there is no lower bound, then the domain is unbounded. - return None; - }; - - // Ensure there is also an upper bound. - if !matches!(domain.upper_bound, I32Ext::I32(_)) { - return None; - } - - Some(DomainIterator(DomainIteratorImpl::Domain { - domain, - next_value: lower_bound, - })) - } - - /// Apply the given [`Atomic`] to the state. - /// - /// Returns true if the state remains consistent, or false if the atomic cannot be true in - /// conjunction with previously applied atomics. - pub fn apply(&mut self, atomic: &Atomic) -> bool { - let identifier = atomic.identifier(); - let domain = self.domains.entry(identifier).or_insert(Domain::new()); - - match atomic.comparison() { - Comparison::GreaterEqual => { - domain.tighten_lower_bound(atomic.value()); - } - - Comparison::LessEqual => { - domain.tighten_upper_bound(atomic.value()); - } - - Comparison::Equal => { - domain.tighten_lower_bound(atomic.value()); - domain.tighten_upper_bound(atomic.value()); - } - - Comparison::NotEqual => { - if domain.lower_bound == atomic.value() { - domain.tighten_lower_bound(atomic.value() + 1); - } - - if domain.upper_bound == atomic.value() { - domain.tighten_upper_bound(atomic.value() - 1); - } - - if domain.lower_bound < atomic.value() && domain.upper_bound > atomic.value() { - let _ = domain.holes.insert(atomic.value()); - } - } - } - - domain.is_consistent() - } - - /// Is the given atomic true in the current state. - pub fn is_true(&self, atomic: &Atomic) -> bool { - let Some(domain) = self.domains.get(&atomic.identifier()) else { - return false; - }; - - match atomic.comparison() { - Comparison::GreaterEqual => domain.lower_bound >= atomic.value(), - - Comparison::LessEqual => domain.upper_bound <= atomic.value(), - - Comparison::Equal => { - domain.lower_bound >= atomic.value() && domain.upper_bound <= atomic.value() - } - - Comparison::NotEqual => { - if domain.lower_bound >= atomic.value() { - return true; - } - - if domain.upper_bound <= atomic.value() { - return true; - } - - if domain.holes.contains(&atomic.value()) { - return true; - } - - false - } - } - } -} - -#[derive(Clone, Debug)] -struct Domain { - lower_bound: I32Ext, - upper_bound: I32Ext, - holes: BTreeSet, -} - -impl Domain { - fn new() -> Domain { - Domain { - lower_bound: I32Ext::NegativeInf, - upper_bound: I32Ext::PositiveInf, - holes: BTreeSet::default(), - } - } - - fn tighten_lower_bound(&mut self, bound: i32) { - if self.lower_bound >= bound { - return; - } - - self.lower_bound = I32Ext::I32(bound); - self.holes = self.holes.split_off(&bound); - - // Take care of the condition where the new bound is already a hole in the domain. - if self.holes.contains(&bound) { - self.tighten_lower_bound(bound + 1); - } - } - - fn tighten_upper_bound(&mut self, bound: i32) { - if self.upper_bound <= bound { - return; - } - - self.upper_bound = I32Ext::I32(bound); - - // Note the '+ 1' to keep the elements <= the upper bound instead of < - // the upper bound. - let _ = self.holes.split_off(&(bound + 1)); - - // Take care of the condition where the new bound is already a hole in the domain. - if self.holes.contains(&bound) { - self.tighten_upper_bound(bound - 1); - } - } - - fn is_consistent(&self) -> bool { - // No need to check holes, as the invariant of `Domain` specifies the bounds are as tight - // as possible, taking holes into account. - - self.lower_bound <= self.upper_bound - } -} - -/// An `i32` or infinity. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum I32Ext { - I32(i32), - NegativeInf, - PositiveInf, -} - -impl From for I32Ext { - fn from(value: i32) -> Self { - I32Ext::I32(value) - } -} - -impl PartialEq for I32Ext { - fn eq(&self, other: &i32) -> bool { - match self { - I32Ext::I32(v1) => v1 == other, - I32Ext::NegativeInf | I32Ext::PositiveInf => false, - } - } -} - -impl PartialOrd for I32Ext { - fn partial_cmp(&self, other: &I32Ext) -> Option { - Some(self.cmp(other)) - } -} - -impl Ord for I32Ext { - fn cmp(&self, other: &Self) -> Ordering { - match self { - I32Ext::I32(v1) => match other { - I32Ext::I32(v2) => v1.cmp(v2), - I32Ext::NegativeInf => Ordering::Greater, - I32Ext::PositiveInf => Ordering::Less, - }, - I32Ext::NegativeInf => match other { - I32Ext::I32(_) => Ordering::Less, - I32Ext::PositiveInf => Ordering::Less, - I32Ext::NegativeInf => Ordering::Equal, - }, - I32Ext::PositiveInf => match other { - I32Ext::I32(_) => Ordering::Greater, - I32Ext::NegativeInf => Ordering::Greater, - I32Ext::PositiveInf => Ordering::Greater, - }, - } - } -} - -impl PartialOrd for I32Ext { - fn partial_cmp(&self, other: &i32) -> Option { - match self { - I32Ext::I32(v1) => v1.partial_cmp(other), - I32Ext::NegativeInf => Some(Ordering::Less), - I32Ext::PositiveInf => Some(Ordering::Greater), - } - } -} - -impl Add for I32Ext { - type Output = I32Ext; - - fn add(self, rhs: I32Ext) -> Self::Output { - match (self, rhs) { - (I32Ext::I32(lhs), I32Ext::I32(rhs)) => I32Ext::I32(lhs + rhs), - (I32Ext::NegativeInf, I32Ext::NegativeInf) => I32Ext::NegativeInf, - (I32Ext::PositiveInf, I32Ext::PositiveInf) => I32Ext::PositiveInf, - (lhs, rhs) => panic!("the result of {lhs:?} + {rhs:?} is indeterminate"), - } - } -} - -impl Add for I32Ext { - type Output = I32Ext; - - fn add(self, rhs: i32) -> Self::Output { - match self { - I32Ext::I32(lhs) => I32Ext::I32(lhs + rhs), - I32Ext::NegativeInf => I32Ext::NegativeInf, - I32Ext::PositiveInf => I32Ext::PositiveInf, - } - } -} - -impl Mul for I32Ext { - type Output = I32Ext; - - fn mul(self, rhs: i32) -> Self::Output { - match self { - I32Ext::I32(lhs) => I32Ext::I32(lhs * rhs), - I32Ext::NegativeInf => { - if rhs >= 0 { - I32Ext::NegativeInf - } else { - I32Ext::PositiveInf - } - } - I32Ext::PositiveInf => { - if rhs >= 0 { - I32Ext::PositiveInf - } else { - I32Ext::NegativeInf - } - } - } - } -} - -/// An iterator over the values in the domain of a variable. -#[derive(Debug)] -pub struct DomainIterator<'a>(DomainIteratorImpl<'a>); - -#[derive(Debug)] -enum DomainIteratorImpl<'a> { - Domain { domain: &'a Domain, next_value: i32 }, -} - -impl Iterator for DomainIterator<'_> { - type Item = i32; - - fn next(&mut self) -> Option { - match self.0 { - DomainIteratorImpl::Domain { - domain, - ref mut next_value, - } => { - let I32Ext::I32(upper_bound) = domain.upper_bound else { - panic!("Only finite domains can be iterated.") - }; - - loop { - // We have completed iterating the domain. - if *next_value > upper_bound { - return None; - } - - let value = *next_value; - *next_value += 1; - - // The next value is not part of the domain. - if domain.holes.contains(&value) { - continue; - } - - // Here the value is part of the domain, so we yield it. - return Some(value); - } - } - } - } -} - -#[cfg(test)] -mod tests { - use drcp_format::IntAtomic; - use drcp_format::IntComparison; - - use super::*; - - #[test] - fn domain_iterator_unbounded() { - let state = VariableState::default(); - let iterator = state.iter_domain(&fzn_rs::VariableExpr::Identifier(Rc::from("x1"))); - - assert!(iterator.is_none()); - } - - #[test] - fn domain_iterator_unbounded_lower_bound() { - let mut state = VariableState::default(); - - let variable_name = Rc::from("x1"); - let variable = fzn_rs::VariableExpr::Identifier(Rc::clone(&variable_name)); - - let _ = state.apply(&IntAtomic { - name: variable_name, - comparison: Comparison::LessEqual, - value: 5, - }); - - let iterator = state.iter_domain(&variable); - - assert!(iterator.is_none()); - } - - #[test] - fn domain_iterator_unbounded_upper_bound() { - let mut state = VariableState::default(); - - let variable_name = Rc::from("x1"); - let variable = fzn_rs::VariableExpr::Identifier(Rc::clone(&variable_name)); - - let _ = state.apply(&IntAtomic { - name: variable_name, - comparison: Comparison::GreaterEqual, - value: 5, - }); - - let iterator = state.iter_domain(&variable); - - assert!(iterator.is_none()); - } - - #[test] - fn domain_iterator_bounded_no_holes() { - let mut state = VariableState::default(); - - let variable_name = Rc::from("x1"); - let variable = fzn_rs::VariableExpr::Identifier(Rc::clone(&variable_name)); - - let _ = state.apply(&IntAtomic { - name: Rc::clone(&variable_name), - comparison: Comparison::GreaterEqual, - value: 5, - }); - - let _ = state.apply(&IntAtomic { - name: variable_name, - comparison: Comparison::LessEqual, - value: 10, - }); - - let values = state - .iter_domain(&variable) - .expect("the domain is bounded") - .collect::>(); - - assert_eq!(values, vec![5, 6, 7, 8, 9, 10]); - } - - #[test] - fn domain_iterator_bounded_with_holes() { - let mut state = VariableState::default(); - - let variable_name = Rc::from("x1"); - let variable = fzn_rs::VariableExpr::Identifier(Rc::clone(&variable_name)); - - let _ = state.apply(&IntAtomic { - name: Rc::clone(&variable_name), - comparison: Comparison::GreaterEqual, - value: 5, - }); - - let _ = state.apply(&IntAtomic { - name: Rc::clone(&variable_name), - comparison: Comparison::NotEqual, - value: 7, - }); - - let _ = state.apply(&IntAtomic { - name: variable_name, - comparison: Comparison::LessEqual, - value: 10, - }); - - let values = state - .iter_domain(&variable) - .expect("the domain is bounded") - .collect::>(); - - assert_eq!(values, vec![5, 6, 8, 9, 10]); - } -} diff --git a/pumpkin-crates/checking/src/variable.rs b/pumpkin-crates/checking/src/variable.rs new file mode 100644 index 000000000..a766fa58f --- /dev/null +++ b/pumpkin-crates/checking/src/variable.rs @@ -0,0 +1,45 @@ +use std::fmt::Debug; + +use crate::{AtomicConstraint, I32Ext, VariableState}; + +/// A variable in a constraint satisfaction problem. +pub trait CheckerVariable: Debug + Clone { + /// Get the atomic constraint `[self <= value]`. + fn atomic_less_than(&self, value: i32) -> Atomic; + + /// Get the atomic constraint `[self <= value]`. + fn atomic_greater_than(&self, value: i32) -> Atomic; + + /// Get the atomic constraint `[self == value]`. + fn atomic_equal(&self, value: i32) -> Atomic; + + /// Get the atomic constraint `[self != value]`. + fn atomic_not_equal(&self, value: i32) -> Atomic; + + /// Get the lower bound of the domain. + fn induced_lower_bound(&self, variable_state: &VariableState) -> I32Ext; + + /// Get the upper bound of the domain. + fn induced_upper_bound(&self, variable_state: &VariableState) -> I32Ext; + + /// Get the value the variable is fixed to, if the variable is fixed. + fn induced_fixed_value(&self, variable_state: &VariableState) -> Option; + + /// Get the holes in the domain. + fn induced_holes<'this, 'state>( + &'this self, + variable_state: &'state VariableState, + ) -> impl Iterator + 'state + where + 'this: 'state; + + /// Iterate the domain of the variable. + /// + /// The order of the values is unspecified. + fn iter_induced_domain<'this, 'state>( + &'this self, + variable_state: &'state VariableState, + ) -> Option + 'state> + where + 'this: 'state; +} diff --git a/pumpkin-crates/checking/src/variable_state.rs b/pumpkin-crates/checking/src/variable_state.rs new file mode 100644 index 000000000..4dffa1e77 --- /dev/null +++ b/pumpkin-crates/checking/src/variable_state.rs @@ -0,0 +1,401 @@ +use std::collections::{BTreeSet, HashMap}; +use std::hash::Hash; + +use crate::{AtomicConstraint, Comparison, I32Ext}; + +/// The domains of all variables in the problem. +/// +/// Domains are initially unbounded. This is why bounds are represented as [`I32Ext`]. +/// +/// Domains can be reduced through [`VariableState::apply`]. By default, the domain of every +/// variable is infinite. +#[derive(Clone, Debug)] +pub struct VariableState { + domains: HashMap, +} + +impl Default for VariableState { + fn default() -> Self { + Self { + domains: Default::default(), + } + } +} + +impl VariableState +where + Ident: Hash + Eq, + Atomic: AtomicConstraint, +{ + /// Create a variable state that applies all the premises and, if present, the negation of the + /// consequent. + /// + /// An [`InferenceChecker`] will receive a [`VariableState`] that conforms to this description. + pub fn prepare_for_conflict_check( + premises: impl IntoIterator, + consequent: Option, + ) -> Option { + let mut variable_state = VariableState::default(); + + let negated_consequent = consequent.as_ref().map(AtomicConstraint::negate); + + // Apply all the premises and the negation of the consequent to the state. + if !premises + .into_iter() + .chain(negated_consequent) + .all(|premise| variable_state.apply(&premise)) + { + return None; + } + + Some(variable_state) + } + + /// Get the lower bound of a variable. + pub fn lower_bound(&self, identifier: &Ident) -> I32Ext { + self.domains + .get(identifier) + .map(|domain| domain.lower_bound) + .unwrap_or(I32Ext::NegativeInf) + } + + /// Get the upper bound of a variable. + pub fn upper_bound(&self, identifier: &Ident) -> I32Ext { + self.domains + .get(identifier) + .map(|domain| domain.upper_bound) + .unwrap_or(I32Ext::PositiveInf) + } + + /// Get the holes within the lower and upper bound of the variable expression. + pub fn holes<'a>(&'a self, identifier: &Ident) -> impl Iterator + 'a + where + Ident: 'a, + { + self.domains + .get(identifier) + .map(|domain| domain.holes.iter().copied()) + .into_iter() + .flatten() + } + + /// Get the fixed value of this variable, if it is fixed. + pub fn fixed_value(&self, identifier: &Ident) -> Option { + let domain = self.domains.get(identifier)?; + + if domain.lower_bound == domain.upper_bound { + let I32Ext::I32(value) = domain.lower_bound else { + panic!( + "lower can only equal upper if they are integers, otherwise the sign of infinity makes them different" + ); + }; + + Some(value) + } else { + None + } + } + + /// Obtain an iterator over the domain of the variable. + /// + /// If the domain is unbounded, then `None` is returned. + pub fn iter_domain<'a>(&'a self, identifier: &Ident) -> Option> + where + Ident: 'a, + { + let domain = self.domains.get(identifier)?; + + let I32Ext::I32(lower_bound) = domain.lower_bound else { + // If there is no lower bound, then the domain is unbounded. + return None; + }; + + // Ensure there is also an upper bound. + if !matches!(domain.upper_bound, I32Ext::I32(_)) { + return None; + } + + Some(DomainIterator { + domain, + next_value: lower_bound, + }) + } + + /// Apply the given [`Atomic`] to the state. + /// + /// Returns true if the state remains consistent, or false if the atomic cannot be true in + /// conjunction with previously applied atomics. + pub fn apply(&mut self, atomic: &Atomic) -> bool { + let identifier = atomic.identifier(); + let domain = self.domains.entry(identifier).or_insert(Domain::new()); + + match atomic.comparison() { + Comparison::GreaterEqual => { + domain.tighten_lower_bound(atomic.value()); + } + + Comparison::LessEqual => { + domain.tighten_upper_bound(atomic.value()); + } + + Comparison::Equal => { + domain.tighten_lower_bound(atomic.value()); + domain.tighten_upper_bound(atomic.value()); + } + + Comparison::NotEqual => { + if domain.lower_bound == atomic.value() { + domain.tighten_lower_bound(atomic.value() + 1); + } + + if domain.upper_bound == atomic.value() { + domain.tighten_upper_bound(atomic.value() - 1); + } + + if domain.lower_bound < atomic.value() && domain.upper_bound > atomic.value() { + let _ = domain.holes.insert(atomic.value()); + } + } + } + + domain.is_consistent() + } + + /// Is the given atomic true in the current state. + pub fn is_true(&self, atomic: &Atomic) -> bool { + let Some(domain) = self.domains.get(&atomic.identifier()) else { + return false; + }; + + match atomic.comparison() { + Comparison::GreaterEqual => domain.lower_bound >= atomic.value(), + + Comparison::LessEqual => domain.upper_bound <= atomic.value(), + + Comparison::Equal => { + domain.lower_bound >= atomic.value() && domain.upper_bound <= atomic.value() + } + + Comparison::NotEqual => { + if domain.lower_bound >= atomic.value() { + return true; + } + + if domain.upper_bound <= atomic.value() { + return true; + } + + if domain.holes.contains(&atomic.value()) { + return true; + } + + false + } + } + } +} + +/// A domain inside the variable state. +#[derive(Clone, Debug)] +struct Domain { + lower_bound: I32Ext, + upper_bound: I32Ext, + holes: BTreeSet, +} + +impl Domain { + fn new() -> Domain { + Domain { + lower_bound: I32Ext::NegativeInf, + upper_bound: I32Ext::PositiveInf, + holes: BTreeSet::default(), + } + } + + /// Tighten the lower bound and remove any holes that are no longer strictly larger than the lower bound. + fn tighten_lower_bound(&mut self, bound: i32) { + if self.lower_bound >= bound { + return; + } + + self.lower_bound = I32Ext::I32(bound); + self.holes = self.holes.split_off(&bound); + + // Take care of the condition where the new bound is already a hole in the domain. + if self.holes.contains(&bound) { + self.tighten_lower_bound(bound + 1); + } + } + + /// Tighten the upper bound and remove any holes that are no longer strictly smaller than the upper bound. + fn tighten_upper_bound(&mut self, bound: i32) { + if self.upper_bound <= bound { + return; + } + + self.upper_bound = I32Ext::I32(bound); + + // Note the '+ 1' to keep the elements <= the upper bound instead of < + // the upper bound. + let _ = self.holes.split_off(&(bound + 1)); + + // Take care of the condition where the new bound is already a hole in the domain. + if self.holes.contains(&bound) { + self.tighten_upper_bound(bound - 1); + } + } + + /// Returns true if the domain contains at least one value. + fn is_consistent(&self) -> bool { + // No need to check holes, as the invariant of `Domain` specifies the bounds are as tight + // as possible, taking holes into account. + + self.lower_bound <= self.upper_bound + } +} + +/// An iterator over the values in the domain of a variable. +#[derive(Debug)] +pub struct DomainIterator<'a> { + domain: &'a Domain, + next_value: i32, +} + +impl Iterator for DomainIterator<'_> { + type Item = i32; + + fn next(&mut self) -> Option { + let DomainIterator { domain, next_value } = self; + + let I32Ext::I32(upper_bound) = domain.upper_bound else { + panic!("Only finite domains can be iterated.") + }; + + loop { + // We have completed iterating the domain. + if *next_value > upper_bound { + return None; + } + + let value = *next_value; + *next_value += 1; + + // The next value is not part of the domain. + if domain.holes.contains(&value) { + continue; + } + + // Here the value is part of the domain, so we yield it. + return Some(value); + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn domain_iterator_unbounded() { + let state = VariableState::default(); + let iterator = state.iter_domain(&fzn_rs::VariableExpr::Identifier(Rc::from("x1"))); + + assert!(iterator.is_none()); + } + + #[test] + fn domain_iterator_unbounded_lower_bound() { + let mut state = VariableState::default(); + + let variable_name = Rc::from("x1"); + let variable = fzn_rs::VariableExpr::Identifier(Rc::clone(&variable_name)); + + let _ = state.apply(&IntAtomic { + name: variable_name, + comparison: Comparison::LessEqual, + value: 5, + }); + + let iterator = state.iter_domain(&variable); + + assert!(iterator.is_none()); + } + + #[test] + fn domain_iterator_unbounded_upper_bound() { + let mut state = VariableState::default(); + + let variable_name = Rc::from("x1"); + let variable = fzn_rs::VariableExpr::Identifier(Rc::clone(&variable_name)); + + let _ = state.apply(&IntAtomic { + name: variable_name, + comparison: Comparison::GreaterEqual, + value: 5, + }); + + let iterator = state.iter_domain(&variable); + + assert!(iterator.is_none()); + } + + #[test] + fn domain_iterator_bounded_no_holes() { + let mut state = VariableState::default(); + + let variable_name = Rc::from("x1"); + let variable = fzn_rs::VariableExpr::Identifier(Rc::clone(&variable_name)); + + let _ = state.apply(&IntAtomic { + name: Rc::clone(&variable_name), + comparison: Comparison::GreaterEqual, + value: 5, + }); + + let _ = state.apply(&IntAtomic { + name: variable_name, + comparison: Comparison::LessEqual, + value: 10, + }); + + let values = state + .iter_domain(&variable) + .expect("the domain is bounded") + .collect::>(); + + assert_eq!(values, vec![5, 6, 7, 8, 9, 10]); + } + + #[test] + fn domain_iterator_bounded_with_holes() { + let mut state = VariableState::default(); + + let variable_name = Rc::from("x1"); + let variable = fzn_rs::VariableExpr::Identifier(Rc::clone(&variable_name)); + + let _ = state.apply(&IntAtomic { + name: Rc::clone(&variable_name), + comparison: Comparison::GreaterEqual, + value: 5, + }); + + let _ = state.apply(&IntAtomic { + name: Rc::clone(&variable_name), + comparison: Comparison::NotEqual, + value: 7, + }); + + let _ = state.apply(&IntAtomic { + name: variable_name, + comparison: Comparison::LessEqual, + value: 10, + }); + + let values = state + .iter_domain(&variable) + .expect("the domain is bounded") + .collect::>(); + + assert_eq!(values, vec![5, 6, 8, 9, 10]); + } +} From 18239a2bfe81467d0ff51c4ff90256a8f66cce46 Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Sun, 11 Jan 2026 14:10:01 +0100 Subject: [PATCH 09/48] Cleanup state implementation for running checkers --- pumpkin-crates/core/src/engine/state.rs | 70 +++++++++++-------------- 1 file changed, 30 insertions(+), 40 deletions(-) diff --git a/pumpkin-crates/core/src/engine/state.rs b/pumpkin-crates/core/src/engine/state.rs index 61bd4c608..04603991a 100644 --- a/pumpkin-crates/core/src/engine/state.rs +++ b/pumpkin-crates/core/src/engine/state.rs @@ -607,6 +607,7 @@ impl State { propagator.propagate(context) }; + #[cfg(feature = "check-propagations")] self.check_propagations(num_trail_entries_before); match propagation_status { @@ -636,28 +637,8 @@ impl State { self.statistics.num_conflicts += 1; if let Conflict::Propagator(inner) = &conflict { #[cfg(feature = "check-propagations")] - { - let checker = - self.checkers.get(&inner.inference_code).unwrap_or_else(|| { - panic!( - "missing checker for inference code {:?}", - inner.inference_code - ) - }); - - let variable_state = VariableState::prepare_for_conflict_check( - inner.conjunction.clone(), - None, - ) - .unwrap_or_else(|| { - panic!( - "inconsistent atomics in inference by {:?}", - inner.inference_code, - ) - }); - - assert!(checker.check(variable_state)); - } + self.run_checker(inner.conjunction.clone(), None, &inner.inference_code); + pumpkin_assert_advanced!(DebugHelper::debug_reported_failure( &self.trailed_values, &self.assignments, @@ -674,12 +655,6 @@ impl State { Ok(()) } - #[cfg(not(feature = "check-propagations"))] - fn check_propagations(&mut self, _: usize) { - // If the feature is disabled, nothing happens here. The compiler will remove the method - // call. - } - /// For every propagation on the trail, run the inference checker for it. /// /// If the checker rejects the inference, this method panics. @@ -688,8 +663,6 @@ impl State { let mut reason_buffer = vec![]; for trail_index in first_propagation_index..self.assignments.num_trail_entries() { - use pumpkin_checking::VariableState; - let entry = self.assignments.get_trail_entry(trail_index); let (reason_ref, inference_code) = entry @@ -709,18 +682,11 @@ impl State { ); assert!(reason_exists, "all propagations have reasons"); - let checker = self - .checkers - .get(&inference_code) - .unwrap_or_else(|| panic!("missing checker for inference code {inference_code:?}")); - - let variable_state = VariableState::prepare_for_conflict_check( + self.run_checker( reason_buffer.drain(..), Some(entry.predicate), - ) - .unwrap_or_else(|| panic!("inconsistent atomics in inference by {inference_code:?}")); - - assert!(checker.check(variable_state)); + &inference_code, + ); } } @@ -765,6 +731,30 @@ impl State { } } +#[cfg(feature = "check-propagations")] +impl State { + /// Run the checker for the given inference code on the given inference. + fn run_checker( + &self, + premises: impl IntoIterator, + consequent: Option, + inference_code: &InferenceCode, + ) { + // Get the checker for the inference code. + let checker = self + .checkers + .get(inference_code) + .unwrap_or_else(|| panic!("missing checker for inference code {inference_code:?}")); + + // Construct the variable state for the conflict check. + let variable_state = VariableState::prepare_for_conflict_check(premises, consequent) + .unwrap_or_else(|| panic!("inconsistent atomics in inference by {:?}", inference_code)); + + // Run the conflict check. + assert!(checker.check(variable_state)); + } +} + impl State { /// This is a temporary accessor to help refactoring. pub(crate) fn get_solution_reference(&self) -> SolutionReference<'_> { From 2c00fad6d90e651c71e7e8dcc34a7df4eefac660 Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Fri, 16 Jan 2026 11:21:04 +0100 Subject: [PATCH 10/48] Apply comments on the PR --- pumpkin-crates/checking/src/i32_ext.rs | 14 +++-- pumpkin-crates/checking/src/lib.rs | 26 ++++++++- pumpkin-crates/checking/src/variable.rs | 4 +- pumpkin-crates/checking/src/variable_state.rs | 16 ++++-- pumpkin-crates/core/src/engine/state.rs | 54 ++++++++++--------- .../core/src/propagation/constructor.rs | 6 ++- .../arithmetic/linear_less_or_equal.rs | 10 ++-- 7 files changed, 87 insertions(+), 43 deletions(-) diff --git a/pumpkin-crates/checking/src/i32_ext.rs b/pumpkin-crates/checking/src/i32_ext.rs index 13a43e05d..1de576f62 100644 --- a/pumpkin-crates/checking/src/i32_ext.rs +++ b/pumpkin-crates/checking/src/i32_ext.rs @@ -1,7 +1,7 @@ -use std::{ - cmp::Ordering, - ops::{Add, Mul}, -}; +use std::cmp::Ordering; +use std::iter::Sum; +use std::ops::Add; +use std::ops::Mul; /// An [`i32`] or positive/negative infinity. /// @@ -116,3 +116,9 @@ impl Mul for I32Ext { } } } + +impl Sum for I32Ext { + fn sum>(iter: I) -> Self { + iter.fold(I32Ext::I32(0), |acc, value| acc + value) + } +} diff --git a/pumpkin-crates/checking/src/lib.rs b/pumpkin-crates/checking/src/lib.rs index 05013e603..df63b8142 100644 --- a/pumpkin-crates/checking/src/lib.rs +++ b/pumpkin-crates/checking/src/lib.rs @@ -10,9 +10,8 @@ mod variable_state; use std::fmt::Debug; -use dyn_clone::DynClone; - pub use atomic_constraint::*; +use dyn_clone::DynClone; pub use i32_ext::*; pub use variable::*; pub use variable_state::*; @@ -23,3 +22,26 @@ pub trait InferenceChecker: Debug + DynClone { /// Returns `true` if `state` is a conflict, and `false` if not. fn check(&self, state: VariableState) -> bool; } + +/// Wrapper around `Box>` that implements [`Clone`]. +#[derive(Debug)] +pub struct BoxedChecker(Box>); + +impl Clone for BoxedChecker { + fn clone(&self) -> Self { + BoxedChecker(dyn_clone::clone_box(&*self.0)) + } +} + +impl From>> for BoxedChecker { + fn from(value: Box>) -> Self { + BoxedChecker(value) + } +} + +impl BoxedChecker { + /// See [`InferenceChecker::check`]. + pub fn check(&self, variable_state: VariableState) -> bool { + self.0.check(variable_state) + } +} diff --git a/pumpkin-crates/checking/src/variable.rs b/pumpkin-crates/checking/src/variable.rs index a766fa58f..77a22c4f0 100644 --- a/pumpkin-crates/checking/src/variable.rs +++ b/pumpkin-crates/checking/src/variable.rs @@ -1,6 +1,8 @@ use std::fmt::Debug; -use crate::{AtomicConstraint, I32Ext, VariableState}; +use crate::AtomicConstraint; +use crate::I32Ext; +use crate::VariableState; /// A variable in a constraint satisfaction problem. pub trait CheckerVariable: Debug + Clone { diff --git a/pumpkin-crates/checking/src/variable_state.rs b/pumpkin-crates/checking/src/variable_state.rs index 4dffa1e77..4843bf346 100644 --- a/pumpkin-crates/checking/src/variable_state.rs +++ b/pumpkin-crates/checking/src/variable_state.rs @@ -1,7 +1,10 @@ -use std::collections::{BTreeSet, HashMap}; +use std::collections::BTreeSet; +use std::collections::HashMap; use std::hash::Hash; -use crate::{AtomicConstraint, Comparison, I32Ext}; +use crate::AtomicConstraint; +use crate::Comparison; +use crate::I32Ext; /// The domains of all variables in the problem. /// @@ -30,6 +33,9 @@ where /// Create a variable state that applies all the premises and, if present, the negation of the /// consequent. /// + /// If `premises /\ !consequent` contain mutually exclusive atomic constraints (e.g., `[x >= + /// 5]` and `[x <= 2]`) then `None` is returned. + /// /// An [`InferenceChecker`] will receive a [`VariableState`] that conforms to this description. pub fn prepare_for_conflict_check( premises: impl IntoIterator, @@ -212,7 +218,8 @@ impl Domain { } } - /// Tighten the lower bound and remove any holes that are no longer strictly larger than the lower bound. + /// Tighten the lower bound and remove any holes that are no longer strictly larger than the + /// lower bound. fn tighten_lower_bound(&mut self, bound: i32) { if self.lower_bound >= bound { return; @@ -227,7 +234,8 @@ impl Domain { } } - /// Tighten the upper bound and remove any holes that are no longer strictly smaller than the upper bound. + /// Tighten the upper bound and remove any holes that are no longer strictly smaller than the + /// upper bound. fn tighten_upper_bound(&mut self, bound: i32) { if self.upper_bound <= bound { return; diff --git a/pumpkin-crates/core/src/engine/state.rs b/pumpkin-crates/core/src/engine/state.rs index 04603991a..ffa905aa3 100644 --- a/pumpkin-crates/core/src/engine/state.rs +++ b/pumpkin-crates/core/src/engine/state.rs @@ -1,5 +1,7 @@ use std::sync::Arc; +#[cfg(feature = "check-propagations")] +use pumpkin_checking::BoxedChecker; #[cfg(feature = "check-propagations")] use pumpkin_checking::InferenceChecker; #[cfg(feature = "check-propagations")] @@ -79,26 +81,7 @@ pub struct State { statistics: StateStatistics, #[cfg(feature = "check-propagations")] - checkers: HashMap, -} - -/// Wrapper around `Box>` that implements [`Clone`]. -#[cfg(feature = "check-propagations")] -#[derive(Debug)] -struct BoxedChecker(Box>); - -#[cfg(feature = "check-propagations")] -impl Clone for BoxedChecker { - fn clone(&self) -> Self { - BoxedChecker(dyn_clone::clone_box(&*self.0)) - } -} - -#[cfg(feature = "check-propagations")] -impl BoxedChecker { - fn check(&self, variable_state: VariableState) -> bool { - self.0.check(variable_state) - } + checkers: HashMap>, } create_statistics_struct!(StateStatistics { @@ -419,7 +402,7 @@ impl State { ) { let previous_checker = self .checkers - .insert(inference_code, BoxedChecker(checker.into())); + .insert(inference_code, BoxedChecker::from(checker.into())); assert!( previous_checker.is_none(), @@ -634,11 +617,11 @@ impl State { ); } Err(conflict) => { + #[cfg(feature = "check-propagations")] + self.check_conflict(&conflict); + self.statistics.num_conflicts += 1; if let Conflict::Propagator(inner) = &conflict { - #[cfg(feature = "check-propagations")] - self.run_checker(inner.conjunction.clone(), None, &inner.inference_code); - pumpkin_assert_advanced!(DebugHelper::debug_reported_failure( &self.trailed_values, &self.assignments, @@ -655,7 +638,28 @@ impl State { Ok(()) } - /// For every propagation on the trail, run the inference checker for it. + /// Check the inference that triggered the given conflict. + /// + /// Does nothing when the conflict is an empty domain. + /// + /// Panics when the inference checker rejects the conflict. + #[cfg(feature = "check-propagations")] + fn check_conflict(&mut self, conflict: &Conflict) { + if let Conflict::Propagator(propagator_conflict) = conflict { + self.run_checker( + propagator_conflict.conjunction.clone(), + None, + &propagator_conflict.inference_code, + ); + } + } + + /// For every item on the trail starting at index `first_propagation_index`, run the + /// inference checker for it. + /// + /// This method should be called after every propagator invocation, so all elements on the + /// trail starting at `first_propagation_index` should be propagations. Otherwise this function + /// will panic. /// /// If the checker rejects the inference, this method panics. #[cfg(feature = "check-propagations")] diff --git a/pumpkin-crates/core/src/propagation/constructor.rs b/pumpkin-crates/core/src/propagation/constructor.rs index cf69e2b70..b7767a51d 100644 --- a/pumpkin-crates/core/src/propagation/constructor.rs +++ b/pumpkin-crates/core/src/propagation/constructor.rs @@ -1,7 +1,7 @@ use std::ops::Deref; use std::ops::DerefMut; -#[cfg(feature = "check-propagations")] +#[cfg(any(feature = "check-propagations", doc))] use pumpkin_checking::InferenceChecker; use super::Domains; @@ -41,13 +41,15 @@ pub trait PropagatorConstructor { /// /// By default this does nothing, and should only be implemented when `check-propagations` is /// turned on. + /// + /// See [`InferenceChecker`] for more information. fn add_inference_checkers(&self, _checkers: InferenceCheckers<'_>) {} /// Create the propagator instance from `Self`. fn create(self, context: PropagatorConstructorContext) -> Self::PropagatorImpl; } -/// Holds all inference checkers in the solver. +/// Interface used to add [`InferenceChecker`]s to the [`State`]. /// /// Only useful if the `check-propagations` feature is enabled. #[derive(Debug)] diff --git a/pumpkin-crates/propagators/src/propagators/arithmetic/linear_less_or_equal.rs b/pumpkin-crates/propagators/src/propagators/arithmetic/linear_less_or_equal.rs index 4d3c4a778..8777e30d3 100644 --- a/pumpkin-crates/propagators/src/propagators/arithmetic/linear_less_or_equal.rs +++ b/pumpkin-crates/propagators/src/propagators/arithmetic/linear_less_or_equal.rs @@ -322,11 +322,11 @@ where // left-hand side must exceed the bound in the constraint. Note that the accumulator is an // I32Ext, and if the lower bound of one of the terms is -infty, then the left-hand side // will be -infty regardless of the other terms. - let left_hand_side = self.terms.iter().fold(I32Ext::I32(0), |acc, variable| { - let lower_bound = variable.induced_lower_bound(&variable_state); - - acc + lower_bound - }); + let left_hand_side: I32Ext = self + .terms + .iter() + .map(|variable| variable.induced_lower_bound(&variable_state)) + .sum(); left_hand_side > self.bound } From a86f7e38ad4ea59aeaf5d2a5764e241d861c5f09 Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Fri, 16 Jan 2026 11:43:17 +0100 Subject: [PATCH 11/48] Fix missing implementations of CheckerVariable --- pumpkin-checker/src/lib.rs | 3 +- pumpkin-checker/src/math.rs | 23 +++++++ pumpkin-checker/src/model.rs | 127 ++++++++++++++++++++++++++++++----- 3 files changed, 137 insertions(+), 16 deletions(-) create mode 100644 pumpkin-checker/src/math.rs diff --git a/pumpkin-checker/src/lib.rs b/pumpkin-checker/src/lib.rs index 863a4a7c6..47409dc4c 100644 --- a/pumpkin-checker/src/lib.rs +++ b/pumpkin-checker/src/lib.rs @@ -12,9 +12,10 @@ use drcp_format::reader::ProofReader; pub mod deductions; pub mod inferences; - pub mod model; +pub(crate) mod math; + use model::*; /// The errors that can be returned by the checker. diff --git a/pumpkin-checker/src/math.rs b/pumpkin-checker/src/math.rs new file mode 100644 index 000000000..44e2aba3b --- /dev/null +++ b/pumpkin-checker/src/math.rs @@ -0,0 +1,23 @@ +pub(crate) fn div_ceil(lhs: i32, other: i32) -> i32 { + // TODO: The source is taken from the standard library nightly implementation of this + // function and div_floor. Once they are stabilized, these definitions can be removed. + // Tracking issue: https://github.com/rust-lang/rust/issues/88581 + let d = lhs / other; + let r = lhs % other; + if (r > 0 && other > 0) || (r < 0 && other < 0) { + d + 1 + } else { + d + } +} + +pub(crate) fn div_floor(lhs: i32, other: i32) -> i32 { + // TODO: See todo in `div_ceil`. + let d = lhs / other; + let r = lhs % other; + if (r > 0 && other < 0) || (r < 0 && other > 0) { + d - 1 + } else { + d + } +} diff --git a/pumpkin-checker/src/model.rs b/pumpkin-checker/src/model.rs index 4e191c41a..dbfc2b566 100644 --- a/pumpkin-checker/src/model.rs +++ b/pumpkin-checker/src/model.rs @@ -17,6 +17,9 @@ use pumpkin_checking::Comparison; use pumpkin_checking::I32Ext; use pumpkin_checking::VariableState; +use crate::math::div_ceil; +use crate::math::div_floor; + #[derive(Clone, Debug)] pub enum Constraint { Nogood(Nogood), @@ -27,11 +30,21 @@ pub enum Constraint { } #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub struct Atomic(IntAtomic, i32>); +pub enum Atomic { + True, + False, + IntAtomic(IntAtomic, i32>), +} impl From, i32>> for Atomic { fn from(value: IntAtomic, i32>) -> Self { - Atomic(value) + Atomic::IntAtomic(value) + } +} + +impl From for Atomic { + fn from(value: bool) -> Self { + if value { Atomic::True } else { Atomic::False } } } @@ -39,11 +52,19 @@ impl AtomicConstraint for Atomic { type Identifier = Rc; fn identifier(&self) -> Self::Identifier { - Rc::clone(&self.0.name) + match self { + Atomic::True => Rc::from("true"), + Atomic::False => Rc::from("false"), + Atomic::IntAtomic(int_atomic) => Rc::clone(&int_atomic.name), + } } fn comparison(&self) -> Comparison { - match self.0.comparison { + let Atomic::IntAtomic(int_atomic) = self else { + return Comparison::Equal; + }; + + match int_atomic.comparison { drcp_format::IntComparison::GreaterEqual => Comparison::GreaterEqual, drcp_format::IntComparison::LessEqual => Comparison::LessEqual, drcp_format::IntComparison::Equal => Comparison::Equal, @@ -52,12 +73,22 @@ impl AtomicConstraint for Atomic { } fn value(&self) -> i32 { - self.0.value + match self { + Atomic::True => 1, + Atomic::False => 0, + Atomic::IntAtomic(int_atomic) => int_atomic.value, + } } fn negate(&self) -> Self { - let owned = self.0.clone(); - Self(!owned) + match self { + Atomic::True => Atomic::False, + Atomic::False => Atomic::True, + Atomic::IntAtomic(int_atomic) => { + let owned = int_atomic.clone(); + Atomic::IntAtomic(!owned) + } + } } } @@ -94,19 +125,47 @@ impl From> for Variable { impl CheckerVariable for Variable { fn atomic_less_than(&self, value: i32) -> Atomic { - todo!() + match self.0 { + VariableExpr::Identifier(ref name) => Atomic::from(IntAtomic { + name: Rc::clone(name), + comparison: drcp_format::IntComparison::LessEqual, + value, + }), + VariableExpr::Constant(constant) => (constant <= value).into(), + } } fn atomic_greater_than(&self, value: i32) -> Atomic { - todo!() + match self.0 { + VariableExpr::Identifier(ref name) => Atomic::from(IntAtomic { + name: Rc::clone(name), + comparison: drcp_format::IntComparison::GreaterEqual, + value, + }), + VariableExpr::Constant(constant) => (constant >= value).into(), + } } fn atomic_equal(&self, value: i32) -> Atomic { - todo!() + match self.0 { + VariableExpr::Identifier(ref name) => Atomic::from(IntAtomic { + name: Rc::clone(name), + comparison: drcp_format::IntComparison::Equal, + value, + }), + VariableExpr::Constant(constant) => (constant == value).into(), + } } fn atomic_not_equal(&self, value: i32) -> Atomic { - todo!() + match self.0 { + VariableExpr::Identifier(ref name) => Atomic::from(IntAtomic { + name: Rc::clone(name), + comparison: drcp_format::IntComparison::NotEqual, + value, + }), + VariableExpr::Constant(constant) => (constant != value).into(), + } } fn induced_lower_bound(&self, variable_state: &VariableState) -> I32Ext { @@ -181,21 +240,59 @@ pub struct Term { pub variable: Variable, } +impl Term { + /// Apply the inverse transformation of this view on a value, to go from the value in the domain + /// of `self` to a value in the domain of `self.inner`. + fn invert(&self, value: i32, rounding: Rounding) -> i32 { + match rounding { + Rounding::Up => div_ceil(value, self.weight.get()), + Rounding::Down => div_floor(value, self.weight.get()), + } + } +} + +enum Rounding { + Up, + Down, +} + impl CheckerVariable for Term { fn atomic_less_than(&self, value: i32) -> Atomic { - todo!() + if self.weight.is_negative() { + let inverted_value = self.invert(value, Rounding::Up); + self.variable.atomic_greater_than(inverted_value) + } else { + let inverted_value = self.invert(value, Rounding::Down); + self.variable.atomic_less_than(inverted_value) + } } fn atomic_greater_than(&self, value: i32) -> Atomic { - todo!() + if self.weight.is_negative() { + let inverted_value = self.invert(value, Rounding::Down); + self.variable.atomic_less_than(inverted_value) + } else { + let inverted_value = self.invert(value, Rounding::Up); + self.variable.atomic_greater_than(inverted_value) + } } fn atomic_equal(&self, value: i32) -> Atomic { - todo!() + if value % self.weight.get() == 0 { + let inverted_value = self.invert(value, Rounding::Up); + self.variable.atomic_equal(inverted_value) + } else { + Atomic::False + } } fn atomic_not_equal(&self, value: i32) -> Atomic { - todo!() + if value % self.weight.get() == 0 { + let inverted_value = self.invert(value, Rounding::Up); + self.variable.atomic_not_equal(inverted_value) + } else { + Atomic::True + } } fn induced_lower_bound(&self, variable_state: &VariableState) -> I32Ext { From 81d20e0839e40a67c74f250dd0fc89557e157cf0 Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Fri, 16 Jan 2026 12:50:52 +0100 Subject: [PATCH 12/48] Fix clippy suggestions --- Cargo.lock | 15 +---- clippy.toml | 5 +- pumpkin-checker/src/inferences/linear.rs | 28 +++++++-- .../checking/src/atomic_constraint.rs | 28 +++++++++ pumpkin-crates/checking/src/variable_state.rs | 57 ++++++++----------- pumpkin-crates/core/Cargo.toml | 2 +- .../core/src/statistics/statistic_logging.rs | 4 +- pumpkin-crates/propagators/Cargo.toml | 2 +- 8 files changed, 86 insertions(+), 55 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index dc94dfc1f..e171713d9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -210,15 +210,6 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" -[[package]] -name = "convert_case" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca" -dependencies = [ - "unicode-segmentation", -] - [[package]] name = "convert_case" version = "0.8.0" @@ -414,7 +405,7 @@ dependencies = [ name = "fzn-rs-derive" version = "0.1.0" dependencies = [ - "convert_case 0.8.0", + "convert_case", "fzn-rs", "proc-macro2", "quote", @@ -759,7 +750,7 @@ dependencies = [ "bitfield", "bitfield-struct", "clap", - "convert_case 0.6.0", + "convert_case", "downcast-rs", "drcp-format", "dyn-clone", @@ -798,7 +789,7 @@ version = "0.2.2" dependencies = [ "bitfield-struct", "clap", - "convert_case 0.6.0", + "convert_case", "enumset", "pumpkin-checking", "pumpkin-constraints", diff --git a/clippy.toml b/clippy.toml index 937f96fe2..bff8f0240 100644 --- a/clippy.toml +++ b/clippy.toml @@ -1 +1,4 @@ -allowed-duplicate-crates = ["regex-automata", "regex-syntax"] +allowed-duplicate-crates = [ + "hashbrown", + "windows-sys", +] diff --git a/pumpkin-checker/src/inferences/linear.rs b/pumpkin-checker/src/inferences/linear.rs index 8812cb334..00da598b3 100644 --- a/pumpkin-checker/src/inferences/linear.rs +++ b/pumpkin-checker/src/inferences/linear.rs @@ -63,6 +63,10 @@ fn verify_linear_inference( #[cfg(test)] mod tests { + use std::num::NonZero; + use std::rc::Rc; + + use drcp_format::IntAtomic; use drcp_format::IntComparison::*; use fzn_rs::VariableExpr::*; @@ -73,21 +77,34 @@ mod tests { fn linear_1() { // x1 - x2 <= -7 let linear = Linear { - terms: vec![(1, Identifier("x1".into())), (-1, Identifier("x2".into()))], + terms: vec![ + Term { + weight: NonZero::new(1).unwrap(), + variable: Identifier(Rc::from("x1")).into(), + }, + Term { + weight: NonZero::new(-1).unwrap(), + variable: Identifier(Rc::from("x2")).into(), + }, + ], bound: -7, }; - let premises = vec![Atomic { + let premises = vec![Atomic::IntAtomic(IntAtomic { name: "x2".into(), comparison: LessEqual, value: 37, - }]; + })]; - let consequent = Some(Atomic { + let consequent = Some(Atomic::IntAtomic(IntAtomic { name: "x1".into(), comparison: LessEqual, value: 30, - }); + })); + + let variable_state = + VariableState::prepare_for_conflict_check(premises.clone(), consequent.clone()) + .expect("no mutually exclusive atomics"); verify_linear_inference( &linear, @@ -95,6 +112,7 @@ mod tests { premises, consequent, }, + variable_state, ) .expect("valid inference"); } diff --git a/pumpkin-crates/checking/src/atomic_constraint.rs b/pumpkin-crates/checking/src/atomic_constraint.rs index 0f1f7b2fa..893063059 100644 --- a/pumpkin-crates/checking/src/atomic_constraint.rs +++ b/pumpkin-crates/checking/src/atomic_constraint.rs @@ -44,3 +44,31 @@ impl Display for Comparison { write!(f, "{s}") } } + +/// A simple implementation of an [`AtomicConstraint`]. +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct TestAtomic { + pub name: &'static str, + pub comparison: Comparison, + pub value: i32, +} + +impl AtomicConstraint for TestAtomic { + type Identifier = &'static str; + + fn identifier(&self) -> Self::Identifier { + self.name + } + + fn comparison(&self) -> Comparison { + self.comparison + } + + fn value(&self) -> i32 { + self.value + } + + fn negate(&self) -> Self { + todo!() + } +} diff --git a/pumpkin-crates/checking/src/variable_state.rs b/pumpkin-crates/checking/src/variable_state.rs index 4843bf346..8c44b381d 100644 --- a/pumpkin-crates/checking/src/variable_state.rs +++ b/pumpkin-crates/checking/src/variable_state.rs @@ -5,6 +5,8 @@ use std::hash::Hash; use crate::AtomicConstraint; use crate::Comparison; use crate::I32Ext; +#[cfg(doc)] +use crate::InferenceChecker; /// The domains of all variables in the problem. /// @@ -127,7 +129,7 @@ where }) } - /// Apply the given [`Atomic`] to the state. + /// Apply the given `Atomic` to the state. /// /// Returns true if the state remains consistent, or false if the atomic cannot be true in /// conjunction with previously applied atomics. @@ -302,11 +304,12 @@ impl Iterator for DomainIterator<'_> { #[cfg(test)] mod tests { use super::*; + use crate::TestAtomic; #[test] fn domain_iterator_unbounded() { - let state = VariableState::default(); - let iterator = state.iter_domain(&fzn_rs::VariableExpr::Identifier(Rc::from("x1"))); + let state = VariableState::::default(); + let iterator = state.iter_domain(&"x1"); assert!(iterator.is_none()); } @@ -315,16 +318,13 @@ mod tests { fn domain_iterator_unbounded_lower_bound() { let mut state = VariableState::default(); - let variable_name = Rc::from("x1"); - let variable = fzn_rs::VariableExpr::Identifier(Rc::clone(&variable_name)); - - let _ = state.apply(&IntAtomic { - name: variable_name, + let _ = state.apply(&TestAtomic { + name: "x1", comparison: Comparison::LessEqual, value: 5, }); - let iterator = state.iter_domain(&variable); + let iterator = state.iter_domain(&"x1"); assert!(iterator.is_none()); } @@ -333,16 +333,13 @@ mod tests { fn domain_iterator_unbounded_upper_bound() { let mut state = VariableState::default(); - let variable_name = Rc::from("x1"); - let variable = fzn_rs::VariableExpr::Identifier(Rc::clone(&variable_name)); - - let _ = state.apply(&IntAtomic { - name: variable_name, + let _ = state.apply(&TestAtomic { + name: "x1", comparison: Comparison::GreaterEqual, value: 5, }); - let iterator = state.iter_domain(&variable); + let iterator = state.iter_domain(&"x1"); assert!(iterator.is_none()); } @@ -351,23 +348,20 @@ mod tests { fn domain_iterator_bounded_no_holes() { let mut state = VariableState::default(); - let variable_name = Rc::from("x1"); - let variable = fzn_rs::VariableExpr::Identifier(Rc::clone(&variable_name)); - - let _ = state.apply(&IntAtomic { - name: Rc::clone(&variable_name), + let _ = state.apply(&TestAtomic { + name: "x1", comparison: Comparison::GreaterEqual, value: 5, }); - let _ = state.apply(&IntAtomic { - name: variable_name, + let _ = state.apply(&TestAtomic { + name: "x1", comparison: Comparison::LessEqual, value: 10, }); let values = state - .iter_domain(&variable) + .iter_domain(&"x1") .expect("the domain is bounded") .collect::>(); @@ -378,29 +372,26 @@ mod tests { fn domain_iterator_bounded_with_holes() { let mut state = VariableState::default(); - let variable_name = Rc::from("x1"); - let variable = fzn_rs::VariableExpr::Identifier(Rc::clone(&variable_name)); - - let _ = state.apply(&IntAtomic { - name: Rc::clone(&variable_name), + let _ = state.apply(&TestAtomic { + name: "x1", comparison: Comparison::GreaterEqual, value: 5, }); - let _ = state.apply(&IntAtomic { - name: Rc::clone(&variable_name), + let _ = state.apply(&TestAtomic { + name: "x1", comparison: Comparison::NotEqual, value: 7, }); - let _ = state.apply(&IntAtomic { - name: variable_name, + let _ = state.apply(&TestAtomic { + name: "x1", comparison: Comparison::LessEqual, value: 10, }); let values = state - .iter_domain(&variable) + .iter_domain(&"x1") .expect("the domain is bounded") .collect::>(); diff --git a/pumpkin-crates/core/Cargo.toml b/pumpkin-crates/core/Cargo.toml index 6f0ac629e..113905020 100644 --- a/pumpkin-crates/core/Cargo.toml +++ b/pumpkin-crates/core/Cargo.toml @@ -21,7 +21,7 @@ rand = { version = "0.8.5", features = [ "small_rng", "alloc" ] } once_cell = "1.19.0" downcast-rs = "1.2.1" drcp-format = { version = "0.3.0", path = "../../drcp-format" } -convert_case = "0.6.0" +convert_case = "0.8.0" itertools = "0.13.0" bitfield-struct = "0.9.2" num = "0.4.3" diff --git a/pumpkin-crates/core/src/statistics/statistic_logging.rs b/pumpkin-crates/core/src/statistics/statistic_logging.rs index 55eeb98e5..8bcae72aa 100644 --- a/pumpkin-crates/core/src/statistics/statistic_logging.rs +++ b/pumpkin-crates/core/src/statistics/statistic_logging.rs @@ -21,7 +21,7 @@ pub struct StatisticOptions<'a> { // A closing line which is printed after all of the statistics have been printed after_statistics: Option<&'a str>, // The casing of the name of the statistic - statistics_casing: Option, + statistics_casing: Option>, // The writer to which the statistics are written statistics_writer: Box, } @@ -48,7 +48,7 @@ static STATISTIC_OPTIONS: OnceLock> = OnceLock::new(); pub fn configure_statistic_logging( prefix: &'static str, after: Option<&'static str>, - casing: Option, + casing: Option>, writer: Option>, ) { let _ = STATISTIC_OPTIONS.get_or_init(|| { diff --git a/pumpkin-crates/propagators/Cargo.toml b/pumpkin-crates/propagators/Cargo.toml index 3b58b1a1f..3fa28bffd 100644 --- a/pumpkin-crates/propagators/Cargo.toml +++ b/pumpkin-crates/propagators/Cargo.toml @@ -15,7 +15,7 @@ pumpkin-core = { version = "0.2.2", path = "../core" } pumpkin-checking = { version = "0.2.2", path = "../checking", optional = true } enumset = "1.1.2" bitfield-struct = "0.9.2" -convert_case = "0.6.0" +convert_case = "0.8.0" clap = { version = "4.5.40", optional = true, features=["derive"]} [dev-dependencies] From 6e047f3b0a9c74e67a5ec49001e0734486083286 Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Fri, 16 Jan 2026 12:58:39 +0100 Subject: [PATCH 13/48] Add missing feature guard --- pumpkin-crates/core/src/engine/variables/literal.rs | 1 + pumpkin-crates/core/src/propagation/constructor.rs | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/pumpkin-crates/core/src/engine/variables/literal.rs b/pumpkin-crates/core/src/engine/variables/literal.rs index 514b2ea12..217fdebaa 100644 --- a/pumpkin-crates/core/src/engine/variables/literal.rs +++ b/pumpkin-crates/core/src/engine/variables/literal.rs @@ -58,6 +58,7 @@ impl Not for Literal { } /// Forwards a function implementation to the field on self. +#[cfg(feature = "check-propagations")] macro_rules! forward { ( $field:ident, diff --git a/pumpkin-crates/core/src/propagation/constructor.rs b/pumpkin-crates/core/src/propagation/constructor.rs index b7767a51d..10a767a64 100644 --- a/pumpkin-crates/core/src/propagation/constructor.rs +++ b/pumpkin-crates/core/src/propagation/constructor.rs @@ -21,6 +21,7 @@ use crate::engine::variables::AffineView; #[cfg(doc)] use crate::engine::variables::DomainId; use crate::predicates::Predicate; +#[cfg(feature = "check-propagations")] use crate::proof::InferenceCode; #[cfg(doc)] use crate::propagation::DomainEvent; @@ -54,6 +55,10 @@ pub trait PropagatorConstructor { /// Only useful if the `check-propagations` feature is enabled. #[derive(Debug)] pub struct InferenceCheckers<'state> { + #[cfg_attr( + not(feature = "check-propagations"), + allow(unused, "only used when the feature 'check-propagations' is enabled") + )] state: &'state mut State, } From 503d7ccd33b7428c14ccf613f90fb5c527ee5bae Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Fri, 16 Jan 2026 13:04:10 +0100 Subject: [PATCH 14/48] Fix 'bad reason argument' --- pumpkin-crates/core/src/propagation/constructor.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pumpkin-crates/core/src/propagation/constructor.rs b/pumpkin-crates/core/src/propagation/constructor.rs index 10a767a64..86d879683 100644 --- a/pumpkin-crates/core/src/propagation/constructor.rs +++ b/pumpkin-crates/core/src/propagation/constructor.rs @@ -57,7 +57,10 @@ pub trait PropagatorConstructor { pub struct InferenceCheckers<'state> { #[cfg_attr( not(feature = "check-propagations"), - allow(unused, "only used when the feature 'check-propagations' is enabled") + allow( + unused, + reason = "only used when the feature 'check-propagations' is enabled" + ) )] state: &'state mut State, } From 06b9fba1f9a96676bff289a75c5c136a9df3a571 Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Fri, 16 Jan 2026 15:57:15 +0100 Subject: [PATCH 15/48] Reduce required feature guards --- pumpkin-checker/Cargo.toml | 2 +- pumpkin-crates/core/Cargo.toml | 4 ++-- .../core/src/engine/predicates/predicate.rs | 2 -- pumpkin-crates/core/src/engine/state.rs | 8 +------- .../core/src/engine/variables/affine_view.rs | 3 --- .../core/src/engine/variables/domain_id.rs | 3 --- .../src/engine/variables/integer_variable.rs | 9 --------- .../core/src/engine/variables/literal.rs | 5 ----- .../core/src/propagation/constructor.rs | 17 ++--------------- pumpkin-crates/propagators/Cargo.toml | 3 +-- .../arithmetic/linear_less_or_equal.rs | 10 ---------- pumpkin-solver/Cargo.toml | 2 +- 12 files changed, 8 insertions(+), 60 deletions(-) diff --git a/pumpkin-checker/Cargo.toml b/pumpkin-checker/Cargo.toml index 1bbfc3a4e..7847eb968 100644 --- a/pumpkin-checker/Cargo.toml +++ b/pumpkin-checker/Cargo.toml @@ -8,7 +8,7 @@ authors.workspace = true [dependencies] pumpkin-checking = { version = "0.2.2", path = "../pumpkin-crates/checking/" } -pumpkin-propagators = { version = "0.2.2", features = ["include-checkers"], path = "../pumpkin-crates/propagators/" } +pumpkin-propagators = { version = "0.2.2", path = "../pumpkin-crates/propagators/" } anyhow = "1.0.99" clap = { version = "4.5.47", features = ["derive"] } drcp-format = { version = "0.3.0", path = "../drcp-format" } diff --git a/pumpkin-crates/core/Cargo.toml b/pumpkin-crates/core/Cargo.toml index 113905020..1dc43e6b0 100644 --- a/pumpkin-crates/core/Cargo.toml +++ b/pumpkin-crates/core/Cargo.toml @@ -11,7 +11,7 @@ description = "The core of the Pumpkin constraint programming solver." workspace = true [dependencies] -pumpkin-checking = { version = "0.2.2", path = "../checking", optional = true } +pumpkin-checking = { version = "0.2.2", path = "../checking" } thiserror = "2.0.12" log = "0.4.17" bitfield = "0.14.0" @@ -43,6 +43,6 @@ getrandom = { version = "0.2", features = ["js"] } wasm-bindgen-test = "0.3" [features] -check-propagations = ["dep:pumpkin-checking"] +check-propagations = [] debug-checks = [] clap = ["dep:clap"] diff --git a/pumpkin-crates/core/src/engine/predicates/predicate.rs b/pumpkin-crates/core/src/engine/predicates/predicate.rs index 1a8a067d2..2e4826d86 100644 --- a/pumpkin-crates/core/src/engine/predicates/predicate.rs +++ b/pumpkin-crates/core/src/engine/predicates/predicate.rs @@ -1,4 +1,3 @@ -#[cfg(feature = "check-propagations")] use pumpkin_checking::AtomicConstraint; use crate::engine::Assignments; @@ -234,7 +233,6 @@ impl std::fmt::Debug for Predicate { } } -#[cfg(feature = "check-propagations")] impl AtomicConstraint for Predicate { type Identifier = DomainId; diff --git a/pumpkin-crates/core/src/engine/state.rs b/pumpkin-crates/core/src/engine/state.rs index ffa905aa3..afacd92c1 100644 --- a/pumpkin-crates/core/src/engine/state.rs +++ b/pumpkin-crates/core/src/engine/state.rs @@ -1,14 +1,11 @@ use std::sync::Arc; -#[cfg(feature = "check-propagations")] use pumpkin_checking::BoxedChecker; -#[cfg(feature = "check-propagations")] use pumpkin_checking::InferenceChecker; #[cfg(feature = "check-propagations")] use pumpkin_checking::VariableState; use crate::basic_types::PropagatorConflict; -#[cfg(feature = "check-propagations")] use crate::containers::HashMap; use crate::containers::KeyGenerator; use crate::create_statistics_struct; @@ -80,7 +77,6 @@ pub struct State { statistics: StateStatistics, - #[cfg(feature = "check-propagations")] checkers: HashMap>, } @@ -166,7 +162,6 @@ impl Default for State { notification_engine: NotificationEngine::default(), statistics: StateStatistics::default(), constraint_tags: KeyGenerator::default(), - #[cfg(feature = "check-propagations")] checkers: HashMap::default(), }; // As a convention, the assignments contain a dummy domain_id=0, which represents a 0-1 @@ -391,10 +386,9 @@ impl State { /// Add an inference checker to the state. /// /// The inference checker will be used to check propagations performed during - /// [`Self::propagate_to_fixed_point`]. + /// [`Self::propagate_to_fixed_point`], if the `check-propagations` feature is enabled. /// /// If an inference checker already exists for the given inference code, a panic is triggered. - #[cfg(feature = "check-propagations")] pub fn add_inference_checker( &mut self, inference_code: InferenceCode, diff --git a/pumpkin-crates/core/src/engine/variables/affine_view.rs b/pumpkin-crates/core/src/engine/variables/affine_view.rs index b16dfb7a3..a2bfaa7e5 100644 --- a/pumpkin-crates/core/src/engine/variables/affine_view.rs +++ b/pumpkin-crates/core/src/engine/variables/affine_view.rs @@ -1,9 +1,7 @@ use std::cmp::Ordering; use enumset::EnumSet; -#[cfg(feature = "check-propagations")] use pumpkin_checking::CheckerVariable; -#[cfg(feature = "check-propagations")] use pumpkin_checking::I32Ext; use super::TransformableVariable; @@ -52,7 +50,6 @@ impl AffineView { } } -#[cfg(feature = "check-propagations")] impl CheckerVariable for AffineView { fn atomic_less_than(&self, value: i32) -> Predicate { use crate::predicate; diff --git a/pumpkin-crates/core/src/engine/variables/domain_id.rs b/pumpkin-crates/core/src/engine/variables/domain_id.rs index cc8dfdebe..6ad5d2f87 100644 --- a/pumpkin-crates/core/src/engine/variables/domain_id.rs +++ b/pumpkin-crates/core/src/engine/variables/domain_id.rs @@ -1,5 +1,4 @@ use enumset::EnumSet; -#[cfg(feature = "check-propagations")] use pumpkin_checking::CheckerVariable; use super::TransformableVariable; @@ -10,7 +9,6 @@ use crate::engine::notifications::OpaqueDomainEvent; use crate::engine::notifications::Watchers; use crate::engine::variables::AffineView; use crate::engine::variables::IntegerVariable; -#[cfg(feature = "check-propagations")] use crate::predicates::Predicate; use crate::pumpkin_assert_simple; @@ -32,7 +30,6 @@ impl DomainId { } } -#[cfg(feature = "check-propagations")] impl CheckerVariable for DomainId { fn atomic_less_than(&self, value: i32) -> Predicate { use crate::predicate; diff --git a/pumpkin-crates/core/src/engine/variables/integer_variable.rs b/pumpkin-crates/core/src/engine/variables/integer_variable.rs index 35922ffed..280fca407 100644 --- a/pumpkin-crates/core/src/engine/variables/integer_variable.rs +++ b/pumpkin-crates/core/src/engine/variables/integer_variable.rs @@ -1,16 +1,7 @@ use std::fmt::Debug; use enumset::EnumSet; -// When the `check-propagations` feature is enabled, all variables should be `CheckerVariable`. -// However, it is not possible to conditionally impose a supertrait bound. So we define a trait -// `CheckerVariable` that does nothing if the feature is disabled, and implement that trait for -// every type. -#[cfg(feature = "check-propagations")] use pumpkin_checking::CheckerVariable; -#[cfg(not(feature = "check-propagations"))] -pub trait CheckerVariable {} -#[cfg(not(feature = "check-propagations"))] -impl CheckerVariable for T {} use super::TransformableVariable; use crate::engine::Assignments; diff --git a/pumpkin-crates/core/src/engine/variables/literal.rs b/pumpkin-crates/core/src/engine/variables/literal.rs index 217fdebaa..edbf29bb6 100644 --- a/pumpkin-crates/core/src/engine/variables/literal.rs +++ b/pumpkin-crates/core/src/engine/variables/literal.rs @@ -1,11 +1,8 @@ use std::ops::Not; use enumset::EnumSet; -#[cfg(feature = "check-propagations")] use pumpkin_checking::CheckerVariable; -#[cfg(feature = "check-propagations")] use pumpkin_checking::I32Ext; -#[cfg(feature = "check-propagations")] use pumpkin_checking::VariableState; use super::DomainId; @@ -58,7 +55,6 @@ impl Not for Literal { } /// Forwards a function implementation to the field on self. -#[cfg(feature = "check-propagations")] macro_rules! forward { ( $field:ident, @@ -77,7 +73,6 @@ macro_rules! forward { } } -#[cfg(feature = "check-propagations")] impl CheckerVariable for Literal { forward!(integer_variable, fn atomic_less_than(&self, value: i32) -> Predicate); forward!(integer_variable, fn atomic_greater_than(&self, value: i32) -> Predicate); diff --git a/pumpkin-crates/core/src/propagation/constructor.rs b/pumpkin-crates/core/src/propagation/constructor.rs index 86d879683..2a42b537d 100644 --- a/pumpkin-crates/core/src/propagation/constructor.rs +++ b/pumpkin-crates/core/src/propagation/constructor.rs @@ -1,7 +1,6 @@ use std::ops::Deref; use std::ops::DerefMut; -#[cfg(any(feature = "check-propagations", doc))] use pumpkin_checking::InferenceChecker; use super::Domains; @@ -21,7 +20,6 @@ use crate::engine::variables::AffineView; #[cfg(doc)] use crate::engine::variables::DomainId; use crate::predicates::Predicate; -#[cfg(feature = "check-propagations")] use crate::proof::InferenceCode; #[cfg(doc)] use crate::propagation::DomainEvent; @@ -51,17 +49,8 @@ pub trait PropagatorConstructor { } /// Interface used to add [`InferenceChecker`]s to the [`State`]. -/// -/// Only useful if the `check-propagations` feature is enabled. #[derive(Debug)] pub struct InferenceCheckers<'state> { - #[cfg_attr( - not(feature = "check-propagations"), - allow( - unused, - reason = "only used when the feature 'check-propagations' is enabled" - ) - )] state: &'state mut State, } @@ -71,7 +60,6 @@ impl<'state> InferenceCheckers<'state> { } } -#[cfg(feature = "check-propagations")] impl InferenceCheckers<'_> { /// Forwards to [`State::add_inference_checker`]. pub fn add_inference_checker( @@ -225,9 +213,8 @@ impl PropagatorConstructorContext<'_> { /// Add an inference checker for inferences produced by the propagator. /// - /// If the `check-propagations` feature is enabled, this forwards to - /// [`State::add_inference_checker`]. Otherwise, nothing happens. - #[cfg(feature = "check-propagations")] + /// If the `check-propagations` feature is not enabled, adding an [`InferenceChecker`] will not + /// do anything. pub fn add_inference_checker( &mut self, inference_code: InferenceCode, diff --git a/pumpkin-crates/propagators/Cargo.toml b/pumpkin-crates/propagators/Cargo.toml index 3fa28bffd..11679e519 100644 --- a/pumpkin-crates/propagators/Cargo.toml +++ b/pumpkin-crates/propagators/Cargo.toml @@ -12,7 +12,7 @@ workspace = true [dependencies] pumpkin-core = { version = "0.2.2", path = "../core" } -pumpkin-checking = { version = "0.2.2", path = "../checking", optional = true } +pumpkin-checking = { version = "0.2.2", path = "../checking" } enumset = "1.1.2" bitfield-struct = "0.9.2" convert_case = "0.8.0" @@ -23,4 +23,3 @@ pumpkin-constraints = { version = "0.2.2", path = "../constraints" } [features] clap = ["dep:clap", "pumpkin-core/clap"] -include-checkers = ["dep:pumpkin-checking", "pumpkin-core/check-propagations"] diff --git a/pumpkin-crates/propagators/src/propagators/arithmetic/linear_less_or_equal.rs b/pumpkin-crates/propagators/src/propagators/arithmetic/linear_less_or_equal.rs index 8777e30d3..f1b1485ef 100644 --- a/pumpkin-crates/propagators/src/propagators/arithmetic/linear_less_or_equal.rs +++ b/pumpkin-crates/propagators/src/propagators/arithmetic/linear_less_or_equal.rs @@ -1,12 +1,7 @@ -#[cfg(feature = "include-checkers")] use pumpkin_checking::AtomicConstraint; -#[cfg(feature = "include-checkers")] use pumpkin_checking::CheckerVariable; -#[cfg(feature = "include-checkers")] use pumpkin_checking::I32Ext; -#[cfg(feature = "include-checkers")] use pumpkin_checking::InferenceChecker; -#[cfg(feature = "include-checkers")] use pumpkin_checking::VariableState; use pumpkin_core::asserts::pumpkin_assert_simple; use pumpkin_core::declare_inference_label; @@ -19,7 +14,6 @@ use pumpkin_core::propagation::DomainEvents; use pumpkin_core::propagation::Domains; use pumpkin_core::propagation::EnqueueDecision; use pumpkin_core::propagation::ExplanationContext; -#[cfg(feature = "include-checkers")] use pumpkin_core::propagation::InferenceCheckers; use pumpkin_core::propagation::LocalId; use pumpkin_core::propagation::NotificationContext; @@ -51,7 +45,6 @@ where { type PropagatorImpl = LinearLessOrEqualPropagator; - #[cfg(feature = "include-checkers")] fn add_inference_checkers(&self, mut checkers: InferenceCheckers<'_>) { checkers.add_inference_checker( InferenceCode::new(self.constraint_tag, LinearBounds), @@ -295,13 +288,11 @@ where } #[derive(Debug, Clone)] -#[cfg(feature = "include-checkers")] pub struct LinearLessOrEqualInferenceChecker { terms: Box<[Var]>, bound: I32Ext, } -#[cfg(feature = "include-checkers")] impl LinearLessOrEqualInferenceChecker { pub fn new(terms: Box<[Var]>, bound: i32) -> Self { LinearLessOrEqualInferenceChecker { @@ -311,7 +302,6 @@ impl LinearLessOrEqualInferenceChecker { } } -#[cfg(feature = "include-checkers")] impl InferenceChecker for LinearLessOrEqualInferenceChecker where Var: CheckerVariable, diff --git a/pumpkin-solver/Cargo.toml b/pumpkin-solver/Cargo.toml index 6beaa78d6..498bd1ad2 100644 --- a/pumpkin-solver/Cargo.toml +++ b/pumpkin-solver/Cargo.toml @@ -33,7 +33,7 @@ workspace = true [features] debug-checks = ["pumpkin-core/debug-checks"] -check-propagations = ["pumpkin-core/check-propagations", "pumpkin-propagators/include-checkers"] +check-propagations = ["pumpkin-core/check-propagations"] [build-dependencies] cc = "1.1.30" From 66d5ec76c736490358ea1fd9e0b9fbf3db883070 Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Fri, 16 Jan 2026 16:19:51 +0100 Subject: [PATCH 16/48] Move time table inference checker to propagator crate --- pumpkin-checker/src/inferences/time_table.rs | 45 +++++++--------- .../cumulative/time_table/checker.rs | 52 +++++++++++++++++++ .../propagators/cumulative/time_table/mod.rs | 3 ++ .../time_table/time_table_over_interval.rs | 22 ++++++++ .../time_table/time_table_per_point.rs | 22 ++++++++ 5 files changed, 118 insertions(+), 26 deletions(-) create mode 100644 pumpkin-crates/propagators/src/propagators/cumulative/time_table/checker.rs diff --git a/pumpkin-checker/src/inferences/time_table.rs b/pumpkin-checker/src/inferences/time_table.rs index b58cac94c..f8ec2def9 100644 --- a/pumpkin-checker/src/inferences/time_table.rs +++ b/pumpkin-checker/src/inferences/time_table.rs @@ -1,7 +1,7 @@ -use std::collections::BTreeMap; - -use pumpkin_checking::CheckerVariable; +use pumpkin_checking::InferenceChecker; use pumpkin_checking::VariableState; +use pumpkin_propagators::cumulative::time_table::CheckerTask; +use pumpkin_propagators::cumulative::time_table::TimeTableChecker; use super::Fact; use crate::inferences::InvalidInference; @@ -21,29 +21,22 @@ pub(crate) fn verify_time_table( return Err(InvalidInference::ConstraintLabelMismatch); }; - // The profile is a key-value store. The keys correspond to time-points, and the values to the - // relative change in resource consumption. A BTreeMap is used to maintain a sorted order of - // the time points. - let mut profile = BTreeMap::new(); - - for task in cumulative.tasks.iter() { - let lst = task.start_time.induced_upper_bound(&state); - let ect = task.start_time.induced_lower_bound(&state) + task.duration; - - if ect <= lst { - *profile.entry(ect).or_insert(0) += task.resource_usage; - *profile.entry(lst).or_insert(0) -= task.resource_usage; - } - } - - let mut usage = 0; - for delta in profile.values() { - usage += delta; + let checker = TimeTableChecker { + tasks: cumulative + .tasks + .iter() + .map(|task| CheckerTask { + start_time: task.start_time.clone(), + resource_usage: task.resource_usage, + duration: task.duration, + }) + .collect(), + capacity: cumulative.capacity, + }; - if usage > cumulative.capacity { - return Ok(()); - } + if checker.check(state) { + Ok(()) + } else { + Err(InvalidInference::Unsound) } - - Err(InvalidInference::Unsound) } diff --git a/pumpkin-crates/propagators/src/propagators/cumulative/time_table/checker.rs b/pumpkin-crates/propagators/src/propagators/cumulative/time_table/checker.rs new file mode 100644 index 000000000..c70c37ddd --- /dev/null +++ b/pumpkin-crates/propagators/src/propagators/cumulative/time_table/checker.rs @@ -0,0 +1,52 @@ +use std::collections::BTreeMap; + +use pumpkin_checking::AtomicConstraint; +use pumpkin_checking::CheckerVariable; +use pumpkin_checking::InferenceChecker; + +#[derive(Clone, Debug)] +pub struct TimeTableChecker { + pub tasks: Box<[CheckerTask]>, + pub capacity: i32, +} + +#[derive(Clone, Debug)] +pub struct CheckerTask { + pub start_time: Var, + pub resource_usage: i32, + pub duration: i32, +} + +impl InferenceChecker for TimeTableChecker +where + Var: CheckerVariable, + Atomic: AtomicConstraint, +{ + fn check(&self, state: pumpkin_checking::VariableState) -> bool { + // The profile is a key-value store. The keys correspond to time-points, and the values to + // the relative change in resource consumption. A BTreeMap is used to maintain a + // sorted order of the time points. + let mut profile = BTreeMap::new(); + + for task in self.tasks.iter() { + let lst = task.start_time.induced_upper_bound(&state); + let ect = task.start_time.induced_lower_bound(&state) + task.duration; + + if ect <= lst { + *profile.entry(ect).or_insert(0) += task.resource_usage; + *profile.entry(lst).or_insert(0) -= task.resource_usage; + } + } + + let mut usage = 0; + for delta in profile.values() { + usage += delta; + + if usage > self.capacity { + return true; + } + } + + false + } +} diff --git a/pumpkin-crates/propagators/src/propagators/cumulative/time_table/mod.rs b/pumpkin-crates/propagators/src/propagators/cumulative/time_table/mod.rs index 30a4d2c77..b542aa54e 100644 --- a/pumpkin-crates/propagators/src/propagators/cumulative/time_table/mod.rs +++ b/pumpkin-crates/propagators/src/propagators/cumulative/time_table/mod.rs @@ -56,6 +56,7 @@ //! Conference, CP 2015, Cork, Ireland, August 31--September 4, 2015, Proceedings 21, 2015, pp. //! 149–157. +mod checker; mod explanations; mod over_interval_incremental_propagator; mod per_point_incremental_propagator; @@ -63,6 +64,8 @@ mod propagation_handler; mod time_table_over_interval; mod time_table_per_point; mod time_table_util; + +pub use checker::*; pub use explanations::CumulativeExplanationType; pub use over_interval_incremental_propagator::*; pub use per_point_incremental_propagator::*; diff --git a/pumpkin-crates/propagators/src/propagators/cumulative/time_table/time_table_over_interval.rs b/pumpkin-crates/propagators/src/propagators/cumulative/time_table/time_table_over_interval.rs index 6bdf00a08..e166a6eea 100644 --- a/pumpkin-crates/propagators/src/propagators/cumulative/time_table/time_table_over_interval.rs +++ b/pumpkin-crates/propagators/src/propagators/cumulative/time_table/time_table_over_interval.rs @@ -9,6 +9,7 @@ use pumpkin_core::proof::InferenceCode; use pumpkin_core::propagation::DomainEvent; use pumpkin_core::propagation::Domains; use pumpkin_core::propagation::EnqueueDecision; +use pumpkin_core::propagation::InferenceCheckers; use pumpkin_core::propagation::LocalId; use pumpkin_core::propagation::NotificationContext; use pumpkin_core::propagation::OpaqueDomainEvent; @@ -32,6 +33,8 @@ use crate::cumulative::ResourceProfile; use crate::cumulative::Task; use crate::cumulative::UpdatableStructures; use crate::cumulative::options::CumulativePropagatorOptions; +use crate::cumulative::time_table::CheckerTask; +use crate::cumulative::time_table::TimeTableChecker; #[cfg(doc)] use crate::cumulative::time_table::TimeTablePerPointPropagator; use crate::cumulative::util::create_tasks; @@ -107,6 +110,25 @@ impl PropagatorConstructor { type PropagatorImpl = Self; + fn add_inference_checkers(&self, mut checkers: InferenceCheckers<'_>) { + checkers.add_inference_checker( + InferenceCode::new(self.constraint_tag, TimeTable), + Box::new(TimeTableChecker { + tasks: self + .parameters + .tasks + .iter() + .map(|task| CheckerTask { + start_time: task.start_variable.clone(), + duration: task.processing_time, + resource_usage: task.resource_usage, + }) + .collect(), + capacity: self.parameters.capacity, + }), + ); + } + fn create(mut self, mut context: PropagatorConstructorContext) -> Self::PropagatorImpl { self.updatable_structures .initialise_bounds_and_remove_fixed(context.domains(), &self.parameters); diff --git a/pumpkin-crates/propagators/src/propagators/cumulative/time_table/time_table_per_point.rs b/pumpkin-crates/propagators/src/propagators/cumulative/time_table/time_table_per_point.rs index 1edfda6be..f72d76d69 100644 --- a/pumpkin-crates/propagators/src/propagators/cumulative/time_table/time_table_per_point.rs +++ b/pumpkin-crates/propagators/src/propagators/cumulative/time_table/time_table_per_point.rs @@ -12,6 +12,7 @@ use pumpkin_core::proof::InferenceCode; use pumpkin_core::propagation::DomainEvent; use pumpkin_core::propagation::Domains; use pumpkin_core::propagation::EnqueueDecision; +use pumpkin_core::propagation::InferenceCheckers; use pumpkin_core::propagation::LocalId; use pumpkin_core::propagation::NotificationContext; use pumpkin_core::propagation::OpaqueDomainEvent; @@ -34,6 +35,8 @@ use crate::cumulative::CumulativeParameters; use crate::cumulative::ResourceProfile; use crate::cumulative::UpdatableStructures; use crate::cumulative::options::CumulativePropagatorOptions; +use crate::cumulative::time_table::CheckerTask; +use crate::cumulative::time_table::TimeTableChecker; use crate::cumulative::util::create_tasks; use crate::cumulative::util::register_tasks; use crate::cumulative::util::update_bounds_task; @@ -98,6 +101,25 @@ impl TimeTablePerPointPropagator { impl PropagatorConstructor for TimeTablePerPointPropagator { type PropagatorImpl = Self; + fn add_inference_checkers(&self, mut checkers: InferenceCheckers<'_>) { + checkers.add_inference_checker( + InferenceCode::new(self.constraint_tag, TimeTable), + Box::new(TimeTableChecker { + tasks: self + .parameters + .tasks + .iter() + .map(|task| CheckerTask { + start_time: task.start_variable.clone(), + duration: task.processing_time, + resource_usage: task.resource_usage, + }) + .collect(), + capacity: self.parameters.capacity, + }), + ); + } + fn create(mut self, mut context: PropagatorConstructorContext) -> Self::PropagatorImpl { self.updatable_structures .initialise_bounds_and_remove_fixed(context.domains(), &self.parameters); From 2ccfc2b1e95f60f9b5bfef70d29954b6c3c000ba Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Fri, 16 Jan 2026 16:53:33 +0100 Subject: [PATCH 17/48] Scope inference checkers to propagator that posts them E.g. when the linear equals is posted, two linear bounds propagators are posted with the same inference code. --- pumpkin-crates/core/src/engine/state.rs | 117 ++++++++++++++---- .../core/src/propagation/constructor.rs | 14 ++- 2 files changed, 106 insertions(+), 25 deletions(-) diff --git a/pumpkin-crates/core/src/engine/state.rs b/pumpkin-crates/core/src/engine/state.rs index afacd92c1..270a9a3ff 100644 --- a/pumpkin-crates/core/src/engine/state.rs +++ b/pumpkin-crates/core/src/engine/state.rs @@ -77,7 +77,21 @@ pub struct State { statistics: StateStatistics, - checkers: HashMap>, + /// Inference checkers to run in the propagation loop. + checkers: HashMap>, +} + +/// The checker key is a combination of an [`InferenceCode`] and an optional [`PropagatorId`]. The +/// latter is optional because a constraint may decompose into multiple constraints that all +/// receive the same inference code. +/// +/// Whenever a checker is added via a [`PropagatorConstructor`], the propagator ID is given, +/// restricting the checker to be executed only when the propagator that added it produced the +/// inference to check. +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +struct CheckerKey { + inference_code: InferenceCode, + propagator_id: Option, } create_statistics_struct!(StateStatistics { @@ -357,10 +371,14 @@ impl State { Constructor: PropagatorConstructor, Constructor::PropagatorImpl: 'static, { - constructor.add_inference_checkers(InferenceCheckers::new(self)); - let original_handle: PropagatorHandle = self.propagators.new_propagator().key(); + + constructor.add_inference_checkers(InferenceCheckers::new( + self, + original_handle.propagator_id(), + )); + let constructor_context = PropagatorConstructorContext::new(original_handle.propagator_id(), self); let propagator = constructor.create(constructor_context); @@ -394,9 +412,40 @@ impl State { inference_code: InferenceCode, checker: impl Into>>, ) { - let previous_checker = self - .checkers - .insert(inference_code, BoxedChecker::from(checker.into())); + let previous_checker = self.checkers.insert( + CheckerKey { + inference_code, + propagator_id: None, + }, + BoxedChecker::from(checker.into()), + ); + + assert!( + previous_checker.is_none(), + "cannot add multiple checkers for the same inference code" + ); + } + + /// Add an inference checker to the state to run for a specific propagator. + /// + /// The inference checker will be used to check propagations performed during + /// [`Self::propagate_to_fixed_point`], if the `check-propagations` feature is enabled. + /// + /// If an inference checker already exists for the given inference code and propagator, a panic + /// is triggered. + pub(crate) fn add_inference_checker_for_propagator( + &mut self, + propagator_id: PropagatorId, + inference_code: InferenceCode, + checker: impl Into>>, + ) { + let previous_checker = self.checkers.insert( + CheckerKey { + inference_code, + propagator_id: Some(propagator_id), + }, + BoxedChecker::from(checker.into()), + ); assert!( previous_checker.is_none(), @@ -612,7 +661,7 @@ impl State { } Err(conflict) => { #[cfg(feature = "check-propagations")] - self.check_conflict(&conflict); + self.check_conflict(propagator_id, &conflict); self.statistics.num_conflicts += 1; if let Conflict::Propagator(inner) = &conflict { @@ -638,12 +687,13 @@ impl State { /// /// Panics when the inference checker rejects the conflict. #[cfg(feature = "check-propagations")] - fn check_conflict(&mut self, conflict: &Conflict) { + fn check_conflict(&mut self, propagator_id: PropagatorId, conflict: &Conflict) { if let Conflict::Propagator(propagator_conflict) = conflict { self.run_checker( propagator_conflict.conjunction.clone(), None, &propagator_conflict.inference_code, + propagator_id, ); } } @@ -667,6 +717,8 @@ impl State { .reason .expect("propagations should only be checked after propagations"); + let propagator_id = self.reason_store.get_propagator(reason_ref); + reason_buffer.clear(); let reason_exists = self.reason_store.get_or_compute( reason_ref, @@ -681,9 +733,10 @@ impl State { assert!(reason_exists, "all propagations have reasons"); self.run_checker( - reason_buffer.drain(..), + std::mem::take(&mut reason_buffer), Some(entry.predicate), &inference_code, + propagator_id, ); } } @@ -734,22 +787,42 @@ impl State { /// Run the checker for the given inference code on the given inference. fn run_checker( &self, - premises: impl IntoIterator, + premises: impl IntoIterator + Clone, consequent: Option, inference_code: &InferenceCode, + propagator_id: PropagatorId, ) { - // Get the checker for the inference code. - let checker = self - .checkers - .get(inference_code) - .unwrap_or_else(|| panic!("missing checker for inference code {inference_code:?}")); - - // Construct the variable state for the conflict check. - let variable_state = VariableState::prepare_for_conflict_check(premises, consequent) - .unwrap_or_else(|| panic!("inconsistent atomics in inference by {:?}", inference_code)); - - // Run the conflict check. - assert!(checker.check(variable_state)); + let key_with_propagator = CheckerKey { + inference_code: inference_code.clone(), + propagator_id: Some(propagator_id), + }; + + let key_without_propagator = CheckerKey { + inference_code: inference_code.clone(), + propagator_id: Some(propagator_id), + }; + + for key in [key_with_propagator, key_without_propagator] { + // Get the checker for the inference code. + let checker = self + .checkers + .get(&key) + .unwrap_or_else(|| panic!("missing checker for inference code {inference_code:?}")); + + // Construct the variable state for the conflict check. + let variable_state = + VariableState::prepare_for_conflict_check(premises.clone(), consequent) + .unwrap_or_else(|| { + panic!("inconsistent atomics in inference by {:?}", inference_code) + }); + + // Run the conflict check. + assert!( + checker.check(variable_state), + "checker for inference code {:?} fails", + inference_code + ); + } } } diff --git a/pumpkin-crates/core/src/propagation/constructor.rs b/pumpkin-crates/core/src/propagation/constructor.rs index 2a42b537d..acf34bf0f 100644 --- a/pumpkin-crates/core/src/propagation/constructor.rs +++ b/pumpkin-crates/core/src/propagation/constructor.rs @@ -52,11 +52,15 @@ pub trait PropagatorConstructor { #[derive(Debug)] pub struct InferenceCheckers<'state> { state: &'state mut State, + propagator_id: PropagatorId, } impl<'state> InferenceCheckers<'state> { - pub(crate) fn new(state: &'state mut State) -> Self { - InferenceCheckers { state } + pub(crate) fn new(state: &'state mut State, propagator_id: PropagatorId) -> Self { + InferenceCheckers { + state, + propagator_id, + } } } @@ -67,7 +71,11 @@ impl InferenceCheckers<'_> { inference_code: InferenceCode, checker: Box>, ) { - self.state.add_inference_checker(inference_code, checker); + self.state.add_inference_checker_for_propagator( + self.propagator_id, + inference_code, + checker, + ); } } From 3bab169e03e946660d1bcf22799be005cf3a136c Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Fri, 16 Jan 2026 17:02:09 +0100 Subject: [PATCH 18/48] Move nogood checker to pumpkin core --- Cargo.lock | 1 + pumpkin-checker/Cargo.toml | 1 + pumpkin-checker/src/inferences/nogood.rs | 10 ++++++++-- .../checking/src/atomic_constraint.rs | 3 ++- pumpkin-crates/core/src/lib.rs | 2 +- pumpkin-crates/core/src/propagators/mod.rs | 2 +- .../core/src/propagators/nogoods/checker.rs | 18 ++++++++++++++++++ .../core/src/propagators/nogoods/mod.rs | 2 ++ 8 files changed, 34 insertions(+), 5 deletions(-) create mode 100644 pumpkin-crates/core/src/propagators/nogoods/checker.rs diff --git a/Cargo.lock b/Cargo.lock index e171713d9..2e86b272a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -724,6 +724,7 @@ dependencies = [ "flate2", "fzn-rs", "pumpkin-checking", + "pumpkin-core", "pumpkin-propagators", "thiserror", ] diff --git a/pumpkin-checker/Cargo.toml b/pumpkin-checker/Cargo.toml index 7847eb968..17e25c99a 100644 --- a/pumpkin-checker/Cargo.toml +++ b/pumpkin-checker/Cargo.toml @@ -7,6 +7,7 @@ license.workspace = true authors.workspace = true [dependencies] +pumpkin-core = { version = "0.2.2", path = "../pumpkin-crates/core/" } pumpkin-checking = { version = "0.2.2", path = "../pumpkin-crates/checking/" } pumpkin-propagators = { version = "0.2.2", path = "../pumpkin-crates/propagators/" } anyhow = "1.0.99" diff --git a/pumpkin-checker/src/inferences/nogood.rs b/pumpkin-checker/src/inferences/nogood.rs index 93fe4164b..2cb91bcae 100644 --- a/pumpkin-checker/src/inferences/nogood.rs +++ b/pumpkin-checker/src/inferences/nogood.rs @@ -1,4 +1,8 @@ +use std::ops::Deref; + +use pumpkin_checking::InferenceChecker; use pumpkin_checking::VariableState; +use pumpkin_core::propagators::nogoods::NogoodChecker; use crate::inferences::Fact; use crate::inferences::InvalidInference; @@ -17,9 +21,11 @@ pub(crate) fn verify_nogood( return Err(InvalidInference::ConstraintLabelMismatch); }; - let is_implied_by_nogood = nogood.iter().all(|atomic| state.is_true(atomic)); + let checker = NogoodChecker { + nogood: nogood.deref().into(), + }; - if is_implied_by_nogood { + if checker.check(state) { Ok(()) } else { Err(InvalidInference::Unsound) diff --git a/pumpkin-crates/checking/src/atomic_constraint.rs b/pumpkin-crates/checking/src/atomic_constraint.rs index 893063059..91d6d27cf 100644 --- a/pumpkin-crates/checking/src/atomic_constraint.rs +++ b/pumpkin-crates/checking/src/atomic_constraint.rs @@ -1,4 +1,5 @@ use std::fmt::Display; +use std::hash::Hash; /// Captures the data associated with an atomic constraint. /// @@ -8,7 +9,7 @@ use std::fmt::Display; /// - and `value` is an integer. pub trait AtomicConstraint: Sized { /// The type of identifier used for variables. - type Identifier; + type Identifier: Hash + Eq; /// The identifier of this atomic constraint. fn identifier(&self) -> Self::Identifier; diff --git a/pumpkin-crates/core/src/lib.rs b/pumpkin-crates/core/src/lib.rs index 244d6755a..36138d519 100644 --- a/pumpkin-crates/core/src/lib.rs +++ b/pumpkin-crates/core/src/lib.rs @@ -16,7 +16,7 @@ pub mod constraints; pub mod optimisation; pub mod proof; pub mod propagation; -pub(crate) mod propagators; +pub mod propagators; pub mod statistics; pub use convert_case; diff --git a/pumpkin-crates/core/src/propagators/mod.rs b/pumpkin-crates/core/src/propagators/mod.rs index 6f7842ca0..010826ef6 100644 --- a/pumpkin-crates/core/src/propagators/mod.rs +++ b/pumpkin-crates/core/src/propagators/mod.rs @@ -1,2 +1,2 @@ -pub(crate) mod nogoods; +pub mod nogoods; pub(crate) mod reified_propagator; diff --git a/pumpkin-crates/core/src/propagators/nogoods/checker.rs b/pumpkin-crates/core/src/propagators/nogoods/checker.rs new file mode 100644 index 000000000..19290475b --- /dev/null +++ b/pumpkin-crates/core/src/propagators/nogoods/checker.rs @@ -0,0 +1,18 @@ +use std::fmt::Debug; + +use pumpkin_checking::AtomicConstraint; +use pumpkin_checking::InferenceChecker; + +#[derive(Debug, Clone)] +pub struct NogoodChecker { + pub nogood: Box<[Atomic]>, +} + +impl InferenceChecker for NogoodChecker +where + Atomic: AtomicConstraint + Clone + Debug, +{ + fn check(&self, state: pumpkin_checking::VariableState) -> bool { + self.nogood.iter().all(|atomic| state.is_true(atomic)) + } +} diff --git a/pumpkin-crates/core/src/propagators/nogoods/mod.rs b/pumpkin-crates/core/src/propagators/nogoods/mod.rs index 417df106c..e04fd791a 100644 --- a/pumpkin-crates/core/src/propagators/nogoods/mod.rs +++ b/pumpkin-crates/core/src/propagators/nogoods/mod.rs @@ -1,9 +1,11 @@ mod arena_allocator; +mod checker; mod learning_options; mod nogood_id; mod nogood_info; mod nogood_propagator; +pub use checker::*; pub use learning_options::*; pub(crate) use nogood_id::*; pub(crate) use nogood_info::*; From ea9d952d990e8fea1b475381168f912c10088f2c Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Fri, 16 Jan 2026 17:14:33 +0100 Subject: [PATCH 19/48] Add nogood checker in solver --- .../resolvers/resolution_resolver.rs | 8 ++++ .../engine/constraint_satisfaction_solver.rs | 8 ++++ pumpkin-crates/core/src/engine/state.rs | 45 ++++++++++++------- 3 files changed, 44 insertions(+), 17 deletions(-) diff --git a/pumpkin-crates/core/src/engine/conflict_analysis/resolvers/resolution_resolver.rs b/pumpkin-crates/core/src/engine/conflict_analysis/resolvers/resolution_resolver.rs index 3c9a77e57..194d5717d 100644 --- a/pumpkin-crates/core/src/engine/conflict_analysis/resolvers/resolution_resolver.rs +++ b/pumpkin-crates/core/src/engine/conflict_analysis/resolvers/resolution_resolver.rs @@ -19,6 +19,7 @@ use crate::proof::InferenceCode; use crate::proof::RootExplanationContext; use crate::proof::explain_root_assignment; use crate::propagation::CurrentNogood; +use crate::propagators::nogoods::NogoodChecker; use crate::propagators::nogoods::NogoodPropagator; use crate::pumpkin_assert_advanced; use crate::pumpkin_assert_moderate; @@ -125,6 +126,13 @@ impl ConflictResolver for ResolutionResolver { .average_learned_nogood_length .add_term(learned_nogood.predicates.len() as u64); + context.state.add_inference_checker( + inference_code.clone(), + Box::new(NogoodChecker { + nogood: learned_nogood.predicates.clone().into(), + }), + ); + self.add_learned_nogood(context, learned_nogood, inference_code); } } diff --git a/pumpkin-crates/core/src/engine/constraint_satisfaction_solver.rs b/pumpkin-crates/core/src/engine/constraint_satisfaction_solver.rs index 828ed1de7..6763cc3ea 100644 --- a/pumpkin-crates/core/src/engine/constraint_satisfaction_solver.rs +++ b/pumpkin-crates/core/src/engine/constraint_satisfaction_solver.rs @@ -50,6 +50,7 @@ use crate::proof::explain_root_assignment; use crate::proof::finalize_proof; use crate::propagation::PropagatorConstructor; use crate::propagation::store::PropagatorHandle; +use crate::propagators::nogoods::NogoodChecker; use crate::propagators::nogoods::NogoodPropagator; use crate::propagators::nogoods::NogoodPropagatorConstructor; use crate::pumpkin_assert_eq_simple; @@ -948,6 +949,13 @@ impl ConstraintSatisfactionSolver { pumpkin_assert_eq_simple!(self.get_checkpoint(), 0); let num_trail_entries = self.state.trail_len(); + self.state.add_inference_checker( + inference_code.clone(), + Box::new(NogoodChecker { + nogood: nogood.clone().into(), + }), + ); + let (nogood_propagator, mut context) = self .state .get_propagator_mut_with_context(self.nogood_propagator_handle); diff --git a/pumpkin-crates/core/src/engine/state.rs b/pumpkin-crates/core/src/engine/state.rs index 270a9a3ff..6fc394703 100644 --- a/pumpkin-crates/core/src/engine/state.rs +++ b/pumpkin-crates/core/src/engine/state.rs @@ -410,14 +410,14 @@ impl State { pub fn add_inference_checker( &mut self, inference_code: InferenceCode, - checker: impl Into>>, + checker: Box>, ) { let previous_checker = self.checkers.insert( CheckerKey { inference_code, propagator_id: None, }, - BoxedChecker::from(checker.into()), + BoxedChecker::from(checker), ); assert!( @@ -792,28 +792,34 @@ impl State { inference_code: &InferenceCode, propagator_id: PropagatorId, ) { - let key_with_propagator = CheckerKey { - inference_code: inference_code.clone(), - propagator_id: Some(propagator_id), - }; + let scoped_checkers = self + .checkers + .get(&CheckerKey { + inference_code: inference_code.clone(), + propagator_id: Some(propagator_id), + }) + .into_iter(); - let key_without_propagator = CheckerKey { - inference_code: inference_code.clone(), - propagator_id: Some(propagator_id), - }; + let unscoped_checkers = self + .checkers + .get(&CheckerKey { + inference_code: inference_code.clone(), + propagator_id: None, + }) + .into_iter(); + + // Will be set to true if we execute at least one checker. If it remains false after the + // loop a panic is triggered. + let mut at_least_one_checker = false; - for key in [key_with_propagator, key_without_propagator] { - // Get the checker for the inference code. - let checker = self - .checkers - .get(&key) - .unwrap_or_else(|| panic!("missing checker for inference code {inference_code:?}")); + for checker in scoped_checkers.chain(unscoped_checkers) { + at_least_one_checker = true; // Construct the variable state for the conflict check. let variable_state = VariableState::prepare_for_conflict_check(premises.clone(), consequent) .unwrap_or_else(|| { - panic!("inconsistent atomics in inference by {:?}", inference_code) + panic!("inconsistent atomics in inference by {inference_code:?}") }); // Run the conflict check. @@ -823,6 +829,11 @@ impl State { inference_code ); } + + assert!( + at_least_one_checker, + "missing checker for inference code {inference_code:?}" + ); } } From f45ff8f1a18bd683d634f7eb435a4db00ca5e900 Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Fri, 16 Jan 2026 17:20:15 +0100 Subject: [PATCH 20/48] Check the propagation by the asserting learned nogood --- .../conflict_analysis/resolvers/resolution_resolver.rs | 6 ++++++ pumpkin-crates/core/src/engine/state.rs | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/pumpkin-crates/core/src/engine/conflict_analysis/resolvers/resolution_resolver.rs b/pumpkin-crates/core/src/engine/conflict_analysis/resolvers/resolution_resolver.rs index 194d5717d..f6ce2dd66 100644 --- a/pumpkin-crates/core/src/engine/conflict_analysis/resolvers/resolution_resolver.rs +++ b/pumpkin-crates/core/src/engine/conflict_analysis/resolvers/resolution_resolver.rs @@ -247,6 +247,9 @@ impl ResolutionResolver { learned_nogood: LearnedNogood, inference_code: InferenceCode, ) { + #[cfg(feature = "check-propagations")] + let trail_len_before_nogood = context.state.trail_len(); + let (nogood_propagator, mut propagation_context) = context .state .get_propagator_mut_with_context(self.nogood_propagator_handle); @@ -259,6 +262,9 @@ impl ResolutionResolver { &mut propagation_context, context.counters, ); + + #[cfg(feature = "check-propagations")] + context.state.check_propagations(trail_len_before_nogood); } /// Clears all data structures to prepare for the new conflict analysis. diff --git a/pumpkin-crates/core/src/engine/state.rs b/pumpkin-crates/core/src/engine/state.rs index 6fc394703..f665cf5b2 100644 --- a/pumpkin-crates/core/src/engine/state.rs +++ b/pumpkin-crates/core/src/engine/state.rs @@ -707,7 +707,7 @@ impl State { /// /// If the checker rejects the inference, this method panics. #[cfg(feature = "check-propagations")] - fn check_propagations(&mut self, first_propagation_index: usize) { + pub(crate) fn check_propagations(&mut self, first_propagation_index: usize) { let mut reason_buffer = vec![]; for trail_index in first_propagation_index..self.assignments.num_trail_entries() { From e783a7a98efd237817410fc96d3db07eec09908d Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Mon, 19 Jan 2026 09:27:18 +0100 Subject: [PATCH 21/48] Always generate fresh constraint tag --- .../engine/constraint_satisfaction_solver.rs | 17 ++++++++------- pumpkin-crates/core/src/proof/mod.rs | 21 +++++++++---------- 2 files changed, 20 insertions(+), 18 deletions(-) diff --git a/pumpkin-crates/core/src/engine/constraint_satisfaction_solver.rs b/pumpkin-crates/core/src/engine/constraint_satisfaction_solver.rs index 6763cc3ea..27f4bc5ce 100644 --- a/pumpkin-crates/core/src/engine/constraint_satisfaction_solver.rs +++ b/pumpkin-crates/core/src/engine/constraint_satisfaction_solver.rs @@ -1249,13 +1249,15 @@ mod tests { fn create_instance1() -> (ConstraintSatisfactionSolver, Vec) { let mut solver = ConstraintSatisfactionSolver::default(); - let constraint_tag = solver.new_constraint_tag(); + let c1 = solver.new_constraint_tag(); + let c2 = solver.new_constraint_tag(); + let c3 = solver.new_constraint_tag(); let lit1 = solver.create_new_literal(None).get_true_predicate(); let lit2 = solver.create_new_literal(None).get_true_predicate(); - let _ = solver.add_clause([lit1, lit2], constraint_tag); - let _ = solver.add_clause([lit1, !lit2], constraint_tag); - let _ = solver.add_clause([!lit1, lit2], constraint_tag); + let _ = solver.add_clause([lit1, lit2], c1); + let _ = solver.add_clause([lit1, !lit2], c2); + let _ = solver.add_clause([!lit1, lit2], c3); (solver, vec![lit1, lit2]) } @@ -1321,13 +1323,14 @@ mod tests { } fn create_instance2() -> (ConstraintSatisfactionSolver, Vec) { let mut solver = ConstraintSatisfactionSolver::default(); - let constraint_tag = solver.new_constraint_tag(); + let c1 = solver.new_constraint_tag(); + let c2 = solver.new_constraint_tag(); let lit1 = solver.create_new_literal(None).get_true_predicate(); let lit2 = solver.create_new_literal(None).get_true_predicate(); let lit3 = solver.create_new_literal(None).get_true_predicate(); - let _ = solver.add_clause([lit1, lit2, lit3], constraint_tag); - let _ = solver.add_clause([lit1, !lit2, lit3], constraint_tag); + let _ = solver.add_clause([lit1, lit2, lit3], c1); + let _ = solver.add_clause([lit1, !lit2, lit3], c2); (solver, vec![lit1, lit2, lit3]) } diff --git a/pumpkin-crates/core/src/proof/mod.rs b/pumpkin-crates/core/src/proof/mod.rs index 284ac9617..6fe243cf8 100644 --- a/pumpkin-crates/core/src/proof/mod.rs +++ b/pumpkin-crates/core/src/proof/mod.rs @@ -25,7 +25,6 @@ use proof_atomics::ProofAtomics; use crate::Solver; use crate::containers::HashMap; use crate::containers::KeyGenerator; -use crate::containers::StorageKey; use crate::engine::variable_names::VariableNames; use crate::predicates::Predicate; use crate::variables::Literal; @@ -84,6 +83,8 @@ impl ProofLog { propagated: Option, variable_names: &VariableNames, ) -> std::io::Result { + let inference_tag = constraint_tags.next_key(); + let Some(ProofImpl::CpProof { writer, propagation_order_hint: Some(propagation_sequence), @@ -91,11 +92,9 @@ impl ProofLog { .. }) = self.internal_proof.as_mut() else { - return Ok(ConstraintTag::create_from_index(0)); + return Ok(inference_tag); }; - let inference_tag = constraint_tags.next_key(); - let inference = Inference { constraint_id: inference_tag.into(), premises: premises @@ -123,6 +122,8 @@ impl ProofLog { variable_names: &VariableNames, constraint_tags: &mut KeyGenerator, ) -> std::io::Result { + let inference_tag = constraint_tags.next_key(); + let Some(ProofImpl::CpProof { writer, propagation_order_hint: Some(propagation_sequence), @@ -131,7 +132,7 @@ impl ProofLog { .. }) = self.internal_proof.as_mut() else { - return Ok(ConstraintTag::create_from_index(0)); + return Ok(inference_tag); }; if let Some(hint_idx) = logged_domain_inferences.get(&predicate).copied() { @@ -145,8 +146,6 @@ impl ProofLog { return Ok(tag); } - let inference_tag = constraint_tags.next_key(); - let inference = Inference { constraint_id: inference_tag.into(), premises: vec![], @@ -176,6 +175,8 @@ impl ProofLog { variable_names: &VariableNames, constraint_tags: &mut KeyGenerator, ) -> std::io::Result { + let constraint_tag = constraint_tags.next_key(); + match &mut self.internal_proof { Some(ProofImpl::CpProof { writer, @@ -187,8 +188,6 @@ impl ProofLog { // Reset the logged domain inferences. logged_domain_inferences.clear(); - let constraint_tag = constraint_tags.next_key(); - let deduction = Deduction { constraint_id: constraint_tag.into(), premises: premises @@ -219,10 +218,10 @@ impl ProofLog { Some(ProofImpl::DimacsProof(writer)) => { let clause = premises.into_iter().map(|predicate| !predicate); writer.learned_clause(clause, variable_names)?; - Ok(ConstraintTag::create_from_index(0)) + Ok(constraint_tag) } - None => Ok(ConstraintTag::create_from_index(0)), + None => Ok(constraint_tag), } } From 76f7a040eae68111e26e9a585d3b043cb6ac7b74 Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Mon, 19 Jan 2026 12:16:47 +0100 Subject: [PATCH 22/48] If any checker accepts an inference, accept it --- pumpkin-crates/core/src/engine/state.rs | 118 ++++-------------- .../core/src/propagation/constructor.rs | 14 +-- 2 files changed, 26 insertions(+), 106 deletions(-) diff --git a/pumpkin-crates/core/src/engine/state.rs b/pumpkin-crates/core/src/engine/state.rs index f665cf5b2..f2336a831 100644 --- a/pumpkin-crates/core/src/engine/state.rs +++ b/pumpkin-crates/core/src/engine/state.rs @@ -78,20 +78,7 @@ pub struct State { statistics: StateStatistics, /// Inference checkers to run in the propagation loop. - checkers: HashMap>, -} - -/// The checker key is a combination of an [`InferenceCode`] and an optional [`PropagatorId`]. The -/// latter is optional because a constraint may decompose into multiple constraints that all -/// receive the same inference code. -/// -/// Whenever a checker is added via a [`PropagatorConstructor`], the propagator ID is given, -/// restricting the checker to be executed only when the propagator that added it produced the -/// inference to check. -#[derive(Clone, Debug, PartialEq, Eq, Hash)] -struct CheckerKey { - inference_code: InferenceCode, - propagator_id: Option, + checkers: HashMap>>, } create_statistics_struct!(StateStatistics { @@ -371,14 +358,11 @@ impl State { Constructor: PropagatorConstructor, Constructor::PropagatorImpl: 'static, { + constructor.add_inference_checkers(InferenceCheckers::new(self)); + let original_handle: PropagatorHandle = self.propagators.new_propagator().key(); - constructor.add_inference_checkers(InferenceCheckers::new( - self, - original_handle.propagator_id(), - )); - let constructor_context = PropagatorConstructorContext::new(original_handle.propagator_id(), self); let propagator = constructor.create(constructor_context); @@ -406,51 +390,15 @@ impl State { /// The inference checker will be used to check propagations performed during /// [`Self::propagate_to_fixed_point`], if the `check-propagations` feature is enabled. /// - /// If an inference checker already exists for the given inference code, a panic is triggered. + /// Multiple inference checkers may be added for the same inference code. In that case, if + /// any checker accepts the inference, the inference is accepted. pub fn add_inference_checker( &mut self, inference_code: InferenceCode, checker: Box>, ) { - let previous_checker = self.checkers.insert( - CheckerKey { - inference_code, - propagator_id: None, - }, - BoxedChecker::from(checker), - ); - - assert!( - previous_checker.is_none(), - "cannot add multiple checkers for the same inference code" - ); - } - - /// Add an inference checker to the state to run for a specific propagator. - /// - /// The inference checker will be used to check propagations performed during - /// [`Self::propagate_to_fixed_point`], if the `check-propagations` feature is enabled. - /// - /// If an inference checker already exists for the given inference code and propagator, a panic - /// is triggered. - pub(crate) fn add_inference_checker_for_propagator( - &mut self, - propagator_id: PropagatorId, - inference_code: InferenceCode, - checker: impl Into>>, - ) { - let previous_checker = self.checkers.insert( - CheckerKey { - inference_code, - propagator_id: Some(propagator_id), - }, - BoxedChecker::from(checker.into()), - ); - - assert!( - previous_checker.is_none(), - "cannot add multiple checkers for the same inference code" - ); + let checkers = self.checkers.entry(inference_code).or_default(); + checkers.push(BoxedChecker::from(checker)); } } @@ -661,7 +609,7 @@ impl State { } Err(conflict) => { #[cfg(feature = "check-propagations")] - self.check_conflict(propagator_id, &conflict); + self.check_conflict(&conflict); self.statistics.num_conflicts += 1; if let Conflict::Propagator(inner) = &conflict { @@ -687,13 +635,12 @@ impl State { /// /// Panics when the inference checker rejects the conflict. #[cfg(feature = "check-propagations")] - fn check_conflict(&mut self, propagator_id: PropagatorId, conflict: &Conflict) { + fn check_conflict(&mut self, conflict: &Conflict) { if let Conflict::Propagator(propagator_conflict) = conflict { self.run_checker( propagator_conflict.conjunction.clone(), None, &propagator_conflict.inference_code, - propagator_id, ); } } @@ -717,8 +664,6 @@ impl State { .reason .expect("propagations should only be checked after propagations"); - let propagator_id = self.reason_store.get_propagator(reason_ref); - reason_buffer.clear(); let reason_exists = self.reason_store.get_or_compute( reason_ref, @@ -736,7 +681,6 @@ impl State { std::mem::take(&mut reason_buffer), Some(entry.predicate), &inference_code, - propagator_id, ); } } @@ -790,31 +734,19 @@ impl State { premises: impl IntoIterator + Clone, consequent: Option, inference_code: &InferenceCode, - propagator_id: PropagatorId, ) { - let scoped_checkers = self + let checkers = self .checkers - .get(&CheckerKey { - inference_code: inference_code.clone(), - propagator_id: Some(propagator_id), - }) - .into_iter(); + .get(inference_code) + .map(|vec| vec.as_slice()) + .unwrap_or(&[]); - let unscoped_checkers = self - .checkers - .get(&CheckerKey { - inference_code: inference_code.clone(), - propagator_id: None, - }) - .into_iter(); - - // Will be set to true if we execute at least one checker. If it remains false after the - // loop a panic is triggered. - let mut at_least_one_checker = false; - - for checker in scoped_checkers.chain(unscoped_checkers) { - at_least_one_checker = true; + assert!( + !checkers.is_empty(), + "missing checker for inference code {inference_code:?}" + ); + let any_checker_accepts_inference = checkers.iter().any(|checker| { // Construct the variable state for the conflict check. let variable_state = VariableState::prepare_for_conflict_check(premises.clone(), consequent) @@ -822,17 +754,13 @@ impl State { panic!("inconsistent atomics in inference by {inference_code:?}") }); - // Run the conflict check. - assert!( - checker.check(variable_state), - "checker for inference code {:?} fails", - inference_code - ); - } + checker.check(variable_state) + }); assert!( - at_least_one_checker, - "missing checker for inference code {inference_code:?}" + !any_checker_accepts_inference, + "checker for inference code {:?} fails", + inference_code ); } } diff --git a/pumpkin-crates/core/src/propagation/constructor.rs b/pumpkin-crates/core/src/propagation/constructor.rs index acf34bf0f..2a42b537d 100644 --- a/pumpkin-crates/core/src/propagation/constructor.rs +++ b/pumpkin-crates/core/src/propagation/constructor.rs @@ -52,15 +52,11 @@ pub trait PropagatorConstructor { #[derive(Debug)] pub struct InferenceCheckers<'state> { state: &'state mut State, - propagator_id: PropagatorId, } impl<'state> InferenceCheckers<'state> { - pub(crate) fn new(state: &'state mut State, propagator_id: PropagatorId) -> Self { - InferenceCheckers { - state, - propagator_id, - } + pub(crate) fn new(state: &'state mut State) -> Self { + InferenceCheckers { state } } } @@ -71,11 +67,7 @@ impl InferenceCheckers<'_> { inference_code: InferenceCode, checker: Box>, ) { - self.state.add_inference_checker_for_propagator( - self.propagator_id, - inference_code, - checker, - ); + self.state.add_inference_checker(inference_code, checker); } } From e2c11470c7ff9b9ad46a8f8aae925833a8bc79cd Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Mon, 19 Jan 2026 12:30:31 +0100 Subject: [PATCH 23/48] Fix addition of I32 with Pos/Neg inf --- pumpkin-crates/checking/src/i32_ext.rs | 34 +++++++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/pumpkin-crates/checking/src/i32_ext.rs b/pumpkin-crates/checking/src/i32_ext.rs index 1de576f62..0b0b99509 100644 --- a/pumpkin-crates/checking/src/i32_ext.rs +++ b/pumpkin-crates/checking/src/i32_ext.rs @@ -74,9 +74,19 @@ impl Add for I32Ext { fn add(self, rhs: I32Ext) -> Self::Output { match (self, rhs) { (I32Ext::I32(lhs), I32Ext::I32(rhs)) => I32Ext::I32(lhs + rhs), + + (I32Ext::I32(_), Self::NegativeInf) => Self::NegativeInf, + (I32Ext::I32(_), Self::PositiveInf) => Self::PositiveInf, + (Self::NegativeInf, I32Ext::I32(_)) => Self::NegativeInf, + (Self::PositiveInf, I32Ext::I32(_)) => Self::PositiveInf, + (I32Ext::NegativeInf, I32Ext::NegativeInf) => I32Ext::NegativeInf, (I32Ext::PositiveInf, I32Ext::PositiveInf) => I32Ext::PositiveInf, - (lhs, rhs) => panic!("the result of {lhs:?} + {rhs:?} is indeterminate"), + + (lhs @ I32Ext::NegativeInf, rhs @ I32Ext::PositiveInf) + | (lhs @ I32Ext::PositiveInf, rhs @ I32Ext::NegativeInf) => { + panic!("the result of {lhs:?} + {rhs:?} is indeterminate") + } } } } @@ -122,3 +132,25 @@ impl Sum for I32Ext { iter.fold(I32Ext::I32(0), |acc, value| acc + value) } } + +#[cfg(test)] +mod tests { + use I32Ext::*; + + use super::*; + + #[test] + fn test_adding_i32s() { + assert_eq!(I32(3) + I32(4), I32(7)); + } + + #[test] + fn test_adding_negative_inf() { + assert_eq!(I32(3) + NegativeInf, NegativeInf); + } + + #[test] + fn test_adding_positive_inf() { + assert_eq!(I32(3) + PositiveInf, PositiveInf); + } +} From 075f75488a4195c135737baaa967d9a47849940c Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Mon, 19 Jan 2026 15:26:21 +0100 Subject: [PATCH 24/48] Only add inference checkers when checking propagations --- pumpkin-crates/core/src/engine/state.rs | 2 ++ pumpkin-crates/core/src/propagation/constructor.rs | 1 + 2 files changed, 3 insertions(+) diff --git a/pumpkin-crates/core/src/engine/state.rs b/pumpkin-crates/core/src/engine/state.rs index f2336a831..b0a4e4153 100644 --- a/pumpkin-crates/core/src/engine/state.rs +++ b/pumpkin-crates/core/src/engine/state.rs @@ -29,6 +29,7 @@ use crate::proof::ProofLog; use crate::propagation::CurrentNogood; use crate::propagation::Domains; use crate::propagation::ExplanationContext; +#[cfg(feature = "check-propagations")] use crate::propagation::InferenceCheckers; use crate::propagation::PropagationContext; use crate::propagation::Propagator; @@ -358,6 +359,7 @@ impl State { Constructor: PropagatorConstructor, Constructor::PropagatorImpl: 'static, { + #[cfg(feature = "check-propagations")] constructor.add_inference_checkers(InferenceCheckers::new(self)); let original_handle: PropagatorHandle = diff --git a/pumpkin-crates/core/src/propagation/constructor.rs b/pumpkin-crates/core/src/propagation/constructor.rs index 2a42b537d..2499ca57f 100644 --- a/pumpkin-crates/core/src/propagation/constructor.rs +++ b/pumpkin-crates/core/src/propagation/constructor.rs @@ -55,6 +55,7 @@ pub struct InferenceCheckers<'state> { } impl<'state> InferenceCheckers<'state> { + #[cfg(feature = "check-propagations")] pub(crate) fn new(state: &'state mut State) -> Self { InferenceCheckers { state } } From da533d7128085e9e9f0f7b33d7c2960aa4c42361 Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Mon, 19 Jan 2026 15:28:09 +0100 Subject: [PATCH 25/48] Clarify docs --- pumpkin-crates/core/src/propagation/constructor.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pumpkin-crates/core/src/propagation/constructor.rs b/pumpkin-crates/core/src/propagation/constructor.rs index 2499ca57f..65592887e 100644 --- a/pumpkin-crates/core/src/propagation/constructor.rs +++ b/pumpkin-crates/core/src/propagation/constructor.rs @@ -38,8 +38,8 @@ pub trait PropagatorConstructor { /// Add inference checkers to the solver if applicable. /// - /// By default this does nothing, and should only be implemented when `check-propagations` is - /// turned on. + /// If the `check-propagations` feature is turned on, then the inference checker will be used + /// to verify the propagations done by this propagator are correct. /// /// See [`InferenceChecker`] for more information. fn add_inference_checkers(&self, _checkers: InferenceCheckers<'_>) {} From 7d344879a6663db6a6bcf068ea34230bc6bd4028 Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Mon, 19 Jan 2026 15:43:32 +0100 Subject: [PATCH 26/48] Accept when any checker accepts --- pumpkin-crates/core/src/engine/state.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/pumpkin-crates/core/src/engine/state.rs b/pumpkin-crates/core/src/engine/state.rs index b0a4e4153..e7bcbddb7 100644 --- a/pumpkin-crates/core/src/engine/state.rs +++ b/pumpkin-crates/core/src/engine/state.rs @@ -760,9 +760,11 @@ impl State { }); assert!( - !any_checker_accepts_inference, - "checker for inference code {:?} fails", - inference_code + any_checker_accepts_inference, + "checker for inference code {:?} fails on inference {:?} -> {:?}", + inference_code, + premises.into_iter().collect::>(), + consequent, ); } } From 94952626c80cc50a69af264495e0584088cc3c26 Mon Sep 17 00:00:00 2001 From: Imko Marijnissen Date: Mon, 19 Jan 2026 15:53:02 +0100 Subject: [PATCH 27/48] fix: post checkers for other constructors as well --- .../time_table_over_interval_incremental.rs | 22 +++++++++++++++++++ .../time_table_per_point_incremental.rs | 22 +++++++++++++++++++ 2 files changed, 44 insertions(+) diff --git a/pumpkin-crates/propagators/src/propagators/cumulative/time_table/over_interval_incremental_propagator/time_table_over_interval_incremental.rs b/pumpkin-crates/propagators/src/propagators/cumulative/time_table/over_interval_incremental_propagator/time_table_over_interval_incremental.rs index fc6ff7f9c..3e343f9c5 100644 --- a/pumpkin-crates/propagators/src/propagators/cumulative/time_table/over_interval_incremental_propagator/time_table_over_interval_incremental.rs +++ b/pumpkin-crates/propagators/src/propagators/cumulative/time_table/over_interval_incremental_propagator/time_table_over_interval_incremental.rs @@ -11,6 +11,7 @@ use pumpkin_core::proof::InferenceCode; use pumpkin_core::propagation::DomainEvent; use pumpkin_core::propagation::Domains; use pumpkin_core::propagation::EnqueueDecision; +use pumpkin_core::propagation::InferenceCheckers; use pumpkin_core::propagation::LocalId; use pumpkin_core::propagation::NotificationContext; use pumpkin_core::propagation::OpaqueDomainEvent; @@ -29,6 +30,8 @@ use super::removal; use crate::cumulative::options::CumulativePropagatorOptions; use crate::cumulative::time_table::create_time_table_over_interval_from_scratch; use crate::cumulative::time_table::propagate_from_scratch_time_table_interval; +use crate::cumulative::time_table::CheckerTask; +use crate::cumulative::time_table::TimeTableChecker; use crate::cumulative::util::check_bounds_equal_at_propagation; use crate::cumulative::util::create_tasks; use crate::cumulative::util::register_tasks; @@ -108,6 +111,25 @@ impl PropagatorConstruc { type PropagatorImpl = Self; + fn add_inference_checkers(&self, mut checkers: InferenceCheckers<'_>) { + checkers.add_inference_checker( + InferenceCode::new(self.constraint_tag, TimeTable), + Box::new(TimeTableChecker { + tasks: self + .parameters + .tasks + .iter() + .map(|task| CheckerTask { + start_time: task.start_variable.clone(), + duration: task.processing_time, + resource_usage: task.resource_usage, + }) + .collect(), + capacity: self.parameters.capacity, + }), + ); + } + fn create(mut self, mut context: PropagatorConstructorContext) -> Self::PropagatorImpl { // We only register for notifications of backtrack events if incremental backtracking is // enabled diff --git a/pumpkin-crates/propagators/src/propagators/cumulative/time_table/per_point_incremental_propagator/time_table_per_point_incremental.rs b/pumpkin-crates/propagators/src/propagators/cumulative/time_table/per_point_incremental_propagator/time_table_per_point_incremental.rs index db3a00614..73328da68 100644 --- a/pumpkin-crates/propagators/src/propagators/cumulative/time_table/per_point_incremental_propagator/time_table_per_point_incremental.rs +++ b/pumpkin-crates/propagators/src/propagators/cumulative/time_table/per_point_incremental_propagator/time_table_per_point_incremental.rs @@ -11,6 +11,7 @@ use pumpkin_core::proof::InferenceCode; use pumpkin_core::propagation::DomainEvent; use pumpkin_core::propagation::Domains; use pumpkin_core::propagation::EnqueueDecision; +use pumpkin_core::propagation::InferenceCheckers; use pumpkin_core::propagation::LocalId; use pumpkin_core::propagation::NotificationContext; use pumpkin_core::propagation::OpaqueDomainEvent; @@ -31,7 +32,9 @@ use crate::cumulative::ResourceProfile; use crate::cumulative::Task; use crate::cumulative::UpdatableStructures; use crate::cumulative::options::CumulativePropagatorOptions; +use crate::cumulative::time_table::CheckerTask; use crate::cumulative::time_table::PerPointTimeTableType; +use crate::cumulative::time_table::TimeTableChecker; #[cfg(doc)] use crate::cumulative::time_table::TimeTablePerPointPropagator; use crate::cumulative::time_table::create_time_table_per_point_from_scratch; @@ -105,6 +108,25 @@ impl Propagator { type PropagatorImpl = Self; + fn add_inference_checkers(&self, mut checkers: InferenceCheckers<'_>) { + checkers.add_inference_checker( + InferenceCode::new(self.constraint_tag, TimeTable), + Box::new(TimeTableChecker { + tasks: self + .parameters + .tasks + .iter() + .map(|task| CheckerTask { + start_time: task.start_variable.clone(), + duration: task.processing_time, + resource_usage: task.resource_usage, + }) + .collect(), + capacity: self.parameters.capacity, + }), + ); + } + fn create(mut self, mut context: PropagatorConstructorContext) -> Self::PropagatorImpl { register_tasks(&self.parameters.tasks, context.reborrow(), true); self.updatable_structures From 0ff8048c03fb4ecab13ea5d84f764a069184a19c Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Mon, 19 Jan 2026 16:25:44 +0100 Subject: [PATCH 28/48] Fix time table checker profile calculation --- pumpkin-crates/checking/src/variable.rs | 68 +++++++++++++++++++ .../core/src/engine/cp/test_solver.rs | 17 +++++ .../src/propagators/reified_propagator.rs | 3 + .../cumulative/time_table/checker.rs | 53 ++++++++++++++- 4 files changed, 138 insertions(+), 3 deletions(-) diff --git a/pumpkin-crates/checking/src/variable.rs b/pumpkin-crates/checking/src/variable.rs index 77a22c4f0..8f66bfac0 100644 --- a/pumpkin-crates/checking/src/variable.rs +++ b/pumpkin-crates/checking/src/variable.rs @@ -1,7 +1,9 @@ use std::fmt::Debug; use crate::AtomicConstraint; +use crate::Comparison; use crate::I32Ext; +use crate::TestAtomic; use crate::VariableState; /// A variable in a constraint satisfaction problem. @@ -45,3 +47,69 @@ pub trait CheckerVariable: Debug + Clone { where 'this: 'state; } + +impl CheckerVariable for &'static str { + fn atomic_less_than(&self, value: i32) -> TestAtomic { + TestAtomic { + name: self, + comparison: Comparison::LessEqual, + value, + } + } + + fn atomic_greater_than(&self, value: i32) -> TestAtomic { + TestAtomic { + name: self, + comparison: Comparison::GreaterEqual, + value, + } + } + + fn atomic_equal(&self, value: i32) -> TestAtomic { + TestAtomic { + name: self, + comparison: Comparison::Equal, + value, + } + } + + fn atomic_not_equal(&self, value: i32) -> TestAtomic { + TestAtomic { + name: self, + comparison: Comparison::NotEqual, + value, + } + } + + fn induced_lower_bound(&self, variable_state: &VariableState) -> I32Ext { + variable_state.lower_bound(self) + } + + fn induced_upper_bound(&self, variable_state: &VariableState) -> I32Ext { + variable_state.upper_bound(self) + } + + fn induced_fixed_value(&self, variable_state: &VariableState) -> Option { + variable_state.fixed_value(self) + } + + fn induced_holes<'this, 'state>( + &'this self, + variable_state: &'state VariableState, + ) -> impl Iterator + 'state + where + 'this: 'state, + { + variable_state.holes(self) + } + + fn iter_induced_domain<'this, 'state>( + &'this self, + variable_state: &'state VariableState, + ) -> Option + 'state> + where + 'this: 'state, + { + variable_state.iter_domain(self) + } +} diff --git a/pumpkin-crates/core/src/engine/cp/test_solver.rs b/pumpkin-crates/core/src/engine/cp/test_solver.rs index f7bfaeee1..830f03406 100644 --- a/pumpkin-crates/core/src/engine/cp/test_solver.rs +++ b/pumpkin-crates/core/src/engine/cp/test_solver.rs @@ -2,6 +2,8 @@ //! setting up specific scenarios under which to test the various operations of a propagator. use std::fmt::Debug; +use pumpkin_checking::InferenceChecker; + use super::PropagatorQueue; use crate::containers::KeyGenerator; use crate::engine::EmptyDomain; @@ -14,6 +16,7 @@ use crate::options::LearningOptions; use crate::predicate; use crate::predicates::PropositionalConjunction; use crate::proof::ConstraintTag; +use crate::proof::InferenceCode; use crate::propagation::Domains; use crate::propagation::EnqueueDecision; use crate::propagation::ExplanationContext; @@ -53,6 +56,20 @@ impl Default for TestSolver { #[deprecated = "Will be replaced by the state API"] impl TestSolver { + pub fn accept_inferences_by(&mut self, inference_code: InferenceCode) { + #[derive(Debug, Clone, Copy)] + struct Checker; + + impl InferenceChecker for Checker { + fn check(&self, _: pumpkin_checking::VariableState) -> bool { + true + } + } + + self.state + .add_inference_checker(inference_code, Box::new(Checker)); + } + pub fn new_variable(&mut self, lb: i32, ub: i32) -> DomainId { self.state.new_interval_variable(lb, ub, None) } diff --git a/pumpkin-crates/core/src/propagators/reified_propagator.rs b/pumpkin-crates/core/src/propagators/reified_propagator.rs index 8047addb9..7ae5ee758 100644 --- a/pumpkin-crates/core/src/propagators/reified_propagator.rs +++ b/pumpkin-crates/core/src/propagators/reified_propagator.rs @@ -248,6 +248,7 @@ mod tests { let t2 = triggered_conflict.clone(); let inference_code = InferenceCode::unknown_label(ConstraintTag::create_from_index(0)); + solver.accept_inferences_by(inference_code.clone()); let i1 = inference_code.clone(); let i2 = inference_code.clone(); @@ -324,6 +325,7 @@ mod tests { let var = solver.new_variable(1, 1); let inference_code = InferenceCode::unknown_label(ConstraintTag::create_from_index(0)); + solver.accept_inferences_by(inference_code.clone()); let inconsistency = solver .new_propagator(ReifiedPropagatorArgs { @@ -364,6 +366,7 @@ mod tests { let var = solver.new_variable(1, 5); let inference_code = InferenceCode::unknown_label(ConstraintTag::create_from_index(0)); + solver.accept_inferences_by(inference_code.clone()); let propagator = solver .new_propagator(ReifiedPropagatorArgs { diff --git a/pumpkin-crates/propagators/src/propagators/cumulative/time_table/checker.rs b/pumpkin-crates/propagators/src/propagators/cumulative/time_table/checker.rs index c70c37ddd..1c016b4b0 100644 --- a/pumpkin-crates/propagators/src/propagators/cumulative/time_table/checker.rs +++ b/pumpkin-crates/propagators/src/propagators/cumulative/time_table/checker.rs @@ -32,9 +32,9 @@ where let lst = task.start_time.induced_upper_bound(&state); let ect = task.start_time.induced_lower_bound(&state) + task.duration; - if ect <= lst { - *profile.entry(ect).or_insert(0) += task.resource_usage; - *profile.entry(lst).or_insert(0) -= task.resource_usage; + if lst <= ect { + *profile.entry(lst).or_insert(0) += task.resource_usage; + *profile.entry(ect).or_insert(0) -= task.resource_usage; } } @@ -50,3 +50,50 @@ where false } } + +#[cfg(test)] +mod tests { + use pumpkin_checking::TestAtomic; + use pumpkin_checking::VariableState; + + use super::*; + + #[test] + fn conflict_on_unary_resource() { + let state = VariableState::prepare_for_conflict_check( + [ + TestAtomic { + name: "x1", + comparison: pumpkin_checking::Comparison::Equal, + value: 1, + }, + TestAtomic { + name: "x2", + comparison: pumpkin_checking::Comparison::Equal, + value: 1, + }, + ], + None, + ) + .expect("no conflicting atomics"); + + let checker = TimeTableChecker { + tasks: vec![ + CheckerTask { + start_time: "x1", + resource_usage: 1, + duration: 1, + }, + CheckerTask { + start_time: "x2", + resource_usage: 1, + duration: 1, + }, + ] + .into(), + capacity: 1, + }; + + assert!(checker.check(state)); + } +} From f327564ac2d9b2447f7d03f823a6c7d1648c3284 Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Mon, 19 Jan 2026 16:58:07 +0100 Subject: [PATCH 29/48] Complete implementation of TestAtomic --- pumpkin-crates/checking/src/atomic_constraint.rs | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/pumpkin-crates/checking/src/atomic_constraint.rs b/pumpkin-crates/checking/src/atomic_constraint.rs index 91d6d27cf..2decb8f89 100644 --- a/pumpkin-crates/checking/src/atomic_constraint.rs +++ b/pumpkin-crates/checking/src/atomic_constraint.rs @@ -70,6 +70,19 @@ impl AtomicConstraint for TestAtomic { } fn negate(&self) -> Self { - todo!() + TestAtomic { + name: self.name, + comparison: match self.comparison { + Comparison::GreaterEqual => Comparison::LessEqual, + Comparison::LessEqual => Comparison::GreaterEqual, + Comparison::Equal => Comparison::NotEqual, + Comparison::NotEqual => Comparison::Equal, + }, + value: match self.comparison { + Comparison::GreaterEqual => self.value - 1, + Comparison::LessEqual => self.value + 1, + Comparison::NotEqual | Comparison::Equal => self.value, + }, + } } } From 3ee7f800756a1d649d3d82afbe9aa826995c6f5b Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Mon, 19 Jan 2026 16:58:47 +0100 Subject: [PATCH 30/48] Enable inference checkers in CI --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 82dcd8121..684e61132 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -26,7 +26,7 @@ jobs: target/ key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} - uses: dtolnay/rust-toolchain@stable - - run: cargo test --release --no-fail-fast + - run: cargo test --release --no-fail-fast --features pumpkin-core/check-propagations wasm-test: name: Test Suite for pumpkin-core in WebAssembly From fe63bae4f5e5169bf8b8da82764cd0e3376a4b5a Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Mon, 19 Jan 2026 17:37:45 +0100 Subject: [PATCH 31/48] Implement absolute value checker --- pumpkin-crates/checking/src/i32_ext.rs | 39 ++++++++++++++ .../propagators/arithmetic/absolute_value.rs | 54 +++++++++++++++++++ 2 files changed, 93 insertions(+) diff --git a/pumpkin-crates/checking/src/i32_ext.rs b/pumpkin-crates/checking/src/i32_ext.rs index 0b0b99509..bd8493812 100644 --- a/pumpkin-crates/checking/src/i32_ext.rs +++ b/pumpkin-crates/checking/src/i32_ext.rs @@ -2,6 +2,7 @@ use std::cmp::Ordering; use std::iter::Sum; use std::ops::Add; use std::ops::Mul; +use std::ops::Neg; /// An [`i32`] or positive/negative infinity. /// @@ -30,6 +31,18 @@ impl PartialEq for I32Ext { } } +impl PartialEq for i32 { + fn eq(&self, other: &I32Ext) -> bool { + other.eq(self) + } +} + +impl PartialOrd for i32 { + fn partial_cmp(&self, other: &I32Ext) -> Option { + other.neg().partial_cmp(&self.neg()) + } +} + impl PartialOrd for I32Ext { fn partial_cmp(&self, other: &I32Ext) -> Option { Some(self.cmp(other)) @@ -127,6 +140,18 @@ impl Mul for I32Ext { } } +impl Neg for I32Ext { + type Output = Self; + + fn neg(self) -> Self::Output { + match self { + I32Ext::I32(value) => I32Ext::I32(-value), + I32Ext::NegativeInf => I32Ext::PositiveInf, + I32Ext::PositiveInf => Self::NegativeInf, + } + } +} + impl Sum for I32Ext { fn sum>(iter: I) -> Self { iter.fold(I32Ext::I32(0), |acc, value| acc + value) @@ -139,6 +164,20 @@ mod tests { use super::*; + #[test] + fn ordering_of_i32_with_i32_ext() { + assert!(I32(2) < 3); + assert!(I32(-1) < 3); + assert!(I32(-10) < -1); + } + + #[test] + fn ordering_of_i32_ext_with_i32() { + assert!(1 < I32(2)); + assert!(-10 < I32(-1)); + assert!(-11 < I32(-10)); + } + #[test] fn test_adding_i32s() { assert_eq!(I32(3) + I32(4), I32(7)); diff --git a/pumpkin-crates/propagators/src/propagators/arithmetic/absolute_value.rs b/pumpkin-crates/propagators/src/propagators/arithmetic/absolute_value.rs index eac1df128..f70751458 100644 --- a/pumpkin-crates/propagators/src/propagators/arithmetic/absolute_value.rs +++ b/pumpkin-crates/propagators/src/propagators/arithmetic/absolute_value.rs @@ -1,9 +1,14 @@ +use pumpkin_checking::AtomicConstraint; +use pumpkin_checking::CheckerVariable; +use pumpkin_checking::I32Ext; +use pumpkin_checking::InferenceChecker; use pumpkin_core::conjunction; use pumpkin_core::declare_inference_label; use pumpkin_core::predicate; use pumpkin_core::proof::ConstraintTag; use pumpkin_core::proof::InferenceCode; use pumpkin_core::propagation::DomainEvents; +use pumpkin_core::propagation::InferenceCheckers; use pumpkin_core::propagation::LocalId; use pumpkin_core::propagation::Priority; use pumpkin_core::propagation::PropagationContext; @@ -30,6 +35,16 @@ where { type PropagatorImpl = AbsoluteValuePropagator; + fn add_inference_checkers(&self, mut checkers: InferenceCheckers<'_>) { + checkers.add_inference_checker( + InferenceCode::new(self.constraint_tag, AbsoluteValue), + Box::new(AbsoluteValueChecker { + signed: self.signed.clone(), + absolute: self.absolute.clone(), + }), + ); + } + fn create(self, mut context: PropagatorConstructorContext) -> Self::PropagatorImpl { let AbsoluteValueArgs { signed, @@ -146,6 +161,45 @@ where } } +#[derive(Clone, Debug)] +pub struct AbsoluteValueChecker { + signed: VA, + absolute: VB, +} + +impl InferenceChecker for AbsoluteValueChecker +where + VA: CheckerVariable, + VB: CheckerVariable, + Atomic: AtomicConstraint, +{ + fn check(&self, state: pumpkin_checking::VariableState) -> bool { + let signed_lower = self.signed.induced_lower_bound(&state); + let signed_upper = self.signed.induced_upper_bound(&state); + let absolute_lower = self.absolute.induced_lower_bound(&state); + let absolute_upper = self.absolute.induced_upper_bound(&state); + + if absolute_lower < 0 { + // The absolute value cannot have negative values. + return true; + } + + // Now we compute the interval for |signed| based on the domain of signed. + let (computed_signed_lower, computed_signed_upper) = if signed_lower >= 0 { + (signed_lower, signed_upper) + } else if signed_upper <= 0 { + (-signed_upper, -signed_lower) + } else if signed_lower < 0 && 0_i32 < signed_upper { + (I32Ext::I32(0), std::cmp::max(-signed_lower, signed_upper)) + } else { + unreachable!() + }; + + // The intervals should not match, otherwise there is no conflict. + computed_signed_lower != absolute_lower || computed_signed_upper != absolute_upper + } +} + #[allow(deprecated, reason = "Will be refactored")] #[cfg(test)] mod tests { From 4f4838a6ea3f3f8ac5813b03d9c480e174b97ecf Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Tue, 20 Jan 2026 10:20:14 +0100 Subject: [PATCH 32/48] Supply the inference being checked to the checkers --- pumpkin-checker/src/inferences/linear.rs | 4 +- pumpkin-checker/src/inferences/nogood.rs | 4 +- pumpkin-checker/src/inferences/time_table.rs | 4 +- pumpkin-crates/checking/src/lib.rs | 19 +++++++-- .../core/src/engine/cp/test_solver.rs | 7 +++- .../core/src/propagators/nogoods/checker.rs | 7 +++- .../propagators/arithmetic/absolute_value.rs | 7 +++- .../arithmetic/linear_less_or_equal.rs | 7 +++- .../cumulative/time_table/checker.rs | 40 ++++++++++--------- 9 files changed, 68 insertions(+), 31 deletions(-) diff --git a/pumpkin-checker/src/inferences/linear.rs b/pumpkin-checker/src/inferences/linear.rs index 00da598b3..4cd522d49 100644 --- a/pumpkin-checker/src/inferences/linear.rs +++ b/pumpkin-checker/src/inferences/linear.rs @@ -49,12 +49,12 @@ pub(super) fn verify_linear_bounds( fn verify_linear_inference( linear: &Linear, - _: &Fact, + fact: &Fact, state: VariableState, ) -> Result<(), InvalidInference> { let checker = LinearLessOrEqualInferenceChecker::new(linear.terms.clone().into(), linear.bound); - if InferenceChecker::::check(&checker, state) { + if checker.check(state, &fact.premises, fact.consequent.as_ref()) { Ok(()) } else { Err(InvalidInference::Unsound) diff --git a/pumpkin-checker/src/inferences/nogood.rs b/pumpkin-checker/src/inferences/nogood.rs index 2cb91bcae..2451a2fe1 100644 --- a/pumpkin-checker/src/inferences/nogood.rs +++ b/pumpkin-checker/src/inferences/nogood.rs @@ -13,7 +13,7 @@ use crate::model::Constraint; /// /// This inference is used to rewrite a nogood `L /\ p -> false` to `L -> not p`. pub(crate) fn verify_nogood( - _: &Fact, + fact: &Fact, constraint: &Constraint, state: VariableState, ) -> Result<(), InvalidInference> { @@ -25,7 +25,7 @@ pub(crate) fn verify_nogood( nogood: nogood.deref().into(), }; - if checker.check(state) { + if checker.check(state, &fact.premises, fact.consequent.as_ref()) { Ok(()) } else { Err(InvalidInference::Unsound) diff --git a/pumpkin-checker/src/inferences/time_table.rs b/pumpkin-checker/src/inferences/time_table.rs index f8ec2def9..2a2a63648 100644 --- a/pumpkin-checker/src/inferences/time_table.rs +++ b/pumpkin-checker/src/inferences/time_table.rs @@ -13,7 +13,7 @@ use crate::model::Constraint; /// The premises and negation of the consequent should lead to an overflow of the resource /// capacity. pub(crate) fn verify_time_table( - _: &Fact, + fact: &Fact, constraint: &Constraint, state: VariableState, ) -> Result<(), InvalidInference> { @@ -34,7 +34,7 @@ pub(crate) fn verify_time_table( capacity: cumulative.capacity, }; - if checker.check(state) { + if checker.check(state, &fact.premises, fact.consequent.as_ref()) { Ok(()) } else { Err(InvalidInference::Unsound) diff --git a/pumpkin-crates/checking/src/lib.rs b/pumpkin-crates/checking/src/lib.rs index df63b8142..982db1411 100644 --- a/pumpkin-crates/checking/src/lib.rs +++ b/pumpkin-crates/checking/src/lib.rs @@ -20,7 +20,15 @@ pub use variable_state::*; /// inference rule. pub trait InferenceChecker: Debug + DynClone { /// Returns `true` if `state` is a conflict, and `false` if not. - fn check(&self, state: VariableState) -> bool; + /// + /// For the conflict check, all the premises are true in the state and the consequent, if + /// present, if false. + fn check( + &self, + state: VariableState, + premises: &[Atomic], + consequent: Option<&Atomic>, + ) -> bool; } /// Wrapper around `Box>` that implements [`Clone`]. @@ -41,7 +49,12 @@ impl From>> for Boxed impl BoxedChecker { /// See [`InferenceChecker::check`]. - pub fn check(&self, variable_state: VariableState) -> bool { - self.0.check(variable_state) + pub fn check( + &self, + variable_state: VariableState, + premises: &[Atomic], + consequent: Option<&Atomic>, + ) -> bool { + self.0.check(variable_state, premises, consequent) } } diff --git a/pumpkin-crates/core/src/engine/cp/test_solver.rs b/pumpkin-crates/core/src/engine/cp/test_solver.rs index 830f03406..50853a8eb 100644 --- a/pumpkin-crates/core/src/engine/cp/test_solver.rs +++ b/pumpkin-crates/core/src/engine/cp/test_solver.rs @@ -61,7 +61,12 @@ impl TestSolver { struct Checker; impl InferenceChecker for Checker { - fn check(&self, _: pumpkin_checking::VariableState) -> bool { + fn check( + &self, + _: pumpkin_checking::VariableState, + _: &[Predicate], + _: Option<&Predicate>, + ) -> bool { true } } diff --git a/pumpkin-crates/core/src/propagators/nogoods/checker.rs b/pumpkin-crates/core/src/propagators/nogoods/checker.rs index 19290475b..700ee6a2c 100644 --- a/pumpkin-crates/core/src/propagators/nogoods/checker.rs +++ b/pumpkin-crates/core/src/propagators/nogoods/checker.rs @@ -12,7 +12,12 @@ impl InferenceChecker for NogoodChecker where Atomic: AtomicConstraint + Clone + Debug, { - fn check(&self, state: pumpkin_checking::VariableState) -> bool { + fn check( + &self, + state: pumpkin_checking::VariableState, + _: &[Atomic], + _: Option<&Atomic>, + ) -> bool { self.nogood.iter().all(|atomic| state.is_true(atomic)) } } diff --git a/pumpkin-crates/propagators/src/propagators/arithmetic/absolute_value.rs b/pumpkin-crates/propagators/src/propagators/arithmetic/absolute_value.rs index f70751458..74a39e747 100644 --- a/pumpkin-crates/propagators/src/propagators/arithmetic/absolute_value.rs +++ b/pumpkin-crates/propagators/src/propagators/arithmetic/absolute_value.rs @@ -173,7 +173,12 @@ where VB: CheckerVariable, Atomic: AtomicConstraint, { - fn check(&self, state: pumpkin_checking::VariableState) -> bool { + fn check( + &self, + state: pumpkin_checking::VariableState, + _: &[Atomic], + _: Option<&Atomic>, + ) -> bool { let signed_lower = self.signed.induced_lower_bound(&state); let signed_upper = self.signed.induced_upper_bound(&state); let absolute_lower = self.absolute.induced_lower_bound(&state); diff --git a/pumpkin-crates/propagators/src/propagators/arithmetic/linear_less_or_equal.rs b/pumpkin-crates/propagators/src/propagators/arithmetic/linear_less_or_equal.rs index f1b1485ef..560086628 100644 --- a/pumpkin-crates/propagators/src/propagators/arithmetic/linear_less_or_equal.rs +++ b/pumpkin-crates/propagators/src/propagators/arithmetic/linear_less_or_equal.rs @@ -307,7 +307,12 @@ where Var: CheckerVariable, Atomic: AtomicConstraint, { - fn check(&self, variable_state: VariableState) -> bool { + fn check( + &self, + variable_state: VariableState, + _: &[Atomic], + _: Option<&Atomic>, + ) -> bool { // Next, we evaluate the linear inequality. The lower bound of the // left-hand side must exceed the bound in the constraint. Note that the accumulator is an // I32Ext, and if the lower bound of one of the terms is -infty, then the left-hand side diff --git a/pumpkin-crates/propagators/src/propagators/cumulative/time_table/checker.rs b/pumpkin-crates/propagators/src/propagators/cumulative/time_table/checker.rs index 1c016b4b0..13cc6e34e 100644 --- a/pumpkin-crates/propagators/src/propagators/cumulative/time_table/checker.rs +++ b/pumpkin-crates/propagators/src/propagators/cumulative/time_table/checker.rs @@ -22,7 +22,12 @@ where Var: CheckerVariable, Atomic: AtomicConstraint, { - fn check(&self, state: pumpkin_checking::VariableState) -> bool { + fn check( + &self, + state: pumpkin_checking::VariableState, + _: &[Atomic], + _: Option<&Atomic>, + ) -> bool { // The profile is a key-value store. The keys correspond to time-points, and the values to // the relative change in resource consumption. A BTreeMap is used to maintain a // sorted order of the time points. @@ -60,22 +65,21 @@ mod tests { #[test] fn conflict_on_unary_resource() { - let state = VariableState::prepare_for_conflict_check( - [ - TestAtomic { - name: "x1", - comparison: pumpkin_checking::Comparison::Equal, - value: 1, - }, - TestAtomic { - name: "x2", - comparison: pumpkin_checking::Comparison::Equal, - value: 1, - }, - ], - None, - ) - .expect("no conflicting atomics"); + let premises = [ + TestAtomic { + name: "x1", + comparison: pumpkin_checking::Comparison::Equal, + value: 1, + }, + TestAtomic { + name: "x2", + comparison: pumpkin_checking::Comparison::Equal, + value: 1, + }, + ]; + + let state = VariableState::prepare_for_conflict_check(premises, None) + .expect("no conflicting atomics"); let checker = TimeTableChecker { tasks: vec![ @@ -94,6 +98,6 @@ mod tests { capacity: 1, }; - assert!(checker.check(state)); + assert!(checker.check(state, &premises, None)); } } From 6f8527320fddd6c00a3cc7d3ddd290273547c922 Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Tue, 20 Jan 2026 10:24:20 +0100 Subject: [PATCH 33/48] Also supply fact in state checker --- pumpkin-crates/core/src/engine/state.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pumpkin-crates/core/src/engine/state.rs b/pumpkin-crates/core/src/engine/state.rs index e7bcbddb7..386bedd57 100644 --- a/pumpkin-crates/core/src/engine/state.rs +++ b/pumpkin-crates/core/src/engine/state.rs @@ -733,10 +733,12 @@ impl State { /// Run the checker for the given inference code on the given inference. fn run_checker( &self, - premises: impl IntoIterator + Clone, + premises: impl IntoIterator, consequent: Option, inference_code: &InferenceCode, ) { + let premises: Vec<_> = premises.into_iter().collect(); + let checkers = self .checkers .get(inference_code) @@ -756,7 +758,7 @@ impl State { panic!("inconsistent atomics in inference by {inference_code:?}") }); - checker.check(variable_state) + checker.check(variable_state, &premises, consequent.as_ref()) }); assert!( From 79acb0d628edf8b2a57af71166345d341d18f449 Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Tue, 20 Jan 2026 10:39:47 +0100 Subject: [PATCH 34/48] Enable testing whether an atomic constrains a specific variable --- pumpkin-checker/src/model.rs | 16 ++++++++++++++++ pumpkin-crates/checking/src/variable.rs | 7 +++++++ .../core/src/engine/variables/affine_view.rs | 4 ++++ .../core/src/engine/variables/domain_id.rs | 4 ++++ .../core/src/engine/variables/literal.rs | 1 + 5 files changed, 32 insertions(+) diff --git a/pumpkin-checker/src/model.rs b/pumpkin-checker/src/model.rs index dbfc2b566..c5c7431ae 100644 --- a/pumpkin-checker/src/model.rs +++ b/pumpkin-checker/src/model.rs @@ -124,6 +124,18 @@ impl From> for Variable { } impl CheckerVariable for Variable { + fn does_atomic_constrain_self(&self, atomic: Atomic) -> bool { + let Variable(VariableExpr::Identifier(ident)) = self else { + return false; + }; + + let Atomic::IntAtomic(atomic) = atomic else { + return false; + }; + + &atomic.name == ident + } + fn atomic_less_than(&self, value: i32) -> Atomic { match self.0 { VariableExpr::Identifier(ref name) => Atomic::from(IntAtomic { @@ -257,6 +269,10 @@ enum Rounding { } impl CheckerVariable for Term { + fn does_atomic_constrain_self(&self, atomic: Atomic) -> bool { + self.variable.does_atomic_constrain_self(atomic) + } + fn atomic_less_than(&self, value: i32) -> Atomic { if self.weight.is_negative() { let inverted_value = self.invert(value, Rounding::Up); diff --git a/pumpkin-crates/checking/src/variable.rs b/pumpkin-crates/checking/src/variable.rs index 8f66bfac0..3f5f296d2 100644 --- a/pumpkin-crates/checking/src/variable.rs +++ b/pumpkin-crates/checking/src/variable.rs @@ -8,6 +8,9 @@ use crate::VariableState; /// A variable in a constraint satisfaction problem. pub trait CheckerVariable: Debug + Clone { + /// Tests whether the given atomic is a statement over the variable `self`. + fn does_atomic_constrain_self(&self, atomic: Atomic) -> bool; + /// Get the atomic constraint `[self <= value]`. fn atomic_less_than(&self, value: i32) -> Atomic; @@ -49,6 +52,10 @@ pub trait CheckerVariable: Debug + Clone { } impl CheckerVariable for &'static str { + fn does_atomic_constrain_self(&self, atomic: TestAtomic) -> bool { + &atomic.name == self + } + fn atomic_less_than(&self, value: i32) -> TestAtomic { TestAtomic { name: self, diff --git a/pumpkin-crates/core/src/engine/variables/affine_view.rs b/pumpkin-crates/core/src/engine/variables/affine_view.rs index a2bfaa7e5..69d159b45 100644 --- a/pumpkin-crates/core/src/engine/variables/affine_view.rs +++ b/pumpkin-crates/core/src/engine/variables/affine_view.rs @@ -51,6 +51,10 @@ impl AffineView { } impl CheckerVariable for AffineView { + fn does_atomic_constrain_self(&self, atomic: Predicate) -> bool { + self.inner.does_atomic_constrain_self(atomic) + } + fn atomic_less_than(&self, value: i32) -> Predicate { use crate::predicate; diff --git a/pumpkin-crates/core/src/engine/variables/domain_id.rs b/pumpkin-crates/core/src/engine/variables/domain_id.rs index 6ad5d2f87..9063b22cf 100644 --- a/pumpkin-crates/core/src/engine/variables/domain_id.rs +++ b/pumpkin-crates/core/src/engine/variables/domain_id.rs @@ -31,6 +31,10 @@ impl DomainId { } impl CheckerVariable for DomainId { + fn does_atomic_constrain_self(&self, atomic: Predicate) -> bool { + atomic.get_domain() == *self + } + fn atomic_less_than(&self, value: i32) -> Predicate { use crate::predicate; diff --git a/pumpkin-crates/core/src/engine/variables/literal.rs b/pumpkin-crates/core/src/engine/variables/literal.rs index edbf29bb6..cc7351734 100644 --- a/pumpkin-crates/core/src/engine/variables/literal.rs +++ b/pumpkin-crates/core/src/engine/variables/literal.rs @@ -74,6 +74,7 @@ macro_rules! forward { } impl CheckerVariable for Literal { + forward!(integer_variable, fn does_atomic_constrain_self(&self, atomic: Predicate) -> bool); forward!(integer_variable, fn atomic_less_than(&self, value: i32) -> Predicate); forward!(integer_variable, fn atomic_greater_than(&self, value: i32) -> Predicate); forward!(integer_variable, fn atomic_not_equal(&self, value: i32) -> Predicate); From 270865671a0a3c413121f564b7f80ef805ba5681 Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Tue, 20 Jan 2026 10:47:17 +0100 Subject: [PATCH 35/48] Implement inference checker for maximum --- .../src/propagators/arithmetic/maximum.rs | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/pumpkin-crates/propagators/src/propagators/arithmetic/maximum.rs b/pumpkin-crates/propagators/src/propagators/arithmetic/maximum.rs index 22c8b86a1..5236a1848 100644 --- a/pumpkin-crates/propagators/src/propagators/arithmetic/maximum.rs +++ b/pumpkin-crates/propagators/src/propagators/arithmetic/maximum.rs @@ -1,3 +1,7 @@ +use pumpkin_checking::AtomicConstraint; +use pumpkin_checking::CheckerVariable; +use pumpkin_checking::I32Ext; +use pumpkin_checking::InferenceChecker; use pumpkin_core::conjunction; use pumpkin_core::declare_inference_label; use pumpkin_core::predicate; @@ -5,6 +9,7 @@ use pumpkin_core::predicates::PropositionalConjunction; use pumpkin_core::proof::ConstraintTag; use pumpkin_core::proof::InferenceCode; use pumpkin_core::propagation::DomainEvents; +use pumpkin_core::propagation::InferenceCheckers; use pumpkin_core::propagation::LocalId; use pumpkin_core::propagation::Priority; use pumpkin_core::propagation::PropagationContext; @@ -31,6 +36,16 @@ where { type PropagatorImpl = MaximumPropagator; + fn add_inference_checkers(&self, mut checkers: InferenceCheckers<'_>) { + checkers.add_inference_checker( + InferenceCode::new(self.constraint_tag, Maximum), + Box::new(MaximumChecker { + array: self.array.clone(), + rhs: self.rhs.clone(), + }), + ); + } + fn create(self, mut context: PropagatorConstructorContext) -> Self::PropagatorImpl { let MaximumArgs { array, @@ -170,6 +185,45 @@ impl Prop } } +#[derive(Clone, Debug)] +pub struct MaximumChecker { + pub array: Box<[ElementVar]>, + pub rhs: Rhs, +} + +impl InferenceChecker for MaximumChecker +where + Atomic: AtomicConstraint, + ElementVar: CheckerVariable, + Rhs: CheckerVariable, +{ + fn check( + &self, + state: pumpkin_checking::VariableState, + _: &[Atomic], + _: Option<&Atomic>, + ) -> bool { + let lowest_maximum = self + .array + .iter() + .map(|element| element.induced_lower_bound(&state)) + .max() + .unwrap_or(I32Ext::NegativeInf); + let highest_maximum = self + .array + .iter() + .map(|element| element.induced_upper_bound(&state)) + .max() + .unwrap_or(I32Ext::PositiveInf); + + // If the intersection between the domain of `rhs` and `[lowest_maximum, + // highest_maximum]` is empty, there is a conflict. + + lowest_maximum > self.rhs.induced_upper_bound(&state) + || highest_maximum < self.rhs.induced_lower_bound(&state) + } +} + #[allow(deprecated, reason = "Will be refactored")] #[cfg(test)] mod tests { From 456ba66a83387b28a7f83d63848d54342a092af2 Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Tue, 20 Jan 2026 10:54:04 +0100 Subject: [PATCH 36/48] Move binary equals checker to propagators crate --- pumpkin-checker/src/inferences/arithmetic.rs | 50 ++++------------- .../arithmetic/binary/binary_equals.rs | 56 +++++++++++++++++++ 2 files changed, 67 insertions(+), 39 deletions(-) diff --git a/pumpkin-checker/src/inferences/arithmetic.rs b/pumpkin-checker/src/inferences/arithmetic.rs index 02573104d..a42b7fda2 100644 --- a/pumpkin-checker/src/inferences/arithmetic.rs +++ b/pumpkin-checker/src/inferences/arithmetic.rs @@ -1,8 +1,9 @@ use std::collections::BTreeSet; use pumpkin_checking::CheckerVariable; -use pumpkin_checking::I32Ext; +use pumpkin_checking::InferenceChecker; use pumpkin_checking::VariableState; +use pumpkin_propagators::arithmetic::BinaryEqualsChecker; use super::Fact; use crate::inferences::InvalidInference; @@ -16,9 +17,9 @@ use crate::model::Constraint; /// `linear_bounds` inference is that in the binary case, we can certify holes in the domain as /// well. pub(crate) fn verify_binary_equals( - _: &Fact, + fact: &Fact, constraint: &Constraint, - mut state: VariableState, + state: VariableState, ) -> Result<(), InvalidInference> { // To check this inference we expect the intersection of both domains to be empty. @@ -31,45 +32,16 @@ pub(crate) fn verify_binary_equals( return Err(InvalidInference::Unsound); } - let term_a = &linear.terms[0]; - let term_b = &linear.terms[1]; - - let weight_a = term_a.weight.get(); - let weight_b = term_b.weight.get(); - - // TODO: Generalize this rule to work with non-unit weights. - // At the moment we expect one term to have weight `-1` and the other term to have weight - // `1`. - if weight_a + weight_b != 0 || weight_a.abs() != 1 || weight_b.abs() != 1 { - return Err(InvalidInference::Unsound); - } - - // We apply the domain of variable 2 to variable 1. If the state remains consistent, then - // the step is unsound! - let mut consistent = true; + let lhs = linear.terms[0].clone(); + let rhs = linear.terms[1].clone(); - if let I32Ext::I32(value) = term_b.induced_upper_bound(&state) { - let atomic = term_a.atomic_less_than(linear.bound + value); - consistent &= state.apply(&atomic); - } - - if let I32Ext::I32(value) = term_b.induced_lower_bound(&state) { - let atomic = term_a.atomic_greater_than(linear.bound + value); - consistent &= state.apply(&atomic); - } + let checker = BinaryEqualsChecker { lhs, rhs }; - for value in term_b.induced_holes(&state).collect::>() { - let atomic = term_a.atomic_not_equal(linear.bound + value); - consistent &= state.apply(&atomic); + if checker.check(state, &fact.premises, fact.consequent.as_ref()) { + Ok(()) + } else { + Err(InvalidInference::Unsound) } - - if consistent { - // The intersection of the domains should yield an inconsistent state for the - // inference to be sound. - return Err(InvalidInference::Unsound); - } - - Ok(()) } /// Verify a `binary_not_equals` inference. diff --git a/pumpkin-crates/propagators/src/propagators/arithmetic/binary/binary_equals.rs b/pumpkin-crates/propagators/src/propagators/arithmetic/binary/binary_equals.rs index 56ee42373..bcbdaea63 100644 --- a/pumpkin-crates/propagators/src/propagators/arithmetic/binary/binary_equals.rs +++ b/pumpkin-crates/propagators/src/propagators/arithmetic/binary/binary_equals.rs @@ -2,6 +2,10 @@ use std::slice; use bitfield_struct::bitfield; +use pumpkin_checking::AtomicConstraint; +use pumpkin_checking::CheckerVariable; +use pumpkin_checking::I32Ext; +use pumpkin_checking::InferenceChecker; use pumpkin_core::asserts::pumpkin_assert_advanced; use pumpkin_core::conjunction; use pumpkin_core::containers::HashSet; @@ -17,6 +21,7 @@ use pumpkin_core::propagation::DomainEvents; use pumpkin_core::propagation::Domains; use pumpkin_core::propagation::EnqueueDecision; use pumpkin_core::propagation::ExplanationContext; +use pumpkin_core::propagation::InferenceCheckers; use pumpkin_core::propagation::LocalId; use pumpkin_core::propagation::NotificationContext; use pumpkin_core::propagation::OpaqueDomainEvent; @@ -48,6 +53,16 @@ where { type PropagatorImpl = BinaryEqualsPropagator; + fn add_inference_checkers(&self, mut checkers: InferenceCheckers<'_>) { + checkers.add_inference_checker( + InferenceCode::new(self.constraint_tag, BinaryEquals), + Box::new(BinaryEqualsChecker { + lhs: self.a.clone(), + rhs: self.b.clone(), + }), + ); + } + fn create(self, mut context: PropagatorConstructorContext) -> Self::PropagatorImpl { let BinaryEqualsPropagatorArgs { a, @@ -383,6 +398,47 @@ struct BinaryEqualsPropagation { __: u16, } +#[derive(Clone, Debug)] +pub struct BinaryEqualsChecker { + pub lhs: Lhs, + pub rhs: Rhs, +} + +impl InferenceChecker for BinaryEqualsChecker +where + Atomic: AtomicConstraint, + Lhs: CheckerVariable, + Rhs: CheckerVariable, +{ + fn check( + &self, + mut state: pumpkin_checking::VariableState, + _: &[Atomic], + _: Option<&Atomic>, + ) -> bool { + // We apply the domain of variable 2 to variable 1. If the state remains consistent, then + // the step is unsound! + let mut consistent = true; + + if let I32Ext::I32(value) = self.rhs.induced_upper_bound(&state) { + let atomic = self.lhs.atomic_less_than(value); + consistent &= state.apply(&atomic); + } + + if let I32Ext::I32(value) = self.rhs.induced_lower_bound(&state) { + let atomic = self.lhs.atomic_greater_than(value); + consistent &= state.apply(&atomic); + } + + for value in self.rhs.induced_holes(&state).collect::>() { + let atomic = self.lhs.atomic_not_equal(value); + consistent &= state.apply(&atomic); + } + + !consistent + } +} + #[allow(deprecated, reason = "Will be refactored")] #[cfg(test)] mod tests { From 94ff8bcb7fd0888703049ad7c715e4583091c156 Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Tue, 20 Jan 2026 10:57:35 +0100 Subject: [PATCH 37/48] Implement binary not equals checker --- .../arithmetic/binary/binary_not_equals.rs | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/pumpkin-crates/propagators/src/propagators/arithmetic/binary/binary_not_equals.rs b/pumpkin-crates/propagators/src/propagators/arithmetic/binary/binary_not_equals.rs index e99bccac9..efe6d8768 100644 --- a/pumpkin-crates/propagators/src/propagators/arithmetic/binary/binary_not_equals.rs +++ b/pumpkin-crates/propagators/src/propagators/arithmetic/binary/binary_not_equals.rs @@ -1,3 +1,6 @@ +use pumpkin_checking::AtomicConstraint; +use pumpkin_checking::CheckerVariable; +use pumpkin_checking::InferenceChecker; use pumpkin_core::conjunction; use pumpkin_core::declare_inference_label; use pumpkin_core::predicate; @@ -5,6 +8,7 @@ use pumpkin_core::proof::ConstraintTag; use pumpkin_core::proof::InferenceCode; use pumpkin_core::propagation::DomainEvents; use pumpkin_core::propagation::Domains; +use pumpkin_core::propagation::InferenceCheckers; use pumpkin_core::propagation::LocalId; use pumpkin_core::propagation::Priority; use pumpkin_core::propagation::PropagationContext; @@ -33,6 +37,16 @@ where { type PropagatorImpl = BinaryNotEqualsPropagator; + fn add_inference_checkers(&self, mut checkers: InferenceCheckers<'_>) { + checkers.add_inference_checker( + InferenceCode::new(self.constraint_tag, BinaryNotEquals), + Box::new(BinaryNotEqualsChecker { + lhs: self.a.clone(), + rhs: self.b.clone(), + }), + ); + } + fn create(self, mut context: PropagatorConstructorContext) -> Self::PropagatorImpl { let BinaryNotEqualsPropagatorArgs { a, @@ -168,6 +182,30 @@ where } } +#[derive(Clone, Debug)] +pub struct BinaryNotEqualsChecker { + pub lhs: Lhs, + pub rhs: Rhs, +} + +impl InferenceChecker for BinaryNotEqualsChecker +where + Atomic: AtomicConstraint, + Lhs: CheckerVariable, + Rhs: CheckerVariable, +{ + fn check( + &self, + state: pumpkin_checking::VariableState, + _: &[Atomic], + _: Option<&Atomic>, + ) -> bool { + // There is a conflict if both variables are fixed to the same values. + + self.lhs.induced_fixed_value(&state) == self.rhs.induced_fixed_value(&state) + } +} + #[allow(deprecated, reason = "Will be refactored")] #[cfg(test)] mod tests { From daacfbc23ecb486f28e4cfcd14a3f20887f5c1be Mon Sep 17 00:00:00 2001 From: Imko Marijnissen Date: Tue, 20 Jan 2026 12:59:44 +0100 Subject: [PATCH 38/48] feat: adding checker for disjunctive edge finding --- pumpkin-checker/src/model.rs | 4 +- .../checking/src/atomic_constraint.rs | 3 +- pumpkin-crates/checking/src/i32_ext.rs | 11 + pumpkin-crates/checking/src/variable.rs | 4 +- .../core/src/engine/variables/affine_view.rs | 2 +- .../core/src/engine/variables/domain_id.rs | 2 +- .../core/src/engine/variables/literal.rs | 2 +- .../src/propagators/disjunctive/checker.rs | 496 ++++++++++++++++++ .../disjunctive/disjunctive_propagator.rs | 18 + .../src/propagators/disjunctive/mod.rs | 2 + .../disjunctive/theta_lambda_tree.rs | 14 +- 11 files changed, 543 insertions(+), 15 deletions(-) create mode 100644 pumpkin-crates/propagators/src/propagators/disjunctive/checker.rs diff --git a/pumpkin-checker/src/model.rs b/pumpkin-checker/src/model.rs index c5c7431ae..7eb64d091 100644 --- a/pumpkin-checker/src/model.rs +++ b/pumpkin-checker/src/model.rs @@ -124,7 +124,7 @@ impl From> for Variable { } impl CheckerVariable for Variable { - fn does_atomic_constrain_self(&self, atomic: Atomic) -> bool { + fn does_atomic_constrain_self(&self, atomic: &Atomic) -> bool { let Variable(VariableExpr::Identifier(ident)) = self else { return false; }; @@ -269,7 +269,7 @@ enum Rounding { } impl CheckerVariable for Term { - fn does_atomic_constrain_self(&self, atomic: Atomic) -> bool { + fn does_atomic_constrain_self(&self, atomic: &Atomic) -> bool { self.variable.does_atomic_constrain_self(atomic) } diff --git a/pumpkin-crates/checking/src/atomic_constraint.rs b/pumpkin-crates/checking/src/atomic_constraint.rs index 2decb8f89..6be4a9508 100644 --- a/pumpkin-crates/checking/src/atomic_constraint.rs +++ b/pumpkin-crates/checking/src/atomic_constraint.rs @@ -1,3 +1,4 @@ +use std::fmt::Debug; use std::fmt::Display; use std::hash::Hash; @@ -7,7 +8,7 @@ use std::hash::Hash; /// - `identifier` identifies a variable, /// - `op` is a [`Comparison`], /// - and `value` is an integer. -pub trait AtomicConstraint: Sized { +pub trait AtomicConstraint: Sized + Debug { /// The type of identifier used for variables. type Identifier: Hash + Eq; diff --git a/pumpkin-crates/checking/src/i32_ext.rs b/pumpkin-crates/checking/src/i32_ext.rs index bd8493812..05922ccc7 100644 --- a/pumpkin-crates/checking/src/i32_ext.rs +++ b/pumpkin-crates/checking/src/i32_ext.rs @@ -22,6 +22,17 @@ impl From for I32Ext { } } +impl TryInto for I32Ext { + type Error = (); + + fn try_into(self) -> Result { + match self { + I32Ext::I32(inner) => Ok(inner), + I32Ext::NegativeInf | I32Ext::PositiveInf => Err(()), + } + } +} + impl PartialEq for I32Ext { fn eq(&self, other: &i32) -> bool { match self { diff --git a/pumpkin-crates/checking/src/variable.rs b/pumpkin-crates/checking/src/variable.rs index 3f5f296d2..f8421bb5a 100644 --- a/pumpkin-crates/checking/src/variable.rs +++ b/pumpkin-crates/checking/src/variable.rs @@ -9,7 +9,7 @@ use crate::VariableState; /// A variable in a constraint satisfaction problem. pub trait CheckerVariable: Debug + Clone { /// Tests whether the given atomic is a statement over the variable `self`. - fn does_atomic_constrain_self(&self, atomic: Atomic) -> bool; + fn does_atomic_constrain_self(&self, atomic: &Atomic) -> bool; /// Get the atomic constraint `[self <= value]`. fn atomic_less_than(&self, value: i32) -> Atomic; @@ -52,7 +52,7 @@ pub trait CheckerVariable: Debug + Clone { } impl CheckerVariable for &'static str { - fn does_atomic_constrain_self(&self, atomic: TestAtomic) -> bool { + fn does_atomic_constrain_self(&self, atomic: &TestAtomic) -> bool { &atomic.name == self } diff --git a/pumpkin-crates/core/src/engine/variables/affine_view.rs b/pumpkin-crates/core/src/engine/variables/affine_view.rs index 69d159b45..610d12dc8 100644 --- a/pumpkin-crates/core/src/engine/variables/affine_view.rs +++ b/pumpkin-crates/core/src/engine/variables/affine_view.rs @@ -51,7 +51,7 @@ impl AffineView { } impl CheckerVariable for AffineView { - fn does_atomic_constrain_self(&self, atomic: Predicate) -> bool { + fn does_atomic_constrain_self(&self, atomic: &Predicate) -> bool { self.inner.does_atomic_constrain_self(atomic) } diff --git a/pumpkin-crates/core/src/engine/variables/domain_id.rs b/pumpkin-crates/core/src/engine/variables/domain_id.rs index 9063b22cf..5b51f93f8 100644 --- a/pumpkin-crates/core/src/engine/variables/domain_id.rs +++ b/pumpkin-crates/core/src/engine/variables/domain_id.rs @@ -31,7 +31,7 @@ impl DomainId { } impl CheckerVariable for DomainId { - fn does_atomic_constrain_self(&self, atomic: Predicate) -> bool { + fn does_atomic_constrain_self(&self, atomic: &Predicate) -> bool { atomic.get_domain() == *self } diff --git a/pumpkin-crates/core/src/engine/variables/literal.rs b/pumpkin-crates/core/src/engine/variables/literal.rs index cc7351734..76da66254 100644 --- a/pumpkin-crates/core/src/engine/variables/literal.rs +++ b/pumpkin-crates/core/src/engine/variables/literal.rs @@ -74,7 +74,7 @@ macro_rules! forward { } impl CheckerVariable for Literal { - forward!(integer_variable, fn does_atomic_constrain_self(&self, atomic: Predicate) -> bool); + forward!(integer_variable, fn does_atomic_constrain_self(&self, atomic: &Predicate) -> bool); forward!(integer_variable, fn atomic_less_than(&self, value: i32) -> Predicate); forward!(integer_variable, fn atomic_greater_than(&self, value: i32) -> Predicate); forward!(integer_variable, fn atomic_not_equal(&self, value: i32) -> Predicate); diff --git a/pumpkin-crates/propagators/src/propagators/disjunctive/checker.rs b/pumpkin-crates/propagators/src/propagators/disjunctive/checker.rs new file mode 100644 index 000000000..2df567ade --- /dev/null +++ b/pumpkin-crates/propagators/src/propagators/disjunctive/checker.rs @@ -0,0 +1,496 @@ +use std::cmp::max; +use std::cmp::min; +use std::marker::PhantomData; + +use pumpkin_checking::AtomicConstraint; +use pumpkin_checking::CheckerVariable; +use pumpkin_checking::I32Ext; +use pumpkin_checking::InferenceChecker; +use pumpkin_checking::VariableState; +use pumpkin_core::containers::KeyedVec; +use pumpkin_core::containers::StorageKey; +use pumpkin_core::propagation::LocalId; + +use crate::disjunctive::ArgDisjunctiveTask; +use crate::disjunctive::disjunctive_task::DisjunctiveTask; +use crate::disjunctive::theta_lambda_tree::Node; + +#[derive(Clone, Debug)] +pub struct DisjunctiveEdgeFindingChecker { + pub tasks: Box<[ArgDisjunctiveTask]>, +} + +impl InferenceChecker for DisjunctiveEdgeFindingChecker +where + Var: CheckerVariable, + Atomic: AtomicConstraint, +{ + fn check( + &self, + state: VariableState, + _premises: &[Atomic], + consequent: Option<&Atomic>, + ) -> bool { + // Recall the following: + // - For conflict detection, the explanation represents a set omega with the following + // property: `p_omega > lct_omega - est_omega`. + // + // We simply need to check whether the interval [est_omega, lct_omega] is overloaded + // - For propagation, the explanation represents a set omega (and omega') such that the + // following holds: `min(est_i, est_omega) + p_omega + p_i > lct_omega -> [s_i >= + // ect_omega]`. + let mut lb_interval = i32::MAX; + let mut ub_interval = i32::MIN; + let mut p = 0; + let mut propagating_task = None; + let mut theta = Vec::new(); + + // We go over all of the tasks + for task in self.tasks.iter() { + // Only if they are present in the explanation, do we actually process them + // - For tasks in omega, both bounds should be present to define the interval + // - For the propagating task, the lower-bound should be present, and the negation of + // the consequent ensures that an upper-bound is present + if task.start_time.induced_lower_bound(&state) != I32Ext::NegativeInf + && task.start_time.induced_upper_bound(&state) != I32Ext::PositiveInf + { + // Now we calculate the durations of tasks + let est_task: i32 = task + .start_time + .induced_lower_bound(&state) + .try_into() + .unwrap(); + let lst_task = + >::try_into(task.start_time.induced_upper_bound(&state)) + .unwrap(); + + let is_propagating_task = if let Some(consequent) = consequent { + task.start_time.does_atomic_constrain_self(consequent) + } else { + false + }; + if !is_propagating_task { + theta.push(task.clone()); + p += task.processing_time; + lb_interval = lb_interval.min(est_task); + ub_interval = ub_interval.max(lst_task + task.processing_time); + } else { + propagating_task = Some(task.clone()); + } + } + } + + if consequent.is_some() { + let propagating_task = propagating_task + .expect("If there is a consequent then there should be a propagating task"); + + let est_task = propagating_task + .start_time + .induced_lower_bound(&state) + .try_into() + .unwrap(); + + let mut theta_lambda_tree = CheckerThetaLambdaTree::new( + &theta + .iter() + .enumerate() + .map(|(index, task)| DisjunctiveTask { + start_time: task.start_time.clone(), + processing_time: task.processing_time, + id: LocalId::from(index as u32), + }) + .collect::>(), + ); + theta_lambda_tree.update(&state); + for (index, task) in theta.iter().enumerate() { + theta_lambda_tree.add_to_theta( + &DisjunctiveTask { + start_time: task.start_time.clone(), + processing_time: task.processing_time, + id: LocalId::from(index as u32), + }, + &state, + ); + } + + min(est_task, lb_interval) + p + propagating_task.processing_time > ub_interval + && theta_lambda_tree.ect() > propagating_task.start_time.induced_upper_bound(&state) + } else { + // We simply check whether the interval is overloaded + p > (ub_interval - lb_interval) + } + } +} + +#[cfg(test)] +mod tests { + use pumpkin_checking::TestAtomic; + use pumpkin_checking::VariableState; + + use super::*; + + #[test] + fn test_simple_propagation() { + let premises = [ + TestAtomic { + name: "x1", + comparison: pumpkin_checking::Comparison::GreaterEqual, + value: 0, + }, + TestAtomic { + name: "x1", + comparison: pumpkin_checking::Comparison::LessEqual, + value: 7, + }, + TestAtomic { + name: "x2", + comparison: pumpkin_checking::Comparison::GreaterEqual, + value: 5, + }, + TestAtomic { + name: "x2", + comparison: pumpkin_checking::Comparison::LessEqual, + value: 6, + }, + TestAtomic { + name: "x3", + comparison: pumpkin_checking::Comparison::GreaterEqual, + value: 0, + }, + ]; + + let consequent = Some(TestAtomic { + name: "x3", + comparison: pumpkin_checking::Comparison::GreaterEqual, + value: 8, + }); + let state = VariableState::prepare_for_conflict_check(premises, consequent) + .expect("no conflicting atomics"); + + let checker = DisjunctiveEdgeFindingChecker { + tasks: vec![ + ArgDisjunctiveTask { + start_time: "x1", + processing_time: 2, + }, + ArgDisjunctiveTask { + start_time: "x2", + processing_time: 3, + }, + ArgDisjunctiveTask { + start_time: "x3", + processing_time: 5, + }, + ] + .into(), + }; + + assert!(checker.check(state, &premises, consequent.as_ref())); + } + + #[test] + fn test_conflict() { + let premises = [ + TestAtomic { + name: "x1", + comparison: pumpkin_checking::Comparison::GreaterEqual, + value: 0, + }, + TestAtomic { + name: "x1", + comparison: pumpkin_checking::Comparison::LessEqual, + value: 1, + }, + TestAtomic { + name: "x2", + comparison: pumpkin_checking::Comparison::GreaterEqual, + value: 0, + }, + TestAtomic { + name: "x2", + comparison: pumpkin_checking::Comparison::LessEqual, + value: 1, + }, + ]; + + let state = VariableState::prepare_for_conflict_check(premises, None) + .expect("no conflicting atomics"); + + let checker = DisjunctiveEdgeFindingChecker { + tasks: vec![ + ArgDisjunctiveTask { + start_time: "x1", + processing_time: 2, + }, + ArgDisjunctiveTask { + start_time: "x2", + processing_time: 3, + }, + ] + .into(), + }; + + assert!(checker.check(state, &premises, None)); + } + + #[test] + fn test_simple_propagation_not_accepted() { + let premises = [ + TestAtomic { + name: "x1", + comparison: pumpkin_checking::Comparison::GreaterEqual, + value: 0, + }, + TestAtomic { + name: "x1", + comparison: pumpkin_checking::Comparison::LessEqual, + value: 7, + }, + TestAtomic { + name: "x2", + comparison: pumpkin_checking::Comparison::GreaterEqual, + value: 5, + }, + TestAtomic { + name: "x2", + comparison: pumpkin_checking::Comparison::LessEqual, + value: 6, + }, + TestAtomic { + name: "x3", + comparison: pumpkin_checking::Comparison::GreaterEqual, + value: 0, + }, + ]; + + let consequent = Some(TestAtomic { + name: "x3", + comparison: pumpkin_checking::Comparison::GreaterEqual, + value: 9, + }); + let state = VariableState::prepare_for_conflict_check(premises, consequent) + .expect("no conflicting atomics"); + + let checker = DisjunctiveEdgeFindingChecker { + tasks: vec![ + ArgDisjunctiveTask { + start_time: "x1", + processing_time: 2, + }, + ArgDisjunctiveTask { + start_time: "x2", + processing_time: 3, + }, + ArgDisjunctiveTask { + start_time: "x3", + processing_time: 5, + }, + ] + .into(), + }; + + assert!(!checker.check(state, &premises, consequent.as_ref())); + } + + #[test] + fn test_conflict_not_accepted() { + let premises = [ + TestAtomic { + name: "x1", + comparison: pumpkin_checking::Comparison::GreaterEqual, + value: 0, + }, + TestAtomic { + name: "x1", + comparison: pumpkin_checking::Comparison::LessEqual, + value: 1, + }, + TestAtomic { + name: "x2", + comparison: pumpkin_checking::Comparison::GreaterEqual, + value: 0, + }, + TestAtomic { + name: "x2", + comparison: pumpkin_checking::Comparison::LessEqual, + value: 2, + }, + ]; + + let state = VariableState::prepare_for_conflict_check(premises, None) + .expect("no conflicting atomics"); + + let checker = DisjunctiveEdgeFindingChecker { + tasks: vec![ + ArgDisjunctiveTask { + start_time: "x1", + processing_time: 2, + }, + ArgDisjunctiveTask { + start_time: "x2", + processing_time: 3, + }, + ] + .into(), + }; + + assert!(!checker.check(state, &premises, None)); + } +} + +#[derive(Debug, Clone)] +pub(super) struct CheckerThetaLambdaTree { + pub(super) nodes: Vec, + /// Then we keep track of a mapping from the [`LocalId`] to its position in the tree since the + /// methods take as input tasks with [`LocalId`]s. + mapping: KeyedVec, + /// The number of internal nodes in the tree; used to calculate the leaf node index based on + /// the index in the tree + number_of_internal_nodes: usize, + /// The tasks which are stored in the leaves of the tree. + /// + /// These tasks are sorted based on non-decreasing start time. + sorted_tasks: Vec>, + phantom_data: PhantomData, +} + +impl> CheckerThetaLambdaTree { + /// Initialises the theta-lambda tree. + /// + /// Note that [`Self::update`] should be called to actually create the tree itself. + pub(super) fn new(tasks: &[DisjunctiveTask]) -> Self { + // Calculate the number of internal nodes which are required to create the binary tree + let mut number_of_internal_nodes = 1; + while number_of_internal_nodes < tasks.len() { + number_of_internal_nodes <<= 1; + } + + CheckerThetaLambdaTree { + nodes: Default::default(), + mapping: KeyedVec::default(), + number_of_internal_nodes: number_of_internal_nodes - 1, + sorted_tasks: tasks.to_vec(), + phantom_data: PhantomData, + } + } + + /// Update the theta-lambda tree based on the provided `context`. + /// + /// It resets theta and lambda to be the empty set. + pub(super) fn update(&mut self, context: &VariableState) { + // First we sort the tasks by lower-bound/earliest start time. + self.sorted_tasks + .sort_by_key(|task| task.start_time.induced_lower_bound(context)); + + // Then we keep track of a mapping from the [`LocalId`] to its position in the tree and a + // reverse mapping + self.mapping.clear(); + for (index, task) in self.sorted_tasks.iter().enumerate() { + while self.mapping.len() <= task.id.index() { + let _ = self.mapping.push(usize::MAX); + } + self.mapping[task.id] = index; + } + + // Finally, we reset the entire tree to be empty + self.nodes.clear(); + for _ in 0..=2 * self.number_of_internal_nodes { + self.nodes.push(Node::empty()) + } + } + + /// Returns the earliest completion time of Theta + pub(super) fn ect(&self) -> i32 { + assert!(!self.nodes.is_empty()); + self.nodes[0].ect + } + + /// Add the provided task to Theta + pub(super) fn add_to_theta( + &mut self, + task: &DisjunctiveTask, + context: &VariableState, + ) { + // We need to find the leaf node index; note that there are |nodes| / 2 leaves + let position = self.nodes.len() / 2 + self.mapping[task.id]; + let ect = task.start_time.induced_lower_bound(context) + task.processing_time; + + self.nodes[position] = Node::new_white_node( + ect.try_into().expect("Should have bounds"), + task.processing_time, + ); + self.upheap(position) + } + + /// Returns the index of the left child of the provided index + fn get_left_child_index(index: usize) -> usize { + 2 * index + 1 + } + + /// Returns the index of the right child of the provided index + fn get_right_child_index(index: usize) -> usize { + 2 * index + 2 + } + + /// Returns the index of the parent of the provided index + fn get_parent(index: usize) -> usize { + assert!(index > 0); + (index - 1) / 2 + } + + /// Calculate the new values for the ancestors of the provided index + pub(super) fn upheap(&mut self, mut index: usize) { + while index != 0 { + let parent = Self::get_parent(index); + let left_child_of_parent = Self::get_left_child_index(parent); + let right_child_of_parent = Self::get_right_child_index(parent); + assert!(left_child_of_parent == index || right_child_of_parent == index); + + // The sum of processing times is the sum of processing times in the left child + the + // sum of processing times in right child + self.nodes[parent].sum_of_processing_times = self.nodes[left_child_of_parent] + .sum_of_processing_times + + self.nodes[right_child_of_parent].sum_of_processing_times; + + // The ECT is either the ECT of the left child node + the processing times of the right + // child or it is the ECT of the right child (we do not know whether the processing + // times of the left child influence the processing times of the right child) + let ect_left = self.nodes[left_child_of_parent].ect + + self.nodes[right_child_of_parent].sum_of_processing_times; + self.nodes[parent].ect = max(self.nodes[right_child_of_parent].ect, ect_left); + + // The sum of processing times (including one element of lambda) is either: + // 1) The sum of processing times of the right child + the sum of processing times of + // the left child including one element of lambda + // 2) The sum of processing times of the left child + the sum of processing times of the + // right child include one element of lambda + let sum_of_processing_times_left_child_lambda = self.nodes[left_child_of_parent] + .sum_of_processing_times_bar + + self.nodes[right_child_of_parent].sum_of_processing_times; + let sum_of_processing_times_right_child_lambda = self.nodes[left_child_of_parent] + .sum_of_processing_times + + self.nodes[right_child_of_parent].sum_of_processing_times_bar; + self.nodes[parent].sum_of_processing_times_bar = max( + sum_of_processing_times_left_child_lambda, + sum_of_processing_times_right_child_lambda, + ); + + // The earliest completion time (including one element of lambda) is either: + // 1) The earliest completion time including one element of lambda from the right child + // 2) The earliest completion time of the right child + the sum of processing times + // including one element of lambda of the right child + // 2) The earliest completion time of the left child + the sum of processing times + // including one element of lambda of the left child + let ect_right_child_lambda = self.nodes[left_child_of_parent].ect + + self.nodes[right_child_of_parent].sum_of_processing_times_bar; + let ect_left_child_lambda = self.nodes[left_child_of_parent].ect_bar + + self.nodes[right_child_of_parent].sum_of_processing_times; + self.nodes[parent].ect_bar = max( + self.nodes[right_child_of_parent].ect_bar, + max(ect_right_child_lambda, ect_left_child_lambda), + ); + + index = parent; + } + } +} diff --git a/pumpkin-crates/propagators/src/propagators/disjunctive/disjunctive_propagator.rs b/pumpkin-crates/propagators/src/propagators/disjunctive/disjunctive_propagator.rs index b34cf84fa..8799b938c 100644 --- a/pumpkin-crates/propagators/src/propagators/disjunctive/disjunctive_propagator.rs +++ b/pumpkin-crates/propagators/src/propagators/disjunctive/disjunctive_propagator.rs @@ -8,6 +8,7 @@ use pumpkin_core::predicates::PropositionalConjunction; use pumpkin_core::proof::ConstraintTag; use pumpkin_core::proof::InferenceCode; use pumpkin_core::propagation::DomainEvents; +use pumpkin_core::propagation::InferenceCheckers; use pumpkin_core::propagation::LocalId; use pumpkin_core::propagation::PropagationContext; use pumpkin_core::propagation::Propagator; @@ -22,6 +23,7 @@ use pumpkin_core::variables::IntegerVariable; use super::disjunctive_task::ArgDisjunctiveTask; use super::disjunctive_task::DisjunctiveTask; use super::theta_lambda_tree::ThetaLambdaTree; +use crate::disjunctive::checker::DisjunctiveEdgeFindingChecker; use crate::propagators::disjunctive::DisjunctiveEdgeFinding; /// [`Propagator`] responsible for using disjunctive reasoning to propagate the [Disjunctive](https://sofdem.github.io/gccat/gccat/Cdisjunctive.html) constraint. @@ -105,6 +107,22 @@ impl PropagatorConstructor for DisjunctiveConstr inference_code, } } + + fn add_inference_checkers(&self, mut checkers: InferenceCheckers<'_>) { + checkers.add_inference_checker( + InferenceCode::new(self.constraint_tag, DisjunctiveEdgeFinding), + Box::new(DisjunctiveEdgeFindingChecker { + tasks: self + .tasks + .iter() + .map(|task| ArgDisjunctiveTask { + start_time: task.start_time.clone(), + processing_time: task.processing_time, + }) + .collect(), + }), + ); + } } impl Propagator for DisjunctivePropagator { diff --git a/pumpkin-crates/propagators/src/propagators/disjunctive/mod.rs b/pumpkin-crates/propagators/src/propagators/disjunctive/mod.rs index 2b2de5801..df24ffae4 100644 --- a/pumpkin-crates/propagators/src/propagators/disjunctive/mod.rs +++ b/pumpkin-crates/propagators/src/propagators/disjunctive/mod.rs @@ -10,5 +10,7 @@ mod theta_tree; pub use disjunctive_propagator::DisjunctiveConstructor; pub use disjunctive_propagator::DisjunctivePropagator; pub use disjunctive_task::ArgDisjunctiveTask; +pub(crate) mod checker; +pub use checker::*; declare_inference_label!(DisjunctiveEdgeFinding); diff --git a/pumpkin-crates/propagators/src/propagators/disjunctive/theta_lambda_tree.rs b/pumpkin-crates/propagators/src/propagators/disjunctive/theta_lambda_tree.rs index b631e7688..e8c8e2834 100644 --- a/pumpkin-crates/propagators/src/propagators/disjunctive/theta_lambda_tree.rs +++ b/pumpkin-crates/propagators/src/propagators/disjunctive/theta_lambda_tree.rs @@ -19,20 +19,20 @@ use super::disjunctive_task::DisjunctiveTask; #[derive(Debug, Clone, PartialEq, Eq)] pub(super) struct Node { /// The earliest completion time of the set of tasks represented by this node. - ect: i32, + pub(super) ect: i32, /// The sum of the processing times of the set of tasks represented by this node. - sum_of_processing_times: i32, + pub(super) sum_of_processing_times: i32, /// The earliest completion time of the set of tasks represented by this node if a single grey /// task can be added to the set of tasks. - ect_bar: i32, + pub(super) ect_bar: i32, /// The sum of processing times of the set of tasks represented by this node if a single grey /// task can be added to the set of tasks. - sum_of_processing_times_bar: i32, + pub(super) sum_of_processing_times_bar: i32, } impl Node { // Constructs an empty node - fn empty() -> Self { + pub(super) fn empty() -> Self { Self { ect: i32::MIN, sum_of_processing_times: 0, @@ -42,7 +42,7 @@ impl Node { } // Construct a new white node with the provided value - fn new_white_node(ect: i32, sum_of_processing_times: i32) -> Self { + pub(super) fn new_white_node(ect: i32, sum_of_processing_times: i32) -> Self { Self { ect, sum_of_processing_times, @@ -52,7 +52,7 @@ impl Node { } // Construct a new gray node with the provided value - fn new_gray_node(ect: i32, sum_of_processing_times: i32) -> Self { + pub(super) fn new_gray_node(ect: i32, sum_of_processing_times: i32) -> Self { Self { ect: i32::MIN, sum_of_processing_times: 0, From 34153f89b32305c13c52dee9c8825ef3b66e21a1 Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Tue, 20 Jan 2026 13:27:34 +0100 Subject: [PATCH 39/48] Implement checker for integer multiplication --- pumpkin-crates/checking/src/i32_ext.rs | 60 +++++++++++++------ .../arithmetic/integer_multiplication.rs | 60 +++++++++++++++++++ 2 files changed, 101 insertions(+), 19 deletions(-) diff --git a/pumpkin-crates/checking/src/i32_ext.rs b/pumpkin-crates/checking/src/i32_ext.rs index bd8493812..8e3934f47 100644 --- a/pumpkin-crates/checking/src/i32_ext.rs +++ b/pumpkin-crates/checking/src/i32_ext.rs @@ -6,9 +6,11 @@ use std::ops::Neg; /// An [`i32`] or positive/negative infinity. /// -/// # Note -/// The result of the operation `infty + -infty` is undetermined, and if evaluated will cause a -/// panic. +/// # Notes on arithmetic operations: +/// - The result of the operation `infty + -infty` is undetermined, and if evaluated will cause a +/// panic. +/// - Multiplying [`I32Ext::PositiveInf`] or [`I32Ext::NegativeInf`] with `I32Ext::I32(0)` will +/// yield `I32Ext::I32(0)`. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum I32Ext { I32(i32), @@ -81,6 +83,14 @@ impl PartialOrd for I32Ext { } } +impl Add for I32Ext { + type Output = I32Ext; + + fn add(self, rhs: i32) -> Self::Output { + self + I32Ext::I32(rhs) + } +} + impl Add for I32Ext { type Output = I32Ext; @@ -104,38 +114,50 @@ impl Add for I32Ext { } } -impl Add for I32Ext { +impl Mul for I32Ext { type Output = I32Ext; - fn add(self, rhs: i32) -> Self::Output { - match self { - I32Ext::I32(lhs) => I32Ext::I32(lhs + rhs), - I32Ext::NegativeInf => I32Ext::NegativeInf, - I32Ext::PositiveInf => I32Ext::PositiveInf, - } + fn mul(self, rhs: i32) -> Self::Output { + self * I32Ext::I32(rhs) } } -impl Mul for I32Ext { - type Output = I32Ext; +impl Mul for I32Ext { + type Output = Self; - fn mul(self, rhs: i32) -> Self::Output { - match self { - I32Ext::I32(lhs) => I32Ext::I32(lhs * rhs), - I32Ext::NegativeInf => { - if rhs >= 0 { + fn mul(self, rhs: Self) -> Self::Output { + match (self, rhs) { + (I32Ext::I32(lhs), I32Ext::I32(rhs)) => I32Ext::I32(lhs * rhs), + + // Multiplication with 0 will always yield 0. + (I32Ext::I32(0), Self::NegativeInf) + | (I32Ext::I32(0), Self::PositiveInf) + | (Self::NegativeInf, I32Ext::I32(0)) + | (Self::PositiveInf, I32Ext::I32(0)) => I32Ext::I32(0), + + (I32Ext::I32(value), I32Ext::NegativeInf) + | (I32Ext::NegativeInf, I32Ext::I32(value)) => { + if value >= 0 { I32Ext::NegativeInf } else { I32Ext::PositiveInf } } - I32Ext::PositiveInf => { - if rhs >= 0 { + + (I32Ext::I32(value), I32Ext::PositiveInf) + | (I32Ext::PositiveInf, I32Ext::I32(value)) => { + if value >= 0 { I32Ext::PositiveInf } else { I32Ext::NegativeInf } } + + (I32Ext::NegativeInf, I32Ext::NegativeInf) + | (I32Ext::PositiveInf, I32Ext::PositiveInf) => I32Ext::PositiveInf, + + (I32Ext::NegativeInf, I32Ext::PositiveInf) + | (I32Ext::PositiveInf, I32Ext::NegativeInf) => I32Ext::NegativeInf, } } } diff --git a/pumpkin-crates/propagators/src/propagators/arithmetic/integer_multiplication.rs b/pumpkin-crates/propagators/src/propagators/arithmetic/integer_multiplication.rs index d6cb8a456..45410a37e 100644 --- a/pumpkin-crates/propagators/src/propagators/arithmetic/integer_multiplication.rs +++ b/pumpkin-crates/propagators/src/propagators/arithmetic/integer_multiplication.rs @@ -1,3 +1,6 @@ +use pumpkin_checking::AtomicConstraint; +use pumpkin_checking::CheckerVariable; +use pumpkin_checking::InferenceChecker; use pumpkin_core::asserts::pumpkin_assert_simple; use pumpkin_core::conjunction; use pumpkin_core::declare_inference_label; @@ -5,6 +8,7 @@ use pumpkin_core::predicate; use pumpkin_core::proof::ConstraintTag; use pumpkin_core::proof::InferenceCode; use pumpkin_core::propagation::DomainEvents; +use pumpkin_core::propagation::InferenceCheckers; use pumpkin_core::propagation::LocalId; use pumpkin_core::propagation::Priority; use pumpkin_core::propagation::PropagationContext; @@ -35,6 +39,17 @@ where { type PropagatorImpl = IntegerMultiplicationPropagator; + fn add_inference_checkers(&self, mut checkers: InferenceCheckers<'_>) { + checkers.add_inference_checker( + InferenceCode::new(self.constraint_tag, IntegerMultiplication), + Box::new(IntegerMultiplicationChecker { + a: self.a.clone(), + b: self.b.clone(), + c: self.c.clone(), + }), + ); + } + fn create(self, mut context: PropagatorConstructorContext) -> Self::PropagatorImpl { let IntegerMultiplicationArgs { a, @@ -357,6 +372,51 @@ fn div_ceil_pos(numerator: i32, denominator: i32) -> i32 { numerator / denominator + (numerator % denominator).signum() } +#[derive(Clone, Debug)] +pub struct IntegerMultiplicationChecker { + pub a: VA, + pub b: VB, + pub c: VC, +} + +impl InferenceChecker for IntegerMultiplicationChecker +where + Atomic: AtomicConstraint, + VA: CheckerVariable, + VB: CheckerVariable, + VC: CheckerVariable, +{ + fn check( + &self, + state: pumpkin_checking::VariableState, + _: &[Atomic], + _: Option<&Atomic>, + ) -> bool { + // We apply interval arithmetic to determine that the computed interval `a times b` + // does not intersect with the domain of `c`. + // + // See https://en.wikipedia.org/wiki/Interval_arithmetic#Interval_operators. + + let x1 = self.a.induced_lower_bound(&state); + let x2 = self.a.induced_upper_bound(&state); + let y1 = self.b.induced_lower_bound(&state); + let y2 = self.b.induced_upper_bound(&state); + + let c_lower = self.c.induced_lower_bound(&state); + let c_upper = self.c.induced_upper_bound(&state); + + let x1y1 = x1 * y1; + let x1y2 = x1 * y2; + let x2y1 = x2 * y1; + let x2y2 = x2 * y2; + + let computed_c_lower = x1y1.min(x1y2).min(x2y1).min(x2y2); + let computed_c_upper = x1y1.max(x1y2).max(x2y1).max(x2y2); + + computed_c_upper < c_lower || computed_c_lower > c_upper + } +} + #[allow(deprecated, reason = "Will be refactored")] #[cfg(test)] mod tests { From 673201069d3873b9a4f14c6c0dac87945e2ade3b Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Tue, 20 Jan 2026 13:38:05 +0100 Subject: [PATCH 40/48] Rename I32Ext to IntExt In preparation of making it generic over the integer type --- pumpkin-checker/src/model.rs | 10 +- pumpkin-crates/checking/src/i32_ext.rs | 154 +++++++++--------- pumpkin-crates/checking/src/variable.rs | 10 +- pumpkin-crates/checking/src/variable_state.rs | 32 ++-- .../core/src/engine/variables/affine_view.rs | 22 +-- .../core/src/engine/variables/domain_id.rs | 4 +- .../core/src/engine/variables/literal.rs | 6 +- .../propagators/arithmetic/absolute_value.rs | 4 +- .../arithmetic/binary/binary_equals.rs | 6 +- .../arithmetic/linear_less_or_equal.rs | 10 +- .../src/propagators/arithmetic/maximum.rs | 6 +- .../src/propagators/disjunctive/checker.rs | 8 +- 12 files changed, 136 insertions(+), 136 deletions(-) diff --git a/pumpkin-checker/src/model.rs b/pumpkin-checker/src/model.rs index 7eb64d091..629bd79b3 100644 --- a/pumpkin-checker/src/model.rs +++ b/pumpkin-checker/src/model.rs @@ -14,7 +14,7 @@ use fzn_rs::ast::Domain; use pumpkin_checking::AtomicConstraint; use pumpkin_checking::CheckerVariable; use pumpkin_checking::Comparison; -use pumpkin_checking::I32Ext; +use pumpkin_checking::IntExt; use pumpkin_checking::VariableState; use crate::math::div_ceil; @@ -180,14 +180,14 @@ impl CheckerVariable for Variable { } } - fn induced_lower_bound(&self, variable_state: &VariableState) -> I32Ext { + fn induced_lower_bound(&self, variable_state: &VariableState) -> IntExt { match self.0 { VariableExpr::Identifier(ref ident) => variable_state.lower_bound(ident), VariableExpr::Constant(value) => value.into(), } } - fn induced_upper_bound(&self, variable_state: &VariableState) -> I32Ext { + fn induced_upper_bound(&self, variable_state: &VariableState) -> IntExt { match self.0 { VariableExpr::Identifier(ref ident) => variable_state.upper_bound(ident), VariableExpr::Constant(value) => value.into(), @@ -311,7 +311,7 @@ impl CheckerVariable for Term { } } - fn induced_lower_bound(&self, variable_state: &VariableState) -> I32Ext { + fn induced_lower_bound(&self, variable_state: &VariableState) -> IntExt { if self.weight.is_positive() { self.variable.induced_lower_bound(variable_state) * self.weight.get() } else { @@ -319,7 +319,7 @@ impl CheckerVariable for Term { } } - fn induced_upper_bound(&self, variable_state: &VariableState) -> I32Ext { + fn induced_upper_bound(&self, variable_state: &VariableState) -> IntExt { if self.weight.is_positive() { self.variable.induced_upper_bound(variable_state) * self.weight.get() } else { diff --git a/pumpkin-crates/checking/src/i32_ext.rs b/pumpkin-crates/checking/src/i32_ext.rs index 338d197d9..421f5ccbb 100644 --- a/pumpkin-crates/checking/src/i32_ext.rs +++ b/pumpkin-crates/checking/src/i32_ext.rs @@ -9,191 +9,191 @@ use std::ops::Neg; /// # Notes on arithmetic operations: /// - The result of the operation `infty + -infty` is undetermined, and if evaluated will cause a /// panic. -/// - Multiplying [`I32Ext::PositiveInf`] or [`I32Ext::NegativeInf`] with `I32Ext::I32(0)` will -/// yield `I32Ext::I32(0)`. +/// - Multiplying [`IntExt::PositiveInf`] or [`IntExt::NegativeInf`] with `IntExt::I32(0)` will +/// yield `IntExt::I32(0)`. #[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum I32Ext { +pub enum IntExt { I32(i32), NegativeInf, PositiveInf, } -impl From for I32Ext { +impl From for IntExt { fn from(value: i32) -> Self { - I32Ext::I32(value) + IntExt::I32(value) } } -impl TryInto for I32Ext { +impl TryInto for IntExt { type Error = (); fn try_into(self) -> Result { match self { - I32Ext::I32(inner) => Ok(inner), - I32Ext::NegativeInf | I32Ext::PositiveInf => Err(()), + IntExt::I32(inner) => Ok(inner), + IntExt::NegativeInf | IntExt::PositiveInf => Err(()), } } } -impl PartialEq for I32Ext { +impl PartialEq for IntExt { fn eq(&self, other: &i32) -> bool { match self { - I32Ext::I32(v1) => v1 == other, - I32Ext::NegativeInf | I32Ext::PositiveInf => false, + IntExt::I32(v1) => v1 == other, + IntExt::NegativeInf | IntExt::PositiveInf => false, } } } -impl PartialEq for i32 { - fn eq(&self, other: &I32Ext) -> bool { +impl PartialEq for i32 { + fn eq(&self, other: &IntExt) -> bool { other.eq(self) } } -impl PartialOrd for i32 { - fn partial_cmp(&self, other: &I32Ext) -> Option { +impl PartialOrd for i32 { + fn partial_cmp(&self, other: &IntExt) -> Option { other.neg().partial_cmp(&self.neg()) } } -impl PartialOrd for I32Ext { - fn partial_cmp(&self, other: &I32Ext) -> Option { +impl PartialOrd for IntExt { + fn partial_cmp(&self, other: &IntExt) -> Option { Some(self.cmp(other)) } } -impl Ord for I32Ext { +impl Ord for IntExt { fn cmp(&self, other: &Self) -> Ordering { match self { - I32Ext::I32(v1) => match other { - I32Ext::I32(v2) => v1.cmp(v2), - I32Ext::NegativeInf => Ordering::Greater, - I32Ext::PositiveInf => Ordering::Less, + IntExt::I32(v1) => match other { + IntExt::I32(v2) => v1.cmp(v2), + IntExt::NegativeInf => Ordering::Greater, + IntExt::PositiveInf => Ordering::Less, }, - I32Ext::NegativeInf => match other { - I32Ext::I32(_) => Ordering::Less, - I32Ext::PositiveInf => Ordering::Less, - I32Ext::NegativeInf => Ordering::Equal, + IntExt::NegativeInf => match other { + IntExt::I32(_) => Ordering::Less, + IntExt::PositiveInf => Ordering::Less, + IntExt::NegativeInf => Ordering::Equal, }, - I32Ext::PositiveInf => match other { - I32Ext::I32(_) => Ordering::Greater, - I32Ext::NegativeInf => Ordering::Greater, - I32Ext::PositiveInf => Ordering::Greater, + IntExt::PositiveInf => match other { + IntExt::I32(_) => Ordering::Greater, + IntExt::NegativeInf => Ordering::Greater, + IntExt::PositiveInf => Ordering::Greater, }, } } } -impl PartialOrd for I32Ext { +impl PartialOrd for IntExt { fn partial_cmp(&self, other: &i32) -> Option { match self { - I32Ext::I32(v1) => v1.partial_cmp(other), - I32Ext::NegativeInf => Some(Ordering::Less), - I32Ext::PositiveInf => Some(Ordering::Greater), + IntExt::I32(v1) => v1.partial_cmp(other), + IntExt::NegativeInf => Some(Ordering::Less), + IntExt::PositiveInf => Some(Ordering::Greater), } } } -impl Add for I32Ext { - type Output = I32Ext; +impl Add for IntExt { + type Output = IntExt; fn add(self, rhs: i32) -> Self::Output { - self + I32Ext::I32(rhs) + self + IntExt::I32(rhs) } } -impl Add for I32Ext { - type Output = I32Ext; +impl Add for IntExt { + type Output = IntExt; - fn add(self, rhs: I32Ext) -> Self::Output { + fn add(self, rhs: IntExt) -> Self::Output { match (self, rhs) { - (I32Ext::I32(lhs), I32Ext::I32(rhs)) => I32Ext::I32(lhs + rhs), + (IntExt::I32(lhs), IntExt::I32(rhs)) => IntExt::I32(lhs + rhs), - (I32Ext::I32(_), Self::NegativeInf) => Self::NegativeInf, - (I32Ext::I32(_), Self::PositiveInf) => Self::PositiveInf, - (Self::NegativeInf, I32Ext::I32(_)) => Self::NegativeInf, - (Self::PositiveInf, I32Ext::I32(_)) => Self::PositiveInf, + (IntExt::I32(_), Self::NegativeInf) => Self::NegativeInf, + (IntExt::I32(_), Self::PositiveInf) => Self::PositiveInf, + (Self::NegativeInf, IntExt::I32(_)) => Self::NegativeInf, + (Self::PositiveInf, IntExt::I32(_)) => Self::PositiveInf, - (I32Ext::NegativeInf, I32Ext::NegativeInf) => I32Ext::NegativeInf, - (I32Ext::PositiveInf, I32Ext::PositiveInf) => I32Ext::PositiveInf, + (IntExt::NegativeInf, IntExt::NegativeInf) => IntExt::NegativeInf, + (IntExt::PositiveInf, IntExt::PositiveInf) => IntExt::PositiveInf, - (lhs @ I32Ext::NegativeInf, rhs @ I32Ext::PositiveInf) - | (lhs @ I32Ext::PositiveInf, rhs @ I32Ext::NegativeInf) => { + (lhs @ IntExt::NegativeInf, rhs @ IntExt::PositiveInf) + | (lhs @ IntExt::PositiveInf, rhs @ IntExt::NegativeInf) => { panic!("the result of {lhs:?} + {rhs:?} is indeterminate") } } } } -impl Mul for I32Ext { - type Output = I32Ext; +impl Mul for IntExt { + type Output = IntExt; fn mul(self, rhs: i32) -> Self::Output { - self * I32Ext::I32(rhs) + self * IntExt::I32(rhs) } } -impl Mul for I32Ext { +impl Mul for IntExt { type Output = Self; fn mul(self, rhs: Self) -> Self::Output { match (self, rhs) { - (I32Ext::I32(lhs), I32Ext::I32(rhs)) => I32Ext::I32(lhs * rhs), + (IntExt::I32(lhs), IntExt::I32(rhs)) => IntExt::I32(lhs * rhs), // Multiplication with 0 will always yield 0. - (I32Ext::I32(0), Self::NegativeInf) - | (I32Ext::I32(0), Self::PositiveInf) - | (Self::NegativeInf, I32Ext::I32(0)) - | (Self::PositiveInf, I32Ext::I32(0)) => I32Ext::I32(0), + (IntExt::I32(0), Self::NegativeInf) + | (IntExt::I32(0), Self::PositiveInf) + | (Self::NegativeInf, IntExt::I32(0)) + | (Self::PositiveInf, IntExt::I32(0)) => IntExt::I32(0), - (I32Ext::I32(value), I32Ext::NegativeInf) - | (I32Ext::NegativeInf, I32Ext::I32(value)) => { + (IntExt::I32(value), IntExt::NegativeInf) + | (IntExt::NegativeInf, IntExt::I32(value)) => { if value >= 0 { - I32Ext::NegativeInf + IntExt::NegativeInf } else { - I32Ext::PositiveInf + IntExt::PositiveInf } } - (I32Ext::I32(value), I32Ext::PositiveInf) - | (I32Ext::PositiveInf, I32Ext::I32(value)) => { + (IntExt::I32(value), IntExt::PositiveInf) + | (IntExt::PositiveInf, IntExt::I32(value)) => { if value >= 0 { - I32Ext::PositiveInf + IntExt::PositiveInf } else { - I32Ext::NegativeInf + IntExt::NegativeInf } } - (I32Ext::NegativeInf, I32Ext::NegativeInf) - | (I32Ext::PositiveInf, I32Ext::PositiveInf) => I32Ext::PositiveInf, + (IntExt::NegativeInf, IntExt::NegativeInf) + | (IntExt::PositiveInf, IntExt::PositiveInf) => IntExt::PositiveInf, - (I32Ext::NegativeInf, I32Ext::PositiveInf) - | (I32Ext::PositiveInf, I32Ext::NegativeInf) => I32Ext::NegativeInf, + (IntExt::NegativeInf, IntExt::PositiveInf) + | (IntExt::PositiveInf, IntExt::NegativeInf) => IntExt::NegativeInf, } } } -impl Neg for I32Ext { +impl Neg for IntExt { type Output = Self; fn neg(self) -> Self::Output { match self { - I32Ext::I32(value) => I32Ext::I32(-value), - I32Ext::NegativeInf => I32Ext::PositiveInf, - I32Ext::PositiveInf => Self::NegativeInf, + IntExt::I32(value) => IntExt::I32(-value), + IntExt::NegativeInf => IntExt::PositiveInf, + IntExt::PositiveInf => Self::NegativeInf, } } } -impl Sum for I32Ext { +impl Sum for IntExt { fn sum>(iter: I) -> Self { - iter.fold(I32Ext::I32(0), |acc, value| acc + value) + iter.fold(IntExt::I32(0), |acc, value| acc + value) } } #[cfg(test)] mod tests { - use I32Ext::*; + use IntExt::*; use super::*; diff --git a/pumpkin-crates/checking/src/variable.rs b/pumpkin-crates/checking/src/variable.rs index f8421bb5a..7f7a08f82 100644 --- a/pumpkin-crates/checking/src/variable.rs +++ b/pumpkin-crates/checking/src/variable.rs @@ -2,7 +2,7 @@ use std::fmt::Debug; use crate::AtomicConstraint; use crate::Comparison; -use crate::I32Ext; +use crate::IntExt; use crate::TestAtomic; use crate::VariableState; @@ -24,10 +24,10 @@ pub trait CheckerVariable: Debug + Clone { fn atomic_not_equal(&self, value: i32) -> Atomic; /// Get the lower bound of the domain. - fn induced_lower_bound(&self, variable_state: &VariableState) -> I32Ext; + fn induced_lower_bound(&self, variable_state: &VariableState) -> IntExt; /// Get the upper bound of the domain. - fn induced_upper_bound(&self, variable_state: &VariableState) -> I32Ext; + fn induced_upper_bound(&self, variable_state: &VariableState) -> IntExt; /// Get the value the variable is fixed to, if the variable is fixed. fn induced_fixed_value(&self, variable_state: &VariableState) -> Option; @@ -88,11 +88,11 @@ impl CheckerVariable for &'static str { } } - fn induced_lower_bound(&self, variable_state: &VariableState) -> I32Ext { + fn induced_lower_bound(&self, variable_state: &VariableState) -> IntExt { variable_state.lower_bound(self) } - fn induced_upper_bound(&self, variable_state: &VariableState) -> I32Ext { + fn induced_upper_bound(&self, variable_state: &VariableState) -> IntExt { variable_state.upper_bound(self) } diff --git a/pumpkin-crates/checking/src/variable_state.rs b/pumpkin-crates/checking/src/variable_state.rs index 8c44b381d..1b2c23cc6 100644 --- a/pumpkin-crates/checking/src/variable_state.rs +++ b/pumpkin-crates/checking/src/variable_state.rs @@ -4,13 +4,13 @@ use std::hash::Hash; use crate::AtomicConstraint; use crate::Comparison; -use crate::I32Ext; #[cfg(doc)] use crate::InferenceChecker; +use crate::IntExt; /// The domains of all variables in the problem. /// -/// Domains are initially unbounded. This is why bounds are represented as [`I32Ext`]. +/// Domains are initially unbounded. This is why bounds are represented as [`IntExt`]. /// /// Domains can be reduced through [`VariableState::apply`]. By default, the domain of every /// variable is infinite. @@ -60,19 +60,19 @@ where } /// Get the lower bound of a variable. - pub fn lower_bound(&self, identifier: &Ident) -> I32Ext { + pub fn lower_bound(&self, identifier: &Ident) -> IntExt { self.domains .get(identifier) .map(|domain| domain.lower_bound) - .unwrap_or(I32Ext::NegativeInf) + .unwrap_or(IntExt::NegativeInf) } /// Get the upper bound of a variable. - pub fn upper_bound(&self, identifier: &Ident) -> I32Ext { + pub fn upper_bound(&self, identifier: &Ident) -> IntExt { self.domains .get(identifier) .map(|domain| domain.upper_bound) - .unwrap_or(I32Ext::PositiveInf) + .unwrap_or(IntExt::PositiveInf) } /// Get the holes within the lower and upper bound of the variable expression. @@ -92,7 +92,7 @@ where let domain = self.domains.get(identifier)?; if domain.lower_bound == domain.upper_bound { - let I32Ext::I32(value) = domain.lower_bound else { + let IntExt::I32(value) = domain.lower_bound else { panic!( "lower can only equal upper if they are integers, otherwise the sign of infinity makes them different" ); @@ -113,13 +113,13 @@ where { let domain = self.domains.get(identifier)?; - let I32Ext::I32(lower_bound) = domain.lower_bound else { + let IntExt::I32(lower_bound) = domain.lower_bound else { // If there is no lower bound, then the domain is unbounded. return None; }; // Ensure there is also an upper bound. - if !matches!(domain.upper_bound, I32Ext::I32(_)) { + if !matches!(domain.upper_bound, IntExt::I32(_)) { return None; } @@ -206,16 +206,16 @@ where /// A domain inside the variable state. #[derive(Clone, Debug)] struct Domain { - lower_bound: I32Ext, - upper_bound: I32Ext, + lower_bound: IntExt, + upper_bound: IntExt, holes: BTreeSet, } impl Domain { fn new() -> Domain { Domain { - lower_bound: I32Ext::NegativeInf, - upper_bound: I32Ext::PositiveInf, + lower_bound: IntExt::NegativeInf, + upper_bound: IntExt::PositiveInf, holes: BTreeSet::default(), } } @@ -227,7 +227,7 @@ impl Domain { return; } - self.lower_bound = I32Ext::I32(bound); + self.lower_bound = IntExt::I32(bound); self.holes = self.holes.split_off(&bound); // Take care of the condition where the new bound is already a hole in the domain. @@ -243,7 +243,7 @@ impl Domain { return; } - self.upper_bound = I32Ext::I32(bound); + self.upper_bound = IntExt::I32(bound); // Note the '+ 1' to keep the elements <= the upper bound instead of < // the upper bound. @@ -277,7 +277,7 @@ impl Iterator for DomainIterator<'_> { fn next(&mut self) -> Option { let DomainIterator { domain, next_value } = self; - let I32Ext::I32(upper_bound) = domain.upper_bound else { + let IntExt::I32(upper_bound) = domain.upper_bound else { panic!("Only finite domains can be iterated.") }; diff --git a/pumpkin-crates/core/src/engine/variables/affine_view.rs b/pumpkin-crates/core/src/engine/variables/affine_view.rs index 610d12dc8..9ce6fd437 100644 --- a/pumpkin-crates/core/src/engine/variables/affine_view.rs +++ b/pumpkin-crates/core/src/engine/variables/affine_view.rs @@ -2,7 +2,7 @@ use std::cmp::Ordering; use enumset::EnumSet; use pumpkin_checking::CheckerVariable; -use pumpkin_checking::I32Ext; +use pumpkin_checking::IntExt; use super::TransformableVariable; use crate::engine::Assignments; @@ -82,17 +82,17 @@ impl CheckerVariable for AffineView { fn induced_lower_bound( &self, variable_state: &pumpkin_checking::VariableState, - ) -> I32Ext { + ) -> IntExt { if self.scale.is_positive() { match self.inner.induced_lower_bound(variable_state) { - I32Ext::I32(value) => I32Ext::I32(self.map(value)), + IntExt::I32(value) => IntExt::I32(self.map(value)), bound => bound, } } else { match self.inner.induced_upper_bound(variable_state) { - I32Ext::I32(value) => I32Ext::I32(self.map(value)), - I32Ext::NegativeInf => I32Ext::PositiveInf, - I32Ext::PositiveInf => I32Ext::NegativeInf, + IntExt::I32(value) => IntExt::I32(self.map(value)), + IntExt::NegativeInf => IntExt::PositiveInf, + IntExt::PositiveInf => IntExt::NegativeInf, } } } @@ -100,17 +100,17 @@ impl CheckerVariable for AffineView { fn induced_upper_bound( &self, variable_state: &pumpkin_checking::VariableState, - ) -> I32Ext { + ) -> IntExt { if self.scale.is_positive() { match self.inner.induced_upper_bound(variable_state) { - I32Ext::I32(value) => I32Ext::I32(self.map(value)), + IntExt::I32(value) => IntExt::I32(self.map(value)), bound => bound, } } else { match self.inner.induced_lower_bound(variable_state) { - I32Ext::I32(value) => I32Ext::I32(self.map(value)), - I32Ext::NegativeInf => I32Ext::PositiveInf, - I32Ext::PositiveInf => I32Ext::NegativeInf, + IntExt::I32(value) => IntExt::I32(self.map(value)), + IntExt::NegativeInf => IntExt::PositiveInf, + IntExt::PositiveInf => IntExt::NegativeInf, } } } diff --git a/pumpkin-crates/core/src/engine/variables/domain_id.rs b/pumpkin-crates/core/src/engine/variables/domain_id.rs index 5b51f93f8..858d726c2 100644 --- a/pumpkin-crates/core/src/engine/variables/domain_id.rs +++ b/pumpkin-crates/core/src/engine/variables/domain_id.rs @@ -62,14 +62,14 @@ impl CheckerVariable for DomainId { fn induced_lower_bound( &self, variable_state: &pumpkin_checking::VariableState, - ) -> pumpkin_checking::I32Ext { + ) -> pumpkin_checking::IntExt { variable_state.lower_bound(self) } fn induced_upper_bound( &self, variable_state: &pumpkin_checking::VariableState, - ) -> pumpkin_checking::I32Ext { + ) -> pumpkin_checking::IntExt { variable_state.upper_bound(self) } diff --git a/pumpkin-crates/core/src/engine/variables/literal.rs b/pumpkin-crates/core/src/engine/variables/literal.rs index 76da66254..ca8d247df 100644 --- a/pumpkin-crates/core/src/engine/variables/literal.rs +++ b/pumpkin-crates/core/src/engine/variables/literal.rs @@ -2,7 +2,7 @@ use std::ops::Not; use enumset::EnumSet; use pumpkin_checking::CheckerVariable; -use pumpkin_checking::I32Ext; +use pumpkin_checking::IntExt; use pumpkin_checking::VariableState; use super::DomainId; @@ -80,8 +80,8 @@ impl CheckerVariable for Literal { forward!(integer_variable, fn atomic_not_equal(&self, value: i32) -> Predicate); forward!(integer_variable, fn atomic_equal(&self, value: i32) -> Predicate); - forward!(integer_variable, fn induced_lower_bound(&self, variable_state: &VariableState) -> I32Ext); - forward!(integer_variable, fn induced_upper_bound(&self, variable_state: &VariableState) -> I32Ext); + forward!(integer_variable, fn induced_lower_bound(&self, variable_state: &VariableState) -> IntExt); + forward!(integer_variable, fn induced_upper_bound(&self, variable_state: &VariableState) -> IntExt); forward!(integer_variable, fn induced_fixed_value(&self, variable_state: &VariableState) -> Option); forward!( integer_variable, diff --git a/pumpkin-crates/propagators/src/propagators/arithmetic/absolute_value.rs b/pumpkin-crates/propagators/src/propagators/arithmetic/absolute_value.rs index 74a39e747..3edad6a6e 100644 --- a/pumpkin-crates/propagators/src/propagators/arithmetic/absolute_value.rs +++ b/pumpkin-crates/propagators/src/propagators/arithmetic/absolute_value.rs @@ -1,7 +1,7 @@ use pumpkin_checking::AtomicConstraint; use pumpkin_checking::CheckerVariable; -use pumpkin_checking::I32Ext; use pumpkin_checking::InferenceChecker; +use pumpkin_checking::IntExt; use pumpkin_core::conjunction; use pumpkin_core::declare_inference_label; use pumpkin_core::predicate; @@ -195,7 +195,7 @@ where } else if signed_upper <= 0 { (-signed_upper, -signed_lower) } else if signed_lower < 0 && 0_i32 < signed_upper { - (I32Ext::I32(0), std::cmp::max(-signed_lower, signed_upper)) + (IntExt::I32(0), std::cmp::max(-signed_lower, signed_upper)) } else { unreachable!() }; diff --git a/pumpkin-crates/propagators/src/propagators/arithmetic/binary/binary_equals.rs b/pumpkin-crates/propagators/src/propagators/arithmetic/binary/binary_equals.rs index bcbdaea63..bdec0d118 100644 --- a/pumpkin-crates/propagators/src/propagators/arithmetic/binary/binary_equals.rs +++ b/pumpkin-crates/propagators/src/propagators/arithmetic/binary/binary_equals.rs @@ -4,8 +4,8 @@ use std::slice; use bitfield_struct::bitfield; use pumpkin_checking::AtomicConstraint; use pumpkin_checking::CheckerVariable; -use pumpkin_checking::I32Ext; use pumpkin_checking::InferenceChecker; +use pumpkin_checking::IntExt; use pumpkin_core::asserts::pumpkin_assert_advanced; use pumpkin_core::conjunction; use pumpkin_core::containers::HashSet; @@ -420,12 +420,12 @@ where // the step is unsound! let mut consistent = true; - if let I32Ext::I32(value) = self.rhs.induced_upper_bound(&state) { + if let IntExt::I32(value) = self.rhs.induced_upper_bound(&state) { let atomic = self.lhs.atomic_less_than(value); consistent &= state.apply(&atomic); } - if let I32Ext::I32(value) = self.rhs.induced_lower_bound(&state) { + if let IntExt::I32(value) = self.rhs.induced_lower_bound(&state) { let atomic = self.lhs.atomic_greater_than(value); consistent &= state.apply(&atomic); } diff --git a/pumpkin-crates/propagators/src/propagators/arithmetic/linear_less_or_equal.rs b/pumpkin-crates/propagators/src/propagators/arithmetic/linear_less_or_equal.rs index 560086628..801a744fb 100644 --- a/pumpkin-crates/propagators/src/propagators/arithmetic/linear_less_or_equal.rs +++ b/pumpkin-crates/propagators/src/propagators/arithmetic/linear_less_or_equal.rs @@ -1,7 +1,7 @@ use pumpkin_checking::AtomicConstraint; use pumpkin_checking::CheckerVariable; -use pumpkin_checking::I32Ext; use pumpkin_checking::InferenceChecker; +use pumpkin_checking::IntExt; use pumpkin_checking::VariableState; use pumpkin_core::asserts::pumpkin_assert_simple; use pumpkin_core::declare_inference_label; @@ -290,14 +290,14 @@ where #[derive(Debug, Clone)] pub struct LinearLessOrEqualInferenceChecker { terms: Box<[Var]>, - bound: I32Ext, + bound: IntExt, } impl LinearLessOrEqualInferenceChecker { pub fn new(terms: Box<[Var]>, bound: i32) -> Self { LinearLessOrEqualInferenceChecker { terms, - bound: I32Ext::I32(bound), + bound: IntExt::I32(bound), } } } @@ -315,9 +315,9 @@ where ) -> bool { // Next, we evaluate the linear inequality. The lower bound of the // left-hand side must exceed the bound in the constraint. Note that the accumulator is an - // I32Ext, and if the lower bound of one of the terms is -infty, then the left-hand side + // IntExt, and if the lower bound of one of the terms is -infty, then the left-hand side // will be -infty regardless of the other terms. - let left_hand_side: I32Ext = self + let left_hand_side: IntExt = self .terms .iter() .map(|variable| variable.induced_lower_bound(&variable_state)) diff --git a/pumpkin-crates/propagators/src/propagators/arithmetic/maximum.rs b/pumpkin-crates/propagators/src/propagators/arithmetic/maximum.rs index 5236a1848..d1f927697 100644 --- a/pumpkin-crates/propagators/src/propagators/arithmetic/maximum.rs +++ b/pumpkin-crates/propagators/src/propagators/arithmetic/maximum.rs @@ -1,7 +1,7 @@ use pumpkin_checking::AtomicConstraint; use pumpkin_checking::CheckerVariable; -use pumpkin_checking::I32Ext; use pumpkin_checking::InferenceChecker; +use pumpkin_checking::IntExt; use pumpkin_core::conjunction; use pumpkin_core::declare_inference_label; use pumpkin_core::predicate; @@ -208,13 +208,13 @@ where .iter() .map(|element| element.induced_lower_bound(&state)) .max() - .unwrap_or(I32Ext::NegativeInf); + .unwrap_or(IntExt::NegativeInf); let highest_maximum = self .array .iter() .map(|element| element.induced_upper_bound(&state)) .max() - .unwrap_or(I32Ext::PositiveInf); + .unwrap_or(IntExt::PositiveInf); // If the intersection between the domain of `rhs` and `[lowest_maximum, // highest_maximum]` is empty, there is a conflict. diff --git a/pumpkin-crates/propagators/src/propagators/disjunctive/checker.rs b/pumpkin-crates/propagators/src/propagators/disjunctive/checker.rs index 2df567ade..f9d18f51f 100644 --- a/pumpkin-crates/propagators/src/propagators/disjunctive/checker.rs +++ b/pumpkin-crates/propagators/src/propagators/disjunctive/checker.rs @@ -4,8 +4,8 @@ use std::marker::PhantomData; use pumpkin_checking::AtomicConstraint; use pumpkin_checking::CheckerVariable; -use pumpkin_checking::I32Ext; use pumpkin_checking::InferenceChecker; +use pumpkin_checking::IntExt; use pumpkin_checking::VariableState; use pumpkin_core::containers::KeyedVec; use pumpkin_core::containers::StorageKey; @@ -51,8 +51,8 @@ where // - For tasks in omega, both bounds should be present to define the interval // - For the propagating task, the lower-bound should be present, and the negation of // the consequent ensures that an upper-bound is present - if task.start_time.induced_lower_bound(&state) != I32Ext::NegativeInf - && task.start_time.induced_upper_bound(&state) != I32Ext::PositiveInf + if task.start_time.induced_lower_bound(&state) != IntExt::NegativeInf + && task.start_time.induced_upper_bound(&state) != IntExt::PositiveInf { // Now we calculate the durations of tasks let est_task: i32 = task @@ -61,7 +61,7 @@ where .try_into() .unwrap(); let lst_task = - >::try_into(task.start_time.induced_upper_bound(&state)) + >::try_into(task.start_time.induced_upper_bound(&state)) .unwrap(); let is_propagating_task = if let Some(consequent) = consequent { From 761be5998741a3b95beb40f77a04c78ff2e22838 Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Tue, 20 Jan 2026 13:43:16 +0100 Subject: [PATCH 41/48] Rename IntExt::I32 to IntExt::Int --- pumpkin-crates/checking/src/i32_ext.rs | 72 +++++++++---------- pumpkin-crates/checking/src/variable_state.rs | 12 ++-- .../core/src/engine/variables/affine_view.rs | 8 +-- .../propagators/arithmetic/absolute_value.rs | 2 +- .../arithmetic/binary/binary_equals.rs | 4 +- .../arithmetic/linear_less_or_equal.rs | 2 +- 6 files changed, 50 insertions(+), 50 deletions(-) diff --git a/pumpkin-crates/checking/src/i32_ext.rs b/pumpkin-crates/checking/src/i32_ext.rs index 421f5ccbb..4de4adc22 100644 --- a/pumpkin-crates/checking/src/i32_ext.rs +++ b/pumpkin-crates/checking/src/i32_ext.rs @@ -13,14 +13,14 @@ use std::ops::Neg; /// yield `IntExt::I32(0)`. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum IntExt { - I32(i32), + Int(i32), NegativeInf, PositiveInf, } impl From for IntExt { fn from(value: i32) -> Self { - IntExt::I32(value) + IntExt::Int(value) } } @@ -29,7 +29,7 @@ impl TryInto for IntExt { fn try_into(self) -> Result { match self { - IntExt::I32(inner) => Ok(inner), + IntExt::Int(inner) => Ok(inner), IntExt::NegativeInf | IntExt::PositiveInf => Err(()), } } @@ -38,7 +38,7 @@ impl TryInto for IntExt { impl PartialEq for IntExt { fn eq(&self, other: &i32) -> bool { match self { - IntExt::I32(v1) => v1 == other, + IntExt::Int(v1) => v1 == other, IntExt::NegativeInf | IntExt::PositiveInf => false, } } @@ -65,18 +65,18 @@ impl PartialOrd for IntExt { impl Ord for IntExt { fn cmp(&self, other: &Self) -> Ordering { match self { - IntExt::I32(v1) => match other { - IntExt::I32(v2) => v1.cmp(v2), + IntExt::Int(v1) => match other { + IntExt::Int(v2) => v1.cmp(v2), IntExt::NegativeInf => Ordering::Greater, IntExt::PositiveInf => Ordering::Less, }, IntExt::NegativeInf => match other { - IntExt::I32(_) => Ordering::Less, + IntExt::Int(_) => Ordering::Less, IntExt::PositiveInf => Ordering::Less, IntExt::NegativeInf => Ordering::Equal, }, IntExt::PositiveInf => match other { - IntExt::I32(_) => Ordering::Greater, + IntExt::Int(_) => Ordering::Greater, IntExt::NegativeInf => Ordering::Greater, IntExt::PositiveInf => Ordering::Greater, }, @@ -87,7 +87,7 @@ impl Ord for IntExt { impl PartialOrd for IntExt { fn partial_cmp(&self, other: &i32) -> Option { match self { - IntExt::I32(v1) => v1.partial_cmp(other), + IntExt::Int(v1) => v1.partial_cmp(other), IntExt::NegativeInf => Some(Ordering::Less), IntExt::PositiveInf => Some(Ordering::Greater), } @@ -98,7 +98,7 @@ impl Add for IntExt { type Output = IntExt; fn add(self, rhs: i32) -> Self::Output { - self + IntExt::I32(rhs) + self + IntExt::Int(rhs) } } @@ -107,12 +107,12 @@ impl Add for IntExt { fn add(self, rhs: IntExt) -> Self::Output { match (self, rhs) { - (IntExt::I32(lhs), IntExt::I32(rhs)) => IntExt::I32(lhs + rhs), + (IntExt::Int(lhs), IntExt::Int(rhs)) => IntExt::Int(lhs + rhs), - (IntExt::I32(_), Self::NegativeInf) => Self::NegativeInf, - (IntExt::I32(_), Self::PositiveInf) => Self::PositiveInf, - (Self::NegativeInf, IntExt::I32(_)) => Self::NegativeInf, - (Self::PositiveInf, IntExt::I32(_)) => Self::PositiveInf, + (IntExt::Int(_), Self::NegativeInf) => Self::NegativeInf, + (IntExt::Int(_), Self::PositiveInf) => Self::PositiveInf, + (Self::NegativeInf, IntExt::Int(_)) => Self::NegativeInf, + (Self::PositiveInf, IntExt::Int(_)) => Self::PositiveInf, (IntExt::NegativeInf, IntExt::NegativeInf) => IntExt::NegativeInf, (IntExt::PositiveInf, IntExt::PositiveInf) => IntExt::PositiveInf, @@ -129,7 +129,7 @@ impl Mul for IntExt { type Output = IntExt; fn mul(self, rhs: i32) -> Self::Output { - self * IntExt::I32(rhs) + self * IntExt::Int(rhs) } } @@ -138,16 +138,16 @@ impl Mul for IntExt { fn mul(self, rhs: Self) -> Self::Output { match (self, rhs) { - (IntExt::I32(lhs), IntExt::I32(rhs)) => IntExt::I32(lhs * rhs), + (IntExt::Int(lhs), IntExt::Int(rhs)) => IntExt::Int(lhs * rhs), // Multiplication with 0 will always yield 0. - (IntExt::I32(0), Self::NegativeInf) - | (IntExt::I32(0), Self::PositiveInf) - | (Self::NegativeInf, IntExt::I32(0)) - | (Self::PositiveInf, IntExt::I32(0)) => IntExt::I32(0), + (IntExt::Int(0), Self::NegativeInf) + | (IntExt::Int(0), Self::PositiveInf) + | (Self::NegativeInf, IntExt::Int(0)) + | (Self::PositiveInf, IntExt::Int(0)) => IntExt::Int(0), - (IntExt::I32(value), IntExt::NegativeInf) - | (IntExt::NegativeInf, IntExt::I32(value)) => { + (IntExt::Int(value), IntExt::NegativeInf) + | (IntExt::NegativeInf, IntExt::Int(value)) => { if value >= 0 { IntExt::NegativeInf } else { @@ -155,8 +155,8 @@ impl Mul for IntExt { } } - (IntExt::I32(value), IntExt::PositiveInf) - | (IntExt::PositiveInf, IntExt::I32(value)) => { + (IntExt::Int(value), IntExt::PositiveInf) + | (IntExt::PositiveInf, IntExt::Int(value)) => { if value >= 0 { IntExt::PositiveInf } else { @@ -178,7 +178,7 @@ impl Neg for IntExt { fn neg(self) -> Self::Output { match self { - IntExt::I32(value) => IntExt::I32(-value), + IntExt::Int(value) => IntExt::Int(-value), IntExt::NegativeInf => IntExt::PositiveInf, IntExt::PositiveInf => Self::NegativeInf, } @@ -187,7 +187,7 @@ impl Neg for IntExt { impl Sum for IntExt { fn sum>(iter: I) -> Self { - iter.fold(IntExt::I32(0), |acc, value| acc + value) + iter.fold(IntExt::Int(0), |acc, value| acc + value) } } @@ -199,30 +199,30 @@ mod tests { #[test] fn ordering_of_i32_with_i32_ext() { - assert!(I32(2) < 3); - assert!(I32(-1) < 3); - assert!(I32(-10) < -1); + assert!(Int(2) < 3); + assert!(Int(-1) < 3); + assert!(Int(-10) < -1); } #[test] fn ordering_of_i32_ext_with_i32() { - assert!(1 < I32(2)); - assert!(-10 < I32(-1)); - assert!(-11 < I32(-10)); + assert!(1 < Int(2)); + assert!(-10 < Int(-1)); + assert!(-11 < Int(-10)); } #[test] fn test_adding_i32s() { - assert_eq!(I32(3) + I32(4), I32(7)); + assert_eq!(Int(3) + Int(4), Int(7)); } #[test] fn test_adding_negative_inf() { - assert_eq!(I32(3) + NegativeInf, NegativeInf); + assert_eq!(Int(3) + NegativeInf, NegativeInf); } #[test] fn test_adding_positive_inf() { - assert_eq!(I32(3) + PositiveInf, PositiveInf); + assert_eq!(Int(3) + PositiveInf, PositiveInf); } } diff --git a/pumpkin-crates/checking/src/variable_state.rs b/pumpkin-crates/checking/src/variable_state.rs index 1b2c23cc6..54041b72c 100644 --- a/pumpkin-crates/checking/src/variable_state.rs +++ b/pumpkin-crates/checking/src/variable_state.rs @@ -92,7 +92,7 @@ where let domain = self.domains.get(identifier)?; if domain.lower_bound == domain.upper_bound { - let IntExt::I32(value) = domain.lower_bound else { + let IntExt::Int(value) = domain.lower_bound else { panic!( "lower can only equal upper if they are integers, otherwise the sign of infinity makes them different" ); @@ -113,13 +113,13 @@ where { let domain = self.domains.get(identifier)?; - let IntExt::I32(lower_bound) = domain.lower_bound else { + let IntExt::Int(lower_bound) = domain.lower_bound else { // If there is no lower bound, then the domain is unbounded. return None; }; // Ensure there is also an upper bound. - if !matches!(domain.upper_bound, IntExt::I32(_)) { + if !matches!(domain.upper_bound, IntExt::Int(_)) { return None; } @@ -227,7 +227,7 @@ impl Domain { return; } - self.lower_bound = IntExt::I32(bound); + self.lower_bound = IntExt::Int(bound); self.holes = self.holes.split_off(&bound); // Take care of the condition where the new bound is already a hole in the domain. @@ -243,7 +243,7 @@ impl Domain { return; } - self.upper_bound = IntExt::I32(bound); + self.upper_bound = IntExt::Int(bound); // Note the '+ 1' to keep the elements <= the upper bound instead of < // the upper bound. @@ -277,7 +277,7 @@ impl Iterator for DomainIterator<'_> { fn next(&mut self) -> Option { let DomainIterator { domain, next_value } = self; - let IntExt::I32(upper_bound) = domain.upper_bound else { + let IntExt::Int(upper_bound) = domain.upper_bound else { panic!("Only finite domains can be iterated.") }; diff --git a/pumpkin-crates/core/src/engine/variables/affine_view.rs b/pumpkin-crates/core/src/engine/variables/affine_view.rs index 9ce6fd437..58435d35d 100644 --- a/pumpkin-crates/core/src/engine/variables/affine_view.rs +++ b/pumpkin-crates/core/src/engine/variables/affine_view.rs @@ -85,12 +85,12 @@ impl CheckerVariable for AffineView { ) -> IntExt { if self.scale.is_positive() { match self.inner.induced_lower_bound(variable_state) { - IntExt::I32(value) => IntExt::I32(self.map(value)), + IntExt::Int(value) => IntExt::Int(self.map(value)), bound => bound, } } else { match self.inner.induced_upper_bound(variable_state) { - IntExt::I32(value) => IntExt::I32(self.map(value)), + IntExt::Int(value) => IntExt::Int(self.map(value)), IntExt::NegativeInf => IntExt::PositiveInf, IntExt::PositiveInf => IntExt::NegativeInf, } @@ -103,12 +103,12 @@ impl CheckerVariable for AffineView { ) -> IntExt { if self.scale.is_positive() { match self.inner.induced_upper_bound(variable_state) { - IntExt::I32(value) => IntExt::I32(self.map(value)), + IntExt::Int(value) => IntExt::Int(self.map(value)), bound => bound, } } else { match self.inner.induced_lower_bound(variable_state) { - IntExt::I32(value) => IntExt::I32(self.map(value)), + IntExt::Int(value) => IntExt::Int(self.map(value)), IntExt::NegativeInf => IntExt::PositiveInf, IntExt::PositiveInf => IntExt::NegativeInf, } diff --git a/pumpkin-crates/propagators/src/propagators/arithmetic/absolute_value.rs b/pumpkin-crates/propagators/src/propagators/arithmetic/absolute_value.rs index 3edad6a6e..95b6e334a 100644 --- a/pumpkin-crates/propagators/src/propagators/arithmetic/absolute_value.rs +++ b/pumpkin-crates/propagators/src/propagators/arithmetic/absolute_value.rs @@ -195,7 +195,7 @@ where } else if signed_upper <= 0 { (-signed_upper, -signed_lower) } else if signed_lower < 0 && 0_i32 < signed_upper { - (IntExt::I32(0), std::cmp::max(-signed_lower, signed_upper)) + (IntExt::Int(0), std::cmp::max(-signed_lower, signed_upper)) } else { unreachable!() }; diff --git a/pumpkin-crates/propagators/src/propagators/arithmetic/binary/binary_equals.rs b/pumpkin-crates/propagators/src/propagators/arithmetic/binary/binary_equals.rs index bdec0d118..964388ae9 100644 --- a/pumpkin-crates/propagators/src/propagators/arithmetic/binary/binary_equals.rs +++ b/pumpkin-crates/propagators/src/propagators/arithmetic/binary/binary_equals.rs @@ -420,12 +420,12 @@ where // the step is unsound! let mut consistent = true; - if let IntExt::I32(value) = self.rhs.induced_upper_bound(&state) { + if let IntExt::Int(value) = self.rhs.induced_upper_bound(&state) { let atomic = self.lhs.atomic_less_than(value); consistent &= state.apply(&atomic); } - if let IntExt::I32(value) = self.rhs.induced_lower_bound(&state) { + if let IntExt::Int(value) = self.rhs.induced_lower_bound(&state) { let atomic = self.lhs.atomic_greater_than(value); consistent &= state.apply(&atomic); } diff --git a/pumpkin-crates/propagators/src/propagators/arithmetic/linear_less_or_equal.rs b/pumpkin-crates/propagators/src/propagators/arithmetic/linear_less_or_equal.rs index 801a744fb..e5e86a458 100644 --- a/pumpkin-crates/propagators/src/propagators/arithmetic/linear_less_or_equal.rs +++ b/pumpkin-crates/propagators/src/propagators/arithmetic/linear_less_or_equal.rs @@ -297,7 +297,7 @@ impl LinearLessOrEqualInferenceChecker { pub fn new(terms: Box<[Var]>, bound: i32) -> Self { LinearLessOrEqualInferenceChecker { terms, - bound: IntExt::I32(bound), + bound: IntExt::Int(bound), } } } From 2537d7322ab6747f72cf1d65aab26ad96dfa5572 Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Tue, 20 Jan 2026 14:06:11 +0100 Subject: [PATCH 42/48] Extend IntExt to work with i64 --- .../checking/src/{i32_ext.rs => int_ext.rs} | 48 +++++++++++++++---- pumpkin-crates/checking/src/lib.rs | 4 +- .../arithmetic/linear_less_or_equal.rs | 13 ++--- 3 files changed, 45 insertions(+), 20 deletions(-) rename pumpkin-crates/checking/src/{i32_ext.rs => int_ext.rs} (82%) diff --git a/pumpkin-crates/checking/src/i32_ext.rs b/pumpkin-crates/checking/src/int_ext.rs similarity index 82% rename from pumpkin-crates/checking/src/i32_ext.rs rename to pumpkin-crates/checking/src/int_ext.rs index 4de4adc22..3ddb7d14a 100644 --- a/pumpkin-crates/checking/src/i32_ext.rs +++ b/pumpkin-crates/checking/src/int_ext.rs @@ -1,4 +1,5 @@ use std::cmp::Ordering; +use std::fmt::Debug; use std::iter::Sum; use std::ops::Add; use std::ops::Mul; @@ -12,8 +13,8 @@ use std::ops::Neg; /// - Multiplying [`IntExt::PositiveInf`] or [`IntExt::NegativeInf`] with `IntExt::I32(0)` will /// yield `IntExt::I32(0)`. #[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum IntExt { - Int(i32), +pub enum IntExt { + Int(Int), NegativeInf, PositiveInf, } @@ -24,6 +25,17 @@ impl From for IntExt { } } +impl From> for IntExt { + fn from(value: IntExt) -> Self { + match value { + IntExt::Int(int) => IntExt::Int(int.into()), + IntExt::NegativeInf => IntExt::NegativeInf, + IntExt::PositiveInf => IntExt::PositiveInf, + } + } +} + +// TODO: This is not a great pattern, but for now I do not want to touch this. impl TryInto for IntExt { type Error = (); @@ -35,8 +47,8 @@ impl TryInto for IntExt { } } -impl PartialEq for IntExt { - fn eq(&self, other: &i32) -> bool { +impl PartialEq for IntExt { + fn eq(&self, other: &Int) -> bool { match self { IntExt::Int(v1) => v1 == other, IntExt::NegativeInf | IntExt::PositiveInf => false, @@ -56,13 +68,13 @@ impl PartialOrd for i32 { } } -impl PartialOrd for IntExt { - fn partial_cmp(&self, other: &IntExt) -> Option { +impl PartialOrd for IntExt { + fn partial_cmp(&self, other: &IntExt) -> Option { Some(self.cmp(other)) } } -impl Ord for IntExt { +impl Ord for IntExt { fn cmp(&self, other: &Self) -> Ordering { match self { IntExt::Int(v1) => match other { @@ -94,6 +106,16 @@ impl PartialOrd for IntExt { } } +impl PartialOrd for IntExt { + fn partial_cmp(&self, other: &i64) -> Option { + match self { + IntExt::Int(v1) => v1.partial_cmp(other), + IntExt::NegativeInf => Some(Ordering::Less), + IntExt::PositiveInf => Some(Ordering::Greater), + } + } +} + impl Add for IntExt { type Output = IntExt; @@ -102,10 +124,10 @@ impl Add for IntExt { } } -impl Add for IntExt { - type Output = IntExt; +impl + Debug> Add for IntExt { + type Output = IntExt; - fn add(self, rhs: IntExt) -> Self::Output { + fn add(self, rhs: IntExt) -> Self::Output { match (self, rhs) { (IntExt::Int(lhs), IntExt::Int(rhs)) => IntExt::Int(lhs + rhs), @@ -191,6 +213,12 @@ impl Sum for IntExt { } } +impl Sum for IntExt { + fn sum>(iter: I) -> Self { + iter.fold(IntExt::Int(0), |acc, value| acc + value) + } +} + #[cfg(test)] mod tests { use IntExt::*; diff --git a/pumpkin-crates/checking/src/lib.rs b/pumpkin-crates/checking/src/lib.rs index 982db1411..44b2963e5 100644 --- a/pumpkin-crates/checking/src/lib.rs +++ b/pumpkin-crates/checking/src/lib.rs @@ -4,7 +4,7 @@ //! inferences are sound w.r.t. an inference rule. mod atomic_constraint; -mod i32_ext; +mod int_ext; mod variable; mod variable_state; @@ -12,7 +12,7 @@ use std::fmt::Debug; pub use atomic_constraint::*; use dyn_clone::DynClone; -pub use i32_ext::*; +pub use int_ext::*; pub use variable::*; pub use variable_state::*; diff --git a/pumpkin-crates/propagators/src/propagators/arithmetic/linear_less_or_equal.rs b/pumpkin-crates/propagators/src/propagators/arithmetic/linear_less_or_equal.rs index e5e86a458..e1e616574 100644 --- a/pumpkin-crates/propagators/src/propagators/arithmetic/linear_less_or_equal.rs +++ b/pumpkin-crates/propagators/src/propagators/arithmetic/linear_less_or_equal.rs @@ -290,15 +290,12 @@ where #[derive(Debug, Clone)] pub struct LinearLessOrEqualInferenceChecker { terms: Box<[Var]>, - bound: IntExt, + bound: i32, } impl LinearLessOrEqualInferenceChecker { pub fn new(terms: Box<[Var]>, bound: i32) -> Self { - LinearLessOrEqualInferenceChecker { - terms, - bound: IntExt::Int(bound), - } + LinearLessOrEqualInferenceChecker { terms, bound } } } @@ -317,13 +314,13 @@ where // left-hand side must exceed the bound in the constraint. Note that the accumulator is an // IntExt, and if the lower bound of one of the terms is -infty, then the left-hand side // will be -infty regardless of the other terms. - let left_hand_side: IntExt = self + let left_hand_side: IntExt = self .terms .iter() - .map(|variable| variable.induced_lower_bound(&variable_state)) + .map(|variable| variable.induced_lower_bound(&variable_state).into()) .sum(); - left_hand_side > self.bound + left_hand_side > i64::from(self.bound) } } From 61cd43b2375545139186cd053cf44bf52018cdc8 Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Tue, 20 Jan 2026 14:18:20 +0100 Subject: [PATCH 43/48] Implement linear not equal checker --- pumpkin-crates/checking/src/int_ext.rs | 16 +++++++ .../arithmetic/linear_not_equal.rs | 44 +++++++++++++++++++ 2 files changed, 60 insertions(+) diff --git a/pumpkin-crates/checking/src/int_ext.rs b/pumpkin-crates/checking/src/int_ext.rs index 3ddb7d14a..751b6fc5a 100644 --- a/pumpkin-crates/checking/src/int_ext.rs +++ b/pumpkin-crates/checking/src/int_ext.rs @@ -2,6 +2,7 @@ use std::cmp::Ordering; use std::fmt::Debug; use std::iter::Sum; use std::ops::Add; +use std::ops::AddAssign; use std::ops::Mul; use std::ops::Neg; @@ -147,6 +148,21 @@ impl + Debug> Add for IntExt { } } +impl AddAssign for IntExt +where + Int: AddAssign, +{ + fn add_assign(&mut self, rhs: Int) { + match self { + IntExt::Int(value) => { + value.add_assign(rhs); + } + + IntExt::NegativeInf | IntExt::PositiveInf => {} + } + } +} + impl Mul for IntExt { type Output = IntExt; diff --git a/pumpkin-crates/propagators/src/propagators/arithmetic/linear_not_equal.rs b/pumpkin-crates/propagators/src/propagators/arithmetic/linear_not_equal.rs index 3b01e76d2..31f8a96e3 100644 --- a/pumpkin-crates/propagators/src/propagators/arithmetic/linear_not_equal.rs +++ b/pumpkin-crates/propagators/src/propagators/arithmetic/linear_not_equal.rs @@ -1,6 +1,11 @@ use std::rc::Rc; use enumset::enum_set; +use pumpkin_checking::AtomicConstraint; +use pumpkin_checking::CheckerVariable; +use pumpkin_checking::InferenceChecker; +use pumpkin_checking::IntExt; +use pumpkin_checking::VariableState; use pumpkin_core::asserts::pumpkin_assert_extreme; use pumpkin_core::asserts::pumpkin_assert_moderate; use pumpkin_core::asserts::pumpkin_assert_simple; @@ -13,6 +18,7 @@ use pumpkin_core::propagation::DomainEvent; use pumpkin_core::propagation::DomainEvents; use pumpkin_core::propagation::Domains; use pumpkin_core::propagation::EnqueueDecision; +use pumpkin_core::propagation::InferenceCheckers; use pumpkin_core::propagation::LocalId; use pumpkin_core::propagation::NotificationContext; use pumpkin_core::propagation::OpaqueDomainEvent; @@ -44,6 +50,16 @@ where { type PropagatorImpl = LinearNotEqualPropagator; + fn add_inference_checkers(&self, mut checkers: InferenceCheckers<'_>) { + checkers.add_inference_checker( + InferenceCode::new(self.constraint_tag, LinearNotEquals), + Box::new(LinearNotEqualChecker { + terms: self.terms.as_ref().into(), + bound: self.rhs, + }), + ); + } + fn create(self, mut context: PropagatorConstructorContext) -> Self::PropagatorImpl { let LinearNotEqualPropagatorArgs { terms, @@ -358,6 +374,34 @@ impl LinearNotEqualPropagator { } } +#[derive(Debug, Clone)] +pub struct LinearNotEqualChecker { + pub terms: Box<[Var]>, + pub bound: i32, +} + +impl InferenceChecker for LinearNotEqualChecker +where + Var: CheckerVariable, + Atomic: AtomicConstraint, +{ + fn check(&self, state: VariableState, _: &[Atomic], _: Option<&Atomic>) -> bool { + // We evaluate the linear sum. It should be fixed to the bound for a conflict to + // exist. + let mut left_hand_side = IntExt::Int(0); + + for term in self.terms.iter() { + let Some(value) = term.induced_fixed_value(&state) else { + return false; + }; + + left_hand_side += i64::from(value); + } + + left_hand_side == i64::from(self.bound) + } +} + #[allow(deprecated, reason = "Will be refactored")] #[cfg(test)] mod tests { From d479d58f1ae1bb6c1f290ebd62329564aff78c62 Mon Sep 17 00:00:00 2001 From: Imko Marijnissen Date: Tue, 20 Jan 2026 15:10:39 +0100 Subject: [PATCH 44/48] fix: adding checker for cumulative time tabling --- pumpkin-checker/src/inferences/time_table.rs | 2 +- .../cumulative/time_table/checker.rs | 501 +++++++++++++++++- .../time_table_over_interval_incremental.rs | 2 +- .../time_table_per_point_incremental.rs | 2 +- .../time_table/time_table_over_interval.rs | 2 +- .../time_table/time_table_per_point.rs | 2 +- 6 files changed, 492 insertions(+), 19 deletions(-) diff --git a/pumpkin-checker/src/inferences/time_table.rs b/pumpkin-checker/src/inferences/time_table.rs index 2a2a63648..0300bb895 100644 --- a/pumpkin-checker/src/inferences/time_table.rs +++ b/pumpkin-checker/src/inferences/time_table.rs @@ -28,7 +28,7 @@ pub(crate) fn verify_time_table( .map(|task| CheckerTask { start_time: task.start_time.clone(), resource_usage: task.resource_usage, - duration: task.duration, + processing_time: task.duration, }) .collect(), capacity: cumulative.capacity, diff --git a/pumpkin-crates/propagators/src/propagators/cumulative/time_table/checker.rs b/pumpkin-crates/propagators/src/propagators/cumulative/time_table/checker.rs index 13cc6e34e..83790b842 100644 --- a/pumpkin-crates/propagators/src/propagators/cumulative/time_table/checker.rs +++ b/pumpkin-crates/propagators/src/propagators/cumulative/time_table/checker.rs @@ -3,6 +3,10 @@ use std::collections::BTreeMap; use pumpkin_checking::AtomicConstraint; use pumpkin_checking::CheckerVariable; use pumpkin_checking::InferenceChecker; +use pumpkin_checking::IntExt; +use pumpkin_checking::VariableState; + +use crate::cumulative::time_table::time_table_util::has_overlap_with_interval; #[derive(Clone, Debug)] pub struct TimeTableChecker { @@ -14,7 +18,69 @@ pub struct TimeTableChecker { pub struct CheckerTask { pub start_time: Var, pub resource_usage: i32, - pub duration: i32, + pub processing_time: i32, +} + +fn lower_bound_can_be_propagated_by_profile< + Var: CheckerVariable, + Atomic: AtomicConstraint, +>( + context: &VariableState, + lower_bound: i32, + task: &CheckerTask, + start: i32, + end: i32, + height: i32, + capacity: i32, +) -> bool { + let upper_bound = task + .start_time + .induced_upper_bound(context) + .try_into() + .unwrap(); + + height + task.resource_usage > capacity + && !(upper_bound < (lower_bound + task.processing_time) + && has_overlap_with_interval( + upper_bound, + lower_bound + task.processing_time, + start, + end, + )) + && has_overlap_with_interval(lower_bound, upper_bound + task.processing_time, start, end) + && (lower_bound + task.processing_time) > start + && lower_bound <= end +} + +fn upper_bound_can_be_propagated_by_profile< + Var: CheckerVariable, + Atomic: AtomicConstraint, +>( + context: &VariableState, + upper_bound: i32, + task: &CheckerTask, + start: i32, + end: i32, + height: i32, + capacity: i32, +) -> bool { + let lower_bound = task + .start_time + .induced_lower_bound(context) + .try_into() + .unwrap(); + + height + task.resource_usage > capacity + && !(upper_bound < (lower_bound + task.processing_time) + && has_overlap_with_interval( + upper_bound, + lower_bound + task.processing_time, + start, + end, + )) + && has_overlap_with_interval(lower_bound, upper_bound + task.processing_time, start, end) + && (upper_bound + task.processing_time) > end + && upper_bound <= end } impl InferenceChecker for TimeTableChecker @@ -24,9 +90,9 @@ where { fn check( &self, - state: pumpkin_checking::VariableState, + state: VariableState, _: &[Atomic], - _: Option<&Atomic>, + consequent: Option<&Atomic>, ) -> bool { // The profile is a key-value store. The keys correspond to time-points, and the values to // the relative change in resource consumption. A BTreeMap is used to maintain a @@ -34,24 +100,99 @@ where let mut profile = BTreeMap::new(); for task in self.tasks.iter() { - let lst = task.start_time.induced_upper_bound(&state); - let ect = task.start_time.induced_lower_bound(&state) + task.duration; + if task.start_time.induced_lower_bound(&state) == IntExt::NegativeInf + || task.start_time.induced_upper_bound(&state) == IntExt::PositiveInf + { + continue; + } + + let lst: i32 = task + .start_time + .induced_upper_bound(&state) + .try_into() + .unwrap(); + let est: i32 = task + .start_time + .induced_lower_bound(&state) + .try_into() + .unwrap(); - if lst <= ect { + if lst < est + task.processing_time { *profile.entry(lst).or_insert(0) += task.resource_usage; - *profile.entry(ect).or_insert(0) -= task.resource_usage; + *profile.entry(est + task.processing_time).or_insert(0) -= task.resource_usage; } } - let mut usage = 0; - for delta in profile.values() { - usage += delta; + let mut profiles = Vec::new(); + let mut current_usage = 0; + let mut previous_time_point = *profile + .first_key_value() + .expect("Expected at least one mandatory part") + .0; + for (time_point, usage) in profile.iter() { + if current_usage > 0 && *time_point != previous_time_point { + profiles.push((previous_time_point, *time_point - 1, current_usage)) + } + + current_usage += *usage; - if usage > self.capacity { + if current_usage > self.capacity { return true; } + + previous_time_point = *time_point; } + if let Some(propagating_task) = consequent.map(|consequent| { + self.tasks + .iter() + .find(|task| task.start_time.does_atomic_constrain_self(consequent)) + .expect("If there is a consequent, then there should be a propagating task") + }) { + let mut lower_bound: i32 = propagating_task + .start_time + .induced_lower_bound(&state) + .try_into() + .unwrap(); + for (start, end_inclusive, height) in profiles.iter() { + if lower_bound_can_be_propagated_by_profile( + &state, + lower_bound, + propagating_task, + *start, + *end_inclusive, + *height, + self.capacity, + ) { + lower_bound = end_inclusive + 1; + } + } + if lower_bound > propagating_task.start_time.induced_upper_bound(&state) { + return true; + } + + let mut upper_bound: i32 = propagating_task + .start_time + .induced_upper_bound(&state) + .try_into() + .unwrap(); + for (start, end_inclusive, height) in profiles.iter().rev() { + if upper_bound_can_be_propagated_by_profile( + &state, + upper_bound, + propagating_task, + *start, + *end_inclusive, + *height, + self.capacity, + ) { + upper_bound = start - propagating_task.processing_time; + } + } + if upper_bound < propagating_task.start_time.induced_lower_bound(&state) { + return true; + } + } false } } @@ -64,7 +205,7 @@ mod tests { use super::*; #[test] - fn conflict_on_unary_resource() { + fn conflict() { let premises = [ TestAtomic { name: "x1", @@ -86,12 +227,12 @@ mod tests { CheckerTask { start_time: "x1", resource_usage: 1, - duration: 1, + processing_time: 1, }, CheckerTask { start_time: "x2", resource_usage: 1, - duration: 1, + processing_time: 1, }, ] .into(), @@ -100,4 +241,336 @@ mod tests { assert!(checker.check(state, &premises, None)); } + + #[test] + fn hole_in_domain() { + let premises = [TestAtomic { + name: "x1", + comparison: pumpkin_checking::Comparison::Equal, + value: 6, + }]; + + let consequent = Some(TestAtomic { + name: "x2", + comparison: pumpkin_checking::Comparison::NotEqual, + value: 2, + }); + let state = VariableState::prepare_for_conflict_check(premises, consequent) + .expect("no conflicting atomics"); + + let checker = TimeTableChecker { + tasks: vec![ + CheckerTask { + start_time: "x1", + resource_usage: 3, + processing_time: 2, + }, + CheckerTask { + start_time: "x2", + resource_usage: 2, + processing_time: 5, + }, + ] + .into(), + capacity: 4, + }; + + assert!(checker.check(state, &premises, consequent.as_ref())); + } + + #[test] + fn lower_bound_chain() { + let premises = [ + TestAtomic { + name: "x1", + comparison: pumpkin_checking::Comparison::Equal, + value: 1, + }, + TestAtomic { + name: "x2", + comparison: pumpkin_checking::Comparison::Equal, + value: 6, + }, + TestAtomic { + name: "x3", + comparison: pumpkin_checking::Comparison::GreaterEqual, + value: 0, + }, + ]; + + let consequent = Some(TestAtomic { + name: "x3", + comparison: pumpkin_checking::Comparison::GreaterEqual, + value: 16, + }); + let state = VariableState::prepare_for_conflict_check(premises, consequent) + .expect("no conflicting atomics"); + + let checker = TimeTableChecker { + tasks: vec![ + CheckerTask { + start_time: "x1", + resource_usage: 3, + processing_time: 2, + }, + CheckerTask { + start_time: "x2", + resource_usage: 3, + processing_time: 10, + }, + CheckerTask { + start_time: "x3", + resource_usage: 2, + processing_time: 5, + }, + ] + .into(), + capacity: 4, + }; + + assert!(checker.check(state, &premises, consequent.as_ref())); + } + + #[test] + fn upper_bound_chain() { + let premises = [ + TestAtomic { + name: "x1", + comparison: pumpkin_checking::Comparison::Equal, + value: 1, + }, + TestAtomic { + name: "x2", + comparison: pumpkin_checking::Comparison::Equal, + value: 6, + }, + TestAtomic { + name: "x3", + comparison: pumpkin_checking::Comparison::LessEqual, + value: 15, + }, + ]; + + let consequent = Some(TestAtomic { + name: "x3", + comparison: pumpkin_checking::Comparison::LessEqual, + value: -4, + }); + let state = VariableState::prepare_for_conflict_check(premises, consequent) + .expect("no conflicting atomics"); + + let checker = TimeTableChecker { + tasks: vec![ + CheckerTask { + start_time: "x1", + resource_usage: 3, + processing_time: 2, + }, + CheckerTask { + start_time: "x2", + resource_usage: 3, + processing_time: 10, + }, + CheckerTask { + start_time: "x3", + resource_usage: 2, + processing_time: 5, + }, + ] + .into(), + capacity: 4, + }; + + assert!(checker.check(state, &premises, consequent.as_ref())); + } + + #[test] + fn hole_in_domain_not_accepted() { + let premises = [TestAtomic { + name: "x1", + comparison: pumpkin_checking::Comparison::Equal, + value: 6, + }]; + + let consequent = Some(TestAtomic { + name: "x2", + comparison: pumpkin_checking::Comparison::NotEqual, + value: 1, + }); + let state = VariableState::prepare_for_conflict_check(premises, consequent) + .expect("no conflicting atomics"); + + let checker = TimeTableChecker { + tasks: vec![ + CheckerTask { + start_time: "x1", + resource_usage: 3, + processing_time: 2, + }, + CheckerTask { + start_time: "x2", + resource_usage: 2, + processing_time: 5, + }, + ] + .into(), + capacity: 4, + }; + + assert!(!checker.check(state, &premises, consequent.as_ref())); + } + + #[test] + fn lower_bound_chain_not_accepted() { + let premises = [ + TestAtomic { + name: "x1", + comparison: pumpkin_checking::Comparison::Equal, + value: 1, + }, + TestAtomic { + name: "x2", + comparison: pumpkin_checking::Comparison::Equal, + value: 8, + }, + TestAtomic { + name: "x3", + comparison: pumpkin_checking::Comparison::GreaterEqual, + value: 0, + }, + ]; + + let consequent = Some(TestAtomic { + name: "x3", + comparison: pumpkin_checking::Comparison::GreaterEqual, + value: 16, + }); + let state = VariableState::prepare_for_conflict_check(premises, consequent) + .expect("no conflicting atomics"); + + let checker = TimeTableChecker { + tasks: vec![ + CheckerTask { + start_time: "x1", + resource_usage: 3, + processing_time: 2, + }, + CheckerTask { + start_time: "x2", + resource_usage: 3, + processing_time: 10, + }, + CheckerTask { + start_time: "x3", + resource_usage: 2, + processing_time: 5, + }, + ] + .into(), + capacity: 4, + }; + + assert!(!checker.check(state, &premises, consequent.as_ref())); + } + + #[test] + fn upper_bound_chain_not_accepted() { + let premises = [ + TestAtomic { + name: "x1", + comparison: pumpkin_checking::Comparison::Equal, + value: 1, + }, + TestAtomic { + name: "x2", + comparison: pumpkin_checking::Comparison::Equal, + value: 8, + }, + TestAtomic { + name: "x3", + comparison: pumpkin_checking::Comparison::LessEqual, + value: 15, + }, + ]; + + let consequent = Some(TestAtomic { + name: "x3", + comparison: pumpkin_checking::Comparison::LessEqual, + value: -4, + }); + let state = VariableState::prepare_for_conflict_check(premises, consequent) + .expect("no conflicting atomics"); + + let checker = TimeTableChecker { + tasks: vec![ + CheckerTask { + start_time: "x1", + resource_usage: 3, + processing_time: 2, + }, + CheckerTask { + start_time: "x2", + resource_usage: 3, + processing_time: 10, + }, + CheckerTask { + start_time: "x3", + resource_usage: 2, + processing_time: 5, + }, + ] + .into(), + capacity: 4, + }; + + assert!(!checker.check(state, &premises, consequent.as_ref())); + } + + #[test] + fn simple_test() { + let premises = [ + TestAtomic { + name: "x3", + comparison: pumpkin_checking::Comparison::GreaterEqual, + value: 5, + }, + TestAtomic { + name: "x3", + comparison: pumpkin_checking::Comparison::LessEqual, + value: 6, + }, + TestAtomic { + name: "x2", + comparison: pumpkin_checking::Comparison::LessEqual, + value: 7, + }, + ]; + + let consequent = Some(TestAtomic { + name: "x2", + comparison: pumpkin_checking::Comparison::LessEqual, + value: 4, + }); + let state = VariableState::prepare_for_conflict_check(premises, consequent) + .expect("no conflicting atomics"); + + let checker = TimeTableChecker { + tasks: vec![ + CheckerTask { + start_time: "x3", + resource_usage: 1, + processing_time: 3, + }, + CheckerTask { + start_time: "x2", + resource_usage: 2, + processing_time: 2, + }, + ] + .into(), + capacity: 2, + }; + + assert!(checker.check(state, &premises, consequent.as_ref())); + } } diff --git a/pumpkin-crates/propagators/src/propagators/cumulative/time_table/over_interval_incremental_propagator/time_table_over_interval_incremental.rs b/pumpkin-crates/propagators/src/propagators/cumulative/time_table/over_interval_incremental_propagator/time_table_over_interval_incremental.rs index 3e343f9c5..3661767a7 100644 --- a/pumpkin-crates/propagators/src/propagators/cumulative/time_table/over_interval_incremental_propagator/time_table_over_interval_incremental.rs +++ b/pumpkin-crates/propagators/src/propagators/cumulative/time_table/over_interval_incremental_propagator/time_table_over_interval_incremental.rs @@ -121,7 +121,7 @@ impl PropagatorConstruc .iter() .map(|task| CheckerTask { start_time: task.start_variable.clone(), - duration: task.processing_time, + processing_time: task.processing_time, resource_usage: task.resource_usage, }) .collect(), diff --git a/pumpkin-crates/propagators/src/propagators/cumulative/time_table/per_point_incremental_propagator/time_table_per_point_incremental.rs b/pumpkin-crates/propagators/src/propagators/cumulative/time_table/per_point_incremental_propagator/time_table_per_point_incremental.rs index 73328da68..cd3db0d92 100644 --- a/pumpkin-crates/propagators/src/propagators/cumulative/time_table/per_point_incremental_propagator/time_table_per_point_incremental.rs +++ b/pumpkin-crates/propagators/src/propagators/cumulative/time_table/per_point_incremental_propagator/time_table_per_point_incremental.rs @@ -118,7 +118,7 @@ impl Propagator .iter() .map(|task| CheckerTask { start_time: task.start_variable.clone(), - duration: task.processing_time, + processing_time: task.processing_time, resource_usage: task.resource_usage, }) .collect(), diff --git a/pumpkin-crates/propagators/src/propagators/cumulative/time_table/time_table_over_interval.rs b/pumpkin-crates/propagators/src/propagators/cumulative/time_table/time_table_over_interval.rs index e166a6eea..663b27216 100644 --- a/pumpkin-crates/propagators/src/propagators/cumulative/time_table/time_table_over_interval.rs +++ b/pumpkin-crates/propagators/src/propagators/cumulative/time_table/time_table_over_interval.rs @@ -120,7 +120,7 @@ impl PropagatorConstructor .iter() .map(|task| CheckerTask { start_time: task.start_variable.clone(), - duration: task.processing_time, + processing_time: task.processing_time, resource_usage: task.resource_usage, }) .collect(), diff --git a/pumpkin-crates/propagators/src/propagators/cumulative/time_table/time_table_per_point.rs b/pumpkin-crates/propagators/src/propagators/cumulative/time_table/time_table_per_point.rs index f72d76d69..24b896191 100644 --- a/pumpkin-crates/propagators/src/propagators/cumulative/time_table/time_table_per_point.rs +++ b/pumpkin-crates/propagators/src/propagators/cumulative/time_table/time_table_per_point.rs @@ -111,7 +111,7 @@ impl PropagatorConstructor for TimeTablePerPoint .iter() .map(|task| CheckerTask { start_time: task.start_variable.clone(), - duration: task.processing_time, + processing_time: task.processing_time, resource_usage: task.resource_usage, }) .collect(), From edd7885dc3d567c04b1a9317841221e698ab1b8b Mon Sep 17 00:00:00 2001 From: Imko Marijnissen Date: Wed, 21 Jan 2026 13:53:57 +0100 Subject: [PATCH 45/48] fix: do not precrocess when checking propagations + bug in cumulative --- .../core/src/propagators/nogoods/nogood_propagator.rs | 7 +++++++ .../propagators/cumulative/time_table/time_table_util.rs | 1 + .../cumulative/utils/structs/resource_profile.rs | 1 + 3 files changed, 9 insertions(+) diff --git a/pumpkin-crates/core/src/propagators/nogoods/nogood_propagator.rs b/pumpkin-crates/core/src/propagators/nogoods/nogood_propagator.rs index 9bec2334e..6fe77f0a1 100644 --- a/pumpkin-crates/core/src/propagators/nogoods/nogood_propagator.rs +++ b/pumpkin-crates/core/src/propagators/nogoods/nogood_propagator.rs @@ -621,6 +621,13 @@ impl NogoodPropagator { // // The preprocessing ensures that all predicates are unassigned. else { + #[cfg(feature = "check-propagations")] + let nogood = input_nogood + .iter() + .map(|predicate| context.get_id(*predicate)) + .collect::>(); + + #[cfg(not(feature = "check-propagations"))] let nogood = nogood .iter() .map(|predicate| context.get_id(*predicate)) diff --git a/pumpkin-crates/propagators/src/propagators/cumulative/time_table/time_table_util.rs b/pumpkin-crates/propagators/src/propagators/cumulative/time_table/time_table_util.rs index 7da67427b..dbf6abbd4 100644 --- a/pumpkin-crates/propagators/src/propagators/cumulative/time_table/time_table_util.rs +++ b/pumpkin-crates/propagators/src/propagators/cumulative/time_table/time_table_util.rs @@ -421,6 +421,7 @@ fn propagate_sequence_of_profiles<'a, Var: IntegerVariable + 'static>( profile.start < context.upper_bound(&task.start_variable) + task.processing_time }); for profile in &time_table[lower_bound_index..upper_bound_index] { + propagation_handler.next_profile(); // Check whether this profile can cause an update if can_be_updated_by_profile(context.domains(), task, profile, parameters.capacity) { diff --git a/pumpkin-crates/propagators/src/propagators/cumulative/utils/structs/resource_profile.rs b/pumpkin-crates/propagators/src/propagators/cumulative/utils/structs/resource_profile.rs index 9997c8377..975316590 100644 --- a/pumpkin-crates/propagators/src/propagators/cumulative/utils/structs/resource_profile.rs +++ b/pumpkin-crates/propagators/src/propagators/cumulative/utils/structs/resource_profile.rs @@ -27,6 +27,7 @@ impl Debug for ResourceProfile { .field("start", &self.start) .field("end", &self.end) .field("height", &self.height) + .field("profile_tasks", &self.profile_tasks) .finish() } } From 20751535dc1b9a9f390fd84cf0a7f5b25bd56531 Mon Sep 17 00:00:00 2001 From: Imko Marijnissen Date: Wed, 21 Jan 2026 15:19:18 +0100 Subject: [PATCH 46/48] feat: checker for division --- pumpkin-crates/checking/src/int_ext.rs | 64 +++++++++++++++ .../arithmetic/integer_division.rs | 81 +++++++++++++++++++ 2 files changed, 145 insertions(+) diff --git a/pumpkin-crates/checking/src/int_ext.rs b/pumpkin-crates/checking/src/int_ext.rs index 751b6fc5a..896843d03 100644 --- a/pumpkin-crates/checking/src/int_ext.rs +++ b/pumpkin-crates/checking/src/int_ext.rs @@ -20,6 +20,70 @@ pub enum IntExt { PositiveInf, } +impl IntExt { + pub fn floor_div(&self, other: &IntExt) -> Option> { + match (self, other) { + (IntExt::Int(inner), IntExt::Int(inner_other)) => { + let inner = *inner as f64; + let inner_other = *inner_other as f64; + + Some(IntExt::Int((inner / inner_other).floor() as i32)) + } + (IntExt::NegativeInf, IntExt::Int(inner)) => { + if inner.is_positive() { + Some(IntExt::NegativeInf) + } else { + Some(IntExt::PositiveInf) + } + } + (IntExt::PositiveInf, IntExt::Int(inner)) => { + if inner.is_positive() { + Some(IntExt::PositiveInf) + } else { + Some(IntExt::NegativeInf) + } + } + (IntExt::PositiveInf, IntExt::NegativeInf) => None, + (IntExt::PositiveInf, IntExt::PositiveInf) => None, + (IntExt::NegativeInf, IntExt::NegativeInf) => None, + (IntExt::NegativeInf, IntExt::PositiveInf) => None, + (IntExt::Int(_), IntExt::NegativeInf) => Some(IntExt::Int(0)), + (IntExt::Int(_), IntExt::PositiveInf) => Some(IntExt::Int(0)), + } + } + + pub fn ceil_div(&self, other: &IntExt) -> Option> { + match (self, other) { + (IntExt::Int(inner), IntExt::Int(inner_other)) => { + let inner = *inner as f64; + let inner_other = *inner_other as f64; + + Some(IntExt::Int((inner / inner_other).ceil() as i32)) + } + (IntExt::NegativeInf, IntExt::Int(inner)) => { + if inner.is_positive() { + Some(IntExt::NegativeInf) + } else { + Some(IntExt::PositiveInf) + } + } + (IntExt::PositiveInf, IntExt::Int(inner)) => { + if inner.is_positive() { + Some(IntExt::PositiveInf) + } else { + Some(IntExt::NegativeInf) + } + } + (IntExt::PositiveInf, IntExt::NegativeInf) => None, + (IntExt::PositiveInf, IntExt::PositiveInf) => None, + (IntExt::NegativeInf, IntExt::NegativeInf) => None, + (IntExt::NegativeInf, IntExt::PositiveInf) => None, + (IntExt::Int(_), IntExt::NegativeInf) => Some(IntExt::Int(0)), + (IntExt::Int(_), IntExt::PositiveInf) => Some(IntExt::Int(0)), + } + } +} + impl From for IntExt { fn from(value: i32) -> Self { IntExt::Int(value) diff --git a/pumpkin-crates/propagators/src/propagators/arithmetic/integer_division.rs b/pumpkin-crates/propagators/src/propagators/arithmetic/integer_division.rs index 4155a532a..69e3fe3e6 100644 --- a/pumpkin-crates/propagators/src/propagators/arithmetic/integer_division.rs +++ b/pumpkin-crates/propagators/src/propagators/arithmetic/integer_division.rs @@ -1,3 +1,6 @@ +use pumpkin_checking::AtomicConstraint; +use pumpkin_checking::CheckerVariable; +use pumpkin_checking::InferenceChecker; use pumpkin_core::asserts::pumpkin_assert_simple; use pumpkin_core::conjunction; use pumpkin_core::declare_inference_label; @@ -5,6 +8,7 @@ use pumpkin_core::predicate; use pumpkin_core::proof::ConstraintTag; use pumpkin_core::proof::InferenceCode; use pumpkin_core::propagation::DomainEvents; +use pumpkin_core::propagation::InferenceCheckers; use pumpkin_core::propagation::LocalId; use pumpkin_core::propagation::Priority; use pumpkin_core::propagation::PropagationContext; @@ -64,6 +68,17 @@ where inference_code, } } + + fn add_inference_checkers(&self, mut checkers: InferenceCheckers<'_>) { + checkers.add_inference_checker( + InferenceCode::new(self.constraint_tag, Division), + Box::new(IntegerDivisionChecker { + numerator: self.numerator.clone(), + denominator: self.denominator.clone(), + rhs: self.rhs.clone(), + }), + ); + } } /// A propagator for maintaining the constraint `numerator / denominator = rhs`; note that this @@ -377,6 +392,72 @@ fn propagate_signs { + pub numerator: VA, + pub denominator: VB, + pub rhs: VC, +} + +impl InferenceChecker for IntegerDivisionChecker +where + Atomic: AtomicConstraint, + VA: CheckerVariable, + VB: CheckerVariable, + VC: CheckerVariable, +{ + fn check( + &self, + state: pumpkin_checking::VariableState, + _premises: &[Atomic], + _consequent: Option<&Atomic>, + ) -> bool { + // We apply interval arithmetic to determine that the computed interval `a div b` + // does not intersect with the domain of `c`. + // + // See https://en.wikipedia.org/wiki/Interval_arithmetic#Interval_operators. + // + // We guarantee that 0 is not the interval [y1, y2]. + + let x1 = self.numerator.induced_lower_bound(&state); + let x2 = self.numerator.induced_upper_bound(&state); + let y1 = self.denominator.induced_lower_bound(&state); + let y2 = self.denominator.induced_upper_bound(&state); + + assert!( + y2 < 0 || y1 > 0, + "The interval of the denominator should not contain 0" + ); + + let floor_x1y1 = x1.floor_div(&y1); + let floor_x1y2 = x1.floor_div(&y2); + let floor_x2y1 = x2.floor_div(&y1); + let floor_x2y2 = x2.floor_div(&y2); + + let ceil_x1y1 = x1.ceil_div(&y1); + let ceil_x1y2 = x1.ceil_div(&y2); + let ceil_x2y1 = x2.ceil_div(&y1); + let ceil_x2y2 = x2.ceil_div(&y2); + + // TODO: Can we just ignore these options? + let computed_c_lower = [ceil_x1y1, ceil_x1y2, ceil_x2y1, ceil_x2y2] + .into_iter() + .flatten() + .min() + .expect("Expected at least one element to be defined"); + let computed_c_upper = [floor_x1y1, floor_x1y2, floor_x2y1, floor_x2y2] + .into_iter() + .flatten() + .max() + .expect("Expected at least one element to be defined"); + + let c_lower = self.rhs.induced_lower_bound(&state); + let c_upper = self.rhs.induced_upper_bound(&state); + + computed_c_upper < c_lower || computed_c_lower > c_upper + } +} + #[allow(deprecated, reason = "Will be refactored")] #[cfg(test)] mod tests { From 502d57b1107c61fd414e3c775ec5e4b5a6847a29 Mon Sep 17 00:00:00 2001 From: Imko Marijnissen Date: Wed, 21 Jan 2026 16:33:35 +0100 Subject: [PATCH 47/48] feat: checker for reified propagator --- pumpkin-checker/src/model.rs | 17 ++++++++ pumpkin-crates/checking/src/variable.rs | 11 +++++ pumpkin-crates/checking/src/variable_state.rs | 11 +++++ .../core/src/engine/variables/affine_view.rs | 13 ++++++ .../core/src/engine/variables/domain_id.rs | 8 ++++ .../core/src/engine/variables/literal.rs | 1 + .../core/src/propagation/constructor.rs | 23 +++++++++- pumpkin-crates/core/src/propagators/mod.rs | 2 + .../src/propagators/reified_propagator.rs | 43 +++++++++++++++++++ .../arithmetic/integer_division.rs | 4 +- 10 files changed, 128 insertions(+), 5 deletions(-) diff --git a/pumpkin-checker/src/model.rs b/pumpkin-checker/src/model.rs index 629bd79b3..f0cae2484 100644 --- a/pumpkin-checker/src/model.rs +++ b/pumpkin-checker/src/model.rs @@ -238,6 +238,13 @@ impl CheckerVariable for Variable { VariableExpr::Constant(value) => Some(Box::new(std::iter::once(value))), } } + + fn induced_domain_contains(&self, variable_state: &VariableState, value: i32) -> bool { + match self.0 { + VariableExpr::Identifier(ref ident) => variable_state.contains(ident, value), + VariableExpr::Constant(constant_value) => constant_value == value, + } + } } #[derive(Clone, Debug)] @@ -360,6 +367,16 @@ impl CheckerVariable for Term { .iter_induced_domain(variable_state) .map(|iter| iter.map(|value| value * self.weight.get())) } + + fn induced_domain_contains(&self, variable_state: &VariableState, value: i32) -> bool { + if value % self.weight.get() == 0 { + let inverted = self.invert(value, Rounding::Up); + self.variable + .induced_domain_contains(variable_state, inverted) + } else { + false + } + } } #[derive(Clone, Debug)] diff --git a/pumpkin-crates/checking/src/variable.rs b/pumpkin-crates/checking/src/variable.rs index 7f7a08f82..8bcca0f59 100644 --- a/pumpkin-crates/checking/src/variable.rs +++ b/pumpkin-crates/checking/src/variable.rs @@ -32,6 +32,9 @@ pub trait CheckerVariable: Debug + Clone { /// Get the value the variable is fixed to, if the variable is fixed. fn induced_fixed_value(&self, variable_state: &VariableState) -> Option; + /// Returns whether the value is in the domain. + fn induced_domain_contains(&self, variable_state: &VariableState, value: i32) -> bool; + /// Get the holes in the domain. fn induced_holes<'this, 'state>( &'this self, @@ -100,6 +103,14 @@ impl CheckerVariable for &'static str { variable_state.fixed_value(self) } + fn induced_domain_contains( + &self, + variable_state: &VariableState, + value: i32, + ) -> bool { + variable_state.contains(self, value) + } + fn induced_holes<'this, 'state>( &'this self, variable_state: &'state VariableState, diff --git a/pumpkin-crates/checking/src/variable_state.rs b/pumpkin-crates/checking/src/variable_state.rs index 54041b72c..f225da98b 100644 --- a/pumpkin-crates/checking/src/variable_state.rs +++ b/pumpkin-crates/checking/src/variable_state.rs @@ -75,6 +75,17 @@ where .unwrap_or(IntExt::PositiveInf) } + pub fn contains(&self, identifier: &Ident, value: i32) -> bool { + self.domains + .get(identifier) + .map(|domain| { + value >= domain.lower_bound + && value <= domain.upper_bound + && !domain.holes.contains(&value) + }) + .unwrap_or(false) + } + /// Get the holes within the lower and upper bound of the variable expression. pub fn holes<'a>(&'a self, identifier: &Ident) -> impl Iterator + 'a where diff --git a/pumpkin-crates/core/src/engine/variables/affine_view.rs b/pumpkin-crates/core/src/engine/variables/affine_view.rs index 58435d35d..4b56b2173 100644 --- a/pumpkin-crates/core/src/engine/variables/affine_view.rs +++ b/pumpkin-crates/core/src/engine/variables/affine_view.rs @@ -147,6 +147,19 @@ impl CheckerVariable for AffineView { .iter_induced_domain(variable_state) .map(|iter| iter.map(|value| self.map(value))) } + + fn induced_domain_contains( + &self, + variable_state: &pumpkin_checking::VariableState, + value: i32, + ) -> bool { + if (value - self.offset) % self.scale == 0 { + let inverted = self.invert(value, Rounding::Up); + self.inner.induced_domain_contains(variable_state, inverted) + } else { + false + } + } } impl IntegerVariable for AffineView diff --git a/pumpkin-crates/core/src/engine/variables/domain_id.rs b/pumpkin-crates/core/src/engine/variables/domain_id.rs index 858d726c2..b64eb55f1 100644 --- a/pumpkin-crates/core/src/engine/variables/domain_id.rs +++ b/pumpkin-crates/core/src/engine/variables/domain_id.rs @@ -99,6 +99,14 @@ impl CheckerVariable for DomainId { { variable_state.iter_domain(self) } + + fn induced_domain_contains( + &self, + variable_state: &pumpkin_checking::VariableState, + value: i32, + ) -> bool { + variable_state.contains(self, value) + } } impl IntegerVariable for DomainId { diff --git a/pumpkin-crates/core/src/engine/variables/literal.rs b/pumpkin-crates/core/src/engine/variables/literal.rs index ca8d247df..3dd8263f9 100644 --- a/pumpkin-crates/core/src/engine/variables/literal.rs +++ b/pumpkin-crates/core/src/engine/variables/literal.rs @@ -83,6 +83,7 @@ impl CheckerVariable for Literal { forward!(integer_variable, fn induced_lower_bound(&self, variable_state: &VariableState) -> IntExt); forward!(integer_variable, fn induced_upper_bound(&self, variable_state: &VariableState) -> IntExt); forward!(integer_variable, fn induced_fixed_value(&self, variable_state: &VariableState) -> Option); + forward!(integer_variable, fn induced_domain_contains(&self, variable_state: &VariableState, value: i32) -> bool); forward!( integer_variable, fn <'this, 'state> induced_holes( diff --git a/pumpkin-crates/core/src/propagation/constructor.rs b/pumpkin-crates/core/src/propagation/constructor.rs index 65592887e..5a13e6a84 100644 --- a/pumpkin-crates/core/src/propagation/constructor.rs +++ b/pumpkin-crates/core/src/propagation/constructor.rs @@ -24,7 +24,9 @@ use crate::proof::InferenceCode; #[cfg(doc)] use crate::propagation::DomainEvent; use crate::propagation::DomainEvents; +use crate::propagators::reified_propagator::ReifiedChecker; use crate::variables::IntegerVariable; +use crate::variables::Literal; /// A propagator constructor creates a fully initialized instance of a [`Propagator`]. /// @@ -52,12 +54,16 @@ pub trait PropagatorConstructor { #[derive(Debug)] pub struct InferenceCheckers<'state> { state: &'state mut State, + reification_literal: Option, } impl<'state> InferenceCheckers<'state> { #[cfg(feature = "check-propagations")] pub(crate) fn new(state: &'state mut State) -> Self { - InferenceCheckers { state } + InferenceCheckers { + state, + reification_literal: None, + } } } @@ -68,7 +74,20 @@ impl InferenceCheckers<'_> { inference_code: InferenceCode, checker: Box>, ) { - self.state.add_inference_checker(inference_code, checker); + if let Some(reification_literal) = self.reification_literal { + let reification_checker = ReifiedChecker { + inner: checker.into(), + reification_literal, + }; + self.state + .add_inference_checker(inference_code, Box::new(reification_checker)); + } else { + self.state.add_inference_checker(inference_code, checker); + } + } + + pub fn with_reification_literal(&mut self, literal: Literal) { + self.reification_literal = Some(literal) } } diff --git a/pumpkin-crates/core/src/propagators/mod.rs b/pumpkin-crates/core/src/propagators/mod.rs index 010826ef6..2d88c10e4 100644 --- a/pumpkin-crates/core/src/propagators/mod.rs +++ b/pumpkin-crates/core/src/propagators/mod.rs @@ -1,2 +1,4 @@ pub mod nogoods; pub(crate) mod reified_propagator; + +pub use reified_propagator::*; diff --git a/pumpkin-crates/core/src/propagators/reified_propagator.rs b/pumpkin-crates/core/src/propagators/reified_propagator.rs index 7ae5ee758..09e163c25 100644 --- a/pumpkin-crates/core/src/propagators/reified_propagator.rs +++ b/pumpkin-crates/core/src/propagators/reified_propagator.rs @@ -1,3 +1,8 @@ +use pumpkin_checking::AtomicConstraint; +use pumpkin_checking::BoxedChecker; +use pumpkin_checking::CheckerVariable; +use pumpkin_checking::InferenceChecker; + use crate::basic_types::PropagationStatusCP; use crate::engine::notifications::OpaqueDomainEvent; use crate::predicates::Predicate; @@ -5,6 +10,7 @@ use crate::propagation::DomainEvents; use crate::propagation::Domains; use crate::propagation::EnqueueDecision; use crate::propagation::ExplanationContext; +use crate::propagation::InferenceCheckers; use crate::propagation::LocalId; use crate::propagation::NotificationContext; use crate::propagation::Priority; @@ -56,6 +62,12 @@ where reason_buffer: vec![], } } + + fn add_inference_checkers(&self, mut checkers: InferenceCheckers<'_>) { + checkers.with_reification_literal(self.reification_literal); + + self.propagator.add_inference_checkers(checkers); + } } /// Propagator for the constraint `r -> p`, where `r` is a Boolean literal and `p` is an arbitrary @@ -221,6 +233,37 @@ impl ReifiedPropagator { } } +#[derive(Debug, Clone)] +pub struct ReifiedChecker { + pub inner: BoxedChecker, + pub reification_literal: Var, +} + +impl> InferenceChecker + for ReifiedChecker +{ + fn check( + &self, + state: pumpkin_checking::VariableState, + premises: &[Atomic], + consequent: Option<&Atomic>, + ) -> bool { + if self.reification_literal.induced_domain_contains(&state, 0) { + return false; + } + + if let Some(consequent) = consequent + && self + .reification_literal + .does_atomic_constrain_self(consequent) + { + self.inner.check(state, premises, None) + } else { + self.inner.check(state, premises, consequent) + } + } +} + #[allow(deprecated, reason = "Will be refactored")] #[cfg(test)] mod tests { diff --git a/pumpkin-crates/propagators/src/propagators/arithmetic/integer_division.rs b/pumpkin-crates/propagators/src/propagators/arithmetic/integer_division.rs index 69e3fe3e6..27c4f121b 100644 --- a/pumpkin-crates/propagators/src/propagators/arithmetic/integer_division.rs +++ b/pumpkin-crates/propagators/src/propagators/arithmetic/integer_division.rs @@ -416,8 +416,6 @@ where // does not intersect with the domain of `c`. // // See https://en.wikipedia.org/wiki/Interval_arithmetic#Interval_operators. - // - // We guarantee that 0 is not the interval [y1, y2]. let x1 = self.numerator.induced_lower_bound(&state); let x2 = self.numerator.induced_upper_bound(&state); @@ -426,7 +424,7 @@ where assert!( y2 < 0 || y1 > 0, - "The interval of the denominator should not contain 0" + "Currentl, the checker does not contain inferences where the denominator spans 0" ); let floor_x1y1 = x1.floor_div(&y1); From 5830149ec7de03747e0fc8f8488ae815700be7c4 Mon Sep 17 00:00:00 2001 From: Imko Marijnissen Date: Wed, 21 Jan 2026 16:39:20 +0100 Subject: [PATCH 48/48] fix: allow scaling of holes with -1 or 1 --- .../core/src/engine/variables/affine_view.rs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/pumpkin-crates/core/src/engine/variables/affine_view.rs b/pumpkin-crates/core/src/engine/variables/affine_view.rs index 4b56b2173..8565880d2 100644 --- a/pumpkin-crates/core/src/engine/variables/affine_view.rs +++ b/pumpkin-crates/core/src/engine/variables/affine_view.rs @@ -126,14 +126,19 @@ impl CheckerVariable for AffineView { fn induced_holes<'this, 'state>( &'this self, - _variable_state: &'state pumpkin_checking::VariableState, + variable_state: &'state pumpkin_checking::VariableState, ) -> impl Iterator + 'state where 'this: 'state, { + if self.scale == 1 || self.scale == -1 { + return self + .inner + .induced_holes(variable_state) + .map(|value| self.map(value)); + } + todo!("how to iterate holes of a scaled domain"); - #[allow(unreachable_code, reason = "todo does not compile to impl Iterator")] - std::iter::empty() } fn iter_induced_domain<'this, 'state>(