use super::bdd::{BddBuildError, Compiled, NODE_BUDGET, compile};
use super::ir::{Expr, Ty, VarId};
use super::randc::{self, RandcState, fingerprint_of};
use super::rng;
use super::sat::{SatLimits, SatSolver};
use biodivine_lib_bdd::{Bdd, BddValuation};
use core::fmt;
use num_bigint::BigUint;
use num_traits::{One, Zero};
use std::collections::{BTreeMap, BTreeSet, HashMap};
use std::sync::{Arc, Mutex};
pub type SatCount = BigUint;
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum SolveError {
Build(BddBuildError),
Unsatisfiable,
BadOrder {
var: VarId,
},
BadPlan {
what: &'static str,
},
SatTimeout,
SatUnsupported {
what: &'static str,
},
SatModelRejected,
SatSolverFailure {
what: &'static str,
},
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Weight {
pub num: BigUint,
pub den: BigUint,
}
impl Weight {
pub fn int(n: impl Into<BigUint>) -> Weight {
Weight {
num: n.into(),
den: BigUint::one(),
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Measure {
Flat,
Models,
Distinct(VarId),
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Weighted {
pub var: VarId,
pub weights: Vec<Weight>,
pub measure: Measure,
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct Group {
pub vars: Vec<VarId>,
pub weighted: Vec<Weighted>,
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct Plan {
pub groups: Vec<Group>,
pub randc: Vec<VarId>,
}
pub(crate) struct ResolvedWeighted<'a> {
pub(crate) var: usize,
pub(crate) weights: &'a [Weight],
pub(crate) measure: Measure,
}
pub(crate) type Resolved<'a> = (Vec<usize>, Vec<ResolvedWeighted<'a>>);
impl fmt::Display for SolveError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
SolveError::Build(e) => fmt::Display::fmt(e, f),
SolveError::Unsatisfiable => f.write_str("the constraints have no solution"),
SolveError::BadOrder { var } => write!(
f,
"solve order names random variable #{var} which is unknown or listed twice"
),
SolveError::BadPlan { what } => write!(f, "malformed solve plan: {what}"),
SolveError::SatTimeout => {
f.write_str("the SAT fallback did not finish within its time limit")
}
SolveError::SatUnsupported { what } => {
write!(f, "not supported by the SAT fallback: {what}")
}
SolveError::SatModelRejected => f.write_str(
"the SAT fallback returned an assignment the constraint evaluator rejects \
(an encoding bug)",
),
SolveError::SatSolverFailure { what } => write!(f, "the SAT fallback failed: {what}"),
}
}
}
impl std::error::Error for SolveError {}
impl From<BddBuildError> for SolveError {
fn from(e: BddBuildError) -> Self {
SolveError::Build(e)
}
}
static SOLVER_CACHE: Mutex<BTreeMap<(u64, u64), Solver>> = Mutex::new(BTreeMap::new());
const SOLVER_CACHE_CAP: usize = 512;
const SOLVER_CACHE_MAX_NODES: usize = 4096;
#[derive(Clone, Debug)]
pub struct Solver {
compiled: Compiled,
count: SatCount,
steps: Arc<Mutex<StepMemo>>,
}
#[derive(Debug)]
struct Step {
ones: Bdd,
ones_count: BigUint,
zeros: Option<Bdd>,
}
#[derive(Debug, Default)]
struct StepMemo {
plans: Vec<(Plan, HashMap<Vec<u32>, Step>)>,
}
const STEP_MEMO_CAP: usize = 100_000;
const STEP_MEMO_PLANS: usize = 4;
impl Solver {
pub fn new(
var_tys: &[Ty],
states: &[Option<BigUint>],
conjuncts: &[Expr],
) -> Result<Solver, SolveError> {
Solver::with_budget(var_tys, states, conjuncts, NODE_BUDGET)
}
pub fn with_budget(
var_tys: &[Ty],
states: &[Option<BigUint>],
conjuncts: &[Expr],
budget: usize,
) -> Result<Solver, SolveError> {
let digest = problem_digest(var_tys, states, conjuncts, budget);
Solver::with_digest(digest, var_tys, states, conjuncts, budget)
}
fn with_digest(
digest: (u64, u64),
var_tys: &[Ty],
states: &[Option<BigUint>],
conjuncts: &[Expr],
budget: usize,
) -> Result<Solver, SolveError> {
if let Some(s) = SOLVER_CACHE
.lock()
.unwrap_or_else(|p| p.into_inner())
.get(&digest)
{
return Ok(s.clone());
}
let compiled = compile(var_tys, states, conjuncts, budget)?;
let count = compiled.bdd.exact_cardinality();
let solver = Solver {
compiled,
count,
steps: Arc::default(),
};
if solver.compiled.bdd.size() <= SOLVER_CACHE_MAX_NODES {
let mut cache = SOLVER_CACHE.lock().unwrap_or_else(|p| p.into_inner());
if cache.len() >= SOLVER_CACHE_CAP {
cache.clear();
}
cache.insert(digest, solver.clone());
}
Ok(solver)
}
pub fn count(&self) -> &SatCount {
&self.count
}
pub fn compiled(&self) -> &Compiled {
&self.compiled
}
pub fn sample(&self, order: &[VarId], seed: &mut u64) -> Result<Vec<BigUint>, SolveError> {
self.sample_groups(&[order.to_vec()], seed)
}
pub fn sample_groups(
&self,
groups: &[Vec<VarId>],
seed: &mut u64,
) -> Result<Vec<BigUint>, SolveError> {
let plan = Plan {
groups: groups
.iter()
.map(|g| Group {
vars: g.clone(),
weighted: Vec::new(),
})
.collect(),
randc: Vec::new(),
};
self.sample_plan(&plan, &mut [], seed)
}
pub fn sample_plan(
&self,
plan: &Plan,
randc_states: &mut [RandcState],
seed: &mut u64,
) -> Result<Vec<BigUint>, SolveError> {
let layout = &self.compiled.layout;
let n = layout.tys.len();
let widths: Vec<usize> = layout.bits.iter().map(Vec::len).collect();
let full = resolve_plan(&widths, plan, randc_states.len())?;
let last = full.len() - 1;
if self.count.is_zero() {
return Err(SolveError::Unsatisfiable);
}
let mut cur = self.compiled.bdd.clone();
let mut out = vec![BigUint::zero(); n];
let mut memo_guard = self.steps.lock().unwrap_or_else(|p| p.into_inner());
let memo_all = &mut *memo_guard;
if memo_all.plans.iter().map(|(_, m)| m.len()).sum::<usize>() >= STEP_MEMO_CAP {
*memo_all = StepMemo::default();
}
let slot = match memo_all.plans.iter().position(|(p, _)| p == plan) {
Some(i) => i,
None => {
if memo_all.plans.len() >= STEP_MEMO_PLANS {
*memo_all = StepMemo::default();
}
memo_all.plans.push((plan.clone(), HashMap::new()));
memo_all.plans.len() - 1
}
};
let memo = &mut memo_all.plans[slot].1;
let mut trace: Vec<u32> = Vec::new();
for (k, &v) in plan.randc.iter().enumerate() {
let v = v as usize;
let others: Vec<_> = (0..n)
.filter(|&o| o != v)
.flat_map(|o| layout.bits[o].iter().copied())
.collect();
let base = self.compiled.bdd.exists(&others);
let now = cur.exists(&others);
let w = layout.bits[v].len();
let holds = |f: &Bdd, x: u32| -> bool {
let mut val = BddValuation::all_false(layout.num_bits() as u16);
for (i, bv) in layout.bits[v].iter().enumerate() {
val.set_value(*bv, (x >> i) & 1 == 1);
}
f.eval_in(&val)
};
let fp = fingerprint_of(&base.to_bytes());
let picked = randc::pick(
&mut randc_states[k],
fp,
&mut || (0..1u32 << w).filter(|&x| holds(&base, x)).collect(),
&mut |x| holds(&now, x),
seed,
)
.ok_or(SolveError::Unsatisfiable)?;
trace.push((1 << 31) | picked);
for (i, bv) in layout.bits[v].iter().enumerate() {
let bit = (picked >> i) & 1 == 1;
out[v].set_bit(i as u64, bit);
cur = cur.var_select(*bv, bit);
}
}
for (gi, (group, weighted)) in full.iter().enumerate() {
trace.push(u32::MAX - gi as u32);
let later: Vec<_> = full[gi + 1..]
.iter()
.flat_map(|(vars, wtd)| vars.iter().chain(wtd.iter().map(|w| &w.var)))
.flat_map(|&v| layout.bits[v].iter().copied())
.collect();
let mut proj = if later.is_empty() {
cur.clone()
} else {
cur.exists(&later)
};
let mut drawn: Vec<usize> = Vec::new();
for wv in weighted {
let v = wv.var;
let bits = &layout.bits[v];
let fixing = |code: usize| -> Vec<_> {
bits.iter()
.enumerate()
.map(|(i, bv)| (*bv, (code >> i) & 1 == 1))
.collect()
};
let all_but = |keep: usize| -> Vec<_> {
(0..n)
.filter(|&o| o != keep)
.flat_map(|o| layout.bits[o].iter().copied())
.collect()
};
let subject_rest = match wv.measure {
Measure::Distinct(s) => Some(all_but(s as usize)),
Measure::Flat | Measure::Models => None,
};
let mut feasible: Vec<(usize, BigUint, &BigUint)> = Vec::new();
for (c, wt) in wv.weights.iter().enumerate() {
if wt.num.is_zero() {
continue;
}
let pc = proj.restrict(&fixing(c));
if pc.is_false() {
continue;
}
let m = match (&wv.measure, &subject_rest) {
(Measure::Models, _) => pc.exact_cardinality(),
(_, Some(rest)) => pc.exists(rest).exact_cardinality(),
_ => BigUint::one(),
};
feasible.push((c, &wt.num * m, &wt.den));
}
let code = draw_weighted(&feasible, seed)?;
trace.push((1 << 31) | code as u32);
let fix = fixing(code);
proj = proj.restrict(&fix);
if gi < last {
cur = cur.restrict(&fix);
}
for (i, _) in bits.iter().enumerate() {
out[v].set_bit(i as u64, (code >> i) & 1 == 1);
}
if let (Measure::Distinct(s), Some(rest)) = (wv.measure, &subject_rest) {
let s = s as usize;
let mut pj = proj.exists(rest);
let mut remaining = pj.exact_cardinality();
for i in (0..layout.bits[s].len()).rev() {
let bv = layout.bits[s][i];
let ones = pj.var_select(bv, true);
let c1 = ones.exact_cardinality();
let bit = rng::below(seed, &remaining) < c1;
if bit {
pj = ones;
remaining = c1;
} else {
remaining -= &c1;
pj = pj.var_select(bv, false);
}
proj = proj.var_select(bv, bit);
trace.push((bv.to_index() as u32) << 1 | u32::from(bit));
if gi < last {
cur = cur.var_select(bv, bit);
}
out[s].set_bit(i as u64, bit);
}
drawn.push(s);
}
}
let mut remaining = proj.exact_cardinality();
let support = (gi == last).then(|| proj.support_set());
let mut skipped = 0u64;
for &v in group {
if drawn.contains(&v) {
continue;
}
for i in (0..layout.bits[v].len()).rev() {
let bv = layout.bits[v][i];
if support.as_ref().is_some_and(|s| !s.contains(&bv)) {
let half = &remaining >> 1u32;
let bit = rng::below(seed, &remaining) < half;
remaining = half;
skipped += 1;
if bit {
out[v].set_bit(i as u64, true);
}
continue;
}
if !memo.contains_key(trace.as_slice()) {
let ones = proj.var_select(bv, true);
let ones_count = ones.exact_cardinality();
memo.insert(
trace.clone(),
Step {
ones,
ones_count,
zeros: None,
},
);
}
let step = memo.get_mut(trace.as_slice()).expect("just inserted");
let c1 = &step.ones_count >> skipped;
let zeros_count = &remaining - &c1;
let pick = rng::below(seed, &remaining);
let bit = pick < c1;
if bit {
out[v].set_bit(i as u64, true);
proj = step.ones.clone();
remaining = c1;
} else {
proj = step
.zeros
.get_or_insert_with(|| proj.var_select(bv, false))
.clone();
remaining = zeros_count;
}
trace.push((bv.to_index() as u32) << 1 | u32::from(bit));
if gi < last {
cur = cur.var_select(bv, bit);
}
}
}
}
debug_assert!(self.compiled.accepts(&out), "sampled a non-model");
Ok(out)
}
}
pub(crate) fn resolve_plan<'a>(
widths: &[usize],
plan: &'a Plan,
randc_states: usize,
) -> Result<Vec<Resolved<'a>>, SolveError> {
let n = widths.len();
if randc_states != plan.randc.len() {
return Err(SolveError::BadPlan {
what: "one randc state per randc variable",
});
}
for &v in &plan.randc {
let w = widths.get(v as usize).copied().unwrap_or(0);
if w > randc::MAX_RANDC_WIDTH as usize {
return Err(SolveError::BadPlan {
what: "a randc variable is wider than the permutation engine supports",
});
}
}
let mut seen = vec![false; n];
let mut mark = |v: VarId| -> Result<usize, SolveError> {
let i = v as usize;
if i >= n || seen[i] {
return Err(SolveError::BadOrder { var: v });
}
seen[i] = true;
Ok(i)
};
for &v in &plan.randc {
mark(v)?;
}
let mut full: Vec<Resolved<'a>> = Vec::with_capacity(plan.groups.len().max(1));
for g in &plan.groups {
let mut weighted = Vec::with_capacity(g.weighted.len());
for w in &g.weighted {
let i = mark(w.var)?;
let width = widths[i];
if width >= 32 || w.weights.len() > 1usize << width {
return Err(SolveError::BadPlan {
what: "a weighted variable has more weights than values",
});
}
if w.weights.iter().any(|x| x.den.is_zero()) {
return Err(SolveError::BadPlan {
what: "a weight has a zero denominator",
});
}
weighted.push(ResolvedWeighted {
var: i,
weights: w.weights.as_slice(),
measure: w.measure,
});
}
let mut members = Vec::with_capacity(g.vars.len());
for &v in &g.vars {
members.push(mark(v)?);
}
for w in &weighted {
if let Measure::Distinct(s) = w.measure
&& !members.contains(&(s as usize))
{
return Err(SolveError::BadPlan {
what: "a weighted variable's subject is not a uniform member of its group",
});
}
}
full.push((members, weighted));
}
if full.is_empty() {
full.push((Vec::new(), Vec::new()));
}
let last = full.len() - 1;
full[last].0.extend((0..n).filter(|i| !seen[*i]));
Ok(full)
}
pub(crate) fn draw_weighted(
feasible: &[(usize, BigUint, &BigUint)],
seed: &mut u64,
) -> Result<usize, SolveError> {
let common = feasible
.iter()
.fold(BigUint::one(), |acc, (_, _, den)| acc * *den);
let scaled: Vec<(usize, BigUint)> = feasible
.iter()
.map(|(c, num, den)| (*c, num * (&common / *den)))
.collect();
let total: BigUint = scaled.iter().map(|(_, w)| w.clone()).sum();
if total.is_zero() {
return Err(SolveError::Unsatisfiable);
}
let mut pick = rng::below(seed, &total);
let mut code = scaled[scaled.len() - 1].0;
for (c, w) in &scaled {
if pick < *w {
code = *c;
break;
}
pick -= w;
}
Ok(code)
}
#[derive(Debug)]
pub enum Engine {
Exact(Solver),
Sat(SatSolver),
}
static BDD_TOO_LARGE: Mutex<BTreeSet<(u64, u64)>> = Mutex::new(BTreeSet::new());
const BDD_TOO_LARGE_CAP: usize = 4096;
fn problem_digest(
var_tys: &[Ty],
states: &[Option<BigUint>],
conjuncts: &[Expr],
budget: usize,
) -> (u64, u64) {
use std::hash::{Hash as _, Hasher as _};
let mut lo = std::collections::hash_map::DefaultHasher::new();
let mut hi = std::collections::hash_map::DefaultHasher::new();
0x1234_5678_9abc_def1_u64.hash(&mut hi);
for h in [&mut lo, &mut hi] {
budget.hash(h);
var_tys.hash(h);
states.hash(h);
conjuncts.hash(h);
}
(lo.finish(), hi.finish())
}
impl Engine {
pub fn new(
var_tys: &[Ty],
states: &[Option<BigUint>],
conjuncts: &[Expr],
) -> Result<Engine, SolveError> {
Engine::with_limits(
var_tys,
states,
conjuncts,
NODE_BUDGET,
SatLimits::default(),
)
}
pub fn with_limits(
var_tys: &[Ty],
states: &[Option<BigUint>],
conjuncts: &[Expr],
budget: usize,
limits: SatLimits,
) -> Result<Engine, SolveError> {
let digest = problem_digest(var_tys, states, conjuncts, budget);
let known_large = BDD_TOO_LARGE
.lock()
.unwrap_or_else(|p| p.into_inner())
.contains(&digest);
if !known_large {
match Solver::with_digest(digest, var_tys, states, conjuncts, budget) {
Ok(s) => return Ok(Engine::Exact(s)),
Err(SolveError::Build(BddBuildError::TooLarge { .. })) => {
let mut seen = BDD_TOO_LARGE.lock().unwrap_or_else(|p| p.into_inner());
if seen.len() >= BDD_TOO_LARGE_CAP {
seen.clear();
}
seen.insert(digest);
}
Err(e) => return Err(e),
}
}
SatSolver::with_limits(var_tys, states, conjuncts, limits).map(Engine::Sat)
}
pub fn is_sat(&self) -> bool {
matches!(self, Engine::Sat(_))
}
pub fn sample_plan(
&self,
plan: &Plan,
randc_states: &mut [RandcState],
seed: &mut u64,
) -> Result<Vec<BigUint>, SolveError> {
match self {
Engine::Exact(s) => s.sample_plan(plan, randc_states, seed),
Engine::Sat(s) => s.sample_plan(plan, randc_states, seed),
}
}
}
pub fn solve(
var_tys: &[Ty],
states: &[Option<BigUint>],
conjuncts: &[Expr],
order: &[VarId],
seed: &mut u64,
) -> Result<Vec<BigUint>, SolveError> {
Solver::new(var_tys, states, conjuncts)?.sample(order, seed)
}
pub fn solve_grouped(
var_tys: &[Ty],
states: &[Option<BigUint>],
conjuncts: &[Expr],
groups: &[Vec<VarId>],
seed: &mut u64,
) -> Result<Vec<BigUint>, SolveError> {
Solver::new(var_tys, states, conjuncts)?.sample_groups(groups, seed)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::constraints::ir::BinOp;
fn bin(op: BinOp, a: Expr, b: Expr, t: Ty) -> Expr {
Expr::Binary(op, Box::new(a), Box::new(b), t)
}
fn table_18_2() -> Solver {
let s = Expr::Var(0, Ty::BIT);
let d = Expr::Var(1, Ty::unsigned(8));
let c = bin(
BinOp::LogImpl,
s,
bin(BinOp::Eq, d, Expr::constant(Ty::unsigned(8), 0u8), Ty::BIT),
Ty::BIT,
);
Solver::new(&[Ty::BIT, Ty::unsigned(8)], &[], &[c]).expect("compiles")
}
fn tally(sv: &Solver, groups: &[Vec<VarId>], n: u32, seed0: u64) -> (u32, u32) {
let mut seed = seed0;
let (mut s1, mut d0) = (0, 0);
for _ in 0..n {
let v = sv.sample_groups(groups, &mut seed).expect("sat");
s1 += u32::from(!v[0].is_zero());
d0 += u32::from(v[1].is_zero());
}
(s1, d0)
}
#[test]
fn solve_s_before_d_is_table_18_2() {
let sv = table_18_2();
assert_eq!(sv.count(), &BigUint::from(257u32));
let n = 40_000;
let (s1, d0) = tally(&sv, &[vec![0], vec![1]], n, 11);
assert!((19_400..20_600).contains(&s1), "s==1: {s1}");
assert!((19_500..20_700).contains(&d0), "d==0: {d0}");
}
#[test]
fn unordered_is_table_18_1() {
let sv = table_18_2();
let n = 40_000;
for groups in [vec![vec![0, 1]], vec![]] {
let (s1, d0) = tally(&sv, &groups, n, 12);
assert!((80..235).contains(&s1), "s==1: {s1}");
assert!((235..390).contains(&d0), "d==0: {d0}");
}
}
#[test]
fn solving_d_first_makes_d_uniform_over_its_feasible_values() {
let sv = table_18_2();
let n = 51_200;
let (s1, d0) = tally(&sv, &[vec![1], vec![0]], n, 13);
assert!((130..270).contains(&d0), "d==0: {d0}");
assert!((60..140).contains(&s1), "s==1: {s1}");
}
#[test]
fn a_variable_the_groups_omit_is_solved_with_the_last_group() {
let sv = table_18_2();
let (s1, _) = tally(&sv, &[vec![0]], 40_000, 14);
assert!((80..235).contains(&s1), "s==1: {s1}");
}
#[test]
fn one_group_is_exactly_the_per_bit_sample() {
let sv = table_18_2();
let (mut a, mut b) = (5u64, 5u64);
for _ in 0..50 {
assert_eq!(
sv.sample(&[1, 0], &mut a).unwrap(),
sv.sample_groups(&[vec![1, 0]], &mut b).unwrap()
);
}
assert_eq!(a, b, "the same draws were consumed");
}
#[test]
fn a_repeated_or_unknown_variable_is_a_bad_order() {
let sv = table_18_2();
let mut seed = 1u64;
assert_eq!(
sv.sample_groups(&[vec![0], vec![0]], &mut seed),
Err(SolveError::BadOrder { var: 0 })
);
assert_eq!(
sv.sample_groups(&[vec![7]], &mut seed),
Err(SolveError::BadOrder { var: 7 })
);
}
#[test]
fn an_unsatisfiable_problem_consumes_no_draws_when_grouped() {
let x = Expr::Var(0, Ty::unsigned(4));
let c = bin(BinOp::Lt, x, Expr::constant(Ty::unsigned(4), 0u8), Ty::BIT);
let sv = Solver::new(&[Ty::unsigned(4)], &[], &[c]).unwrap();
let mut seed = 9u64;
assert_eq!(
sv.sample_groups(&[vec![0]], &mut seed),
Err(SolveError::Unsatisfiable)
);
assert_eq!(seed, 9);
}
#[test]
fn three_levels_are_each_uniform_over_their_feasible_values() {
let t = Ty::unsigned(3);
let (a, b, c) = (Expr::Var(0, t), Expr::Var(1, t), Expr::Var(2, t));
let conj = vec![
bin(BinOp::Lt, a, b.clone(), Ty::BIT),
bin(BinOp::Lt, b, c, Ty::BIT),
];
let sv = Solver::new(&[t, t, t], &[], &conj).unwrap();
let n = 36_000u32;
let mut seed = 21u64;
let mut hist = [0u32; 8];
for _ in 0..n {
let v = sv
.sample_groups(&[vec![0], vec![1], vec![2]], &mut seed)
.unwrap();
assert!(v[0] < v[1] && v[1] < v[2], "{v:?}");
hist[usize::try_from(v[0].clone()).unwrap()] += 1;
}
for (i, h) in hist.iter().enumerate() {
if i < 6 {
assert!((5_500..6_500).contains(h), "a=={i}: {hist:?}");
} else {
assert_eq!(*h, 0, "a=={i} is infeasible: {hist:?}");
}
}
}
fn plan(randc: Vec<VarId>, groups: Vec<Group>) -> Plan {
Plan { groups, randc }
}
fn uniform(vars: Vec<VarId>) -> Group {
Group {
vars,
weighted: Vec::new(),
}
}
#[test]
fn a_randc_variable_cycles_through_every_value_across_calls() {
let t = Ty::unsigned(2);
let sv = Solver::new(&[t, Ty::BIT], &[], &[]).unwrap();
let p = plan(vec![0], vec![uniform(vec![1])]);
let mut states = vec![RandcState::default()];
let mut seed = 31u64;
for cycle in 0..40 {
let mut got: Vec<u32> = (0..4)
.map(|_| {
let v = sv.sample_plan(&p, &mut states, &mut seed).unwrap();
u32::try_from(v[0].clone()).unwrap()
})
.collect();
got.sort_unstable();
assert_eq!(got, vec![0, 1, 2, 3], "cycle {cycle}");
}
}
#[test]
fn randc_variables_are_solved_before_every_rand_group() {
let t = Ty::unsigned(2);
let c = bin(
BinOp::LogImpl,
Expr::Var(1, Ty::BIT),
bin(BinOp::Eq, Expr::Var(0, t), Expr::constant(t, 0u8), Ty::BIT),
Ty::BIT,
);
let sv = Solver::new(&[t, Ty::BIT], &[], &[c]).unwrap();
let p = plan(vec![0], vec![uniform(vec![1])]);
let mut states = vec![RandcState::default()];
let mut seed = 32u64;
let (mut v1_ones, mut v0_zero) = (0u32, 0u32);
for _ in 0..4000 {
let v = sv.sample_plan(&p, &mut states, &mut seed).unwrap();
v0_zero += u32::from(v[0].is_zero());
if !v[1].is_zero() {
v1_ones += 1;
assert!(v[0].is_zero(), "{v:?}");
}
}
assert_eq!(v0_zero, 1000, "a randc value occurs once per cycle");
assert!((400..600).contains(&v1_ones), "v1==1: {v1_ones}");
}
#[test]
fn a_randc_variable_whose_legal_set_changes_restarts_its_cycle() {
let t = Ty::unsigned(3);
let c = bin(BinOp::Lt, Expr::Var(0, t), Expr::State(0, t), Ty::BIT);
let p = plan(vec![0], vec![]);
let mut states = vec![RandcState::default()];
let mut seed = 33u64;
let wide =
Solver::new(&[t], &[Some(BigUint::from(4u8))], std::slice::from_ref(&c)).unwrap();
let first = wide.sample_plan(&p, &mut states, &mut seed).unwrap();
assert_eq!(states[0].remaining.len(), 3);
let narrow = Solver::new(&[t], &[Some(BigUint::from(2u8))], &[c]).unwrap();
let mut seen = std::collections::BTreeSet::new();
for _ in 0..2 {
let v = narrow.sample_plan(&p, &mut states, &mut seed).unwrap();
seen.insert(u32::try_from(v[0].clone()).unwrap());
}
assert_eq!(seen, [0, 1].into_iter().collect());
assert!(first[0] < BigUint::from(4u8));
}
#[test]
fn equal_functions_serialise_to_equal_bytes() {
let t = Ty::unsigned(3);
let x = || Expr::Var(0, t);
let k = |n: u8| Expr::constant(t, n);
let a = bin(BinOp::Lt, x(), k(2), Ty::BIT);
let b = bin(
BinOp::LogOr,
bin(BinOp::Eq, x(), k(0), Ty::BIT),
bin(BinOp::Eq, x(), k(1), Ty::BIT),
Ty::BIT,
);
let c = bin(BinOp::Lt, x(), k(3), Ty::BIT);
let bytes = |e: Expr| {
Solver::new(&[t], &[], &[e])
.unwrap()
.compiled()
.bdd
.to_bytes()
};
assert_eq!(bytes(a.clone()), bytes(b));
assert_ne!(bytes(a), bytes(c));
}
#[test]
fn a_weighted_variable_is_drawn_in_proportion_to_its_weights() {
let t = Ty::unsigned(2);
let sv = Solver::new(&[t], &[], &[]).unwrap();
let p = plan(
vec![],
vec![Group {
vars: vec![],
weighted: vec![Weighted {
var: 0,
weights: ints(&[1, 3, 0]),
measure: Measure::Flat,
}],
}],
);
let mut seed = 34u64;
let mut hist = [0u32; 4];
for _ in 0..40_000 {
let v = sv.sample_plan(&p, &mut [], &mut seed).unwrap();
hist[usize::try_from(v[0].clone()).unwrap()] += 1;
}
assert_eq!(hist[2] + hist[3], 0, "{hist:?}");
assert!((9_600..10_400).contains(&hist[0]), "{hist:?}");
assert!((29_600..30_400).contains(&hist[1]), "{hist:?}");
}
#[test]
fn a_weighted_variable_only_takes_feasible_codes() {
let t = Ty::unsigned(2);
let c = bin(BinOp::Ne, Expr::Var(0, t), Expr::constant(t, 1u8), Ty::BIT);
let sv = Solver::new(&[t], &[], &[c]).unwrap();
let p = plan(
vec![],
vec![Group {
vars: vec![],
weighted: vec![Weighted {
var: 0,
weights: ints(&[1, 100, 1, 0]),
measure: Measure::Flat,
}],
}],
);
let mut seed = 35u64;
let mut hist = [0u32; 4];
for _ in 0..4_000 {
let v = sv.sample_plan(&p, &mut [], &mut seed).unwrap();
hist[usize::try_from(v[0].clone()).unwrap()] += 1;
}
assert_eq!(hist[1] + hist[3], 0, "{hist:?}");
assert!((1_800..2_200).contains(&hist[0]), "{hist:?}");
}
#[test]
fn weights_with_no_feasible_code_are_unsatisfiable() {
let t = Ty::unsigned(2);
let sv = Solver::new(&[t], &[], &[]).unwrap();
let p = plan(
vec![],
vec![Group {
vars: vec![],
weighted: vec![Weighted {
var: 0,
weights: ints(&[0; 4]),
measure: Measure::Flat,
}],
}],
);
let mut seed = 36u64;
assert_eq!(
sv.sample_plan(&p, &mut [], &mut seed),
Err(SolveError::Unsatisfiable)
);
}
#[test]
fn malformed_plans_are_rejected_loudly() {
let t = Ty::unsigned(2);
let sv = Solver::new(&[t, Ty::unsigned(17)], &[], &[]).unwrap();
let mut seed = 37u64;
assert!(matches!(
sv.sample_plan(&plan(vec![0], vec![]), &mut [], &mut seed),
Err(SolveError::BadPlan { .. })
));
assert!(matches!(
sv.sample_plan(
&plan(vec![1], vec![]),
&mut [RandcState::default()],
&mut seed
),
Err(SolveError::BadPlan { .. })
));
let too_many = Group {
vars: vec![],
weighted: vec![Weighted {
var: 0,
weights: ints(&[1; 5]),
measure: Measure::Flat,
}],
};
assert!(matches!(
sv.sample_plan(&plan(vec![], vec![too_many]), &mut [], &mut seed),
Err(SolveError::BadPlan { .. })
));
assert_eq!(
sv.sample_plan(
&plan(vec![0], vec![uniform(vec![0])]),
&mut [RandcState::default()],
&mut seed
),
Err(SolveError::BadOrder { var: 0 })
);
}
fn ints(ws: &[u32]) -> Vec<Weight> {
ws.iter().map(|&w| Weight::int(w)).collect()
}
fn is_code(k: u32, kt: Ty, i: u64) -> Expr {
bin(BinOp::Eq, Expr::Var(k, kt), Expr::constant(kt, i), Ty::BIT)
}
fn in_range(x: &Expr, lo: u64, hi: u64) -> Expr {
let t = x.ty();
bin(
BinOp::LogAnd,
bin(BinOp::Ge, x.clone(), Expr::constant(t, lo), Ty::BIT),
bin(BinOp::Le, x.clone(), Expr::constant(t, hi), Ty::BIT),
Ty::BIT,
)
}
fn dist_conjunct(x: &Expr, items: &[(u64, u64)]) -> (Expr, Ty) {
let kt = Ty::unsigned(3);
let mut acc: Option<Expr> = None;
for (i, &(lo, hi)) in items.iter().enumerate() {
let term = bin(
BinOp::LogAnd,
is_code(1, kt, i as u64),
in_range(x, lo, hi),
Ty::BIT,
);
acc = Some(match acc {
None => term,
Some(p) => bin(BinOp::LogOr, p, term, Ty::BIT),
});
}
(acc.expect("an item"), kt)
}
fn dist_hist(
xt: Ty,
items: &[(u64, u64)],
weights: Vec<Weight>,
extra: &[Expr],
n: u32,
seed0: u64,
) -> std::collections::BTreeMap<u32, u32> {
let x = Expr::Var(0, xt);
let (c, kt) = dist_conjunct(&x, items);
let mut conj = vec![c];
conj.extend_from_slice(extra);
let sv = Solver::new(&[xt, kt], &[], &conj).unwrap();
let p = plan(
vec![],
vec![Group {
vars: vec![0],
weighted: vec![Weighted {
var: 1,
weights,
measure: Measure::Distinct(0),
}],
}],
);
let mut seed = seed0;
let mut hist = std::collections::BTreeMap::new();
for _ in 0..n {
let v = sv.sample_plan(&p, &mut [], &mut seed).unwrap();
let x = u32::try_from(v[0].clone()).unwrap();
*hist.entry(x).or_insert(0) += 1;
}
hist
}
#[test]
fn a_dist_with_an_excluded_value_renormalises_over_the_rest() {
let t = Ty::unsigned(9);
let ne = bin(
BinOp::Ne,
Expr::Var(0, t),
Expr::constant(t, 200u32),
Ty::BIT,
);
let h = dist_hist(
t,
&[(100, 100), (200, 200), (300, 300)],
ints(&[1, 2, 5]),
&[ne],
36_000,
41,
);
assert_eq!(
h.keys().copied().collect::<Vec<_>>(),
vec![100, 300],
"{h:?}"
);
assert!((5_600..6_400).contains(&h[&100]), "{h:?}");
}
#[test]
fn a_partly_excluded_range_item_keeps_only_its_feasible_values() {
let t = Ty::unsigned(8);
let gt = bin(
BinOp::Gt,
Expr::Var(0, t),
Expr::constant(t, 101u32),
Ty::BIT,
);
let h = dist_hist(
t,
&[(100, 102), (103, 103)],
ints(&[3, 1]),
&[gt],
40_000,
42,
);
assert_eq!(
h.keys().copied().collect::<Vec<_>>(),
vec![102, 103],
"{h:?}"
);
assert!((29_600..30_400).contains(&h[&102]), "{h:?}");
}
#[test]
fn overlapping_items_add_their_per_value_weights() {
let t = Ty::unsigned(4);
let h = dist_hist(t, &[(0, 3), (2, 5)], ints(&[1, 1]), &[], 48_000, 43);
for v in [0, 1, 4, 5] {
assert!((5_600..6_400).contains(&h[&v]), "{v}: {h:?}");
}
for v in [2, 3] {
assert!((11_500..12_500).contains(&h[&v]), "{v}: {h:?}");
}
assert_eq!(h.len(), 6, "{h:?}");
}
#[test]
fn a_range_weight_spread_with_a_denominator_is_per_value() {
let t = Ty::unsigned(4);
let w = vec![
Weight {
num: 4u32.into(),
den: 4u32.into(),
},
Weight::int(1u32),
];
let h = dist_hist(t, &[(0, 3), (7, 7)], w, &[], 40_000, 44);
assert_eq!(h.len(), 5, "{h:?}");
for (v, c) in &h {
assert!((7_550..8_450).contains(c), "{v}: {h:?}");
}
}
#[test]
fn the_dist_subject_is_uniform_over_its_values_not_over_completions() {
let t = Ty::unsigned(2);
let (x, y) = (Expr::Var(0, t), Expr::Var(2, t));
let (c, kt) = dist_conjunct(&x, &[(0, 3)]);
let le = bin(BinOp::Le, y, x, Ty::BIT);
let sv = Solver::new(&[t, kt, t], &[], &[c, le]).unwrap();
let p = plan(
vec![],
vec![Group {
vars: vec![0, 2],
weighted: vec![Weighted {
var: 1,
weights: ints(&[1]),
measure: Measure::Distinct(0),
}],
}],
);
let mut seed = 45u64;
let (mut hx, mut ymax) = ([0u32; 4], 0u32);
for _ in 0..40_000 {
let v = sv.sample_plan(&p, &mut [], &mut seed).unwrap();
assert!(v[2] <= v[0], "{v:?}");
hx[usize::try_from(v[0].clone()).unwrap()] += 1;
ymax += u32::from(v[0] == BigUint::from(3u8) && v[2] == BigUint::from(3u8));
}
for h in hx {
assert!((9_550..10_450).contains(&h), "{hx:?}");
}
assert!((2_250..2_750).contains(&ymax), "{ymax}");
}
#[test]
fn the_models_measure_weights_a_selector_by_its_completions() {
let t = Ty::unsigned(2);
let (x, y) = (Expr::Var(0, t), Expr::Var(2, t));
let (c, kt) = dist_conjunct(&x, &[(0, 3)]);
let le = bin(BinOp::Le, y, x, Ty::BIT);
let sv = Solver::new(&[t, kt, t], &[], &[c, le]).unwrap();
let p = plan(
vec![],
vec![Group {
vars: vec![0, 2],
weighted: vec![Weighted {
var: 1,
weights: ints(&[1]),
measure: Measure::Models,
}],
}],
);
let mut seed = 46u64;
let mut x3 = 0u32;
for _ in 0..40_000 {
let v = sv.sample_plan(&p, &mut [], &mut seed).unwrap();
x3 += u32::from(v[0] == BigUint::from(3u8));
}
assert!((15_500..16_500).contains(&x3), "{x3}");
}
#[test]
fn a_selector_with_no_positive_feasible_weight_is_unsatisfiable() {
let t = Ty::unsigned(4);
let x = Expr::Var(0, t);
let (c, kt) = dist_conjunct(&x, &[(0, 3), (5, 5)]);
let sv = Solver::new(&[t, kt], &[], &[c]).unwrap();
let p = plan(
vec![],
vec![Group {
vars: vec![0],
weighted: vec![Weighted {
var: 1,
weights: ints(&[0, 0]),
measure: Measure::Distinct(0),
}],
}],
);
let mut seed = 47u64;
assert_eq!(
sv.sample_plan(&p, &mut [], &mut seed),
Err(SolveError::Unsatisfiable)
);
assert_eq!(seed, 47, "no draws consumed");
}
#[test]
fn a_distinct_subject_outside_the_group_or_a_zero_denominator_is_a_bad_plan() {
let t = Ty::unsigned(4);
let x = Expr::Var(0, t);
let (c, kt) = dist_conjunct(&x, &[(0, 3)]);
let sv = Solver::new(&[t, kt], &[], &[c]).unwrap();
let mut seed = 48u64;
let weighted = |w: Vec<Weight>| Weighted {
var: 1,
weights: w,
measure: Measure::Distinct(0),
};
let p = plan(
vec![],
vec![Group {
vars: vec![],
weighted: vec![weighted(ints(&[1]))],
}],
);
assert!(matches!(
sv.sample_plan(&p, &mut [], &mut seed),
Err(SolveError::BadPlan { .. })
));
let bad = vec![Weight {
num: 1u32.into(),
den: 0u32.into(),
}];
let p = plan(
vec![],
vec![Group {
vars: vec![0],
weighted: vec![weighted(bad)],
}],
);
assert!(matches!(
sv.sample_plan(&p, &mut [], &mut seed),
Err(SolveError::BadPlan { .. })
));
}
#[test]
fn unconstrained_bits_draw_as_the_full_walk_does() {
let x = Expr::Var(0, Ty::unsigned(8));
let c = bin(
BinOp::Lt,
x,
Expr::constant(Ty::unsigned(8), 100u8),
Ty::BIT,
);
let tys = [Ty::unsigned(8), Ty::unsigned(8), Ty::unsigned(4)];
let sv = Solver::new(&tys, &[], &[c]).expect("compiles");
let mut seed = 7;
let mut got = Vec::new();
for _ in 0..6 {
let v = sv.sample(&[1, 0, 2], &mut seed).expect("sat");
assert!(v[0] < BigUint::from(100u8));
got.push(
v.iter()
.map(ToString::to_string)
.collect::<Vec<_>>()
.join(","),
);
}
assert_eq!(
got.join(" "),
"21,216,3 52,172,1 61,93,5 42,49,7 18,1,0 31,165,6"
);
}
#[test]
fn repeated_compile_is_cached_and_equal() {
let x = Expr::Var(0, Ty::unsigned(6));
let c = bin(BinOp::Lt, x, Expr::constant(Ty::unsigned(6), 37u8), Ty::BIT);
let tys = [Ty::unsigned(6)];
let a = Solver::new(&tys, &[], std::slice::from_ref(&c)).unwrap();
let b = Solver::new(&tys, &[], std::slice::from_ref(&c)).unwrap();
assert_eq!(a.count(), &BigUint::from(37u32));
assert_eq!(a.count(), b.count());
let digest = problem_digest(&tys, &[], &[c], NODE_BUDGET);
assert!(SOLVER_CACHE.lock().unwrap().contains_key(&digest));
}
}