Skip to content
Open
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
2e9cba5
Initial implementation of consistency checkers
maartenflippo Feb 27, 2026
aacd712
Run the consistency checkers when enabled
maartenflippo Feb 27, 2026
ece98c7
Implement bound consistency checker
maartenflippo Feb 27, 2026
5013527
Give access to domains in witness generator
maartenflippo Feb 27, 2026
c7a3b9c
Implement checker for absolute value
maartenflippo Feb 27, 2026
5fbad25
Remove redundant min
maartenflippo Feb 27, 2026
fd04f2e
Include the value to support in linear witness generator
maartenflippo Feb 27, 2026
0e7329e
Don't add to supported values of the domain being witnessed
maartenflippo Feb 27, 2026
5c7a9d1
Proper mechanism to add to the scope
maartenflippo Feb 27, 2026
8b143be
Update todo notes
maartenflippo Feb 27, 2026
4c5eda3
Implement Witness generator for multiplication
maartenflippo Mar 2, 2026
348779d
Transform consistency checker interface.
maartenflippo Mar 3, 2026
5fd33cb
Implement new interface
maartenflippo Mar 3, 2026
0c5afe2
Implement weak consistency checkers
maartenflippo Mar 3, 2026
9a528c3
Remove the dedicated concept of Scope
maartenflippo Mar 3, 2026
43a80a2
Proper witness generator for linear
maartenflippo Mar 3, 2026
7b4bab9
Add documentation to strong consistency checker
maartenflippo Mar 4, 2026
6998a47
Proper witness generator for multiplication bound consistency
maartenflippo Mar 4, 2026
7ec48e0
Remove unused import
maartenflippo Mar 4, 2026
c295d0c
Implement witness generator for maximum
maartenflippo Mar 5, 2026
360c961
No consistency checking for multiplication
maartenflippo Mar 5, 2026
d545f47
Add circuit and witness generator for element
maartenflippo Mar 5, 2026
7088788
Fixup element propagator
maartenflippo Mar 5, 2026
380e2e0
Disable circuit due to consistency bugs
maartenflippo Mar 5, 2026
b4eeb11
Separate flags for consistency and inference checking
maartenflippo Mar 6, 2026
91ef2ea
Implement consistency checking for reified
maartenflippo Mar 9, 2026
1615dee
Filter checkers and fix reified checker
maartenflippo Mar 10, 2026
96c6ae5
Fix propagator checker filter cli
maartenflippo Mar 10, 2026
09eec3d
Enable filtering of checker types
maartenflippo Mar 10, 2026
d5ac88c
Add circuit propagator
maartenflippo Mar 12, 2026
c47ab88
Get the final propagators in
maartenflippo Mar 12, 2026
dca7515
More bug fixes
maartenflippo Mar 13, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions pumpkin-checker/src/inferences/linear.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use pumpkin_checking::InferenceChecker;
use pumpkin_checking::VariableState;
use pumpkin_propagators::arithmetic::LinearLessOrEqualInferenceChecker;
use pumpkin_propagators::arithmetic::LinearLessOrEqualChecker;

