Skip to content

Commit 94943f5

Browse files
feat(pumpkin-checker): Introduce the uncertified proof checker (#328)
Introduce an uncertified proof checker. This checker can be used to verify DRCP proofs produced by Pumpkin. In this initial version, we support the following constraints: - Integer linear inequality (`int_lin_le`) - Integer linear equality (`int_lin_eq`) - Cumulative (`pumpkin_cumulative`) - All Different (`pumpkin_all_different`) For those constraints the checker implements the following inferences: - Linear bounds for integer linear (in)equalities - Binary equality for an integer equality over two variables - Binary disequality for all different - Hall set inferences for all different - Time table for cumulative - Nogoods
1 parent 884c4ea commit 94943f5

File tree

14 files changed

+1708
-13
lines changed

14 files changed

+1708
-13
lines changed

Cargo.lock

Lines changed: 24 additions & 12 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
[workspace]
2-
members = ["./pumpkin-solver", "./drcp-format", "./pumpkin-solver-py", "./pumpkin-macros", "./drcp-debugger", "./pumpkin-crates/*", "./fzn-rs", "./fzn-rs-derive"]
2+
members = ["./pumpkin-solver", "./pumpkin-checker", "./drcp-format", "./pumpkin-solver-py", "./pumpkin-macros", "./drcp-debugger", "./pumpkin-crates/*", "./fzn-rs", "./fzn-rs-derive"]
33
resolver = "2"
44

55
[workspace.package]

pumpkin-checker/Cargo.toml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
[package]
2+
name = "pumpkin-checker"
3+
version = "0.1.0"
4+
repository.workspace = true
5+
edition.workspace = true
6+
license.workspace = true
7+
authors.workspace = true
8+
9+
[dependencies]
10+
anyhow = "1.0.99"
11+
clap = { version = "4.5.47", features = ["derive"] }
12+
drcp-format = { version = "0.3.0", path = "../drcp-format" }
13+
flate2 = "1.1.2"
14+
fzn-rs = { version = "0.1.0", path = "../fzn-rs" }
15+
thiserror = "2.0.16"
16+
17+
[lints]
18+
workspace = true

pumpkin-checker/src/deductions.rs

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
use std::collections::BTreeMap;
2+
use std::rc::Rc;
3+
4+
use drcp_format::ConstraintId;
5+
use drcp_format::IntAtomic;
6+
7+
use crate::inferences::Fact;
8+
use crate::model::Nogood;
9+
use crate::state::VariableState;
10+
11+
/// An inference that was ignored when checking a deduction.
12+
#[derive(Clone, Debug)]
13+
pub struct IgnoredInference {
14+
/// The ID of the ignored inference.
15+
pub constraint_id: ConstraintId,
16+
17+
/// The premises that were not satisfied when the inference was evaluated.
18+
pub unsatisfied_premises: Vec<IntAtomic<String, i32>>,
19+
}
20+
21+
/// A deduction is rejected by the checker.
22+
#[derive(thiserror::Error, Debug)]
23+
#[error("invalid deduction")]
24+
pub enum InvalidDeduction {
25+
/// The constraint ID of the deduction is already used by an existing constraint.
26+
#[error("constraint id {0} already in use")]
27+
DuplicateConstraintId(ConstraintId),
28+
29+
/// An inference in the deduction sequence does not exist in the proof stage.
30+
#[error("inference {0} does not exist")]
31+
UnknownInference(ConstraintId),
32+
33+
/// The inferences in the proof stage do not derive an empty domain or an explicit
34+
/// conflict.
35+
#[error("no conflict was derived after applying all inferences")]
36+
NoConflict(Vec<IgnoredInference>),
37+
38+
/// The premise contains mutually exclusive atomic constraints.
39+
#[error("the deduction contains inconsistent premises")]
40+
InconsistentPremises,
41+
}
42+
43+
/// Verify that a deduction is valid given the inferences in the proof stage.
44+
pub fn verify_deduction(
45+
deduction: &drcp_format::Deduction<Rc<str>, i32>,
46+
facts_in_proof_stage: &BTreeMap<ConstraintId, Fact>,
47+
) -> Result<Nogood, InvalidDeduction> {
48+
// To verify a deduction, we assume that the premises are true. Then we go over all the
49+
// facts in the sequence, and if all the premises are satisfied, we apply the consequent.
50+
// At some point, this should either reach a fact without a consequent or derive an
51+
// inconsistent domain.
52+
53+
let mut variable_state =
54+
VariableState::prepare_for_conflict_check(&Fact::nogood(deduction.premises.clone()))
55+
.ok_or(InvalidDeduction::InconsistentPremises)?;
56+
57+
let mut unused_inferences = Vec::new();
58+
59+
for constraint_id in deduction.sequence.iter() {
60+
// Get the fact associated with the constraint ID from the sequence.
61+
let fact = facts_in_proof_stage
62+
.get(constraint_id)
63+
.ok_or(InvalidDeduction::UnknownInference(*constraint_id))?;
64+
65+
// Collect all premises that do not evaluate to `true` under the current variable
66+
// state.
67+
let unsatisfied_premises: Vec<IntAtomic<String, i32>> = fact
68+
.premises
69+
.iter()
70+
.filter_map::<IntAtomic<String, i32>, _>(|premise| {
71+
if variable_state.is_true(premise) {
72+
None
73+
} else {
74+
// We need to convert the premise name from a `Rc<str>` to a
75+
// `String`. The former does not implement `Send`, but that is
76+
// required for our error type to be used with anyhow.
77+
Some(IntAtomic {
78+
name: String::from(premise.name.as_ref()),
79+
comparison: premise.comparison,
80+
value: premise.value,
81+
})
82+
}
83+
})
84+
.collect::<Vec<_>>();
85+
86+
// If at least one premise is unassigned, this fact is ignored for the conflict
87+
// check and recorded as unused.
88+
if !unsatisfied_premises.is_empty() {
89+
unused_inferences.push(IgnoredInference {
90+
constraint_id: *constraint_id,
91+
unsatisfied_premises,
92+
});
93+
94+
continue;
95+
}
96+
97+
// At this point the premises are satisfied so we handle the consequent of the
98+
// inference.
99+
match &fact.consequent {
100+
Some(consequent) => {
101+
if !variable_state.apply(consequent) {
102+
// If applying the consequent yields an empty domain for a
103+
// variable, then the deduction is valid.
104+
return Ok(Nogood::from(deduction.premises.clone()));
105+
}
106+
}
107+
// If the consequent is explicitly false, then the deduction is valid.
108+
None => return Ok(Nogood::from(deduction.premises.clone())),
109+
}
110+
}
111+
112+
// Reaching this point means that the conjunction of inferences did not yield to a
113+
// conflict. Therefore the deduction is invalid.
114+
Err(InvalidDeduction::NoConflict(unused_inferences))
115+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
use std::collections::HashSet;
2+
3+
use super::Fact;
4+
use crate::inferences::InvalidInference;
5+
use crate::model::Constraint;
6+
use crate::state::VariableState;
7+
8+
/// Verify an `all_different` inference.
9+
///
10+
/// The checker tests that the premises and the negation of the consequent form a hall-set. If that
11+
/// is the case, the inference is accepted. Otherwise, the inference is rejected.
12+
///
13+
/// The checker will reject inferences with redundant atomic constraints.
14+
pub(crate) fn verify_all_different(
15+
fact: &Fact,
16+
constraint: &Constraint,
17+
) -> Result<(), InvalidInference> {
18+
// This checker takes the union of the domains of the variables in the constraint. If there
19+
// are fewer values in the union of the domain than there are variables, then there is a
20+
// conflict and the inference is valid.
21+
22+
let Constraint::AllDifferent(all_different) = constraint else {
23+
return Err(InvalidInference::ConstraintLabelMismatch);
24+
};
25+
26+
let variable_state = VariableState::prepare_for_conflict_check(fact)
27+
.ok_or(InvalidInference::InconsistentPremises)?;
28+
29+
// Collect all values present in at least one of the domains.
30+
let union_of_domains = all_different
31+
.variables
32+
.iter()
33+
.filter_map(|variable| variable_state.iter_domain(variable))
34+
.flatten()
35+
.collect::<HashSet<_>>();
36+
37+
// Collect the variables mentioned in the fact. Here we ignore variables with a domain
38+
// equal to all integers, as they are not mentioned in the fact. Therefore they do not
39+
// contribute in the hall-set reasoning.
40+
let num_variables = all_different
41+
.variables
42+
.iter()
43+
.filter(|variable| variable_state.iter_domain(variable).is_some())
44+
.count();
45+
46+
if union_of_domains.len() < num_variables {
47+
Ok(())
48+
} else {
49+
Err(InvalidInference::Unsound)
50+
}
51+
}

0 commit comments

Comments
 (0)