use std::{
cell::RefCell,
cmp::{max, min, Ordering},
collections::VecDeque,
fmt::{self, Display},
iter::once,
ops::{Add, AddAssign, Deref, DerefMut, Mul, MulAssign, Neg, Range, Sub, SubAssign},
rc::Rc,
};
use itertools::Itertools;
use rustc_hash::{FxBuildHasher, FxHashMap};
use crate::{
cardinality::Cardinality,
cardinality_one::{BitwiseEncoder, CardinalityOne},
helpers::{as_binary, is_powers_of_two, new_named_lit},
integer::{
lex_leq_const, Consistency, IntVar, IntVarEnc, IntVarOrd, Lin, Model, GROUND_BINARY_AT_LB,
},
propositional_logic::{Formula, TseitinEncoder},
sorted::{Sorted, SortedEncoder},
BoolVal, Checker, ClauseDatabase, ClauseDatabaseTools, Coeff, Encoder, IntEncoding, Lit,
Result, Unsatisfiable, Valuation, Var,
};
#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)]
pub struct AdderEncoder {}
#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)]
pub struct BddEncoder {
add_consistency: bool,
cutoff: Option<Coeff>,
}
#[derive(Debug, Clone, PartialEq)]
enum BddNode {
Val,
Gap,
View(Coeff),
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct BoolLinAggregator {
sorted_encoder: SortedEncoder,
sort_same_coefficients: usize,
}
#[derive(Clone, Debug)]
pub struct BoolLinExp {
terms: VecDeque<(Lit, Coeff)>,
num_free: usize,
constraints: Vec<(Constraint, usize)>,
add: Coeff,
mult: Coeff,
}
#[derive(Debug)]
pub enum BoolLinVariant {
Linear(NormalizedBoolLinear),
Cardinality(Cardinality),
CardinalityOne(CardinalityOne),
Trivial,
}
#[derive(Debug, Clone)]
pub struct BoolLinear {
exp: BoolLinExp,
cmp: Comparator,
k: Coeff,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum Comparator {
LessEq,
Equal,
GreaterEq,
}
#[derive(Debug, Clone)]
pub(crate) enum Constraint {
AtMostOne,
ImplicationChain,
Domain { lb: Coeff, ub: Coeff },
}
#[derive(Clone, Debug, Eq, PartialEq, Hash)]
pub(crate) enum LimitComp {
Equal,
LessEq,
}
pub(crate) trait LinMarker {}
#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)]
pub struct LinearEncoder<Enc = StaticLinEncoder, Agg = BoolLinAggregator> {
enc: Enc,
agg: Agg,
}
#[derive(Debug, Clone)]
pub struct NormalizedBoolLinear {
terms: Vec<Part>,
cmp: LimitComp,
k: PosCoeff,
}
#[derive(Debug, Clone)]
pub(crate) enum Part {
Amo(Vec<(Lit, PosCoeff)>),
Ic(Vec<(Lit, PosCoeff)>),
Dom(Vec<(Lit, PosCoeff)>, PosCoeff, PosCoeff),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub(crate) struct PosCoeff(Coeff);
#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)]
pub struct StaticLinEncoder<
LinEnc = AdderEncoder,
CardEnc = AdderEncoder, Card1Enc = BitwiseEncoder,
> {
lin_enc: LinEnc,
card_enc: CardEnc,
amo_enc: Card1Enc,
}
#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)]
pub struct SwcEncoder {
add_consistency: bool,
add_propagation: Consistency,
cutoff: Option<Coeff>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Hash)]
pub struct TotalizerEncoder {
add_consistency: bool,
add_propagation: Consistency,
cutoff: Option<Coeff>,
}
impl AdderEncoder {
#[cfg_attr(any(feature = "tracing", test), tracing::instrument(name = "carry_circuit", skip_all, fields(constraint = Self::trace_print_carry(input, &output))))]
fn carry_circuit<Db>(db: &mut Db, input: &[Lit], output: BoolVal) -> Result
where
Db: ClauseDatabase + ?Sized,
{
match output {
BoolVal::Lit(carry) => match *input {
[a, b] => {
db.add_clause([!a, !b, carry])?;
db.add_clause([a, !carry])?;
db.add_clause([b, !carry])
}
[a, b, c] => {
db.add_clause([a, b, !carry])?;
db.add_clause([a, c, !carry])?;
db.add_clause([b, c, !carry])?;
db.add_clause([!a, !b, carry])?;
db.add_clause([!a, !c, carry])?;
db.add_clause([!b, !c, carry])
}
_ => unreachable!(),
},
BoolVal::Const(k) => match *input {
[a, b] => {
if k {
db.add_clause([a])?;
db.add_clause([b])
} else {
db.add_clause([!a, !b])
}
}
[a, b, c] => {
let neg = |x: Lit| if k { x } else { !x };
db.add_clause([neg(a), neg(b)])?;
db.add_clause([neg(a), neg(c)])?;
db.add_clause([neg(b), neg(c)])
}
_ => unreachable!(),
},
}
}
#[cfg_attr(any(feature = "tracing", test), tracing::instrument(name = "sum_circuit", skip_all, fields(constraint = Self::trace_print_sum(input, &output))))]
fn sum_circuit<Db>(db: &mut Db, input: &[Lit], output: BoolVal) -> Result
where
Db: ClauseDatabase + ?Sized,
{
match output {
BoolVal::Lit(sum) => match *input {
[a, b] => {
db.add_clause([!a, !b, !sum])?;
db.add_clause([!a, b, sum])?;
db.add_clause([a, !b, sum])?;
db.add_clause([a, b, !sum])
}
[a, b, c] => {
db.add_clause([a, b, c, !sum])?;
db.add_clause([a, !b, !c, !sum])?;
db.add_clause([!a, b, !c, !sum])?;
db.add_clause([!a, !b, c, !sum])?;
db.add_clause([!a, !b, !c, sum])?;
db.add_clause([!a, b, c, sum])?;
db.add_clause([a, !b, c, sum])?;
db.add_clause([a, b, !c, sum])
}
_ => unreachable!(),
},
BoolVal::Const(true) => {
let xor = Formula::Xor(input.iter().map(|&l| Formula::Atom(l)).collect_vec());
TseitinEncoder.encode(db, &xor)
}
BoolVal::Const(false) => match *input {
[a, b] => {
db.add_clause([a, !b])?;
db.add_clause([!a, b])
}
[a, b, c] => {
db.add_clause([!a, !b, !c])?;
db.add_clause([!a, b, c])?;
db.add_clause([a, !b, c])?;
db.add_clause([a, b, !c])
}
_ => unreachable!(),
},
}
}
#[cfg(any(feature = "tracing", test))]
fn trace_print_carry(input: &[Lit], output: &BoolVal) -> String {
use crate::trace::trace_print_lit;
let inner = itertools::join(input.iter().map(trace_print_lit), " + ");
match output {
BoolVal::Lit(r) => format!("{} ≡ ({} > 1)", trace_print_lit(r), inner),
BoolVal::Const(true) => format!("{inner} > 1"),
BoolVal::Const(false) => format!("{inner} ≤ 1"),
}
}
#[cfg(any(feature = "tracing", test))]
fn trace_print_sum(input: &[Lit], output: &BoolVal) -> String {
use crate::trace::trace_print_lit;
let inner = itertools::join(input.iter().map(trace_print_lit), " ⊻ ");
match output {
BoolVal::Lit(r) => format!("{} ≡ {}", trace_print_lit(r), inner),
BoolVal::Const(true) => inner,
BoolVal::Const(false) => format!("¬({inner})"),
}
}
}
impl<Db> Encoder<Db, NormalizedBoolLinear> for AdderEncoder
where
Db: ClauseDatabase + ?Sized,
{
#[cfg_attr(
any(feature = "tracing", test),
tracing::instrument(name = "adder_encoder", skip_all, fields(constraint = lin.trace_print()))
)]
fn encode(&self, db: &mut Db, lin: &NormalizedBoolLinear) -> Result {
debug_assert!(lin.cmp == LimitComp::LessEq || lin.cmp == LimitComp::Equal);
const ZERO: Coeff = 0;
let bits = ZERO.leading_zeros() - lin.k.leading_zeros();
let mut k = as_binary(lin.k, Some(bits));
let first_zero = lin.k.trailing_ones() as usize;
let bits = bits as usize;
debug_assert!(k[bits - 1]);
let all_terms = || {
lin.terms
.iter()
.flat_map(|part| part.iter().map(|&(lit, coef)| (lit, coef)))
};
let mut bucket = vec![Vec::new(); bits];
for (i, bucket) in bucket.iter_mut().enumerate().take(bits) {
for (lit, coef) in all_terms() {
if *coef & (1 << i) != 0 {
bucket.push(lit);
}
}
}
let mut sum = vec![None; bits];
for b in 0..bits {
match bucket[b].len() {
0 => {
if k[b] && lin.cmp == LimitComp::Equal {
return db.contradiction();
}
}
1 => {
let x = bucket[b].pop().unwrap();
if lin.cmp == LimitComp::Equal {
db.add_clause([if k[b] { x } else { !x }])?;
} else {
sum[b] = Some(x);
}
}
_ => {
while bucket[b].len() >= 2 {
let last = bucket[b].len() <= 3;
let lits = if last {
bucket[b].split_off(0)
} else {
let i = bucket[b].len() - 3;
bucket[b].split_off(i)
};
debug_assert!(lits.len() == 3 || lits.len() == 2);
if last && lin.cmp == LimitComp::Equal {
Self::sum_circuit(db, lits.as_slice(), BoolVal::Const(k[b]))?;
} else if lin.cmp != LimitComp::LessEq || !last || b >= first_zero {
let sum = new_named_lit!(
db,
if last {
crate::trace::subscripted_name("∑", b)
} else {
crate::trace::subscripted_name(
&format!("iS{b}"),
(bucket[b].len() / 3) + 1,
)
}
);
Self::sum_circuit(db, lits.as_slice(), BoolVal::Lit(sum))?;
bucket[b].push(sum);
}
if b + 1 >= bits {
if lits.len() == 2 && lin.cmp == LimitComp::Equal {
} else {
Self::carry_circuit(db, &lits[..], BoolVal::Const(false))?;
}
} else if last && lin.cmp == LimitComp::Equal && bucket[b + 1].is_empty() {
Self::carry_circuit(db, &lits[..], BoolVal::Const(k[b + 1]))?;
k[b + 1] = false;
} else {
let carry = new_named_lit!(
db,
if last {
crate::trace::subscripted_name("c", b)
} else {
crate::trace::subscripted_name(
&format!("iC{b}"),
(bucket[b].len() / 3) + 1,
)
}
);
Self::carry_circuit(db, lits.as_slice(), BoolVal::Lit(carry))?;
bucket[b + 1].push(carry);
}
}
debug_assert!(
(lin.cmp == LimitComp::Equal && bucket[b].is_empty())
|| (lin.cmp == LimitComp::LessEq
&& (bucket[b].len() == 1 || b < first_zero))
);
sum[b] = bucket[b].pop();
}
}
}
debug_assert!(lin.cmp != LimitComp::Equal || sum.iter().all(|x| x.is_none()));
if lin.cmp == LimitComp::LessEq {
lex_leq_const(db, sum.as_slice(), lin.k, bits)?;
}
Ok(())
}
}
impl LinMarker for AdderEncoder {}
impl BddEncoder {
fn bdd(
i: usize,
xs: &Vec<IntVarEnc>,
sum: Coeff,
ws: &mut Vec<Vec<(Range<Coeff>, BddNode)>>,
) -> (Range<Coeff>, BddNode) {
if let Ok(pos) = ws[i].binary_search_by(|(r, _)| {
if r.contains(&sum) {
Ordering::Equal
} else if r.end <= sum {
Ordering::Less
} else {
Ordering::Greater
}
}) {
return ws[i][pos].clone();
}
let views = xs[i]
.dom()
.iter()
.flatten()
.map(|v| (v, Self::bdd(i + 1, xs, sum + v, ws)))
.collect_vec();
let is_gap = views.iter().all(|(_, (_, v))| v == &BddNode::Gap);
let view = (views.iter().map(|(_, (iv, _))| iv).all_equal())
.then(|| views.first().unwrap().1 .0.end - 1);
let interval = views
.into_iter()
.map(|(v, (interval, _))| (interval.start - v)..(interval.end - v))
.reduce(|a, b| max(a.start, b.start)..min(a.end, b.end))
.unwrap();
let node = if is_gap {
BddNode::Gap
} else if let Some(view) = view {
BddNode::View(view)
} else {
BddNode::Val
};
let pos = match ws[i].binary_search_by_key(&interval.start, |(r, _)| r.start) {
Ok(i) | Err(i) => i,
};
ws[i].insert(pos, (interval.clone(), node.clone()));
debug_assert!(
pos == 0 || ws[i][pos - 1].0.end <= ws[i][pos].0.start,
"Overlapping interval {interval:?} (overlapping with {:?}) inserted into {:?}",
ws[i][pos - 1].0,
ws[i]
);
debug_assert!(
pos + 1 == ws[i].len() || ws[i][pos].0.end <= ws[i][pos + 1].0.start,
"Overlapping interval {interval:?} (overlapping with {:?}) inserted into {:?}",
ws[i][pos + 1].1,
ws[i]
);
(interval, node)
}
fn construct_bdd(
xs: &Vec<IntVarEnc>,
cmp: &LimitComp,
k: PosCoeff,
) -> Vec<Vec<(Range<Coeff>, BddNode)>> {
let k = *k;
let bounds = xs
.iter()
.scan((0, 0), |state, x| {
*state = (state.0 + x.lb(), state.1 + x.ub());
Some(*state)
})
.chain(once((0, k)))
.collect_vec();
let margins = xs
.iter()
.rev()
.scan((k, k), |state, x| {
*state = (state.0 - x.ub(), state.1 - x.lb());
Some(*state)
})
.collect_vec();
let inf = xs.iter().fold(0, |a, x| a + x.ub()) + 1;
let mut ws: Vec<Vec<(Range<Coeff>, BddNode)>> = margins
.into_iter()
.rev()
.chain(once((k, k)))
.zip(bounds)
.map(|((lb_margin, ub_margin), (lb, ub))| {
match cmp {
LimitComp::LessEq => vec![
(lb_margin > lb).then_some((0..(lb_margin + 1), BddNode::Val)),
(ub_margin <= ub).then_some(((ub_margin + 1)..inf, BddNode::Gap)),
],
LimitComp::Equal => vec![
(lb_margin > lb).then_some((0..lb_margin, BddNode::Gap)),
(lb_margin == ub_margin).then_some((k..(k + 1), BddNode::Val)),
(ub_margin <= ub).then_some(((ub_margin + 1)..inf, BddNode::Gap)),
],
}
.into_iter()
.flatten()
.collect()
})
.collect();
debug_assert!(
ws.iter().all(|layer| layer
.iter()
.tuple_windows()
.all(|((a, _), (b, _))| a.end <= b.end)),
"layers must be sorted and non-overlapping"
);
let _ = Self::bdd(0, xs, 0, &mut ws);
ws
}
pub fn with_consistency(&mut self, b: bool) -> &mut Self {
self.add_consistency = b;
self
}
pub fn with_cutoff(&mut self, c: Option<Coeff>) -> &mut Self {
self.cutoff = c;
self
}
}
impl<Db> Encoder<Db, NormalizedBoolLinear> for BddEncoder
where
Db: ClauseDatabase + ?Sized,
{
#[cfg_attr(
any(feature = "tracing", test),
tracing::instrument(name = "bdd_encoder", skip_all, fields(constraint = lin.trace_print()))
)]
fn encode(&self, db: &mut Db, lin: &NormalizedBoolLinear) -> Result {
let xs = lin
.terms
.iter()
.enumerate()
.flat_map(|(i, part)| IntVarEnc::from_part(db, part, lin.k, format!("x_{i}")))
.sorted_by(|a: &IntVarEnc, b: &IntVarEnc| b.ub().cmp(&a.ub())) .collect_vec();
let mut model = Model::default();
let ys = Self::construct_bdd(&xs, &lin.cmp, lin.k);
let xs = xs
.into_iter()
.map(|x| Rc::new(RefCell::new(model.add_int_var_enc(x))))
.collect_vec();
let ys: Vec<_> = ys
.into_iter()
.map(|nodes| -> Result<_, Unsatisfiable> {
let mut views = FxHashMap::default();
Ok(Rc::new(RefCell::new({
let mut y = model.new_var(
nodes
.into_iter()
.filter_map(|(iv, node)| match node {
BddNode::Gap => None,
BddNode::Val => Some(iv.end - 1),
BddNode::View(view) => {
let val = iv.end - 1;
let _ = views.insert(val, view);
Some(val)
}
})
.map(|v| v..=v)
.collect(),
self.add_consistency,
);
if y.dom.is_empty() {
db.contradiction()?;
unreachable!();
}
y.views = views
.into_iter()
.map(|(val, view)| (val, (y.id + 1, view)))
.collect();
y
})))
})
.try_collect()?;
let mut ys = ys.into_iter();
let first = ys.next().unwrap();
let _ = xs.iter().zip(ys).fold(first, |curr, (x_i, next)| {
model.cons.push(Lin::tern(
curr,
Rc::clone(x_i),
lin.cmp.clone(),
Rc::clone(&next),
));
next
});
model.encode(db, self.cutoff)?;
Ok(())
}
}
impl LinMarker for BddEncoder {}
impl BoolLinAggregator {
#[cfg_attr(
any(feature = "tracing", test),
tracing::instrument(name = "aggregator", skip_all, fields(constraint = lin.trace_print()))
)]
pub fn aggregate<Db>(&self, db: &mut Db, lin: &BoolLinear) -> Result<BoolLinVariant>
where
Db: ClauseDatabase + ?Sized,
{
let mut k = lin.k;
let mut agg = FxHashMap::with_capacity_and_hasher(lin.exp.terms.len(), FxBuildHasher);
for term in &lin.exp.terms {
let var = term.0.var();
let entry = agg.entry(var).or_insert(0);
let mut coef = term.1 * lin.exp.mult;
if term.0.is_negated() {
k -= coef;
coef = -coef;
}
*entry += coef;
}
if lin.cmp == Comparator::GreaterEq {
agg = agg.into_iter().map(|(var, coef)| (var, -coef)).collect();
k = -k;
}
let mut partition: Vec<(Constraint, Vec<(Lit, Coeff)>)> =
Vec::with_capacity(lin.exp.constraints.len());
let mut iter = lin.exp.terms.iter().skip(lin.exp.num_free);
for con in &lin.exp.constraints {
let mut terms = Vec::with_capacity(con.1);
for _ in 0..con.1 {
let term = iter.next().unwrap();
if let Some((var, i)) = agg.remove_entry(&term.0.var()) {
terms.push((var.into(), i));
}
}
if !terms.is_empty() {
match con.0 {
Constraint::Domain { lb, ub } => {
if terms.len() == con.1 && is_powers_of_two(terms.iter().map(|(_, c)| *c)) {
let (lb, ub) = if lin.cmp == Comparator::GreaterEq {
let range = -terms.iter().fold(0, |acc, (_, coef)| acc + *coef);
(range - ub, range - lb)
} else {
(terms[0].1 * lb, terms[0].1 * ub)
};
partition.push((Constraint::Domain { lb, ub }, terms));
} else {
for term in terms {
partition.push((Constraint::AtMostOne, vec![term]));
}
}
}
_ => partition.push((con.0.clone(), terms)),
}
}
}
debug_assert!(agg.len() <= lin.exp.num_free);
let agg_keys: Vec<Var> = agg.keys().copied().sorted().collect();
for var in agg_keys {
partition.push((Constraint::AtMostOne, vec![(var.into(), agg[&var])]));
}
k -= lin.exp.add;
let cmp = match lin.cmp {
Comparator::LessEq | Comparator::GreaterEq => LimitComp::LessEq,
Comparator::Equal => LimitComp::Equal,
};
let convert_term_if_negative = |term: (Lit, Coeff), k: &mut Coeff| -> (Lit, PosCoeff) {
let (mut lit, mut coef) = term;
if coef.is_negative() {
coef = -coef;
lit = !lit;
*k += coef;
};
(lit, PosCoeff::new(coef))
};
let partition: Vec<Part> = partition
.into_iter()
.filter(|(_, t)| !t.is_empty()) .flat_map(|part| -> Vec<Part> {
match part {
(Constraint::AtMostOne, mut terms) => {
if terms.len() == 1 {
return vec![Part::Amo(
terms
.into_iter()
.filter(|&(_, coef)| coef != 0)
.map(|(lit, coef)| {
convert_term_if_negative((lit, coef), &mut k)
})
.collect(),
)];
}
let (min_index, (_, min_coef)) = terms
.iter()
.enumerate()
.min_by(|(_, (_, a)), (_, (_, b))| a.cmp(b))
.expect("Partition should not contain constraint on zero terms");
if min_coef.is_negative() {
let q = -*min_coef;
let y = db.new_lit();
db.add_clause(
terms
.iter()
.map(|(lit, _)| *lit)
.chain(once(y))
)
.unwrap();
for lit in terms.iter().map(|tup| tup.0) {
db.add_clause( [!y, !lit]).unwrap();
}
let _ =terms.remove(min_index);
terms.push((y, 0)); terms = terms
.iter()
.map(|(lit, coef)| (*lit, *coef + q))
.collect();
k += q;
}
vec![Part::Amo(
terms
.into_iter()
.map(|(lit, coef)| (lit, PosCoeff::new(coef)))
.collect(),
)]
}
(Constraint::ImplicationChain, terms) => {
let (pos_chain, neg_chain): (_, Vec<_>) =
terms.into_iter().partition(|(_, coef)| coef.is_positive());
vec![
Part::Ic(
pos_chain
.into_iter()
.map(|(lit, coef)| (lit, PosCoeff::new(coef)))
.collect(),
),
Part::Ic(
neg_chain
.into_iter()
.map(|(lit, coef)| {
convert_term_if_negative((lit, coef), &mut k)
})
.rev() .collect(),
),
]
}
(Constraint::Domain { lb: l, ub: u }, terms) => {
assert!(
terms.iter().all(|(_,coef)| coef.is_positive())
|| terms.iter().all(|(_,coef)| coef.is_negative()),
"Normalizing mixed positive/negative coefficients not yet supported for Dom constraint on {terms:?}"
);
vec![Part::Dom(
terms
.into_iter()
.map(|(lit, coef)| convert_term_if_negative((lit, coef), &mut k))
.collect(),
PosCoeff::new(l),
PosCoeff::new(u),
)]
}
}
})
.map(|part| {
let filter_zero_coefficients = |terms: Vec<(Lit, PosCoeff)>| -> Vec<(Lit, PosCoeff)> {
terms
.into_iter()
.filter(|&(_, coef)| *coef != 0)
.collect()
};
match part {
Part::Amo(terms) => Part::Amo(filter_zero_coefficients(terms)),
Part::Ic(terms) => Part::Ic(filter_zero_coefficients(terms)),
Part::Dom(terms, l, u) => Part::Dom(filter_zero_coefficients(terms), l, u),
}
})
.filter(|part| part.iter().next().is_some()) .collect();
if k < 0 {
db.contradiction()?;
unreachable!();
}
if k == 0 {
for part in partition {
for (lit, _) in part.iter() {
db.add_clause([!*lit])?;
}
}
return Ok(BoolLinVariant::Trivial);
}
let mut k = PosCoeff::new(k);
let partition = partition
.into_iter()
.map(|part| match part {
Part::Amo(terms) => Part::Amo(
terms
.into_iter()
.filter(|(lit, coef)| {
if coef > &k {
db.add_clause([!*lit]).unwrap();
false
} else {
true
}
})
.collect(),
),
Part::Ic(terms) => {
let mut acc = 0;
Part::Ic(
terms
.into_iter()
.filter(|&(lit, coef)| {
acc += *coef;
if acc > *k {
db.add_clause([!lit]).unwrap();
false
} else {
true
}
})
.collect(),
)
}
Part::Dom(terms, l, u) => {
let terms = terms
.into_iter()
.filter(|(lit, coef)| {
if coef > &k {
db.add_clause([!*lit]).unwrap();
false
} else {
true
}
})
.collect_vec();
let u = PosCoeff::new(min(*u, terms.iter().map(|&(_, coef)| *coef).sum()));
Part::Dom(terms, l, u)
}
})
.filter(|part| part.iter().next().is_some()) .collect_vec();
let lhs_ub = PosCoeff::new(
partition
.iter()
.map(|part| match part {
Part::Amo(terms) => terms.iter().map(|&(_, i)| *i).max().unwrap_or(0),
Part::Ic(terms) | Part::Dom(terms, _, _) => {
terms.iter().map(|&(_, coef)| *coef).sum()
}
})
.sum(),
);
match cmp {
LimitComp::LessEq => {
if lhs_ub <= k {
return Ok(BoolLinVariant::Trivial);
}
if partition.iter().flat_map(|part| part.iter()).count() == 2 {
db.add_clause(
partition
.iter()
.flat_map(|part| part.iter())
.map(|(lit, _)| !*lit)
.collect_vec(),
)?;
return Ok(BoolLinVariant::Trivial);
}
}
LimitComp::Equal => {
if lhs_ub < k {
db.contradiction()?;
unreachable!();
}
if lhs_ub == k {
for part in partition {
match part {
Part::Amo(terms) => {
db.add_clause([terms
.iter()
.max_by(|(_, a), (_, b)| a.cmp(b))
.unwrap()
.0])?;
}
Part::Ic(terms) | Part::Dom(terms, _, _) => {
for (lit, _) in terms {
db.add_clause([lit])?;
}
}
};
}
return Ok(BoolLinVariant::Trivial);
}
}
}
debug_assert!(partition
.iter()
.flat_map(|part| part.iter())
.next()
.is_some());
let val = partition
.iter()
.flat_map(|part| part.iter().map(|&(_, coef)| coef))
.next()
.unwrap();
if partition
.iter()
.flat_map(|part| part.iter())
.all(|&(_, coef)| coef == val)
{
if cmp == LimitComp::Equal && *k % *val != 0 {
db.contradiction()?;
unreachable!();
}
k = PosCoeff::new(*k / *val);
let partition = partition
.iter()
.flat_map(|part| part.iter())
.map(|&(lit, _)| lit)
.collect_vec();
if *k == 1 {
return Ok(BoolLinVariant::CardinalityOne(CardinalityOne {
lits: partition,
cmp,
}));
}
if partition.len() == (*k + 1) as usize {
let neg = partition.iter().map(|&l| !l);
db.add_clause(neg.clone())?;
if cmp == LimitComp::LessEq {
return Ok(BoolLinVariant::Trivial);
} else {
return Ok(BoolLinVariant::CardinalityOne(CardinalityOne {
lits: neg.collect_vec(),
cmp: LimitComp::LessEq,
}));
}
}
return Ok(BoolLinVariant::Cardinality(Cardinality {
lits: partition,
cmp,
k,
}));
}
let partition = if self.sort_same_coefficients >= 2 {
let (free_lits, mut partition): (Vec<_>, Vec<_>) = partition.into_iter().partition(
|part| matches!(part, Part::Amo(x) | Part::Ic(x) | Part::Dom(x, _, _) if x.len() == 1),
);
for (coef, lits) in free_lits
.into_iter()
.map(|part| match part {
Part::Amo(x) | Part::Ic(x) | Part::Dom(x, _, _) if x.len() == 1 => x[0],
_ => unreachable!(),
})
.map(|(lit, coef)| (coef, lit))
.into_group_map()
.into_iter()
{
if self.sort_same_coefficients >= 2 && lits.len() >= self.sort_same_coefficients {
let c = *k / *coef;
let y = IntVarOrd::from_bounds(db, 0, c, String::from("s")).into();
self.sorted_encoder
.encode(db, &Sorted::new(&lits, cmp.clone(), &y))
.unwrap();
let lin_exp = BoolLinExp::from(&y);
partition.push(Part::Ic(
lin_exp
.terms
.into_iter()
.map(|(lit, _)| (lit, coef))
.collect(),
));
} else {
for x in lits {
partition.push(Part::Amo(vec![(x, coef)]));
}
}
}
partition
} else {
partition
};
Ok(BoolLinVariant::Linear(NormalizedBoolLinear {
terms: partition,
cmp,
k,
}))
}
pub fn sort_same_coefficients(&mut self, sorted_encoder: SortedEncoder, n: usize) -> &mut Self {
self.sorted_encoder = sorted_encoder;
self.sort_same_coefficients = n;
self
}
}
impl BoolLinExp {
pub fn add_bounded_log_encoding(
mut self,
terms: &[(Lit, Coeff)],
lb: Coeff,
ub: Coeff,
) -> Self {
self.constraints
.push((Constraint::Domain { lb, ub }, terms.len()));
self.terms.extend(terms.iter().cloned());
self
}
pub fn add_chain(mut self, chain: &[(Lit, Coeff)]) -> Self {
if let [term] = chain {
self.terms.push_front(*term);
self.num_free += 1;
} else {
self.terms.extend(chain.iter().cloned());
self.constraints
.push((Constraint::ImplicationChain, chain.len()));
}
self
}
pub fn add_choice(mut self, choice: &[(Lit, Coeff)]) -> Self {
if let [term] = choice {
self.terms.push_front(*term);
self.num_free += 1;
} else {
self.terms.extend(choice.iter().cloned());
self.constraints.push((Constraint::AtMostOne, choice.len()));
}
self
}
pub fn add_constant(mut self, k: Coeff) -> Self {
self.add += k;
self
}
pub fn add_lit(mut self, lit: Lit) -> Self {
self.terms.push_front((lit, 1));
self.num_free += 1;
self
}
pub fn from_slices(coeffs: &[Coeff], lits: &[Lit]) -> Self {
assert_eq!(
coeffs.len(),
lits.len(),
"the number of weights and literals must be equal"
);
Self {
terms: lits.iter().cloned().zip(coeffs.iter().cloned()).collect(),
num_free: lits.len(),
..Default::default()
}
}
pub fn from_terms(terms: &[(Lit, Coeff)]) -> Self {
Self {
terms: terms.iter().cloned().collect(),
num_free: terms.len(),
..Default::default()
}
}
pub(crate) fn iter(&self) -> impl Iterator<Item = (Option<Constraint>, Vec<&(Lit, Coeff)>)> {
let mut it = self.terms.iter();
once((
None,
Vec::from_iter((0..self.num_free).map(|_| it.next().unwrap())),
))
.chain(self.constraints.iter().map(move |constraint| {
let mut terms = Vec::with_capacity(constraint.1);
for _ in 0..constraint.1 {
if let Some(term) = it.next() {
terms.push(term);
}
}
(Some(constraint.0.clone()), terms)
}))
}
pub fn terms(&self) -> impl Iterator<Item = (Lit, Coeff)> + '_ {
self.terms.iter().copied()
}
pub(crate) fn value<F: Valuation + ?Sized>(&self, sol: &F) -> Result<Coeff> {
let mut total = self.add;
for (constraint, terms) in self.iter() {
let sum = terms
.iter()
.filter(|(lit, _)| sol.value(*lit))
.map(|(_, i)| i)
.sum();
match constraint {
Some(Constraint::AtMostOne)
if sum != 0 && terms.iter().filter(|&&&(l, _)| sol.value(l)).count() > 1 =>
{
return Err(Unsatisfiable);
}
Some(Constraint::ImplicationChain)
if terms
.iter()
.map(|(l, _)| *l)
.tuple_windows()
.any(|(a, b)| !sol.value(a) & sol.value(b)) =>
{
return Err(Unsatisfiable);
}
Some(Constraint::Domain { lb, ub }) => {
if GROUND_BINARY_AT_LB {
if sum > ub - lb {
return Err(Unsatisfiable);
}
} else if lb > sum || sum > ub {
return Err(Unsatisfiable);
}
}
_ => {}
};
total += sum;
}
Ok(total * self.mult)
}
}
impl Add for BoolLinExp {
type Output = BoolLinExp;
fn add(mut self, rhs: Self) -> Self::Output {
self += rhs;
self
}
}
impl Add<Coeff> for BoolLinExp {
type Output = BoolLinExp;
fn add(mut self, rhs: Coeff) -> Self::Output {
self += rhs;
self
}
}
impl<'a> Add<IntEncoding<'a>> for BoolLinExp {
type Output = BoolLinExp;
fn add(mut self, rhs: IntEncoding<'a>) -> Self::Output {
self += rhs;
self
}
}
impl AddAssign for BoolLinExp {
fn add_assign(&mut self, rhs: Self) {
if self.mult != 1 {
self.add *= self.mult;
for term in &mut self.terms {
term.1 *= self.mult;
}
}
self.mult = 1;
self.add += rhs.add * rhs.mult;
let mut rh_terms = rhs.terms;
self.terms.extend(
rh_terms
.drain(rhs.num_free..)
.map(|(l, c)| (l, c * rhs.mult)),
);
debug_assert!(rh_terms.len() == rhs.num_free);
self.terms
.extend(rh_terms.into_iter().map(|(l, c)| (l, c * rhs.mult)));
self.terms.rotate_right(rhs.num_free);
self.num_free += rhs.num_free;
self.constraints.extend(rhs.constraints);
}
}
impl AddAssign<Coeff> for BoolLinExp {
fn add_assign(&mut self, rhs: Coeff) {
self.add += rhs;
}
}
impl<'a> AddAssign<IntEncoding<'a>> for BoolLinExp {
fn add_assign(&mut self, rhs: IntEncoding<'a>) {
match rhs {
IntEncoding::Direct { first, vals } => {
for (k, lit) in (first..).zip(vals.iter()) {
self.terms.push_back((*lit, k));
}
self.constraints.push((Constraint::AtMostOne, vals.len()));
}
IntEncoding::Order { first, vals } => {
for lit in vals {
self.terms.push_back((*lit, 1));
}
self.add += first;
self.constraints
.push((Constraint::ImplicationChain, vals.len()));
}
IntEncoding::Log { signed, bits } => {
let two = 1 + 1;
let mut k = 1;
for lit in bits {
self.terms.push_front((*lit, k));
k *= two;
}
if signed {
self.terms.front_mut().unwrap().1 *= -1;
}
self.num_free += bits.len();
}
}
}
}
impl Default for BoolLinExp {
fn default() -> Self {
Self {
terms: Default::default(),
num_free: 0,
constraints: Default::default(),
add: 0,
mult: 1,
}
}
}
impl Display for BoolLinExp {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{}",
self.terms
.iter()
.map(|(lit, c)| (lit, c * self.mult))
.format_with(" + ", |elt, f| match elt.1 {
1 => f(&format_args!("{}", elt.0)),
-1 => f(&format_args!("-{}", elt.0)),
_ => f(&format_args!("{}*{}", elt.1, elt.0)),
})
)?;
if self.add != 0 {
if !self.terms.is_empty() {
write!(f, " + ")?;
}
write!(f, "{}", self.add)?;
}
Ok(())
}
}
impl From<Coeff> for BoolLinExp {
fn from(value: Coeff) -> Self {
Self {
add: value,
..Default::default()
}
}
}
impl<'a> From<IntEncoding<'a>> for BoolLinExp {
fn from(var: IntEncoding<'a>) -> Self {
match var {
IntEncoding::Direct { first, vals } => {
let mut terms = VecDeque::with_capacity(vals.len());
for (k, lit) in (first..).zip(vals.iter()) {
terms.push_back((*lit, k));
}
Self {
terms,
constraints: vec![(Constraint::AtMostOne, vals.len())],
..Default::default()
}
}
IntEncoding::Order { first, vals } => Self {
terms: vals.iter().map(|lit| (*lit, 1)).collect(),
constraints: vec![(Constraint::ImplicationChain, vals.len())],
add: first,
..Default::default()
},
IntEncoding::Log { signed, bits } => {
let mut terms = VecDeque::with_capacity(bits.len());
let two = 1 + 1;
let mut k = 1;
for lit in bits {
terms.push_back((*lit, k));
k *= two;
}
if signed {
terms.back_mut().unwrap().1 *= -1;
}
Self {
terms,
num_free: bits.len(),
..Default::default()
}
}
}
}
}
impl From<Lit> for BoolLinExp {
fn from(lit: Lit) -> Self {
Self {
terms: VecDeque::from([(lit, 1)]),
num_free: 1,
..Default::default()
}
}
}
impl From<bool> for BoolLinExp {
fn from(b: bool) -> Self {
Self {
add: b.into(),
..Default::default()
}
}
}
impl Mul<Coeff> for BoolLinExp {
type Output = BoolLinExp;
fn mul(mut self, rhs: Coeff) -> Self::Output {
self *= rhs;
self
}
}
impl MulAssign<Coeff> for BoolLinExp {
fn mul_assign(&mut self, rhs: Coeff) {
self.mult *= rhs;
}
}
impl Neg for BoolLinExp {
type Output = Self;
fn neg(mut self) -> Self::Output {
self.mult = -self.mult;
self
}
}
impl Sub for BoolLinExp {
type Output = Self;
fn sub(self, rhs: Self) -> Self::Output {
let mut res = self.clone();
res -= rhs;
res
}
}
impl SubAssign for BoolLinExp {
fn sub_assign(&mut self, rhs: Self) {
self.add_assign(-rhs);
}
}
impl BoolLinear {
pub fn new(exp: BoolLinExp, cmp: Comparator, k: Coeff) -> Self {
Self { exp, cmp, k }
}
pub fn set_cmp(&mut self, cmp: Comparator) {
self.cmp = cmp;
}
#[cfg(any(feature = "tracing", test))]
pub(crate) fn trace_print(&self) -> String {
use crate::trace::trace_print_lit;
let x = itertools::join(
self.exp
.terms
.iter()
.map(|(l, c)| format!("{c:?}·{}", trace_print_lit(l))),
" + ",
);
let op = match self.cmp {
Comparator::LessEq => "≤",
Comparator::Equal => "=",
Comparator::GreaterEq => "≥",
};
format!("{x} {op} {:?}", self.k)
}
}
impl Checker for BoolLinear {
fn check<F: Valuation + ?Sized>(&self, value: &F) -> Result<()> {
let lhs = self.exp.value(value)?;
if match self.cmp {
Comparator::LessEq => lhs <= self.k,
Comparator::Equal => lhs == self.k,
Comparator::GreaterEq => lhs >= self.k,
} {
Ok(())
} else {
Err(Unsatisfiable)
}
}
}
impl Display for BoolLinear {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{} {} {}",
self.exp,
match self.cmp {
Comparator::Equal => "==",
Comparator::LessEq => "<=",
Comparator::GreaterEq => ">=",
},
self.k
)
}
}
impl From<NormalizedBoolLinear> for BoolLinear {
fn from(lin: NormalizedBoolLinear) -> Self {
BoolLinear {
exp: BoolLinExp::from_terms(
lin.terms
.iter()
.flat_map(|part| part.into_iter().map(|&(l, c)| (l, *c)))
.collect_vec()
.as_slice(),
),
cmp: lin.cmp.into(),
k: *lin.k,
}
}
}
impl From<PosCoeff> for Coeff {
fn from(val: PosCoeff) -> Self {
val.0
}
}
impl From<LimitComp> for Comparator {
fn from(value: LimitComp) -> Self {
match value {
LimitComp::Equal => Comparator::Equal,
LimitComp::LessEq => Comparator::LessEq,
}
}
}
impl<Db, Enc> Encoder<Db, Cardinality> for Enc
where
Db: ClauseDatabase + ?Sized,
Enc: Encoder<Db, NormalizedBoolLinear> + LinMarker,
{
fn encode(&self, db: &mut Db, con: &Cardinality) -> Result {
self.encode(db, &NormalizedBoolLinear::from(con.clone()))
}
}
impl Display for LimitComp {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
LimitComp::Equal => write!(f, "=="),
LimitComp::LessEq => write!(f, "<="),
}
}
}
impl<Enc, Agg> LinearEncoder<Enc, Agg> {
pub fn linear_aggregator(&self) -> &Agg {
&self.agg
}
pub fn new(enc: Enc, agg: Agg) -> Self {
Self { enc, agg }
}
pub fn variant_encoder(&self) -> &Enc {
&self.enc
}
pub fn with_linear_aggregator(&mut self, agg: Agg) -> &mut Self {
self.agg = agg;
self
}
pub fn with_variant_encoder(&mut self, enc: Enc) -> &mut Self {
self.enc = enc;
self
}
}
impl<Db, Enc> Encoder<Db, BoolLinear> for LinearEncoder<Enc>
where
Db: ClauseDatabase + ?Sized,
Enc: Encoder<Db, BoolLinVariant>,
{
#[cfg_attr(
any(feature = "tracing", test),
tracing::instrument(name = "linear_encoder", skip_all, fields(constraint = lin.trace_print()))
)]
fn encode(&self, db: &mut Db, lin: &BoolLinear) -> Result {
let variant = self.agg.aggregate(db, lin)?;
self.enc.encode(db, &variant)
}
}
impl NormalizedBoolLinear {
pub fn comparator(&self) -> Comparator {
self.cmp.clone().into()
}
pub fn is_empty(&self) -> bool {
self.terms.is_empty()
}
pub fn iter_terms(&self) -> impl Iterator<Item = (Lit, Coeff)> + '_ {
self.terms
.iter()
.flat_map(|part| part.iter().map(|&(lit, coef)| (lit, coef.into())))
}
pub fn len(&self) -> usize {
self.terms.len()
}
pub fn rhs(&self) -> Coeff {
self.k.into()
}
pub fn set_rhs(&mut self, k: Coeff) {
self.k = PosCoeff::new(k);
}
#[cfg(any(feature = "tracing", test))]
pub(crate) fn trace_print(&self) -> String {
use crate::trace::trace_print_lit;
let x = itertools::join(
self.terms
.iter()
.flat_map(|part| part.iter().map(|(lit, coef)| (lit, **coef)))
.map(|(l, c)| format!("{c:?}·{}", trace_print_lit(l))),
" + ",
);
let op = if self.cmp == LimitComp::LessEq {
"≤"
} else {
"="
};
format!("{x} {op} {:?}", *self.k)
}
}
impl Checker for NormalizedBoolLinear {
fn check<F: Valuation + ?Sized>(&self, sol: &F) -> Result<()> {
let sum: Coeff = self
.terms
.iter()
.flat_map(|p| p.iter().copied())
.filter_map(|(l, c)| {
if sol.value(l) {
Some(Coeff::from(c))
} else {
None
}
})
.sum();
if match self.cmp {
LimitComp::LessEq => sum <= *self.k,
LimitComp::Equal => sum == *self.k,
} {
Ok(())
} else {
Err(Unsatisfiable)
}
}
}
impl From<Cardinality> for NormalizedBoolLinear {
fn from(card: Cardinality) -> Self {
Self {
terms: card
.lits
.into_iter()
.map(|l| Part::Amo(vec![(l, PosCoeff::new(1))]))
.collect(),
cmp: card.cmp,
k: card.k,
}
}
}
impl From<CardinalityOne> for NormalizedBoolLinear {
fn from(amo: CardinalityOne) -> Self {
Self::from(Cardinality::from(amo))
}
}
impl Part {
pub(crate) fn iter(&self) -> impl Iterator<Item = &(Lit, PosCoeff)> {
self.into_iter()
}
}
impl<'a> IntoIterator for &'a Part {
type IntoIter = std::slice::Iter<'a, (Lit, PosCoeff)>;
type Item = &'a (Lit, PosCoeff);
fn into_iter(self) -> Self::IntoIter {
match self {
Part::Amo(terms) => terms.iter(),
Part::Ic(terms) => terms.iter(),
Part::Dom(terms, _lb, _ub) => terms.iter(),
}
}
}
impl PosCoeff {
pub(crate) fn new(c: Coeff) -> Self {
if c < 0 {
panic!("cannot create a PosCoeff with a negative value")
}
Self(c)
}
}
impl Deref for PosCoeff {
type Target = Coeff;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DerefMut for PosCoeff {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl Display for PosCoeff {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
impl<LinEnc, CardEnc, AmoEnc> StaticLinEncoder<LinEnc, CardEnc, AmoEnc> {
pub fn amo_encoder(&mut self) -> &mut AmoEnc {
&mut self.amo_enc
}
pub fn card_encoder(&mut self) -> &mut CardEnc {
&mut self.card_enc
}
pub fn lin_encoder(&mut self) -> &mut LinEnc {
&mut self.lin_enc
}
pub fn new(lin_enc: LinEnc, card_enc: CardEnc, amo_enc: AmoEnc) -> Self {
Self {
lin_enc,
card_enc,
amo_enc,
}
}
}
impl<Db, LinEnc, CardEnc, AmoEnc> Encoder<Db, BoolLinVariant>
for StaticLinEncoder<LinEnc, CardEnc, AmoEnc>
where
Db: ClauseDatabase + ?Sized,
LinEnc: Encoder<Db, NormalizedBoolLinear>,
CardEnc: Encoder<Db, Cardinality>,
AmoEnc: Encoder<Db, CardinalityOne>,
{
fn encode(&self, db: &mut Db, lin: &BoolLinVariant) -> Result {
match &lin {
BoolLinVariant::Linear(lin) => self.lin_enc.encode(db, lin),
BoolLinVariant::Cardinality(card) => self.card_enc.encode(db, card),
BoolLinVariant::CardinalityOne(amo) => self.amo_enc.encode(db, amo),
BoolLinVariant::Trivial => Ok(()),
}
}
}
impl SwcEncoder {
pub fn with_consistency(&mut self, b: bool) -> &mut Self {
self.add_consistency = b;
self
}
pub fn with_cutoff(&mut self, c: Option<Coeff>) -> &mut Self {
self.cutoff = c;
self
}
pub fn with_propagation(&mut self, c: Consistency) -> &mut Self {
self.add_propagation = c;
self
}
}
impl<Db> Encoder<Db, NormalizedBoolLinear> for SwcEncoder
where
Db: ClauseDatabase + ?Sized,
{
#[cfg_attr(
any(feature = "tracing", test),
tracing::instrument(name = "swc_encoder", skip_all, fields(constraint = lin.trace_print()))
)]
fn encode(&self, db: &mut Db, lin: &NormalizedBoolLinear) -> Result {
let mut model = Model::default();
let xs = lin
.terms
.iter()
.enumerate()
.flat_map(|(i, part)| IntVarEnc::from_part(db, part, lin.k, format!("x_{i}")))
.map(|x| Rc::new(RefCell::new(model.add_int_var_enc(x))))
.collect_vec();
let n = xs.len();
let ys = once(model.new_constant(0))
.chain(
(1..n)
.map(|_| model.new_var((-(*lin.k)..=0).into(), self.add_consistency))
.take(n),
)
.collect_vec()
.into_iter()
.chain(once(model.new_constant(-*lin.k)))
.map(|y| Rc::new(RefCell::new(y)))
.collect_vec();
ys.into_iter()
.tuple_windows()
.zip(xs)
.for_each(|((y_curr, y_next), x)| {
model
.cons
.push(Lin::tern(x, y_next, lin.cmp.clone(), y_curr));
});
model.propagate(&self.add_propagation, vec![model.cons.len() - 1])?;
model.encode(db, self.cutoff)
}
}
impl LinMarker for SwcEncoder {}
impl TotalizerEncoder {
const EQUALIZE_INTERMEDIATES: bool = false;
pub fn with_consistency(&mut self, b: bool) -> &mut Self {
self.add_consistency = b;
self
}
pub fn with_cutoff(&mut self, c: Option<Coeff>) -> &mut Self {
self.cutoff = c;
self
}
pub fn with_propagation(&mut self, c: Consistency) -> &mut Self {
self.add_propagation = c;
self
}
}
impl TotalizerEncoder {
fn build_totalizer(&self, xs: Vec<IntVarEnc>, cmp: &LimitComp, k: Coeff) -> Model {
let mut model = Model::default();
let mut layer = xs
.into_iter()
.map(|x| Rc::new(RefCell::new(model.add_int_var_enc(x))))
.collect_vec();
while layer.len() > 1 {
let mut next_layer = Vec::<Rc<RefCell<IntVar>>>::new();
for children in layer.chunks(2) {
match children {
[x] => {
next_layer.push(Rc::clone(x));
}
[left, right] => {
let at_root = layer.len() == 2;
let dom = if at_root {
(k..=k).into()
} else {
left.borrow()
.dom
.iter()
.flatten()
.cartesian_product(right.borrow().dom.iter().flatten())
.map(|(a, b)| a + b)
.filter(|&d| d <= k)
.map(|v| v..=v)
.collect()
};
let parent =
Rc::new(RefCell::new(model.new_var(dom, self.add_consistency)));
model.cons.push(Lin::tern(
Rc::clone(left),
Rc::clone(right),
if !at_root && Self::EQUALIZE_INTERMEDIATES {
LimitComp::Equal
} else {
cmp.clone()
},
Rc::clone(&parent),
));
next_layer.push(parent);
}
_ => panic!(),
}
}
layer = next_layer;
}
model
}
}
impl<Db> Encoder<Db, NormalizedBoolLinear> for TotalizerEncoder
where
Db: ClauseDatabase + ?Sized,
{
#[cfg_attr(
any(feature = "tracing", test),
tracing::instrument(name = "totalizer_encoder", skip_all, fields(constraint = lin.trace_print()))
)]
fn encode(&self, db: &mut Db, lin: &NormalizedBoolLinear) -> Result {
let xs = lin
.terms
.iter()
.enumerate()
.flat_map(|(i, part)| IntVarEnc::from_part(db, part, lin.k, format!("x_{i}")))
.sorted_by_key(|x| x.ub())
.collect_vec();
let mut model = self.build_totalizer(xs, &lin.cmp, *lin.k);
model.propagate(&self.add_propagation, vec![model.cons.len() - 1])?;
model.encode(db, self.cutoff)
}
}
impl LinMarker for TotalizerEncoder {}
#[cfg(test)]
mod tests {
macro_rules! linear_test_suite {
($module:ident, $encoder:expr) => {
mod $module {
use traced_test::test;
use crate::{
bool_linear::{
tests::construct_terms, LimitComp, NormalizedBoolLinear, PosCoeff,
},
helpers::tests::{assert_solutions, expect_file},
ClauseDatabaseTools, Cnf, Encoder,
};
#[test]
fn small_le_1() {
let mut cnf = Cnf::default();
let a = cnf.new_lit();
let b = cnf.new_lit();
let c = cnf.new_lit();
$encoder
.encode(
&mut cnf,
&NormalizedBoolLinear {
terms: construct_terms(&[(a, 2), (b, 3), (c, 5)]),
cmp: LimitComp::LessEq,
k: PosCoeff::new(6),
},
)
.unwrap();
assert_solutions(
&cnf,
vec![a, b, c],
&expect_file!["linear/test_small_le_1.sol"],
);
}
#[test]
fn small_le_2() {
let mut cnf = Cnf::default();
let a = cnf.new_lit();
let b = cnf.new_lit();
let c = cnf.new_lit();
let d = cnf.new_lit();
let e = cnf.new_lit();
let f = cnf.new_lit();
$encoder
.encode(
&mut cnf,
&NormalizedBoolLinear {
terms: construct_terms(&[
(!a, 3),
(!b, 6),
(!c, 1),
(!d, 2),
(!e, 3),
(!f, 6),
]),
cmp: LimitComp::LessEq,
k: PosCoeff::new(19),
},
)
.unwrap();
assert_solutions(
&cnf,
vec![a, b, c, d, e, f],
&expect_file!["linear/test_small_le_2.sol"],
);
}
#[test]
fn small_le_3() {
let mut cnf = Cnf::default();
let a = cnf.new_lit();
let b = cnf.new_lit();
let c = cnf.new_lit();
$encoder
.encode(
&mut cnf,
&NormalizedBoolLinear {
terms: construct_terms(&[(a, 1), (b, 2), (c, 4)]),
cmp: LimitComp::LessEq,
k: PosCoeff::new(5),
},
)
.unwrap();
assert_solutions(
&cnf,
vec![a, b, c],
&expect_file!["linear/test_small_le_3.sol"],
);
}
#[test]
fn small_le_4() {
let mut cnf = Cnf::default();
let a = cnf.new_lit();
let b = cnf.new_lit();
let c = cnf.new_lit();
$encoder
.encode(
&mut cnf,
&NormalizedBoolLinear {
terms: construct_terms(&[(a, 4), (b, 6), (c, 7)]),
cmp: LimitComp::LessEq,
k: PosCoeff::new(10),
},
)
.unwrap();
assert_solutions(
&cnf,
vec![a, b, c],
&expect_file!["linear/test_small_le_4.sol"],
);
}
#[test]
fn small_eq_1() {
let mut cnf = Cnf::default();
let a = cnf.new_lit();
let b = cnf.new_lit();
let c = cnf.new_lit();
$encoder
.encode(
&mut cnf,
&NormalizedBoolLinear {
terms: construct_terms(&[(a, 1), (b, 2), (c, 4)]),
cmp: LimitComp::Equal,
k: PosCoeff::new(5),
},
)
.unwrap();
assert_solutions(
&cnf,
vec![a, b, c],
&expect_file!["linear/test_small_eq_1.sol"],
);
}
#[test]
fn small_eq_2() {
let mut cnf = Cnf::default();
let a = cnf.new_lit();
let b = cnf.new_lit();
let c = cnf.new_lit();
$encoder
.encode(
&mut cnf,
&NormalizedBoolLinear {
terms: construct_terms(&[(a, 1), (b, 2), (c, 3)]),
cmp: LimitComp::Equal,
k: PosCoeff::new(3),
},
)
.unwrap();
assert_solutions(
&cnf,
vec![a, b, c],
&expect_file!["linear/test_small_eq_2.sol"],
);
}
#[test]
fn small_eq_3() {
let mut cnf = Cnf::default();
let a = cnf.new_lit();
let b = cnf.new_lit();
let c = cnf.new_lit();
let d = cnf.new_lit();
$encoder
.encode(
&mut cnf,
&NormalizedBoolLinear {
terms: construct_terms(&[(a, 2), (b, 3), (c, 5), (d, 7)]),
cmp: LimitComp::Equal,
k: PosCoeff::new(10),
},
)
.unwrap();
assert_solutions(
&cnf,
vec![a, b, c, d],
&expect_file!["linear/test_small_eq_3.sol"],
);
}
#[test]
fn small_eq_4() {
let mut cnf = Cnf::default();
let a = cnf.new_lit();
let b = cnf.new_lit();
let c = cnf.new_lit();
let d = cnf.new_lit();
$encoder
.encode(
&mut cnf,
&NormalizedBoolLinear {
terms: construct_terms(&[(a, 2), (b, 1), (c, 2), (d, 2)]),
cmp: LimitComp::Equal,
k: PosCoeff::new(4),
},
)
.unwrap();
assert_solutions(
&cnf,
vec![a, b, c, d],
&expect_file!["linear/test_small_eq_4.sol"],
);
}
#[test]
fn issue_177() {
let mut cnf = Cnf::default();
let a = cnf.new_lit();
let b = cnf.new_lit();
let res = $encoder.encode(
&mut cnf,
&NormalizedBoolLinear {
terms: construct_terms(&[(a, 3), (b, 9)]),
cmp: LimitComp::Equal,
k: PosCoeff::new(10),
},
);
if res.is_ok() {
assert_solutions(
&cnf,
vec![a, b],
&expect_file!["linear/test_issue_177.sol"],
);
}
}
}
};
}
use std::{cmp::Ordering, num::NonZeroI32};
use itertools::Itertools;
use traced_test::test;
use crate::{
bool_linear::{
AdderEncoder, BoolLinAggregator, BoolLinExp, BoolLinVariant, BoolLinear, Comparator,
LimitComp, LinearEncoder, NormalizedBoolLinear, Part, PosCoeff, StaticLinEncoder,
TotalizerEncoder,
},
cardinality::{tests::card_test_suite, Cardinality},
cardinality_one::{tests::card1_test_suite, CardinalityOne, PairwiseEncoder},
helpers::tests::{assert_checker, assert_encoding, assert_solutions, expect_file},
sorted::SortedEncoder,
ClauseDatabase, ClauseDatabaseTools, Cnf, Coeff, Encoder, Lit, Unsatisfiable,
};
#[test]
fn aggregator_at_least_one_negated() {
let mut cnf = Cnf::default();
let (a, b, c, d) = cnf.new_lits();
assert_eq!(
BoolLinAggregator::default().aggregate(
&mut cnf,
&BoolLinear::new(
BoolLinExp::from_slices(&[1, 1, 1, 1], &[a, b, c, d]),
Comparator::LessEq,
3
)
),
Ok(BoolLinVariant::Trivial)
);
assert_encoding(
&cnf,
&expect_file!["linear/aggregator/test_at_least_one_negated.cnf"],
);
let mut cnf = Cnf::default();
let (a, b, c) = cnf.new_lits();
assert_eq!(
BoolLinAggregator::default().aggregate(
&mut cnf,
&BoolLinear::new(
BoolLinExp::from_slices(&[1, 1, 1], &[a, b, c]),
Comparator::Equal,
2
)
),
Ok(BoolLinVariant::CardinalityOne(CardinalityOne {
lits: vec![!a, !b, !c],
cmp: LimitComp::LessEq,
}))
);
}
#[test]
fn aggregator_combine() {
let mut cnf = Cnf::default();
let (a, b, c) = cnf.new_lits();
assert_eq!(
BoolLinAggregator::default().aggregate(
&mut cnf,
&BoolLinear::new(
BoolLinExp::from_slices(&[1, 2, 1, 2], &[a, a, b, c]),
Comparator::LessEq,
3
)
),
Ok(BoolLinVariant::Linear(NormalizedBoolLinear {
terms: construct_terms(&[(1, 3), (2, 1), (3, 2)]),
cmp: LimitComp::LessEq,
k: PosCoeff::new(3)
}))
);
assert_eq!(
BoolLinAggregator::default().aggregate(
&mut cnf,
&BoolLinear::new(
BoolLinExp::from_slices(&[1, 2, 1, 2], &[a, !a, b, c]),
Comparator::LessEq,
3
)
),
Ok(BoolLinVariant::Linear(NormalizedBoolLinear {
terms: construct_terms(&[(!a, 1), (b, 1), (c, 2)]),
cmp: LimitComp::LessEq,
k: PosCoeff::new(2)
}))
);
assert_eq!(
BoolLinAggregator::default().aggregate(
&mut cnf,
&BoolLinear::new(
BoolLinExp::from_slices(&[1, -2, 1, 2], &[a, a, b, c]),
Comparator::LessEq,
2,
)
),
Ok(BoolLinVariant::Linear(NormalizedBoolLinear {
terms: construct_terms(&[(!a, 1), (b, 1), (c, 2)]),
cmp: LimitComp::LessEq,
k: PosCoeff::new(3)
}))
);
assert_eq!(cnf.num_clauses(), 0);
}
#[test]
fn aggregator_detection() {
let mut cnf = Cnf::default();
let (a, b, c, d) = cnf.new_lits();
assert_eq!(
BoolLinAggregator::default().aggregate(
&mut cnf,
&BoolLinear::new(
BoolLinExp::from_slices(&[1, 1, 1], &[a, b, c]),
Comparator::LessEq,
1
)
),
Ok(BoolLinVariant::CardinalityOne(CardinalityOne {
lits: vec![a, b, c],
cmp: LimitComp::LessEq
}))
);
assert_eq!(
BoolLinAggregator::default().aggregate(
&mut cnf,
&BoolLinear::new(
BoolLinExp::from_slices(&[2, 2, 2], &[a, b, c]),
Comparator::LessEq,
2
)
),
Ok(BoolLinVariant::CardinalityOne(CardinalityOne {
lits: vec![a, b, c],
cmp: LimitComp::LessEq
}))
);
assert_eq!(
BoolLinAggregator::default().aggregate(
&mut cnf,
&BoolLinear::new(
BoolLinExp::from_slices(&[1, 1, 1, 1], &[a, b, c, d]),
Comparator::LessEq,
2
)
),
Ok(BoolLinVariant::Cardinality(Cardinality {
lits: vec![a, b, c, d],
cmp: LimitComp::LessEq,
k: PosCoeff::new(2),
}))
);
assert_eq!(
BoolLinAggregator::default().aggregate(
&mut cnf,
&BoolLinear::new(
BoolLinExp::from_slices(&[3, 3, 3, 3], &[a, b, c, d]),
Comparator::LessEq,
7
)
),
Ok(BoolLinVariant::Cardinality(Cardinality {
lits: vec![a, b, c, d],
cmp: LimitComp::LessEq,
k: PosCoeff::new(2),
}))
);
assert_eq!(
BoolLinAggregator::default().aggregate(
&mut cnf,
&BoolLinear::new(
BoolLinExp::from_slices(&[1, 1, 1, 1], &[a, b, c, d]),
Comparator::Equal,
2
)
),
Ok(BoolLinVariant::Cardinality(Cardinality {
lits: vec![a, b, c, d],
cmp: LimitComp::Equal,
k: PosCoeff::new(2),
}))
);
assert_eq!(
BoolLinAggregator::default().aggregate(
&mut cnf,
&BoolLinear::new(
BoolLinExp::from_slices(&[3, 3, 3, 3], &[a, b, c, d]),
Comparator::Equal,
6
)
),
Ok(BoolLinVariant::Cardinality(Cardinality {
lits: vec![a, b, c, d],
cmp: LimitComp::Equal,
k: PosCoeff::new(2),
}))
);
assert_eq!(
BoolLinAggregator::default().aggregate(
&mut cnf,
&BoolLinear::new(
BoolLinExp::from_slices(&[1, 2, 2], &[a, b, c]),
Comparator::LessEq,
2
)
),
Ok(BoolLinVariant::Linear(NormalizedBoolLinear {
terms: construct_terms(&[(a, 1), (b, 2), (c, 2)]),
cmp: LimitComp::LessEq,
k: PosCoeff::new(2),
}))
);
assert_eq!(
BoolLinAggregator::default().aggregate(
&mut cnf,
&BoolLinear::new(
BoolLinExp::from_slices(&[1, 2, 2], &[a, b, c]),
Comparator::Equal,
2
)
),
Ok(BoolLinVariant::Linear(NormalizedBoolLinear {
terms: construct_terms(&[(a, 1), (b, 2), (c, 2)]),
cmp: LimitComp::Equal,
k: PosCoeff::new(2),
}))
);
assert_eq!(
BoolLinAggregator::default().aggregate(
&mut cnf,
&BoolLinear::new(
BoolLinExp::from_terms(&[(c, -1)]).add_choice(&[(a, -1), (b, -1)]),
Comparator::LessEq,
-2,
)
),
Ok(BoolLinVariant::Trivial)
);
}
#[test]
fn aggregator_equal_one() {
let mut cnf = Cnf::default();
let vars = cnf.new_var_range(3).iter_lits().collect_vec();
assert_eq!(
BoolLinAggregator::default().aggregate(
&mut cnf,
&BoolLinear::new(
BoolLinExp::from_slices(&[1, 1, 1], &vars),
Comparator::Equal,
1
)
),
Ok(BoolLinVariant::CardinalityOne(CardinalityOne {
lits: vars,
cmp: LimitComp::Equal
}))
);
assert_eq!(cnf.num_clauses(), 0);
}
#[test]
fn aggregator_false_trivial_unsat() {
let mut cnf = Cnf::default();
let (a, b, c, d, e, f, g) = cnf.new_lits();
assert_eq!(
BoolLinAggregator::default().aggregate(
&mut cnf,
&BoolLinear::new(
BoolLinExp::from_slices(&[1, 2, 1, 1, 4, 1, 1], &[a, !b, c, d, !e, f, !g]),
Comparator::GreaterEq,
7
)
),
Ok(BoolLinVariant::Linear(NormalizedBoolLinear {
terms: construct_terms(&[
(e, 4),
(b, 2),
(g, 1),
(!d, 1),
(!a, 1),
(!f, 1),
(!c, 1)
]),
cmp: LimitComp::LessEq,
k: PosCoeff::new(4),
}))
);
assert_eq!(cnf.num_clauses(), 0);
}
#[test]
fn aggregator_neg_coeff() {
let mut cnf = Cnf::default();
let (a, b, c) = cnf.new_lits();
assert_eq!(
BoolLinAggregator::default().aggregate(
&mut cnf,
&BoolLinear::new(
BoolLinExp::from_slices(&[2, 3, -2], &[a, b, c]),
Comparator::LessEq,
2
)
),
Ok(BoolLinVariant::Linear(NormalizedBoolLinear {
terms: construct_terms(&[(a, 2), (b, 3), (!c, 2)]),
cmp: LimitComp::LessEq,
k: PosCoeff::new(4),
}))
);
assert_eq!(
BoolLinAggregator::default().aggregate(
&mut cnf,
&BoolLinear::new(
BoolLinExp::from_slices(&[-1, -1, -1], &[a, b, c]),
Comparator::LessEq,
-2,
)
),
Ok(BoolLinVariant::CardinalityOne(CardinalityOne {
lits: vec![!a, !b, !c],
cmp: LimitComp::LessEq
}))
);
assert_eq!(
BoolLinAggregator::default().aggregate(
&mut cnf,
&BoolLinear::new(
BoolLinExp::from_slices(&[-1, -2, -3], &[a, b, c]),
Comparator::LessEq,
-2,
)
),
Ok(BoolLinVariant::Linear(NormalizedBoolLinear {
terms: construct_terms(&[(!a, 1), (!b, 2), (!c, 3)]),
cmp: LimitComp::LessEq,
k: PosCoeff::new(4),
}))
);
let mut cnf = Cnf::default();
let (a, b, c, d, e, f) = cnf.new_lits();
assert_eq!(
BoolLinAggregator::default().aggregate(
&mut cnf,
&BoolLinear::new(
BoolLinExp::default()
.add_choice(&[(a, -1), (b, -3), (c, -4)])
.add_choice(&[(d, -2), (e, -3), (f, -5)]),
Comparator::LessEq,
-4,
)
),
Ok(BoolLinVariant::Linear(NormalizedBoolLinear {
terms: vec![
Part::Amo(vec![
(a, PosCoeff::new(3)),
(b, PosCoeff::new(1)),
(Lit(NonZeroI32::new(7).unwrap()), PosCoeff::new(4))
]),
Part::Amo(vec![
(d, PosCoeff::new(3)),
(e, PosCoeff::new(2)),
(Lit(NonZeroI32::new(8).unwrap()), PosCoeff::new(5))
]),
],
cmp: LimitComp::LessEq,
k: PosCoeff::new(5),
}))
);
let mut cnf = Cnf::default();
let (a, b, c, d, e, f) = cnf.new_lits();
assert_eq!(
BoolLinAggregator::default().aggregate(
&mut cnf,
&BoolLinear::new(
BoolLinExp::default().add_chain(&[
(a, 1),
(b, -3),
(c, -2),
(d, 2),
(e, 5),
(f, -3)
]),
Comparator::LessEq,
3
)
),
Ok(BoolLinVariant::Linear(NormalizedBoolLinear {
terms: vec![
Part::Ic(vec![
(a, PosCoeff::new(1)),
(d, PosCoeff::new(2)),
(e, PosCoeff::new(5))
]),
Part::Ic(vec![
(!f, PosCoeff::new(3)),
(!c, PosCoeff::new(2)),
(!b, PosCoeff::new(3))
]),
],
cmp: LimitComp::LessEq,
k: PosCoeff::new(11),
}))
);
let mut cnf = Cnf::default();
let (a, b, c, d, e, f) = cnf.new_lits();
assert_eq!(
BoolLinAggregator::default().aggregate(
&mut cnf,
&BoolLinear::new(
BoolLinExp::default()
.add_choice(&[(a, 1), (b, 2), (c, 3), (d, 4)])
.add_choice(&[(e, 1), (f, 3)]),
Comparator::GreaterEq,
3,
)
),
Ok(BoolLinVariant::Linear(NormalizedBoolLinear {
terms: vec![
Part::Amo(vec![
(a, PosCoeff::new(3)),
(b, PosCoeff::new(2)),
(c, PosCoeff::new(1)),
(Lit(NonZeroI32::new(7).unwrap()), PosCoeff::new(4))
]),
Part::Amo(vec![
(e, PosCoeff::new(2)),
(Lit(NonZeroI32::new(8).unwrap()), PosCoeff::new(3))
]),
],
cmp: LimitComp::LessEq,
k: PosCoeff::new(4), }))
);
let mut cnf = Cnf::default();
let (a, b, c, d, e, f) = cnf.new_lits();
assert_eq!(
BoolLinAggregator::default().aggregate(
&mut cnf,
&BoolLinear::new(
BoolLinExp::default()
.add_chain(&[(a, 1), (b, 1), (c, 1), (d, 1)])
.add_chain(&[(e, 1), (f, 2)]),
Comparator::GreaterEq,
3,
)
),
Ok(BoolLinVariant::Linear(NormalizedBoolLinear {
terms: vec![
Part::Ic(vec![
(!d, PosCoeff::new(1)),
(!c, PosCoeff::new(1)),
(!b, PosCoeff::new(1)),
(!a, PosCoeff::new(1)),
]),
Part::Ic(vec![(!f, PosCoeff::new(2)), (!e, PosCoeff::new(1))]),
],
cmp: LimitComp::LessEq,
k: PosCoeff::new(4),
}))
);
let mut cnf = Cnf::default();
let (a, b, c) = cnf.new_lits();
assert_eq!(
BoolLinAggregator::default().aggregate(
&mut cnf,
&BoolLinear::new(
BoolLinExp::default().add_bounded_log_encoding(&[(a, 1), (b, 2), (c, 4)], 0, 3),
Comparator::LessEq,
5,
)
),
Ok(BoolLinVariant::Linear(NormalizedBoolLinear {
terms: vec![Part::Dom(
vec![
(a, PosCoeff::new(1)),
(b, PosCoeff::new(2)),
(c, PosCoeff::new(4))
],
PosCoeff::new(0),
PosCoeff::new(7)
),],
cmp: LimitComp::LessEq,
k: PosCoeff::new(5),
}))
);
let mut cnf = Cnf::default();
let (a, b, c, d, e) = cnf.new_lits();
assert_eq!(
BoolLinAggregator::default().aggregate(
&mut cnf,
&BoolLinear::new(
BoolLinExp::default()
.add_bounded_log_encoding(&[(a, 1), (b, 2), (c, 4)], 0, 5)
.add_bounded_log_encoding(&[(d, 3), (e, 6)], 0, 2),
Comparator::GreaterEq,
3,
)
),
Ok(BoolLinVariant::Linear(NormalizedBoolLinear {
terms: vec![
Part::Dom(
vec![
(!a, PosCoeff::new(1)),
(!b, PosCoeff::new(2)),
(!c, PosCoeff::new(4))
],
PosCoeff::new(2),
PosCoeff::new(7),
),
Part::Dom(
vec![(!d, PosCoeff::new(3)), (!e, PosCoeff::new(6))],
PosCoeff::new(7),
PosCoeff::new(9),
),
],
cmp: LimitComp::LessEq,
k: PosCoeff::new(13),
}))
);
}
#[test]
fn aggregator_sort_same_coefficients() {
let mut cnf = Cnf::default();
let (a, b, c, d) = cnf.new_lits();
assert_eq!(
BoolLinAggregator::default()
.sort_same_coefficients(SortedEncoder::default(), 2)
.aggregate(
&mut cnf,
&BoolLinear::new(
BoolLinExp::from_slices(&[3, 3, 5, 3], &[a, b, d, c]),
Comparator::LessEq,
10
)
),
Ok(BoolLinVariant::Linear(NormalizedBoolLinear {
terms: vec![
Part::Ic(vec![
(Lit(NonZeroI32::new(5).unwrap()), PosCoeff::new(3)),
(Lit(NonZeroI32::new(6).unwrap()), PosCoeff::new(3)),
(Lit(NonZeroI32::new(7).unwrap()), PosCoeff::new(3))
]),
Part::Amo(vec![(d, PosCoeff::new(5))]),
],
cmp: LimitComp::LessEq,
k: PosCoeff::new(10),
}))
);
}
#[test]
fn aggregator_sort_same_coefficients_using_minimal_chain() {
let mut cnf = Cnf::default();
let vars = cnf.new_var_range(5).iter_lits().collect_vec();
assert_eq!(
BoolLinAggregator::default()
.sort_same_coefficients(SortedEncoder::default(), 2)
.aggregate(
&mut cnf,
&BoolLinear::new(
BoolLinExp::from_slices(&[5, 5, 5, 5, 4], &vars),
Comparator::LessEq,
12 )
),
Ok(BoolLinVariant::Linear(NormalizedBoolLinear {
terms: vec![
Part::Amo(vec![(*vars.last().unwrap(), PosCoeff::new(4))]),
Part::Ic(vec![
(Lit(NonZeroI32::new(6).unwrap()), PosCoeff::new(5)),
(Lit(NonZeroI32::new(7).unwrap()), PosCoeff::new(5))
]),
],
cmp: LimitComp::LessEq,
k: PosCoeff::new(12),
}))
);
}
#[test]
fn aggregator_unsat() {
let mut db = Cnf::default();
let vars = db.new_var_range(3).iter_lits().collect_vec();
assert_eq!(
BoolLinAggregator::default().aggregate(
&mut db,
&BoolLinear::new(
BoolLinExp::from_slices(&[1, 2, 2], &vars),
Comparator::Equal,
6
)
),
Err(Unsatisfiable)
);
assert_eq!(
BoolLinAggregator::default().aggregate(
&mut db,
&BoolLinear::new(
BoolLinExp::from_slices(&[1, 2, 2], &vars),
Comparator::GreaterEq,
6,
)
),
Err(Unsatisfiable)
);
assert_eq!(
BoolLinAggregator::default().aggregate(
&mut db,
&BoolLinear::new(
BoolLinExp::from_slices(&[1, 2, 2], &vars),
Comparator::LessEq,
-1
)
),
Err(Unsatisfiable)
);
assert_eq!(
BoolLinAggregator::default().aggregate(
&mut db,
&BoolLinear::new(
BoolLinExp::from_slices(&[4, 4, 4], &vars),
Comparator::Equal,
6
)
),
Err(Unsatisfiable)
);
}
pub(crate) fn construct_terms<L: Into<Lit> + Clone>(terms: &[(L, Coeff)]) -> Vec<Part> {
terms
.iter()
.map(|(lit, coef)| Part::Amo(vec![(lit.clone().into(), PosCoeff::new(*coef))]))
.collect()
}
#[test]
fn encoders() {
let mut cnf = Cnf::default();
let (a, b, c, d) = cnf.new_lits();
PairwiseEncoder::default()
.encode(
&mut cnf,
&CardinalityOne {
lits: vec![a, b],
cmp: LimitComp::LessEq,
},
)
.unwrap();
PairwiseEncoder::default()
.encode(
&mut cnf,
&CardinalityOne {
lits: vec![c, d],
cmp: LimitComp::LessEq,
},
)
.unwrap();
LinearEncoder::<StaticLinEncoder<AdderEncoder>>::default()
.encode(
&mut cnf,
&BoolLinear::new(
BoolLinExp::default()
.add_choice(&[(a, 7), (b, 10)])
.add_choice(&[(c, 4), (d, 4)]),
Comparator::LessEq,
9,
),
)
.unwrap();
assert_solutions(
&cnf,
vec![a, b, c, d],
&expect_file!["linear/adder/test_encoders.sol"],
);
}
#[test]
fn pb_encode() {
let mut cnf = Cnf::default();
let vars = cnf.new_var_range(4).iter_lits().collect_vec();
LinearEncoder::<StaticLinEncoder>::default()
.encode(
&mut cnf,
&BoolLinear::new(
BoolLinExp::from_slices(&[1, 1, 1, 2], &vars),
Comparator::LessEq,
1,
),
)
.unwrap();
assert_encoding(&cnf, &expect_file!["linear/adder/test_pb_encode.cnf"]);
assert_solutions(&cnf, vars, &expect_file!["linear/adder/test_pb_encode.sol"]);
}
#[test]
fn sort_same_coefficients_2() {
let mut db = Cnf::default();
let vars = db.new_var_range(5).iter_lits().collect_vec();
let mut agg = BoolLinAggregator::default();
let _ = agg.sort_same_coefficients(SortedEncoder::default(), 3);
let mut encoder = LinearEncoder::<StaticLinEncoder<TotalizerEncoder>>::default();
let _ = encoder.with_linear_aggregator(agg);
let con = BoolLinear::new(
BoolLinExp::from_slices(&[3, 3, 1, 1, 3], &vars),
Comparator::GreaterEq,
2,
);
encoder.encode(&mut db, &con).unwrap();
assert_checker(&db, &con);
}
impl PartialEq for BoolLinVariant {
fn eq(&self, other: &Self) -> bool {
let liteq =
|a: &Vec<Lit>, b: &Vec<Lit>| itertools::equal(a.iter().sorted(), b.iter().sorted());
let parteq = |a: &Vec<Part>, b: &Vec<Part>| {
itertools::equal(
a.iter().map(|p| p.iter().sorted().collect_vec()).sorted(),
b.iter().map(|p| p.iter().sorted().collect_vec()).sorted(),
)
};
match self {
BoolLinVariant::Linear(NormalizedBoolLinear { terms, cmp, k }) => {
if let BoolLinVariant::Linear(NormalizedBoolLinear {
terms: oterms,
cmp: oc,
k: l,
}) = other
{
cmp == oc && k == l && parteq(terms, oterms)
} else {
false
}
}
BoolLinVariant::Cardinality(Cardinality { lits, cmp, k }) => {
if let BoolLinVariant::Cardinality(Cardinality {
lits: olits,
cmp: oc,
k: l,
}) = other
{
cmp == oc && k == l && liteq(lits, olits)
} else {
false
}
}
BoolLinVariant::CardinalityOne(amo) => {
if let BoolLinVariant::CardinalityOne(oamo) = other {
liteq(&amo.lits, &oamo.lits)
} else {
false
}
}
BoolLinVariant::Trivial => {
matches!(other, BoolLinVariant::Trivial)
}
}
}
}
impl PartialEq for Part {
fn eq(&self, other: &Self) -> bool {
let term_eq = |a: &Vec<(_, _)>, b: &Vec<(_, _)>| {
itertools::equal(a.iter().sorted(), b.iter().sorted())
};
match self {
Part::Amo(terms) => {
if let Part::Amo(oterms) = other {
term_eq(terms, oterms)
} else {
false
}
}
Part::Ic(terms) => {
if let Part::Ic(oterms) = other {
term_eq(terms, oterms)
} else {
false
}
}
Part::Dom(terms, l, u) => {
if let Part::Dom(oterms, ol, ou) = other {
term_eq(terms, oterms) && l == ol && u == ou
} else {
false
}
}
}
}
}
impl PartialOrd for Part {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
let termcmp = |a: &Vec<(Lit, PosCoeff)>, b: &Vec<(Lit, PosCoeff)>| {
let cmp = a.len().cmp(&b.len());
if cmp != Ordering::Equal {
cmp
} else {
for (a, b) in a.iter().sorted().zip_eq(other.iter().sorted()) {
let cmp = a.0.cmp(&b.0);
if cmp != Ordering::Equal {
return cmp;
}
let cmp = a.1.cmp(&b.1);
if cmp != Ordering::Equal {
return cmp;
}
}
Ordering::Equal
}
};
Some(match self {
Part::Amo(terms) => {
if let Part::Amo(oterms) = other {
termcmp(terms, oterms)
} else {
Ordering::Less
}
}
Part::Ic(terms) => {
if let Part::Ic(oterms) = other {
termcmp(terms, oterms)
} else {
Ordering::Greater
}
}
Part::Dom(terms, _, _) => {
if let Part::Dom(oterms, _, _) = other {
termcmp(terms, oterms)
} else {
Ordering::Less
}
}
})
}
}
card_test_suite!(AdderEncoder::default());
card1_test_suite! {
adder_encoder_card1, crate::bool_linear::AdderEncoder::default()
}
linear_test_suite! {adder_encoder, crate::bool_linear::AdderEncoder::default()}
linear_test_suite! {bdd_encoder, crate::bool_linear::BddEncoder::default()}
linear_test_suite! {swc_encoder, crate::bool_linear::SwcEncoder::default()}
linear_test_suite!(
totalizer_encoder,
crate::bool_linear::TotalizerEncoder::default()
);
linear_test_suite!(
totalizer_encoder_prop_bounds,
crate::bool_linear::TotalizerEncoder::default()
.with_propagation(crate::integer::Consistency::Bounds)
);
linear_test_suite!(
totalizer_encoder_prop_doms,
crate::bool_linear::TotalizerEncoder::default()
.with_propagation(crate::integer::Consistency::Domain)
);
}