use crate::inferences::Fact;
use crate::inferences::InvalidInference;
Expand Down Expand Up @@ -52,7 +52,7 @@ fn verify_linear_inference(
fact: &Fact,
state: VariableState<Atomic>,
) -> Result<(), InvalidInference> {
let checker = LinearLessOrEqualInferenceChecker::new(linear.terms.clone().into(), linear.bound);
let checker = LinearLessOrEqualChecker::new(linear.terms.clone().into(), linear.bound);

if checker.check(state, &fact.premises, fact.consequent.as_ref()) {
Ok(())
Expand Down
19 changes: 19 additions & 0 deletions pumpkin-crates/core/src/engine/notifications/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ use enumset::EnumSet;
pub(crate) use predicate_notification::PredicateNotifier;

use crate::basic_types::PredicateId;
#[cfg(feature = "check-propagations")]
use crate::containers::HashSet;
use crate::containers::KeyedVec;
use crate::engine::Assignments;
use crate::engine::PropagatorQueue;
Expand Down Expand Up @@ -43,6 +45,10 @@ pub(crate) struct NotificationEngine {
/// Backtrack events which have occurred since the last of backtrack notifications have taken
/// place
backtrack_events: EventSink,
/// All the propagators that have been notified since the last call to
/// [`Self::drain_notified_propagators`].
#[cfg(feature = "check-propagations")]
notified_propagators: HashSet<PropagatorId>,
}

impl Default for NotificationEngine {
Expand All @@ -54,6 +60,8 @@ impl Default for NotificationEngine {
last_notified_trail_index: 0,
events: Default::default(),
backtrack_events: Default::default(),
#[cfg(feature = "check-propagations")]
notified_propagators: Default::default(),
};
// Grow for the dummy predicate
result.grow();
Expand All @@ -77,6 +85,8 @@ impl NotificationEngine {
last_notified_trail_index: usize::MAX,
events: Default::default(),
backtrack_events: Default::default(),
#[cfg(feature = "check-propagations")]
notified_propagators: Default::default(),
};
// Grow for the dummy predicate
result.grow();
Expand Down Expand Up @@ -333,6 +343,9 @@ impl NotificationEngine {
assignments,
trailed_values,
);

#[cfg(feature = "check-propagations")]
let _ = self.notified_propagators.insert(propagator_id);
}
}

Expand Down Expand Up @@ -579,4 +592,10 @@ impl NotificationEngine {
.predicate_id_assignments
.synchronise(backtrack_level)
}

/// Get all propagagators that have been notified since the previous call to this function.
#[cfg(feature = "check-propagations")]
pub(crate) fn drain_notified_propagators(&mut self) -> impl Iterator<Item = PropagatorId> {
self.notified_propagators.drain()
}
}
48 changes: 47 additions & 1 deletion pumpkin-crates/core/src/engine/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ use crate::propagation::Propagator;
use crate::propagation::PropagatorConstructor;
use crate::propagation::PropagatorConstructorContext;
use crate::propagation::PropagatorId;
#[cfg(feature = "check-propagations")]
use crate::propagation::checkers::BoxedConsistencyChecker;
#[cfg(feature = "check-propagations")]
use crate::propagation::checkers::Scope;
use crate::propagation::store::PropagatorStore;
use crate::pumpkin_assert_advanced;
use crate::pumpkin_assert_eq_simple;
Expand Down Expand Up @@ -83,6 +87,11 @@ pub struct State {

/// Inference checkers to run in the propagation loop.
checkers: HashMap<InferenceCode, Vec<BoxedChecker<Predicate>>>,
/// For every propagator identify what variables are associated with it.
#[cfg(feature = "check-propagations")]
pub(crate) scopes: HashMap<PropagatorId, Scope>,
#[cfg(feature = "check-propagations")]
consistency_checkers: HashMap<PropagatorId, BoxedConsistencyChecker>,
}

