From 2be6e8d653e0e2daa23eb75157273792acba89f9 Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Mon, 18 May 2026 12:02:32 +1000 Subject: [PATCH 01/23] Started reworking multiplication for consistency checkers. --- .../arithmetic/integer_multiplication.rs | 617 ------------------ .../src/propagators/arithmetic/mod.rs | 4 +- .../arithmetic/multiplication/constructor.rs | 70 ++ .../multiplication/inference_checker.rs | 48 ++ .../arithmetic/multiplication/mod.rs | 16 + .../arithmetic/multiplication/propagator.rs | 304 +++++++++ 6 files changed, 440 insertions(+), 619 deletions(-) delete mode 100644 pumpkin-crates/propagators/src/propagators/arithmetic/integer_multiplication.rs create mode 100644 pumpkin-crates/propagators/src/propagators/arithmetic/multiplication/constructor.rs create mode 100644 pumpkin-crates/propagators/src/propagators/arithmetic/multiplication/inference_checker.rs create mode 100644 pumpkin-crates/propagators/src/propagators/arithmetic/multiplication/mod.rs create mode 100644 pumpkin-crates/propagators/src/propagators/arithmetic/multiplication/propagator.rs diff --git a/pumpkin-crates/propagators/src/propagators/arithmetic/integer_multiplication.rs b/pumpkin-crates/propagators/src/propagators/arithmetic/integer_multiplication.rs deleted file mode 100644 index 9e7d8d953..000000000 --- a/pumpkin-crates/propagators/src/propagators/arithmetic/integer_multiplication.rs +++ /dev/null @@ -1,617 +0,0 @@ -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; -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; -use pumpkin_core::propagation::Propagator; -use pumpkin_core::propagation::PropagatorConstructor; -use pumpkin_core::propagation::PropagatorConstructorContext; -use pumpkin_core::propagation::ReadDomains; -use pumpkin_core::state::PropagationStatusCP; -use pumpkin_core::state::propagator_conflict; -use pumpkin_core::variables::IntegerVariable; - -declare_inference_label!(IntegerMultiplication); - -/// The [`PropagatorConstructor`] for [`IntegerMultiplicationPropagator`]. -#[derive(Clone, Debug)] -pub struct IntegerMultiplicationArgs { - pub a: VA, - pub b: VB, - pub c: VC, - pub constraint_tag: ConstraintTag, -} - -impl PropagatorConstructor for IntegerMultiplicationArgs -where - VA: IntegerVariable + 'static, - VB: IntegerVariable + 'static, - VC: IntegerVariable + 'static, -{ - 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, - b, - c, - constraint_tag, - } = self; - - context.register(a.clone(), DomainEvents::ANY_INT, ID_A); - context.register(b.clone(), DomainEvents::ANY_INT, ID_B); - context.register(c.clone(), DomainEvents::ANY_INT, ID_C); - - IntegerMultiplicationPropagator { - a, - b, - c, - inference_code: InferenceCode::new(constraint_tag, IntegerMultiplication), - } - } -} - -/// A propagator for maintaining the constraint `a * b = c`. The propagator -/// (currently) only propagates the signs of the variables, the case where a, b, c >= 0, and detects -/// a conflict if the variables are fixed. -#[derive(Clone, Debug)] -pub struct IntegerMultiplicationPropagator { - a: VA, - b: VB, - c: VC, - inference_code: InferenceCode, -} - -const ID_A: LocalId = LocalId::from(0); -const ID_B: LocalId = LocalId::from(1); -const ID_C: LocalId = LocalId::from(2); - -impl Propagator - for IntegerMultiplicationPropagator -where - VA: IntegerVariable, - VB: IntegerVariable, - VC: IntegerVariable, -{ - fn priority(&self) -> Priority { - Priority::High - } - - fn name(&self) -> &str { - "IntTimes" - } - - fn propagate_from_scratch(&self, context: PropagationContext) -> PropagationStatusCP { - perform_propagation(context, &self.a, &self.b, &self.c, &self.inference_code) - } -} - -fn perform_propagation( - mut context: PropagationContext, - a: &VA, - b: &VB, - c: &VC, - inference_code: &InferenceCode, -) -> PropagationStatusCP { - // First we propagate the signs - propagate_signs(&mut context, a, b, c, inference_code)?; - - let a_min = context.lower_bound(a); - let a_max = context.upper_bound(a); - let b_min = context.lower_bound(b); - let b_max = context.upper_bound(b); - let c_min = context.lower_bound(c); - let c_max = context.upper_bound(c); - - if a_min >= 0 && b_min >= 0 { - let new_max_c = a_max.saturating_mul(b_max); - let new_min_c = a_min.saturating_mul(b_min); - - // c is smaller than the maximum value that a * b can take - // - // We need the lower-bounds in the explanation as well because the reasoning does not - // hold in the case of a negative lower-bound - context.post( - predicate![c <= new_max_c], - ( - conjunction!([a >= 0] & [a <= a_max] & [b >= 0] & [b <= b_max]), - inference_code, - ), - )?; - - // c is larger than the minimum value that a * b can take - context.post( - predicate![c >= new_min_c], - (conjunction!([a >= a_min] & [b >= b_min]), inference_code), - )?; - } - - if b_min >= 0 && b_max >= 1 && c_min >= 1 { - // a >= ceil(c.min / b.max) - let bound = div_ceil_pos(c_min, b_max); - context.post( - predicate![a >= bound], - ( - conjunction!([c >= c_min] & [b >= 0] & [b <= b_max]), - inference_code, - ), - )?; - } - - if b_min >= 1 && c_min >= 0 && c_max >= 1 { - // a <= floor(c.max / b.min) - let bound = c_max / b_min; - context.post( - predicate![a <= bound], - ( - conjunction!([c >= 0] & [c <= c_max] & [b >= b_min]), - inference_code, - ), - )?; - } - - if a_min >= 1 && c_min >= 0 && c_max >= 1 { - // b <= floor(c.max / a.min) - let bound = c_max / a_min; - context.post( - predicate![b <= bound], - ( - conjunction!([c >= 0] & [c <= c_max] & [a >= a_min]), - inference_code, - ), - )?; - } - - // b >= ceil(c.min / a.max) - if a_min >= 0 && a_max >= 1 && c_min >= 1 { - let bound = div_ceil_pos(c_min, a_max); - - context.post( - predicate![b >= bound], - ( - conjunction!([c >= c_min] & [a >= 0] & [a <= a_max]), - inference_code, - ), - )?; - } - - if let Some(fixed_a) = context.fixed_value(a) - && let Some(fixed_b) = context.fixed_value(b) - && let Some(fixed_c) = context.fixed_value(c) - && (fixed_a * fixed_b) != fixed_c - { - // All variables are assigned but the resulting value is not correct, so we report a - // conflict - return propagator_conflict( - conjunction!( - [a == context.lower_bound(a)] - & [b == context.lower_bound(b)] - & [c == context.lower_bound(c)] - ), - inference_code, - ); - } - - Ok(()) -} - -/// Propagates the signs of the variables, it performs the following propagations: -/// - Propagating based on positive bounds -/// - If a is positive and b is positive then c is positive -/// - If a is positive and c is positive then b is positive -/// - If b is positive and c is positive then a is positive -/// - Propagating based on negative bounds -/// - If a is negative and b is negative then c is positive -/// - If a is negative and c is negative then b is positive -/// - If b is negative and c is negative then b is positive -/// - Propagating based on mixed bounds -/// - Propagating c based on a and b -/// - If a is negative and b is positive then c is negative -/// - If a is positive and b is negative then c is negative -/// - Propagating b based on a and c -/// - If a is negative and c is positive then b is negative -/// - If a is positive and c is negative then b is negative -/// - Propagating a based on b and c -/// - If b is negative and c is positive then a is negative -/// - If b is positive and c is negative then a is negative -/// -/// Note that this method does not propagate a value if 0 is in the domain as, for example, 0 * -3 = -/// 0 and 0 * 3 = 0 are both equally valid. -fn propagate_signs( - context: &mut PropagationContext, - a: &VA, - b: &VB, - c: &VC, - inference_code: &InferenceCode, -) -> PropagationStatusCP { - let a_min = context.lower_bound(a); - let a_max = context.upper_bound(a); - let b_min = context.lower_bound(b); - let b_max = context.upper_bound(b); - let c_min = context.lower_bound(c); - let c_max = context.upper_bound(c); - - // Propagating based on positive bounds - // a is positive and b is positive -> c is positive - if a_min >= 0 && b_min >= 0 { - context.post( - predicate![c >= 0], - (conjunction!([a >= 0] & [b >= 0]), inference_code), - )?; - } - - // a is positive and c is positive -> b is positive - if a_min >= 1 && c_min >= 1 { - context.post( - predicate![b >= 1], - (conjunction!([a >= 1] & [c >= 1]), inference_code), - )?; - } - - // b is positive and c is positive -> a is positive - if b_min >= 1 && c_min >= 1 { - context.post( - predicate![a >= 1], - (conjunction!([b >= 1] & [c >= 1]), inference_code), - )?; - } - - // Propagating based on negative bounds - // a is negative and b is negative -> c is positive - if a_max <= 0 && b_max <= 0 { - context.post( - predicate![c >= 0], - (conjunction!([a <= 0] & [b <= 0]), inference_code), - )?; - } - - // a is negative and c is negative -> b is positive - if a_max <= -1 && c_max <= -1 { - context.post( - predicate![b >= 1], - (conjunction!([a <= -1] & [c <= -1]), inference_code), - )?; - } - - // b is negative and c is negative -> a is positive - if b_max <= -1 && c_max <= -1 { - context.post( - predicate![a >= 1], - (conjunction!([b <= -1] & [c <= -1]), inference_code), - )?; - } - - // Propagating based on mixed bounds (i.e. one positive and one negative) - // Propagating c based on a and b - // a is negative and b is positive -> c is negative - if a_max <= 0 && b_min >= 0 { - context.post( - predicate![c <= 0], - (conjunction!([a <= 0] & [b >= 0]), inference_code), - )?; - } - - // a is positive and b is negative -> c is negative - if a_min >= 0 && b_max <= 0 { - context.post( - predicate![c <= 0], - (conjunction!([a >= 0] & [b <= 0]), inference_code), - )?; - } - - // Propagating b based on a and c - // a is negative and c is positive -> b is negative - if a_max <= -1 && c_min >= 1 { - context.post( - predicate![b <= -1], - (conjunction!([a <= -1] & [c >= 1]), inference_code), - )?; - } - - // a is positive and c is negative -> b is negative - if a_min >= 1 && c_max <= -1 { - context.post( - predicate![b <= -1], - (conjunction!([a >= 1] & [c <= -1]), inference_code), - )?; - } - - // Propagating a based on b and c - // b is negative and c is positive -> a is negative - if b_max <= -1 && c_min >= 1 { - context.post( - predicate![a <= -1], - (conjunction!([b <= -1] & [c >= 1]), inference_code), - )?; - } - - // b is positive and c is negative -> a is negative - if b_min >= 1 && c_max <= -1 { - context.post( - predicate![a <= -1], - (conjunction!([b >= 1] & [c <= -1]), inference_code), - )?; - } - - Ok(()) -} - -/// Compute `ceil(numerator / denominator)`. -/// -/// Assumes `numerator, denominator > 0`. -#[inline] -fn div_ceil_pos(numerator: i32, denominator: i32) -> i32 { - pumpkin_assert_simple!( - numerator > 0 && denominator > 0, - "Either the numerator {numerator} was non-positive or the denominator {denominator} was non-positive" - ); - 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 - } -} - -#[cfg(test)] -mod tests { - use pumpkin_core::predicate; - use pumpkin_core::predicates::Predicate; - use pumpkin_core::predicates::PropositionalConjunction; - use pumpkin_core::propagation::CurrentNogood; - use pumpkin_core::state::State; - - use super::*; - use crate::StateExt; - - #[test] - fn bounds_of_a_and_b_propagate_bounds_c() { - let mut state = State::default(); - let a = state.new_interval_variable(1, 3, None); - let b = state.new_interval_variable(0, 4, None); - let c = state.new_interval_variable(-10, 20, None); - - let constraint_tag = state.new_constraint_tag(); - - let _ = state.add_propagator(IntegerMultiplicationArgs { - a, - b, - c, - constraint_tag, - }); - state.propagate_to_fixed_point().expect("no empty domains"); - - state.assert_bounds(a, 1, 3); - state.assert_bounds(b, 0, 4); - state.assert_bounds(c, 0, 12); - - let mut reason_buffer: Vec = vec![]; - let _ = state.get_propagation_reason( - predicate![c >= 0], - &mut reason_buffer, - CurrentNogood::empty(), - ); - let reason_lb: PropositionalConjunction = reason_buffer.into(); - assert_eq!(conjunction!([a >= 0] & [b >= 0]), reason_lb); - - let mut reason_buffer: Vec = vec![]; - let _ = state.get_propagation_reason( - predicate![c <= 12], - &mut reason_buffer, - CurrentNogood::empty(), - ); - let reason_ub: PropositionalConjunction = reason_buffer.into(); - assert_eq!( - conjunction!([a >= 0] & [a <= 3] & [b >= 0] & [b <= 4]), - reason_ub - ); - } - - #[test] - fn bounds_of_a_and_c_propagate_bounds_b() { - let mut state = State::default(); - let a = state.new_interval_variable(2, 3, None); - let b = state.new_interval_variable(0, 12, None); - let c = state.new_interval_variable(2, 12, None); - - let constraint_tag = state.new_constraint_tag(); - - let _ = state.add_propagator(IntegerMultiplicationArgs { - a, - b, - c, - constraint_tag, - }); - state.propagate_to_fixed_point().expect("no empty domains"); - - state.assert_bounds(a, 2, 3); - state.assert_bounds(b, 1, 6); - state.assert_bounds(c, 2, 12); - - let mut reason_buffer: Vec = vec![]; - let _ = state.get_propagation_reason( - predicate![b >= 1], - &mut reason_buffer, - CurrentNogood::empty(), - ); - let reason_lb: PropositionalConjunction = reason_buffer.into(); - assert_eq!(conjunction!([a >= 1] & [c >= 1]), reason_lb); - - let mut reason_buffer: Vec = vec![]; - let _ = state.get_propagation_reason( - predicate![b <= 6], - &mut reason_buffer, - CurrentNogood::empty(), - ); - let reason_ub: PropositionalConjunction = reason_buffer.into(); - assert_eq!(conjunction!([a >= 2] & [c >= 0] & [c <= 12]), reason_ub); - } - - #[test] - fn bounds_of_b_and_c_propagate_bounds_a() { - let mut state = State::default(); - let a = state.new_interval_variable(0, 10, None); - let b = state.new_interval_variable(3, 6, None); - let c = state.new_interval_variable(2, 12, None); - - let constraint_tag = state.new_constraint_tag(); - - let _ = state.add_propagator(IntegerMultiplicationArgs { - a, - b, - c, - constraint_tag, - }); - state.propagate_to_fixed_point().expect("no empty domains"); - - state.assert_bounds(a, 1, 4); - state.assert_bounds(b, 3, 6); - state.assert_bounds(c, 3, 12); - - let mut reason_buffer: Vec = vec![]; - let _ = state.get_propagation_reason( - predicate![a >= 1], - &mut reason_buffer, - CurrentNogood::empty(), - ); - let reason_lb: PropositionalConjunction = reason_buffer.into(); - assert_eq!(conjunction!([b >= 1] & [c >= 1]), reason_lb); - - let mut reason_buffer: Vec = vec![]; - let _ = state.get_propagation_reason( - predicate![a <= 4], - &mut reason_buffer, - CurrentNogood::empty(), - ); - let reason_ub: PropositionalConjunction = reason_buffer.into(); - assert_eq!(conjunction!([b >= 3] & [c >= 0] & [c <= 12]), reason_ub); - } - - #[test] - fn b_unbounded_does_not_panic() { - let mut state = State::default(); - let a = state.new_interval_variable(12, 12, None); - let b = state.new_interval_variable(i32::MIN, i32::MAX, None); - let c = state.new_interval_variable(144, 144, None); - - let constraint_tag = state.new_constraint_tag(); - let _ = state.add_propagator(IntegerMultiplicationArgs { - a, - b, - c, - constraint_tag, - }); - state.propagate_to_fixed_point().expect("No empty domains"); - } - - #[test] - fn a_unbounded_does_not_panic() { - let mut state = State::default(); - let a = state.new_interval_variable(i32::MIN, i32::MAX, None); - let b = state.new_interval_variable(12, 12, None); - let c = state.new_interval_variable(144, 144, None); - - let constraint_tag = state.new_constraint_tag(); - let _ = state.add_propagator(IntegerMultiplicationArgs { - a, - b, - c, - constraint_tag, - }); - state.propagate_to_fixed_point().expect("No empty domains"); - } - - #[test] - fn c_unbounded_does_not_panic() { - let mut state = State::default(); - let a = state.new_interval_variable(12, 12, None); - let b = state.new_interval_variable(12, 12, None); - let c = state.new_interval_variable(i32::MIN, i32::MAX, None); - - let constraint_tag = state.new_constraint_tag(); - let _ = state.add_propagator(IntegerMultiplicationArgs { - a, - b, - c, - constraint_tag, - }); - state.propagate_to_fixed_point().expect("No empty domains"); - } - - #[test] - fn all_unbounded_does_not_panic() { - let mut state = State::default(); - let a = state.new_interval_variable(i32::MIN, i32::MAX, None); - let b = state.new_interval_variable(i32::MIN, i32::MAX, None); - let c = state.new_interval_variable(i32::MIN, i32::MAX, None); - - let constraint_tag = state.new_constraint_tag(); - let _ = state.add_propagator(IntegerMultiplicationArgs { - a, - b, - c, - constraint_tag, - }); - state.propagate_to_fixed_point().expect("No empty domains"); - } -} diff --git a/pumpkin-crates/propagators/src/propagators/arithmetic/mod.rs b/pumpkin-crates/propagators/src/propagators/arithmetic/mod.rs index 21fc6eb9a..461ed4da1 100644 --- a/pumpkin-crates/propagators/src/propagators/arithmetic/mod.rs +++ b/pumpkin-crates/propagators/src/propagators/arithmetic/mod.rs @@ -2,15 +2,15 @@ pub(crate) mod absolute_value; pub(crate) mod binary; pub(crate) mod integer_division; -pub(crate) mod integer_multiplication; pub(crate) mod linear_less_or_equal; pub(crate) mod linear_not_equal; pub(crate) mod maximum; +mod multiplication; pub use absolute_value::*; pub use binary::*; pub use integer_division::*; -pub use integer_multiplication::*; pub use linear_less_or_equal::*; pub use linear_not_equal::*; pub use maximum::*; +pub use multiplication::*; diff --git a/pumpkin-crates/propagators/src/propagators/arithmetic/multiplication/constructor.rs b/pumpkin-crates/propagators/src/propagators/arithmetic/multiplication/constructor.rs new file mode 100644 index 000000000..14715dedd --- /dev/null +++ b/pumpkin-crates/propagators/src/propagators/arithmetic/multiplication/constructor.rs @@ -0,0 +1,70 @@ +use pumpkin_core::proof::ConstraintTag; +use pumpkin_core::proof::InferenceCode; +use pumpkin_core::propagation::DomainEvents; +use pumpkin_core::propagation::InferenceCheckers; +use pumpkin_core::propagation::PropagatorConstructor; +use pumpkin_core::propagation::PropagatorConstructorContext; +use pumpkin_core::variables::IntegerVariable; + +use crate::arithmetic::IntegerMultiplicationPropagator; +use crate::arithmetic::multiplication::IntegerMultiplication; +use crate::arithmetic::multiplication::inference_checker::IntegerMultiplicationChecker; + +/// The [`PropagatorConstructor`] for [`IntegerMultiplicationPropagator`]. +#[derive(Clone, Debug)] +pub struct IntegerMultiplicationArgs { + pub a: VA, + pub b: VB, + pub c: VC, + pub constraint_tag: ConstraintTag, +} + +impl PropagatorConstructor for IntegerMultiplicationArgs +where + VA: IntegerVariable + 'static, + VB: IntegerVariable + 'static, + VC: IntegerVariable + 'static, +{ + 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(), + }), + ); + + checkers.add_consistency_checker( + self.constraint_tag, + [&self.a, &self.b, &self.c], + BoundsConsistencyChecker::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, + b, + c, + constraint_tag, + } = self; + + context.register(a.clone(), DomainEvents::ANY_INT, super::ID_A); + context.register(b.clone(), DomainEvents::ANY_INT, super::ID_B); + context.register(c.clone(), DomainEvents::ANY_INT, super::ID_C); + + IntegerMultiplicationPropagator { + a, + b, + c, + inference_code: InferenceCode::new(constraint_tag, IntegerMultiplication), + } + } +} diff --git a/pumpkin-crates/propagators/src/propagators/arithmetic/multiplication/inference_checker.rs b/pumpkin-crates/propagators/src/propagators/arithmetic/multiplication/inference_checker.rs new file mode 100644 index 000000000..79ad39d8e --- /dev/null +++ b/pumpkin-crates/propagators/src/propagators/arithmetic/multiplication/inference_checker.rs @@ -0,0 +1,48 @@ +use pumpkin_checking::AtomicConstraint; +use pumpkin_checking::CheckerVariable; +use pumpkin_checking::InferenceChecker; + +#[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 + } +} diff --git a/pumpkin-crates/propagators/src/propagators/arithmetic/multiplication/mod.rs b/pumpkin-crates/propagators/src/propagators/arithmetic/multiplication/mod.rs new file mode 100644 index 000000000..f0bbf8855 --- /dev/null +++ b/pumpkin-crates/propagators/src/propagators/arithmetic/multiplication/mod.rs @@ -0,0 +1,16 @@ +mod constructor; +mod inference_checker; +mod propagator; + +pub use constructor::*; +pub use propagator::*; +use pumpkin_core::declare_inference_label; +use pumpkin_core::propagation::LocalId; + +// The LocalId's for the variables. +const ID_A: LocalId = LocalId::from(0); +const ID_B: LocalId = LocalId::from(1); +const ID_C: LocalId = LocalId::from(2); + +// The inference label for integer multiplication. +declare_inference_label!(IntegerMultiplication); diff --git a/pumpkin-crates/propagators/src/propagators/arithmetic/multiplication/propagator.rs b/pumpkin-crates/propagators/src/propagators/arithmetic/multiplication/propagator.rs new file mode 100644 index 000000000..9e4f9cac5 --- /dev/null +++ b/pumpkin-crates/propagators/src/propagators/arithmetic/multiplication/propagator.rs @@ -0,0 +1,304 @@ +use pumpkin_core::asserts::pumpkin_assert_simple; +use pumpkin_core::conjunction; +use pumpkin_core::predicate; +use pumpkin_core::proof::InferenceCode; +use pumpkin_core::propagation::Priority; +use pumpkin_core::propagation::PropagationContext; +use pumpkin_core::propagation::Propagator; +use pumpkin_core::propagation::ReadDomains; +use pumpkin_core::state::PropagationStatusCP; +use pumpkin_core::state::propagator_conflict; +use pumpkin_core::variables::IntegerVariable; + +/// A propagator for maintaining the constraint `a * b = c`. The propagator +/// (currently) only propagates the signs of the variables, the case where a, b, c >= 0, and detects +/// a conflict if the variables are fixed. +#[derive(Clone, Debug)] +pub struct IntegerMultiplicationPropagator { + pub(super) a: VA, + pub(super) b: VB, + pub(super) c: VC, + pub(super) inference_code: InferenceCode, +} + +impl Propagator + for IntegerMultiplicationPropagator +where + VA: IntegerVariable, + VB: IntegerVariable, + VC: IntegerVariable, +{ + fn priority(&self) -> Priority { + Priority::High + } + + fn name(&self) -> &str { + "IntTimes" + } + + fn propagate_from_scratch(&self, context: PropagationContext) -> PropagationStatusCP { + perform_propagation(context, &self.a, &self.b, &self.c, &self.inference_code) + } +} + +fn perform_propagation( + mut context: PropagationContext, + a: &VA, + b: &VB, + c: &VC, + inference_code: &InferenceCode, +) -> PropagationStatusCP { + // First we propagate the signs + propagate_signs(&mut context, a, b, c, inference_code)?; + + let a_min = context.lower_bound(a); + let a_max = context.upper_bound(a); + let b_min = context.lower_bound(b); + let b_max = context.upper_bound(b); + let c_min = context.lower_bound(c); + let c_max = context.upper_bound(c); + + if a_min >= 0 && b_min >= 0 { + let new_max_c = a_max.saturating_mul(b_max); + let new_min_c = a_min.saturating_mul(b_min); + + // c is smaller than the maximum value that a * b can take + // + // We need the lower-bounds in the explanation as well because the reasoning does not + // hold in the case of a negative lower-bound + context.post( + predicate![c <= new_max_c], + ( + conjunction!([a >= 0] & [a <= a_max] & [b >= 0] & [b <= b_max]), + inference_code, + ), + )?; + + // c is larger than the minimum value that a * b can take + context.post( + predicate![c >= new_min_c], + (conjunction!([a >= a_min] & [b >= b_min]), inference_code), + )?; + } + + if b_min >= 0 && b_max >= 1 && c_min >= 1 { + // a >= ceil(c.min / b.max) + let bound = div_ceil_pos(c_min, b_max); + context.post( + predicate![a >= bound], + ( + conjunction!([c >= c_min] & [b >= 0] & [b <= b_max]), + inference_code, + ), + )?; + } + + if b_min >= 1 && c_min >= 0 && c_max >= 1 { + // a <= floor(c.max / b.min) + let bound = c_max / b_min; + context.post( + predicate![a <= bound], + ( + conjunction!([c >= 0] & [c <= c_max] & [b >= b_min]), + inference_code, + ), + )?; + } + + if a_min >= 1 && c_min >= 0 && c_max >= 1 { + // b <= floor(c.max / a.min) + let bound = c_max / a_min; + context.post( + predicate![b <= bound], + ( + conjunction!([c >= 0] & [c <= c_max] & [a >= a_min]), + inference_code, + ), + )?; + } + + // b >= ceil(c.min / a.max) + if a_min >= 0 && a_max >= 1 && c_min >= 1 { + let bound = div_ceil_pos(c_min, a_max); + + context.post( + predicate![b >= bound], + ( + conjunction!([c >= c_min] & [a >= 0] & [a <= a_max]), + inference_code, + ), + )?; + } + + if let Some(fixed_a) = context.fixed_value(a) + && let Some(fixed_b) = context.fixed_value(b) + && let Some(fixed_c) = context.fixed_value(c) + && (fixed_a * fixed_b) != fixed_c + { + // All variables are assigned but the resulting value is not correct, so we report a + // conflict + return propagator_conflict( + conjunction!( + [a == context.lower_bound(a)] + & [b == context.lower_bound(b)] + & [c == context.lower_bound(c)] + ), + inference_code, + ); + } + + Ok(()) +} + +/// Propagates the signs of the variables, it performs the following propagations: +/// - Propagating based on positive bounds +/// - If a is positive and b is positive then c is positive +/// - If a is positive and c is positive then b is positive +/// - If b is positive and c is positive then a is positive +/// - Propagating based on negative bounds +/// - If a is negative and b is negative then c is positive +/// - If a is negative and c is negative then b is positive +/// - If b is negative and c is negative then b is positive +/// - Propagating based on mixed bounds +/// - Propagating c based on a and b +/// - If a is negative and b is positive then c is negative +/// - If a is positive and b is negative then c is negative +/// - Propagating b based on a and c +/// - If a is negative and c is positive then b is negative +/// - If a is positive and c is negative then b is negative +/// - Propagating a based on b and c +/// - If b is negative and c is positive then a is negative +/// - If b is positive and c is negative then a is negative +/// +/// Note that this method does not propagate a value if 0 is in the domain as, for example, 0 * -3 = +/// 0 and 0 * 3 = 0 are both equally valid. +fn propagate_signs( + context: &mut PropagationContext, + a: &VA, + b: &VB, + c: &VC, + inference_code: &InferenceCode, +) -> PropagationStatusCP { + let a_min = context.lower_bound(a); + let a_max = context.upper_bound(a); + let b_min = context.lower_bound(b); + let b_max = context.upper_bound(b); + let c_min = context.lower_bound(c); + let c_max = context.upper_bound(c); + + // Propagating based on positive bounds + // a is positive and b is positive -> c is positive + if a_min >= 0 && b_min >= 0 { + context.post( + predicate![c >= 0], + (conjunction!([a >= 0] & [b >= 0]), inference_code), + )?; + } + + // a is positive and c is positive -> b is positive + if a_min >= 1 && c_min >= 1 { + context.post( + predicate![b >= 1], + (conjunction!([a >= 1] & [c >= 1]), inference_code), + )?; + } + + // b is positive and c is positive -> a is positive + if b_min >= 1 && c_min >= 1 { + context.post( + predicate![a >= 1], + (conjunction!([b >= 1] & [c >= 1]), inference_code), + )?; + } + + // Propagating based on negative bounds + // a is negative and b is negative -> c is positive + if a_max <= 0 && b_max <= 0 { + context.post( + predicate![c >= 0], + (conjunction!([a <= 0] & [b <= 0]), inference_code), + )?; + } + + // a is negative and c is negative -> b is positive + if a_max <= -1 && c_max <= -1 { + context.post( + predicate![b >= 1], + (conjunction!([a <= -1] & [c <= -1]), inference_code), + )?; + } + + // b is negative and c is negative -> a is positive + if b_max <= -1 && c_max <= -1 { + context.post( + predicate![a >= 1], + (conjunction!([b <= -1] & [c <= -1]), inference_code), + )?; + } + + // Propagating based on mixed bounds (i.e. one positive and one negative) + // Propagating c based on a and b + // a is negative and b is positive -> c is negative + if a_max <= 0 && b_min >= 0 { + context.post( + predicate![c <= 0], + (conjunction!([a <= 0] & [b >= 0]), inference_code), + )?; + } + + // a is positive and b is negative -> c is negative + if a_min >= 0 && b_max <= 0 { + context.post( + predicate![c <= 0], + (conjunction!([a >= 0] & [b <= 0]), inference_code), + )?; + } + + // Propagating b based on a and c + // a is negative and c is positive -> b is negative + if a_max <= -1 && c_min >= 1 { + context.post( + predicate![b <= -1], + (conjunction!([a <= -1] & [c >= 1]), inference_code), + )?; + } + + // a is positive and c is negative -> b is negative + if a_min >= 1 && c_max <= -1 { + context.post( + predicate![b <= -1], + (conjunction!([a >= 1] & [c <= -1]), inference_code), + )?; + } + + // Propagating a based on b and c + // b is negative and c is positive -> a is negative + if b_max <= -1 && c_min >= 1 { + context.post( + predicate![a <= -1], + (conjunction!([b <= -1] & [c >= 1]), inference_code), + )?; + } + + // b is positive and c is negative -> a is negative + if b_min >= 1 && c_max <= -1 { + context.post( + predicate![a <= -1], + (conjunction!([b >= 1] & [c <= -1]), inference_code), + )?; + } + + Ok(()) +} + +/// Compute `ceil(numerator / denominator)`. +/// +/// Assumes `numerator, denominator > 0`. +#[inline] +fn div_ceil_pos(numerator: i32, denominator: i32) -> i32 { + pumpkin_assert_simple!( + numerator > 0 && denominator > 0, + "Either the numerator {numerator} was non-positive or the denominator {denominator} was non-positive" + ); + numerator / denominator + (numerator % denominator).signum() +} From 4ca596a0134aaa788efdf4fb016dc735ff5d27ec Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Wed, 20 May 2026 13:44:19 +1000 Subject: [PATCH 02/23] feat(pumpkin-solver): Implement consistency checker infrastructure --- .github/workflows/ci.yml | 8 +- Cargo.lock | 19 +++ .../src/constraints/arithmetic/mod.rs | 7 +- pumpkin-crates/core/Cargo.toml | 2 + .../core/src/checkers/bounds_consistency.rs | 80 +++++++++ pumpkin-crates/core/src/checkers/mod.rs | 45 +++++ pumpkin-crates/core/src/checkers/scope.rs | 53 ++++++ pumpkin-crates/core/src/checkers/store.rs | 74 +++++++++ pumpkin-crates/core/src/checkers/support.rs | 157 ++++++++++++++++++ .../core/src/containers/keyed_bit_set.rs | 50 ++++++ pumpkin-crates/core/src/containers/mod.rs | 2 + pumpkin-crates/core/src/engine/state.rs | 40 ++++- .../core/src/engine/variables/affine_view.rs | 59 +++++++ .../core/src/engine/variables/domain_id.rs | 39 +++++ .../src/engine/variables/integer_variable.rs | 2 + .../core/src/engine/variables/literal.rs | 17 ++ pumpkin-crates/core/src/lib.rs | 1 + .../core/src/propagation/constructor.rs | 16 +- .../arithmetic/multiplication/checker.rs | 152 +++++++++++++++++ .../arithmetic/multiplication/constructor.rs | 16 +- .../multiplication/inference_checker.rs | 48 ------ .../arithmetic/multiplication/mod.rs | 3 +- 22 files changed, 829 insertions(+), 61 deletions(-) create mode 100644 pumpkin-crates/core/src/checkers/bounds_consistency.rs create mode 100644 pumpkin-crates/core/src/checkers/mod.rs create mode 100644 pumpkin-crates/core/src/checkers/scope.rs create mode 100644 pumpkin-crates/core/src/checkers/store.rs create mode 100644 pumpkin-crates/core/src/checkers/support.rs create mode 100644 pumpkin-crates/core/src/containers/keyed_bit_set.rs create mode 100644 pumpkin-crates/propagators/src/propagators/arithmetic/multiplication/checker.rs delete mode 100644 pumpkin-crates/propagators/src/propagators/arithmetic/multiplication/inference_checker.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 48a8e3ab6..27dc0aee3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -26,7 +26,13 @@ jobs: target/ key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} - uses: dtolnay/rust-toolchain@stable - - run: cargo test --release --no-fail-fast --features pumpkin-solver/check-propagations --features pumpkin-core/check-deductions + - run: | + cargo test \ + --release \ + --no-fail-fast \ + --features pumpkin-solver/check-propagations \ + --features pumpkin-core/check-consistency \ + --features pumpkin-core/check-deductions wasm-test: name: Test Suite for pumpkin-core in WebAssembly diff --git a/Cargo.lock b/Cargo.lock index adbdcb88d..08e486f5a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -120,6 +120,24 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +[[package]] +name = "bit-set" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2f926cc3060f09db9ebc5b52823d85268d24bb917e472c0c4bea35780a7d" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b71798fca2c1fe1086445a7258a4bc81e6e49dcd24c8d0dd9a1e57395b603f51" +dependencies = [ + "serde", +] + [[package]] name = "bitfield" version = "0.19.4" @@ -973,6 +991,7 @@ dependencies = [ name = "pumpkin-core" version = "0.3.0" dependencies = [ + "bit-set", "bitfield", "bitfield-struct", "clap", diff --git a/pumpkin-crates/constraints/src/constraints/arithmetic/mod.rs b/pumpkin-crates/constraints/src/constraints/arithmetic/mod.rs index fe41b4721..eccea63c6 100644 --- a/pumpkin-crates/constraints/src/constraints/arithmetic/mod.rs +++ b/pumpkin-crates/constraints/src/constraints/arithmetic/mod.rs @@ -3,6 +3,7 @@ mod inequality; pub use equality::*; pub use inequality::*; +use pumpkin_core::checkers::support::SupportsValue; use pumpkin_core::constraints::Constraint; use pumpkin_core::proof::ConstraintTag; use pumpkin_core::variables::IntegerVariable; @@ -23,9 +24,9 @@ pub fn plus( /// Creates the [`Constraint`] `a * b = c`. pub fn times( - a: impl IntegerVariable + 'static, - b: impl IntegerVariable + 'static, - c: impl IntegerVariable + 'static, + a: impl IntegerVariable + SupportsValue + 'static, + b: impl IntegerVariable + SupportsValue + 'static, + c: impl IntegerVariable + SupportsValue + 'static, constraint_tag: ConstraintTag, ) -> impl Constraint { IntegerMultiplicationArgs { diff --git a/pumpkin-crates/core/Cargo.toml b/pumpkin-crates/core/Cargo.toml index 371849216..564d9398e 100644 --- a/pumpkin-crates/core/Cargo.toml +++ b/pumpkin-crates/core/Cargo.toml @@ -30,6 +30,7 @@ clap = { version = "4.5.40", optional = true, features=["derive"] } indexmap = "2.10.0" dyn-clone = "1.0.20" flate2 = { version = "1.1.2" } +bit-set = "0.10.0" [target.'cfg(target_arch = "wasm32")'.dependencies] web-time = "1.1" @@ -39,6 +40,7 @@ getrandom = { version = "0.4.2", features = ["wasm_js"] } wasm-bindgen-test = "0.3" [features] +check-consistency = [] check-propagations = [] check-deductions = [] debug-checks = [] diff --git a/pumpkin-crates/core/src/checkers/bounds_consistency.rs b/pumpkin-crates/core/src/checkers/bounds_consistency.rs new file mode 100644 index 000000000..97a3ccd0a --- /dev/null +++ b/pumpkin-crates/core/src/checkers/bounds_consistency.rs @@ -0,0 +1,80 @@ +use super::Scope; +use crate::checkers::ConsistencyChecker; +use crate::checkers::support::Support; +use crate::checkers::support::SupportGenerator; +use crate::checkers::support::SupportValue; +use crate::checkers::support::UnsupportedValue; +use crate::containers::HashSet; +use crate::propagation::Domains; +use crate::propagation::ReadDomains; +use crate::variables::DomainId; + +#[derive(Clone, Debug)] +pub struct BoundsConsistencyChecker { + supports: Supports, + supported_values: HashSet<(DomainId, i32)>, + + support: Support, +} + +impl BoundsConsistencyChecker { + pub fn new(supports: Supports) -> Self { + BoundsConsistencyChecker { + supports, + supported_values: HashSet::default(), + support: Support::default(), + } + } +} + +impl ConsistencyChecker for BoundsConsistencyChecker { + fn check_consistency(&mut self, scope: &Scope, mut domains: Domains<'_>) -> bool { + self.supported_values.clear(); + + for (local_id, domain) in scope.domains() { + let values_to_support = [domains.lower_bound(&domain), domains.upper_bound(&domain)]; + + for value in values_to_support { + if self.supported_values.contains(&(domain, value)) { + continue; + } + + self.supports.support( + &mut self.support, + local_id, + UnsupportedValue(value), + domains.reborrow(), + ); + + if !self.process_support(domains.reborrow()) { + return false; + } + } + } + + true + } +} + +impl BoundsConsistencyChecker { + fn process_support(&mut self, mut domains: Domains<'_>) -> bool { + // TODO: Check that the support is a solution. + if !self.supports.is_solution(&self.support) { + log::error!("Support is not a solution"); + return false; + } + + for (domain, value) in self.support.drain() { + if !value.is_in(domain, domains.reborrow()) { + log::error!("Support value is not in the domain"); + return false; + } + + if let Some(int) = value.as_int() { + let _ = self.supported_values.insert((domain, int)); + } + } + + true + } +} diff --git a/pumpkin-crates/core/src/checkers/mod.rs b/pumpkin-crates/core/src/checkers/mod.rs new file mode 100644 index 000000000..cec8ea293 --- /dev/null +++ b/pumpkin-crates/core/src/checkers/mod.rs @@ -0,0 +1,45 @@ +mod bounds_consistency; +mod scope; +mod store; +pub mod support; + +use std::fmt::Debug; + +pub use bounds_consistency::*; +use dyn_clone::DynClone; +pub use scope::*; +pub use store::*; + +use crate::propagation::Domains; + +pub trait ConsistencyChecker: Debug + DynClone { + /// Ensure the domains of all items in the scope are at the advertised consistency level. + /// + /// Returns `true` if the consistency check passes, or `false` otherwise. + fn check_consistency(&mut self, scope: &Scope, domains: Domains<'_>) -> bool; +} + +/// Wrapper around `Box` that implements [`Clone`]. +#[derive(Debug)] +pub struct BoxedConsistencyChecker(Box); + +impl Clone for BoxedConsistencyChecker { + fn clone(&self) -> Self { + BoxedConsistencyChecker(dyn_clone::clone_box(&*self.0)) + } +} + +impl From for BoxedConsistencyChecker +where + T: ConsistencyChecker + 'static, +{ + fn from(value: T) -> Self { + BoxedConsistencyChecker(Box::new(value)) + } +} + +impl BoxedConsistencyChecker { + pub fn check_consistency(&mut self, scope: &Scope, domains: Domains<'_>) -> bool { + self.0.check_consistency(scope, domains) + } +} diff --git a/pumpkin-crates/core/src/checkers/scope.rs b/pumpkin-crates/core/src/checkers/scope.rs new file mode 100644 index 000000000..0058af9fd --- /dev/null +++ b/pumpkin-crates/core/src/checkers/scope.rs @@ -0,0 +1,53 @@ +use crate::containers::HashMap; +use crate::propagation::LocalId; +use crate::variables::DomainId; + +/// The scope of a constraint is the collection of variables involved in the relation. +#[derive(Clone, Debug, Default)] +pub struct Scope { + domains: HashMap, +} + +impl Scope { + /// Add a new domain to the scope with the given local id. + /// + /// Any previous occurrance of this local id will be overridden. + pub fn add_domain(&mut self, local_id: LocalId, domain_id: DomainId) { + let _ = self.domains.insert(local_id, domain_id); + } + + /// The integer domains in the scope with the [`LocalId`]s they are registered. + pub fn domains(&self) -> impl ExactSizeIterator { + self.domains.iter().map(|(lid, did)| (*lid, *did)) + } +} + +impl From<((LocalId, &VA), (LocalId, &VB), (LocalId, &VC))> for Scope +where + VA: ScopeItem, + VB: ScopeItem, + VC: ScopeItem, +{ + fn from( + ((la, va), (lb, vb), (lc, vc)): ((LocalId, &VA), (LocalId, &VB), (LocalId, &VC)), + ) -> Self { + let mut scope = Scope::default(); + + va.add_to_scope(&mut scope, la); + vb.add_to_scope(&mut scope, lb); + vc.add_to_scope(&mut scope, lc); + + scope + } +} + +pub trait ScopeItem { + /// Adds self to the given scope with the given [`LocalId`]. + fn add_to_scope(&self, scope: &mut Scope, local_id: LocalId); +} + +impl ScopeItem for i32 { + fn add_to_scope(&self, _: &mut Scope, _: LocalId) { + // Do nothing + } +} diff --git a/pumpkin-crates/core/src/checkers/store.rs b/pumpkin-crates/core/src/checkers/store.rs new file mode 100644 index 000000000..b116cd143 --- /dev/null +++ b/pumpkin-crates/core/src/checkers/store.rs @@ -0,0 +1,74 @@ +use crate::checkers::BoxedConsistencyChecker; +use crate::checkers::Scope; +use crate::containers::KeyedBitSet; +use crate::containers::KeyedVec; +use crate::containers::StorageKey; +use crate::propagation::Domains; +use crate::variables::DomainId; + +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] +pub struct CheckerId(u32); + +impl StorageKey for CheckerId { + fn index(&self) -> usize { + self.0 as usize + } + + fn create_from_index(index: usize) -> Self { + CheckerId(index as u32) + } +} + +#[derive(Clone, Debug, Default)] +pub struct ConsistencyCheckerStore { + store: KeyedVec, + watch_list: KeyedVec>, + + queue: Vec, + enqueued: KeyedBitSet, +} + +impl ConsistencyCheckerStore { + pub fn register(&mut self, scope: Scope, checker: BoxedConsistencyChecker) { + let checker_slot = self.store.new_slot(); + + for (_, domain) in scope.domains() { + self.watch_list.accomodate(domain, vec![]); + self.watch_list[domain].push(checker_slot.key()); + } + + let _ = checker_slot.populate((scope, checker)); + } + + /// Called when the domain is modified. + /// + /// Causes the checkers for this domain to be enqueued. + pub fn on_domain_event(&mut self, domain_id: DomainId) { + let Some(list) = self.watch_list.get(domain_id) else { + return; + }; + + for &checker_id in list { + if !self.enqueued.insert(checker_id) { + continue; + } + + self.queue.push(checker_id); + } + } + + /// Run the enqueued consistency checkers. + pub fn run_enqueued(&mut self, mut domains: Domains<'_>) -> bool { + for checker_id in self.queue.drain(..) { + assert!(self.enqueued.remove(checker_id)); + + let (scope, checker) = &mut self.store[checker_id]; + + if !checker.check_consistency(scope, domains.reborrow()) { + return false; + } + } + + true + } +} diff --git a/pumpkin-crates/core/src/checkers/support.rs b/pumpkin-crates/core/src/checkers/support.rs new file mode 100644 index 000000000..72363b7eb --- /dev/null +++ b/pumpkin-crates/core/src/checkers/support.rs @@ -0,0 +1,157 @@ +use std::fmt::Debug; + +use crate::containers::HashMap; +use crate::propagation::Domains; +use crate::propagation::LocalId; +use crate::propagation::ReadDomains; +use crate::variables::DomainId; + +/// A [`SupportGenerator`] can produce [`Support`]s for values in domains. +pub trait SupportGenerator: Clone + Debug { + /// The type of value used in the support. + /// + /// Depending on how the generator is used, this may be a float or an integer. + type Value: SupportValue; + + /// Produce a support where the domain corresponding to `local_id` is assigned to `value`. + /// + /// The support is written into the `support` buffer. Implementations can assume that this + /// support is empty when this function is called. + /// + /// The support must satisfy the constraint it is supporting, and all assignments must be + /// within the domain bounds. + fn support( + &mut self, + support: &mut Support, + local_id: LocalId, + value: UnsupportedValue, + domains: Domains<'_>, + ); + + /// Returns true if the support is a solution to the constraint. + fn is_solution(&self, support: &Support) -> bool; +} + +/// A value that may be used in a [`Support`]. +pub trait SupportValue: Clone + Debug { + fn is_in(&self, domain: DomainId, domains: Domains<'_>) -> bool; + + /// If the value is an integer, we can cache it to prevent recreating supports for the same + /// value. + fn as_int(&self) -> Option; +} + +impl SupportValue for i32 { + fn is_in(&self, domain: DomainId, domains: Domains<'_>) -> bool { + domains.contains(&domain, *self) + } + + fn as_int(&self) -> Option { + Some(*self) + } +} + +impl SupportValue for f32 { + fn is_in(&self, domain: DomainId, domains: Domains<'_>) -> bool { + let lb = domains.lower_bound(&domain) as f32; + let ub = domains.upper_bound(&domain) as f32; + + lb <= *self && *self <= ub + } + + fn as_int(&self) -> Option { + if (self.round() - self).abs() < f32::EPSILON { + Some(self.round() as i32) + } else { + None + } + } +} + +/// An assignment which supports a value for a particular domain. +#[derive(Clone, Debug)] +pub struct Support { + assignment: HashMap, +} + +impl Default for Support { + fn default() -> Self { + Self { + assignment: Default::default(), + } + } +} + +impl Support { + /// Add a domain assignment to the support. + /// + /// Previous assignments of the given domain are overwritten. + pub fn with_assignment(&mut self, domain_id: DomainId, value: Value) { + let _ = self.assignment.insert(domain_id, value); + } + + /// Get the value for the given domain in this support. + /// + /// Panics if the domain is unassigned. + pub fn assignment(&self, domain_id: DomainId) -> Value { + self.assignment + .get(&domain_id) + .cloned() + .unwrap_or_else(|| panic!("could not get assignment for {domain_id}")) + } + + /// Drain the support of all its assigned domains. + /// + /// Will leave the support empty. + pub(super) fn drain(&mut self) -> impl ExactSizeIterator { + self.assignment.drain() + } +} + +/// A domain value that needs to be unpacked through [`UnpackUnsupportedValue::unpack`]. +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct UnsupportedValue(pub(crate) i32); + +pub trait UnpackUnsupportedValue { + /// Turn the unsupported value into an item in the domain of [`self`] (the variable). + fn unpack(&self, unsupported_value: UnsupportedValue) -> i32; +} + +pub trait SupportsValue: UnpackUnsupportedValue { + /// Add the assignment `self = value` to the `support`. + fn assign(&self, value: Value, support: &mut Support); + + /// Get the value from the given support. + /// + /// Called with the result of [`SupportsValue::assign`]. + /// + /// Panics if the support has no value for this variable. + fn support_value(&self, support: &Support) -> Value; +} + +impl UnpackUnsupportedValue for i32 { + fn unpack(&self, UnsupportedValue(value): UnsupportedValue) -> i32 { + assert_eq!(value, *self); + value + } +} + +impl SupportsValue for i32 { + fn assign(&self, _: i32, _: &mut Support) { + // Do nothing + } + + fn support_value(&self, _: &Support) -> i32 { + *self + } +} + +impl SupportsValue for i32 { + fn assign(&self, _: f32, _: &mut Support) { + // Do nothing + } + + fn support_value(&self, _: &Support) -> f32 { + *self as f32 + } +} diff --git a/pumpkin-crates/core/src/containers/keyed_bit_set.rs b/pumpkin-crates/core/src/containers/keyed_bit_set.rs new file mode 100644 index 000000000..c57715cf7 --- /dev/null +++ b/pumpkin-crates/core/src/containers/keyed_bit_set.rs @@ -0,0 +1,50 @@ +use std::marker::PhantomData; + +use bit_set::BitSet; + +use crate::containers::StorageKey; + +#[derive(Debug)] +pub struct KeyedBitSet { + bitset: BitSet, + key: PhantomData, +} + +impl KeyedBitSet { + /// Add the key to the set. + /// + /// Returns `true` if the set did _not_ previously contain `key`. + pub fn insert(&mut self, key: Key) -> bool { + self.bitset.insert(key.index()) + } + + /// Remove the key from the set. + /// + /// If the key was present, returns true. + pub fn remove(&mut self, key: Key) -> bool { + self.bitset.remove(key.index()) + } + + /// Get all keys in the set and remove them. + pub fn drain(&self) -> impl Iterator { + self.bitset.iter().map(Key::create_from_index) + } +} + +impl Clone for KeyedBitSet { + fn clone(&self) -> Self { + Self { + bitset: self.bitset.clone(), + key: PhantomData, + } + } +} + +impl Default for KeyedBitSet { + fn default() -> Self { + Self { + bitset: BitSet::default(), + key: PhantomData, + } + } +} diff --git a/pumpkin-crates/core/src/containers/mod.rs b/pumpkin-crates/core/src/containers/mod.rs index 39343eb44..d16b86ae7 100644 --- a/pumpkin-crates/core/src/containers/mod.rs +++ b/pumpkin-crates/core/src/containers/mod.rs @@ -1,12 +1,14 @@ //! Contains containers which are used by the solver. mod key_generator; mod key_value_heap; +mod keyed_bit_set; mod keyed_vec; mod sparse_set; use fnv::FnvBuildHasher; pub use key_generator::*; pub use key_value_heap::*; +pub use keyed_bit_set::*; pub use keyed_vec::*; pub use sparse_set::*; diff --git a/pumpkin-crates/core/src/engine/state.rs b/pumpkin-crates/core/src/engine/state.rs index 9e358fab6..e568dae34 100644 --- a/pumpkin-crates/core/src/engine/state.rs +++ b/pumpkin-crates/core/src/engine/state.rs @@ -5,6 +5,9 @@ use pumpkin_checking::InferenceChecker; #[cfg(feature = "check-propagations")] use pumpkin_checking::VariableState; +use crate::checkers::BoxedConsistencyChecker; +use crate::checkers::ConsistencyCheckerStore; +use crate::checkers::Scope; use crate::containers::HashMap; use crate::containers::KeyGenerator; use crate::create_statistics_struct; @@ -28,7 +31,7 @@ use crate::proof::InferenceCode; use crate::propagation::CurrentNogood; use crate::propagation::Domains; use crate::propagation::ExplanationContext; -#[cfg(feature = "check-propagations")] +#[cfg(any(feature = "check-propagations", feature = "check-consistency"))] use crate::propagation::InferenceCheckers; use crate::propagation::NotificationContext; use crate::propagation::PropagationContext; @@ -82,6 +85,7 @@ pub struct State { /// Inference checkers to run in the propagation loop. checkers: HashMap>>, + consistency_checkers: ConsistencyCheckerStore, } create_statistics_struct!(StateStatistics { @@ -112,6 +116,7 @@ impl Default for State { statistics: StateStatistics::default(), constraint_tags: KeyGenerator::default(), checkers: HashMap::default(), + consistency_checkers: Default::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 @@ -333,7 +338,7 @@ impl State { Constructor: PropagatorConstructor, Constructor::PropagatorImpl: 'static, { - #[cfg(feature = "check-propagations")] + #[cfg(any(feature = "check-propagations", feature = "check-consistency"))] constructor.add_inference_checkers(InferenceCheckers::new(self)); let original_handle: PropagatorHandle = @@ -376,6 +381,17 @@ impl State { let checkers = self.checkers.entry(inference_code).or_default(); checkers.push(BoxedChecker::from(checker)); } + + /// Add a consistency checker for the given constraint and scope. + pub fn add_consistency_checker( + &mut self, + _constraint_tag: ConstraintTag, + scope: impl Into, + checker: impl Into, + ) { + self.consistency_checkers + .register(scope.into(), checker.into()); + } } /// Operations for retrieving propagators. @@ -618,6 +634,9 @@ impl State { #[cfg(feature = "check-propagations")] self.check_propagations(num_trail_entries_before); + #[cfg(feature = "check-consistency")] + self.enqueue_consistency_checkers(num_trail_entries_before); + match propagation_status { Ok(_) => { // Notify other propagators of the propagations and continue. @@ -718,6 +737,16 @@ impl State { } } + #[cfg(feature = "check-consistency")] + fn enqueue_consistency_checkers(&mut 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); + + self.consistency_checkers + .on_domain_event(entry.predicate.get_domain()); + } + } + /// Performs fixed-point propagation using the propagators defined in the [`State`]. /// /// The posted [`Predicate`]s (using [`State::post`]) and added propagators (using @@ -746,6 +775,13 @@ impl State { self.propagate(propagator_id)?; } + if cfg!(feature = "check-consistency") { + assert!( + self.consistency_checkers + .run_enqueued(Domains::new(&self.assignments, &mut self.trailed_values)) + ); + } + // Only check fixed point propagation if there was no reported conflict, // since otherwise the state may be inconsistent. pumpkin_assert_extreme!(DebugHelper::debug_fixed_point_propagation( diff --git a/pumpkin-crates/core/src/engine/variables/affine_view.rs b/pumpkin-crates/core/src/engine/variables/affine_view.rs index 404184e57..daf56fd59 100644 --- a/pumpkin-crates/core/src/engine/variables/affine_view.rs +++ b/pumpkin-crates/core/src/engine/variables/affine_view.rs @@ -5,6 +5,12 @@ use pumpkin_checking::CheckerVariable; use pumpkin_checking::IntExt; use super::TransformableVariable; +use crate::checkers::Scope; +use crate::checkers::ScopeItem; +use crate::checkers::support::Support; +use crate::checkers::support::SupportsValue; +use crate::checkers::support::UnpackUnsupportedValue; +use crate::checkers::support::UnsupportedValue; use crate::engine::Assignments; use crate::engine::notifications::DomainEvent; use crate::engine::notifications::OpaqueDomainEvent; @@ -14,6 +20,7 @@ use crate::engine::predicates::predicate_constructor::PredicateConstructor; use crate::engine::variables::DomainId; use crate::engine::variables::IntegerVariable; use crate::math::num_ext::NumExt; +use crate::propagation::LocalId; /// Models the constraint `y = ax + b`, by expressing the domain of `y` as a transformation of the /// domain of `x`. @@ -46,6 +53,13 @@ impl AffineView { match rounding { Rounding::Up => ::div_ceil(inverted_translation, self.scale), Rounding::Down => ::div_floor(inverted_translation, self.scale), + Rounding::None => { + if inverted_translation % self.scale == 0 { + inverted_translation / self.scale + } else { + panic!("do not want to round but cannot unscale") + } + } } } @@ -54,6 +68,50 @@ impl AffineView { } } +impl ScopeItem for AffineView { + fn add_to_scope(&self, scope: &mut Scope, local_id: LocalId) { + self.inner.add_to_scope(scope, local_id); + } +} + +impl UnpackUnsupportedValue for AffineView +where + Inner: UnpackUnsupportedValue, +{ + fn unpack(&self, unsupported_value: UnsupportedValue) -> i32 { + self.map(self.inner.unpack(unsupported_value)) + } +} + +impl SupportsValue for AffineView +where + Inner: SupportsValue, +{ + fn assign(&self, value: i32, support: &mut Support) { + let value = self.invert(value, Rounding::None); + self.inner.assign(value, support); + } + + fn support_value(&self, support: &Support) -> i32 { + self.map(self.inner.support_value(support)) + } +} + +impl SupportsValue for AffineView +where + Inner: SupportsValue, +{ + fn assign(&self, value: f32, support: &mut Support) { + let inverted_translation = value - self.offset as f32; + let value = inverted_translation / self.scale as f32; + self.inner.assign(value, support); + } + + fn support_value(&self, support: &Support) -> f32 { + self.scale as f32 * self.inner.support_value(support) + self.offset as f32 + } +} + impl CheckerVariable for AffineView { fn does_atomic_constrain_self(&self, atomic: &Predicate) -> bool { self.inner.does_atomic_constrain_self(atomic) @@ -404,6 +462,7 @@ impl From for AffineView { enum Rounding { Up, Down, + None, } #[cfg(test)] diff --git a/pumpkin-crates/core/src/engine/variables/domain_id.rs b/pumpkin-crates/core/src/engine/variables/domain_id.rs index 22ede3a40..9a1a02af7 100644 --- a/pumpkin-crates/core/src/engine/variables/domain_id.rs +++ b/pumpkin-crates/core/src/engine/variables/domain_id.rs @@ -2,6 +2,12 @@ use enumset::EnumSet; use pumpkin_checking::CheckerVariable; use super::TransformableVariable; +use crate::checkers::Scope; +use crate::checkers::ScopeItem; +use crate::checkers::support::Support; +use crate::checkers::support::SupportsValue; +use crate::checkers::support::UnpackUnsupportedValue; +use crate::checkers::support::UnsupportedValue; use crate::containers::StorageKey; use crate::engine::Assignments; use crate::engine::notifications::DomainEvent; @@ -12,6 +18,7 @@ use crate::engine::variables::IntegerVariable; use crate::predicates::Predicate; use crate::predicates::PredicateConstructor; use crate::predicates::PredicateType; +use crate::propagation::LocalId; use crate::pumpkin_assert_simple; /// A structure which represents the most basic [`IntegerVariable`]; it is simply the id which links @@ -32,6 +39,38 @@ impl DomainId { } } +impl ScopeItem for DomainId { + fn add_to_scope(&self, scope: &mut Scope, local_id: LocalId) { + scope.add_domain(local_id, *self); + } +} + +impl UnpackUnsupportedValue for DomainId { + fn unpack(&self, UnsupportedValue(value): UnsupportedValue) -> i32 { + value + } +} + +impl SupportsValue for DomainId { + fn assign(&self, value: i32, support: &mut Support) { + support.with_assignment(*self, value); + } + + fn support_value(&self, support: &Support) -> i32 { + support.assignment(*self) + } +} + +impl SupportsValue for DomainId { + fn assign(&self, value: f32, support: &mut Support) { + support.with_assignment(*self, value); + } + + fn support_value(&self, support: &Support) -> f32 { + support.assignment(*self) + } +} + impl CheckerVariable for DomainId { fn does_atomic_constrain_self(&self, atomic: &Predicate) -> bool { atomic.get_domain() == *self diff --git a/pumpkin-crates/core/src/engine/variables/integer_variable.rs b/pumpkin-crates/core/src/engine/variables/integer_variable.rs index 09badb92e..fd6993292 100644 --- a/pumpkin-crates/core/src/engine/variables/integer_variable.rs +++ b/pumpkin-crates/core/src/engine/variables/integer_variable.rs @@ -4,6 +4,7 @@ use enumset::EnumSet; use pumpkin_checking::CheckerVariable; use super::TransformableVariable; +use crate::checkers::ScopeItem; use crate::engine::Assignments; use crate::engine::notifications::DomainEvent; use crate::engine::notifications::OpaqueDomainEvent; @@ -19,6 +20,7 @@ pub trait IntegerVariable: + TransformableVariable + Debug + CheckerVariable + + ScopeItem { type AffineView: IntegerVariable; diff --git a/pumpkin-crates/core/src/engine/variables/literal.rs b/pumpkin-crates/core/src/engine/variables/literal.rs index 980ffa737..b59ac06c1 100644 --- a/pumpkin-crates/core/src/engine/variables/literal.rs +++ b/pumpkin-crates/core/src/engine/variables/literal.rs @@ -8,6 +8,10 @@ use pumpkin_checking::VariableState; use super::DomainId; use super::IntegerVariable; use super::TransformableVariable; +use crate::checkers::Scope; +use crate::checkers::ScopeItem; +use crate::checkers::support::UnpackUnsupportedValue; +use crate::checkers::support::UnsupportedValue; use crate::engine::Assignments; use crate::engine::notifications::DomainEvent; use crate::engine::notifications::OpaqueDomainEvent; @@ -15,6 +19,7 @@ use crate::engine::notifications::Watchers; use crate::engine::predicates::predicate::Predicate; use crate::engine::predicates::predicate_constructor::PredicateConstructor; use crate::engine::variables::AffineView; +use crate::propagation::LocalId; #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub struct Literal { @@ -73,6 +78,18 @@ macro_rules! forward { } } +impl ScopeItem for Literal { + fn add_to_scope(&self, scope: &mut Scope, local_id: LocalId) { + self.integer_variable.add_to_scope(scope, local_id); + } +} + +impl UnpackUnsupportedValue for Literal { + fn unpack(&self, unsupported_value: UnsupportedValue) -> i32 { + self.integer_variable.unpack(unsupported_value) + } +} + 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); diff --git a/pumpkin-crates/core/src/lib.rs b/pumpkin-crates/core/src/lib.rs index 2a8680544..19d6fcefb 100644 --- a/pumpkin-crates/core/src/lib.rs +++ b/pumpkin-crates/core/src/lib.rs @@ -12,6 +12,7 @@ use crate::branching::Brancher; use crate::termination::TerminationCondition; pub mod branching; +pub mod checkers; pub mod conflict_resolving; pub mod constraints; pub mod optimisation; diff --git a/pumpkin-crates/core/src/propagation/constructor.rs b/pumpkin-crates/core/src/propagation/constructor.rs index 791627880..e93c61aa0 100644 --- a/pumpkin-crates/core/src/propagation/constructor.rs +++ b/pumpkin-crates/core/src/propagation/constructor.rs @@ -12,6 +12,8 @@ use super::PropagatorVarId; use crate::Solver; use crate::basic_types::PredicateId; use crate::basic_types::RefOrOwned; +use crate::checkers::BoxedConsistencyChecker; +use crate::checkers::Scope; use crate::engine::Assignments; use crate::engine::State; use crate::engine::TrailedValues; @@ -21,6 +23,7 @@ use crate::engine::variables::AffineView; #[cfg(doc)] use crate::engine::variables::DomainId; use crate::predicates::Predicate; +use crate::proof::ConstraintTag; use crate::proof::InferenceCode; #[cfg(doc)] use crate::propagation::DomainEvent; @@ -59,7 +62,7 @@ pub struct InferenceCheckers<'state> { } impl<'state> InferenceCheckers<'state> { - #[cfg(feature = "check-propagations")] + #[cfg(any(feature = "check-propagations", feature = "check-consistency"))] pub(crate) fn new(state: &'state mut State) -> Self { InferenceCheckers { state, @@ -69,6 +72,17 @@ impl<'state> InferenceCheckers<'state> { } impl InferenceCheckers<'_> { + /// Add a consistency checker for the given constraint and scope. + pub fn add_consistency_checker( + &mut self, + constraint_tag: ConstraintTag, + scope: impl Into, + checker: impl Into, + ) { + self.state + .add_consistency_checker(constraint_tag, scope, checker); + } + /// Forwards to [`State::add_inference_checker`]. pub fn add_inference_checker( &mut self, diff --git a/pumpkin-crates/propagators/src/propagators/arithmetic/multiplication/checker.rs b/pumpkin-crates/propagators/src/propagators/arithmetic/multiplication/checker.rs new file mode 100644 index 000000000..149400f52 --- /dev/null +++ b/pumpkin-crates/propagators/src/propagators/arithmetic/multiplication/checker.rs @@ -0,0 +1,152 @@ +use pumpkin_checking::AtomicConstraint; +use pumpkin_checking::CheckerVariable; +use pumpkin_checking::InferenceChecker; +use pumpkin_core::checkers::support::Support; +use pumpkin_core::checkers::support::SupportGenerator; +use pumpkin_core::checkers::support::SupportsValue; +use pumpkin_core::checkers::support::UnsupportedValue; +use pumpkin_core::propagation::Domains; +use pumpkin_core::propagation::LocalId; +use pumpkin_core::propagation::ReadDomains; +use pumpkin_core::variables::IntegerVariable; + +#[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 + } +} + +impl SupportGenerator for IntegerMultiplicationChecker +where + VA: IntegerVariable + SupportsValue, + VB: IntegerVariable + SupportsValue, + VC: IntegerVariable + SupportsValue, +{ + type Value = f32; + + fn support( + &mut self, + support: &mut Support, + local_id: LocalId, + unsupported_value: UnsupportedValue, + domains: Domains<'_>, + ) { + let a_min = domains.lower_bound(&self.a) as f32; + let a_max = domains.upper_bound(&self.a) as f32; + let b_min = domains.lower_bound(&self.b) as f32; + let b_max = domains.upper_bound(&self.b) as f32; + let c_min = domains.lower_bound(&self.c) as f32; + let c_max = domains.upper_bound(&self.c) as f32; + + dbg!((&self.a, &self.b, &self.c)); + dbg!((a_min, a_max)); + dbg!((b_min, b_max)); + dbg!((c_min, c_max)); + + let (value_a, value_b, value_c) = match local_id { + super::ID_A => { + let value_a = self.a.unpack(unsupported_value) as f32; + + let Some((value_b, value_c)) = [b_min, b_max].into_iter().find_map(|value_b| { + let value_c = value_a * value_b; + dbg!((value_a, value_b, value_c)); + if c_min <= value_c && value_c <= c_max { + Some((value_b, value_c)) + } else { + None + } + }) else { + return; + }; + + (value_a, value_b, value_c) + } + super::ID_B => { + let value_b = self.b.unpack(unsupported_value) as f32; + + let Some((value_a, value_c)) = [a_min, a_max].into_iter().find_map(|value_a| { + let value_c = value_a * value_b; + dbg!((value_a, value_b, value_c)); + if c_min <= value_c && value_c <= c_max { + Some((value_a, value_c)) + } else { + None + } + }) else { + return; + }; + + (value_a, value_b, value_c) + } + super::ID_C => { + let value_c = self.c.unpack(unsupported_value) as f32; + + let Some(values) = [a_min, a_max].into_iter().find_map(|value_a| { + let value_b = value_c / value_a; + dbg!((value_a, value_b, value_c)); + + if b_min <= value_b && value_b <= b_max { + Some((value_a, value_b, value_c)) + } else { + None + } + }) else { + return; + }; + + values + } + + _ => unreachable!(), + }; + + + self.a.assign(value_a, support); + self.b.assign(value_b, support); + self.c.assign(value_c, support); + } + + fn is_solution(&self, support: &Support) -> bool { + self.a.support_value(support) * self.b.support_value(support) + == self.c.support_value(support) + } +} diff --git a/pumpkin-crates/propagators/src/propagators/arithmetic/multiplication/constructor.rs b/pumpkin-crates/propagators/src/propagators/arithmetic/multiplication/constructor.rs index 14715dedd..25ac1e8f6 100644 --- a/pumpkin-crates/propagators/src/propagators/arithmetic/multiplication/constructor.rs +++ b/pumpkin-crates/propagators/src/propagators/arithmetic/multiplication/constructor.rs @@ -1,3 +1,5 @@ +use pumpkin_core::checkers::BoundsConsistencyChecker; +use pumpkin_core::checkers::support::SupportsValue; use pumpkin_core::proof::ConstraintTag; use pumpkin_core::proof::InferenceCode; use pumpkin_core::propagation::DomainEvents; @@ -8,7 +10,7 @@ use pumpkin_core::variables::IntegerVariable; use crate::arithmetic::IntegerMultiplicationPropagator; use crate::arithmetic::multiplication::IntegerMultiplication; -use crate::arithmetic::multiplication::inference_checker::IntegerMultiplicationChecker; +use crate::arithmetic::multiplication::checker::IntegerMultiplicationChecker; /// The [`PropagatorConstructor`] for [`IntegerMultiplicationPropagator`]. #[derive(Clone, Debug)] @@ -21,9 +23,9 @@ pub struct IntegerMultiplicationArgs { impl PropagatorConstructor for IntegerMultiplicationArgs where - VA: IntegerVariable + 'static, - VB: IntegerVariable + 'static, - VC: IntegerVariable + 'static, + VA: IntegerVariable + SupportsValue + 'static, + VB: IntegerVariable + SupportsValue + 'static, + VC: IntegerVariable + SupportsValue + 'static, { type PropagatorImpl = IntegerMultiplicationPropagator; @@ -39,7 +41,11 @@ where checkers.add_consistency_checker( self.constraint_tag, - [&self.a, &self.b, &self.c], + ( + (super::ID_A, &self.a), + (super::ID_B, &self.b), + (super::ID_C, &self.c), + ), BoundsConsistencyChecker::new(IntegerMultiplicationChecker { a: self.a.clone(), b: self.b.clone(), diff --git a/pumpkin-crates/propagators/src/propagators/arithmetic/multiplication/inference_checker.rs b/pumpkin-crates/propagators/src/propagators/arithmetic/multiplication/inference_checker.rs deleted file mode 100644 index 79ad39d8e..000000000 --- a/pumpkin-crates/propagators/src/propagators/arithmetic/multiplication/inference_checker.rs +++ /dev/null @@ -1,48 +0,0 @@ -use pumpkin_checking::AtomicConstraint; -use pumpkin_checking::CheckerVariable; -use pumpkin_checking::InferenceChecker; - -#[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 - } -} diff --git a/pumpkin-crates/propagators/src/propagators/arithmetic/multiplication/mod.rs b/pumpkin-crates/propagators/src/propagators/arithmetic/multiplication/mod.rs index f0bbf8855..afef4b2bb 100644 --- a/pumpkin-crates/propagators/src/propagators/arithmetic/multiplication/mod.rs +++ b/pumpkin-crates/propagators/src/propagators/arithmetic/multiplication/mod.rs @@ -1,7 +1,8 @@ +mod checker; mod constructor; -mod inference_checker; mod propagator; +pub use checker::*; pub use constructor::*; pub use propagator::*; use pumpkin_core::declare_inference_label; From f7c9b6934e821c9613da52c24b3e427053d2e0cb Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Wed, 20 May 2026 13:51:15 +1000 Subject: [PATCH 03/23] Fix multiline test command syntax --- .github/workflows/ci.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 27dc0aee3..6e243b506 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -27,12 +27,12 @@ jobs: key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} - uses: dtolnay/rust-toolchain@stable - run: | - cargo test \ - --release \ - --no-fail-fast \ - --features pumpkin-solver/check-propagations \ - --features pumpkin-core/check-consistency \ - --features pumpkin-core/check-deductions + cargo test \ + --release \ + --no-fail-fast \ + --features pumpkin-solver/check-propagations \ + --features pumpkin-core/check-consistency \ + --features pumpkin-core/check-deductions wasm-test: name: Test Suite for pumpkin-core in WebAssembly From 11511e23d36a3bfa34aef1beb4ee3e79d1c10895 Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Wed, 20 May 2026 15:24:51 +1000 Subject: [PATCH 04/23] Implement domain consistency checker --- pumpkin-crates/core/src/checkers/mod.rs | 4 +- pumpkin-crates/core/src/checkers/scope.rs | 37 +++--- ...s_consistency.rs => strong_consistency.rs} | 34 +++-- pumpkin-crates/core/src/checkers/support.rs | 2 +- .../src/engine/variables/integer_variable.rs | 2 + .../core/src/engine/variables/literal.rs | 12 ++ .../src/propagators/arithmetic/binary/mod.rs | 2 - .../arithmetic/binary_equals/checker.rs | 81 ++++++++++++ .../arithmetic/binary_equals/constructor.rs | 78 ++++++++++++ .../arithmetic/binary_equals/mod.rs | 14 ++ .../propagator.rs} | 120 ++---------------- .../src/propagators/arithmetic/mod.rs | 2 + .../arithmetic/multiplication/checker.rs | 7 +- .../arithmetic/multiplication/constructor.rs | 16 ++- 14 files changed, 259 insertions(+), 152 deletions(-) rename pumpkin-crates/core/src/checkers/{bounds_consistency.rs => strong_consistency.rs} (64%) create mode 100644 pumpkin-crates/propagators/src/propagators/arithmetic/binary_equals/checker.rs create mode 100644 pumpkin-crates/propagators/src/propagators/arithmetic/binary_equals/constructor.rs create mode 100644 pumpkin-crates/propagators/src/propagators/arithmetic/binary_equals/mod.rs rename pumpkin-crates/propagators/src/propagators/arithmetic/{binary/binary_equals.rs => binary_equals/propagator.rs} (81%) diff --git a/pumpkin-crates/core/src/checkers/mod.rs b/pumpkin-crates/core/src/checkers/mod.rs index cec8ea293..d93bae97f 100644 --- a/pumpkin-crates/core/src/checkers/mod.rs +++ b/pumpkin-crates/core/src/checkers/mod.rs @@ -1,14 +1,14 @@ -mod bounds_consistency; mod scope; mod store; +mod strong_consistency; pub mod support; use std::fmt::Debug; -pub use bounds_consistency::*; use dyn_clone::DynClone; pub use scope::*; pub use store::*; +pub use strong_consistency::*; use crate::propagation::Domains; diff --git a/pumpkin-crates/core/src/checkers/scope.rs b/pumpkin-crates/core/src/checkers/scope.rs index 0058af9fd..c15ce436b 100644 --- a/pumpkin-crates/core/src/checkers/scope.rs +++ b/pumpkin-crates/core/src/checkers/scope.rs @@ -22,25 +22,28 @@ impl Scope { } } -impl From<((LocalId, &VA), (LocalId, &VB), (LocalId, &VC))> for Scope -where - VA: ScopeItem, - VB: ScopeItem, - VC: ScopeItem, -{ - fn from( - ((la, va), (lb, vb), (lc, vc)): ((LocalId, &VA), (LocalId, &VB), (LocalId, &VC)), - ) -> Self { - let mut scope = Scope::default(); - - va.add_to_scope(&mut scope, la); - vb.add_to_scope(&mut scope, lb); - vc.add_to_scope(&mut scope, lc); - - scope - } +macro_rules! impl_scope_from_tuple { + ($($lid_name:ident,$var_name:ident : $ty_name:ident),+) => { + impl<$($ty_name),+> From<($((LocalId, &$ty_name)),+)> for Scope + where + $($ty_name: ScopeItem),+ + { + fn from( + ($(($lid_name, $var_name)),+): ($((LocalId, &$ty_name)),+), + ) -> Self { + let mut scope = Scope::default(); + + $($var_name.add_to_scope(&mut scope, $lid_name);)+ + + scope + } + } + }; } +impl_scope_from_tuple!(la,va: VA, lb,vb: VB); +impl_scope_from_tuple!(la,va: VA, lb,vb: VB, lc,vc: VC); + pub trait ScopeItem { /// Adds self to the given scope with the given [`LocalId`]. fn add_to_scope(&self, scope: &mut Scope, local_id: LocalId); diff --git a/pumpkin-crates/core/src/checkers/bounds_consistency.rs b/pumpkin-crates/core/src/checkers/strong_consistency.rs similarity index 64% rename from pumpkin-crates/core/src/checkers/bounds_consistency.rs rename to pumpkin-crates/core/src/checkers/strong_consistency.rs index 97a3ccd0a..291b16d40 100644 --- a/pumpkin-crates/core/src/checkers/bounds_consistency.rs +++ b/pumpkin-crates/core/src/checkers/strong_consistency.rs @@ -9,17 +9,26 @@ use crate::propagation::Domains; use crate::propagation::ReadDomains; use crate::variables::DomainId; +/// The consistency level advertised by the propagator. +#[derive(Clone, Copy, Debug)] +pub enum StrongConsistency { + Domain, + Bounds, +} + #[derive(Clone, Debug)] -pub struct BoundsConsistencyChecker { +pub struct StrongConsistencyChecker { supports: Supports, supported_values: HashSet<(DomainId, i32)>, + consistency_level: StrongConsistency, support: Support, } -impl BoundsConsistencyChecker { - pub fn new(supports: Supports) -> Self { - BoundsConsistencyChecker { +impl StrongConsistencyChecker { + pub fn new(consistency_level: StrongConsistency, supports: Supports) -> Self { + StrongConsistencyChecker { + consistency_level, supports, supported_values: HashSet::default(), support: Support::default(), @@ -27,12 +36,22 @@ impl BoundsConsistencyChecker { } } -impl ConsistencyChecker for BoundsConsistencyChecker { +impl ConsistencyChecker for StrongConsistencyChecker { fn check_consistency(&mut self, scope: &Scope, mut domains: Domains<'_>) -> bool { self.supported_values.clear(); for (local_id, domain) in scope.domains() { - let values_to_support = [domains.lower_bound(&domain), domains.upper_bound(&domain)]; + let values_to_support = match self.consistency_level { + StrongConsistency::Domain => itertools::Either::Left( + domains + .iterate_domain(&domain) + .collect::>() + .into_iter(), + ), + StrongConsistency::Bounds => itertools::Either::Right( + [domains.lower_bound(&domain), domains.upper_bound(&domain)].into_iter(), + ), + }; for value in values_to_support { if self.supported_values.contains(&(domain, value)) { @@ -56,9 +75,8 @@ impl ConsistencyChecker for BoundsConsistencyChecker } } -impl BoundsConsistencyChecker { +impl StrongConsistencyChecker { fn process_support(&mut self, mut domains: Domains<'_>) -> bool { - // TODO: Check that the support is a solution. if !self.supports.is_solution(&self.support) { log::error!("Support is not a solution"); return false; diff --git a/pumpkin-crates/core/src/checkers/support.rs b/pumpkin-crates/core/src/checkers/support.rs index 72363b7eb..2b8913d23 100644 --- a/pumpkin-crates/core/src/checkers/support.rs +++ b/pumpkin-crates/core/src/checkers/support.rs @@ -117,7 +117,7 @@ pub trait UnpackUnsupportedValue { fn unpack(&self, unsupported_value: UnsupportedValue) -> i32; } -pub trait SupportsValue: UnpackUnsupportedValue { +pub trait SupportsValue: UnpackUnsupportedValue { /// Add the assignment `self = value` to the `support`. fn assign(&self, value: Value, support: &mut Support); diff --git a/pumpkin-crates/core/src/engine/variables/integer_variable.rs b/pumpkin-crates/core/src/engine/variables/integer_variable.rs index fd6993292..1ed710e61 100644 --- a/pumpkin-crates/core/src/engine/variables/integer_variable.rs +++ b/pumpkin-crates/core/src/engine/variables/integer_variable.rs @@ -5,6 +5,7 @@ use pumpkin_checking::CheckerVariable; use super::TransformableVariable; use crate::checkers::ScopeItem; +use crate::checkers::support::SupportsValue; use crate::engine::Assignments; use crate::engine::notifications::DomainEvent; use crate::engine::notifications::OpaqueDomainEvent; @@ -21,6 +22,7 @@ pub trait IntegerVariable: + Debug + CheckerVariable + ScopeItem + + SupportsValue { type AffineView: IntegerVariable; diff --git a/pumpkin-crates/core/src/engine/variables/literal.rs b/pumpkin-crates/core/src/engine/variables/literal.rs index b59ac06c1..a2c4a88e0 100644 --- a/pumpkin-crates/core/src/engine/variables/literal.rs +++ b/pumpkin-crates/core/src/engine/variables/literal.rs @@ -10,6 +10,8 @@ use super::IntegerVariable; use super::TransformableVariable; use crate::checkers::Scope; use crate::checkers::ScopeItem; +use crate::checkers::support::Support; +use crate::checkers::support::SupportsValue; use crate::checkers::support::UnpackUnsupportedValue; use crate::checkers::support::UnsupportedValue; use crate::engine::Assignments; @@ -90,6 +92,16 @@ impl UnpackUnsupportedValue for Literal { } } +impl SupportsValue for Literal { + fn assign(&self, value: i32, support: &mut Support) { + self.integer_variable.assign(value, support) + } + + fn support_value(&self, support: &Support) -> i32 { + self.integer_variable.support_value(support) + } +} + 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); diff --git a/pumpkin-crates/propagators/src/propagators/arithmetic/binary/mod.rs b/pumpkin-crates/propagators/src/propagators/arithmetic/binary/mod.rs index 8d3c6bbb8..39cfb5e78 100644 --- a/pumpkin-crates/propagators/src/propagators/arithmetic/binary/mod.rs +++ b/pumpkin-crates/propagators/src/propagators/arithmetic/binary/mod.rs @@ -1,5 +1,3 @@ -pub(crate) mod binary_equals; pub(crate) mod binary_not_equals; -pub use binary_equals::*; pub use binary_not_equals::*; diff --git a/pumpkin-crates/propagators/src/propagators/arithmetic/binary_equals/checker.rs b/pumpkin-crates/propagators/src/propagators/arithmetic/binary_equals/checker.rs new file mode 100644 index 000000000..11072e37e --- /dev/null +++ b/pumpkin-crates/propagators/src/propagators/arithmetic/binary_equals/checker.rs @@ -0,0 +1,81 @@ +use pumpkin_checking::AtomicConstraint; +use pumpkin_checking::CheckerVariable; +use pumpkin_checking::InferenceChecker; +use pumpkin_checking::IntExt; +use pumpkin_core::checkers::support::Support; +use pumpkin_core::checkers::support::SupportGenerator; +use pumpkin_core::checkers::support::SupportsValue; +use pumpkin_core::checkers::support::UnsupportedValue; +use pumpkin_core::propagation::Domains; +use pumpkin_core::propagation::LocalId; +use pumpkin_core::variables::IntegerVariable; + +#[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 IntExt::Int(value) = self.rhs.induced_upper_bound(&state) { + let atomic = self.lhs.atomic_less_than(value); + consistent &= state.apply(&atomic); + } + + if let IntExt::Int(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 + } +} + +impl SupportGenerator for BinaryEqualsChecker +where + Lhs: IntegerVariable + SupportsValue, + Rhs: IntegerVariable + SupportsValue, +{ + type Value = i32; + + fn support( + &mut self, + support: &mut Support, + local_id: LocalId, + value: UnsupportedValue, + _: Domains<'_>, + ) { + let value = match local_id { + super::ID_LHS => self.lhs.unpack(value), + super::ID_RHS => self.rhs.unpack(value), + _ => unreachable!(), + }; + + self.lhs.assign(value, support); + self.rhs.assign(value, support); + } + + fn is_solution(&self, support: &Support) -> bool { + self.lhs.support_value(support) == self.rhs.support_value(support) + } +} diff --git a/pumpkin-crates/propagators/src/propagators/arithmetic/binary_equals/constructor.rs b/pumpkin-crates/propagators/src/propagators/arithmetic/binary_equals/constructor.rs new file mode 100644 index 000000000..481b6ae0a --- /dev/null +++ b/pumpkin-crates/propagators/src/propagators/arithmetic/binary_equals/constructor.rs @@ -0,0 +1,78 @@ +use pumpkin_core::checkers::StrongConsistency; +use pumpkin_core::checkers::StrongConsistencyChecker; +use pumpkin_core::checkers::support::SupportsValue; +use pumpkin_core::containers::HashSet; +use pumpkin_core::predicates::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::PropagatorConstructor; +use pumpkin_core::propagation::PropagatorConstructorContext; +use pumpkin_core::variables::IntegerVariable; + +use crate::arithmetic::BinaryEqualsChecker; +use crate::arithmetic::BinaryEqualsPropagator; + +/// The [`PropagatorConstructor`] for the [`BinaryEqualsPropagator`]. +#[derive(Clone, Debug)] +pub struct BinaryEqualsPropagatorArgs { + pub a: AVar, + pub b: BVar, + pub constraint_tag: ConstraintTag, +} + +impl PropagatorConstructor for BinaryEqualsPropagatorArgs +where + AVar: IntegerVariable + SupportsValue + 'static, + BVar: IntegerVariable + SupportsValue + 'static, +{ + type PropagatorImpl = BinaryEqualsPropagator; + + fn add_inference_checkers(&self, mut checkers: InferenceCheckers<'_>) { + checkers.add_inference_checker( + InferenceCode::new(self.constraint_tag, super::BinaryEquals), + Box::new(BinaryEqualsChecker { + lhs: self.a.clone(), + rhs: self.b.clone(), + }), + ); + + checkers.add_consistency_checker( + self.constraint_tag, + ((super::ID_LHS, &self.a), (super::ID_RHS, &self.b)), + StrongConsistencyChecker::new( + StrongConsistency::Domain, + BinaryEqualsChecker { + lhs: self.a.clone(), + rhs: self.b.clone(), + }, + ), + ); + } + + fn create(self, mut context: PropagatorConstructorContext) -> Self::PropagatorImpl { + let BinaryEqualsPropagatorArgs { + a, + b, + constraint_tag, + } = self; + + context.register(a.clone(), DomainEvents::ANY_INT, super::ID_LHS); + context.register(b.clone(), DomainEvents::ANY_INT, super::ID_RHS); + + BinaryEqualsPropagator { + a, + b, + + a_removed_values: HashSet::default(), + b_removed_values: HashSet::default(), + + inference_code: InferenceCode::new(constraint_tag, super::BinaryEquals), + + has_backtracked: false, + first_propagation_loop: true, + reason: Predicate::trivially_false(), + } + } +} diff --git a/pumpkin-crates/propagators/src/propagators/arithmetic/binary_equals/mod.rs b/pumpkin-crates/propagators/src/propagators/arithmetic/binary_equals/mod.rs new file mode 100644 index 000000000..bc6e585ef --- /dev/null +++ b/pumpkin-crates/propagators/src/propagators/arithmetic/binary_equals/mod.rs @@ -0,0 +1,14 @@ +mod checker; +mod constructor; +mod propagator; + +pub use checker::*; +pub use constructor::*; +pub use propagator::*; +use pumpkin_core::declare_inference_label; +use pumpkin_core::propagation::LocalId; + +const ID_LHS: LocalId = LocalId::from(0); +const ID_RHS: LocalId = LocalId::from(1); + +declare_inference_label!(BinaryEquals); diff --git a/pumpkin-crates/propagators/src/propagators/arithmetic/binary/binary_equals.rs b/pumpkin-crates/propagators/src/propagators/arithmetic/binary_equals/propagator.rs similarity index 81% rename from pumpkin-crates/propagators/src/propagators/arithmetic/binary/binary_equals.rs rename to pumpkin-crates/propagators/src/propagators/arithmetic/binary_equals/propagator.rs index b95b86b46..166af6a37 100644 --- a/pumpkin-crates/propagators/src/propagators/arithmetic/binary/binary_equals.rs +++ b/pumpkin-crates/propagators/src/propagators/arithmetic/binary_equals/propagator.rs @@ -2,26 +2,18 @@ use std::slice; use bitfield_struct::bitfield; -use pumpkin_checking::AtomicConstraint; -use pumpkin_checking::CheckerVariable; -use pumpkin_checking::InferenceChecker; -use pumpkin_checking::IntExt; use pumpkin_core::asserts::pumpkin_assert_advanced; use pumpkin_core::conjunction; use pumpkin_core::containers::HashSet; -use pumpkin_core::declare_inference_label; use pumpkin_core::predicate; use pumpkin_core::predicates::Predicate; use pumpkin_core::predicates::PredicateConstructor; use pumpkin_core::predicates::PredicateType; -use pumpkin_core::proof::ConstraintTag; use pumpkin_core::proof::InferenceCode; use pumpkin_core::propagation::DomainEvent; -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::LazyExplanation; use pumpkin_core::propagation::LocalId; use pumpkin_core::propagation::NotificationContext; @@ -29,99 +21,44 @@ use pumpkin_core::propagation::OpaqueDomainEvent; use pumpkin_core::propagation::Priority; use pumpkin_core::propagation::PropagationContext; use pumpkin_core::propagation::Propagator; -use pumpkin_core::propagation::PropagatorConstructor; -use pumpkin_core::propagation::PropagatorConstructorContext; use pumpkin_core::propagation::ReadDomains; use pumpkin_core::state::EmptyDomainConflict; use pumpkin_core::state::PropagationStatusCP; use pumpkin_core::state::PropagatorConflict; use pumpkin_core::variables::IntegerVariable; -declare_inference_label!(BinaryEquals); - -/// The [`PropagatorConstructor`] for the [`BinaryEqualsPropagator`]. -#[derive(Clone, Debug)] -pub struct BinaryEqualsPropagatorArgs { - pub a: AVar, - pub b: BVar, - pub constraint_tag: ConstraintTag, -} - -impl PropagatorConstructor for BinaryEqualsPropagatorArgs -where - AVar: IntegerVariable + 'static, - BVar: IntegerVariable + 'static, -{ - 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, - b, - constraint_tag, - } = self; - - context.register(a.clone(), DomainEvents::ANY_INT, LocalId::from(0)); - context.register(b.clone(), DomainEvents::ANY_INT, LocalId::from(1)); - - BinaryEqualsPropagator { - a, - b, - - a_removed_values: HashSet::default(), - b_removed_values: HashSet::default(), - - inference_code: InferenceCode::new(constraint_tag, BinaryEquals), - - has_backtracked: false, - first_propagation_loop: true, - reason: Predicate::trivially_false(), - } - } -} - /// Propagator for the constraint `a = b`. #[derive(Clone, Debug)] pub struct BinaryEqualsPropagator { - a: AVar, - b: BVar, + pub(super) a: AVar, + pub(super) b: BVar, /// The removed value from [`Self::a`]. /// /// These are tracked to make sure that they are also removed from [`Self::b`]. - a_removed_values: HashSet, + pub(super) a_removed_values: HashSet, /// The removed value from [`Self::b`] /// /// These are tracked to make sure that they are also removed from [`Self::a`]. - b_removed_values: HashSet, + pub(super) b_removed_values: HashSet, /// If a backtrack has occurred which caused one of the removals to be backtracked then we need /// to ensure that we do not erroneously remove values which are now part of the domain after /// backtracking. - has_backtracked: bool, + pub(super) has_backtracked: bool, /// If it is the first time that the propagator is called then we need to ensure that the /// domains of [`Self::a`] and [`Self::b`] are equal to the intersection of these domains. - first_propagation_loop: bool, + pub(super) first_propagation_loop: bool, - inference_code: InferenceCode, + pub(super) inference_code: InferenceCode, /// A re-usable buffer to store the explanations of propagations. This will always be a single /// [`Predicate`]. /// /// This field is only written to in the `lazy_explanation` function, as that returns a slice /// which needs to be owned somewhere. Hence we put that ownership here. - reason: Predicate, + pub(super) reason: Predicate, } impl BinaryEqualsPropagator @@ -401,47 +338,6 @@ 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 IntExt::Int(value) = self.rhs.induced_upper_bound(&state) { - let atomic = self.lhs.atomic_less_than(value); - consistent &= state.apply(&atomic); - } - - if let IntExt::Int(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 - } -} - #[cfg(test)] mod tests { use pumpkin_core::state::State; diff --git a/pumpkin-crates/propagators/src/propagators/arithmetic/mod.rs b/pumpkin-crates/propagators/src/propagators/arithmetic/mod.rs index 461ed4da1..c849b2426 100644 --- a/pumpkin-crates/propagators/src/propagators/arithmetic/mod.rs +++ b/pumpkin-crates/propagators/src/propagators/arithmetic/mod.rs @@ -1,6 +1,7 @@ //! Contains a number of propagators for a variety of arithmetic constraints. pub(crate) mod absolute_value; pub(crate) mod binary; +mod binary_equals; pub(crate) mod integer_division; pub(crate) mod linear_less_or_equal; pub(crate) mod linear_not_equal; @@ -9,6 +10,7 @@ mod multiplication; pub use absolute_value::*; pub use binary::*; +pub use binary_equals::*; pub use integer_division::*; pub use linear_less_or_equal::*; pub use linear_not_equal::*; diff --git a/pumpkin-crates/propagators/src/propagators/arithmetic/multiplication/checker.rs b/pumpkin-crates/propagators/src/propagators/arithmetic/multiplication/checker.rs index 149400f52..8e6ae4ac8 100644 --- a/pumpkin-crates/propagators/src/propagators/arithmetic/multiplication/checker.rs +++ b/pumpkin-crates/propagators/src/propagators/arithmetic/multiplication/checker.rs @@ -88,7 +88,7 @@ where let Some((value_b, value_c)) = [b_min, b_max].into_iter().find_map(|value_b| { let value_c = value_a * value_b; - dbg!((value_a, value_b, value_c)); + dbg!((value_a, value_b, value_c)); if c_min <= value_c && value_c <= c_max { Some((value_b, value_c)) } else { @@ -105,7 +105,7 @@ where let Some((value_a, value_c)) = [a_min, a_max].into_iter().find_map(|value_a| { let value_c = value_a * value_b; - dbg!((value_a, value_b, value_c)); + dbg!((value_a, value_b, value_c)); if c_min <= value_c && value_c <= c_max { Some((value_a, value_c)) } else { @@ -122,7 +122,7 @@ where let Some(values) = [a_min, a_max].into_iter().find_map(|value_a| { let value_b = value_c / value_a; - dbg!((value_a, value_b, value_c)); + dbg!((value_a, value_b, value_c)); if b_min <= value_b && value_b <= b_max { Some((value_a, value_b, value_c)) @@ -139,7 +139,6 @@ where _ => unreachable!(), }; - self.a.assign(value_a, support); self.b.assign(value_b, support); self.c.assign(value_c, support); diff --git a/pumpkin-crates/propagators/src/propagators/arithmetic/multiplication/constructor.rs b/pumpkin-crates/propagators/src/propagators/arithmetic/multiplication/constructor.rs index 25ac1e8f6..bcf54c578 100644 --- a/pumpkin-crates/propagators/src/propagators/arithmetic/multiplication/constructor.rs +++ b/pumpkin-crates/propagators/src/propagators/arithmetic/multiplication/constructor.rs @@ -1,4 +1,5 @@ -use pumpkin_core::checkers::BoundsConsistencyChecker; +use pumpkin_core::checkers::StrongConsistency; +use pumpkin_core::checkers::StrongConsistencyChecker; use pumpkin_core::checkers::support::SupportsValue; use pumpkin_core::proof::ConstraintTag; use pumpkin_core::proof::InferenceCode; @@ -46,11 +47,14 @@ where (super::ID_B, &self.b), (super::ID_C, &self.c), ), - BoundsConsistencyChecker::new(IntegerMultiplicationChecker { - a: self.a.clone(), - b: self.b.clone(), - c: self.c.clone(), - }), + StrongConsistencyChecker::new( + StrongConsistency::Bounds, + IntegerMultiplicationChecker { + a: self.a.clone(), + b: self.b.clone(), + c: self.c.clone(), + }, + ), ); } From 70a2c311064ec65a810d2ddffb634896228c4428 Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Wed, 20 May 2026 15:31:55 +1000 Subject: [PATCH 05/23] Avoid copying domains when doing domain consistency checks --- .../core/src/checkers/strong_consistency.rs | 19 ++++++++----------- pumpkin-crates/core/src/checkers/support.rs | 8 ++++---- .../arithmetic/binary_equals/checker.rs | 2 +- .../arithmetic/multiplication/checker.rs | 2 +- 4 files changed, 14 insertions(+), 17 deletions(-) diff --git a/pumpkin-crates/core/src/checkers/strong_consistency.rs b/pumpkin-crates/core/src/checkers/strong_consistency.rs index 291b16d40..f044d9791 100644 --- a/pumpkin-crates/core/src/checkers/strong_consistency.rs +++ b/pumpkin-crates/core/src/checkers/strong_consistency.rs @@ -37,17 +37,14 @@ impl StrongConsistencyChecker { } impl ConsistencyChecker for StrongConsistencyChecker { - fn check_consistency(&mut self, scope: &Scope, mut domains: Domains<'_>) -> bool { + fn check_consistency(&mut self, scope: &Scope, domains: Domains<'_>) -> bool { self.supported_values.clear(); for (local_id, domain) in scope.domains() { let values_to_support = match self.consistency_level { - StrongConsistency::Domain => itertools::Either::Left( - domains - .iterate_domain(&domain) - .collect::>() - .into_iter(), - ), + StrongConsistency::Domain => { + itertools::Either::Left(domains.iterate_domain(&domain)) + } StrongConsistency::Bounds => itertools::Either::Right( [domains.lower_bound(&domain), domains.upper_bound(&domain)].into_iter(), ), @@ -62,10 +59,10 @@ impl ConsistencyChecker for StrongConsistencyChecker &mut self.support, local_id, UnsupportedValue(value), - domains.reborrow(), + &domains, ); - if !self.process_support(domains.reborrow()) { + if !self.process_support(&domains) { return false; } } @@ -76,14 +73,14 @@ impl ConsistencyChecker for StrongConsistencyChecker } impl StrongConsistencyChecker { - fn process_support(&mut self, mut domains: Domains<'_>) -> bool { + fn process_support(&mut self, domains: &Domains<'_>) -> bool { if !self.supports.is_solution(&self.support) { log::error!("Support is not a solution"); return false; } for (domain, value) in self.support.drain() { - if !value.is_in(domain, domains.reborrow()) { + if !value.is_in(domain, &domains) { log::error!("Support value is not in the domain"); return false; } diff --git a/pumpkin-crates/core/src/checkers/support.rs b/pumpkin-crates/core/src/checkers/support.rs index 2b8913d23..4e3617558 100644 --- a/pumpkin-crates/core/src/checkers/support.rs +++ b/pumpkin-crates/core/src/checkers/support.rs @@ -25,7 +25,7 @@ pub trait SupportGenerator: Clone + Debug { support: &mut Support, local_id: LocalId, value: UnsupportedValue, - domains: Domains<'_>, + domains: &Domains<'_>, ); /// Returns true if the support is a solution to the constraint. @@ -34,7 +34,7 @@ pub trait SupportGenerator: Clone + Debug { /// A value that may be used in a [`Support`]. pub trait SupportValue: Clone + Debug { - fn is_in(&self, domain: DomainId, domains: Domains<'_>) -> bool; + fn is_in(&self, domain: DomainId, domains: &Domains<'_>) -> bool; /// If the value is an integer, we can cache it to prevent recreating supports for the same /// value. @@ -42,7 +42,7 @@ pub trait SupportValue: Clone + Debug { } impl SupportValue for i32 { - fn is_in(&self, domain: DomainId, domains: Domains<'_>) -> bool { + fn is_in(&self, domain: DomainId, domains: &Domains<'_>) -> bool { domains.contains(&domain, *self) } @@ -52,7 +52,7 @@ impl SupportValue for i32 { } impl SupportValue for f32 { - fn is_in(&self, domain: DomainId, domains: Domains<'_>) -> bool { + fn is_in(&self, domain: DomainId, domains: &Domains<'_>) -> bool { let lb = domains.lower_bound(&domain) as f32; let ub = domains.upper_bound(&domain) as f32; diff --git a/pumpkin-crates/propagators/src/propagators/arithmetic/binary_equals/checker.rs b/pumpkin-crates/propagators/src/propagators/arithmetic/binary_equals/checker.rs index 11072e37e..e50818450 100644 --- a/pumpkin-crates/propagators/src/propagators/arithmetic/binary_equals/checker.rs +++ b/pumpkin-crates/propagators/src/propagators/arithmetic/binary_equals/checker.rs @@ -63,7 +63,7 @@ where support: &mut Support, local_id: LocalId, value: UnsupportedValue, - _: Domains<'_>, + _: &Domains<'_>, ) { let value = match local_id { super::ID_LHS => self.lhs.unpack(value), diff --git a/pumpkin-crates/propagators/src/propagators/arithmetic/multiplication/checker.rs b/pumpkin-crates/propagators/src/propagators/arithmetic/multiplication/checker.rs index 8e6ae4ac8..4f8904898 100644 --- a/pumpkin-crates/propagators/src/propagators/arithmetic/multiplication/checker.rs +++ b/pumpkin-crates/propagators/src/propagators/arithmetic/multiplication/checker.rs @@ -68,7 +68,7 @@ where support: &mut Support, local_id: LocalId, unsupported_value: UnsupportedValue, - domains: Domains<'_>, + domains: &Domains<'_>, ) { let a_min = domains.lower_bound(&self.a) as f32; let a_max = domains.upper_bound(&self.a) as f32; From 1d325651c900372517dc5b8612d86eb0e13fcec2 Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Fri, 22 May 2026 14:34:28 +1000 Subject: [PATCH 06/23] Refactor PropagatorConstructor to remove `add_inference_checkers` --- Cargo.lock | 7 + pumpkin-crates/core/Cargo.toml | 1 + pumpkin-crates/core/src/checkers/scope.rs | 7 + pumpkin-crates/core/src/engine/state.rs | 8 +- .../core/src/propagation/constructor.rs | 135 +++++++++--------- .../hypercube_linear/propagator.rs | 21 ++- .../src/propagators/reified_propagator.rs | 75 +++++++++- .../propagators/arithmetic/absolute_value.rs | 19 ++- .../arithmetic/binary/binary_not_equals.rs | 19 ++- .../arithmetic/binary_equals/constructor.rs | 39 +++-- .../arithmetic/integer_division.rs | 21 ++- .../arithmetic/linear_less_or_equal.rs | 16 +-- .../arithmetic/linear_not_equal.rs | 19 ++- .../src/propagators/arithmetic/maximum.rs | 19 ++- .../arithmetic/multiplication/constructor.rs | 44 +++--- .../time_table_over_interval_incremental.rs | 8 +- .../time_table_per_point_incremental.rs | 8 +- .../time_table/time_table_over_interval.rs | 8 +- .../time_table/time_table_per_point.rs | 8 +- .../disjunctive/disjunctive_propagator.rs | 31 ++-- .../propagators/src/propagators/element.rs | 17 +-- 21 files changed, 268 insertions(+), 262 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 08e486f5a..e84e49133 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1011,6 +1011,7 @@ dependencies = [ "once_cell", "pumpkin-checking", "rand", + "replace_with", "thiserror", "wasm-bindgen-test", "web-time", @@ -1212,6 +1213,12 @@ version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" +[[package]] +name = "replace_with" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51743d3e274e2b18df81c4dc6caf8a5b8e15dbe799e0dca05c7617380094e884" + [[package]] name = "rustc_version" version = "0.4.1" diff --git a/pumpkin-crates/core/Cargo.toml b/pumpkin-crates/core/Cargo.toml index 564d9398e..36796d6d3 100644 --- a/pumpkin-crates/core/Cargo.toml +++ b/pumpkin-crates/core/Cargo.toml @@ -31,6 +31,7 @@ indexmap = "2.10.0" dyn-clone = "1.0.20" flate2 = { version = "1.1.2" } bit-set = "0.10.0" +replace_with = "0.1.8" [target.'cfg(target_arch = "wasm32")'.dependencies] web-time = "1.1" diff --git a/pumpkin-crates/core/src/checkers/scope.rs b/pumpkin-crates/core/src/checkers/scope.rs index c15ce436b..af19db82c 100644 --- a/pumpkin-crates/core/src/checkers/scope.rs +++ b/pumpkin-crates/core/src/checkers/scope.rs @@ -20,6 +20,13 @@ impl Scope { pub fn domains(&self) -> impl ExactSizeIterator { self.domains.iter().map(|(lid, did)| (*lid, *did)) } + + /// Returns a copy of this scope with the entry for `local_id` removed. + pub fn without(&self, local_id: LocalId) -> Scope { + let mut scope = self.clone(); + let _ = scope.domains.remove(&local_id); + scope + } } macro_rules! impl_scope_from_tuple { diff --git a/pumpkin-crates/core/src/engine/state.rs b/pumpkin-crates/core/src/engine/state.rs index e568dae34..01a85f195 100644 --- a/pumpkin-crates/core/src/engine/state.rs +++ b/pumpkin-crates/core/src/engine/state.rs @@ -31,8 +31,6 @@ use crate::proof::InferenceCode; use crate::propagation::CurrentNogood; use crate::propagation::Domains; use crate::propagation::ExplanationContext; -#[cfg(any(feature = "check-propagations", feature = "check-consistency"))] -use crate::propagation::InferenceCheckers; use crate::propagation::NotificationContext; use crate::propagation::PropagationContext; use crate::propagation::Propagator; @@ -338,9 +336,6 @@ impl State { Constructor: PropagatorConstructor, Constructor::PropagatorImpl: 'static, { - #[cfg(any(feature = "check-propagations", feature = "check-consistency"))] - constructor.add_inference_checkers(InferenceCheckers::new(self)); - let original_handle: PropagatorHandle = self.propagators.new_propagator().key(); @@ -382,10 +377,9 @@ impl State { checkers.push(BoxedChecker::from(checker)); } - /// Add a consistency checker for the given constraint and scope. + /// Add a consistency checker for the scope. pub fn add_consistency_checker( &mut self, - _constraint_tag: ConstraintTag, scope: impl Into, checker: impl Into, ) { diff --git a/pumpkin-crates/core/src/propagation/constructor.rs b/pumpkin-crates/core/src/propagation/constructor.rs index e93c61aa0..ffbc4ebbb 100644 --- a/pumpkin-crates/core/src/propagation/constructor.rs +++ b/pumpkin-crates/core/src/propagation/constructor.rs @@ -23,14 +23,11 @@ use crate::engine::variables::AffineView; #[cfg(doc)] use crate::engine::variables::DomainId; use crate::predicates::Predicate; -use crate::proof::ConstraintTag; 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`]. /// @@ -38,74 +35,18 @@ use crate::variables::Literal; /// 1) Indicating on which [`DomainEvent`]s the propagator should be enqueued (via the /// [`PropagatorConstructorContext`]). /// 2) Initialising the [`PropagatorConstructor::PropagatorImpl`] and its structures. +/// +/// Inference checkers and consistency checkers should be registered inside [`Self::create`] via +/// [`PropagatorConstructorContext::add_inference_checker`] and +/// [`PropagatorConstructorContext::add_consistency_checker`]. pub trait PropagatorConstructor { /// The propagator that is produced by this constructor. type PropagatorImpl: Propagator + Clone; - /// Add inference checkers to the solver if applicable. - /// - /// 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<'_>) {} - /// Create the propagator instance from `Self`. fn create(self, context: PropagatorConstructorContext) -> Self::PropagatorImpl; } -/// Interface used to add [`InferenceChecker`]s to the [`State`]. -#[derive(Debug)] -pub struct InferenceCheckers<'state> { - state: &'state mut State, - reification_literal: Option, -} - -impl<'state> InferenceCheckers<'state> { - #[cfg(any(feature = "check-propagations", feature = "check-consistency"))] - pub(crate) fn new(state: &'state mut State) -> Self { - InferenceCheckers { - state, - reification_literal: None, - } - } -} - -impl InferenceCheckers<'_> { - /// Add a consistency checker for the given constraint and scope. - pub fn add_consistency_checker( - &mut self, - constraint_tag: ConstraintTag, - scope: impl Into, - checker: impl Into, - ) { - self.state - .add_consistency_checker(constraint_tag, scope, checker); - } - - /// Forwards to [`State::add_inference_checker`]. - pub fn add_inference_checker( - &mut self, - inference_code: InferenceCode, - checker: Box>, - ) { - 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) - } -} - /// [`PropagatorConstructorContext`] is used when [`Propagator`]s are initialised after creation. /// /// It represents a communication point between the [`Solver`] and the [`Propagator`]. @@ -113,17 +54,26 @@ impl InferenceCheckers<'_> { /// of variables and to retrieve the current bounds of variables. #[derive(Debug)] pub struct PropagatorConstructorContext<'a> { - state: &'a mut State, + pub(crate) state: &'a mut State, pub(crate) propagator_id: PropagatorId, /// A [`LocalId`] that is guaranteed not to be used to register any variables yet. This is /// either a reference or an owned value, to support /// [`PropagatorConstructorContext::reborrow`]. - next_local_id: RefOrOwned<'a, LocalId>, + pub(crate) next_local_id: RefOrOwned<'a, LocalId>, /// Marker to indicate whether the constructor registered for at least one domain event or /// predicate becoming assigned. If not, the [`Drop`] implementation will cause a panic. - did_register: RefOrOwned<'a, bool>, + pub(crate) did_register: RefOrOwned<'a, bool>, + + /// Pending consistency checkers to be registered into [`State`] when this context is dropped. + #[cfg(feature = "check-consistency")] + pub(crate) pending_consistency_checkers: RefOrOwned<'a, Vec<(Scope, BoxedConsistencyChecker)>>, + + /// Pending inference checkers to be registered into [`State`] when this context is dropped. + #[cfg(feature = "check-propagations")] + pub(crate) pending_inference_checkers: + RefOrOwned<'a, Vec<(InferenceCode, Box>)>>, } impl PropagatorConstructorContext<'_> { @@ -136,6 +86,10 @@ impl PropagatorConstructorContext<'_> { propagator_id, state, did_register: RefOrOwned::Owned(false), + #[cfg(feature = "check-consistency")] + pending_consistency_checkers: RefOrOwned::Owned(vec![]), + #[cfg(feature = "check-propagations")] + pending_inference_checkers: RefOrOwned::Owned(vec![]), } } @@ -239,19 +193,49 @@ impl PropagatorConstructorContext<'_> { next_local_id: self.next_local_id.reborrow(), did_register: self.did_register.reborrow(), state: self.state, + #[cfg(feature = "check-consistency")] + pending_consistency_checkers: self.pending_consistency_checkers.reborrow(), + #[cfg(feature = "check-propagations")] + pending_inference_checkers: self.pending_inference_checkers.reborrow(), + } + } + + /// Add a consistency checker for the given constraint and scope. + /// + /// If the `check-consistency` feature is not enabled, this is a no-op. + pub fn add_consistency_checker( + &mut self, + scope: impl Into, + checker: impl Into, + ) { + #[cfg(feature = "check-consistency")] + self.pending_consistency_checkers + .push((scope.into(), checker.into())); + + #[cfg(not(feature = "check-consistency"))] + { + let _ = scope; + let _ = checker; } } /// Add an inference checker for inferences produced by the propagator. /// - /// If the `check-propagations` feature is not enabled, adding an [`InferenceChecker`] will not - /// do anything. + /// If the `check-propagations` feature is not enabled, this is a no-op. pub fn add_inference_checker( &mut self, inference_code: InferenceCode, checker: Box>, ) { - self.state.add_inference_checker(inference_code, checker); + #[cfg(feature = "check-propagations")] + self.pending_inference_checkers + .push((inference_code, checker)); + + #[cfg(not(feature = "check-propagations"))] + { + let _ = inference_code; + let _ = checker; + } } /// Set the next local id to be at least one more than the largest encountered local id. @@ -270,7 +254,8 @@ impl Drop for PropagatorConstructorContext<'_> { } let did_register = match self.did_register { - // If we are in a reborrowed context, we do not want to enforce registration. + // If we are in a reborrowed context, we do not want to enforce registration or drain + // pending checkers (the root context handles this). RefOrOwned::Ref(_) => return, RefOrOwned::Owned(did_register) => did_register, @@ -281,6 +266,16 @@ impl Drop for PropagatorConstructorContext<'_> { "Propagator did not register to be enqueued. If this is intentional, call PropagatorConstructorContext::will_not_register_any_events()." ); } + + #[cfg(feature = "check-consistency")] + for (scope, checker) in std::mem::take(&mut *self.pending_consistency_checkers) { + self.state.add_consistency_checker(scope, checker); + } + + #[cfg(feature = "check-propagations")] + for (inference_code, checker) in std::mem::take(&mut *self.pending_inference_checkers) { + self.state.add_inference_checker(inference_code, checker); + } } } diff --git a/pumpkin-crates/core/src/propagators/hypercube_linear/propagator.rs b/pumpkin-crates/core/src/propagators/hypercube_linear/propagator.rs index fc1e0a3f7..92d0f7fb9 100644 --- a/pumpkin-crates/core/src/propagators/hypercube_linear/propagator.rs +++ b/pumpkin-crates/core/src/propagators/hypercube_linear/propagator.rs @@ -7,7 +7,6 @@ use crate::predicates::PropositionalConjunction; use crate::proof::ConstraintTag; use crate::proof::InferenceCode; use crate::propagation::DomainEvents; -use crate::propagation::InferenceCheckers; use crate::propagation::LocalId; use crate::propagation::PropagationContext; use crate::propagation::Propagator; @@ -33,17 +32,6 @@ pub struct HypercubeLinearConstructor { impl PropagatorConstructor for HypercubeLinearConstructor { type PropagatorImpl = HypercubeLinearPropagator; - fn add_inference_checkers(&self, mut checkers: InferenceCheckers<'_>) { - checkers.add_inference_checker( - InferenceCode::new(self.constraint_tag, HypercubeLinear), - Box::new(HypercubeLinearChecker { - hypercube: self.hypercube.iter_predicates().collect(), - terms: self.linear.terms().collect(), - bound: self.linear.bound(), - }), - ); - } - fn create(self, mut context: PropagatorConstructorContext) -> Self::PropagatorImpl { let HypercubeLinearConstructor { hypercube, @@ -51,6 +39,15 @@ impl PropagatorConstructor for HypercubeLinearConstructor { constraint_tag, } = self; + context.add_inference_checker( + InferenceCode::new(constraint_tag, HypercubeLinear), + Box::new(HypercubeLinearChecker { + hypercube: hypercube.iter_predicates().collect(), + terms: linear.terms().collect(), + bound: linear.bound(), + }), + ); + let hypercube_predicates = hypercube.iter_predicates().collect::>(); let watched_predicates = if hypercube_predicates.is_empty() { diff --git a/pumpkin-crates/core/src/propagators/reified_propagator.rs b/pumpkin-crates/core/src/propagators/reified_propagator.rs index 0bb782495..21e9d3dd6 100644 --- a/pumpkin-crates/core/src/propagators/reified_propagator.rs +++ b/pumpkin-crates/core/src/propagators/reified_propagator.rs @@ -3,6 +3,9 @@ use pumpkin_checking::BoxedChecker; use pumpkin_checking::CheckerVariable; use pumpkin_checking::InferenceChecker; +use crate::checkers::BoxedConsistencyChecker; +use crate::checkers::ConsistencyChecker; +use crate::checkers::Scope; use crate::engine::PropagationStatusCP; use crate::engine::notifications::OpaqueDomainEvent; use crate::predicates::Predicate; @@ -10,7 +13,6 @@ use crate::propagation::DomainEvents; use crate::propagation::Domains; use crate::propagation::EnqueueDecision; use crate::propagation::ExplanationContext; -use crate::propagation::InferenceCheckers; use crate::propagation::LazyExplanation; use crate::propagation::LocalId; use crate::propagation::NotificationContext; @@ -45,14 +47,21 @@ where } = self; let propagator = propagator.create(context.reborrow()); + let reification_literal_id = context.get_next_local_id(); context.register( - self.reification_literal, + reification_literal, DomainEvents::BOUNDS, reification_literal_id, ); + #[cfg(feature = "check-propagations")] + wrap_inference_checkers(&mut context, reification_literal); + + #[cfg(feature = "check-consistency")] + wrap_consistency_checkers(&mut context, reification_literal, reification_literal_id); + let name = format!("Reified({})", propagator.name()); ReifiedPropagator { @@ -63,11 +72,44 @@ where reason_buffer: vec![], } } +} - fn add_inference_checkers(&self, mut checkers: InferenceCheckers<'_>) { - checkers.with_reification_literal(self.reification_literal); +/// Wrap inference checkers: the literal is already known, no local id needed. +#[cfg(feature = "check-propagations")] +fn wrap_inference_checkers( + context: &mut PropagatorConstructorContext<'_>, + reification_literal: Literal, +) { + for (_, checker) in context.pending_inference_checkers.iter_mut() { + replace_with::replace_with_or_abort(checker, |inner_checker| { + Box::new(ReifiedChecker { + inner: BoxedChecker::from(inner_checker), + reification_literal, + }) + }); + } +} + +/// Wrap consistency checkers: add the reification literal to each scope with the now-known +/// local id, then wrap the checker. +#[cfg(feature = "check-consistency")] +fn wrap_consistency_checkers( + context: &mut PropagatorConstructorContext<'_>, + reification_literal: Literal, + reification_literal_id: LocalId, +) { + use crate::checkers::ScopeItem; - self.propagator.add_inference_checkers(checkers); + for (scope, checker) in context.pending_consistency_checkers.iter_mut() { + reification_literal.add_to_scope(scope, reification_literal_id); + + replace_with::replace_with_or_abort(checker, |inner_checker| { + BoxedConsistencyChecker::from(ReifiedConsistencyChecker { + inner: inner_checker, + reification_literal, + reification_literal_id, + }) + }); } } @@ -231,6 +273,29 @@ impl ReifiedPropagator { } } +/// A [`ConsistencyChecker`] wrapper that skips the inner check when the reification literal is +/// not assigned to true. +#[derive(Debug, Clone)] +pub struct ReifiedConsistencyChecker { + pub inner: BoxedConsistencyChecker, + pub reification_literal: Literal, + /// The [`LocalId`] of the reification literal in the scope, used to strip it before passing + /// the scope to the inner checker. + pub reification_literal_id: LocalId, +} + +impl ConsistencyChecker for ReifiedConsistencyChecker { + fn check_consistency(&mut self, scope: &Scope, domains: Domains<'_>) -> bool { + let check_inner = domains.evaluate_literal(self.reification_literal) == Some(true); + if !check_inner { + return true; + } + + let inner_scope = scope.without(self.reification_literal_id); + self.inner.check_consistency(&inner_scope, domains) + } +} + #[derive(Debug, Clone)] pub struct ReifiedChecker { pub inner: BoxedChecker, diff --git a/pumpkin-crates/propagators/src/propagators/arithmetic/absolute_value.rs b/pumpkin-crates/propagators/src/propagators/arithmetic/absolute_value.rs index 2881dfa39..e06585952 100644 --- a/pumpkin-crates/propagators/src/propagators/arithmetic/absolute_value.rs +++ b/pumpkin-crates/propagators/src/propagators/arithmetic/absolute_value.rs @@ -8,7 +8,6 @@ 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,16 +34,6 @@ 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, @@ -52,6 +41,14 @@ where constraint_tag, } = self; + context.add_inference_checker( + InferenceCode::new(constraint_tag, AbsoluteValue), + Box::new(AbsoluteValueChecker { + signed: signed.clone(), + absolute: absolute.clone(), + }), + ); + context.register(signed.clone(), DomainEvents::BOUNDS, LocalId::from(0)); context.register(absolute.clone(), DomainEvents::BOUNDS, LocalId::from(1)); 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 b33a98543..e7e4669c8 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 @@ -8,7 +8,6 @@ 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; @@ -37,16 +36,6 @@ 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, @@ -54,6 +43,14 @@ where constraint_tag, } = self; + context.add_inference_checker( + InferenceCode::new(constraint_tag, BinaryNotEquals), + Box::new(BinaryNotEqualsChecker { + lhs: a.clone(), + rhs: b.clone(), + }), + ); + // We only care about the case where one of the two is assigned context.register(a.clone(), DomainEvents::ASSIGN, LocalId::from(0)); context.register(b.clone(), DomainEvents::ASSIGN, LocalId::from(1)); diff --git a/pumpkin-crates/propagators/src/propagators/arithmetic/binary_equals/constructor.rs b/pumpkin-crates/propagators/src/propagators/arithmetic/binary_equals/constructor.rs index 481b6ae0a..8d75f208f 100644 --- a/pumpkin-crates/propagators/src/propagators/arithmetic/binary_equals/constructor.rs +++ b/pumpkin-crates/propagators/src/propagators/arithmetic/binary_equals/constructor.rs @@ -1,12 +1,10 @@ use pumpkin_core::checkers::StrongConsistency; use pumpkin_core::checkers::StrongConsistencyChecker; -use pumpkin_core::checkers::support::SupportsValue; use pumpkin_core::containers::HashSet; use pumpkin_core::predicates::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::PropagatorConstructor; use pumpkin_core::propagation::PropagatorConstructorContext; use pumpkin_core::variables::IntegerVariable; @@ -24,39 +22,36 @@ pub struct BinaryEqualsPropagatorArgs { impl PropagatorConstructor for BinaryEqualsPropagatorArgs where - AVar: IntegerVariable + SupportsValue + 'static, - BVar: IntegerVariable + SupportsValue + 'static, + AVar: IntegerVariable + 'static, + BVar: IntegerVariable + 'static, { type PropagatorImpl = BinaryEqualsPropagator; - fn add_inference_checkers(&self, mut checkers: InferenceCheckers<'_>) { - checkers.add_inference_checker( - InferenceCode::new(self.constraint_tag, super::BinaryEquals), + fn create(self, mut context: PropagatorConstructorContext) -> Self::PropagatorImpl { + let BinaryEqualsPropagatorArgs { + a, + b, + constraint_tag, + } = self; + + context.add_inference_checker( + InferenceCode::new(constraint_tag, super::BinaryEquals), Box::new(BinaryEqualsChecker { - lhs: self.a.clone(), - rhs: self.b.clone(), + lhs: a.clone(), + rhs: b.clone(), }), ); - checkers.add_consistency_checker( - self.constraint_tag, - ((super::ID_LHS, &self.a), (super::ID_RHS, &self.b)), + context.add_consistency_checker( + ((super::ID_LHS, &a), (super::ID_RHS, &b)), StrongConsistencyChecker::new( StrongConsistency::Domain, BinaryEqualsChecker { - lhs: self.a.clone(), - rhs: self.b.clone(), + lhs: a.clone(), + rhs: b.clone(), }, ), ); - } - - fn create(self, mut context: PropagatorConstructorContext) -> Self::PropagatorImpl { - let BinaryEqualsPropagatorArgs { - a, - b, - constraint_tag, - } = self; context.register(a.clone(), DomainEvents::ANY_INT, super::ID_LHS); context.register(b.clone(), DomainEvents::ANY_INT, super::ID_RHS); diff --git a/pumpkin-crates/propagators/src/propagators/arithmetic/integer_division.rs b/pumpkin-crates/propagators/src/propagators/arithmetic/integer_division.rs index 882495a48..a9a22125b 100644 --- a/pumpkin-crates/propagators/src/propagators/arithmetic/integer_division.rs +++ b/pumpkin-crates/propagators/src/propagators/arithmetic/integer_division.rs @@ -9,7 +9,6 @@ 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; @@ -51,6 +50,15 @@ where constraint_tag, } = self; + context.add_inference_checker( + InferenceCode::new(constraint_tag, Division), + Box::new(IntegerDivisionChecker { + numerator: numerator.clone(), + denominator: denominator.clone(), + rhs: rhs.clone(), + }), + ); + pumpkin_assert_simple!( !context.contains(&denominator, 0), "Denominator cannot contain 0" @@ -69,17 +77,6 @@ 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 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 075af43d9..7cde056cf 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 @@ -14,7 +14,6 @@ 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::LazyExplanation; use pumpkin_core::propagation::LocalId; use pumpkin_core::propagation::NotificationContext; @@ -46,16 +45,6 @@ where { type PropagatorImpl = LinearLessOrEqualPropagator; - 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, @@ -63,6 +52,11 @@ where constraint_tag, } = self; + context.add_inference_checker( + InferenceCode::new(constraint_tag, LinearBounds), + Box::new(LinearLessOrEqualInferenceChecker::new(x.clone(), c)), + ); + let mut lower_bound_left_hand_side = 0_i64; let mut current_bounds = vec![]; 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 130d2b2d8..2aa6e11d9 100644 --- a/pumpkin-crates/propagators/src/propagators/arithmetic/linear_not_equal.rs +++ b/pumpkin-crates/propagators/src/propagators/arithmetic/linear_not_equal.rs @@ -18,7 +18,6 @@ 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; @@ -50,16 +49,6 @@ 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, @@ -67,6 +56,14 @@ where constraint_tag, } = self; + context.add_inference_checker( + InferenceCode::new(constraint_tag, LinearNotEquals), + Box::new(LinearNotEqualChecker { + terms: terms.as_ref().into(), + bound: rhs, + }), + ); + for (i, x_i) in terms.iter().enumerate() { context.register(x_i.clone(), DomainEvents::ASSIGN, LocalId::from(i as u32)); context.register_backtrack( diff --git a/pumpkin-crates/propagators/src/propagators/arithmetic/maximum.rs b/pumpkin-crates/propagators/src/propagators/arithmetic/maximum.rs index 2a847bc4c..693869274 100644 --- a/pumpkin-crates/propagators/src/propagators/arithmetic/maximum.rs +++ b/pumpkin-crates/propagators/src/propagators/arithmetic/maximum.rs @@ -9,7 +9,6 @@ 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; @@ -36,16 +35,6 @@ 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, @@ -53,6 +42,14 @@ where constraint_tag, } = self; + context.add_inference_checker( + InferenceCode::new(constraint_tag, Maximum), + Box::new(MaximumChecker { + array: array.clone(), + rhs: rhs.clone(), + }), + ); + for (idx, var) in array.iter().enumerate() { context.register(var.clone(), DomainEvents::BOUNDS, LocalId::from(idx as u32)); } diff --git a/pumpkin-crates/propagators/src/propagators/arithmetic/multiplication/constructor.rs b/pumpkin-crates/propagators/src/propagators/arithmetic/multiplication/constructor.rs index bcf54c578..84569b507 100644 --- a/pumpkin-crates/propagators/src/propagators/arithmetic/multiplication/constructor.rs +++ b/pumpkin-crates/propagators/src/propagators/arithmetic/multiplication/constructor.rs @@ -4,7 +4,6 @@ use pumpkin_core::checkers::support::SupportsValue; use pumpkin_core::proof::ConstraintTag; use pumpkin_core::proof::InferenceCode; use pumpkin_core::propagation::DomainEvents; -use pumpkin_core::propagation::InferenceCheckers; use pumpkin_core::propagation::PropagatorConstructor; use pumpkin_core::propagation::PropagatorConstructorContext; use pumpkin_core::variables::IntegerVariable; @@ -30,41 +29,34 @@ where { type PropagatorImpl = IntegerMultiplicationPropagator; - fn add_inference_checkers(&self, mut checkers: InferenceCheckers<'_>) { - checkers.add_inference_checker( - InferenceCode::new(self.constraint_tag, IntegerMultiplication), + fn create(self, mut context: PropagatorConstructorContext) -> Self::PropagatorImpl { + let IntegerMultiplicationArgs { + a, + b, + c, + constraint_tag, + } = self; + + context.add_inference_checker( + InferenceCode::new(constraint_tag, IntegerMultiplication), Box::new(IntegerMultiplicationChecker { - a: self.a.clone(), - b: self.b.clone(), - c: self.c.clone(), + a: a.clone(), + b: b.clone(), + c: c.clone(), }), ); - checkers.add_consistency_checker( - self.constraint_tag, - ( - (super::ID_A, &self.a), - (super::ID_B, &self.b), - (super::ID_C, &self.c), - ), + context.add_consistency_checker( + ((super::ID_A, &a), (super::ID_B, &b), (super::ID_C, &c)), StrongConsistencyChecker::new( StrongConsistency::Bounds, IntegerMultiplicationChecker { - a: self.a.clone(), - b: self.b.clone(), - c: self.c.clone(), + a: a.clone(), + b: b.clone(), + c: c.clone(), }, ), ); - } - - fn create(self, mut context: PropagatorConstructorContext) -> Self::PropagatorImpl { - let IntegerMultiplicationArgs { - a, - b, - c, - constraint_tag, - } = self; context.register(a.clone(), DomainEvents::ANY_INT, super::ID_A); context.register(b.clone(), DomainEvents::ANY_INT, super::ID_B); 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 fc5d108e4..e67e186ed 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,7 +11,6 @@ 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; @@ -110,8 +109,8 @@ impl PropagatorConstruc { type PropagatorImpl = Self; - fn add_inference_checkers(&self, mut checkers: InferenceCheckers<'_>) { - checkers.add_inference_checker( + fn create(mut self, mut context: PropagatorConstructorContext) -> Self::PropagatorImpl { + context.add_inference_checker( InferenceCode::new(self.constraint_tag, TimeTable), Box::new(TimeTableChecker { tasks: self @@ -127,9 +126,6 @@ impl PropagatorConstruc 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 register_tasks( 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 9b4ffa1e5..05ac11900 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,7 +11,6 @@ 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; @@ -107,8 +106,8 @@ impl Propagator { type PropagatorImpl = Self; - fn add_inference_checkers(&self, mut checkers: InferenceCheckers<'_>) { - checkers.add_inference_checker( + fn create(mut self, mut context: PropagatorConstructorContext) -> Self::PropagatorImpl { + context.add_inference_checker( InferenceCode::new(self.constraint_tag, TimeTable), Box::new(TimeTableChecker { tasks: self @@ -124,9 +123,6 @@ impl Propagator capacity: self.parameters.capacity, }), ); - } - - fn create(mut self, mut context: PropagatorConstructorContext) -> Self::PropagatorImpl { register_tasks(&self.parameters.tasks, context.reborrow(), true); self.updatable_structures .reset_all_bounds_and_remove_fixed(context.domains(), &self.parameters); 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 cb8b6926b..b8b3fa314 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,7 +9,6 @@ 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; @@ -110,8 +109,8 @@ impl PropagatorConstructor { type PropagatorImpl = Self; - fn add_inference_checkers(&self, mut checkers: InferenceCheckers<'_>) { - checkers.add_inference_checker( + fn create(mut self, mut context: PropagatorConstructorContext) -> Self::PropagatorImpl { + context.add_inference_checker( InferenceCode::new(self.constraint_tag, TimeTable), Box::new(TimeTableChecker { tasks: self @@ -127,9 +126,6 @@ impl PropagatorConstructor 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); register_tasks(&self.parameters.tasks, context.reborrow(), false); 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 5451cd01d..0c7fceed6 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 @@ -11,7 +11,6 @@ use pumpkin_core::proof::ConstraintTag; use pumpkin_core::proof::InferenceCode; use pumpkin_core::propagation::DomainEvent; 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; @@ -100,8 +99,8 @@ impl TimeTablePerPointPropagator { impl PropagatorConstructor for TimeTablePerPointPropagator { type PropagatorImpl = Self; - fn add_inference_checkers(&self, mut checkers: InferenceCheckers<'_>) { - checkers.add_inference_checker( + fn create(mut self, mut context: PropagatorConstructorContext) -> Self::PropagatorImpl { + context.add_inference_checker( InferenceCode::new(self.constraint_tag, TimeTable), Box::new(TimeTableChecker { tasks: self @@ -117,9 +116,6 @@ impl PropagatorConstructor for TimeTablePerPoint 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); register_tasks(&self.parameters.tasks, context.reborrow(), false); diff --git a/pumpkin-crates/propagators/src/propagators/disjunctive/disjunctive_propagator.rs b/pumpkin-crates/propagators/src/propagators/disjunctive/disjunctive_propagator.rs index 0d2da6002..376712d4b 100644 --- a/pumpkin-crates/propagators/src/propagators/disjunctive/disjunctive_propagator.rs +++ b/pumpkin-crates/propagators/src/propagators/disjunctive/disjunctive_propagator.rs @@ -8,7 +8,6 @@ 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; @@ -80,6 +79,20 @@ impl PropagatorConstructor for DisjunctiveConstr type PropagatorImpl = DisjunctivePropagator; fn create(self, mut context: PropagatorConstructorContext) -> Self::PropagatorImpl { + context.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(), + }), + ); + let tasks = self .tasks .into_iter() @@ -106,22 +119,6 @@ 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/element.rs b/pumpkin-crates/propagators/src/propagators/element.rs index 8874a259c..df5a78149 100644 --- a/pumpkin-crates/propagators/src/propagators/element.rs +++ b/pumpkin-crates/propagators/src/propagators/element.rs @@ -18,7 +18,6 @@ use pumpkin_core::proof::ConstraintTag; use pumpkin_core::proof::InferenceCode; use pumpkin_core::propagation::DomainEvents; use pumpkin_core::propagation::ExplanationContext; -use pumpkin_core::propagation::InferenceCheckers; use pumpkin_core::propagation::LazyExplanation; use pumpkin_core::propagation::LocalId; use pumpkin_core::propagation::Priority; @@ -49,17 +48,6 @@ where { type PropagatorImpl = ElementPropagator; - fn add_inference_checkers(&self, mut checkers: InferenceCheckers<'_>) { - checkers.add_inference_checker( - InferenceCode::new(self.constraint_tag, Element), - Box::new(ElementChecker::new( - self.array.clone(), - self.index.clone(), - self.rhs.clone(), - )), - ); - } - fn create(self, mut context: PropagatorConstructorContext) -> Self::PropagatorImpl { let ElementArgs { array, @@ -68,6 +56,11 @@ where constraint_tag, } = self; + context.add_inference_checker( + InferenceCode::new(constraint_tag, Element), + Box::new(ElementChecker::new(array.clone(), index.clone(), rhs.clone())), + ); + for (i, x_i) in array.iter().enumerate() { context.register( x_i.clone(), From a9324b9caa651542d6d65a519ee4a3990206d7e5 Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Fri, 22 May 2026 14:50:56 +1000 Subject: [PATCH 07/23] Fix conditional compilation errors and split up reified propagator --- pumpkin-crates/core/src/api/mod.rs | 2 +- pumpkin-crates/core/src/constraints/mod.rs | 2 +- pumpkin-crates/core/src/propagators/mod.rs | 2 +- .../propagators/reified_propagator/checker.rs | 68 ++++++++ .../reified_propagator/constructor.rs | 103 +++++++++++ .../src/propagators/reified_propagator/mod.rs | 7 + .../propagator.rs} | 162 +----------------- .../propagators/src/propagators/element.rs | 6 +- 8 files changed, 191 insertions(+), 161 deletions(-) create mode 100644 pumpkin-crates/core/src/propagators/reified_propagator/checker.rs create mode 100644 pumpkin-crates/core/src/propagators/reified_propagator/constructor.rs create mode 100644 pumpkin-crates/core/src/propagators/reified_propagator/mod.rs rename pumpkin-crates/core/src/propagators/{reified_propagator.rs => reified_propagator/propagator.rs} (74%) diff --git a/pumpkin-crates/core/src/api/mod.rs b/pumpkin-crates/core/src/api/mod.rs index 22f3a90d7..1f5944d9a 100644 --- a/pumpkin-crates/core/src/api/mod.rs +++ b/pumpkin-crates/core/src/api/mod.rs @@ -69,8 +69,8 @@ pub mod options { pub use crate::engine::ConflictResolverType; pub use crate::engine::RestartOptions; pub use crate::engine::SatisfactionSolverOptions as SolverOptions; + pub use crate::propagators::ReifiedPropagatorArgs; pub use crate::propagators::nogoods::LearningOptions; - pub use crate::propagators::reified_propagator::ReifiedPropagatorArgs; } pub mod termination { diff --git a/pumpkin-crates/core/src/constraints/mod.rs b/pumpkin-crates/core/src/constraints/mod.rs index 1e7f34304..1e0fb5c4b 100644 --- a/pumpkin-crates/core/src/constraints/mod.rs +++ b/pumpkin-crates/core/src/constraints/mod.rs @@ -2,7 +2,7 @@ use crate::ConstraintOperationError; use crate::Solver; use crate::propagation::PropagatorConstructor; -use crate::propagators::reified_propagator::ReifiedPropagatorArgs; +use crate::propagators::ReifiedPropagatorArgs; use crate::variables::Literal; mod constraint_poster; diff --git a/pumpkin-crates/core/src/propagators/mod.rs b/pumpkin-crates/core/src/propagators/mod.rs index 2af1b61a5..25afdc21d 100644 --- a/pumpkin-crates/core/src/propagators/mod.rs +++ b/pumpkin-crates/core/src/propagators/mod.rs @@ -1,5 +1,5 @@ pub mod hypercube_linear; pub mod nogoods; -pub(crate) mod reified_propagator; +mod reified_propagator; pub use reified_propagator::*; diff --git a/pumpkin-crates/core/src/propagators/reified_propagator/checker.rs b/pumpkin-crates/core/src/propagators/reified_propagator/checker.rs new file mode 100644 index 000000000..6cc092316 --- /dev/null +++ b/pumpkin-crates/core/src/propagators/reified_propagator/checker.rs @@ -0,0 +1,68 @@ +use pumpkin_checking::AtomicConstraint; +use pumpkin_checking::BoxedChecker; +use pumpkin_checking::CheckerVariable; +use pumpkin_checking::InferenceChecker; +use pumpkin_checking::VariableState; + +use crate::checkers::BoxedConsistencyChecker; +use crate::checkers::ConsistencyChecker; +use crate::checkers::Scope; +use crate::propagation::Domains; +use crate::propagation::LocalId; +use crate::propagation::ReadDomains; +use crate::variables::Literal; + +/// A [`ConsistencyChecker`] wrapper that skips the inner check when the reification literal is +/// not assigned to true. +#[derive(Debug, Clone)] +pub struct ReifiedConsistencyChecker { + pub inner: BoxedConsistencyChecker, + pub reification_literal: Literal, + /// The [`LocalId`] of the reification literal in the scope, used to strip it before passing + /// the scope to the inner checker. + pub reification_literal_id: LocalId, +} + +impl ConsistencyChecker for ReifiedConsistencyChecker { + fn check_consistency(&mut self, scope: &Scope, domains: Domains<'_>) -> bool { + if domains.evaluate_literal(self.reification_literal) != Some(true) { + return true; + } + + let inner_scope = scope.without(self.reification_literal_id); + self.inner.check_consistency(&inner_scope, domains) + } +} + +#[derive(Debug, Clone)] +pub struct ReifiedChecker { + pub inner: BoxedChecker, + pub reification_literal: Var, +} + +impl InferenceChecker for ReifiedChecker +where + Atomic: AtomicConstraint + Clone, + Var: CheckerVariable, +{ + fn check( + &self, + state: 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) + } + } +} diff --git a/pumpkin-crates/core/src/propagators/reified_propagator/constructor.rs b/pumpkin-crates/core/src/propagators/reified_propagator/constructor.rs new file mode 100644 index 000000000..e6864b0e3 --- /dev/null +++ b/pumpkin-crates/core/src/propagators/reified_propagator/constructor.rs @@ -0,0 +1,103 @@ +#[cfg(feature = "check-consistency")] +use crate::checkers::BoxedConsistencyChecker; +use crate::propagation::DomainEvents; +#[cfg(feature = "check-consistency")] +use crate::propagation::LocalId; +use crate::propagation::Propagator; +use crate::propagation::PropagatorConstructor; +use crate::propagation::PropagatorConstructorContext; +#[cfg(feature = "check-consistency")] +use crate::propagators::ReifiedConsistencyChecker; +use crate::propagators::ReifiedPropagator; +use crate::variables::Literal; + +/// A [`PropagatorConstructor`] for the reified propagator. +#[derive(Clone, Debug)] +pub struct ReifiedPropagatorArgs { + pub propagator: WrappedArgs, + pub reification_literal: Literal, +} + +impl PropagatorConstructor for ReifiedPropagatorArgs +where + WrappedArgs: PropagatorConstructor, + WrappedPropagator: Propagator + Clone, +{ + type PropagatorImpl = ReifiedPropagator; + + fn create(self, mut context: PropagatorConstructorContext) -> Self::PropagatorImpl { + let ReifiedPropagatorArgs { + propagator, + reification_literal, + } = self; + + let propagator = propagator.create(context.reborrow()); + + let reification_literal_id = context.get_next_local_id(); + + context.register( + reification_literal, + DomainEvents::BOUNDS, + reification_literal_id, + ); + + #[cfg(feature = "check-propagations")] + wrap_inference_checkers(&mut context, reification_literal); + + #[cfg(feature = "check-consistency")] + wrap_consistency_checkers(&mut context, reification_literal, reification_literal_id); + + let name = format!("Reified({})", propagator.name()); + + ReifiedPropagator { + propagator, + reification_literal, + reification_literal_id, + name, + reason_buffer: vec![], + } + } +} + +/// Wrap inference checkers: the literal is already known, no local id needed. +#[cfg(feature = "check-propagations")] +fn wrap_inference_checkers( + context: &mut PropagatorConstructorContext<'_>, + reification_literal: Literal, +) { + use crate::propagators::ReifiedChecker; + + for (_, checker) in context.pending_inference_checkers.iter_mut() { + replace_with::replace_with_or_abort(checker, |inner_checker| { + use pumpkin_checking::BoxedChecker; + + Box::new(ReifiedChecker { + inner: BoxedChecker::from(inner_checker), + reification_literal, + }) + }); + } +} + +/// Wrap consistency checkers: add the reification literal to each scope with the now-known +/// local id, then wrap the checker. +#[cfg(feature = "check-consistency")] +fn wrap_consistency_checkers( + context: &mut PropagatorConstructorContext<'_>, + reification_literal: Literal, + reification_literal_id: LocalId, +) { + use crate::checkers::ScopeItem; + + for (scope, checker) in context.pending_consistency_checkers.iter_mut() { + reification_literal.add_to_scope(scope, reification_literal_id); + + replace_with::replace_with_or_abort(checker, |inner_checker| { + BoxedConsistencyChecker::from(ReifiedConsistencyChecker { + inner: inner_checker, + reification_literal, + reification_literal_id, + }) + }); + } +} diff --git a/pumpkin-crates/core/src/propagators/reified_propagator/mod.rs b/pumpkin-crates/core/src/propagators/reified_propagator/mod.rs new file mode 100644 index 000000000..550564f93 --- /dev/null +++ b/pumpkin-crates/core/src/propagators/reified_propagator/mod.rs @@ -0,0 +1,7 @@ +mod checker; +mod constructor; +mod propagator; + +pub use checker::*; +pub use constructor::*; +pub use propagator::*; diff --git a/pumpkin-crates/core/src/propagators/reified_propagator.rs b/pumpkin-crates/core/src/propagators/reified_propagator/propagator.rs similarity index 74% rename from pumpkin-crates/core/src/propagators/reified_propagator.rs rename to pumpkin-crates/core/src/propagators/reified_propagator/propagator.rs index 21e9d3dd6..2f1306295 100644 --- a/pumpkin-crates/core/src/propagators/reified_propagator.rs +++ b/pumpkin-crates/core/src/propagators/reified_propagator/propagator.rs @@ -1,15 +1,6 @@ -use pumpkin_checking::AtomicConstraint; -use pumpkin_checking::BoxedChecker; -use pumpkin_checking::CheckerVariable; -use pumpkin_checking::InferenceChecker; - -use crate::checkers::BoxedConsistencyChecker; -use crate::checkers::ConsistencyChecker; -use crate::checkers::Scope; use crate::engine::PropagationStatusCP; use crate::engine::notifications::OpaqueDomainEvent; use crate::predicates::Predicate; -use crate::propagation::DomainEvents; use crate::propagation::Domains; use crate::propagation::EnqueueDecision; use crate::propagation::ExplanationContext; @@ -19,100 +10,11 @@ use crate::propagation::NotificationContext; use crate::propagation::Priority; use crate::propagation::PropagationContext; use crate::propagation::Propagator; -use crate::propagation::PropagatorConstructor; -use crate::propagation::PropagatorConstructorContext; use crate::propagation::ReadDomains; use crate::pumpkin_assert_simple; use crate::state::Conflict; use crate::variables::Literal; -/// A [`PropagatorConstructor`] for the reified propagator. -#[derive(Clone, Debug)] -pub struct ReifiedPropagatorArgs { - pub propagator: WrappedArgs, - pub reification_literal: Literal, -} - -impl PropagatorConstructor for ReifiedPropagatorArgs -where - WrappedArgs: PropagatorConstructor, - WrappedPropagator: Propagator + Clone, -{ - type PropagatorImpl = ReifiedPropagator; - - fn create(self, mut context: PropagatorConstructorContext) -> Self::PropagatorImpl { - let ReifiedPropagatorArgs { - propagator, - reification_literal, - } = self; - - let propagator = propagator.create(context.reborrow()); - - let reification_literal_id = context.get_next_local_id(); - - context.register( - reification_literal, - DomainEvents::BOUNDS, - reification_literal_id, - ); - - #[cfg(feature = "check-propagations")] - wrap_inference_checkers(&mut context, reification_literal); - - #[cfg(feature = "check-consistency")] - wrap_consistency_checkers(&mut context, reification_literal, reification_literal_id); - - let name = format!("Reified({})", propagator.name()); - - ReifiedPropagator { - propagator, - reification_literal, - reification_literal_id, - name, - reason_buffer: vec![], - } - } -} - -/// Wrap inference checkers: the literal is already known, no local id needed. -#[cfg(feature = "check-propagations")] -fn wrap_inference_checkers( - context: &mut PropagatorConstructorContext<'_>, - reification_literal: Literal, -) { - for (_, checker) in context.pending_inference_checkers.iter_mut() { - replace_with::replace_with_or_abort(checker, |inner_checker| { - Box::new(ReifiedChecker { - inner: BoxedChecker::from(inner_checker), - reification_literal, - }) - }); - } -} - -/// Wrap consistency checkers: add the reification literal to each scope with the now-known -/// local id, then wrap the checker. -#[cfg(feature = "check-consistency")] -fn wrap_consistency_checkers( - context: &mut PropagatorConstructorContext<'_>, - reification_literal: Literal, - reification_literal_id: LocalId, -) { - use crate::checkers::ScopeItem; - - for (scope, checker) in context.pending_consistency_checkers.iter_mut() { - reification_literal.add_to_scope(scope, reification_literal_id); - - replace_with::replace_with_or_abort(checker, |inner_checker| { - BoxedConsistencyChecker::from(ReifiedConsistencyChecker { - inner: inner_checker, - reification_literal, - reification_literal_id, - }) - }); - } -} - /// Propagator for the constraint `r -> p`, where `r` is a Boolean literal and `p` is an arbitrary /// propagator. /// @@ -122,16 +24,16 @@ fn wrap_consistency_checkers( /// propagated to false. #[derive(Clone, Debug)] pub struct ReifiedPropagator { - propagator: WrappedPropagator, - reification_literal: Literal, + pub(super) propagator: WrappedPropagator, + pub(super) reification_literal: Literal, /// The formatted name of the propagator. - name: String, + pub(super) name: String, /// The `LocalId` of the reification literal. Is guaranteed to be a larger ID than any of the /// registered ids of the wrapped propagator. - reification_literal_id: LocalId, + pub(super) reification_literal_id: LocalId, /// Holds the lazy explanations. - reason_buffer: Vec, + pub(super) reason_buffer: Vec, } impl Propagator for ReifiedPropagator { @@ -273,60 +175,6 @@ impl ReifiedPropagator { } } -/// A [`ConsistencyChecker`] wrapper that skips the inner check when the reification literal is -/// not assigned to true. -#[derive(Debug, Clone)] -pub struct ReifiedConsistencyChecker { - pub inner: BoxedConsistencyChecker, - pub reification_literal: Literal, - /// The [`LocalId`] of the reification literal in the scope, used to strip it before passing - /// the scope to the inner checker. - pub reification_literal_id: LocalId, -} - -impl ConsistencyChecker for ReifiedConsistencyChecker { - fn check_consistency(&mut self, scope: &Scope, domains: Domains<'_>) -> bool { - let check_inner = domains.evaluate_literal(self.reification_literal) == Some(true); - if !check_inner { - return true; - } - - let inner_scope = scope.without(self.reification_literal_id); - self.inner.check_consistency(&inner_scope, domains) - } -} - -#[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/element.rs b/pumpkin-crates/propagators/src/propagators/element.rs index df5a78149..19139c998 100644 --- a/pumpkin-crates/propagators/src/propagators/element.rs +++ b/pumpkin-crates/propagators/src/propagators/element.rs @@ -58,7 +58,11 @@ where context.add_inference_checker( InferenceCode::new(constraint_tag, Element), - Box::new(ElementChecker::new(array.clone(), index.clone(), rhs.clone())), + Box::new(ElementChecker::new( + array.clone(), + index.clone(), + rhs.clone(), + )), ); for (i, x_i) in array.iter().enumerate() { From 5fc57f9bcfcce800a5966a83f5289c41ff9e5d96 Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Fri, 22 May 2026 15:03:11 +1000 Subject: [PATCH 08/23] Fix formatting --- pumpkin-crates/core/src/checkers/strong_consistency.rs | 2 +- .../core/src/propagators/reified_propagator/propagator.rs | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/pumpkin-crates/core/src/checkers/strong_consistency.rs b/pumpkin-crates/core/src/checkers/strong_consistency.rs index f044d9791..0ac823d41 100644 --- a/pumpkin-crates/core/src/checkers/strong_consistency.rs +++ b/pumpkin-crates/core/src/checkers/strong_consistency.rs @@ -80,7 +80,7 @@ impl StrongConsistencyChecker { } for (domain, value) in self.support.drain() { - if !value.is_in(domain, &domains) { + if !value.is_in(domain, domains) { log::error!("Support value is not in the domain"); return false; } diff --git a/pumpkin-crates/core/src/propagators/reified_propagator/propagator.rs b/pumpkin-crates/core/src/propagators/reified_propagator/propagator.rs index 2f1306295..6a683be04 100644 --- a/pumpkin-crates/core/src/propagators/reified_propagator/propagator.rs +++ b/pumpkin-crates/core/src/propagators/reified_propagator/propagator.rs @@ -187,6 +187,10 @@ mod tests { use crate::predicates::PropositionalConjunction; use crate::proof::ConstraintTag; use crate::proof::InferenceCode; + use crate::propagation::DomainEvents; + use crate::propagation::PropagatorConstructor; + use crate::propagation::PropagatorConstructorContext; + use crate::propagators::ReifiedPropagatorArgs; use crate::variables::DomainId; #[test] From dee4887303c5be9106bb5479cb5a7e50955a81de Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Fri, 22 May 2026 16:32:16 +1000 Subject: [PATCH 09/23] Implement the checkers for nogood propagator --- pumpkin-crates/core/src/checkers/mod.rs | 2 + .../core/src/checkers/self_disabling.rs | 28 ++++ .../core/src/engine/cp/test_solver.rs | 56 ++++++-- .../core/src/engine/debug_helper.rs | 40 ++++++ pumpkin-crates/core/src/engine/state.rs | 88 +++++++++--- .../core/src/propagation/constructor.rs | 4 + .../contexts/propagation_context.rs | 77 +++++++++++ .../core/src/propagators/nogoods/checker.rs | 39 +++++- .../propagators/nogoods/nogood_propagator.rs | 126 +++++++++++++++++- 9 files changed, 421 insertions(+), 39 deletions(-) create mode 100644 pumpkin-crates/core/src/checkers/self_disabling.rs diff --git a/pumpkin-crates/core/src/checkers/mod.rs b/pumpkin-crates/core/src/checkers/mod.rs index d93bae97f..c97161e15 100644 --- a/pumpkin-crates/core/src/checkers/mod.rs +++ b/pumpkin-crates/core/src/checkers/mod.rs @@ -1,4 +1,5 @@ mod scope; +mod self_disabling; mod store; mod strong_consistency; pub mod support; @@ -7,6 +8,7 @@ use std::fmt::Debug; use dyn_clone::DynClone; pub use scope::*; +pub use self_disabling::*; pub use store::*; pub use strong_consistency::*; diff --git a/pumpkin-crates/core/src/checkers/self_disabling.rs b/pumpkin-crates/core/src/checkers/self_disabling.rs new file mode 100644 index 000000000..8e7498b2a --- /dev/null +++ b/pumpkin-crates/core/src/checkers/self_disabling.rs @@ -0,0 +1,28 @@ +use std::sync::Arc; +use std::sync::atomic::AtomicBool; +use std::sync::atomic::Ordering; + +use super::BoxedConsistencyChecker; +use super::ConsistencyChecker; +use super::Scope; +use crate::propagation::Domains; + +/// A [`ConsistencyChecker`] wrapper that skips the inner check when the associated constraint has +/// been deleted. +/// +/// The deletion flag is shared with the constraint owner (e.g. the nogood propagator). Setting the +/// flag to `true` causes the checker to become a permanent no-op. +#[derive(Debug, Clone)] +pub struct SelfDisablingChecker { + pub inner: BoxedConsistencyChecker, + pub is_deleted: Arc, +} + +impl ConsistencyChecker for SelfDisablingChecker { + fn check_consistency(&mut self, scope: &Scope, domains: Domains<'_>) -> bool { + if self.is_deleted.load(Ordering::Relaxed) { + return true; + } + self.inner.check_consistency(scope, domains) + } +} diff --git a/pumpkin-crates/core/src/engine/cp/test_solver.rs b/pumpkin-crates/core/src/engine/cp/test_solver.rs index 4bbcdf795..87bfcca8a 100644 --- a/pumpkin-crates/core/src/engine/cp/test_solver.rs +++ b/pumpkin-crates/core/src/engine/cp/test_solver.rs @@ -232,14 +232,32 @@ impl TestSolver { } pub fn propagate(&mut self, propagator: PropagatorId) -> Result<(), Conflict> { + let State { + propagators, + trailed_values, + assignments, + reason_store, + notification_engine, + #[cfg(feature = "check-consistency")] + consistency_checkers, + #[cfg(feature = "check-propagations")] + checkers, + .. + } = &mut self.state; + let context = PropagationContext::new( - &mut self.state.trailed_values, - &mut self.state.assignments, - &mut self.state.reason_store, - &mut self.state.notification_engine, + trailed_values, + assignments, + reason_store, + notification_engine, propagator, + #[cfg(feature = "check-consistency")] + consistency_checkers, + #[cfg(feature = "check-propagations")] + checkers, ); - self.state.propagators[propagator].propagate(context) + + propagators[propagator].propagate(context) } pub fn propagate_until_fixed_point( @@ -251,14 +269,32 @@ impl TestSolver { loop { { // Specify the life-times to be able to retrieve the trail entries + let State { + propagators, + trailed_values, + assignments, + reason_store, + notification_engine, + #[cfg(feature = "check-consistency")] + consistency_checkers, + #[cfg(feature = "check-propagations")] + checkers, + .. + } = &mut self.state; + let context = PropagationContext::new( - &mut self.state.trailed_values, - &mut self.state.assignments, - &mut self.state.reason_store, - &mut self.state.notification_engine, + trailed_values, + assignments, + reason_store, + notification_engine, propagator, + #[cfg(feature = "check-consistency")] + consistency_checkers, + #[cfg(feature = "check-propagations")] + checkers, ); - self.state.propagators[propagator].propagate(context)?; + + propagators[propagator].propagate(context)?; self.notify_propagator(propagator); } if self.state.assignments.num_trail_entries() == num_trail_entries { diff --git a/pumpkin-crates/core/src/engine/debug_helper.rs b/pumpkin-crates/core/src/engine/debug_helper.rs index 8e7a334ae..0e90f5760 100644 --- a/pumpkin-crates/core/src/engine/debug_helper.rs +++ b/pumpkin-crates/core/src/engine/debug_helper.rs @@ -9,6 +9,10 @@ use super::notifications::NotificationEngine; use super::predicates::predicate::Predicate; use super::reason::ReasonStore; use crate::basic_types::PropositionalConjunction; +#[cfg(feature = "check-consistency")] +use crate::checkers::ConsistencyCheckerStore; +#[cfg(feature = "check-propagations")] +use crate::containers::HashMap; use crate::engine::cp::Assignments; use crate::propagation::ExplanationContext; use crate::propagation::PropagationContext; @@ -76,6 +80,11 @@ impl DebugHelper { let num_entries_on_trail_before_propagation = assignments_clone.num_trail_entries(); + #[cfg(feature = "check-consistency")] + let mut consistency_checkers = ConsistencyCheckerStore::default(); + #[cfg(feature = "check-propagations")] + let mut inference_checkers = HashMap::default(); + let mut reason_store = Default::default(); let context = PropagationContext::new( &mut trailed_values_clone, @@ -83,6 +92,10 @@ impl DebugHelper { &mut reason_store, &mut notification_engine_clone, PropagatorId(propagator_id as u32), + #[cfg(feature = "check-consistency")] + &mut consistency_checkers, + #[cfg(feature = "check-propagations")] + &mut inference_checkers, ); let propagation_status_cp = propagator.propagate_from_scratch(context); @@ -252,6 +265,11 @@ impl DebugHelper { notification_engine_clone.debug_create_from_assignments(&assignments_clone); if adding_predicates_was_successful { + #[cfg(feature = "check-consistency")] + let mut consistency_checkers = ConsistencyCheckerStore::default(); + #[cfg(feature = "check-propagations")] + let mut inference_checkers = HashMap::default(); + // Now propagate using the debug propagation method. let mut reason_store = Default::default(); let context = PropagationContext::new( @@ -260,6 +278,10 @@ impl DebugHelper { &mut reason_store, &mut notification_engine_clone, propagator_id, + #[cfg(feature = "check-consistency")] + &mut consistency_checkers, + #[cfg(feature = "check-propagations")] + &mut inference_checkers, ); let debug_propagation_status_cp = propagator.propagate_from_scratch(context); @@ -369,12 +391,21 @@ impl DebugHelper { loop { let num_predicates_before = assignments_clone.num_trail_entries(); + #[cfg(feature = "check-consistency")] + let mut consistency_checkers = ConsistencyCheckerStore::default(); + #[cfg(feature = "check-propagations")] + let mut inference_checkers = HashMap::default(); + let context = PropagationContext::new( &mut trailed_values_clone, &mut assignments_clone, &mut reason_store, &mut notification_engine_clone, propagator_id, + #[cfg(feature = "check-consistency")] + &mut consistency_checkers, + #[cfg(feature = "check-propagations")] + &mut inference_checkers, ); let debug_propagation_status_cp = propagator.propagate_from_scratch(context); @@ -433,6 +464,11 @@ impl DebugHelper { notification_engine_clone.debug_create_from_assignments(&assignments_clone); if adding_predicates_was_successful { + #[cfg(feature = "check-consistency")] + let mut consistency_checkers = ConsistencyCheckerStore::default(); + #[cfg(feature = "check-propagations")] + let mut inference_checkers = HashMap::default(); + // now propagate using the debug propagation method let mut reason_store = Default::default(); let context = PropagationContext::new( @@ -441,6 +477,10 @@ impl DebugHelper { &mut reason_store, &mut notification_engine_clone, propagator_id, + #[cfg(feature = "check-consistency")] + &mut consistency_checkers, + #[cfg(feature = "check-propagations")] + &mut inference_checkers, ); let debug_propagation_status_cp = propagator.propagate_from_scratch(context); assert!( diff --git a/pumpkin-crates/core/src/engine/state.rs b/pumpkin-crates/core/src/engine/state.rs index 01a85f195..49a3a855b 100644 --- a/pumpkin-crates/core/src/engine/state.rs +++ b/pumpkin-crates/core/src/engine/state.rs @@ -82,8 +82,8 @@ pub struct State { statistics: StateStatistics, /// Inference checkers to run in the propagation loop. - checkers: HashMap>>, - consistency_checkers: ConsistencyCheckerStore, + pub(crate) checkers: HashMap>>, + pub(crate) consistency_checkers: ConsistencyCheckerStore, } create_statistics_struct!(StateStatistics { @@ -422,16 +422,31 @@ impl State { &mut self, handle: PropagatorHandle

, ) -> (Option<&mut P>, PropagationContext<'_>) { - ( - self.propagators.get_propagator_mut(handle), - PropagationContext::new( - &mut self.trailed_values, - &mut self.assignments, - &mut self.reason_store, - &mut self.notification_engine, - handle.propagator_id(), - ), - ) + let Self { + propagators, + trailed_values, + assignments, + reason_store, + notification_engine, + #[cfg(feature = "check-consistency")] + consistency_checkers, + #[cfg(feature = "check-propagations")] + checkers, + .. + } = self; + let propagator = propagators.get_propagator_mut(handle); + let context = PropagationContext::new( + trailed_values, + assignments, + reason_store, + notification_engine, + handle.propagator_id(), + #[cfg(feature = "check-consistency")] + consistency_checkers, + #[cfg(feature = "check-propagations")] + checkers, + ); + (propagator, context) } } @@ -614,13 +629,29 @@ impl State { let num_trail_entries_before = self.assignments.num_trail_entries(); let propagation_status = { - let propagator = &mut self.propagators[propagator_id]; + let Self { + propagators, + trailed_values, + assignments, + reason_store, + notification_engine, + #[cfg(feature = "check-consistency")] + consistency_checkers, + #[cfg(feature = "check-propagations")] + checkers, + .. + } = self; + let propagator = &mut propagators[propagator_id]; let context = PropagationContext::new( - &mut self.trailed_values, - &mut self.assignments, - &mut self.reason_store, - &mut self.notification_engine, + trailed_values, + assignments, + reason_store, + notification_engine, propagator_id, + #[cfg(feature = "check-consistency")] + consistency_checkers, + #[cfg(feature = "check-propagations")] + checkers, ); propagator.propagate(context) }; @@ -1172,12 +1203,27 @@ impl State { } pub fn get_propagation_context(&mut self) -> PropagationContext<'_> { + let Self { + trailed_values, + assignments, + reason_store, + notification_engine, + #[cfg(feature = "check-consistency")] + consistency_checkers, + #[cfg(feature = "check-propagations")] + checkers, + .. + } = self; PropagationContext::new( - &mut self.trailed_values, - &mut self.assignments, - &mut self.reason_store, - &mut self.notification_engine, + trailed_values, + assignments, + reason_store, + notification_engine, PropagatorId(0), + #[cfg(feature = "check-consistency")] + consistency_checkers, + #[cfg(feature = "check-propagations")] + checkers, ) } } diff --git a/pumpkin-crates/core/src/propagation/constructor.rs b/pumpkin-crates/core/src/propagation/constructor.rs index ffbc4ebbb..5c91b1136 100644 --- a/pumpkin-crates/core/src/propagation/constructor.rs +++ b/pumpkin-crates/core/src/propagation/constructor.rs @@ -72,6 +72,10 @@ pub struct PropagatorConstructorContext<'a> { /// Pending inference checkers to be registered into [`State`] when this context is dropped. #[cfg(feature = "check-propagations")] + #[allow( + clippy::type_complexity, + reason = "it's not clear where the type alias would live" + )] pub(crate) pending_inference_checkers: RefOrOwned<'a, Vec<(InferenceCode, Box>)>>, } diff --git a/pumpkin-crates/core/src/propagation/contexts/propagation_context.rs b/pumpkin-crates/core/src/propagation/contexts/propagation_context.rs index ab3218552..7f929f78f 100644 --- a/pumpkin-crates/core/src/propagation/contexts/propagation_context.rs +++ b/pumpkin-crates/core/src/propagation/contexts/propagation_context.rs @@ -1,4 +1,10 @@ +use pumpkin_checking::BoxedChecker; +use pumpkin_checking::InferenceChecker; + use crate::basic_types::PredicateId; +use crate::checkers::BoxedConsistencyChecker; +use crate::checkers::Scope; +use crate::containers::HashMap; use crate::engine::Assignments; use crate::engine::EmptyDomain; use crate::engine::EmptyDomainConflict; @@ -10,6 +16,7 @@ use crate::engine::reason::Reason; use crate::engine::reason::ReasonStore; use crate::engine::reason::StoredReason; use crate::engine::variables::Literal; +use crate::proof::InferenceCode; use crate::propagation::DomainEvents; use crate::propagation::Domains; use crate::propagation::HasAssignments; @@ -84,6 +91,11 @@ pub struct PropagationContext<'a> { pub(crate) propagator_id: PropagatorId, pub(crate) notification_engine: &'a mut NotificationEngine, reification_literal: Option, + + #[cfg(feature = "check-consistency")] + pub(crate) consistency_checkers: &'a mut ConsistencyCheckerStore, + #[cfg(feature = "check-propagations")] + pub(crate) inference_checkers: &'a mut HashMap>>, } impl<'a> HasAssignments for PropagationContext<'a> { @@ -107,6 +119,11 @@ impl<'a> PropagationContext<'a> { reason_store: &'a mut ReasonStore, notification_engine: &'a mut NotificationEngine, propagator_id: PropagatorId, + #[cfg(feature = "check-consistency")] consistency_checkers: &'a mut ConsistencyCheckerStore, + #[cfg(feature = "check-propagations")] inference_checkers: &'a mut HashMap< + InferenceCode, + Vec>, + >, ) -> Self { PropagationContext { trailed_values, @@ -115,6 +132,62 @@ impl<'a> PropagationContext<'a> { propagator_id, notification_engine, reification_literal: None, + #[cfg(feature = "check-consistency")] + consistency_checkers, + #[cfg(feature = "check-propagations")] + inference_checkers, + } + } + + /// Add a consistency checker for the given constraint and scope. + /// + /// If the `check-consistency` feature is not enabled, this is a no-op. + pub fn add_consistency_checker( + &mut self, + scope: impl Into, + checker: impl Into, + ) { + pumpkin_assert_simple!( + self.reification_literal.is_none(), + "Cannot add consistency checkers from within a reified propagation context." + ); + + #[cfg(feature = "check-consistency")] + self.consistency_checkers + .register(scope.into(), checker.into()); + + // Use variables to avoid unused warnings. + #[cfg(not(feature = "check-consistency"))] + { + let _ = scope; + let _ = checker; + } + } + + /// Add an inference checker for inferences produced by the propagator. + /// + /// If the `check-propagations` feature is not enabled, this is a no-op. + pub fn add_inference_checker( + &mut self, + inference_code: InferenceCode, + checker: Box>, + ) { + pumpkin_assert_simple!( + self.reification_literal.is_none(), + "Cannot add inference checkers from within a reified propagation context." + ); + + #[cfg(feature = "check-propagations")] + self.inference_checkers + .entry(inference_code) + .or_default() + .push(BoxedChecker::from(checker)); + + // Use variables to avoid unused warnings. + #[cfg(not(feature = "check-propagations"))] + { + let _ = inference_code; + let _ = checker; } } @@ -224,6 +297,10 @@ impl<'a> PropagationContext<'a> { propagator_id: self.propagator_id, notification_engine: self.notification_engine, reification_literal: self.reification_literal, + #[cfg(feature = "check-consistency")] + consistency_checkers: self.consistency_checkers, + #[cfg(feature = "check-propagations")] + inference_checkers: self.inference_checkers, } } } diff --git a/pumpkin-crates/core/src/propagators/nogoods/checker.rs b/pumpkin-crates/core/src/propagators/nogoods/checker.rs index 700ee6a2c..285f65c81 100644 --- a/pumpkin-crates/core/src/propagators/nogoods/checker.rs +++ b/pumpkin-crates/core/src/propagators/nogoods/checker.rs @@ -2,6 +2,13 @@ use std::fmt::Debug; use pumpkin_checking::AtomicConstraint; use pumpkin_checking::InferenceChecker; +use pumpkin_checking::VariableState; + +use crate::checkers::ConsistencyChecker; +use crate::checkers::Scope; +use crate::predicates::Predicate; +use crate::propagation::Domains; +use crate::propagation::ReadDomains; #[derive(Debug, Clone)] pub struct NogoodChecker { @@ -12,12 +19,32 @@ impl InferenceChecker for NogoodChecker where Atomic: AtomicConstraint + Clone + Debug, { - fn check( - &self, - state: pumpkin_checking::VariableState, - _: &[Atomic], - _: Option<&Atomic>, - ) -> bool { + fn check(&self, state: VariableState, _: &[Atomic], _: Option<&Atomic>) -> bool { self.nogood.iter().all(|atomic| state.is_true(atomic)) } } + +impl ConsistencyChecker for NogoodChecker { + fn check_consistency(&mut self, _: &Scope, domains: Domains<'_>) -> bool { + // For unit propagation, the state is consistent if: + // - at least two predicates are unassigned + // - or otherwise, at least one predicate is assigned + + let untrue_predicate_count = self + .nogood + .iter() + .filter(|&&predicate| domains.evaluate_predicate(predicate) != Some(true)) + .count(); + + if untrue_predicate_count >= 2 { + // If at least two predicates are not true, then the domain is + // unit-propagation consistent. + return true; + } + + // At least one predicate must be false for the domain to be unit-propagation consistent. + self.nogood + .iter() + .any(|&predicate| domains.evaluate_predicate(predicate) == Some(false)) + } +} diff --git a/pumpkin-crates/core/src/propagators/nogoods/nogood_propagator.rs b/pumpkin-crates/core/src/propagators/nogoods/nogood_propagator.rs index 4312f65cd..a0f622242 100644 --- a/pumpkin-crates/core/src/propagators/nogoods/nogood_propagator.rs +++ b/pumpkin-crates/core/src/propagators/nogoods/nogood_propagator.rs @@ -1,5 +1,11 @@ use std::cmp::max; use std::ops::Not; +#[cfg(feature = "check-consistency")] +use std::sync::Arc; +#[cfg(feature = "check-consistency")] +use std::sync::atomic::AtomicBool; +#[cfg(feature = "check-consistency")] +use std::sync::atomic::Ordering; use log::warn; @@ -8,6 +14,10 @@ use super::NogoodId; use super::NogoodInfo; use crate::basic_types::PredicateId; use crate::basic_types::PropositionalConjunction; +#[cfg(feature = "check-consistency")] +use crate::checkers::Scope; +#[cfg(feature = "check-consistency")] +use crate::checkers::SelfDisablingChecker; use crate::containers::KeyedVec; use crate::containers::StorageKey; use crate::engine::Assignments; @@ -23,6 +33,8 @@ use crate::proof::InferenceCode; use crate::propagation::EnqueueDecision; use crate::propagation::ExplanationContext; use crate::propagation::LazyExplanation; +#[cfg(feature = "check-consistency")] +use crate::propagation::LocalId; use crate::propagation::NotificationContext; use crate::propagation::Priority; use crate::propagation::PropagationContext; @@ -79,6 +91,13 @@ pub struct NogoodPropagator { /// current subtree. To test for that, we compare this handle with the propagator ID of a /// proapgated literal to see if this propagator propagated a predicate. handle: PropagatorHandle, + + /// Flags shared with consistency checkers to signal that a nogood has been deleted. + /// + /// When clause management deletes a nogood, the corresponding flag is set to `true`, causing + /// the checker to become a no-op. + #[cfg(feature = "check-consistency")] + deletion_flags: KeyedVec>, } /// [`PropagatorConstructor`] for constructing a new instance of the [`NogoodPropagator`] with the @@ -117,6 +136,8 @@ impl PropagatorConstructor for NogoodPropagatorConstructor { lbd_helper: Default::default(), bumped_nogoods: Default::default(), temp_nogood_reason: Default::default(), + #[cfg(feature = "check-consistency")] + deletion_flags: Default::default(), } } } @@ -452,6 +473,10 @@ impl NogoodPropagator { .lbd_helper .compute_lbd(&nogood.as_slice()[1..], context); + // Capture checker predicates before conversion to PredicateIds. + #[cfg(any(feature = "check-consistency", feature = "check-propagations"))] + let checker_predicates: Box<[Predicate]> = nogood.clone().into(); + let nogood = nogood .iter() .map(|predicate| context.get_id(*predicate)) @@ -464,7 +489,34 @@ impl NogoodPropagator { let _ = self .nogood_info .push(NogoodInfo::new_learned_nogood_info(lbd)); - let _ = self.inference_codes.push(inference_code); + let _ = self.inference_codes.push(inference_code.clone()); + + #[cfg(feature = "check-consistency")] + { + let flag = Arc::new(AtomicBool::new(false)); + let _ = self.deletion_flags.push(flag.clone()); + let scope = NogoodPropagator::build_nogood_scope(&checker_predicates); + context.add_consistency_checker( + scope, + SelfDisablingChecker { + inner: super::NogoodChecker { + nogood: checker_predicates.clone(), + } + .into(), + is_deleted: flag, + }, + ); + } + + #[cfg(feature = "check-propagations")] + { + context.add_inference_checker( + inference_code, + Box::new(super::NogoodChecker { + nogood: checker_predicates, + }), + ); + } let watcher = Watcher { nogood_id, @@ -619,6 +671,10 @@ impl NogoodPropagator { // // The preprocessing ensures that all predicates are unassigned. else { + // Capture the checker predicates before conversion to PredicateIds. + #[cfg(any(feature = "check-consistency", feature = "check-propagations"))] + let checker_predicates: Box<[Predicate]> = input_nogood.clone().into(); + #[cfg(feature = "check-propagations")] let nogood = input_nogood .iter() @@ -638,10 +694,37 @@ impl NogoodPropagator { let _ = self .nogood_info .push(NogoodInfo::new_permanent_nogood_info()); - let _ = self.inference_codes.push(inference_code); + let _ = self.inference_codes.push(inference_code.clone()); self.permanent_nogood_ids.push(nogood_id); + #[cfg(feature = "check-consistency")] + { + let flag = Arc::new(AtomicBool::new(false)); + let _ = self.deletion_flags.push(flag.clone()); + let scope = NogoodPropagator::build_nogood_scope(&checker_predicates); + context.add_consistency_checker( + scope, + SelfDisablingChecker { + inner: super::NogoodChecker { + nogood: checker_predicates.clone(), + } + .into(), + is_deleted: flag, + }, + ); + } + + #[cfg(feature = "check-propagations")] + { + context.add_inference_checker( + inference_code, + Box::new(super::NogoodChecker { + nogood: checker_predicates, + }), + ); + } + let watcher = Watcher { nogood_id, cached_predicate: self.nogood_predicates[nogood_id][0], @@ -665,6 +748,43 @@ impl NogoodPropagator { } } +/// Methods concerning checkers +#[cfg(any(feature = "check-consistency", feature = "check-propagations"))] +impl NogoodPropagator { + /// Build a [`Scope`] for a nogood by extracting unique [`DomainId`]s from its predicates. + #[cfg(feature = "check-consistency")] + fn build_nogood_scope(predicates: &[Predicate]) -> Scope { + use crate::containers::HashSet; + + let mut scope = Scope::default(); + let mut seen: HashSet = HashSet::default(); + let mut next_id = 0u32; + for predicate in predicates { + let domain = predicate.get_domain(); + if seen.insert(domain) { + scope.add_domain(LocalId::from(next_id), domain); + next_id += 1; + } + } + scope + } +} + +#[cfg(feature = "check-consistency")] +impl NogoodPropagator { + /// Set the deletion flag for every nogood that has been marked as deleted in `nogood_info`. + /// + /// Called after clause management removes nogoods so that consistency checkers self-disable. + fn signal_deleted_checker_flags(&self) { + for idx in 0..self.nogood_info.len() { + let idx = NogoodIndex::create_from_index(idx); + if self.nogood_info[idx].is_deleted { + self.deletion_flags[idx].store(true, Ordering::Relaxed); + } + } + } +} + /// Methods concerning the watchers and watch lists impl NogoodPropagator { /// Adds a watcher to the predicate. @@ -810,6 +930,8 @@ impl NogoodPropagator { } if removed_at_least_one_nogood { + #[cfg(feature = "check-consistency")] + self.signal_deleted_checker_flags(); self.remove_deleted_nogoods_from_watchers(assignments, notification_engine); } } From 1c9f258002a71c56bc2c519403ce1d98f644dabe Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Fri, 22 May 2026 16:53:08 +1000 Subject: [PATCH 10/23] Cleanup code --- .../core/src/checkers/consistency_checker.rs | 38 ++++++++++++++ pumpkin-crates/core/src/checkers/mod.rs | 39 +-------------- pumpkin-crates/core/src/checkers/store.rs | 44 +++++++++++------ .../core/src/checkers/strong_consistency.rs | 20 +++++++- pumpkin-crates/core/src/checkers/support.rs | 19 ++++--- .../core/src/containers/keyed_bit_set.rs | 6 +++ pumpkin-crates/core/src/engine/state.rs | 3 ++ .../core/src/propagation/constructor.rs | 12 +++-- .../contexts/propagation_context.rs | 4 ++ .../propagators/nogoods/nogood_propagator.rs | 49 +++++++++---------- .../arithmetic/multiplication/checker.rs | 8 --- 11 files changed, 145 insertions(+), 97 deletions(-) create mode 100644 pumpkin-crates/core/src/checkers/consistency_checker.rs diff --git a/pumpkin-crates/core/src/checkers/consistency_checker.rs b/pumpkin-crates/core/src/checkers/consistency_checker.rs new file mode 100644 index 000000000..2340ec5eb --- /dev/null +++ b/pumpkin-crates/core/src/checkers/consistency_checker.rs @@ -0,0 +1,38 @@ +use std::fmt::Debug; + +use dyn_clone::DynClone; + +use crate::{checkers::Scope, propagation::Domains}; + +/// A runtime verifier of the consistency of a propagator. +pub trait ConsistencyChecker: Debug + DynClone { + /// Ensure the domains of all items in the scope are at the advertised consistency level. + /// + /// Returns `true` if the consistency check passes, or `false` otherwise. + fn check_consistency(&mut self, scope: &Scope, domains: Domains<'_>) -> bool; +} + +/// Wrapper around `Box` that implements [`Clone`]. +#[derive(Debug)] +pub struct BoxedConsistencyChecker(Box); + +impl Clone for BoxedConsistencyChecker { + fn clone(&self) -> Self { + BoxedConsistencyChecker(dyn_clone::clone_box(&*self.0)) + } +} + +impl From for BoxedConsistencyChecker +where + T: ConsistencyChecker + 'static, +{ + fn from(value: T) -> Self { + BoxedConsistencyChecker(Box::new(value)) + } +} + +impl BoxedConsistencyChecker { + pub fn check_consistency(&mut self, scope: &Scope, domains: Domains<'_>) -> bool { + self.0.check_consistency(scope, domains) + } +} diff --git a/pumpkin-crates/core/src/checkers/mod.rs b/pumpkin-crates/core/src/checkers/mod.rs index c97161e15..cffe28962 100644 --- a/pumpkin-crates/core/src/checkers/mod.rs +++ b/pumpkin-crates/core/src/checkers/mod.rs @@ -1,47 +1,12 @@ +mod consistency_checker; mod scope; mod self_disabling; mod store; mod strong_consistency; pub mod support; -use std::fmt::Debug; - -use dyn_clone::DynClone; +pub use consistency_checker::*; pub use scope::*; pub use self_disabling::*; pub use store::*; pub use strong_consistency::*; - -use crate::propagation::Domains; - -pub trait ConsistencyChecker: Debug + DynClone { - /// Ensure the domains of all items in the scope are at the advertised consistency level. - /// - /// Returns `true` if the consistency check passes, or `false` otherwise. - fn check_consistency(&mut self, scope: &Scope, domains: Domains<'_>) -> bool; -} - -/// Wrapper around `Box` that implements [`Clone`]. -#[derive(Debug)] -pub struct BoxedConsistencyChecker(Box); - -impl Clone for BoxedConsistencyChecker { - fn clone(&self) -> Self { - BoxedConsistencyChecker(dyn_clone::clone_box(&*self.0)) - } -} - -impl From for BoxedConsistencyChecker -where - T: ConsistencyChecker + 'static, -{ - fn from(value: T) -> Self { - BoxedConsistencyChecker(Box::new(value)) - } -} - -impl BoxedConsistencyChecker { - pub fn check_consistency(&mut self, scope: &Scope, domains: Domains<'_>) -> bool { - self.0.check_consistency(scope, domains) - } -} diff --git a/pumpkin-crates/core/src/checkers/store.rs b/pumpkin-crates/core/src/checkers/store.rs index b116cd143..6b7705277 100644 --- a/pumpkin-crates/core/src/checkers/store.rs +++ b/pumpkin-crates/core/src/checkers/store.rs @@ -6,29 +6,25 @@ use crate::containers::StorageKey; use crate::propagation::Domains; use crate::variables::DomainId; -#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] -pub struct CheckerId(u32); - -impl StorageKey for CheckerId { - fn index(&self) -> usize { - self.0 as usize - } - - fn create_from_index(index: usize) -> Self { - CheckerId(index as u32) - } -} - +/// Holds the consistency checkers in the solver. +/// +/// Also responsible for enqueueing the checkers and dispatching them when instructed via +/// [`ConsistencyCheckerStore::run_enqueued`]. #[derive(Clone, Debug, Default)] pub struct ConsistencyCheckerStore { + /// The checkers in the store. store: KeyedVec, + /// Map from [`DomainId`] to the relevant checkers via their ID. watch_list: KeyedVec>, - + /// The checkers to run the next time. queue: Vec, + /// Marks which checkers are enqueued to prevent duplicate checkers in + /// [`ConsistencyCheckerStore::queue`]. enqueued: KeyedBitSet, } impl ConsistencyCheckerStore { + /// Add a new `checker` to the store with the given `scope`. pub fn register(&mut self, scope: Scope, checker: BoxedConsistencyChecker) { let checker_slot = self.store.new_slot(); @@ -71,4 +67,24 @@ impl ConsistencyCheckerStore { true } + + /// Clear the queue of consistency checkers. + pub fn clear_queue(&mut self) { + self.queue.clear(); + self.enqueued.clear(); + } +} + +/// An identifier for added checkers. +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] +struct CheckerId(u32); + +impl StorageKey for CheckerId { + fn index(&self) -> usize { + self.0 as usize + } + + fn create_from_index(index: usize) -> Self { + CheckerId(index as u32) + } } diff --git a/pumpkin-crates/core/src/checkers/strong_consistency.rs b/pumpkin-crates/core/src/checkers/strong_consistency.rs index 0ac823d41..387f26cdb 100644 --- a/pumpkin-crates/core/src/checkers/strong_consistency.rs +++ b/pumpkin-crates/core/src/checkers/strong_consistency.rs @@ -16,12 +16,18 @@ pub enum StrongConsistency { Bounds, } +/// A [`ConsistencyChecker`] that enforces a strong consistency property. +/// +/// The level of consistency is configured via [`StrongConsistency`]. #[derive(Clone, Debug)] pub struct StrongConsistencyChecker { + /// The generator of supports. supports: Supports, + /// A cache of domain-value pairs that are supported. supported_values: HashSet<(DomainId, i32)>, + /// The consistency level to test for. consistency_level: StrongConsistency, - + /// Re-usable buffer of the current support that is operated on. support: Support, } @@ -38,6 +44,8 @@ impl StrongConsistencyChecker { impl ConsistencyChecker for StrongConsistencyChecker { fn check_consistency(&mut self, scope: &Scope, domains: Domains<'_>) -> bool { + // Make sure to clear the cache of supported values. At the beginning, no values are + // supported. self.supported_values.clear(); for (local_id, domain) in scope.domains() { @@ -52,9 +60,12 @@ impl ConsistencyChecker for StrongConsistencyChecker for value in values_to_support { if self.supported_values.contains(&(domain, value)) { + // If this domain-value pair is already supported in this check + // then there is no need to generate a new support for it. continue; } + // Generate the support for this domain-value pair. self.supports.support( &mut self.support, local_id, @@ -63,16 +74,23 @@ impl ConsistencyChecker for StrongConsistencyChecker ); if !self.process_support(&domains) { + // The support was incomplete or not a solution. Either way, the + // consistency check fails. return false; } } } + // All required values are successfully supported, so the check passes. true } } impl StrongConsistencyChecker { + /// Tests whether the [`StrongConsistencyChecker::support`] is a valid support. + /// + /// Drains the support in the process, so it can be used again by subsequent calls to + /// [`SupportGenerator::support`]. fn process_support(&mut self, domains: &Domains<'_>) -> bool { if !self.supports.is_solution(&self.support) { log::error!("Support is not a solution"); diff --git a/pumpkin-crates/core/src/checkers/support.rs b/pumpkin-crates/core/src/checkers/support.rs index 4e3617558..fb06a3077 100644 --- a/pumpkin-crates/core/src/checkers/support.rs +++ b/pumpkin-crates/core/src/checkers/support.rs @@ -29,11 +29,14 @@ pub trait SupportGenerator: Clone + Debug { ); /// Returns true if the support is a solution to the constraint. + /// + /// Called with the support generated by [`SupportGenerator::support`]. fn is_solution(&self, support: &Support) -> bool; } /// A value that may be used in a [`Support`]. pub trait SupportValue: Clone + Debug { + /// Returns `true` if `self` is in the given domain. fn is_in(&self, domain: DomainId, domains: &Domains<'_>) -> bool; /// If the value is an integer, we can cache it to prevent recreating supports for the same @@ -112,11 +115,20 @@ impl Support { #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct UnsupportedValue(pub(crate) i32); +/// Implementors know how to unpack an [`UnsupportedValue`]. pub trait UnpackUnsupportedValue { /// Turn the unsupported value into an item in the domain of [`self`] (the variable). fn unpack(&self, unsupported_value: UnsupportedValue) -> i32; } +impl UnpackUnsupportedValue for i32 { + fn unpack(&self, UnsupportedValue(value): UnsupportedValue) -> i32 { + assert_eq!(value, *self); + value + } +} + +/// A trait to identify types that can serve as variables in a support. pub trait SupportsValue: UnpackUnsupportedValue { /// Add the assignment `self = value` to the `support`. fn assign(&self, value: Value, support: &mut Support); @@ -129,13 +141,6 @@ pub trait SupportsValue: UnpackUnsupportedValue { fn support_value(&self, support: &Support) -> Value; } -impl UnpackUnsupportedValue for i32 { - fn unpack(&self, UnsupportedValue(value): UnsupportedValue) -> i32 { - assert_eq!(value, *self); - value - } -} - impl SupportsValue for i32 { fn assign(&self, _: i32, _: &mut Support) { // Do nothing diff --git a/pumpkin-crates/core/src/containers/keyed_bit_set.rs b/pumpkin-crates/core/src/containers/keyed_bit_set.rs index c57715cf7..194a22874 100644 --- a/pumpkin-crates/core/src/containers/keyed_bit_set.rs +++ b/pumpkin-crates/core/src/containers/keyed_bit_set.rs @@ -4,6 +4,7 @@ use bit_set::BitSet; use crate::containers::StorageKey; +/// A bit-set for types that implement [`StorageKey`]. #[derive(Debug)] pub struct KeyedBitSet { bitset: BitSet, @@ -29,6 +30,11 @@ impl KeyedBitSet { pub fn drain(&self) -> impl Iterator { self.bitset.iter().map(Key::create_from_index) } + + /// Remove all keys in the set. + pub fn clear(&mut self) { + self.bitset.make_empty(); + } } impl Clone for KeyedBitSet { diff --git a/pumpkin-crates/core/src/engine/state.rs b/pumpkin-crates/core/src/engine/state.rs index 49a3a855b..f4f0782af 100644 --- a/pumpkin-crates/core/src/engine/state.rs +++ b/pumpkin-crates/core/src/engine/state.rs @@ -689,6 +689,9 @@ impl State { #[cfg(feature = "check-propagations")] self.check_conflict(&conflict); + #[cfg(feature = "check-propagations")] + self.consistency_checkers.clear_queue(); + self.statistics.num_conflicts += 1; if let Conflict::Propagator(inner) = &conflict { pumpkin_assert_advanced!(DebugHelper::debug_reported_failure( diff --git a/pumpkin-crates/core/src/propagation/constructor.rs b/pumpkin-crates/core/src/propagation/constructor.rs index 5c91b1136..d8cc08d87 100644 --- a/pumpkin-crates/core/src/propagation/constructor.rs +++ b/pumpkin-crates/core/src/propagation/constructor.rs @@ -54,17 +54,17 @@ pub trait PropagatorConstructor { /// of variables and to retrieve the current bounds of variables. #[derive(Debug)] pub struct PropagatorConstructorContext<'a> { - pub(crate) state: &'a mut State, + state: &'a mut State, pub(crate) propagator_id: PropagatorId, /// A [`LocalId`] that is guaranteed not to be used to register any variables yet. This is /// either a reference or an owned value, to support /// [`PropagatorConstructorContext::reborrow`]. - pub(crate) next_local_id: RefOrOwned<'a, LocalId>, + next_local_id: RefOrOwned<'a, LocalId>, /// Marker to indicate whether the constructor registered for at least one domain event or /// predicate becoming assigned. If not, the [`Drop`] implementation will cause a panic. - pub(crate) did_register: RefOrOwned<'a, bool>, + did_register: RefOrOwned<'a, bool>, /// Pending consistency checkers to be registered into [`State`] when this context is dropped. #[cfg(feature = "check-consistency")] @@ -216,6 +216,7 @@ impl PropagatorConstructorContext<'_> { self.pending_consistency_checkers .push((scope.into(), checker.into())); + // Avoid unused variable warning. #[cfg(not(feature = "check-consistency"))] { let _ = scope; @@ -235,6 +236,7 @@ impl PropagatorConstructorContext<'_> { self.pending_inference_checkers .push((inference_code, checker)); + // Avoid unused variable warning. #[cfg(not(feature = "check-propagations"))] { let _ = inference_code; @@ -272,12 +274,12 @@ impl Drop for PropagatorConstructorContext<'_> { } #[cfg(feature = "check-consistency")] - for (scope, checker) in std::mem::take(&mut *self.pending_consistency_checkers) { + for (scope, checker) in self.pending_consistency_checkers.drain(..) { self.state.add_consistency_checker(scope, checker); } #[cfg(feature = "check-propagations")] - for (inference_code, checker) in std::mem::take(&mut *self.pending_inference_checkers) { + for (inference_code, checker) in self.pending_inference_checkers.drain(..) { self.state.add_inference_checker(inference_code, checker); } } diff --git a/pumpkin-crates/core/src/propagation/contexts/propagation_context.rs b/pumpkin-crates/core/src/propagation/contexts/propagation_context.rs index 7f929f78f..153899019 100644 --- a/pumpkin-crates/core/src/propagation/contexts/propagation_context.rs +++ b/pumpkin-crates/core/src/propagation/contexts/propagation_context.rs @@ -1,9 +1,13 @@ +#[cfg(feature = "check-propagations")] use pumpkin_checking::BoxedChecker; use pumpkin_checking::InferenceChecker; use crate::basic_types::PredicateId; use crate::checkers::BoxedConsistencyChecker; +#[cfg(feature = "check-consistency")] +use crate::checkers::ConsistencyCheckerStore; use crate::checkers::Scope; +#[cfg(feature = "check-propagations")] use crate::containers::HashMap; use crate::engine::Assignments; use crate::engine::EmptyDomain; diff --git a/pumpkin-crates/core/src/propagators/nogoods/nogood_propagator.rs b/pumpkin-crates/core/src/propagators/nogoods/nogood_propagator.rs index a0f622242..132b1da35 100644 --- a/pumpkin-crates/core/src/propagators/nogoods/nogood_propagator.rs +++ b/pumpkin-crates/core/src/propagators/nogoods/nogood_propagator.rs @@ -33,8 +33,6 @@ use crate::proof::InferenceCode; use crate::propagation::EnqueueDecision; use crate::propagation::ExplanationContext; use crate::propagation::LazyExplanation; -#[cfg(feature = "check-consistency")] -use crate::propagation::LocalId; use crate::propagation::NotificationContext; use crate::propagation::Priority; use crate::propagation::PropagationContext; @@ -494,7 +492,7 @@ impl NogoodPropagator { #[cfg(feature = "check-consistency")] { let flag = Arc::new(AtomicBool::new(false)); - let _ = self.deletion_flags.push(flag.clone()); + let _ = self.deletion_flags.push(Arc::clone(&flag)); let scope = NogoodPropagator::build_nogood_scope(&checker_predicates); context.add_consistency_checker( scope, @@ -509,14 +507,12 @@ impl NogoodPropagator { } #[cfg(feature = "check-propagations")] - { - context.add_inference_checker( - inference_code, - Box::new(super::NogoodChecker { - nogood: checker_predicates, - }), - ); - } + context.add_inference_checker( + inference_code, + Box::new(super::NogoodChecker { + nogood: checker_predicates, + }), + ); let watcher = Watcher { nogood_id, @@ -701,7 +697,7 @@ impl NogoodPropagator { #[cfg(feature = "check-consistency")] { let flag = Arc::new(AtomicBool::new(false)); - let _ = self.deletion_flags.push(flag.clone()); + let _ = self.deletion_flags.push(Arc::clone(&flag)); let scope = NogoodPropagator::build_nogood_scope(&checker_predicates); context.add_consistency_checker( scope, @@ -716,14 +712,12 @@ impl NogoodPropagator { } #[cfg(feature = "check-propagations")] - { - context.add_inference_checker( - inference_code, - Box::new(super::NogoodChecker { - nogood: checker_predicates, - }), - ); - } + context.add_inference_checker( + inference_code, + Box::new(super::NogoodChecker { + nogood: checker_predicates, + }), + ); let watcher = Watcher { nogood_id, @@ -752,20 +746,25 @@ impl NogoodPropagator { #[cfg(any(feature = "check-consistency", feature = "check-propagations"))] impl NogoodPropagator { /// Build a [`Scope`] for a nogood by extracting unique [`DomainId`]s from its predicates. + /// + /// Avoids multiple enqueuing of the consistency checker if the nogood contains multiple + /// predicates over the same variable. #[cfg(feature = "check-consistency")] fn build_nogood_scope(predicates: &[Predicate]) -> Scope { - use crate::containers::HashSet; + use crate::containers::{HashSet, KeyGenerator}; + use crate::variables::DomainId; let mut scope = Scope::default(); - let mut seen: HashSet = HashSet::default(); - let mut next_id = 0u32; + let mut seen: HashSet = HashSet::default(); + let mut id_generator = KeyGenerator::default(); + for predicate in predicates { let domain = predicate.get_domain(); if seen.insert(domain) { - scope.add_domain(LocalId::from(next_id), domain); - next_id += 1; + scope.add_domain(id_generator.next_key(), domain); } } + scope } } diff --git a/pumpkin-crates/propagators/src/propagators/arithmetic/multiplication/checker.rs b/pumpkin-crates/propagators/src/propagators/arithmetic/multiplication/checker.rs index 4f8904898..cdb5fa200 100644 --- a/pumpkin-crates/propagators/src/propagators/arithmetic/multiplication/checker.rs +++ b/pumpkin-crates/propagators/src/propagators/arithmetic/multiplication/checker.rs @@ -77,18 +77,12 @@ where let c_min = domains.lower_bound(&self.c) as f32; let c_max = domains.upper_bound(&self.c) as f32; - dbg!((&self.a, &self.b, &self.c)); - dbg!((a_min, a_max)); - dbg!((b_min, b_max)); - dbg!((c_min, c_max)); - let (value_a, value_b, value_c) = match local_id { super::ID_A => { let value_a = self.a.unpack(unsupported_value) as f32; let Some((value_b, value_c)) = [b_min, b_max].into_iter().find_map(|value_b| { let value_c = value_a * value_b; - dbg!((value_a, value_b, value_c)); if c_min <= value_c && value_c <= c_max { Some((value_b, value_c)) } else { @@ -105,7 +99,6 @@ where let Some((value_a, value_c)) = [a_min, a_max].into_iter().find_map(|value_a| { let value_c = value_a * value_b; - dbg!((value_a, value_b, value_c)); if c_min <= value_c && value_c <= c_max { Some((value_a, value_c)) } else { @@ -122,7 +115,6 @@ where let Some(values) = [a_min, a_max].into_iter().find_map(|value_a| { let value_b = value_c / value_a; - dbg!((value_a, value_b, value_c)); if b_min <= value_b && value_b <= b_max { Some((value_a, value_b, value_c)) From 893989d114ba6e979a488b7d13505246d72a13a6 Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Sat, 23 May 2026 13:48:06 +1000 Subject: [PATCH 11/23] Various cleanup --- .../basic_types/propositional_conjunction.rs | 6 ++ .../core/src/checkers/consistency_checker.rs | 3 +- pumpkin-crates/core/src/checkers/scope.rs | 8 ++ .../core/src/checkers/self_disabling.rs | 26 ++++-- .../core/src/propagators/nogoods/checker.rs | 53 +++++++++++ .../propagators/nogoods/nogood_propagator.rs | 88 ++++++++----------- 6 files changed, 125 insertions(+), 59 deletions(-) diff --git a/pumpkin-crates/core/src/basic_types/propositional_conjunction.rs b/pumpkin-crates/core/src/basic_types/propositional_conjunction.rs index 97c2f2ef1..4a718b654 100644 --- a/pumpkin-crates/core/src/basic_types/propositional_conjunction.rs +++ b/pumpkin-crates/core/src/basic_types/propositional_conjunction.rs @@ -21,6 +21,12 @@ impl Deref for PropositionalConjunction { } } +impl Into> for PropositionalConjunction { + fn into(self) -> Box<[Predicate]> { + self.predicates_in_conjunction.into() + } +} + impl PropositionalConjunction { pub fn new(predicates_in_conjunction: Vec) -> Self { PropositionalConjunction { diff --git a/pumpkin-crates/core/src/checkers/consistency_checker.rs b/pumpkin-crates/core/src/checkers/consistency_checker.rs index 2340ec5eb..fbe8093a0 100644 --- a/pumpkin-crates/core/src/checkers/consistency_checker.rs +++ b/pumpkin-crates/core/src/checkers/consistency_checker.rs @@ -2,7 +2,8 @@ use std::fmt::Debug; use dyn_clone::DynClone; -use crate::{checkers::Scope, propagation::Domains}; +use crate::checkers::Scope; +use crate::propagation::Domains; /// A runtime verifier of the consistency of a propagator. pub trait ConsistencyChecker: Debug + DynClone { diff --git a/pumpkin-crates/core/src/checkers/scope.rs b/pumpkin-crates/core/src/checkers/scope.rs index af19db82c..b8b9c9279 100644 --- a/pumpkin-crates/core/src/checkers/scope.rs +++ b/pumpkin-crates/core/src/checkers/scope.rs @@ -8,6 +8,14 @@ pub struct Scope { domains: HashMap, } +impl FromIterator<(LocalId, DomainId)> for Scope { + fn from_iter>(iter: T) -> Self { + Scope { + domains: iter.into_iter().collect(), + } + } +} + impl Scope { /// Add a new domain to the scope with the given local id. /// diff --git a/pumpkin-crates/core/src/checkers/self_disabling.rs b/pumpkin-crates/core/src/checkers/self_disabling.rs index 8e7498b2a..63ae14f3d 100644 --- a/pumpkin-crates/core/src/checkers/self_disabling.rs +++ b/pumpkin-crates/core/src/checkers/self_disabling.rs @@ -2,7 +2,6 @@ use std::sync::Arc; use std::sync::atomic::AtomicBool; use std::sync::atomic::Ordering; -use super::BoxedConsistencyChecker; use super::ConsistencyChecker; use super::Scope; use crate::propagation::Domains; @@ -13,12 +12,29 @@ use crate::propagation::Domains; /// The deletion flag is shared with the constraint owner (e.g. the nogood propagator). Setting the /// flag to `true` causes the checker to become a permanent no-op. #[derive(Debug, Clone)] -pub struct SelfDisablingChecker { - pub inner: BoxedConsistencyChecker, - pub is_deleted: Arc, +pub struct SelfDisablingChecker { + inner: T, + is_deleted: Arc, } -impl ConsistencyChecker for SelfDisablingChecker { +impl SelfDisablingChecker { + /// Create a new self-disabling checker. + /// + /// The deletion flag can be obtained with [`SelfDisablingChecker::deletion_flag`]. + pub fn new(checker: T) -> Self { + SelfDisablingChecker { + inner: checker, + is_deleted: Arc::new(AtomicBool::new(false)), + } + } + + /// The deletion flag for this self-disabling checker. + pub fn deletion_flag(&self) -> Arc { + Arc::clone(&self.is_deleted) + } +} + +impl ConsistencyChecker for SelfDisablingChecker { fn check_consistency(&mut self, scope: &Scope, domains: Domains<'_>) -> bool { if self.is_deleted.load(Ordering::Relaxed) { return true; diff --git a/pumpkin-crates/core/src/propagators/nogoods/checker.rs b/pumpkin-crates/core/src/propagators/nogoods/checker.rs index 285f65c81..e1456e229 100644 --- a/pumpkin-crates/core/src/propagators/nogoods/checker.rs +++ b/pumpkin-crates/core/src/propagators/nogoods/checker.rs @@ -48,3 +48,56 @@ impl ConsistencyChecker for NogoodChecker { .any(|&predicate| domains.evaluate_predicate(predicate) == Some(false)) } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::conjunction; + use crate::propagation::LocalId; + use crate::state::State; + + #[test] + fn a_nogood_with_multiple_untrue_predicates_is_consistent() { + let mut state = State::default(); + + let x = state.new_interval_variable(1, 5, Some("x".into())); + let y = state.new_interval_variable(1, 5, Some("y".into())); + + let mut checker = NogoodChecker { + nogood: conjunction!([x >= 4] & [y <= 2]).into(), + }; + + let scope = Scope::from_iter([(LocalId::from(0), x), (LocalId::from(1), y)]); + assert!(checker.check_consistency(&scope, state.get_domains())); + } + + #[test] + fn a_nogood_with_one_untrue_predicates_and_no_false_predicates_is_inconsistent() { + let mut state = State::default(); + + let x = state.new_interval_variable(1, 5, Some("x".into())); + let y = state.new_interval_variable(1, 5, Some("y".into())); + + let mut checker = NogoodChecker { + nogood: conjunction!([x >= 4] & [y <= 5]).into(), + }; + + let scope = Scope::from_iter([(LocalId::from(0), x), (LocalId::from(1), y)]); + assert!(!checker.check_consistency(&scope, state.get_domains())); + } + + #[test] + fn a_nogood_with_any_false_predicates_is_consistent() { + let mut state = State::default(); + + let x = state.new_interval_variable(1, 3, Some("x".into())); + let y = state.new_interval_variable(1, 5, Some("y".into())); + + let mut checker = NogoodChecker { + nogood: conjunction!([x >= 4] & [y <= 2]).into(), + }; + + let scope = Scope::from_iter([(LocalId::from(0), x), (LocalId::from(1), y)]); + assert!(checker.check_consistency(&scope, state.get_domains())); + } +} diff --git a/pumpkin-crates/core/src/propagators/nogoods/nogood_propagator.rs b/pumpkin-crates/core/src/propagators/nogoods/nogood_propagator.rs index 132b1da35..291493054 100644 --- a/pumpkin-crates/core/src/propagators/nogoods/nogood_propagator.rs +++ b/pumpkin-crates/core/src/propagators/nogoods/nogood_propagator.rs @@ -490,21 +490,7 @@ impl NogoodPropagator { let _ = self.inference_codes.push(inference_code.clone()); #[cfg(feature = "check-consistency")] - { - let flag = Arc::new(AtomicBool::new(false)); - let _ = self.deletion_flags.push(Arc::clone(&flag)); - let scope = NogoodPropagator::build_nogood_scope(&checker_predicates); - context.add_consistency_checker( - scope, - SelfDisablingChecker { - inner: super::NogoodChecker { - nogood: checker_predicates.clone(), - } - .into(), - is_deleted: flag, - }, - ); - } + self.add_consistency_checker(checker_predicates.clone(), context); #[cfg(feature = "check-propagations")] context.add_inference_checker( @@ -695,21 +681,7 @@ impl NogoodPropagator { self.permanent_nogood_ids.push(nogood_id); #[cfg(feature = "check-consistency")] - { - let flag = Arc::new(AtomicBool::new(false)); - let _ = self.deletion_flags.push(Arc::clone(&flag)); - let scope = NogoodPropagator::build_nogood_scope(&checker_predicates); - context.add_consistency_checker( - scope, - SelfDisablingChecker { - inner: super::NogoodChecker { - nogood: checker_predicates.clone(), - } - .into(), - is_deleted: flag, - }, - ); - } + self.add_consistency_checker(checker_predicates.clone(), context); #[cfg(feature = "check-propagations")] context.add_inference_checker( @@ -740,33 +712,43 @@ impl NogoodPropagator { Ok(()) } } -} -/// Methods concerning checkers -#[cfg(any(feature = "check-consistency", feature = "check-propagations"))] -impl NogoodPropagator { - /// Build a [`Scope`] for a nogood by extracting unique [`DomainId`]s from its predicates. - /// - /// Avoids multiple enqueuing of the consistency checker if the nogood contains multiple - /// predicates over the same variable. + /// Add a consistency checker for the given nogood predicates. #[cfg(feature = "check-consistency")] - fn build_nogood_scope(predicates: &[Predicate]) -> Scope { - use crate::containers::{HashSet, KeyGenerator}; - use crate::variables::DomainId; - - let mut scope = Scope::default(); - let mut seen: HashSet = HashSet::default(); - let mut id_generator = KeyGenerator::default(); - - for predicate in predicates { - let domain = predicate.get_domain(); - if seen.insert(domain) { - scope.add_domain(id_generator.next_key(), domain); - } - } + fn add_consistency_checker( + &mut self, + nogood: Box<[Predicate]>, + context: &mut PropagationContext, + ) { + let scope = build_nogood_scope(&nogood); + let checker = SelfDisablingChecker::new(super::NogoodChecker { nogood }); + let _ = self.deletion_flags.push(checker.deletion_flag()); + context.add_consistency_checker(scope, checker); + } +} - scope +/// Build a [`Scope`] for a nogood by extracting unique [`DomainId`]s from its predicates. +/// +/// Avoids multiple enqueuing of the consistency checker if the nogood contains multiple +/// predicates over the same variable. +#[cfg(feature = "check-consistency")] +fn build_nogood_scope(predicates: &[Predicate]) -> Scope { + use crate::containers::HashSet; + use crate::containers::KeyGenerator; + use crate::variables::DomainId; + + let mut scope = Scope::default(); + let mut seen: HashSet = HashSet::default(); + let mut id_generator = KeyGenerator::default(); + + for predicate in predicates { + let domain = predicate.get_domain(); + if seen.insert(domain) { + scope.add_domain(id_generator.next_key(), domain); + } } + + scope } #[cfg(feature = "check-consistency")] From 2d999b7537823191b4924b1f4c8d95c8d092b703 Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Sun, 24 May 2026 12:24:51 +1000 Subject: [PATCH 12/23] Rename 'ConsistencyChecker' to 'RetentionChecker' --- .../core/src/checkers/consistency_checker.rs | 39 ------------------- pumpkin-crates/core/src/checkers/mod.rs | 8 ++-- .../core/src/checkers/retention_checker.rs | 39 +++++++++++++++++++ .../core/src/checkers/self_disabling.rs | 8 ++-- pumpkin-crates/core/src/checkers/store.rs | 8 ++-- ...istency.rs => strong_retention_checker.rs} | 14 +++---- pumpkin-crates/core/src/engine/state.rs | 4 +- .../core/src/propagation/constructor.rs | 6 +-- .../contexts/propagation_context.rs | 4 +- .../core/src/propagators/nogoods/checker.rs | 12 +++--- .../propagators/reified_propagator/checker.rs | 12 +++--- .../reified_propagator/constructor.rs | 4 +- .../arithmetic/binary_equals/constructor.rs | 4 +- .../arithmetic/multiplication/constructor.rs | 4 +- 14 files changed, 83 insertions(+), 83 deletions(-) delete mode 100644 pumpkin-crates/core/src/checkers/consistency_checker.rs create mode 100644 pumpkin-crates/core/src/checkers/retention_checker.rs rename pumpkin-crates/core/src/checkers/{strong_consistency.rs => strong_retention_checker.rs} (88%) diff --git a/pumpkin-crates/core/src/checkers/consistency_checker.rs b/pumpkin-crates/core/src/checkers/consistency_checker.rs deleted file mode 100644 index fbe8093a0..000000000 --- a/pumpkin-crates/core/src/checkers/consistency_checker.rs +++ /dev/null @@ -1,39 +0,0 @@ -use std::fmt::Debug; - -use dyn_clone::DynClone; - -use crate::checkers::Scope; -use crate::propagation::Domains; - -/// A runtime verifier of the consistency of a propagator. -pub trait ConsistencyChecker: Debug + DynClone { - /// Ensure the domains of all items in the scope are at the advertised consistency level. - /// - /// Returns `true` if the consistency check passes, or `false` otherwise. - fn check_consistency(&mut self, scope: &Scope, domains: Domains<'_>) -> bool; -} - -/// Wrapper around `Box` that implements [`Clone`]. -#[derive(Debug)] -pub struct BoxedConsistencyChecker(Box); - -impl Clone for BoxedConsistencyChecker { - fn clone(&self) -> Self { - BoxedConsistencyChecker(dyn_clone::clone_box(&*self.0)) - } -} - -impl From for BoxedConsistencyChecker -where - T: ConsistencyChecker + 'static, -{ - fn from(value: T) -> Self { - BoxedConsistencyChecker(Box::new(value)) - } -} - -impl BoxedConsistencyChecker { - pub fn check_consistency(&mut self, scope: &Scope, domains: Domains<'_>) -> bool { - self.0.check_consistency(scope, domains) - } -} diff --git a/pumpkin-crates/core/src/checkers/mod.rs b/pumpkin-crates/core/src/checkers/mod.rs index cffe28962..3040d9dae 100644 --- a/pumpkin-crates/core/src/checkers/mod.rs +++ b/pumpkin-crates/core/src/checkers/mod.rs @@ -1,12 +1,12 @@ -mod consistency_checker; +mod retention_checker; mod scope; mod self_disabling; mod store; -mod strong_consistency; +mod strong_retention_checker; pub mod support; -pub use consistency_checker::*; +pub use retention_checker::*; pub use scope::*; pub use self_disabling::*; pub use store::*; -pub use strong_consistency::*; +pub use strong_retention_checker::*; diff --git a/pumpkin-crates/core/src/checkers/retention_checker.rs b/pumpkin-crates/core/src/checkers/retention_checker.rs new file mode 100644 index 000000000..e3dd157bf --- /dev/null +++ b/pumpkin-crates/core/src/checkers/retention_checker.rs @@ -0,0 +1,39 @@ +use std::fmt::Debug; + +use dyn_clone::DynClone; + +use crate::checkers::Scope; +use crate::propagation::Domains; + +/// A runtime verifier that determines whether domains are sufficiently pruned. +pub trait RetentionChecker: Debug + DynClone { + /// Ensure the domains do not have values that should have been removed by propagation. + /// + /// Returns `true` if the domains are sufficiently pruned, or `false` otherwise. + fn check_retention(&mut self, scope: &Scope, domains: Domains<'_>) -> bool; +} + +/// Wrapper around `Box` that implements [`Clone`]. +#[derive(Debug)] +pub struct BoxedRetentionChecker(Box); + +impl Clone for BoxedRetentionChecker { + fn clone(&self) -> Self { + BoxedRetentionChecker(dyn_clone::clone_box(&*self.0)) + } +} + +impl From for BoxedRetentionChecker +where + T: RetentionChecker + 'static, +{ + fn from(value: T) -> Self { + BoxedRetentionChecker(Box::new(value)) + } +} + +impl BoxedRetentionChecker { + pub fn check_retention(&mut self, scope: &Scope, domains: Domains<'_>) -> bool { + self.0.check_retention(scope, domains) + } +} diff --git a/pumpkin-crates/core/src/checkers/self_disabling.rs b/pumpkin-crates/core/src/checkers/self_disabling.rs index 63ae14f3d..9a5770f3a 100644 --- a/pumpkin-crates/core/src/checkers/self_disabling.rs +++ b/pumpkin-crates/core/src/checkers/self_disabling.rs @@ -2,7 +2,7 @@ use std::sync::Arc; use std::sync::atomic::AtomicBool; use std::sync::atomic::Ordering; -use super::ConsistencyChecker; +use super::RetentionChecker; use super::Scope; use crate::propagation::Domains; @@ -34,11 +34,11 @@ impl SelfDisablingChecker { } } -impl ConsistencyChecker for SelfDisablingChecker { - fn check_consistency(&mut self, scope: &Scope, domains: Domains<'_>) -> bool { +impl RetentionChecker for SelfDisablingChecker { + fn check_retention(&mut self, scope: &Scope, domains: Domains<'_>) -> bool { if self.is_deleted.load(Ordering::Relaxed) { return true; } - self.inner.check_consistency(scope, domains) + self.inner.check_retention(scope, domains) } } diff --git a/pumpkin-crates/core/src/checkers/store.rs b/pumpkin-crates/core/src/checkers/store.rs index 6b7705277..79e141422 100644 --- a/pumpkin-crates/core/src/checkers/store.rs +++ b/pumpkin-crates/core/src/checkers/store.rs @@ -1,4 +1,4 @@ -use crate::checkers::BoxedConsistencyChecker; +use crate::checkers::BoxedRetentionChecker; use crate::checkers::Scope; use crate::containers::KeyedBitSet; use crate::containers::KeyedVec; @@ -13,7 +13,7 @@ use crate::variables::DomainId; #[derive(Clone, Debug, Default)] pub struct ConsistencyCheckerStore { /// The checkers in the store. - store: KeyedVec, + store: KeyedVec, /// Map from [`DomainId`] to the relevant checkers via their ID. watch_list: KeyedVec>, /// The checkers to run the next time. @@ -25,7 +25,7 @@ pub struct ConsistencyCheckerStore { impl ConsistencyCheckerStore { /// Add a new `checker` to the store with the given `scope`. - pub fn register(&mut self, scope: Scope, checker: BoxedConsistencyChecker) { + pub fn register(&mut self, scope: Scope, checker: BoxedRetentionChecker) { let checker_slot = self.store.new_slot(); for (_, domain) in scope.domains() { @@ -60,7 +60,7 @@ impl ConsistencyCheckerStore { let (scope, checker) = &mut self.store[checker_id]; - if !checker.check_consistency(scope, domains.reborrow()) { + if !checker.check_retention(scope, domains.reborrow()) { return false; } } diff --git a/pumpkin-crates/core/src/checkers/strong_consistency.rs b/pumpkin-crates/core/src/checkers/strong_retention_checker.rs similarity index 88% rename from pumpkin-crates/core/src/checkers/strong_consistency.rs rename to pumpkin-crates/core/src/checkers/strong_retention_checker.rs index 387f26cdb..3c151a994 100644 --- a/pumpkin-crates/core/src/checkers/strong_consistency.rs +++ b/pumpkin-crates/core/src/checkers/strong_retention_checker.rs @@ -1,5 +1,5 @@ use super::Scope; -use crate::checkers::ConsistencyChecker; +use crate::checkers::RetentionChecker; use crate::checkers::support::Support; use crate::checkers::support::SupportGenerator; use crate::checkers::support::SupportValue; @@ -20,7 +20,7 @@ pub enum StrongConsistency { /// /// The level of consistency is configured via [`StrongConsistency`]. #[derive(Clone, Debug)] -pub struct StrongConsistencyChecker { +pub struct StrongRetentionChecker { /// The generator of supports. supports: Supports, /// A cache of domain-value pairs that are supported. @@ -31,9 +31,9 @@ pub struct StrongConsistencyChecker { support: Support, } -impl StrongConsistencyChecker { +impl StrongRetentionChecker { pub fn new(consistency_level: StrongConsistency, supports: Supports) -> Self { - StrongConsistencyChecker { + StrongRetentionChecker { consistency_level, supports, supported_values: HashSet::default(), @@ -42,8 +42,8 @@ impl StrongConsistencyChecker { } } -impl ConsistencyChecker for StrongConsistencyChecker { - fn check_consistency(&mut self, scope: &Scope, domains: Domains<'_>) -> bool { +impl RetentionChecker for StrongRetentionChecker { + fn check_retention(&mut self, scope: &Scope, domains: Domains<'_>) -> bool { // Make sure to clear the cache of supported values. At the beginning, no values are // supported. self.supported_values.clear(); @@ -86,7 +86,7 @@ impl ConsistencyChecker for StrongConsistencyChecker } } -impl StrongConsistencyChecker { +impl StrongRetentionChecker { /// Tests whether the [`StrongConsistencyChecker::support`] is a valid support. /// /// Drains the support in the process, so it can be used again by subsequent calls to diff --git a/pumpkin-crates/core/src/engine/state.rs b/pumpkin-crates/core/src/engine/state.rs index f4f0782af..facb355c5 100644 --- a/pumpkin-crates/core/src/engine/state.rs +++ b/pumpkin-crates/core/src/engine/state.rs @@ -5,7 +5,7 @@ use pumpkin_checking::InferenceChecker; #[cfg(feature = "check-propagations")] use pumpkin_checking::VariableState; -use crate::checkers::BoxedConsistencyChecker; +use crate::checkers::BoxedRetentionChecker; use crate::checkers::ConsistencyCheckerStore; use crate::checkers::Scope; use crate::containers::HashMap; @@ -381,7 +381,7 @@ impl State { pub fn add_consistency_checker( &mut self, scope: impl Into, - checker: impl Into, + checker: impl Into, ) { self.consistency_checkers .register(scope.into(), checker.into()); diff --git a/pumpkin-crates/core/src/propagation/constructor.rs b/pumpkin-crates/core/src/propagation/constructor.rs index d8cc08d87..220441c3d 100644 --- a/pumpkin-crates/core/src/propagation/constructor.rs +++ b/pumpkin-crates/core/src/propagation/constructor.rs @@ -12,7 +12,7 @@ use super::PropagatorVarId; use crate::Solver; use crate::basic_types::PredicateId; use crate::basic_types::RefOrOwned; -use crate::checkers::BoxedConsistencyChecker; +use crate::checkers::BoxedRetentionChecker; use crate::checkers::Scope; use crate::engine::Assignments; use crate::engine::State; @@ -68,7 +68,7 @@ pub struct PropagatorConstructorContext<'a> { /// Pending consistency checkers to be registered into [`State`] when this context is dropped. #[cfg(feature = "check-consistency")] - pub(crate) pending_consistency_checkers: RefOrOwned<'a, Vec<(Scope, BoxedConsistencyChecker)>>, + pub(crate) pending_consistency_checkers: RefOrOwned<'a, Vec<(Scope, BoxedRetentionChecker)>>, /// Pending inference checkers to be registered into [`State`] when this context is dropped. #[cfg(feature = "check-propagations")] @@ -210,7 +210,7 @@ impl PropagatorConstructorContext<'_> { pub fn add_consistency_checker( &mut self, scope: impl Into, - checker: impl Into, + checker: impl Into, ) { #[cfg(feature = "check-consistency")] self.pending_consistency_checkers diff --git a/pumpkin-crates/core/src/propagation/contexts/propagation_context.rs b/pumpkin-crates/core/src/propagation/contexts/propagation_context.rs index 153899019..2878eec41 100644 --- a/pumpkin-crates/core/src/propagation/contexts/propagation_context.rs +++ b/pumpkin-crates/core/src/propagation/contexts/propagation_context.rs @@ -3,7 +3,7 @@ use pumpkin_checking::BoxedChecker; use pumpkin_checking::InferenceChecker; use crate::basic_types::PredicateId; -use crate::checkers::BoxedConsistencyChecker; +use crate::checkers::BoxedRetentionChecker; #[cfg(feature = "check-consistency")] use crate::checkers::ConsistencyCheckerStore; use crate::checkers::Scope; @@ -149,7 +149,7 @@ impl<'a> PropagationContext<'a> { pub fn add_consistency_checker( &mut self, scope: impl Into, - checker: impl Into, + checker: impl Into, ) { pumpkin_assert_simple!( self.reification_literal.is_none(), diff --git a/pumpkin-crates/core/src/propagators/nogoods/checker.rs b/pumpkin-crates/core/src/propagators/nogoods/checker.rs index e1456e229..381b15ba4 100644 --- a/pumpkin-crates/core/src/propagators/nogoods/checker.rs +++ b/pumpkin-crates/core/src/propagators/nogoods/checker.rs @@ -4,7 +4,7 @@ use pumpkin_checking::AtomicConstraint; use pumpkin_checking::InferenceChecker; use pumpkin_checking::VariableState; -use crate::checkers::ConsistencyChecker; +use crate::checkers::RetentionChecker; use crate::checkers::Scope; use crate::predicates::Predicate; use crate::propagation::Domains; @@ -24,8 +24,8 @@ where } } -impl ConsistencyChecker for NogoodChecker { - fn check_consistency(&mut self, _: &Scope, domains: Domains<'_>) -> bool { +impl RetentionChecker for NogoodChecker { + fn check_retention(&mut self, _: &Scope, domains: Domains<'_>) -> bool { // For unit propagation, the state is consistent if: // - at least two predicates are unassigned // - or otherwise, at least one predicate is assigned @@ -68,7 +68,7 @@ mod tests { }; let scope = Scope::from_iter([(LocalId::from(0), x), (LocalId::from(1), y)]); - assert!(checker.check_consistency(&scope, state.get_domains())); + assert!(checker.check_retention(&scope, state.get_domains())); } #[test] @@ -83,7 +83,7 @@ mod tests { }; let scope = Scope::from_iter([(LocalId::from(0), x), (LocalId::from(1), y)]); - assert!(!checker.check_consistency(&scope, state.get_domains())); + assert!(!checker.check_retention(&scope, state.get_domains())); } #[test] @@ -98,6 +98,6 @@ mod tests { }; let scope = Scope::from_iter([(LocalId::from(0), x), (LocalId::from(1), y)]); - assert!(checker.check_consistency(&scope, state.get_domains())); + assert!(checker.check_retention(&scope, state.get_domains())); } } diff --git a/pumpkin-crates/core/src/propagators/reified_propagator/checker.rs b/pumpkin-crates/core/src/propagators/reified_propagator/checker.rs index 6cc092316..825b62e6b 100644 --- a/pumpkin-crates/core/src/propagators/reified_propagator/checker.rs +++ b/pumpkin-crates/core/src/propagators/reified_propagator/checker.rs @@ -4,8 +4,8 @@ use pumpkin_checking::CheckerVariable; use pumpkin_checking::InferenceChecker; use pumpkin_checking::VariableState; -use crate::checkers::BoxedConsistencyChecker; -use crate::checkers::ConsistencyChecker; +use crate::checkers::BoxedRetentionChecker; +use crate::checkers::RetentionChecker; use crate::checkers::Scope; use crate::propagation::Domains; use crate::propagation::LocalId; @@ -16,21 +16,21 @@ use crate::variables::Literal; /// not assigned to true. #[derive(Debug, Clone)] pub struct ReifiedConsistencyChecker { - pub inner: BoxedConsistencyChecker, + pub inner: BoxedRetentionChecker, pub reification_literal: Literal, /// The [`LocalId`] of the reification literal in the scope, used to strip it before passing /// the scope to the inner checker. pub reification_literal_id: LocalId, } -impl ConsistencyChecker for ReifiedConsistencyChecker { - fn check_consistency(&mut self, scope: &Scope, domains: Domains<'_>) -> bool { +impl RetentionChecker for ReifiedConsistencyChecker { + fn check_retention(&mut self, scope: &Scope, domains: Domains<'_>) -> bool { if domains.evaluate_literal(self.reification_literal) != Some(true) { return true; } let inner_scope = scope.without(self.reification_literal_id); - self.inner.check_consistency(&inner_scope, domains) + self.inner.check_retention(&inner_scope, domains) } } diff --git a/pumpkin-crates/core/src/propagators/reified_propagator/constructor.rs b/pumpkin-crates/core/src/propagators/reified_propagator/constructor.rs index e6864b0e3..deabe1a0e 100644 --- a/pumpkin-crates/core/src/propagators/reified_propagator/constructor.rs +++ b/pumpkin-crates/core/src/propagators/reified_propagator/constructor.rs @@ -1,5 +1,5 @@ #[cfg(feature = "check-consistency")] -use crate::checkers::BoxedConsistencyChecker; +use crate::checkers::BoxedRetentionChecker; use crate::propagation::DomainEvents; #[cfg(feature = "check-consistency")] use crate::propagation::LocalId; @@ -93,7 +93,7 @@ fn wrap_consistency_checkers( reification_literal.add_to_scope(scope, reification_literal_id); replace_with::replace_with_or_abort(checker, |inner_checker| { - BoxedConsistencyChecker::from(ReifiedConsistencyChecker { + BoxedRetentionChecker::from(ReifiedConsistencyChecker { inner: inner_checker, reification_literal, reification_literal_id, diff --git a/pumpkin-crates/propagators/src/propagators/arithmetic/binary_equals/constructor.rs b/pumpkin-crates/propagators/src/propagators/arithmetic/binary_equals/constructor.rs index 8d75f208f..b144492ed 100644 --- a/pumpkin-crates/propagators/src/propagators/arithmetic/binary_equals/constructor.rs +++ b/pumpkin-crates/propagators/src/propagators/arithmetic/binary_equals/constructor.rs @@ -1,5 +1,5 @@ use pumpkin_core::checkers::StrongConsistency; -use pumpkin_core::checkers::StrongConsistencyChecker; +use pumpkin_core::checkers::StrongRetentionChecker; use pumpkin_core::containers::HashSet; use pumpkin_core::predicates::Predicate; use pumpkin_core::proof::ConstraintTag; @@ -44,7 +44,7 @@ where context.add_consistency_checker( ((super::ID_LHS, &a), (super::ID_RHS, &b)), - StrongConsistencyChecker::new( + StrongRetentionChecker::new( StrongConsistency::Domain, BinaryEqualsChecker { lhs: a.clone(), diff --git a/pumpkin-crates/propagators/src/propagators/arithmetic/multiplication/constructor.rs b/pumpkin-crates/propagators/src/propagators/arithmetic/multiplication/constructor.rs index 84569b507..fa69b8383 100644 --- a/pumpkin-crates/propagators/src/propagators/arithmetic/multiplication/constructor.rs +++ b/pumpkin-crates/propagators/src/propagators/arithmetic/multiplication/constructor.rs @@ -1,5 +1,5 @@ use pumpkin_core::checkers::StrongConsistency; -use pumpkin_core::checkers::StrongConsistencyChecker; +use pumpkin_core::checkers::StrongRetentionChecker; use pumpkin_core::checkers::support::SupportsValue; use pumpkin_core::proof::ConstraintTag; use pumpkin_core::proof::InferenceCode; @@ -48,7 +48,7 @@ where context.add_consistency_checker( ((super::ID_A, &a), (super::ID_B, &b), (super::ID_C, &c)), - StrongConsistencyChecker::new( + StrongRetentionChecker::new( StrongConsistency::Bounds, IntegerMultiplicationChecker { a: a.clone(), From 2bbdd59648cbc344f082bce28c2183128ba379c7 Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Sun, 24 May 2026 12:51:46 +1000 Subject: [PATCH 13/23] Explicitly introduce the concept of 'PropagationChecker' --- pumpkin-crates/core/src/checkers/mod.rs | 2 + .../core/src/checkers/propagation_checker.rs | 64 +++++++++++++++++++ .../core/src/engine/cp/test_solver.rs | 8 +-- pumpkin-crates/core/src/engine/state.rs | 38 +++++------ .../contexts/propagation_context.rs | 8 ++- 5 files changed, 91 insertions(+), 29 deletions(-) create mode 100644 pumpkin-crates/core/src/checkers/propagation_checker.rs diff --git a/pumpkin-crates/core/src/checkers/mod.rs b/pumpkin-crates/core/src/checkers/mod.rs index 3040d9dae..05d6bc62d 100644 --- a/pumpkin-crates/core/src/checkers/mod.rs +++ b/pumpkin-crates/core/src/checkers/mod.rs @@ -1,3 +1,4 @@ +mod propagation_checker; mod retention_checker; mod scope; mod self_disabling; @@ -5,6 +6,7 @@ mod store; mod strong_retention_checker; pub mod support; +pub use propagation_checker::*; pub use retention_checker::*; pub use scope::*; pub use self_disabling::*; diff --git a/pumpkin-crates/core/src/checkers/propagation_checker.rs b/pumpkin-crates/core/src/checkers/propagation_checker.rs new file mode 100644 index 000000000..67de6fd32 --- /dev/null +++ b/pumpkin-crates/core/src/checkers/propagation_checker.rs @@ -0,0 +1,64 @@ +use pumpkin_checking::BoxedChecker; +use pumpkin_checking::VariableState; + +use crate::predicates::Predicate; +use crate::propagation::Domains; +use crate::propagation::ReadDomains; +use crate::variables::DomainId; + +/// Tests whether an inference is correct given the solver state. +/// +/// An inference is correct when: +/// 1. All premises are satisfied. +/// 2. The conjunction of the premises and negation of the consequent is consistent. +/// 3. The consequent is logically entailed given the inference code. +#[derive(Clone, Debug)] +pub struct PropagationChecker { + inference_checker: BoxedChecker, +} + +impl PropagationChecker { + /// Create a new propagation checker given an inference checker and inference code. + pub fn new(inference_checker: BoxedChecker) -> PropagationChecker { + PropagationChecker { inference_checker } + } + + /// Run the propagation checker for the given inference. + pub fn check( + &self, + premises: &[Predicate], + consequent: Option, + domains: Domains<'_>, + ) -> Result<(), InvalidInference> { + let premises_satisfied = premises + .iter() + .all(|&premise| domains.evaluate_predicate(premise) == Some(true)); + + if !premises_satisfied { + return Err(InvalidInference::UnsatisfiedPremises); + } + + let variable_state = + VariableState::prepare_for_conflict_check(premises.iter().copied(), consequent) + .map_err(InvalidInference::InconsistentPredicates)?; + + if self + .inference_checker + .check(variable_state, &premises, consequent.as_ref()) + { + Ok(()) + } else { + Err(InvalidInference::Unsound) + } + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum InvalidInference { + /// Not all premises are true given the current state. + UnsatisfiedPremises, + /// The predicates that make up the inference are trivially inconsistent. + InconsistentPredicates(DomainId), + /// Cannot establish that the inference is sound. + Unsound, +} diff --git a/pumpkin-crates/core/src/engine/cp/test_solver.rs b/pumpkin-crates/core/src/engine/cp/test_solver.rs index 87bfcca8a..a75ba9821 100644 --- a/pumpkin-crates/core/src/engine/cp/test_solver.rs +++ b/pumpkin-crates/core/src/engine/cp/test_solver.rs @@ -241,7 +241,7 @@ impl TestSolver { #[cfg(feature = "check-consistency")] consistency_checkers, #[cfg(feature = "check-propagations")] - checkers, + propagation_checkers, .. } = &mut self.state; @@ -254,7 +254,7 @@ impl TestSolver { #[cfg(feature = "check-consistency")] consistency_checkers, #[cfg(feature = "check-propagations")] - checkers, + propagation_checkers, ); propagators[propagator].propagate(context) @@ -278,7 +278,7 @@ impl TestSolver { #[cfg(feature = "check-consistency")] consistency_checkers, #[cfg(feature = "check-propagations")] - checkers, + propagation_checkers, .. } = &mut self.state; @@ -291,7 +291,7 @@ impl TestSolver { #[cfg(feature = "check-consistency")] consistency_checkers, #[cfg(feature = "check-propagations")] - checkers, + propagation_checkers, ); propagators[propagator].propagate(context)?; diff --git a/pumpkin-crates/core/src/engine/state.rs b/pumpkin-crates/core/src/engine/state.rs index facb355c5..6b79dd269 100644 --- a/pumpkin-crates/core/src/engine/state.rs +++ b/pumpkin-crates/core/src/engine/state.rs @@ -2,11 +2,10 @@ use std::sync::Arc; use pumpkin_checking::BoxedChecker; use pumpkin_checking::InferenceChecker; -#[cfg(feature = "check-propagations")] -use pumpkin_checking::VariableState; use crate::checkers::BoxedRetentionChecker; use crate::checkers::ConsistencyCheckerStore; +use crate::checkers::PropagationChecker; use crate::checkers::Scope; use crate::containers::HashMap; use crate::containers::KeyGenerator; @@ -82,7 +81,7 @@ pub struct State { statistics: StateStatistics, /// Inference checkers to run in the propagation loop. - pub(crate) checkers: HashMap>>, + pub(crate) propagation_checkers: HashMap>, pub(crate) consistency_checkers: ConsistencyCheckerStore, } @@ -113,7 +112,7 @@ impl Default for State { notification_engine: NotificationEngine::default(), statistics: StateStatistics::default(), constraint_tags: KeyGenerator::default(), - checkers: HashMap::default(), + propagation_checkers: HashMap::default(), consistency_checkers: Default::default(), }; // As a convention, the assignments contain a dummy domain_id=0, which represents a 0-1 @@ -373,8 +372,8 @@ impl State { inference_code: InferenceCode, checker: Box>, ) { - let checkers = self.checkers.entry(inference_code).or_default(); - checkers.push(BoxedChecker::from(checker)); + let checkers = self.propagation_checkers.entry(inference_code).or_default(); + checkers.push(PropagationChecker::new(BoxedChecker::from(checker))); } /// Add a consistency checker for the scope. @@ -431,7 +430,7 @@ impl State { #[cfg(feature = "check-consistency")] consistency_checkers, #[cfg(feature = "check-propagations")] - checkers, + propagation_checkers: checkers, .. } = self; let propagator = propagators.get_propagator_mut(handle); @@ -638,7 +637,7 @@ impl State { #[cfg(feature = "check-consistency")] consistency_checkers, #[cfg(feature = "check-propagations")] - checkers, + propagation_checkers: checkers, .. } = self; let propagator = &mut propagators[propagator_id]; @@ -827,7 +826,7 @@ impl State { impl State { /// Run the checker for the given inference code on the given inference. fn run_checker( - &self, + &mut self, premises: impl IntoIterator, consequent: Option, inference_code: &InferenceCode, @@ -835,7 +834,7 @@ impl State { let premises: Vec<_> = premises.into_iter().collect(); let checkers = self - .checkers + .propagation_checkers .get(inference_code) .map(|vec| vec.as_slice()) .unwrap_or(&[]); @@ -846,18 +845,13 @@ impl State { ); 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, - ) - .unwrap_or_else(|domain| { - panic!( - "inconsistent atomics over domain {domain:?} in inference by {inference_code:?}" + checker + .check( + &premises, + consequent, + Domains::new(&self.assignments, &mut self.trailed_values), ) - }); - - checker.check(variable_state, &premises, consequent.as_ref()) + .is_ok() }); assert!( @@ -1214,7 +1208,7 @@ impl State { #[cfg(feature = "check-consistency")] consistency_checkers, #[cfg(feature = "check-propagations")] - checkers, + propagation_checkers: checkers, .. } = self; PropagationContext::new( diff --git a/pumpkin-crates/core/src/propagation/contexts/propagation_context.rs b/pumpkin-crates/core/src/propagation/contexts/propagation_context.rs index 2878eec41..0ebe4ffd0 100644 --- a/pumpkin-crates/core/src/propagation/contexts/propagation_context.rs +++ b/pumpkin-crates/core/src/propagation/contexts/propagation_context.rs @@ -6,6 +6,8 @@ use crate::basic_types::PredicateId; use crate::checkers::BoxedRetentionChecker; #[cfg(feature = "check-consistency")] use crate::checkers::ConsistencyCheckerStore; +#[cfg(feature = "check-propagations")] +use crate::checkers::PropagationChecker; use crate::checkers::Scope; #[cfg(feature = "check-propagations")] use crate::containers::HashMap; @@ -99,7 +101,7 @@ pub struct PropagationContext<'a> { #[cfg(feature = "check-consistency")] pub(crate) consistency_checkers: &'a mut ConsistencyCheckerStore, #[cfg(feature = "check-propagations")] - pub(crate) inference_checkers: &'a mut HashMap>>, + pub(crate) inference_checkers: &'a mut HashMap>, } impl<'a> HasAssignments for PropagationContext<'a> { @@ -126,7 +128,7 @@ impl<'a> PropagationContext<'a> { #[cfg(feature = "check-consistency")] consistency_checkers: &'a mut ConsistencyCheckerStore, #[cfg(feature = "check-propagations")] inference_checkers: &'a mut HashMap< InferenceCode, - Vec>, + Vec, >, ) -> Self { PropagationContext { @@ -185,7 +187,7 @@ impl<'a> PropagationContext<'a> { self.inference_checkers .entry(inference_code) .or_default() - .push(BoxedChecker::from(checker)); + .push(PropagationChecker::new(BoxedChecker::from(checker))); // Use variables to avoid unused warnings. #[cfg(not(feature = "check-propagations"))] From 12e8f2b89e360f6d904ba3a0f5107b880b7ebbc8 Mon Sep 17 00:00:00 2001 From: Imko Marijnissen Date: Tue, 26 May 2026 09:10:27 +0200 Subject: [PATCH 14/23] refactor: split up maximum propagator --- .../src/constraints/arithmetic/mod.rs | 4 +- .../core/src/checkers/propagation_checker.rs | 2 +- .../propagators/arithmetic/maximum/checker.rs | 43 +++++++ .../arithmetic/maximum/constructor.rs | 60 +++++++++ .../src/propagators/arithmetic/maximum/mod.rs | 11 ++ .../{maximum.rs => maximum/propagator.rs} | 115 ++---------------- 6 files changed, 125 insertions(+), 110 deletions(-) create mode 100644 pumpkin-crates/propagators/src/propagators/arithmetic/maximum/checker.rs create mode 100644 pumpkin-crates/propagators/src/propagators/arithmetic/maximum/constructor.rs create mode 100644 pumpkin-crates/propagators/src/propagators/arithmetic/maximum/mod.rs rename pumpkin-crates/propagators/src/propagators/arithmetic/{maximum.rs => maximum/propagator.rs} (71%) diff --git a/pumpkin-crates/constraints/src/constraints/arithmetic/mod.rs b/pumpkin-crates/constraints/src/constraints/arithmetic/mod.rs index eccea63c6..4f2bbf8ed 100644 --- a/pumpkin-crates/constraints/src/constraints/arithmetic/mod.rs +++ b/pumpkin-crates/constraints/src/constraints/arithmetic/mod.rs @@ -10,7 +10,7 @@ use pumpkin_core::variables::IntegerVariable; use pumpkin_propagators::arithmetic::AbsoluteValueArgs; use pumpkin_propagators::arithmetic::DivisionArgs; use pumpkin_propagators::arithmetic::IntegerMultiplicationArgs; -use pumpkin_propagators::arithmetic::MaximumArgs; +use pumpkin_propagators::arithmetic::MaximumConstructor; /// Creates the [`Constraint`] `a + b = c`. pub fn plus( @@ -76,7 +76,7 @@ pub fn maximum( rhs: impl IntegerVariable + 'static, constraint_tag: ConstraintTag, ) -> impl Constraint { - MaximumArgs { + MaximumConstructor { array: array.into_iter().collect(), rhs, constraint_tag, diff --git a/pumpkin-crates/core/src/checkers/propagation_checker.rs b/pumpkin-crates/core/src/checkers/propagation_checker.rs index 67de6fd32..7c7d5289a 100644 --- a/pumpkin-crates/core/src/checkers/propagation_checker.rs +++ b/pumpkin-crates/core/src/checkers/propagation_checker.rs @@ -44,7 +44,7 @@ impl PropagationChecker { if self .inference_checker - .check(variable_state, &premises, consequent.as_ref()) + .check(variable_state, premises, consequent.as_ref()) { Ok(()) } else { diff --git a/pumpkin-crates/propagators/src/propagators/arithmetic/maximum/checker.rs b/pumpkin-crates/propagators/src/propagators/arithmetic/maximum/checker.rs new file mode 100644 index 000000000..1f95b0370 --- /dev/null +++ b/pumpkin-crates/propagators/src/propagators/arithmetic/maximum/checker.rs @@ -0,0 +1,43 @@ +use pumpkin_checking::AtomicConstraint; +use pumpkin_checking::CheckerVariable; +use pumpkin_checking::InferenceChecker; +use pumpkin_checking::IntExt; + +#[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(IntExt::NegativeInf); + let highest_maximum = self + .array + .iter() + .map(|element| element.induced_upper_bound(&state)) + .max() + .unwrap_or(IntExt::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) + } +} diff --git a/pumpkin-crates/propagators/src/propagators/arithmetic/maximum/constructor.rs b/pumpkin-crates/propagators/src/propagators/arithmetic/maximum/constructor.rs new file mode 100644 index 000000000..b62d3ace3 --- /dev/null +++ b/pumpkin-crates/propagators/src/propagators/arithmetic/maximum/constructor.rs @@ -0,0 +1,60 @@ +use pumpkin_core::proof::ConstraintTag; +use pumpkin_core::proof::InferenceCode; +use pumpkin_core::propagation::DomainEvents; +use pumpkin_core::propagation::LocalId; +use pumpkin_core::propagation::PropagatorConstructor; +use pumpkin_core::propagation::PropagatorConstructorContext; +use pumpkin_core::variables::IntegerVariable; + +use crate::arithmetic::MaximumChecker; +use crate::arithmetic::MaximumPropagator; +use crate::arithmetic::maximum::Maximum; + +#[derive(Clone, Debug)] +pub struct MaximumConstructor { + pub array: Box<[ElementVar]>, + pub rhs: Rhs, + pub constraint_tag: ConstraintTag, +} + +impl PropagatorConstructor for MaximumConstructor +where + ElementVar: IntegerVariable + 'static, + Rhs: IntegerVariable + 'static, +{ + type PropagatorImpl = MaximumPropagator; + + fn create(self, mut context: PropagatorConstructorContext) -> Self::PropagatorImpl { + let MaximumConstructor { + array, + rhs, + constraint_tag, + } = self; + + context.add_inference_checker( + InferenceCode::new(constraint_tag, Maximum), + Box::new(MaximumChecker { + array: array.clone(), + rhs: rhs.clone(), + }), + ); + + for (idx, var) in array.iter().enumerate() { + context.register(var.clone(), DomainEvents::BOUNDS, LocalId::from(idx as u32)); + } + + context.register( + rhs.clone(), + DomainEvents::BOUNDS, + LocalId::from(array.len() as u32), + ); + + let inference_code = InferenceCode::new(constraint_tag, Maximum); + + MaximumPropagator { + array, + rhs, + inference_code, + } + } +} diff --git a/pumpkin-crates/propagators/src/propagators/arithmetic/maximum/mod.rs b/pumpkin-crates/propagators/src/propagators/arithmetic/maximum/mod.rs new file mode 100644 index 000000000..d8029502e --- /dev/null +++ b/pumpkin-crates/propagators/src/propagators/arithmetic/maximum/mod.rs @@ -0,0 +1,11 @@ +mod checker; +mod constructor; +mod propagator; + +pub use checker::*; +pub use constructor::*; +pub use propagator::*; +use pumpkin_core::declare_inference_label; + +// The inference label for maximum. +declare_inference_label!(Maximum); diff --git a/pumpkin-crates/propagators/src/propagators/arithmetic/maximum.rs b/pumpkin-crates/propagators/src/propagators/arithmetic/maximum/propagator.rs similarity index 71% rename from pumpkin-crates/propagators/src/propagators/arithmetic/maximum.rs rename to pumpkin-crates/propagators/src/propagators/arithmetic/maximum/propagator.rs index 693869274..93ab32554 100644 --- a/pumpkin-crates/propagators/src/propagators/arithmetic/maximum.rs +++ b/pumpkin-crates/propagators/src/propagators/arithmetic/maximum/propagator.rs @@ -1,82 +1,21 @@ -use pumpkin_checking::AtomicConstraint; -use pumpkin_checking::CheckerVariable; -use pumpkin_checking::InferenceChecker; -use pumpkin_checking::IntExt; use pumpkin_core::conjunction; -use pumpkin_core::declare_inference_label; use pumpkin_core::predicate; use pumpkin_core::predicates::PropositionalConjunction; -use pumpkin_core::proof::ConstraintTag; use pumpkin_core::proof::InferenceCode; -use pumpkin_core::propagation::DomainEvents; -use pumpkin_core::propagation::LocalId; use pumpkin_core::propagation::Priority; use pumpkin_core::propagation::PropagationContext; use pumpkin_core::propagation::Propagator; -use pumpkin_core::propagation::PropagatorConstructor; -use pumpkin_core::propagation::PropagatorConstructorContext; use pumpkin_core::propagation::ReadDomains; use pumpkin_core::state::PropagationStatusCP; use pumpkin_core::variables::IntegerVariable; -#[derive(Clone, Debug)] -pub struct MaximumArgs { - pub array: Box<[ElementVar]>, - pub rhs: Rhs, - pub constraint_tag: ConstraintTag, -} - -declare_inference_label!(Maximum); - -impl PropagatorConstructor for MaximumArgs -where - ElementVar: IntegerVariable + 'static, - Rhs: IntegerVariable + 'static, -{ - type PropagatorImpl = MaximumPropagator; - - fn create(self, mut context: PropagatorConstructorContext) -> Self::PropagatorImpl { - let MaximumArgs { - array, - rhs, - constraint_tag, - } = self; - - context.add_inference_checker( - InferenceCode::new(constraint_tag, Maximum), - Box::new(MaximumChecker { - array: array.clone(), - rhs: rhs.clone(), - }), - ); - - for (idx, var) in array.iter().enumerate() { - context.register(var.clone(), DomainEvents::BOUNDS, LocalId::from(idx as u32)); - } - - context.register( - rhs.clone(), - DomainEvents::BOUNDS, - LocalId::from(array.len() as u32), - ); - - let inference_code = InferenceCode::new(constraint_tag, Maximum); - - MaximumPropagator { - array, - rhs, - inference_code, - } - } -} - /// Bounds-consistent propagator which enforces `max(array) = rhs`. Can be constructed through /// [`MaximumArgs`]. #[derive(Clone, Debug)] pub struct MaximumPropagator { - array: Box<[ElementVar]>, - rhs: Rhs, - inference_code: InferenceCode, + pub(crate) array: Box<[ElementVar]>, + pub(crate) rhs: Rhs, + pub(crate) inference_code: InferenceCode, } impl Propagator @@ -181,45 +120,6 @@ 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(IntExt::NegativeInf); - let highest_maximum = self - .array - .iter() - .map(|element| element.induced_upper_bound(&state)) - .max() - .unwrap_or(IntExt::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) - } -} - #[cfg(test)] mod tests { use pumpkin_core::predicate; @@ -230,6 +130,7 @@ mod tests { use super::*; use crate::StateExt; + use crate::arithmetic::MaximumConstructor; #[test] fn upper_bound_of_rhs_matches_maximum_upper_bound_of_array_at_initialise() { @@ -242,7 +143,7 @@ mod tests { let rhs = state.new_interval_variable(1, 10, None); let constraint_tag = state.new_constraint_tag(); - let _ = state.add_propagator(MaximumArgs { + let _ = state.add_propagator(MaximumConstructor { array: [a, b, c].into(), rhs, constraint_tag, @@ -272,7 +173,7 @@ mod tests { let rhs = state.new_interval_variable(1, 10, None); let constraint_tag = state.new_constraint_tag(); - let _ = state.add_propagator(MaximumArgs { + let _ = state.add_propagator(MaximumConstructor { array: [a, b, c].into(), rhs, constraint_tag, @@ -302,7 +203,7 @@ mod tests { let rhs = state.new_interval_variable(1, 3, None); let constraint_tag = state.new_constraint_tag(); - let _ = state.add_propagator(MaximumArgs { + let _ = state.add_propagator(MaximumConstructor { array: array.clone(), rhs, constraint_tag, @@ -334,7 +235,7 @@ mod tests { let rhs = state.new_interval_variable(45, 60, None); let constraint_tag = state.new_constraint_tag(); - let _ = state.add_propagator(MaximumArgs { + let _ = state.add_propagator(MaximumConstructor { array: array.clone(), rhs, constraint_tag, From 5dbb7e155cb71858367cf97180919bc59357110b Mon Sep 17 00:00:00 2001 From: Imko Marijnissen Date: Tue, 26 May 2026 10:27:57 +0200 Subject: [PATCH 15/23] feat: adding consistency checker for maximum --- .../src/checkers/strong_retention_checker.rs | 5 +- .../propagators/arithmetic/maximum/checker.rs | 121 ++++++++++++++++++ .../arithmetic/maximum/constructor.rs | 32 +++-- 3 files changed, 149 insertions(+), 9 deletions(-) diff --git a/pumpkin-crates/core/src/checkers/strong_retention_checker.rs b/pumpkin-crates/core/src/checkers/strong_retention_checker.rs index 3c151a994..197dde804 100644 --- a/pumpkin-crates/core/src/checkers/strong_retention_checker.rs +++ b/pumpkin-crates/core/src/checkers/strong_retention_checker.rs @@ -99,7 +99,10 @@ impl StrongRetentionChecker { for (domain, value) in self.support.drain() { if !value.is_in(domain, domains) { - log::error!("Support value is not in the domain"); + log::error!( + "Support value {value:?} is not in the domain of {domain:?} - {domain}: {:?}", + domains.iterate_domain(&domain).collect::>() + ); return false; } diff --git a/pumpkin-crates/propagators/src/propagators/arithmetic/maximum/checker.rs b/pumpkin-crates/propagators/src/propagators/arithmetic/maximum/checker.rs index 1f95b0370..10b01ff00 100644 --- a/pumpkin-crates/propagators/src/propagators/arithmetic/maximum/checker.rs +++ b/pumpkin-crates/propagators/src/propagators/arithmetic/maximum/checker.rs @@ -2,6 +2,15 @@ use pumpkin_checking::AtomicConstraint; use pumpkin_checking::CheckerVariable; use pumpkin_checking::InferenceChecker; use pumpkin_checking::IntExt; +use pumpkin_core::asserts::pumpkin_assert_simple; +use pumpkin_core::checkers::support::Support; +use pumpkin_core::checkers::support::SupportGenerator; +use pumpkin_core::checkers::support::SupportsValue; +use pumpkin_core::checkers::support::UnsupportedValue; +use pumpkin_core::propagation::Domains; +use pumpkin_core::propagation::LocalId; +use pumpkin_core::propagation::ReadDomains; +use pumpkin_core::variables::IntegerVariable; #[derive(Clone, Debug)] pub struct MaximumChecker { @@ -41,3 +50,115 @@ where || highest_maximum < self.rhs.induced_lower_bound(&state) } } + +impl SupportGenerator for MaximumChecker +where + ElementVar: IntegerVariable + SupportsValue, + Rhs: IntegerVariable + SupportsValue, +{ + type Value = i32; + + fn support( + &mut self, + support: &mut Support, + local_id: LocalId, + value: UnsupportedValue, + domains: &Domains<'_>, + ) { + if local_id.unpack() as usize == self.array.len() { + // We are trying to find support for the rhs. + // + // Regardless of whether we are trying to generate support for a lower-bound or an + // upper-bound, we try to assign the variables that can be assigned to the value to the + // value of the RHS and assign the rest to their lower-bound. + let value = self.rhs.unpack(value); + self.rhs.assign(value, support); + + let lb = domains.lower_bound(&self.rhs); + let ub = domains.upper_bound(&self.rhs); + + pumpkin_assert_simple!(value == lb || value == ub); + + self.array.iter().for_each(|element| { + if domains.contains(element, value) { + element.assign(value, support); + } else { + element.assign(domains.lower_bound(element), support); + } + }) + } else { + // We are trying to find support for an element in the array. + // + // This is somewhat trickier then generating support for the rhs. + // + // We need to find a maximum value such that: + // 1. It is in the domain of `self.rhs`. + // 2. It is in the domain of at least 1 variable in `self.array`. + // 3. Every variable can take a value which is <= that value. + // 4. It is at least as large as the unsupported value. + // + // If we have found this value, then we assign the domain in `self.array` and `self.rhs` + // to that value, and the rest to their lower-bound. + let var = &self.array[local_id.unpack() as usize]; + let value = var.unpack(value); + var.assign(value, support); + + let lb = domains.lower_bound(var); + let ub = domains.upper_bound(var); + + pumpkin_assert_simple!(value == lb || value == ub); + + // We go over all possible maximum values to create a solution. + for possible_value in value..=domains.upper_bound(&self.rhs) { + if !domains.contains(&self.rhs, possible_value) { + // The rhs cannot be assigned to this maximum value so we can continue. + continue; + } + + // Next, we find an element in `self.array` which has the `possible_value` in its + // domain. + let Some((has_value_in_domain_index, has_value_in_domain)) = self + .array + .iter() + .enumerate() + .find(|(_, element)| domains.contains(*element, possible_value)) + else { + // If we cannot find such an element then we move onto the next possible value. + continue; + }; + + // If there is an element in `self.array` which cannot be assigned to + // `possible_value`, then we continue looking for a value. + if self.array.iter().enumerate().any(|(index, element)| { + index != local_id.unpack() as usize + && domains.lower_bound(element) > possible_value + }) { + continue; + } + + // Finally, we do the assignment. + has_value_in_domain.assign(possible_value, support); + self.rhs.assign(possible_value, support); + self.array + .iter() + .enumerate() + .filter(|(index, _)| { + *index != has_value_in_domain_index && *index != local_id.unpack() as usize + }) + .for_each(|(_, element)| element.assign(domains.lower_bound(element), support)); + + return; + } + panic!("Was not able to generate support for Maximum"); + } + } + + fn is_solution(&self, support: &Support) -> bool { + self.array + .iter() + .map(|element| element.support_value(support)) + .max() + .expect("Expected at least one element in the maximum constraint") + == self.rhs.support_value(support) + } +} diff --git a/pumpkin-crates/propagators/src/propagators/arithmetic/maximum/constructor.rs b/pumpkin-crates/propagators/src/propagators/arithmetic/maximum/constructor.rs index b62d3ace3..58bb888ec 100644 --- a/pumpkin-crates/propagators/src/propagators/arithmetic/maximum/constructor.rs +++ b/pumpkin-crates/propagators/src/propagators/arithmetic/maximum/constructor.rs @@ -1,3 +1,6 @@ +use pumpkin_core::checkers::Scope; +use pumpkin_core::checkers::StrongConsistency; +use pumpkin_core::checkers::StrongRetentionChecker; use pumpkin_core::proof::ConstraintTag; use pumpkin_core::proof::InferenceCode; use pumpkin_core::propagation::DomainEvents; @@ -31,6 +34,18 @@ where constraint_tag, } = self; + let mut scope = Scope::default(); + + for (idx, var) in array.iter().enumerate() { + let local_id = LocalId::from(idx as u32); + context.register(var.clone(), DomainEvents::BOUNDS, local_id); + var.add_to_scope(&mut scope, local_id); + } + + let rhs_local_id = LocalId::from(array.len() as u32); + context.register(rhs.clone(), DomainEvents::BOUNDS, rhs_local_id); + rhs.add_to_scope(&mut scope, rhs_local_id); + context.add_inference_checker( InferenceCode::new(constraint_tag, Maximum), Box::new(MaximumChecker { @@ -39,14 +54,15 @@ where }), ); - for (idx, var) in array.iter().enumerate() { - context.register(var.clone(), DomainEvents::BOUNDS, LocalId::from(idx as u32)); - } - - context.register( - rhs.clone(), - DomainEvents::BOUNDS, - LocalId::from(array.len() as u32), + context.add_consistency_checker( + scope, + StrongRetentionChecker::new( + StrongConsistency::Bounds, + MaximumChecker { + array: array.clone(), + rhs: rhs.clone(), + }, + ), ); let inference_code = InferenceCode::new(constraint_tag, Maximum); From d220a641da5b953e758f2c5119e31964b9b8ac2b Mon Sep 17 00:00:00 2001 From: Imko Marijnissen Date: Tue, 26 May 2026 11:34:48 +0200 Subject: [PATCH 16/23] feat: adding weak consistency checker --- .../basic_types/propositional_conjunction.rs | 6 +- pumpkin-crates/core/src/checkers/mod.rs | 2 + .../src/checkers/weak_retention_checker.rs | 95 +++++++++++++++++++ 3 files changed, 100 insertions(+), 3 deletions(-) create mode 100644 pumpkin-crates/core/src/checkers/weak_retention_checker.rs diff --git a/pumpkin-crates/core/src/basic_types/propositional_conjunction.rs b/pumpkin-crates/core/src/basic_types/propositional_conjunction.rs index 4a718b654..517b82de4 100644 --- a/pumpkin-crates/core/src/basic_types/propositional_conjunction.rs +++ b/pumpkin-crates/core/src/basic_types/propositional_conjunction.rs @@ -21,9 +21,9 @@ impl Deref for PropositionalConjunction { } } -impl Into> for PropositionalConjunction { - fn into(self) -> Box<[Predicate]> { - self.predicates_in_conjunction.into() +impl From for Box<[Predicate]> { + fn from(val: PropositionalConjunction) -> Self { + val.predicates_in_conjunction.into() } } diff --git a/pumpkin-crates/core/src/checkers/mod.rs b/pumpkin-crates/core/src/checkers/mod.rs index 05d6bc62d..135d65ac1 100644 --- a/pumpkin-crates/core/src/checkers/mod.rs +++ b/pumpkin-crates/core/src/checkers/mod.rs @@ -5,6 +5,7 @@ mod self_disabling; mod store; mod strong_retention_checker; pub mod support; +mod weak_retention_checker; pub use propagation_checker::*; pub use retention_checker::*; @@ -12,3 +13,4 @@ pub use scope::*; pub use self_disabling::*; pub use store::*; pub use strong_retention_checker::*; +pub use weak_retention_checker::*; diff --git a/pumpkin-crates/core/src/checkers/weak_retention_checker.rs b/pumpkin-crates/core/src/checkers/weak_retention_checker.rs new file mode 100644 index 000000000..11f4569cf --- /dev/null +++ b/pumpkin-crates/core/src/checkers/weak_retention_checker.rs @@ -0,0 +1,95 @@ +use pumpkin_checking::InferenceChecker; +use pumpkin_checking::VariableState; + +use super::Scope; +use crate::checkers::RetentionChecker; +use crate::conjunction; +use crate::predicate; +use crate::predicates::Predicate; +use crate::propagation::Domains; +use crate::propagation::ReadDomains; + +/// The consistency level advertised by the propagator. +#[derive(Clone, Copy, Debug)] +pub enum WeakConsistency { + Domain, + Bounds, +} + +#[derive(Debug, Clone)] +pub struct WeakRetentionChecker + Clone> { + /// The inference checker. + inference_checker: Inferences, + /// The consistency level to test for. + consistency_level: WeakConsistency, +} + +impl + Clone> WeakRetentionChecker { + pub fn new(consistency_level: WeakConsistency, inference_checker: Inferences) -> Self { + WeakRetentionChecker { + inference_checker, + consistency_level, + } + } + + fn bound_not_updatable(&mut self, consequent: Predicate, premises: &[Predicate]) -> bool { + let state = + VariableState::prepare_for_conflict_check(premises.iter().copied(), Some(consequent)) + .expect("Domain should not be inconsistent"); + + !self + .inference_checker + .check(state, premises, Some(&consequent)) + } +} + +impl + Clone> RetentionChecker + for WeakRetentionChecker +{ + fn check_retention(&mut self, scope: &Scope, domains: Domains<'_>) -> bool { + let premises = scope + .domains() + .flat_map(|(_, domain_id)| { + let lower_bound = domains.lower_bound(&domain_id); + let upper_bound = domains.upper_bound(&domain_id); + + domains + .get_holes(&domain_id) + .filter(|&hole| lower_bound <= hole && hole <= upper_bound) + .map(|hole| predicate![domain_id != hole]) + .chain(conjunction!( + [domain_id >= lower_bound] & [domain_id <= upper_bound] + )) + .collect::>() + }) + .collect::>(); + + scope + .domains() + .all(|(_, domain_id)| match self.consistency_level { + WeakConsistency::Domain => domains.iterate_domain(&domain_id).all(|value| { + let consequent = Some(predicate!(domain_id != value)); + let state = VariableState::prepare_for_conflict_check( + premises.iter().copied(), + consequent, + ) + .expect("Domain should not be inconsistent"); + + !self + .inference_checker + .check(state, &premises.clone(), consequent.as_ref()) + }), + WeakConsistency::Bounds => { + let lb = domains.lower_bound(&domain_id); + let lb_not_updatable = + self.bound_not_updatable(predicate![domain_id >= lb + 1], &premises); + + let ub = domains.upper_bound(&domain_id); + let ub_not_updatable = + self.bound_not_updatable(predicate![domain_id <= ub - 1], &premises); + + lb_not_updatable && ub_not_updatable + } + }) + } +} From 11713ff4fa6fe0b7611f3ca342bbd5542887ab03 Mon Sep 17 00:00:00 2001 From: Imko Marijnissen Date: Tue, 26 May 2026 11:35:25 +0200 Subject: [PATCH 17/23] feat: adding consistency checker for time-tabling --- .../time_table_over_interval_incremental.rs | 20 +------ .../time_table_per_point_incremental.rs | 20 +------ .../time_table/time_table_over_interval.rs | 20 +------ .../time_table/time_table_per_point.rs | 20 +------ .../cumulative/time_table/time_table_util.rs | 58 +++++++++++++++++++ 5 files changed, 66 insertions(+), 72 deletions(-) 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 e67e186ed..23dafc85e 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 @@ -28,8 +28,7 @@ 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::time_table::time_table_util::register_checkers; use crate::cumulative::util::check_bounds_equal_at_propagation; use crate::cumulative::util::create_tasks; use crate::cumulative::util::register_tasks; @@ -110,22 +109,6 @@ impl PropagatorConstruc type PropagatorImpl = Self; fn create(mut self, mut context: PropagatorConstructorContext) -> Self::PropagatorImpl { - context.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(), - processing_time: task.processing_time, - resource_usage: task.resource_usage, - }) - .collect(), - capacity: self.parameters.capacity, - }), - ); // We only register for notifications of backtrack events if incremental backtracking is // enabled register_tasks( @@ -133,6 +116,7 @@ impl PropagatorConstruc context.reborrow(), self.parameters.options.incremental_backtracking, ); + register_checkers(&mut context, self.constraint_tag, &self.parameters); // First we store the bounds in the parameters self.updatable_structures 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 05ac11900..c20a15ab3 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 @@ -30,13 +30,12 @@ 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; use crate::cumulative::time_table::propagate_from_scratch_time_table_point; +use crate::cumulative::time_table::time_table_util::register_checkers; use crate::cumulative::util::check_bounds_equal_at_propagation; use crate::cumulative::util::create_tasks; use crate::cumulative::util::register_tasks; @@ -107,25 +106,10 @@ impl Propagator type PropagatorImpl = Self; fn create(mut self, mut context: PropagatorConstructorContext) -> Self::PropagatorImpl { - context.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(), - processing_time: task.processing_time, - resource_usage: task.resource_usage, - }) - .collect(), - capacity: self.parameters.capacity, - }), - ); register_tasks(&self.parameters.tasks, context.reborrow(), true); self.updatable_structures .reset_all_bounds_and_remove_fixed(context.domains(), &self.parameters); + register_checkers(&mut context, self.constraint_tag, &self.parameters); // Then we do normal propagation self.is_time_table_outdated = true; 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 b8b3fa314..2cdfb2ba0 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 @@ -32,10 +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::TimeTableChecker; #[cfg(doc)] use crate::cumulative::time_table::TimeTablePerPointPropagator; +use crate::cumulative::time_table::time_table_util::register_checkers; use crate::cumulative::util::create_tasks; use crate::cumulative::util::register_tasks; use crate::cumulative::util::update_bounds_task; @@ -110,25 +109,10 @@ impl PropagatorConstructor type PropagatorImpl = Self; fn create(mut self, mut context: PropagatorConstructorContext) -> Self::PropagatorImpl { - context.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(), - processing_time: task.processing_time, - resource_usage: task.resource_usage, - }) - .collect(), - capacity: self.parameters.capacity, - }), - ); self.updatable_structures .initialise_bounds_and_remove_fixed(context.domains(), &self.parameters); register_tasks(&self.parameters.tasks, context.reborrow(), false); + register_checkers(&mut context, self.constraint_tag, &self.parameters); self.inference_code = Some(InferenceCode::new(self.constraint_tag, TimeTable)); 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 0c7fceed6..04bbd747d 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 @@ -33,8 +33,7 @@ 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::time_table::time_table_util::register_checkers; use crate::cumulative::util::create_tasks; use crate::cumulative::util::register_tasks; use crate::cumulative::util::update_bounds_task; @@ -100,25 +99,10 @@ impl PropagatorConstructor for TimeTablePerPoint type PropagatorImpl = Self; fn create(mut self, mut context: PropagatorConstructorContext) -> Self::PropagatorImpl { - context.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(), - processing_time: task.processing_time, - resource_usage: task.resource_usage, - }) - .collect(), - capacity: self.parameters.capacity, - }), - ); self.updatable_structures .initialise_bounds_and_remove_fixed(context.domains(), &self.parameters); register_tasks(&self.parameters.tasks, context.reborrow(), false); + register_checkers(&mut context, self.constraint_tag, &self.parameters); self.inference_code = Some(InferenceCode::new(self.constraint_tag, TimeTable)); 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 3328db381..6979f002c 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 @@ -6,10 +6,15 @@ use std::cmp::min; use std::rc::Rc; use pumpkin_core::asserts::pumpkin_assert_extreme; +use pumpkin_core::checkers::Scope; +use pumpkin_core::checkers::WeakConsistency; +use pumpkin_core::checkers::WeakRetentionChecker; +use pumpkin_core::proof::ConstraintTag; use pumpkin_core::proof::InferenceCode; use pumpkin_core::propagation::Domains; use pumpkin_core::propagation::EnqueueDecision; use pumpkin_core::propagation::PropagationContext; +use pumpkin_core::propagation::PropagatorConstructorContext; use pumpkin_core::propagation::ReadDomains; use pumpkin_core::state::PropagationStatusCP; use pumpkin_core::variables::IntegerVariable; @@ -19,6 +24,9 @@ use crate::cumulative::ResourceProfile; use crate::cumulative::Task; use crate::cumulative::UpdatableStructures; use crate::cumulative::UpdatedTaskInfo; +use crate::cumulative::time_table::CheckerTask; +use crate::cumulative::time_table::TimeTable; +use crate::cumulative::time_table::TimeTableChecker; use crate::propagators::cumulative::time_table::propagation_handler::CumulativePropagationHandler; /// The result of [`should_enqueue`], contains the [`EnqueueDecision`] whether the propagator should @@ -35,6 +43,56 @@ pub(crate) struct ShouldEnqueueResult { pub(crate) update: Option>, } +pub(crate) fn register_checkers( + context: &mut PropagatorConstructorContext, + constraint_tag: ConstraintTag, + parameters: &CumulativeParameters, +) { + context.add_inference_checker( + InferenceCode::new(constraint_tag, TimeTable), + Box::new(TimeTableChecker { + tasks: parameters + .tasks + .iter() + .map(|task| CheckerTask { + start_time: task.start_variable.clone(), + processing_time: task.processing_time, + resource_usage: task.resource_usage, + }) + .collect(), + capacity: parameters.capacity, + }), + ); + + let mut scope = Scope::default(); + parameters.tasks.iter().for_each(|task| { + task.start_variable.add_to_scope(&mut scope, task.id); + }); + + context.add_consistency_checker( + scope, + WeakRetentionChecker::new( + if parameters.options.allow_holes_in_domain { + WeakConsistency::Domain + } else { + WeakConsistency::Bounds + }, + TimeTableChecker { + tasks: parameters + .tasks + .iter() + .map(|task| CheckerTask { + start_time: task.start_variable.clone(), + processing_time: task.processing_time, + resource_usage: task.resource_usage, + }) + .collect(), + capacity: parameters.capacity, + }, + ), + ); +} + /// Determines whether a time-table propagator should enqueue and returns a structure containing the /// [`EnqueueDecision`] and the info of the task with the extended mandatory part (or [`None`] if no /// such task exists). This method should be called in the From ca935d04ece30e8071860b63f4eb3227f3ea14de Mon Sep 17 00:00:00 2001 From: Imko Marijnissen Date: Tue, 26 May 2026 11:46:45 +0200 Subject: [PATCH 18/23] chore: add proper error logging to weak retention checker --- .../src/checkers/weak_retention_checker.rs | 38 +++++++++++++++++-- 1 file changed, 34 insertions(+), 4 deletions(-) diff --git a/pumpkin-crates/core/src/checkers/weak_retention_checker.rs b/pumpkin-crates/core/src/checkers/weak_retention_checker.rs index 11f4569cf..dcba255f9 100644 --- a/pumpkin-crates/core/src/checkers/weak_retention_checker.rs +++ b/pumpkin-crates/core/src/checkers/weak_retention_checker.rs @@ -75,20 +75,50 @@ impl + Clone> RetentionChecker ) .expect("Domain should not be inconsistent"); - !self - .inference_checker - .check(state, &premises.clone(), consequent.as_ref()) + let value_could_not_be_removed = !self.inference_checker.check( + state, + &premises.clone(), + consequent.as_ref(), + ); + + if !value_could_not_be_removed { + log::error!( + "The value {value} could be removed from {domain_id}; the propagator is not weakly domain consistent.\n{premises:?} -> {:?}", + predicate!(domain_id != value) + ); + } + + value_could_not_be_removed }), WeakConsistency::Bounds => { let lb = domains.lower_bound(&domain_id); let lb_not_updatable = self.bound_not_updatable(predicate![domain_id >= lb + 1], &premises); + if !lb_not_updatable { + log::error!( + "The lower-bound of {domain_id} could be updated to {}; the propagator is not weakly bound consistent.\n{premises:?} -> {:?}", + lb + 1, + predicate!(domain_id >= lb + 1) + ); + + return false; + } + let ub = domains.upper_bound(&domain_id); let ub_not_updatable = self.bound_not_updatable(predicate![domain_id <= ub - 1], &premises); - lb_not_updatable && ub_not_updatable + if !ub_not_updatable { + log::error!( + "The upper-bound of {domain_id} could be updated to {}; the propagator is not weakly bound consistent.\n{premises:?} -> {:?}", + ub - 1, + predicate!(domain_id <= ub - 1) + ); + return false; + } + + true } }) } From 4d22b0964d9d54be72d956cd4876e8492b8cf2d9 Mon Sep 17 00:00:00 2001 From: Imko Marijnissen Date: Tue, 26 May 2026 13:33:51 +0200 Subject: [PATCH 19/23] fix: test cases --- .../core/src/checkers/self_disabling.rs | 2 +- .../src/checkers/strong_retention_checker.rs | 2 +- pumpkin-crates/core/src/engine/cp/mod.rs | 39 +++++++++++++++++++ .../propagators/reified_propagator/checker.rs | 6 +-- .../reified_propagator/constructor.rs | 6 +-- .../arithmetic/maximum/propagator.rs | 5 ++- 6 files changed, 51 insertions(+), 9 deletions(-) diff --git a/pumpkin-crates/core/src/checkers/self_disabling.rs b/pumpkin-crates/core/src/checkers/self_disabling.rs index 9a5770f3a..ae7ff5f4e 100644 --- a/pumpkin-crates/core/src/checkers/self_disabling.rs +++ b/pumpkin-crates/core/src/checkers/self_disabling.rs @@ -6,7 +6,7 @@ use super::RetentionChecker; use super::Scope; use crate::propagation::Domains; -/// A [`ConsistencyChecker`] wrapper that skips the inner check when the associated constraint has +/// A [`RetentionChecker`] wrapper that skips the inner check when the associated constraint has /// been deleted. /// /// The deletion flag is shared with the constraint owner (e.g. the nogood propagator). Setting the diff --git a/pumpkin-crates/core/src/checkers/strong_retention_checker.rs b/pumpkin-crates/core/src/checkers/strong_retention_checker.rs index 197dde804..c2876f7a9 100644 --- a/pumpkin-crates/core/src/checkers/strong_retention_checker.rs +++ b/pumpkin-crates/core/src/checkers/strong_retention_checker.rs @@ -16,7 +16,7 @@ pub enum StrongConsistency { Bounds, } -/// A [`ConsistencyChecker`] that enforces a strong consistency property. +/// A [`RetentionChecker`] that enforces a strong consistency property. /// /// The level of consistency is configured via [`StrongConsistency`]. #[derive(Clone, Debug)] diff --git a/pumpkin-crates/core/src/engine/cp/mod.rs b/pumpkin-crates/core/src/engine/cp/mod.rs index c0af305bb..f1b231585 100644 --- a/pumpkin-crates/core/src/engine/cp/mod.rs +++ b/pumpkin-crates/core/src/engine/cp/mod.rs @@ -14,7 +14,13 @@ pub use trailed::*; mod tests { use assignments::Assignments; + #[cfg(feature = "check-consistency")] + use crate::checkers::ConsistencyCheckerStore; + #[cfg(feature = "check-propagations")] + use crate::checkers::PropagationChecker; use crate::conjunction; + #[cfg(feature = "check-propagations")] + use crate::containers::HashMap; use crate::containers::StorageKey; use crate::engine::TrailedValues; use crate::engine::cp::assignments; @@ -36,12 +42,23 @@ mod tests { assert_eq!(reason_store.len(), 0); { let mut notification_engine = NotificationEngine::default(); + #[cfg(feature = "check-consistency")] + let mut consistency_checker_store = ConsistencyCheckerStore::default(); + #[cfg(feature = "check-propagations")] + let mut inference_checkers: HashMap< + InferenceCode, + Vec, + > = HashMap::default(); let mut context = PropagationContext::new( &mut trailed_values, &mut assignments, &mut reason_store, &mut notification_engine, PropagatorId(0), + #[cfg(feature = "check-consistency")] + &mut consistency_checker_store, + #[cfg(feature = "check-propagations")] + &mut inference_checkers, ); let result = context.post( @@ -67,12 +84,23 @@ mod tests { assert_eq!(reason_store.len(), 0); { let mut notification_engine = NotificationEngine::default(); + #[cfg(feature = "check-consistency")] + let mut consistency_checker_store = ConsistencyCheckerStore::default(); + #[cfg(feature = "check-propagations")] + let mut inference_checkers: HashMap< + InferenceCode, + Vec, + > = HashMap::default(); let mut context = PropagationContext::new( &mut trailed_values, &mut assignments, &mut reason_store, &mut notification_engine, PropagatorId(0), + #[cfg(feature = "check-consistency")] + &mut consistency_checker_store, + #[cfg(feature = "check-propagations")] + &mut inference_checkers, ); let result = context.post( @@ -98,12 +126,23 @@ mod tests { assert_eq!(reason_store.len(), 0); { let mut notification_engine = NotificationEngine::default(); + #[cfg(feature = "check-consistency")] + let mut consistency_checker_store = ConsistencyCheckerStore::default(); + #[cfg(feature = "check-propagations")] + let mut inference_checkers: HashMap< + InferenceCode, + Vec, + > = HashMap::default(); let mut context = PropagationContext::new( &mut trailed_values, &mut assignments, &mut reason_store, &mut notification_engine, PropagatorId(0), + #[cfg(feature = "check-consistency")] + &mut consistency_checker_store, + #[cfg(feature = "check-propagations")] + &mut inference_checkers, ); let result = context.post( diff --git a/pumpkin-crates/core/src/propagators/reified_propagator/checker.rs b/pumpkin-crates/core/src/propagators/reified_propagator/checker.rs index 825b62e6b..2e5055837 100644 --- a/pumpkin-crates/core/src/propagators/reified_propagator/checker.rs +++ b/pumpkin-crates/core/src/propagators/reified_propagator/checker.rs @@ -12,10 +12,10 @@ use crate::propagation::LocalId; use crate::propagation::ReadDomains; use crate::variables::Literal; -/// A [`ConsistencyChecker`] wrapper that skips the inner check when the reification literal is +/// A [`RetentionChecker`] wrapper that skips the inner check when the reification literal is /// not assigned to true. #[derive(Debug, Clone)] -pub struct ReifiedConsistencyChecker { +pub struct ReifiedRetentionChecker { pub inner: BoxedRetentionChecker, pub reification_literal: Literal, /// The [`LocalId`] of the reification literal in the scope, used to strip it before passing @@ -23,7 +23,7 @@ pub struct ReifiedConsistencyChecker { pub reification_literal_id: LocalId, } -impl RetentionChecker for ReifiedConsistencyChecker { +impl RetentionChecker for ReifiedRetentionChecker { fn check_retention(&mut self, scope: &Scope, domains: Domains<'_>) -> bool { if domains.evaluate_literal(self.reification_literal) != Some(true) { return true; diff --git a/pumpkin-crates/core/src/propagators/reified_propagator/constructor.rs b/pumpkin-crates/core/src/propagators/reified_propagator/constructor.rs index deabe1a0e..7584291ef 100644 --- a/pumpkin-crates/core/src/propagators/reified_propagator/constructor.rs +++ b/pumpkin-crates/core/src/propagators/reified_propagator/constructor.rs @@ -6,9 +6,9 @@ use crate::propagation::LocalId; use crate::propagation::Propagator; use crate::propagation::PropagatorConstructor; use crate::propagation::PropagatorConstructorContext; -#[cfg(feature = "check-consistency")] -use crate::propagators::ReifiedConsistencyChecker; use crate::propagators::ReifiedPropagator; +#[cfg(feature = "check-consistency")] +use crate::propagators::ReifiedRetentionChecker; use crate::variables::Literal; /// A [`PropagatorConstructor`] for the reified propagator. @@ -93,7 +93,7 @@ fn wrap_consistency_checkers( reification_literal.add_to_scope(scope, reification_literal_id); replace_with::replace_with_or_abort(checker, |inner_checker| { - BoxedRetentionChecker::from(ReifiedConsistencyChecker { + BoxedRetentionChecker::from(ReifiedRetentionChecker { inner: inner_checker, reification_literal, reification_literal_id, diff --git a/pumpkin-crates/propagators/src/propagators/arithmetic/maximum/propagator.rs b/pumpkin-crates/propagators/src/propagators/arithmetic/maximum/propagator.rs index 93ab32554..c37b62379 100644 --- a/pumpkin-crates/propagators/src/propagators/arithmetic/maximum/propagator.rs +++ b/pumpkin-crates/propagators/src/propagators/arithmetic/maximum/propagator.rs @@ -9,8 +9,11 @@ use pumpkin_core::propagation::ReadDomains; use pumpkin_core::state::PropagationStatusCP; use pumpkin_core::variables::IntegerVariable; +#[cfg(doc)] +use super::MaximumConstructor; + /// Bounds-consistent propagator which enforces `max(array) = rhs`. Can be constructed through -/// [`MaximumArgs`]. +/// [`MaximumConstructor`]. #[derive(Clone, Debug)] pub struct MaximumPropagator { pub(crate) array: Box<[ElementVar]>, From c676757dc4a405d36135e94371b746aa6bb3841e Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Thu, 28 May 2026 09:31:03 +1000 Subject: [PATCH 20/23] Squashed commit of the following: commit b95ad412dfce9418666814864ef8b83df6952606 Author: Maarten Flippo Date: Wed May 27 23:29:26 2026 +1000 Also update the proof processor commit b4568923f4ccd9bbf911575cfae9039276a5cfb7 Author: Maarten Flippo Date: Wed May 27 23:18:14 2026 +1000 Implement the new interface in the rest of pumpkin commit c5290b13ebcd469b051e904782590824b1608c89 Author: Maarten Flippo Date: Wed May 27 22:46:16 2026 +1000 Remove Watchers::watch_all in favor of event target commit 27ed751b9f9c3a35c8a1ecfc0b4988ba6187ec34 Author: Maarten Flippo Date: Wed May 27 22:28:36 2026 +1000 Do the registration in the state commit 4dd42225ba27f6d6758378244848285062142606 Author: Maarten Flippo Date: Wed May 27 22:26:41 2026 +1000 Remove registration from PropagatorConstructorContext commit 39f97fd8f43b705a4fb54e6ed814d67bdf7fdc66 Author: Maarten Flippo Date: Wed May 27 22:09:21 2026 +1000 PropagatatorConstructor::create now returns an EventRegistration commit 7777f42068f6c00bcf4daf21b0dbddf6cbfb4cc0 Author: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun May 24 12:56:42 2026 +1000 chore(deps): bump enumset from 1.1.12 to 1.1.13 (#452) Bumps [enumset](https://github.com/Lymia/enumset) from 1.1.12 to 1.1.13.

Changelog

Sourced from enumset's changelog.

Version 1.1.13 (2026-05-18)

  • Revert a semver breaking change introduced in 1.1.12 relating to using functions like EnumSet<T>.symmetric_difference with Enum::A.into().
  • Deprecate EnumSet<T>.symmetrical_difference in favor of symmetric_difference, to better match the standard library sets.
Commits
  • 3ccc570 Bump version to 1.1.13
  • fec58cc Bless new trybuild output.
  • 2bc3757 Deprecate symmetrical_difference in favor of symmetric_difference
  • 9cfdb1d Fix accidental breaking change in the signatures of e.g. EnumSet.is_disjoint
  • 117824a Clarify the format of the bit indexes returned.
  • See full diff in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=enumset&package-manager=cargo&previous-version=1.1.12&new-version=1.1.13)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
--------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Maarten Flippo commit dfbdff1cf3930c38f80264e9d991f6f9168be688 Author: Imko Marijnissen <50290518+ImkoMarijnissen@users.noreply.github.com> Date: Sun May 24 04:54:59 2026 +0200 fix: README links + updating component overview (#453) commit bbfd857a03cff0a7a6d2937e44d7fa43ba9721f4 Author: Imko Marijnissen <50290518+ImkoMarijnissen@users.noreply.github.com> Date: Thu May 21 09:38:47 2026 +0200 chore: avoid rebuilding unnecessarily (#450) #438 introduced a `rerun-if-changed` based on git directories. However, this causes rebuilding even if nothing has changed. I propose to remove these lines (i.e., to go back to the rebuilding strategy before #438). commit 7425839e4f6e04f1171cb851f8db2af97972e87b Author: Imko Marijnissen <50290518+ImkoMarijnissen@users.noreply.github.com> Date: Thu May 21 09:33:32 2026 +0200 fix: disambiguate .msc files (#451) Currently, we have two separate [`.msc`](https://docs.minizinc.dev/en/stable/fzn-spec.html#solver-configuration-files) files: one for "default" Pumpkin and another for Pumpkin with proof logging. However, there is a name clash when using these two approaches, making it unclear which library is in use. This PR fixes this by changing the name in the `pumpkin-for-proofs.msc` file. It will then show up as follows in the listing: ``` Pumpkin 0.1 (nl.tudelft.algorithmics.pumpkin, cp, lcg, int) PumpkinProof 0.1 (nl.tudelft.algorithmics.pumpkin, cp, lcg, int) ``` We could also consider moving it to a separate folder. --- Cargo.lock | 4 +- README.md | 29 ++-- minizinc/pumpkin-for-proofs.msc | 4 +- pumpkin-crates/core/Cargo.toml | 2 +- pumpkin-crates/core/src/basic_types/mod.rs | 2 - .../core/src/basic_types/ref_or_owned.rs | 49 ------ .../domain_event_watch_list.rs | 5 - pumpkin-crates/core/src/engine/state.rs | 13 +- .../core/src/engine/variables/affine_view.rs | 29 ++-- .../core/src/engine/variables/constant.rs | 23 ++- .../core/src/engine/variables/domain_id.rs | 17 +- .../src/engine/variables/integer_variable.rs | 5 +- .../core/src/engine/variables/literal.rs | 17 +- .../core/src/propagation/constructor.rs | 148 +----------------- .../contexts/propagation_context.rs | 37 ++++- .../src/propagation/event_registration.rs | 117 ++++++++++++++ .../core/src/propagation/local_id.rs | 5 + pumpkin-crates/core/src/propagation/mod.rs | 3 + .../hypercube_linear/propagator.rs | 16 +- .../propagators/nogoods/nogood_propagator.rs | 22 ++- .../reified_propagator/propagator.rs | 80 ++++++++-- pumpkin-crates/propagators/Cargo.toml | 2 +- .../propagators/arithmetic/absolute_value.rs | 16 +- .../arithmetic/binary/binary_not_equals.rs | 16 +- .../arithmetic/binary_equals/constructor.rs | 5 +- .../arithmetic/binary_equals/propagator.rs | 1 + .../arithmetic/integer_division.rs | 21 ++- .../arithmetic/linear_less_or_equal.rs | 20 ++- .../arithmetic/linear_not_equal.rs | 12 +- .../arithmetic/maximum/constructor.rs | 30 +++- .../arithmetic/maximum/propagator.rs | 50 ++++++ .../time_table_over_interval_incremental.rs | 11 +- .../time_table_per_point_incremental.rs | 11 +- .../time_table/time_table_over_interval.rs | 12 +- .../time_table/time_table_per_point.rs | 12 +- .../src/propagators/cumulative/utils/util.rs | 16 +- .../disjunctive/disjunctive_propagator.rs | 31 ++-- .../propagators/src/propagators/element.rs | 18 ++- .../src/deduction_propagator.rs | 12 +- pumpkin-solver/build.rs | 4 +- 40 files changed, 561 insertions(+), 366 deletions(-) delete mode 100644 pumpkin-crates/core/src/basic_types/ref_or_owned.rs create mode 100644 pumpkin-crates/core/src/propagation/event_registration.rs diff --git a/Cargo.lock b/Cargo.lock index e84e49133..f65024419 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -439,9 +439,9 @@ dependencies = [ [[package]] name = "enumset" -version = "1.1.12" +version = "1.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f96a4a12fe60ac746ae295a1a4ecb5bb02debc20856506c8635288065f142de" +checksum = "839c4174b41e75c8f7306110b2c51996a293b8d1d850edd529011841d9fede7d" dependencies = [ "enumset_derive", ] diff --git a/README.md b/README.md index ea423b07d..24aac4e00 100644 --- a/README.md +++ b/README.md @@ -23,10 +23,10 @@ A unique feature of Pumpkin is that it can produce a certificate of unsatisfiabi The solver currently supports integer variables and a number of (global) constraints: -- [Cumulative global constraint](https://github.com/ConSol-Lab/Pumpkin/tree/main/pumpkin-crates/core/src/propagators/cumulative). -- [Disjunctive global constraint](https://github.com/ConSol-Lab/Pumpkin/tree/main/pumpkin-crates/core/src/propagators/disjunctive) -- [Element global constraint](https://github.com/ConSol-Lab/Pumpkin/blob/main/pumpkin-crates/core/src/propagators/element.rs). -- [Arithmetic constraints](https://github.com/ConSol-Lab/Pumpkin/tree/main/pumpkin-crates/core/src/propagators/arithmetic): [linear integer (in)equalities](https://github.com/ConSol-Lab/Pumpkin/blob/main/pumpkin-crates/core/src/propagators/arithmetic/linear_less_or_equal.rs), [integer division](https://github.com/ConSol-Lab/Pumpkin/blob/main/pumpkin-crates/core/src/propagators/arithmetic/integer_division.rs), [integer multiplication](https://github.com/ConSol-Lab/Pumpkin/blob/main/pumpkin-crates/core/src/propagators/arithmetic/integer_multiplication.rs), [maximum](https://github.com/ConSol-Lab/Pumpkin/blob/main/pumpkin-crates/core/src/propagators/arithmetic/maximum.rs), [absolute value](https://github.com/ConSol-Lab/Pumpkin/blob/main/pumpkin-crates/core/src/propagators/arithmetic/absolute_value.rs). +- [Cumulative global constraint](https://github.com/ConSol-Lab/Pumpkin/tree/main/pumpkin-crates/propagators/src/propagators/cumulative). +- [Disjunctive global constraint](https://github.com/ConSol-Lab/Pumpkin/tree/main/pumpkin-crates/propagators/src/propagators/disjunctive) +- [Element global constraint](https://github.com/ConSol-Lab/Pumpkin/blob/main/pumpkin-crates/propagators/src/propagators/element.rs). +- [Arithmetic constraints](https://github.com/ConSol-Lab/Pumpkin/tree/main/pumpkin-crates/propagators/src/propagators/arithmetic): [linear integer (in)equalities](https://github.com/ConSol-Lab/Pumpkin/blob/main/pumpkin-crates/propagators/src/propagators/arithmetic/linear_less_or_equal.rs), [integer division](https://github.com/ConSol-Lab/Pumpkin/blob/main/pumpkin-crates/propagators/src/propagators/arithmetic/integer_division.rs), [integer multiplication](https://github.com/ConSol-Lab/Pumpkin/blob/main/pumpkin-crates/propagators/src/propagators/arithmetic/integer_multiplication.rs), [maximum](https://github.com/ConSol-Lab/Pumpkin/blob/main/pumpkin-crates/propagators/src/propagators/arithmetic/maximum.rs), [absolute value](https://github.com/ConSol-Lab/Pumpkin/blob/main/pumpkin-crates/propagators/src/propagators/arithmetic/absolute_value.rs). - Clausal constraints. We are actively developing Pumpkin and would be happy to hear from you should you have any questions or feature requests! @@ -98,13 +98,22 @@ To use it as such a backend, follow the following steps: - Step 3: Add the following to the `MZN_SOLVER_PATH` environment variable: `/minizinc` (see [this thread](https://askubuntu.com/questions/58814/how-do-i-add-environment-variables) on how to do this using a shell). - Step 4: Check whether the installation worked using the command `minizinc --help pumpkin`. -## Components -Pumpkin consists of 3 different crates: +This will add Pumpkin and PumpkinProof (which uses a flattening library specific for proof logging). -- The library contained in [core](https://github.com/ConSol-Lab/Pumpkin/tree/main/pumpkin-crates/core); defines the API through which the solver can be used via Rust. -- The CLI contained in [pumpkin-solver](https://github.com/ConSol-Lab/Pumpkin/tree/main/pumpkin-solver/src/bin/pumpkin-solver); defines the usage of Pumpkin through a command line. -- The proof logging contained in [drcp-format](https://github.com/ConSol-Lab/Pumpkin/tree/main/drcp-format); defines proof logging which can be used in combination with Pumpkin. -- The python bindings contained in [pumpkin-solver-py](https://github.com/ConSol-Lab/Pumpkin/tree/main/pumpkin-solver-py); defines the python interface for Pumpkin +## Components +Pumpkin consists of several different components: + +- The crates contained in [pumpkin-crates](https://github.com/ConSol-Lab/Pumpkin/tree/main/pumpkin-crates): + - [pumpkin-core](https://github.com/ConSol-Lab/Pumpkin/tree/main/pumpkin-crates/core); defines the API through which the solver can be used via Rust. + - [pumpkin-propagators](https://github.com/ConSol-Lab/Pumpkin/tree/main/pumpkin-crates/propagators); contains (most of) the propagators used by Pumpkin. + - [pumpkin-constraints](https://github.com/ConSol-Lab/Pumpkin/tree/main/pumpkin-crates/constraints); contains convenient ways to add one or more propagators modelling certain constraints to the solver. + - [pumpkin-conflict-resolvers](https://github.com/ConSol-Lab/Pumpkin/tree/main/pumpkin-crates/conflict-resolvers); contains the conflict resolvers (e.g., 1UIP or All-Decision conflit resolvers) used by Pumpkin. + - [pumpkin-checking](https://github.com/ConSol-Lab/Pumpkin/tree/main/pumpkin-crates/checking); contains the types used for checking the soundness of propagators in Pumpkin. +- The CLI contained in [pumpkin-solver](https://github.com/ConSol-Lab/Pumpkin/tree/main/pumpkin-solver). +- The python bindings contained in [pumpkin-solver-py](https://github.com/ConSol-Lab/Pumpkin/tree/main/pumpkin-solver-py). +- The proof logging contained in [drcp-format](https://github.com/ConSol-Lab/Pumpkin/tree/main/drcp-format); a file reader and writer for the DRCP proof format (the proof format used by Pumpkin). +- The (unverified) proof processor contained in [pumpkin-proof-processor](https://github.com/ConSol-Lab/Pumpkin/tree/main/pumpkin-proof-processor). +- A debugger for DRCP proofs contained in [drcp-debugger](https://github.com/ConSol-Lab/Pumpkin/tree/main/drcp-debugger). The easiest way to get to know the different modules is through the documentation. This documentation can be created automatically using the command: ```sh diff --git a/minizinc/pumpkin-for-proofs.msc b/minizinc/pumpkin-for-proofs.msc index 4ef5d6654..d2cd57413 100644 --- a/minizinc/pumpkin-for-proofs.msc +++ b/minizinc/pumpkin-for-proofs.msc @@ -1,6 +1,6 @@ { - "name": "Pumpkin", - "id": "nl.tudelft.algorithmics.pumpkin", + "name": "PumpkinProof", + "id": "nl.tudelft.algorithmics.pumpkin_proof", "version": "0.1", "executable": "../target/release/pumpkin-solver", "mznlib": "./lib-for-proofs/", diff --git a/pumpkin-crates/core/Cargo.toml b/pumpkin-crates/core/Cargo.toml index 36796d6d3..f787b9da3 100644 --- a/pumpkin-crates/core/Cargo.toml +++ b/pumpkin-crates/core/Cargo.toml @@ -15,7 +15,7 @@ pumpkin-checking = { version = "0.3.0", path = "../checking" } thiserror = "2.0.12" log = "0.4.17" bitfield = "0.19.4" -enumset = "1.1.12" +enumset = "1.1.13" fnv = "1.0.7" # We require features which are on the `main` branch of the repository but are not on crates.io rand = { version = "0.10.1", features = [ "alloc" ] } once_cell = "1.19.0" diff --git a/pumpkin-crates/core/src/basic_types/mod.rs b/pumpkin-crates/core/src/basic_types/mod.rs index b87c9a856..b1fd78c9c 100644 --- a/pumpkin-crates/core/src/basic_types/mod.rs +++ b/pumpkin-crates/core/src/basic_types/mod.rs @@ -4,7 +4,6 @@ mod function; mod predicate_id_generators; mod propositional_conjunction; mod random; -mod ref_or_owned; pub(crate) mod sequence_generators; mod solution; mod stored_conflict_info; @@ -19,7 +18,6 @@ pub use predicate_id_generators::PredicateId; pub use predicate_id_generators::PredicateIdGenerator; pub use propositional_conjunction::PropositionalConjunction; pub use random::*; -pub(crate) use ref_or_owned::*; pub use solution::ProblemSolution; pub use solution::Solution; pub use solution::SolutionReference; diff --git a/pumpkin-crates/core/src/basic_types/ref_or_owned.rs b/pumpkin-crates/core/src/basic_types/ref_or_owned.rs deleted file mode 100644 index f16f3d6c1..000000000 --- a/pumpkin-crates/core/src/basic_types/ref_or_owned.rs +++ /dev/null @@ -1,49 +0,0 @@ -use std::ops::Deref; -use std::ops::DerefMut; - -/// Either owns a value or has a mutable reference to a value. -/// -/// Used to store data in a reborrowed context that needs to be 'shared' with the original context -/// that was reborrowed from. For example, when dropping a reborrowed context, we want -/// [`PropagatorConstructorContext::get_next_local_id`] in the original context to 'know' about the -/// registered local ids in the reborrowed context. -#[derive(Debug)] -pub(crate) enum RefOrOwned<'a, T> { - Ref(&'a mut T), - Owned(T), -} - -impl RefOrOwned<'_, T> { - pub(crate) fn reborrow(&mut self) -> RefOrOwned<'_, T> { - match self { - RefOrOwned::Ref(ref_to_t) => RefOrOwned::Ref(ref_to_t), - RefOrOwned::Owned(value) => RefOrOwned::Ref(value), - } - } -} - -impl From for RefOrOwned<'_, T> { - fn from(value: T) -> Self { - RefOrOwned::Owned(value) - } -} - -impl Deref for RefOrOwned<'_, T> { - type Target = T; - - fn deref(&self) -> &Self::Target { - match self { - RefOrOwned::Ref(reference) => reference, - RefOrOwned::Owned(value) => value, - } - } -} - -impl DerefMut for RefOrOwned<'_, T> { - fn deref_mut(&mut self) -> &mut Self::Target { - match self { - RefOrOwned::Ref(reference) => reference, - RefOrOwned::Owned(value) => value, - } - } -} diff --git a/pumpkin-crates/core/src/engine/notifications/domain_event_notification/domain_event_watch_list.rs b/pumpkin-crates/core/src/engine/notifications/domain_event_notification/domain_event_watch_list.rs index 9d3220c2c..6646aecad 100644 --- a/pumpkin-crates/core/src/engine/notifications/domain_event_notification/domain_event_watch_list.rs +++ b/pumpkin-crates/core/src/engine/notifications/domain_event_notification/domain_event_watch_list.rs @@ -102,11 +102,6 @@ impl<'a> Watchers<'a> { } } - pub(crate) fn watch_all(&mut self, domain: DomainId, events: EnumSet) { - self.notification_engine - .watch_all(domain, events, self.propagator_var); - } - pub(crate) fn unwatch_all(&mut self, domain: DomainId) { self.notification_engine .unwatch_all(domain, self.propagator_var); diff --git a/pumpkin-crates/core/src/engine/state.rs b/pumpkin-crates/core/src/engine/state.rs index 6b79dd269..5d33f051e 100644 --- a/pumpkin-crates/core/src/engine/state.rs +++ b/pumpkin-crates/core/src/engine/state.rs @@ -36,6 +36,7 @@ use crate::propagation::Propagator; use crate::propagation::PropagatorConstructor; use crate::propagation::PropagatorConstructorContext; use crate::propagation::PropagatorId; +use crate::propagation::PropagatorVarId; use crate::propagation::store::PropagatorStore; use crate::pumpkin_assert_advanced; use crate::pumpkin_assert_eq_simple; @@ -340,7 +341,17 @@ impl State { let constructor_context = PropagatorConstructorContext::new(original_handle.propagator_id(), self); - let propagator = constructor.create(constructor_context); + let (registration, propagator) = constructor.create(constructor_context); + + for (domain_id, events, local_id) in registration.iter() { + let propagator_var = PropagatorVarId { + propagator: original_handle.propagator_id(), + variable: local_id, + }; + + self.notification_engine + .watch_all(domain_id, events, propagator_var); + } pumpkin_assert_simple!( propagator.priority() as u8 <= 3, diff --git a/pumpkin-crates/core/src/engine/variables/affine_view.rs b/pumpkin-crates/core/src/engine/variables/affine_view.rs index daf56fd59..d5a2b7b84 100644 --- a/pumpkin-crates/core/src/engine/variables/affine_view.rs +++ b/pumpkin-crates/core/src/engine/variables/affine_view.rs @@ -20,6 +20,8 @@ use crate::engine::predicates::predicate_constructor::PredicateConstructor; use crate::engine::variables::DomainId; use crate::engine::variables::IntegerVariable; use crate::math::num_ext::NumExt; +use crate::propagation::EventDispatcher; +use crate::propagation::EventTarget; use crate::propagation::LocalId; /// Models the constraint `y = ax + b`, by expressing the domain of `y` as a transformation of the @@ -112,6 +114,22 @@ where } } +impl EventTarget for AffineView { + fn register( + &self, + registration: &mut impl EventDispatcher, + mut events: EnumSet, + local_id: LocalId, + ) { + let bound = DomainEvent::LowerBound | DomainEvent::UpperBound; + let intersection = events.intersection(bound); + if intersection.len() == 1 && self.scale.is_negative() { + events = events.symmetric_difference(bound); + } + self.inner.register(registration, events, local_id); + } +} + impl CheckerVariable for AffineView { fn does_atomic_constrain_self(&self, atomic: &Predicate) -> bool { self.inner.does_atomic_constrain_self(atomic) @@ -323,15 +341,6 @@ where .map(|value| self.map(value)) } - fn watch_all(&self, watchers: &mut Watchers<'_>, mut events: EnumSet) { - let bound = DomainEvent::LowerBound | DomainEvent::UpperBound; - let intersection = events.intersection(bound); - if intersection.len() == 1 && self.scale.is_negative() { - events = events.symmetrical_difference(bound); - } - self.inner.watch_all(watchers, events); - } - fn unwatch_all(&self, watchers: &mut Watchers<'_>) { self.inner.unwatch_all(watchers); } @@ -340,7 +349,7 @@ where let bound = DomainEvent::LowerBound | DomainEvent::UpperBound; let intersection = events.intersection(bound); if intersection.len() == 1 && self.scale.is_negative() { - events = events.symmetrical_difference(bound); + events = events.symmetric_difference(bound); } self.inner.watch_all_backtrack(watchers, events); } diff --git a/pumpkin-crates/core/src/engine/variables/constant.rs b/pumpkin-crates/core/src/engine/variables/constant.rs index 486ea28ab..af328ba3b 100644 --- a/pumpkin-crates/core/src/engine/variables/constant.rs +++ b/pumpkin-crates/core/src/engine/variables/constant.rs @@ -1,12 +1,21 @@ +use enumset::EnumSet; use pumpkin_checking::CheckerVariable; use pumpkin_checking::IntExt; use crate::engine::Assignments; use crate::predicates::Predicate; use crate::predicates::PredicateConstructor; +use crate::propagation::DomainEvent; +use crate::propagation::EventDispatcher; +use crate::propagation::EventTarget; +use crate::propagation::LocalId; use crate::variables::IntegerVariable; use crate::variables::TransformableVariable; +impl EventTarget for i32 { + fn register(&self, _: &mut impl EventDispatcher, _: EnumSet, _: LocalId) {} +} + impl IntegerVariable for i32 { type AffineView = i32; @@ -51,26 +60,16 @@ impl IntegerVariable for i32 { std::iter::once(*self) } - fn watch_all( - &self, - _watchers: &mut crate::engine::notifications::Watchers<'_>, - _events: enumset::EnumSet, - ) { - } - fn unwatch_all(&self, _watchers: &mut crate::engine::notifications::Watchers<'_>) {} fn watch_all_backtrack( &self, _watchers: &mut crate::engine::notifications::Watchers<'_>, - _events: enumset::EnumSet, + _events: EnumSet, ) { } - fn unpack_event( - &self, - _event: crate::propagation::OpaqueDomainEvent, - ) -> crate::propagation::DomainEvent { + fn unpack_event(&self, _event: crate::propagation::OpaqueDomainEvent) -> DomainEvent { unreachable!() } diff --git a/pumpkin-crates/core/src/engine/variables/domain_id.rs b/pumpkin-crates/core/src/engine/variables/domain_id.rs index 9a1a02af7..d652da294 100644 --- a/pumpkin-crates/core/src/engine/variables/domain_id.rs +++ b/pumpkin-crates/core/src/engine/variables/domain_id.rs @@ -18,6 +18,8 @@ use crate::engine::variables::IntegerVariable; use crate::predicates::Predicate; use crate::predicates::PredicateConstructor; use crate::predicates::PredicateType; +use crate::propagation::EventDispatcher; +use crate::propagation::EventTarget; use crate::propagation::LocalId; use crate::pumpkin_assert_simple; @@ -71,6 +73,17 @@ impl SupportsValue for DomainId { } } +impl EventTarget for DomainId { + fn register( + &self, + registration: &mut impl EventDispatcher, + events: EnumSet, + local_id: LocalId, + ) { + registration.register(*self, events, local_id); + } +} + impl CheckerVariable for DomainId { fn does_atomic_constrain_self(&self, atomic: &Predicate) -> bool { atomic.get_domain() == *self @@ -194,10 +207,6 @@ impl IntegerVariable for DomainId { assignment.get_domain_iterator(*self) } - fn watch_all(&self, watchers: &mut Watchers<'_>, events: EnumSet) { - watchers.watch_all(*self, events); - } - fn unwatch_all(&self, watchers: &mut Watchers<'_>) { watchers.unwatch_all(*self); } diff --git a/pumpkin-crates/core/src/engine/variables/integer_variable.rs b/pumpkin-crates/core/src/engine/variables/integer_variable.rs index 1ed710e61..37a677b77 100644 --- a/pumpkin-crates/core/src/engine/variables/integer_variable.rs +++ b/pumpkin-crates/core/src/engine/variables/integer_variable.rs @@ -12,6 +12,7 @@ use crate::engine::notifications::OpaqueDomainEvent; use crate::engine::notifications::Watchers; use crate::engine::predicates::predicate_constructor::PredicateConstructor; use crate::predicates::Predicate; +use crate::propagation::EventTarget; /// A trait specifying the required behaviour of an integer variable such as retrieving a /// lower-bound ([`IntegerVariable::lower_bound`]). @@ -23,6 +24,7 @@ pub trait IntegerVariable: + CheckerVariable + ScopeItem + SupportsValue + + EventTarget { type AffineView: IntegerVariable; @@ -54,9 +56,6 @@ pub trait IntegerVariable: /// Iterate over the values of the domain. fn iterate_domain(&self, assignment: &Assignments) -> impl Iterator; - /// Register a watch for this variable on the given domain events. - fn watch_all(&self, watchers: &mut Watchers<'_>, events: EnumSet); - /// Remove the watcher on this variable. fn unwatch_all(&self, watchers: &mut Watchers<'_>); diff --git a/pumpkin-crates/core/src/engine/variables/literal.rs b/pumpkin-crates/core/src/engine/variables/literal.rs index a2c4a88e0..644f0e675 100644 --- a/pumpkin-crates/core/src/engine/variables/literal.rs +++ b/pumpkin-crates/core/src/engine/variables/literal.rs @@ -21,6 +21,7 @@ use crate::engine::notifications::Watchers; use crate::engine::predicates::predicate::Predicate; use crate::engine::predicates::predicate_constructor::PredicateConstructor; use crate::engine::variables::AffineView; +use crate::propagation::EventTarget; use crate::propagation::LocalId; #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] @@ -102,6 +103,18 @@ impl SupportsValue for Literal { } } +impl EventTarget for Literal { + fn register( + &self, + registration: &mut impl EventDispatcher, + events: EnumSet, + local_id: LocalId, + ) { + self.integer_variable + .register(registration, events, local_id); + } +} + 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); @@ -193,10 +206,6 @@ impl IntegerVariable for Literal { self.integer_variable.iterate_domain(assignment) } - fn watch_all(&self, watchers: &mut Watchers<'_>, events: EnumSet) { - self.integer_variable.watch_all(watchers, events) - } - fn unwatch_all(&self, watchers: &mut Watchers<'_>) { self.integer_variable.unwatch_all(watchers) } diff --git a/pumpkin-crates/core/src/propagation/constructor.rs b/pumpkin-crates/core/src/propagation/constructor.rs index 220441c3d..5a0699ca3 100644 --- a/pumpkin-crates/core/src/propagation/constructor.rs +++ b/pumpkin-crates/core/src/propagation/constructor.rs @@ -1,6 +1,3 @@ -use std::ops::Deref; -use std::ops::DerefMut; - use pumpkin_checking::InferenceChecker; use super::Domains; @@ -27,6 +24,8 @@ use crate::proof::InferenceCode; #[cfg(doc)] use crate::propagation::DomainEvent; use crate::propagation::DomainEvents; +use crate::propagation::EventRegistration; +use crate::propagators::reified_propagator::ReifiedChecker; use crate::variables::IntegerVariable; /// A propagator constructor creates a fully initialized instance of a [`Propagator`]. @@ -44,7 +43,10 @@ pub trait PropagatorConstructor { type PropagatorImpl: Propagator + Clone; /// Create the propagator instance from `Self`. - fn create(self, context: PropagatorConstructorContext) -> Self::PropagatorImpl; + fn create( + self, + context: PropagatorConstructorContext, + ) -> (EventRegistration, Self::PropagatorImpl); } /// [`PropagatorConstructorContext`] is used when [`Propagator`]s are initialised after creation. @@ -57,15 +59,6 @@ pub struct PropagatorConstructorContext<'a> { state: &'a mut State, pub(crate) propagator_id: PropagatorId, - /// A [`LocalId`] that is guaranteed not to be used to register any variables yet. This is - /// either a reference or an owned value, to support - /// [`PropagatorConstructorContext::reborrow`]. - next_local_id: RefOrOwned<'a, LocalId>, - - /// Marker to indicate whether the constructor registered for at least one domain event or - /// predicate becoming assigned. If not, the [`Drop`] implementation will cause a panic. - did_register: RefOrOwned<'a, bool>, - /// Pending consistency checkers to be registered into [`State`] when this context is dropped. #[cfg(feature = "check-consistency")] pub(crate) pending_consistency_checkers: RefOrOwned<'a, Vec<(Scope, BoxedRetentionChecker)>>, @@ -86,10 +79,8 @@ impl PropagatorConstructorContext<'_> { state: &'a mut State, ) -> PropagatorConstructorContext<'a> { PropagatorConstructorContext { - next_local_id: RefOrOwned::Owned(LocalId::from(0)), propagator_id, state, - did_register: RefOrOwned::Owned(false), #[cfg(feature = "check-consistency")] pending_consistency_checkers: RefOrOwned::Owned(vec![]), #[cfg(feature = "check-propagations")] @@ -97,54 +88,14 @@ impl PropagatorConstructorContext<'_> { } } - /// Indicate that the constructor is deliberately not registering the propagator to be enqueued - /// at any time. - /// - /// If this is called and later a registration happens, then the registration will still go - /// through. Calling this function only prevents the crash if no registration happens. - pub fn will_not_register_any_events(&mut self) { - *self.did_register = true; - } - /// Get domain information. pub fn domains(&mut self) -> Domains<'_> { Domains::new(&self.state.assignments, &mut self.state.trailed_values) } - /// Subscribes the propagator to the given [`DomainEvents`]. - /// - /// The domain events determine when [`Propagator::notify()`] will be called on the propagator. - /// The [`LocalId`] is internal information related to the propagator, - /// which is used when calling [`Propagator::notify()`] to identify the variable. - /// - /// Each variable *must* have a unique [`LocalId`]. Most often this would be its index of the - /// variable in the internal array of variables. - /// - /// Duplicate registrations are ignored. - pub fn register( - &mut self, - var: impl IntegerVariable, - domain_events: DomainEvents, - local_id: LocalId, - ) { - self.will_not_register_any_events(); - - let propagator_var = PropagatorVarId { - propagator: self.propagator_id, - variable: local_id, - }; - - self.update_next_local_id(local_id); - - let mut watchers = Watchers::new(propagator_var, &mut self.state.notification_engine); - var.watch_all(&mut watchers, domain_events.events()); - } - /// Register the propagator to be enqueued when the given [`Predicate`] becomes true. /// Returns the [`PredicateId`] used by the solver to track the predicate. pub fn register_predicate(&mut self, predicate: Predicate) -> PredicateId { - self.will_not_register_any_events(); - self.state.notification_engine.watch_predicate( predicate, self.propagator_id, @@ -177,25 +128,16 @@ impl PropagatorConstructorContext<'_> { variable: local_id, }; - self.update_next_local_id(local_id); - let mut watchers = Watchers::new(propagator_var, &mut self.state.notification_engine); var.watch_all_backtrack(&mut watchers, domain_events.events()); } - /// Get a new [`LocalId`] which is guaranteed to be unused. - pub(crate) fn get_next_local_id(&self) -> LocalId { - *self.next_local_id.deref() - } - /// Reborrow the current context to a new value with a shorter lifetime. Should be used when /// passing `Self` to another function that takes ownership, but the value is still needed /// afterwards. pub fn reborrow(&mut self) -> PropagatorConstructorContext<'_> { PropagatorConstructorContext { propagator_id: self.propagator_id, - next_local_id: self.next_local_id.reborrow(), - did_register: self.did_register.reborrow(), state: self.state, #[cfg(feature = "check-consistency")] pending_consistency_checkers: self.pending_consistency_checkers.reborrow(), @@ -243,36 +185,10 @@ impl PropagatorConstructorContext<'_> { let _ = 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)); - - *self.next_local_id.deref_mut() = next_local_id; - } } impl Drop for PropagatorConstructorContext<'_> { fn drop(&mut self) { - if std::thread::panicking() { - // If we are already unwinding due to a panic, we do not want to trigger another one. - return; - } - - let did_register = match self.did_register { - // If we are in a reborrowed context, we do not want to enforce registration or drain - // pending checkers (the root context handles this). - RefOrOwned::Ref(_) => return, - - RefOrOwned::Owned(did_register) => did_register, - }; - - if !did_register { - panic!( - "Propagator did not register to be enqueued. If this is intentional, call PropagatorConstructorContext::will_not_register_any_events()." - ); - } - #[cfg(feature = "check-consistency")] for (scope, checker) in self.pending_consistency_checkers.drain(..) { self.state.add_consistency_checker(scope, checker); @@ -303,55 +219,3 @@ mod private { } } } - -#[cfg(test)] -mod tests { - - use super::*; - use crate::variables::DomainId; - - #[test] - #[should_panic] - fn panic_when_no_registration_happened() { - let mut state = State::default(); - state.notification_engine.grow(); - - let _c1 = PropagatorConstructorContext::new(PropagatorId(0), &mut state); - } - - #[test] - fn do_not_panic_if_told_no_registration_will_happen() { - let mut state = State::default(); - state.notification_engine.grow(); - - let mut ctx = PropagatorConstructorContext::new(PropagatorId(0), &mut state); - ctx.will_not_register_any_events(); - } - - #[test] - fn do_not_panic_if_no_registration_happens_in_reborrowed() { - let mut state = State::default(); - state.notification_engine.grow(); - - let mut ctx = PropagatorConstructorContext::new(PropagatorId(0), &mut state); - let ctx2 = ctx.reborrow(); - drop(ctx2); - - ctx.will_not_register_any_events(); - } - - #[test] - fn reborrowing_remembers_next_local_id() { - let mut state = State::default(); - state.notification_engine.grow(); - - let mut c1 = PropagatorConstructorContext::new(PropagatorId(0), &mut state); - c1.will_not_register_any_events(); - - let mut c2 = c1.reborrow(); - c2.register(DomainId::new(0), DomainEvents::ANY_INT, LocalId::from(1)); - drop(c2); - - assert_eq!(LocalId::from(2), c1.get_next_local_id()); - } -} diff --git a/pumpkin-crates/core/src/propagation/contexts/propagation_context.rs b/pumpkin-crates/core/src/propagation/contexts/propagation_context.rs index 0ebe4ffd0..465412702 100644 --- a/pumpkin-crates/core/src/propagation/contexts/propagation_context.rs +++ b/pumpkin-crates/core/src/propagation/contexts/propagation_context.rs @@ -1,3 +1,4 @@ +use enumset::EnumSet; #[cfg(feature = "check-propagations")] use pumpkin_checking::BoxedChecker; use pumpkin_checking::InferenceChecker; @@ -23,8 +24,10 @@ use crate::engine::reason::ReasonStore; use crate::engine::reason::StoredReason; use crate::engine::variables::Literal; use crate::proof::InferenceCode; +use crate::propagation::DomainEvent; use crate::propagation::DomainEvents; use crate::propagation::Domains; +use crate::propagation::EventDispatcher; use crate::propagation::HasAssignments; use crate::propagation::LocalId; #[cfg(doc)] @@ -36,6 +39,7 @@ use crate::propagation::PropagatorVarId; #[cfg(doc)] use crate::propagation::ReadDomains; use crate::pumpkin_assert_simple; +use crate::variables::DomainId; use crate::variables::IntegerVariable; /// Provided to the propagator when it is notified of a domain event. @@ -225,13 +229,14 @@ impl<'a> PropagationContext<'a> { domain_events: DomainEvents, local_id: LocalId, ) { - let propagator_var = PropagatorVarId { - propagator: self.propagator_id, - variable: local_id, - }; - - let mut watchers = Watchers::new(propagator_var, self.notification_engine); - var.watch_all(&mut watchers, domain_events.events()); + var.register( + &mut NotificationEngineWatchers { + notificaton_engine: self.notification_engine, + propagator_id: self.propagator_id, + }, + domain_events.events(), + local_id, + ); } /// Stop being enqueued for events on the given integer variable. @@ -373,3 +378,21 @@ pub(crate) fn build_reason( Reason::DynamicLazy(code) => StoredReason::DynamicLazy(code), } } + +struct NotificationEngineWatchers<'a> { + propagator_id: PropagatorId, + notificaton_engine: &'a mut NotificationEngine, +} + +impl EventDispatcher for NotificationEngineWatchers<'_> { + fn register(&mut self, domain_id: DomainId, events: EnumSet, local_id: LocalId) { + self.notificaton_engine.watch_all( + domain_id, + events, + PropagatorVarId { + propagator: self.propagator_id, + variable: local_id, + }, + ); + } +} diff --git a/pumpkin-crates/core/src/propagation/event_registration.rs b/pumpkin-crates/core/src/propagation/event_registration.rs new file mode 100644 index 000000000..665b92ed5 --- /dev/null +++ b/pumpkin-crates/core/src/propagation/event_registration.rs @@ -0,0 +1,117 @@ +use enumset::EnumSet; + +use crate::propagation::DomainEvent; +use crate::propagation::DomainEvents; +use crate::propagation::LocalId; +use crate::variables::DomainId; + +/// Anything that can subscribe to domain events. +pub trait EventTarget { + /// Add a registration of self for the given domain events with a local id. + fn register( + &self, + registration: &mut impl EventDispatcher, + events: EnumSet, + local_id: LocalId, + ); +} + +pub trait EventDispatcher { + /// Register the [`DomainId`] with the given [`LocalId`] on the given [`DomainEvents`]. + fn register(&mut self, domain_id: DomainId, events: EnumSet, local_id: LocalId); +} + +/// Contains all the events and domains that a propagator needs to be enqueued for. +#[derive(Clone, Debug)] +pub struct EventRegistration(Vec<(DomainId, EnumSet, LocalId)>); + +impl EventRegistration { + /// Create an [`EventRegistration`] without any variables. + /// + /// This is the uncommon case. Without registering for variable events, a propagator will never + /// be enqueued. + pub fn empty() -> EventRegistration { + EventRegistration(vec![]) + } + + /// Create a new [`EventRegistrationBuilder`]. + /// + /// If no event registrations will be made, use [`EventRegistration::empty`] instead. + /// Calling [`EventRegistrationBuilder::build`] without any registrations will cause a panic. + /// + /// # Example + /// + /// ``` + /// use pumpkin_core::propagation::DomainEvents; + /// use pumpkin_core::propagation::EventRegistration; + /// use pumpkin_core::propagation::LocalId; + /// use pumpkin_core::variables::DomainId; + /// + /// let v1 = DomainId::new(0); + /// let v2 = DomainId::new(0); + /// let registration = EventRegistration::builder() + /// .add(&v1, DomainEvents::ANY_INT, LocalId::from(0)) + /// .add(&v2, DomainEvents::ANY_INT, LocalId::from(1)) + /// .build(); + /// ``` + pub fn builder() -> EventRegistrationBuilder { + EventRegistrationBuilder { + registrations: EventRegistration(vec![]), + } + } + + /// Add a new event registration. + pub fn add( + &mut self, + target: &impl EventTarget, + domain_events: DomainEvents, + local_id: LocalId, + ) { + target.register(self, domain_events.events(), local_id); + } + + /// Iterate the registrations already made. + pub fn iter(&self) -> impl ExactSizeIterator, LocalId)> { + self.0.iter().copied() + } +} + +impl EventDispatcher for EventRegistration { + fn register(&mut self, domain_id: DomainId, events: EnumSet, local_id: LocalId) { + self.0.push((domain_id, events, local_id)); + } +} + +/// Used to construct an [`EventRegistration`] for heterogeneous [`EventTarget`] implementations. +/// +/// See [`EventRegistration::builder`] for a usage example. +#[derive(Clone, Debug)] +pub struct EventRegistrationBuilder { + registrations: EventRegistration, +} + +impl EventRegistrationBuilder { + /// Add a new event registration. + pub fn add( + mut self, + target: &impl EventTarget, + domain_events: DomainEvents, + local_id: LocalId, + ) -> Self { + self.registrations.add(target, domain_events, local_id); + self + } + + /// Finish constructing the [`EventRegistration`]. + /// + /// If no variables are registered, then this panics. If no variables can be registered during + /// construction, use [`EventRegistration::empty`]. + pub fn build(self) -> EventRegistration { + assert!( + !self.registrations.0.is_empty(), + "did not register for any events" + ); + + self.registrations + } +} diff --git a/pumpkin-crates/core/src/propagation/local_id.rs b/pumpkin-crates/core/src/propagation/local_id.rs index b3a3334df..baff94a32 100644 --- a/pumpkin-crates/core/src/propagation/local_id.rs +++ b/pumpkin-crates/core/src/propagation/local_id.rs @@ -10,6 +10,11 @@ impl LocalId { LocalId(value) } + /// Get the next [`LocalId`]. + pub const fn successor(self) -> Self { + LocalId(self.0 + 1) + } + pub fn unpack(self) -> u32 { self.0 } diff --git a/pumpkin-crates/core/src/propagation/mod.rs b/pumpkin-crates/core/src/propagation/mod.rs index 7c4b5b02b..c74ee7106 100644 --- a/pumpkin-crates/core/src/propagation/mod.rs +++ b/pumpkin-crates/core/src/propagation/mod.rs @@ -69,6 +69,7 @@ mod constructor; mod contexts; mod domains; +mod event_registration; mod local_id; mod propagator; @@ -76,6 +77,8 @@ pub(crate) mod propagator_id; pub(crate) mod propagator_var_id; pub(crate) mod store; +pub use event_registration::*; + mod reexports { // Re-exports of types not in this module according to the file tree. // These will probably be be moved at some point, but for now they are simply re-exported here diff --git a/pumpkin-crates/core/src/propagators/hypercube_linear/propagator.rs b/pumpkin-crates/core/src/propagators/hypercube_linear/propagator.rs index 92d0f7fb9..6a24f44bc 100644 --- a/pumpkin-crates/core/src/propagators/hypercube_linear/propagator.rs +++ b/pumpkin-crates/core/src/propagators/hypercube_linear/propagator.rs @@ -7,6 +7,8 @@ use crate::predicates::PropositionalConjunction; use crate::proof::ConstraintTag; use crate::proof::InferenceCode; use crate::propagation::DomainEvents; +use crate::propagation::EventRegistration; +use crate::propagation::InferenceCheckers; use crate::propagation::LocalId; use crate::propagation::PropagationContext; use crate::propagation::Propagator; @@ -32,7 +34,10 @@ pub struct HypercubeLinearConstructor { impl PropagatorConstructor for HypercubeLinearConstructor { type PropagatorImpl = HypercubeLinearPropagator; - fn create(self, mut context: PropagatorConstructorContext) -> Self::PropagatorImpl { + fn create( + self, + mut context: PropagatorConstructorContext, + ) -> (EventRegistration, Self::PropagatorImpl) { let HypercubeLinearConstructor { hypercube, linear, @@ -62,13 +67,18 @@ impl PropagatorConstructor for HypercubeLinearConstructor { ] }; - HypercubeLinearPropagator { + let propagator = HypercubeLinearPropagator { linear, hypercube_predicates, watched_predicates, inference_code: InferenceCode::new(constraint_tag, HypercubeLinear), - } + }; + + // TODO: This will be expanded with registration of predicates. + let registration = EventRegistration::empty(); + + (registration, propagator) } } diff --git a/pumpkin-crates/core/src/propagators/nogoods/nogood_propagator.rs b/pumpkin-crates/core/src/propagators/nogoods/nogood_propagator.rs index 291493054..7b69960a6 100644 --- a/pumpkin-crates/core/src/propagators/nogoods/nogood_propagator.rs +++ b/pumpkin-crates/core/src/propagators/nogoods/nogood_propagator.rs @@ -31,6 +31,7 @@ use crate::engine::reason::ReasonStore; use crate::predicate; use crate::proof::InferenceCode; use crate::propagation::EnqueueDecision; +use crate::propagation::EventRegistration; use crate::propagation::ExplanationContext; use crate::propagation::LazyExplanation; use crate::propagation::NotificationContext; @@ -118,10 +119,11 @@ impl NogoodPropagatorConstructor { impl PropagatorConstructor for NogoodPropagatorConstructor { type PropagatorImpl = NogoodPropagator; - fn create(self, mut context: PropagatorConstructorContext) -> Self::PropagatorImpl { - context.will_not_register_any_events(); - - NogoodPropagator { + fn create( + self, + context: PropagatorConstructorContext, + ) -> (EventRegistration, Self::PropagatorImpl) { + let propagator = NogoodPropagator { handle: PropagatorHandle::new(context.propagator_id), parameters: self.parameters, nogood_predicates: ArenaAllocator::new(self.capacity), @@ -136,7 +138,9 @@ impl PropagatorConstructor for NogoodPropagatorConstructor { temp_nogood_reason: Default::default(), #[cfg(feature = "check-consistency")] deletion_flags: Default::default(), - } + }; + + (EventRegistration::empty(), propagator) } } @@ -152,14 +156,6 @@ struct Watcher { cached_predicate: PredicateId, } -impl PropagatorConstructor for NogoodPropagator { - type PropagatorImpl = Self; - - fn create(self, _: PropagatorConstructorContext) -> Self::PropagatorImpl { - self - } -} - /// Keeps track of three tiers of nogoods: /// - "low" LBD nogoods /// - "mid" LBD nogoods diff --git a/pumpkin-crates/core/src/propagators/reified_propagator/propagator.rs b/pumpkin-crates/core/src/propagators/reified_propagator/propagator.rs index 6a683be04..ef71b8fd0 100644 --- a/pumpkin-crates/core/src/propagators/reified_propagator/propagator.rs +++ b/pumpkin-crates/core/src/propagators/reified_propagator/propagator.rs @@ -3,6 +3,7 @@ use crate::engine::notifications::OpaqueDomainEvent; use crate::predicates::Predicate; use crate::propagation::Domains; use crate::propagation::EnqueueDecision; +use crate::propagation::EventRegistration; use crate::propagation::ExplanationContext; use crate::propagation::LazyExplanation; use crate::propagation::LocalId; @@ -15,6 +16,58 @@ use crate::pumpkin_assert_simple; use crate::state::Conflict; use crate::variables::Literal; +/// A [`PropagatorConstructor`] for the reified propagator. +#[derive(Clone, Debug)] +pub struct ReifiedPropagatorArgs { + pub propagator: WrappedArgs, + pub reification_literal: Literal, +} + +impl PropagatorConstructor for ReifiedPropagatorArgs +where + WrappedArgs: PropagatorConstructor, + WrappedPropagator: Propagator + Clone, +{ + type PropagatorImpl = ReifiedPropagator; + + fn create( + self, + mut context: PropagatorConstructorContext, + ) -> (EventRegistration, Self::PropagatorImpl) { + let ReifiedPropagatorArgs { + propagator, + reification_literal, + } = self; + + let (mut registration, propagator) = propagator.create(context.reborrow()); + + let reification_literal_id = registration + .iter() + .map(|(_, _, lid)| lid) + .max() + .expect("cannot reify propagators that do not register all variables immediately") + .successor(); + + registration.add( + &self.reification_literal, + DomainEvents::BOUNDS, + reification_literal_id, + ); + + let name = format!("Reified({})", propagator.name()); + + let propagator = ReifiedPropagator { + propagator, + reification_literal, + reification_literal_id, + name, + reason_buffer: vec![], + }; + + (registration, propagator) + } +} + /// Propagator for the constraint `r -> p`, where `r` is a Boolean literal and `p` is an arbitrary /// propagator. /// @@ -213,6 +266,7 @@ mod tests { let _ = solver .new_propagator(ReifiedPropagatorArgs { propagator: GenericPropagator::new( + vec![a, b], move |_: PropagationContext| { Err(PropagatorConflict { conjunction: t1.clone(), @@ -247,6 +301,7 @@ mod tests { let propagator = solver .new_propagator(ReifiedPropagatorArgs { propagator: GenericPropagator::new( + vec![var], move |mut ctx: PropagationContext| { ctx.post( predicate![var >= 3], @@ -290,6 +345,7 @@ mod tests { let inconsistency = solver .new_propagator(ReifiedPropagatorArgs { propagator: GenericPropagator::new( + vec![var], move |_: PropagationContext| { Err(PropagatorConflict { conjunction: conjunction!([var >= 1]), @@ -331,6 +387,7 @@ mod tests { let propagator = solver .new_propagator(ReifiedPropagatorArgs { propagator: GenericPropagator::new( + vec![var], |_: PropagationContext| Ok(()), move |context: Domains| { if context.is_fixed(&var) { @@ -367,16 +424,17 @@ mod tests { { type PropagatorImpl = Self; - fn create(self, mut context: PropagatorConstructorContext) -> Self::PropagatorImpl { + fn create( + self, + _: PropagatorConstructorContext, + ) -> (EventRegistration, Self::PropagatorImpl) { + let mut registration = EventRegistration::empty(); + for (index, variable) in self.variables_to_register.iter().enumerate() { - context.register( - *variable, - DomainEvents::ANY_INT, - LocalId::from(index as u32), - ); + registration.add(variable, DomainEvents::ANY_INT, LocalId::from(index as u32)); } - self + (registration, self) } } @@ -403,11 +461,15 @@ mod tests { Propagation: Fn(PropagationContext) -> PropagationStatusCP, ConsistencyCheck: Fn(Domains) -> Option, { - pub(crate) fn new(propagation: Propagation, consistency_check: ConsistencyCheck) -> Self { + pub(crate) fn new( + variables_to_register: Vec, + propagation: Propagation, + consistency_check: ConsistencyCheck, + ) -> Self { GenericPropagator { propagation, consistency_check, - variables_to_register: vec![], + variables_to_register, } } diff --git a/pumpkin-crates/propagators/Cargo.toml b/pumpkin-crates/propagators/Cargo.toml index 93d8e709c..c7794e235 100644 --- a/pumpkin-crates/propagators/Cargo.toml +++ b/pumpkin-crates/propagators/Cargo.toml @@ -13,7 +13,7 @@ workspace = true [dependencies] pumpkin-core = { version = "0.3.0", path = "../core" } pumpkin-checking = { version = "0.3.0", path = "../checking" } -enumset = "1.1.12" +enumset = "1.1.13" bitfield-struct = "0.13.0" convert_case = "0.11.0" clap = { version = "4.5.40", optional = true, features=["derive"]} diff --git a/pumpkin-crates/propagators/src/propagators/arithmetic/absolute_value.rs b/pumpkin-crates/propagators/src/propagators/arithmetic/absolute_value.rs index e06585952..6150ac76b 100644 --- a/pumpkin-crates/propagators/src/propagators/arithmetic/absolute_value.rs +++ b/pumpkin-crates/propagators/src/propagators/arithmetic/absolute_value.rs @@ -8,6 +8,8 @@ use pumpkin_core::predicate; use pumpkin_core::proof::ConstraintTag; use pumpkin_core::proof::InferenceCode; use pumpkin_core::propagation::DomainEvents; +use pumpkin_core::propagation::EventRegistration; +use pumpkin_core::propagation::InferenceCheckers; use pumpkin_core::propagation::LocalId; use pumpkin_core::propagation::Priority; use pumpkin_core::propagation::PropagationContext; @@ -34,7 +36,7 @@ where { type PropagatorImpl = AbsoluteValuePropagator; - fn create(self, mut context: PropagatorConstructorContext) -> Self::PropagatorImpl { + fn create(self, _: PropagatorConstructorContext) -> (EventRegistration, Self::PropagatorImpl) { let AbsoluteValueArgs { signed, absolute, @@ -49,16 +51,20 @@ where }), ); - context.register(signed.clone(), DomainEvents::BOUNDS, LocalId::from(0)); - context.register(absolute.clone(), DomainEvents::BOUNDS, LocalId::from(1)); + let registration = EventRegistration::builder() + .add(&signed, DomainEvents::BOUNDS, LocalId::from(0)) + .add(&absolute, DomainEvents::BOUNDS, LocalId::from(1)) + .build(); let inference_code = InferenceCode::new(constraint_tag, AbsoluteValue); - AbsoluteValuePropagator { + let propagator = AbsoluteValuePropagator { signed, absolute, inference_code, - } + }; + + (registration, propagator) } } 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 e7e4669c8..9db0c4ec8 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 @@ -8,6 +8,8 @@ use pumpkin_core::proof::ConstraintTag; use pumpkin_core::proof::InferenceCode; use pumpkin_core::propagation::DomainEvents; use pumpkin_core::propagation::Domains; +use pumpkin_core::propagation::EventRegistration; +use pumpkin_core::propagation::InferenceCheckers; use pumpkin_core::propagation::LocalId; use pumpkin_core::propagation::Priority; use pumpkin_core::propagation::PropagationContext; @@ -36,7 +38,7 @@ where { type PropagatorImpl = BinaryNotEqualsPropagator; - fn create(self, mut context: PropagatorConstructorContext) -> Self::PropagatorImpl { + fn create(self, _: PropagatorConstructorContext) -> (EventRegistration, Self::PropagatorImpl) { let BinaryNotEqualsPropagatorArgs { a, b, @@ -52,15 +54,19 @@ where ); // We only care about the case where one of the two is assigned - context.register(a.clone(), DomainEvents::ASSIGN, LocalId::from(0)); - context.register(b.clone(), DomainEvents::ASSIGN, LocalId::from(1)); + let registration = EventRegistration::builder() + .add(&a, DomainEvents::ASSIGN, LocalId::from(0)) + .add(&b, DomainEvents::ASSIGN, LocalId::from(1)) + .build(); - BinaryNotEqualsPropagator { + let propagator = BinaryNotEqualsPropagator { a, b, inference_code: InferenceCode::new(constraint_tag, BinaryNotEquals), - } + }; + + (registration, propagator) } } diff --git a/pumpkin-crates/propagators/src/propagators/arithmetic/binary_equals/constructor.rs b/pumpkin-crates/propagators/src/propagators/arithmetic/binary_equals/constructor.rs index b144492ed..53810589b 100644 --- a/pumpkin-crates/propagators/src/propagators/arithmetic/binary_equals/constructor.rs +++ b/pumpkin-crates/propagators/src/propagators/arithmetic/binary_equals/constructor.rs @@ -27,7 +27,10 @@ where { type PropagatorImpl = BinaryEqualsPropagator; - fn create(self, mut context: PropagatorConstructorContext) -> Self::PropagatorImpl { + fn create( + self, + mut context: PropagatorConstructorContext, + ) -> (EventRegistration, Self::PropagatorImpl) { let BinaryEqualsPropagatorArgs { a, b, diff --git a/pumpkin-crates/propagators/src/propagators/arithmetic/binary_equals/propagator.rs b/pumpkin-crates/propagators/src/propagators/arithmetic/binary_equals/propagator.rs index 166af6a37..7907034f2 100644 --- a/pumpkin-crates/propagators/src/propagators/arithmetic/binary_equals/propagator.rs +++ b/pumpkin-crates/propagators/src/propagators/arithmetic/binary_equals/propagator.rs @@ -13,6 +13,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::EventRegistration; use pumpkin_core::propagation::ExplanationContext; use pumpkin_core::propagation::LazyExplanation; use pumpkin_core::propagation::LocalId; diff --git a/pumpkin-crates/propagators/src/propagators/arithmetic/integer_division.rs b/pumpkin-crates/propagators/src/propagators/arithmetic/integer_division.rs index a9a22125b..7fbc1a268 100644 --- a/pumpkin-crates/propagators/src/propagators/arithmetic/integer_division.rs +++ b/pumpkin-crates/propagators/src/propagators/arithmetic/integer_division.rs @@ -9,6 +9,8 @@ use pumpkin_core::predicate; use pumpkin_core::proof::ConstraintTag; use pumpkin_core::proof::InferenceCode; use pumpkin_core::propagation::DomainEvents; +use pumpkin_core::propagation::EventRegistration; +use pumpkin_core::propagation::InferenceCheckers; use pumpkin_core::propagation::LocalId; use pumpkin_core::propagation::Priority; use pumpkin_core::propagation::PropagationContext; @@ -42,7 +44,10 @@ where { type PropagatorImpl = DivisionPropagator; - fn create(self, mut context: PropagatorConstructorContext) -> Self::PropagatorImpl { + fn create( + self, + mut context: PropagatorConstructorContext, + ) -> (EventRegistration, Self::PropagatorImpl) { let DivisionArgs { numerator, denominator, @@ -64,18 +69,22 @@ where "Denominator cannot contain 0" ); - context.register(numerator.clone(), DomainEvents::BOUNDS, ID_NUMERATOR); - context.register(denominator.clone(), DomainEvents::BOUNDS, ID_DENOMINATOR); - context.register(rhs.clone(), DomainEvents::BOUNDS, ID_RHS); + let registration = EventRegistration::builder() + .add(&numerator, DomainEvents::BOUNDS, ID_NUMERATOR) + .add(&denominator, DomainEvents::BOUNDS, ID_DENOMINATOR) + .add(&rhs, DomainEvents::BOUNDS, ID_RHS) + .build(); let inference_code = InferenceCode::new(constraint_tag, Division); - DivisionPropagator { + let propagator = DivisionPropagator { numerator, denominator, rhs, inference_code, - } + }; + + (registration, propagator) } } 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 7cde056cf..01caa7c07 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 @@ -13,6 +13,7 @@ use pumpkin_core::proof::InferenceCode; use pumpkin_core::propagation::DomainEvents; use pumpkin_core::propagation::Domains; use pumpkin_core::propagation::EnqueueDecision; +use pumpkin_core::propagation::EventRegistration; use pumpkin_core::propagation::ExplanationContext; use pumpkin_core::propagation::LazyExplanation; use pumpkin_core::propagation::LocalId; @@ -45,7 +46,10 @@ where { type PropagatorImpl = LinearLessOrEqualPropagator; - fn create(self, mut context: PropagatorConstructorContext) -> Self::PropagatorImpl { + fn create( + self, + mut context: PropagatorConstructorContext, + ) -> (EventRegistration, Self::PropagatorImpl) { let LinearLessOrEqualPropagatorArgs { x, c, @@ -60,26 +64,26 @@ where let mut lower_bound_left_hand_side = 0_i64; let mut current_bounds = vec![]; + let mut registration = EventRegistration::builder(); for (i, x_i) in x.iter().enumerate() { - context.register( - x_i.clone(), - DomainEvents::LOWER_BOUND, - LocalId::from(i as u32), - ); + registration = + registration.add(x_i, DomainEvents::LOWER_BOUND, LocalId::from(i as u32)); lower_bound_left_hand_side += context.lower_bound(x_i) as i64; current_bounds.push(context.new_trailed_integer(context.lower_bound(x_i) as i64)); } let lower_bound_left_hand_side = context.new_trailed_integer(lower_bound_left_hand_side); - LinearLessOrEqualPropagator { + let propagator = LinearLessOrEqualPropagator { x, c, lower_bound_left_hand_side, current_bounds: current_bounds.into(), inference_code: InferenceCode::new(constraint_tag, LinearBounds), reason_buffer: Vec::default(), - } + }; + + (registration.build(), propagator) } } 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 2aa6e11d9..525d66155 100644 --- a/pumpkin-crates/propagators/src/propagators/arithmetic/linear_not_equal.rs +++ b/pumpkin-crates/propagators/src/propagators/arithmetic/linear_not_equal.rs @@ -18,6 +18,8 @@ use pumpkin_core::propagation::DomainEvent; use pumpkin_core::propagation::DomainEvents; use pumpkin_core::propagation::Domains; use pumpkin_core::propagation::EnqueueDecision; +use pumpkin_core::propagation::EventRegistration; +use pumpkin_core::propagation::InferenceCheckers; use pumpkin_core::propagation::LocalId; use pumpkin_core::propagation::NotificationContext; use pumpkin_core::propagation::OpaqueDomainEvent; @@ -49,7 +51,10 @@ where { type PropagatorImpl = LinearNotEqualPropagator; - fn create(self, mut context: PropagatorConstructorContext) -> Self::PropagatorImpl { + fn create( + self, + mut context: PropagatorConstructorContext, + ) -> (EventRegistration, Self::PropagatorImpl) { let LinearNotEqualPropagatorArgs { terms, rhs, @@ -64,8 +69,9 @@ where }), ); + let mut registration = EventRegistration::builder(); for (i, x_i) in terms.iter().enumerate() { - context.register(x_i.clone(), DomainEvents::ASSIGN, LocalId::from(i as u32)); + registration = registration.add(x_i, DomainEvents::ASSIGN, LocalId::from(i as u32)); context.register_backtrack( x_i.clone(), DomainEvents::new(enum_set!(DomainEvent::Assign | DomainEvent::Removal)), @@ -85,7 +91,7 @@ where propagator.recalculate_fixed_variables(context.domains()); - propagator + (registration.build(), propagator) } } diff --git a/pumpkin-crates/propagators/src/propagators/arithmetic/maximum/constructor.rs b/pumpkin-crates/propagators/src/propagators/arithmetic/maximum/constructor.rs index 58bb888ec..af7aa61cb 100644 --- a/pumpkin-crates/propagators/src/propagators/arithmetic/maximum/constructor.rs +++ b/pumpkin-crates/propagators/src/propagators/arithmetic/maximum/constructor.rs @@ -20,20 +20,36 @@ pub struct MaximumConstructor { pub constraint_tag: ConstraintTag, } -impl PropagatorConstructor for MaximumConstructor +impl PropagatorConstructor for MaximumArgs where ElementVar: IntegerVariable + 'static, Rhs: IntegerVariable + 'static, { type PropagatorImpl = MaximumPropagator; - fn create(self, mut context: PropagatorConstructorContext) -> Self::PropagatorImpl { - let MaximumConstructor { + fn create( + self, + context: PropagatorConstructorContext, + ) -> (EventRegistration, Self::PropagatorImpl) { + let MaximumArgs { array, rhs, constraint_tag, } = self; + let mut registration = EventRegistration::builder(); + for (idx, var) in array.iter().enumerate() { + registration = registration.add(var, DomainEvents::BOUNDS, LocalId::from(idx as u32)); + } + + registration = registration.add( + &rhs, + DomainEvents::BOUNDS, + LocalId::from(array.len() as u32), + ); + + let inference_code = InferenceCode::new(constraint_tag, Maximum); + let mut scope = Scope::default(); for (idx, var) in array.iter().enumerate() { @@ -65,12 +81,12 @@ where ), ); - let inference_code = InferenceCode::new(constraint_tag, Maximum); - - MaximumPropagator { + let propagator = MaximumPropagator { array, rhs, inference_code, - } + }; + + (registration.build(), propagator) } } diff --git a/pumpkin-crates/propagators/src/propagators/arithmetic/maximum/propagator.rs b/pumpkin-crates/propagators/src/propagators/arithmetic/maximum/propagator.rs index c37b62379..818e38577 100644 --- a/pumpkin-crates/propagators/src/propagators/arithmetic/maximum/propagator.rs +++ b/pumpkin-crates/propagators/src/propagators/arithmetic/maximum/propagator.rs @@ -2,6 +2,10 @@ use pumpkin_core::conjunction; use pumpkin_core::predicate; use pumpkin_core::predicates::PropositionalConjunction; use pumpkin_core::proof::InferenceCode; +use pumpkin_core::propagation::DomainEvents; +use pumpkin_core::propagation::EventRegistration; +use pumpkin_core::propagation::InferenceCheckers; +use pumpkin_core::propagation::LocalId; use pumpkin_core::propagation::Priority; use pumpkin_core::propagation::PropagationContext; use pumpkin_core::propagation::Propagator; @@ -12,6 +16,52 @@ use pumpkin_core::variables::IntegerVariable; #[cfg(doc)] use super::MaximumConstructor; +#[derive(Clone, Debug)] +pub struct MaximumArgs { + pub array: Box<[ElementVar]>, + pub rhs: Rhs, + pub constraint_tag: ConstraintTag, +} + +declare_inference_label!(Maximum); + +impl PropagatorConstructor for MaximumArgs +where + ElementVar: IntegerVariable + 'static, + Rhs: IntegerVariable + 'static, +{ + type PropagatorImpl = MaximumPropagator; + + fn create(self, _: PropagatorConstructorContext) -> (EventRegistration, Self::PropagatorImpl) { + let MaximumArgs { + array, + rhs, + constraint_tag, + } = self; + + let mut registration = EventRegistration::builder(); + for (idx, var) in array.iter().enumerate() { + registration = registration.add(var, DomainEvents::BOUNDS, LocalId::from(idx as u32)); + } + + registration = registration.add( + &rhs, + DomainEvents::BOUNDS, + LocalId::from(array.len() as u32), + ); + + let inference_code = InferenceCode::new(constraint_tag, Maximum); + + let propagator = MaximumPropagator { + array, + rhs, + inference_code, + }; + + (registration.build(), propagator) + } +} + /// Bounds-consistent propagator which enforces `max(array) = rhs`. Can be constructed through /// [`MaximumConstructor`]. #[derive(Clone, Debug)] 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 23dafc85e..dac7ad5d8 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,8 @@ use pumpkin_core::proof::InferenceCode; use pumpkin_core::propagation::DomainEvent; use pumpkin_core::propagation::Domains; use pumpkin_core::propagation::EnqueueDecision; +use pumpkin_core::propagation::EventRegistration; +use pumpkin_core::propagation::InferenceCheckers; use pumpkin_core::propagation::LocalId; use pumpkin_core::propagation::NotificationContext; use pumpkin_core::propagation::OpaqueDomainEvent; @@ -108,10 +110,13 @@ impl PropagatorConstruc { type PropagatorImpl = Self; - fn create(mut self, mut context: PropagatorConstructorContext) -> Self::PropagatorImpl { + fn create( + mut self, + mut context: PropagatorConstructorContext, + ) -> (EventRegistration, Self::PropagatorImpl) { // We only register for notifications of backtrack events if incremental backtracking is // enabled - register_tasks( + let registration = register_tasks( &self.parameters.tasks, context.reborrow(), self.parameters.options.incremental_backtracking, @@ -126,7 +131,7 @@ impl PropagatorConstruc self.inference_code = Some(InferenceCode::new(self.constraint_tag, TimeTable)); - self + (registration, self) } } 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 c20a15ab3..688771d18 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,8 @@ use pumpkin_core::proof::InferenceCode; use pumpkin_core::propagation::DomainEvent; use pumpkin_core::propagation::Domains; use pumpkin_core::propagation::EnqueueDecision; +use pumpkin_core::propagation::EventRegistration; +use pumpkin_core::propagation::InferenceCheckers; use pumpkin_core::propagation::LocalId; use pumpkin_core::propagation::NotificationContext; use pumpkin_core::propagation::OpaqueDomainEvent; @@ -105,8 +107,11 @@ impl Propagator { type PropagatorImpl = Self; - fn create(mut self, mut context: PropagatorConstructorContext) -> Self::PropagatorImpl { - register_tasks(&self.parameters.tasks, context.reborrow(), true); + fn create( + mut self, + mut context: PropagatorConstructorContext, + ) -> (EventRegistration, Self::PropagatorImpl) { + let registration = register_tasks(&self.parameters.tasks, context.reborrow(), true); self.updatable_structures .reset_all_bounds_and_remove_fixed(context.domains(), &self.parameters); register_checkers(&mut context, self.constraint_tag, &self.parameters); @@ -116,7 +121,7 @@ impl Propagator self.inference_code = Some(InferenceCode::new(self.constraint_tag, TimeTable)); - self + (registration, self) } } 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 2cdfb2ba0..e42bb9870 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,8 @@ use pumpkin_core::proof::InferenceCode; use pumpkin_core::propagation::DomainEvent; use pumpkin_core::propagation::Domains; use pumpkin_core::propagation::EnqueueDecision; +use pumpkin_core::propagation::EventRegistration; +use pumpkin_core::propagation::InferenceCheckers; use pumpkin_core::propagation::LocalId; use pumpkin_core::propagation::NotificationContext; use pumpkin_core::propagation::OpaqueDomainEvent; @@ -108,15 +110,17 @@ impl PropagatorConstructor { type PropagatorImpl = Self; - fn create(mut self, mut context: PropagatorConstructorContext) -> Self::PropagatorImpl { + fn create( + mut self, + mut context: PropagatorConstructorContext, + ) -> (EventRegistration, Self::PropagatorImpl) { self.updatable_structures .initialise_bounds_and_remove_fixed(context.domains(), &self.parameters); - register_tasks(&self.parameters.tasks, context.reborrow(), false); - register_checkers(&mut context, self.constraint_tag, &self.parameters); + let registration = register_tasks(&self.parameters.tasks, context.reborrow(), false); self.inference_code = Some(InferenceCode::new(self.constraint_tag, TimeTable)); - self + (registration, self) } } 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 04bbd747d..e2e30e8e2 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 @@ -11,6 +11,8 @@ use pumpkin_core::proof::ConstraintTag; use pumpkin_core::proof::InferenceCode; use pumpkin_core::propagation::DomainEvent; use pumpkin_core::propagation::EnqueueDecision; +use pumpkin_core::propagation::EventRegistration; +use pumpkin_core::propagation::InferenceCheckers; use pumpkin_core::propagation::LocalId; use pumpkin_core::propagation::NotificationContext; use pumpkin_core::propagation::OpaqueDomainEvent; @@ -98,15 +100,17 @@ impl TimeTablePerPointPropagator { impl PropagatorConstructor for TimeTablePerPointPropagator { type PropagatorImpl = Self; - fn create(mut self, mut context: PropagatorConstructorContext) -> Self::PropagatorImpl { + fn create( + mut self, + mut context: PropagatorConstructorContext, + ) -> (EventRegistration, Self::PropagatorImpl) { self.updatable_structures .initialise_bounds_and_remove_fixed(context.domains(), &self.parameters); - register_tasks(&self.parameters.tasks, context.reborrow(), false); - register_checkers(&mut context, self.constraint_tag, &self.parameters); + let registration = register_tasks(&self.parameters.tasks, context.reborrow(), false); self.inference_code = Some(InferenceCode::new(self.constraint_tag, TimeTable)); - self + (registration, self) } } diff --git a/pumpkin-crates/propagators/src/propagators/cumulative/utils/util.rs b/pumpkin-crates/propagators/src/propagators/cumulative/utils/util.rs index 3b1d3f790..063042abf 100644 --- a/pumpkin-crates/propagators/src/propagators/cumulative/utils/util.rs +++ b/pumpkin-crates/propagators/src/propagators/cumulative/utils/util.rs @@ -7,6 +7,7 @@ use enumset::enum_set; use pumpkin_core::propagation::DomainEvent; use pumpkin_core::propagation::DomainEvents; use pumpkin_core::propagation::Domains; +use pumpkin_core::propagation::EventRegistration; use pumpkin_core::propagation::LocalId; use pumpkin_core::propagation::PropagatorConstructorContext; use pumpkin_core::propagation::ReadDomains; @@ -52,15 +53,18 @@ pub(crate) fn register_tasks( tasks: &[Rc>], mut context: PropagatorConstructorContext<'_>, register_backtrack: bool, -) { - tasks.iter().for_each(|task| { - context.register( - task.start_variable.clone(), +) -> EventRegistration { + let mut registration = EventRegistration::builder(); + + for task in tasks.iter() { + registration = registration.add( + &task.start_variable, DomainEvents::new(enum_set!( DomainEvent::LowerBound | DomainEvent::UpperBound | DomainEvent::Assign )), task.id, ); + if register_backtrack { context.register_backtrack( task.start_variable.clone(), @@ -70,7 +74,9 @@ pub(crate) fn register_tasks( task.id, ); } - }); + } + + registration.build() } /// Updates the bounds of the provided [`Task`] to those stored in diff --git a/pumpkin-crates/propagators/src/propagators/disjunctive/disjunctive_propagator.rs b/pumpkin-crates/propagators/src/propagators/disjunctive/disjunctive_propagator.rs index 376712d4b..ee33b7f17 100644 --- a/pumpkin-crates/propagators/src/propagators/disjunctive/disjunctive_propagator.rs +++ b/pumpkin-crates/propagators/src/propagators/disjunctive/disjunctive_propagator.rs @@ -8,6 +8,8 @@ use pumpkin_core::predicates::PropositionalConjunction; use pumpkin_core::proof::ConstraintTag; use pumpkin_core::proof::InferenceCode; use pumpkin_core::propagation::DomainEvents; +use pumpkin_core::propagation::EventRegistration; +use pumpkin_core::propagation::InferenceCheckers; use pumpkin_core::propagation::LocalId; use pumpkin_core::propagation::PropagationContext; use pumpkin_core::propagation::Propagator; @@ -78,21 +80,7 @@ impl DisjunctiveConstructor { impl PropagatorConstructor for DisjunctiveConstructor { type PropagatorImpl = DisjunctivePropagator; - fn create(self, mut context: PropagatorConstructorContext) -> Self::PropagatorImpl { - context.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(), - }), - ); - + fn create(self, _: PropagatorConstructorContext) -> (EventRegistration, Self::PropagatorImpl) { let tasks = self .tasks .into_iter() @@ -107,17 +95,20 @@ impl PropagatorConstructor for DisjunctiveConstr let inference_code = InferenceCode::new(self.constraint_tag, DisjunctiveEdgeFinding); - tasks.iter().for_each(|task| { - context.register(task.start_time.clone(), DomainEvents::BOUNDS, task.id); - }); + let mut registration = EventRegistration::builder(); + for task in tasks.iter() { + registration = registration.add(&task.start_time, DomainEvents::BOUNDS, task.id); + } - DisjunctivePropagator { + let propagator = DisjunctivePropagator { tasks: tasks.clone().into_boxed_slice(), sorted_tasks: tasks, theta_lambda_tree, inference_code, - } + }; + + (registration.build(), propagator) } } diff --git a/pumpkin-crates/propagators/src/propagators/element.rs b/pumpkin-crates/propagators/src/propagators/element.rs index 19139c998..8a9fb4edb 100644 --- a/pumpkin-crates/propagators/src/propagators/element.rs +++ b/pumpkin-crates/propagators/src/propagators/element.rs @@ -17,6 +17,7 @@ use pumpkin_core::predicates::Predicate; use pumpkin_core::proof::ConstraintTag; use pumpkin_core::proof::InferenceCode; use pumpkin_core::propagation::DomainEvents; +use pumpkin_core::propagation::EventRegistration; use pumpkin_core::propagation::ExplanationContext; use pumpkin_core::propagation::LazyExplanation; use pumpkin_core::propagation::LocalId; @@ -48,7 +49,7 @@ where { type PropagatorImpl = ElementPropagator; - fn create(self, mut context: PropagatorConstructorContext) -> Self::PropagatorImpl { + fn create(self, _: PropagatorConstructorContext) -> (EventRegistration, Self::PropagatorImpl) { let ElementArgs { array, index, @@ -65,26 +66,29 @@ where )), ); + let mut registration = EventRegistration::builder(); for (i, x_i) in array.iter().enumerate() { - context.register( - x_i.clone(), + registration = registration.add( + x_i, DomainEvents::ANY_INT, LocalId::from(i as u32 + ID_X_OFFSET), ); } - context.register(index.clone(), DomainEvents::ANY_INT, ID_INDEX); - context.register(rhs.clone(), DomainEvents::ANY_INT, ID_RHS); + registration = registration.add(&index, DomainEvents::ANY_INT, ID_INDEX); + registration = registration.add(&rhs, DomainEvents::ANY_INT, ID_RHS); let inference_code = InferenceCode::new(constraint_tag, Element); - ElementPropagator { + let propagator = ElementPropagator { array, index, rhs, inference_code, rhs_reason_buffer: vec![], - } + }; + + (registration.build(), propagator) } } diff --git a/pumpkin-proof-processor/src/deduction_propagator.rs b/pumpkin-proof-processor/src/deduction_propagator.rs index bba962e49..80c5bc33c 100644 --- a/pumpkin-proof-processor/src/deduction_propagator.rs +++ b/pumpkin-proof-processor/src/deduction_propagator.rs @@ -2,6 +2,7 @@ use pumpkin_core::declare_inference_label; use pumpkin_core::predicates::PropositionalConjunction; use pumpkin_core::proof::ConstraintTag; use pumpkin_core::proof::InferenceCode; +use pumpkin_core::propagation::EventRegistration; use pumpkin_core::propagation::PredicateId; use pumpkin_core::propagation::PropagationContext; use pumpkin_core::propagation::Propagator; @@ -24,7 +25,10 @@ pub(crate) struct DeductionPropagatorConstructor { impl PropagatorConstructor for DeductionPropagatorConstructor { type PropagatorImpl = DeductionPropagator; - fn create(self, mut context: PropagatorConstructorContext) -> Self::PropagatorImpl { + fn create( + self, + mut context: PropagatorConstructorContext, + ) -> (EventRegistration, Self::PropagatorImpl) { declare_inference_label!(Nogood); let DeductionPropagatorConstructor { @@ -37,12 +41,14 @@ impl PropagatorConstructor for DeductionPropagatorConstructor { .map(|&predicate| context.register_predicate(predicate)) .collect(); - DeductionPropagator { + let propagator = DeductionPropagator { nogood, ids, inference_code: InferenceCode::new(constraint_tag, Nogood), active: true, - } + }; + + (EventRegistration::empty(), propagator) } } diff --git a/pumpkin-solver/build.rs b/pumpkin-solver/build.rs index b33637a91..9033cf52b 100644 --- a/pumpkin-solver/build.rs +++ b/pumpkin-solver/build.rs @@ -50,8 +50,8 @@ fn determine_git_hash() { println!("cargo:rustc-env=GIT_SHA={}", git_sha); // Rerun if HEAD changes (new commits, branch switches, etc.) - println!("cargo:rerun-if-changed=.git/HEAD"); - println!("cargo:rerun-if-changed=.git/refs"); + println!("cargo:rerun-if-changed=../.git/HEAD"); + println!("cargo:rerun-if-changed=../.git/refs"); } fn compile_c_binary>( From f2169ee65b980a0a22b89e9081f21b83ad0e30f3 Mon Sep 17 00:00:00 2001 From: Imko Marijnissen Date: Thu, 28 May 2026 13:18:40 +0200 Subject: [PATCH 21/23] refactor: simplify disjunctive propagation checker --- .../src/constraints/disjunctive_strict.rs | 22 +-- .../src/propagators/disjunctive/checker.rs | 184 ++++++++++-------- 2 files changed, 112 insertions(+), 94 deletions(-) diff --git a/pumpkin-crates/constraints/src/constraints/disjunctive_strict.rs b/pumpkin-crates/constraints/src/constraints/disjunctive_strict.rs index c857d3456..468ffc42e 100644 --- a/pumpkin-crates/constraints/src/constraints/disjunctive_strict.rs +++ b/pumpkin-crates/constraints/src/constraints/disjunctive_strict.rs @@ -42,17 +42,17 @@ struct DisjunctiveConstraint { impl Constraint for DisjunctiveConstraint { fn post(self, solver: &mut Solver) -> Result<(), ConstraintOperationError> { // We post both the propagator on the lower-bound and the propagator on the upper-bound. - DisjunctiveConstructor::new(self.tasks.clone(), self.constraint_tag).post(solver)?; - DisjunctiveConstructor::new( - self.tasks.iter().map(|task| ArgDisjunctiveTask { - // The propagations on the upper-bound take place by "reversing" the tasks such - // that instead of going from [EST, LST], the domain goes from [-LCT, -ECT] - start_time: task.start_time.offset(task.processing_time).scaled(-1), - processing_time: task.processing_time, - }), - self.constraint_tag, - ) - .post(solver) + DisjunctiveConstructor::new(self.tasks.clone(), self.constraint_tag).post(solver) + // DisjunctiveConstructor::new( + // self.tasks.iter().map(|task| ArgDisjunctiveTask { + // // The propagations on the upper-bound take place by "reversing" the tasks such + // // that instead of going from [EST, LST], the domain goes from [-LCT, -ECT] + // start_time: task.start_time.offset(task.processing_time).scaled(-1), + // processing_time: task.processing_time, + // }), + // self.constraint_tag, + // ) + // .post(solver) } fn implied_by( diff --git a/pumpkin-crates/propagators/src/propagators/disjunctive/checker.rs b/pumpkin-crates/propagators/src/propagators/disjunctive/checker.rs index f9d18f51f..4d42528b1 100644 --- a/pumpkin-crates/propagators/src/propagators/disjunctive/checker.rs +++ b/pumpkin-crates/propagators/src/propagators/disjunctive/checker.rs @@ -1,5 +1,4 @@ use std::cmp::max; -use std::cmp::min; use std::marker::PhantomData; use pumpkin_checking::AtomicConstraint; @@ -7,6 +6,7 @@ use pumpkin_checking::CheckerVariable; use pumpkin_checking::InferenceChecker; use pumpkin_checking::IntExt; use pumpkin_checking::VariableState; +use pumpkin_core::asserts::pumpkin_assert_simple; use pumpkin_core::containers::KeyedVec; use pumpkin_core::containers::StorageKey; use pumpkin_core::propagation::LocalId; @@ -20,10 +20,75 @@ pub struct DisjunctiveEdgeFindingChecker { pub tasks: Box<[ArgDisjunctiveTask]>, } +/// Performs overload checking on the provided `tasks` and returns true if a conflict could be +/// found. +/// +/// Recall the following: +/// We try to find a set omega of jobs with the following property: `p_omega > lct_omega - +/// est_omega`. +fn overload_checking>( + tasks: &[ArgDisjunctiveTask], + state: &VariableState, +) -> bool { + // First, we create our theta-lambda tree + let mut theta = CheckerThetaLambdaTree::new( + &tasks + .iter() + .enumerate() + .map(|(index, task)| DisjunctiveTask { + start_time: task.start_time.clone(), + processing_time: task.processing_time, + id: LocalId::from(index as u32), + }) + .collect::>(), + ); + // And update it with the current state. + theta.update(state); + + // Next, we sort based on non-decreasing latest completion time. + let mut sorted_tasks = tasks + .iter() + .enumerate() + .filter(|(_, task)| { + task.start_time.induced_lower_bound(&state) != IntExt::NegativeInf + && task.start_time.induced_upper_bound(&state) != IntExt::PositiveInf + }) + .collect::>(); + sorted_tasks.sort_by_key(|(_, task)| { + task.start_time.induced_upper_bound(&state) + task.processing_time + }); + + // Then we go over the tasks which are bounded in the state. + for (index, task) in sorted_tasks { + pumpkin_assert_simple!( + task.start_time.induced_lower_bound(&state) != IntExt::NegativeInf + && task.start_time.induced_upper_bound(&state) != IntExt::PositiveInf + ); + // And we add it to the theta. + theta.add_to_theta( + &DisjunctiveTask { + start_time: task.start_time.clone(), + processing_time: task.processing_time, + id: LocalId::from(index as u32), + }, + &state, + ); + + // If there is an overload of the interval, then we can report that a conflict has been + // found. + if theta.ect() > task.start_time.induced_upper_bound(&state) + task.processing_time { + return true; + } + } + + false +} + impl InferenceChecker for DisjunctiveEdgeFindingChecker where Var: CheckerVariable, Atomic: AtomicConstraint, + ::Identifier: Clone, { fn check( &self, @@ -31,93 +96,46 @@ where _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) != IntExt::NegativeInf - && task.start_time.induced_upper_bound(&state) != IntExt::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 + // We want to detect conflicts, and we split into two cases: + // 1. If it is a conflict explanation then overload checking can be applied directly and + // should lead to a conflict. + // 2. If it is a propagation explanation, then for any value in the domain of the propagated + // variable, scheduling it at that time-point should lead to a conflict (using overload + // checking). + if let Some(consequent) = consequent { + // First we retrieve the propagating task. + let task = self + .tasks + .iter() + .find(|task| task.start_time.does_atomic_constrain_self(consequent)) + .expect("Expected to be able to find atomic"); + + let lb: i32 = 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, - ); + .expect("expected non-infinity value"); + let ub: i32 = task + .start_time + .induced_upper_bound(&state) + .try_into() + .expect("expected non-infinity value"); + + // Then we go over every value in its domain. + for i in lb..=ub { + // We assign the propagating variable to that value. + let mut assigned_state = state.clone(); + let _ = assigned_state.apply(&task.start_time.atomic_equal(i)); + + // If we do not find a conflict using overload checking, then it is not a valid + // explanation. + if !overload_checking(&self.tasks, &assigned_state) { + return false; + } } - - min(est_task, lb_interval) + p + propagating_task.processing_time > ub_interval - && theta_lambda_tree.ect() > propagating_task.start_time.induced_upper_bound(&state) + true } else { - // We simply check whether the interval is overloaded - p > (ub_interval - lb_interval) + overload_checking(&self.tasks, &state) } } } From a5b25485f5ca61b5c1e7729272492194267526f4 Mon Sep 17 00:00:00 2001 From: Imko Marijnissen Date: Thu, 28 May 2026 13:23:04 +0200 Subject: [PATCH 22/23] feat: add consistency checker for disjunctive --- .../disjunctive/disjunctive_propagator.rs | 27 ++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/pumpkin-crates/propagators/src/propagators/disjunctive/disjunctive_propagator.rs b/pumpkin-crates/propagators/src/propagators/disjunctive/disjunctive_propagator.rs index ee33b7f17..c6c96a65d 100644 --- a/pumpkin-crates/propagators/src/propagators/disjunctive/disjunctive_propagator.rs +++ b/pumpkin-crates/propagators/src/propagators/disjunctive/disjunctive_propagator.rs @@ -2,6 +2,9 @@ use std::cmp::Reverse; use std::cmp::min; use pumpkin_core::asserts::pumpkin_assert_simple; +use pumpkin_core::checkers::Scope; +use pumpkin_core::checkers::WeakConsistency; +use pumpkin_core::checkers::WeakRetentionChecker; use pumpkin_core::containers::StorageKey; use pumpkin_core::predicate; use pumpkin_core::predicates::PropositionalConjunction; @@ -80,7 +83,10 @@ impl DisjunctiveConstructor { impl PropagatorConstructor for DisjunctiveConstructor { type PropagatorImpl = DisjunctivePropagator; - fn create(self, _: PropagatorConstructorContext) -> (EventRegistration, Self::PropagatorImpl) { + fn create( + self, + mut context: PropagatorConstructorContext, + ) -> (EventRegistration, Self::PropagatorImpl) { let tasks = self .tasks .into_iter() @@ -95,11 +101,30 @@ impl PropagatorConstructor for DisjunctiveConstr let inference_code = InferenceCode::new(self.constraint_tag, DisjunctiveEdgeFinding); + let mut scope = Scope::default(); let mut registration = EventRegistration::builder(); for task in tasks.iter() { registration = registration.add(&task.start_time, DomainEvents::BOUNDS, task.id); + task.start_time.add_to_scope(&mut scope, task.id); } + context.add_inference_checker( + inference_code, + Box::new(DisjunctiveEdgeFindingChecker { + tasks: self.tasks.clone().into(), + }), + ); + + context.add_consistency_checker( + scope, + Box::new(WeakRetentionChecker::new( + WeakConsistency::Bounds, + DisjunctiveEdgeFindingChecker { + tasks: self.tasks.clone().into(), + }, + )), + ); + let propagator = DisjunctivePropagator { tasks: tasks.clone().into_boxed_slice(), sorted_tasks: tasks, From 2d8f33b92ecd5a659393763c5d9a6c336feea235 Mon Sep 17 00:00:00 2001 From: Imko Marijnissen Date: Thu, 28 May 2026 13:24:28 +0200 Subject: [PATCH 23/23] chore: add back commented out lines --- .../src/constraints/disjunctive_strict.rs | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/pumpkin-crates/constraints/src/constraints/disjunctive_strict.rs b/pumpkin-crates/constraints/src/constraints/disjunctive_strict.rs index 468ffc42e..c857d3456 100644 --- a/pumpkin-crates/constraints/src/constraints/disjunctive_strict.rs +++ b/pumpkin-crates/constraints/src/constraints/disjunctive_strict.rs @@ -42,17 +42,17 @@ struct DisjunctiveConstraint { impl Constraint for DisjunctiveConstraint { fn post(self, solver: &mut Solver) -> Result<(), ConstraintOperationError> { // We post both the propagator on the lower-bound and the propagator on the upper-bound. - DisjunctiveConstructor::new(self.tasks.clone(), self.constraint_tag).post(solver) - // DisjunctiveConstructor::new( - // self.tasks.iter().map(|task| ArgDisjunctiveTask { - // // The propagations on the upper-bound take place by "reversing" the tasks such - // // that instead of going from [EST, LST], the domain goes from [-LCT, -ECT] - // start_time: task.start_time.offset(task.processing_time).scaled(-1), - // processing_time: task.processing_time, - // }), - // self.constraint_tag, - // ) - // .post(solver) + DisjunctiveConstructor::new(self.tasks.clone(), self.constraint_tag).post(solver)?; + DisjunctiveConstructor::new( + self.tasks.iter().map(|task| ArgDisjunctiveTask { + // The propagations on the upper-bound take place by "reversing" the tasks such + // that instead of going from [EST, LST], the domain goes from [-LCT, -ECT] + start_time: task.start_time.offset(task.processing_time).scaled(-1), + processing_time: task.processing_time, + }), + self.constraint_tag, + ) + .post(solver) } fn implied_by(