create_statistics_struct!(StateStatistics {
Expand Down Expand Up @@ -177,6 +186,10 @@ impl Default for State {
statistics: StateStatistics::default(),
constraint_tags: KeyGenerator::default(),
checkers: HashMap::default(),
#[cfg(feature = "check-propagations")]
scopes: HashMap::default(),
#[cfg(feature = "check-propagations")]
consistency_checkers: HashMap::default(),
};
// As a convention, the assignments contain a dummy domain_id=0, which represents a 0-1
// variable that is assigned to one. We use it to represent predicates that are
Expand Down Expand Up @@ -389,7 +402,7 @@ impl State {
Constructor::PropagatorImpl: 'static,
{
#[cfg(feature = "check-propagations")]
constructor.add_inference_checkers(InferenceCheckers::new(self));
let consistency_checker = constructor.add_inference_checkers(InferenceCheckers::new(self));

let original_handle: PropagatorHandle<Constructor::PropagatorImpl> =
self.propagators.new_propagator().key();
Expand All @@ -408,6 +421,23 @@ impl State {
let slot = self.propagators.new_propagator();
let handle = slot.populate(propagator);

#[cfg(feature = "check-propagations")]
{
use crate::propagation::checkers::ConsistencyChecker;

#[allow(trivial_casts, reason = "removing it causes a compiler error")]
let previous_checker = self.consistency_checkers.insert(
handle.propagator_id(),
BoxedConsistencyChecker::from(
Box::new(consistency_checker) as Box<dyn ConsistencyChecker>
),
);
assert!(
previous_checker.is_none(),
"somehow adding multiple consistency checkers to the same propagator"
);
}
Comment thread
ImkoMarijnissen marked this conversation as resolved.
Outdated

pumpkin_assert_eq_simple!(handle.propagator_id(), original_handle.propagator_id());

#[allow(deprecated, reason = "Will be refactored")]
Expand Down Expand Up @@ -790,6 +820,22 @@ impl State {
self.propagate(propagator_id)?;
}

#[cfg(feature = "check-propagations")]
for propagator in self.notification_engine.drain_notified_propagators() {
let checker = &self.consistency_checkers[&propagator];
let scope = &self.scopes[&propagator];

let propagator_name = self.propagators[propagator].name();

assert!(
checker.check_consistency(
Domains::new(&self.assignments, &mut self.trailed_values),
scope,
),
"propagator {propagator_name} is not at its reported consistency"
);
}

// 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(
Expand Down
23 changes: 23 additions & 0 deletions pumpkin-crates/core/src/engine/variables/affine_view.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@ 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;
use crate::propagation::checkers::ScopeBuilder;
use crate::propagation::checkers::SingleVariableAssignment;
use crate::propagation::checkers::ValueToWitness;
use crate::propagation::checkers::WitnessedVariable;

/// Models the constraint `y = ax + b`, by expressing the domain of `y` as a transformation of the
/// domain of `x`.
Expand Down Expand Up @@ -406,6 +411,24 @@ enum Rounding {
Down,
}

impl<Inner: WitnessedVariable> WitnessedVariable for AffineView<Inner> {
fn add_to_scope(&self, scope: &mut ScopeBuilder, local_id: LocalId) {
self.inner.add_to_scope(scope, local_id);
}

fn unpack_value(&self, value: ValueToWitness) -> i32 {
let inner_value = self.inner.unpack_value(value);

self.map(inner_value)
}

fn assign(&self, value: i32) -> SingleVariableAssignment {
assert_eq!((value - self.offset) % self.scale, 0);
let inner_assignment = (value - self.offset) / self.scale;
Comment thread
ImkoMarijnissen marked this conversation as resolved.
self.inner.assign(inner_assignment)
}
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down
19 changes: 19 additions & 0 deletions pumpkin-crates/core/src/engine/variables/domain_id.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@ use crate::engine::notifications::Watchers;
use crate::engine::variables::AffineView;
use crate::engine::variables::IntegerVariable;
use crate::predicates::Predicate;
use crate::propagation::LocalId;
use crate::propagation::checkers::ScopeBuilder;
use crate::propagation::checkers::SingleVariableAssignment;
use crate::propagation::checkers::ValueToWitness;
use crate::propagation::checkers::WitnessedVariable;
use crate::pumpkin_assert_simple;

/// A structure which represents the most basic [`IntegerVariable`]; it is simply the id which links
Expand Down Expand Up @@ -191,6 +196,20 @@ impl TransformableVariable<AffineView<DomainId>> for DomainId {
}
}

impl WitnessedVariable for DomainId {
fn add_to_scope(&self, scope: &mut ScopeBuilder, local_id: LocalId) {
scope.add(local_id, *self);
}

fn unpack_value(&self, value: ValueToWitness) -> i32 {
value.unpack()
}

fn assign(&self, value: i32) -> SingleVariableAssignment {
SingleVariableAssignment::new(*self, value)
}
}

impl StorageKey for DomainId {
fn index(&self) -> usize {
self.id as usize
Expand Down
2 changes: 2 additions & 0 deletions pumpkin-crates/core/src/engine/variables/integer_variable.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,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::checkers::WitnessedVariable;

/// A trait specifying the required behaviour of an integer variable such as retrieving a
/// lower-bound ([`IntegerVariable::lower_bound`]).
Expand All @@ -19,6 +20,7 @@ pub trait IntegerVariable:
+ TransformableVariable<Self::AffineView>
+ Debug
+ CheckerVariable<Predicate>
+ WitnessedVariable
{
type AffineView: IntegerVariable;

Expand Down
18 changes: 18 additions & 0 deletions pumpkin-crates/core/src/engine/variables/literal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ 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;
use crate::propagation::checkers::ScopeBuilder;
use crate::propagation::checkers::ValueToWitness;
use crate::propagation::checkers::WitnessedVariable;

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct Literal {
Expand Down Expand Up @@ -222,3 +226,17 @@ impl TransformableVariable<AffineView<Literal>> for Literal {
AffineView::new(*self, 1, offset)
}
}

impl WitnessedVariable for Literal {
fn add_to_scope(&self, scope: &mut ScopeBuilder, local_id: LocalId) {
self.integer_variable.add_to_scope(scope, local_id);
}

fn unpack_value(&self, value: ValueToWitness) -> i32 {
self.integer_variable.unpack_value(value)
}

fn assign(&self, value: i32) -> crate::propagation::checkers::SingleVariableAssignment {
self.integer_variable.assign(value)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
use pumpkin_checking::InferenceChecker;

use crate::predicates::Predicate;
use crate::propagation::Domains;
use crate::propagation::checkers::ConsistencyChecker;
use crate::propagation::checkers::Scope;
use crate::propagation::checkers::WitnessGenerator;

#[derive(Clone, Debug)]
pub struct BoundConsistencyChecker<C> {
witness_generator: C,
}

impl<C> BoundConsistencyChecker<C> {
pub fn new(witness_generator: C) -> Self {
BoundConsistencyChecker { witness_generator }
}
}

impl<C> ConsistencyChecker for BoundConsistencyChecker<C>
where
C: WitnessGenerator + InferenceChecker<Predicate> + Clone,
{
fn check_consistency(&self, domains: Domains<'_>, scope: &Scope) -> bool {
super::assert_consistency(
domains,
scope,
&self.witness_generator,
super::Consistency::Bounds,
);

true
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
use std::fmt::Debug;

use dyn_clone::DynClone;

use crate::propagation::Domains;
use crate::propagation::checkers::Scope;

/// A consistency checker ensures that a propagator is at a certain level of consistency.
///
/// Given a current set of domains and a scope, the checker identifies whether the desired
/// consistency level is reached.
pub trait ConsistencyChecker: Debug + DynClone {
/// Returns `true` if the variables in `scope` are at the required consistency in
/// `domains`.
fn check_consistency(&self, domains: Domains<'_>, scope: &Scope) -> bool;
}

/// Wrapper around `Box<dyn ConsistencyChecker>` that implements [`Clone`].
#[derive(Debug)]
pub struct BoxedConsistencyChecker(Box<dyn ConsistencyChecker>);

impl Clone for BoxedConsistencyChecker {
fn clone(&self) -> Self {
BoxedConsistencyChecker(dyn_clone::clone_box(&*self.0))
}
}

impl From<Box<dyn ConsistencyChecker>> for BoxedConsistencyChecker {
fn from(value: Box<dyn ConsistencyChecker>) -> Self {
BoxedConsistencyChecker(value)
}
}

impl BoxedConsistencyChecker {
/// See [`ConsistencyChecker::check_consistency`].
pub fn check_consistency(&self, domains: Domains<'_>, scope: &Scope) -> bool {
self.0.check_consistency(domains, scope)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
use pumpkin_checking::InferenceChecker;

use crate::predicates::Predicate;
use crate::propagation::Domains;
use crate::propagation::checkers::ConsistencyChecker;
use crate::propagation::checkers::Scope;
use crate::propagation::checkers::WitnessGenerator;

#[derive(Clone, Debug)]
pub struct DomainConsistencyChecker<C> {
witness_generator: C,
}

impl<C> DomainConsistencyChecker<C> {
pub fn new(witness_generator: C) -> Self {
DomainConsistencyChecker { witness_generator }
}
}

impl<C> ConsistencyChecker for DomainConsistencyChecker<C>
where
C: WitnessGenerator + InferenceChecker<Predicate> + Clone,
{
fn check_consistency(&self, domains: Domains<'_>, scope: &Scope) -> bool {
super::assert_consistency(
domains,
scope,
&self.witness_generator,
super::Consistency::Domain,
);

true
}
}
Loading