#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Debug, Hash)]
pub struct Var(u32);
impl Var {
#[inline]
pub fn index(self) -> usize {
self.0 as usize
}
}
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Debug, Hash)]
pub struct Lit {
var: Var,
positive: bool,
}
impl Lit {
#[inline]
pub fn pos(v: Var) -> Self {
Lit { var: v, positive: true }
}
#[inline]
pub fn neg(v: Var) -> Self {
Lit { var: v, positive: false }
}
#[inline]
pub fn var(self) -> Var {
self.var
}
#[inline]
pub fn is_positive(self) -> bool {
self.positive
}
#[inline]
pub fn holds(self, values: &[bool]) -> bool {
match values.get(self.var.index()) {
Some(&v) => v == self.positive,
None => false,
}
}
}
impl std::ops::Not for Lit {
type Output = Lit;
#[inline]
fn not(self) -> Lit {
Lit { var: self.var, positive: !self.positive }
}
}
#[derive(Clone, Copy, Debug)]
struct Soft {
guard: Var,
weight: u32,
}
const AMO_PAIRWISE_MAX: usize = crate::grammar::MAX_PROTOTYPES;
#[derive(Debug)]
pub struct Problem {
n_vars: u32,
lits: Vec<Lit>,
starts: Vec<u32>,
soft: Vec<Soft>,
prefer: Vec<Option<bool>>,
}
impl Default for Problem {
fn default() -> Self {
Self::new()
}
}
impl Problem {
pub fn new() -> Self {
let mut p = Problem {
n_vars: 0,
lits: Vec::new(),
starts: vec![0],
soft: Vec::new(),
prefer: Vec::new(),
};
let t = p.var();
p.lits.push(Lit::pos(t));
p.starts.push(p.lits.len() as u32);
p
}
pub fn var(&mut self) -> Var {
let v = Var(self.n_vars);
self.n_vars += 1;
v
}
pub fn prefer(&mut self, v: Var, value: bool) {
if self.prefer.len() <= v.index() {
self.prefer.resize(v.index() + 1, None);
}
if let Some(slot) = self.prefer.get_mut(v.index()) {
*slot = Some(value);
}
}
#[inline]
pub fn vars(&self) -> usize {
self.n_vars as usize
}
#[inline]
pub fn clauses(&self) -> usize {
self.starts.len() - 1
}
#[inline]
pub fn clause(&self, i: usize) -> Option<&[Lit]> {
let (a, b) = (*self.starts.get(i)?, *self.starts.get(i + 1)?);
self.lits.get(a as usize..b as usize)
}
pub fn iter_clauses(&self) -> impl Iterator<Item = &[Lit]> + '_ {
(0..self.clauses()).filter_map(|i| self.clause(i))
}
#[inline]
pub fn always_true(&self) -> Lit {
Lit::pos(Var(0))
}
#[inline]
pub fn always_false(&self) -> Lit {
Lit::neg(Var(0))
}
pub fn add_clause(&mut self, lits: &[Lit]) {
let t = self.always_true();
let f = self.always_false();
let mut out: Vec<Lit> = Vec::with_capacity(lits.len());
for &l in lits {
if l == t || out.contains(&!l) {
return; }
if l == f || out.contains(&l) {
continue;
}
out.push(l);
}
self.lits.extend_from_slice(&out);
self.starts.push(self.lits.len() as u32);
}
pub fn conj(&mut self, lits: &[Lit]) -> Lit {
match lits {
[] => return self.always_true(),
[only] => return *only,
_ => {}
}
let y = Lit::pos(self.var());
for &l in lits {
self.add_clause(&[!y, l]);
}
let mut back: Vec<Lit> = lits.iter().map(|&l| !l).collect();
back.push(y);
self.add_clause(&back);
y
}
fn guard(&mut self, weight: Option<u32>) -> Lit {
match weight {
None => self.always_false(),
Some(w) => {
let g = self.var();
self.soft.push(Soft { guard: g, weight: w });
Lit::neg(g)
}
}
}
pub fn count(&mut self, vs: &[Lit], lo: u32, hi: u32, weight: Option<u32>) {
let n = vs.len() as u32;
if lo > hi || lo > n {
let g = self.guard(weight);
self.add_clause(&[g]); return;
}
if lo == 0 && hi >= n {
return; }
if hi == 1 && (n as usize) <= AMO_PAIRWISE_MAX {
let g = self.guard(weight);
if lo == 1 {
let mut any = Vec::with_capacity(vs.len() + 1);
any.push(g);
any.extend_from_slice(vs);
self.add_clause(&any);
}
for (i, &a) in vs.iter().enumerate() {
for &b in &vs[i + 1..] {
self.add_clause(&[g, !a, !b]);
}
}
return;
}
let ceiling = if hi < n { hi + 1 } else { 0 };
let k = lo.max(ceiling);
let f = self.always_false();
let s: Vec<Lit> = (0..n * k).map(|_| Lit::pos(self.var())).collect();
let at = |i: u32, j: u32| -> Lit {
if j == 0 {
Lit::pos(Var(0)) } else if i == 0 {
f
} else {
s[((i - 1) * k + (j - 1)) as usize]
}
};
for i in 1..=n {
let Some(&x) = vs.get((i - 1) as usize) else {
continue; };
for j in 1..=k {
let sij = at(i, j);
let prev_j = at(i - 1, j);
let prev_below = at(i - 1, j - 1);
self.add_clause(&[!prev_j, sij]);
self.add_clause(&[!x, !prev_below, sij]);
self.add_clause(&[!sij, prev_j, x]);
self.add_clause(&[!sij, prev_j, prev_below]);
}
}
let g = self.guard(weight);
if lo > 0 {
self.add_clause(&[g, at(n, lo)]);
}
if ceiling > 0 {
self.add_clause(&[g, !at(n, ceiling)]);
}
}
pub fn implies_any(&mut self, l: Lit, ms: &[Lit], weight: Option<u32>) {
let g = self.guard(weight);
let mut clause = Vec::with_capacity(ms.len() + 2);
clause.push(g);
clause.push(!l);
clause.extend_from_slice(ms);
self.add_clause(&clause);
}
pub fn check(&self, values: &[bool]) -> Result<(), String> {
if values.len() < self.vars() {
return Err(format!(
"constraints: an assignment of {} values cannot answer for {} variables",
values.len(),
self.vars()
));
}
for (i, clause) in self.iter_clauses().enumerate() {
if !clause.iter().any(|l| l.holds(values)) {
return Err(format!("constraints: clause {i} of {} is unsatisfied", self.clauses()));
}
}
Ok(())
}
pub fn unmet(&self, values: &[bool]) -> u64 {
self.soft
.iter()
.filter(|s| !Lit::pos(s.guard).holds(values))
.fold(0u64, |acc, s| acc.saturating_add(s.weight as u64))
}
#[inline]
pub fn soft_count(&self) -> usize {
self.soft.len()
}
pub fn solve(&self, conflicts: u64) -> Result<Solution, String> {
let mut hints = self.prefer.clone();
hints.resize(self.vars(), None);
let mut solver = bevy_grimoire_solver::Solver::with_preferences(&hints)?;
for clause in self.iter_clauses() {
let lits: Vec<bevy_grimoire_solver::Literal> = clause.iter().map(to_dimacs).collect();
solver.add_clause(&lits)?;
}
let guards: Vec<bevy_grimoire_solver::Literal> =
self.soft.iter().map(|s| to_dimacs(&Lit::pos(s.guard))).collect();
let weights: Vec<u32> = self.soft.iter().map(|s| s.weight).collect();
let mut items: Vec<bevy_grimoire_solver::Literal> = guards;
let mut residual: Vec<u64> = weights.iter().map(|&w| w as u64).collect();
for _ in 0..max_rounds(self.soft.len()) {
let assumed: Vec<bevy_grimoire_solver::Literal> = items
.iter()
.zip(&residual)
.filter(|&(_, &r)| r > 0)
.map(|(&l, _)| l)
.collect();
match solver.solve(&assumed, bevy_grimoire_solver::Budget::conflicts(conflicts))? {
bevy_grimoire_solver::Answer::Satisfied(m) => {
return Solution::from_assignment(self, m.values().to_vec());
}
bevy_grimoire_solver::Answer::Unsatisfiable { core } => {
let found: Vec<usize> = core
.iter()
.filter_map(|l| items.iter().position(|it| it == l))
.filter(|&i| residual.get(i).copied().unwrap_or(0) > 0)
.collect();
if found.is_empty() {
return Err(
"constraints: no arrangement satisfies these rules. The grammar cannot \
tile this region under what has been asked of it — free some pinned \
cells, or extend the example so there are more ways to join things up."
.to_owned(),
);
}
let Some(wmin) = found.iter().filter_map(|&i| residual.get(i)).min().copied()
else {
return Err("constraints: a core with no weight to spend".to_owned());
};
for &i in &found {
if let Some(r) = residual.get_mut(i) {
*r = r.saturating_sub(wmin);
}
}
if found.len() > 1 {
let inputs: Vec<bevy_grimoire_solver::Literal> =
found.iter().filter_map(|&i| items.get(i)).map(|&l| -l).collect();
let outputs = totalizer(&mut solver, &inputs)?;
for &o in outputs.iter().skip(1) {
items.push(-o);
residual.push(wmin);
}
}
}
bevy_grimoire_solver::Answer::Exhausted => {
return Err(format!(
"constraints: gave up after {conflicts} conflicts without settling the \
question. That is not a statement that no arrangement exists — raise the \
budget to find out."
))
}
}
}
Err(format!(
"constraints: the optimisation did not converge within {} rounds over {} soft \
constraint(s). Every round raises a proven lower bound on the cost, so this means the \
wishes conflict far more than the budget allows to price — ask for fewer at once.",
max_rounds(self.soft.len()),
self.soft.len()
))
}
}
fn max_rounds(softs: usize) -> usize {
256 + softs * 64
}
fn totalizer(
solver: &mut bevy_grimoire_solver::Solver,
inputs: &[bevy_grimoire_solver::Literal],
) -> Result<Vec<bevy_grimoire_solver::Literal>, String> {
match inputs {
[] => Ok(Vec::new()),
[only] => Ok(vec![*only]),
_ => {
let (left, right) = inputs.split_at(inputs.len() / 2);
let a = totalizer(solver, left)?;
let b = totalizer(solver, right)?;
let mut out = Vec::with_capacity(a.len() + b.len());
for _ in 0..a.len() + b.len() {
out.push(solver.add_var()?);
}
for i in 0..=a.len() {
for j in 0..=b.len() {
if i + j == 0 {
continue;
}
let mut clause = Vec::with_capacity(3);
if i > 0 {
clause.push(-a[i - 1]);
}
if j > 0 {
clause.push(-b[j - 1]);
}
clause.push(out[i + j - 1]);
solver.add_clause(&clause)?;
}
}
Ok(out)
}
}
}
fn to_dimacs(l: &Lit) -> bevy_grimoire_solver::Literal {
let v = l.var().index() as bevy_grimoire_solver::Literal + 1;
if l.is_positive() { v } else { -v }
}
#[derive(Clone, Debug)]
pub struct Solution {
values: Vec<bool>,
unmet: u64,
}
impl Solution {
pub fn from_assignment(problem: &Problem, values: Vec<bool>) -> Result<Self, String> {
problem.check(&values)?;
let unmet = problem.unmet(&values);
Ok(Solution { values, unmet })
}
#[inline]
pub fn get(&self, v: Var) -> bool {
self.values.get(v.index()).copied().unwrap_or(false)
}
#[inline]
pub fn unmet(&self) -> u64 {
self.unmet
}
#[inline]
pub fn values(&self) -> &[bool] {
&self.values
}
}
pub fn tile_rules(p: &mut Problem, cells: usize, protos: usize) -> Vec<Vec<Var>> {
let mut place: Vec<Vec<Var>> = Vec::with_capacity(cells);
for _ in 0..cells {
let row: Vec<Var> = (0..protos).map(|_| p.var()).collect();
let lits: Vec<Lit> = row.iter().map(|&v| Lit::pos(v)).collect();
p.count(&lits, 1, 1, None);
place.push(row);
}
place
}
pub fn domain_rules(p: &mut Problem, place: &[Vec<Var>], initial: &[u32]) {
for (c, row) in place.iter().enumerate() {
let Some(&mask) = initial.get(c) else {
continue; };
for (proto, &v) in row.iter().enumerate() {
if proto < 32 && mask & (1u32 << proto) == 0 {
p.add_clause(&[Lit::neg(v)]);
}
}
}
}
pub fn preference_rules(
p: &mut Problem,
place: &[Vec<Var>],
weights: &[f64],
seed: u64,
) -> Result<(), String> {
if weights.is_empty() {
return Err("constraints: a grammar with no prototypes has nothing to wish for".to_owned());
}
if let Some(bad) = weights.iter().find(|w| !w.is_finite() || **w < 0.0) {
return Err(format!("constraints: {bad} is not a usable selection weight"));
}
let total: f64 = weights.iter().sum();
if !matches!(total.partial_cmp(&0.0), Some(std::cmp::Ordering::Greater)) {
return Err(
"constraints: every prototype has weight zero, so there is nothing to draw from"
.to_owned(),
);
}
use crate::rng::DetRng;
let mut rng = crate::rng::seeded(seed);
for row in place {
let mut r = rng.unit() * total;
let mut wanted = weights.len() - 1;
for (k, w) in weights.iter().enumerate() {
r -= w;
if r <= 0.0 {
wanted = k;
break;
}
}
if row.get(wanted).is_none() {
continue; }
for (q, &v) in row.iter().enumerate() {
p.prefer(v, q == wanted);
}
}
Ok(())
}
pub fn pattern_rules(
p: &mut Problem,
place: &[Vec<Var>],
support: &[Vec<u32>; 4],
w: usize,
h: usize,
) {
for z in 0..h {
for x in 0..w {
let c = z * w + x;
let Some(row) = place.get(c) else {
continue;
};
for dir in 0..4 {
let Some((nx, nz)) = step(w, h, x, z, dir) else {
continue;
};
let Some(neighbour) = place.get(nz * w + nx) else {
continue;
};
for (a, &va) in row.iter().enumerate() {
let Some(mask) = support.get(dir).and_then(|t| t.get(a)).copied() else {
continue;
};
let allowed: Vec<Lit> = neighbour
.iter()
.enumerate()
.filter(|(b, _)| *b < 32 && mask & (1u32 << *b) != 0)
.map(|(_, &vb)| Lit::pos(vb))
.collect();
p.implies_any(Lit::pos(va), &allowed, None);
}
}
}
}
}
use crate::wfc::{E, N, S, W};
#[inline]
fn opposite(dir: usize) -> usize {
match dir {
N => S,
S => N,
E => W,
_ => E,
}
}
#[inline]
fn step(w: usize, h: usize, x: usize, z: usize, dir: usize) -> Option<(usize, usize)> {
match dir {
N => z.checked_sub(1).map(|z| (x, z)),
E => (x + 1 < w).then_some((x + 1, z)),
S => (z + 1 < h).then_some((x, z + 1)),
W => x.checked_sub(1).map(|x| (x, z)),
_ => None,
}
}
pub fn enclosure_rules(
p: &mut Problem,
place: &[Vec<Var>],
faces: &crate::range::Faces,
w: usize,
h: usize,
want: f32,
weight: Option<u32>,
) -> Result<(), String> {
if w == 0 || h == 0 {
return Err(format!("constraints: a {w} x {h} region has no cells to enclose"));
}
let cells = w * h;
if place.len() != cells {
return Err(format!(
"constraints: {} cells of placement for a {w} x {h} region",
place.len()
));
}
if !want.is_finite() || !(0.0..=1.0).contains(&want) {
return Err(format!(
"constraints: an enclosure of {want} is not a fraction — it must lie in [0, 1]"
));
}
let Some(protos) = place.first().map(Vec::len) else {
return Err("constraints: no cells to enclose".to_owned());
};
if place.iter().any(|r| r.len() != protos) {
return Err("constraints: the cells do not agree on how many prototypes there are".to_owned());
}
let walls: Vec<[bool; 4]> = (0..protos)
.map(|q| [faces.wall(q, N), faces.wall(q, E), faces.wall(q, S), faces.wall(q, W)])
.collect();
let mut face_open: Vec<Option<Lit>> = vec![None; cells * 4];
for z in 0..h {
for x in 0..w {
let c = z * w + x;
for dir in 0..4 {
if step(w, h, x, z, dir).is_none() {
continue;
}
let open = Lit::pos(p.var());
face_open[c * 4 + dir] = Some(open);
for (q, wall) in walls.iter().enumerate() {
let Some(&here) = place.get(c).and_then(|r| r.get(q)) else {
continue;
};
let laid = Lit::pos(here);
if wall[dir] {
p.add_clause(&[!laid, !open]);
} else {
p.add_clause(&[!laid, open]);
}
}
}
}
}
let mut seam: Vec<Option<Lit>> = vec![None; cells * 4];
for z in 0..h {
for x in 0..w {
let c = z * w + x;
for dir in [E, S] {
let Some((nx, nz)) = step(w, h, x, z, dir) else {
continue;
};
let n = nz * w + nx;
let (Some(near), Some(far)) =
(face_open[c * 4 + dir], face_open[n * 4 + opposite(dir)])
else {
continue;
};
let open = p.conj(&[near, far]);
seam[c * 4 + dir] = Some(open);
seam[n * 4 + opposite(dir)] = Some(open);
}
}
}
let outside: Vec<Lit> = (0..cells).map(|_| Lit::pos(p.var())).collect();
for z in 0..h {
for x in 0..w {
if (x == 0 || z == 0 || x + 1 == w || z + 1 == h)
&& let Some(&o) = outside.get(z * w + x)
{
p.add_clause(&[o]);
}
}
}
for z in 0..h {
for x in 0..w {
let c = z * w + x;
for dir in 0..4 {
let Some((nx, nz)) = step(w, h, x, z, dir) else {
continue;
};
let n = nz * w + nx;
let (Some(&here), Some(&there), Some(open)) =
(outside.get(c), outside.get(n), seam[c * 4 + dir])
else {
continue;
};
p.add_clause(&[!there, !open, here]);
}
}
}
let enclosed: Vec<Lit> = (0..cells).map(|_| Lit::pos(p.var())).collect();
for c in 0..cells {
let (Some(&is_in), Some(&out)) = (enclosed.get(c), outside.get(c)) else {
continue;
};
if let Some(&empty) = place.get(c).and_then(|r| r.first()) {
p.add_clause(&[!is_in, !Lit::pos(empty)]); }
p.add_clause(&[!is_in, !out]); }
let lo = (want * cells as f32).ceil().max(0.0) as u32;
p.count(&enclosed, lo, cells as u32, weight);
Ok(())
}
#[derive(Debug)]
pub struct GridProblem {
pub problem: Problem,
pub place: Vec<Vec<Var>>,
pub width: usize,
pub height: usize,
protos: usize,
}
impl GridProblem {
pub fn encode(
support: &[Vec<u32>; 4],
initial: &[u32],
width: usize,
height: usize,
protos: usize,
) -> Result<Self, String> {
if width == 0 || height == 0 {
return Err(format!("constraints: a {width} x {height} region has no cells to fill"));
}
if protos == 0 {
return Err("constraints: a grammar with no prototypes cannot tile anything".to_owned());
}
if protos > 32 {
return Err(format!(
"constraints: {protos} prototypes is more than a 32-bit domain mask can carry"
));
}
let cells = width * height;
if initial.len() != cells {
return Err(format!(
"constraints: {} starting domains for a {width} x {height} region of {cells} cells",
initial.len()
));
}
for (dir, table) in support.iter().enumerate() {
if table.len() != protos {
return Err(format!(
"constraints: the support table for direction {dir} covers {} prototypes, not {protos}",
table.len()
));
}
}
let mut problem = Problem::new();
let place = tile_rules(&mut problem, cells, protos);
domain_rules(&mut problem, &place, initial);
pattern_rules(&mut problem, &place, support, width, height);
Ok(GridProblem { problem, place, width, height, protos })
}
pub fn read(&self, solution: &Solution) -> Result<Vec<usize>, String> {
let mut grid = Vec::with_capacity(self.place.len());
for (c, row) in self.place.iter().enumerate() {
let mut held: Option<usize> = None;
for (proto, &v) in row.iter().enumerate() {
if solution.get(v) {
if held.is_some() {
return Err(format!(
"constraints: the solution puts more than one prototype in cell {c}"
));
}
held = Some(proto);
}
}
match held {
Some(p) => grid.push(p),
None => {
return Err(format!("constraints: the solution puts no prototype in cell {c}"))
}
}
}
Ok(grid)
}
#[inline]
pub fn protos(&self) -> usize {
self.protos
}
}
#[cfg(test)]
mod tests {
use super::*;
const MODEL_CAP: usize = 100_000;
fn propagate(p: &Problem, val: &mut [Option<bool>]) -> bool {
loop {
let mut changed = false;
for clause in p.iter_clauses() {
let mut free: Option<Lit> = None;
let mut n_free = 0usize;
let mut satisfied = false;
for &l in clause {
match val.get(l.var().index()).copied().flatten() {
Some(b) if b == l.is_positive() => {
satisfied = true;
break;
}
Some(_) => {}
None => {
free = Some(l);
n_free += 1;
}
}
}
if satisfied {
continue;
}
match (n_free, free) {
(0, _) => return false,
(1, Some(l)) => {
val[l.var().index()] = Some(l.is_positive());
changed = true;
}
_ => {}
}
}
if !changed {
return true;
}
}
}
fn models(p: &Problem) -> Vec<Vec<bool>> {
fn search(p: &Problem, val: &mut [Option<bool>], out: &mut Vec<Vec<bool>>) {
if !propagate(p, val) {
return;
}
match val.iter().position(|v| v.is_none()) {
None => {
let full: Vec<bool> = val.iter().map(|v| v.unwrap_or(false)).collect();
assert!(p.check(&full).is_ok(), "propagation completed an unsatisfying assignment");
out.push(full);
assert!(out.len() <= MODEL_CAP, "more than {MODEL_CAP} models — the fixture is too big");
}
Some(next) => {
for guess in [true, false] {
let mut branch = val.to_vec();
branch[next] = Some(guess);
search(p, &mut branch, out);
}
}
}
}
let mut out = Vec::new();
search(p, &mut vec![None; p.vars()], &mut out);
out
}
fn popcount(vs: &[Lit], values: &[bool]) -> u32 {
vs.iter().filter(|l| l.holds(values)).count() as u32
}
#[test]
fn variable_zero_is_true_in_every_model() {
let p = Problem::new();
let ms = models(&p);
assert!(!ms.is_empty(), "an empty problem has models");
for m in &ms {
assert!(p.always_true().holds(m), "always_true must hold");
assert!(!p.always_false().holds(m), "always_false must not hold");
}
}
#[test]
fn conj_is_the_conjunction_in_both_directions() {
let mut p = Problem::new();
let a = Lit::pos(p.var());
let b = Lit::pos(p.var());
let c = Lit::pos(p.var());
let y = p.conj(&[a, b, c]);
let ms = models(&p);
assert_eq!(ms.len(), 8, "three free inputs, and y is determined by them");
for m in &ms {
let all = a.holds(m) && b.holds(m) && c.holds(m);
assert_eq!(y.holds(m), all, "y must be exactly a ∧ b ∧ c");
}
}
#[test]
fn conj_of_nothing_and_of_one_allocate_nothing() {
let mut p = Problem::new();
let before = p.vars();
assert_eq!(p.conj(&[]), p.always_true(), "the empty conjunction is true");
let a = Lit::pos(p.var());
assert_eq!(p.conj(&[a]), a, "a one-literal conjunction is that literal");
assert_eq!(p.vars(), before + 1, "only `a` itself was allocated");
}
#[test]
fn count_admits_exactly_the_assignments_in_range() {
const N: u32 = 4;
for lo in 0..=N + 1 {
for hi in 0..=N + 1 {
let mut p = Problem::new();
let vs: Vec<Lit> = (0..N).map(|_| Lit::pos(p.var())).collect();
p.count(&vs, lo, hi, None);
let ms = models(&p);
let want_any = lo <= hi && lo <= N;
assert_eq!(ms.is_empty(), !want_any, "lo={lo} hi={hi}: satisfiability");
for m in &ms {
let k = popcount(&vs, m);
assert!(k >= lo && k <= hi, "lo={lo} hi={hi}: admitted a count of {k}");
}
let reached: std::collections::BTreeSet<u32> =
ms.iter().map(|m| popcount(&vs, m)).collect();
for k in 0..=N {
if k >= lo && k <= hi {
assert!(reached.contains(&k), "lo={lo} hi={hi}: a count of {k} is unreachable");
}
}
}
}
}
#[test]
fn count_of_exactly_one_admits_each_singleton_and_nothing_else() {
let mut p = Problem::new();
let vs: Vec<Lit> = (0..5).map(|_| Lit::pos(p.var())).collect();
p.count(&vs, 1, 1, None);
let ms = models(&p);
let reached: std::collections::BTreeSet<usize> = ms
.iter()
.filter_map(|m| vs.iter().position(|l| l.holds(m)))
.collect();
assert_eq!(reached.len(), 5, "each of the five must be reachable alone");
for m in &ms {
assert_eq!(popcount(&vs, m), 1, "exactly one must mean exactly one");
}
}
#[test]
fn count_over_negative_literals_counts_them_as_written() {
let mut p = Problem::new();
let a = Lit::pos(p.var());
let b = Lit::pos(p.var());
let c = Lit::pos(p.var());
p.count(&[!a, !b, !c], 2, 2, None);
for m in &models(&p) {
assert_eq!(popcount(&[!a, !b, !c], m), 2, "exactly two must be false");
}
}
#[test]
fn an_impossible_hard_count_makes_the_problem_unsatisfiable() {
let mut p = Problem::new();
let vs: Vec<Lit> = (0..3).map(|_| Lit::pos(p.var())).collect();
p.count(&vs, 3, 1, None); assert!(models(&p).is_empty(), "an impossible hard constraint admits nothing");
let mut q = Problem::new();
let ws: Vec<Lit> = (0..2).map(|_| Lit::pos(q.var())).collect();
q.count(&ws, 3, 3, None); assert!(models(&q).is_empty(), "asking for more than exists admits nothing");
}
#[test]
fn a_vacuous_count_says_nothing() {
let mut p = Problem::new();
let before = (p.vars(), p.clauses());
let vs: Vec<Lit> = (0..4).map(|_| Lit::pos(p.var())).collect();
p.count(&vs, 0, 9, None);
assert_eq!(p.clauses(), before.1, "a vacuous bound must add no clause");
assert_eq!(p.vars(), before.0 + 4, "and allocate no counter");
}
#[test]
fn implies_any_is_the_clause_it_claims() {
let mut p = Problem::new();
let l = Lit::pos(p.var());
let m1 = Lit::pos(p.var());
let m2 = Lit::pos(p.var());
p.implies_any(l, &[m1, m2], None);
for m in &models(&p) {
if l.holds(m) {
assert!(m1.holds(m) || m2.holds(m), "l must imply one of the ms");
}
}
let free = models(&p).iter().filter(|m| !l.holds(m)).count();
assert_eq!(free, 4, "a false antecedent constrains nothing");
}
#[test]
fn implies_any_of_nothing_denies_its_antecedent() {
let mut p = Problem::new();
let l = Lit::pos(p.var());
p.implies_any(l, &[], None);
for m in &models(&p) {
assert!(!l.holds(m), "with nothing to imply, l cannot hold");
}
}
#[test]
fn a_soft_constraint_can_be_bought_and_costs_its_weight() {
let mut p = Problem::new();
let a = Lit::pos(p.var());
let b = Lit::pos(p.var());
p.count(&[a, b], 0, 1, None);
p.count(&[a, b], 2, 2, Some(7));
let ms = models(&p);
assert!(!ms.is_empty(), "a soft conflict must not make the problem unsatisfiable");
for m in &ms {
assert!(!(a.holds(m) && b.holds(m)), "the hard bound still holds");
assert_eq!(p.unmet(m), 7, "the unmeetable soft constraint costs its weight");
}
}
#[test]
fn a_satisfiable_soft_constraint_can_cost_nothing() {
let mut p = Problem::new();
let a = Lit::pos(p.var());
let b = Lit::pos(p.var());
p.count(&[a, b], 2, 2, Some(5));
let ms = models(&p);
assert!(
ms.iter().any(|m| p.unmet(m) == 0 && a.holds(m) && b.holds(m)),
"a soft constraint that can be met must have a zero-cost model that really meets it"
);
for m in &ms {
if p.unmet(m) == 0 {
assert!(a.holds(m) && b.holds(m), "a zero cost must mean the constraint really held");
}
}
}
#[test]
fn soft_weights_add_up_across_constraints() {
let mut p = Problem::new();
let a = Lit::pos(p.var());
p.add_clause(&[!a]); p.implies_any(p.always_true(), &[a], Some(3));
p.implies_any(p.always_true(), &[a], Some(4));
let ms = models(&p);
assert!(!ms.is_empty());
for m in &ms {
assert_eq!(p.unmet(m), 7, "two unmeetable soft constraints cost 3 + 4");
}
}
#[test]
fn an_impossible_soft_count_costs_its_weight_rather_than_refusing() {
let mut p = Problem::new();
let vs: Vec<Lit> = (0..2).map(|_| Lit::pos(p.var())).collect();
p.count(&vs, 2, 1, Some(11)); let ms = models(&p);
assert!(!ms.is_empty(), "an impossible SOFT constraint must not refuse");
for m in &ms {
assert_eq!(p.unmet(m), 11, "it costs its weight in every model");
}
}
#[test]
fn the_counters_own_clauses_are_not_relaxable() {
let mut p = Problem::new();
let vs: Vec<Lit> = (0..3).map(|_| Lit::pos(p.var())).collect();
p.count(&vs, 3, 3, Some(2));
let ms = models(&p);
assert!(ms.iter().any(|m| popcount(&vs, m) < 3), "the fixture must reach unmet states");
for m in &ms {
if p.unmet(m) == 0 {
assert_eq!(popcount(&vs, m), 3, "a zero cost must mean the count really reached 3");
}
}
}
#[test]
fn a_tautological_clause_is_dropped_and_a_false_literal_removed() {
let mut p = Problem::new();
let a = Lit::pos(p.var());
let before = p.clauses();
p.add_clause(&[a, !a]);
p.add_clause(&[a, p.always_true()]);
assert_eq!(p.clauses(), before, "tautologies add nothing");
p.add_clause(&[a, p.always_false(), a]);
assert_eq!(p.clauses(), before + 1, "and the real clause is kept");
assert_eq!(p.clause(before), Some(&[a][..]), "with the dead literals removed");
}
#[test]
fn the_empty_clause_is_kept() {
let mut p = Problem::new();
p.add_clause(&[p.always_false()]);
assert!(models(&p).is_empty(), "the falsifying clause must survive simplification");
}
#[test]
fn check_refuses_an_assignment_that_is_too_short() {
let mut p = Problem::new();
let _ = p.var();
assert!(p.check(&[true]).is_err(), "a short assignment cannot answer for every variable");
}
#[test]
fn tile_rules_put_exactly_one_prototype_in_every_cell() {
let mut p = Problem::new();
let place = tile_rules(&mut p, 2, 3);
assert_eq!(place.len(), 2);
for m in &models(&p) {
for row in &place {
let k = row.iter().filter(|&&v| m[v.index()]).count();
assert_eq!(k, 1, "each cell holds exactly one prototype");
}
}
assert_eq!(models(&p).len(), 9, "3 prototypes over 2 cells is 9 arrangements");
}
#[test]
fn domain_rules_forbid_the_prototypes_a_mask_clears() {
let mut p = Problem::new();
let place = tile_rules(&mut p, 2, 3);
domain_rules(&mut p, &place, &[1 << 2, 0b011]);
let ms = models(&p);
assert_eq!(ms.len(), 2, "one pinned cell times two free choices");
for m in &ms {
assert!(m[place[0][2].index()], "cell 0 must be prototype 2");
assert!(!m[place[1][2].index()], "cell 1 must not be prototype 2");
}
}
#[test]
fn an_empty_domain_refuses_rather_than_substituting() {
let mut p = Problem::new();
let place = tile_rules(&mut p, 1, 3);
domain_rules(&mut p, &place, &[0]);
assert!(models(&p).is_empty(), "a cell with nothing legal must make the problem refuse");
}
fn checkerboard_support() -> [Vec<u32>; 4] {
let alternate = vec![0b10u32, 0b01];
[alternate.clone(), alternate.clone(), alternate.clone(), alternate]
}
#[test]
fn pattern_rules_admit_exactly_the_two_checkerboards() {
let mut p = Problem::new();
let (w, h) = (2, 2);
let place = tile_rules(&mut p, w * h, 2);
pattern_rules(&mut p, &place, &checkerboard_support(), w, h);
let ms = models(&p);
assert_eq!(ms.len(), 2, "a 2x2 checkerboard has exactly two colourings");
for m in &ms {
for z in 0..h {
for x in 0..w {
let here = if m[place[z * w + x][0].index()] { 0 } else { 1 };
let want = (x + z) % 2;
let first = if m[place[0][0].index()] { 0 } else { 1 };
assert_eq!(here, (want + first) % 2, "cell ({x},{z}) breaks the alternation");
}
}
}
}
#[test]
fn pattern_rules_agree_with_wfcs_own_propagation_on_what_is_legal() {
let (w, h) = (2usize, 2usize);
let protos = 3usize;
let support: [Vec<u32>; 4] = [
vec![0b001, 0b010, 0b100], vec![0b011, 0b110, 0b101], vec![0b111, 0b111, 0b111], vec![0b101, 0b011, 0b110], ];
let mut p = Problem::new();
let place = tile_rules(&mut p, w * h, protos);
pattern_rules(&mut p, &place, &support, w, h);
let mut from_sat: Vec<Vec<usize>> = models(&p)
.iter()
.filter_map(|m| {
(0..w * h)
.map(|c| place[c].iter().position(|&v| m[v.index()]))
.collect::<Option<Vec<usize>>>()
})
.collect();
from_sat.sort();
const STEPS: [(i32, i32); 4] = [(0, -1), (1, 0), (0, 1), (-1, 0)];
let mut by_hand: Vec<Vec<usize>> = Vec::new();
for bits in 0..protos.pow((w * h) as u32) {
let grid: Vec<usize> = (0..w * h).map(|c| bits / protos.pow(c as u32) % protos).collect();
let ok = (0..h).all(|z| {
(0..w).all(|x| {
STEPS.iter().enumerate().all(|(dir, (dx, dz))| {
let (nx, nz) = (x as i32 + dx, z as i32 + dz);
if nx < 0 || nz < 0 || nx as usize >= w || nz as usize >= h {
return true;
}
let a = grid[z * w + x];
let b = grid[nz as usize * w + nx as usize];
support[dir][a] & (1 << b) != 0
})
})
});
if ok {
by_hand.push(grid);
}
}
by_hand.sort();
assert_eq!(from_sat, by_hand, "the clauses and the adjacency table must agree exactly");
assert!(!by_hand.is_empty(), "the fixture must not be vacuous");
}
#[test]
fn encode_then_read_returns_the_grid_wfc_would_have_returned() {
let (w, h) = (2usize, 2usize);
let full = 0b11u32;
let gp = GridProblem::encode(&checkerboard_support(), &[full; 4], w, h, 2)
.expect("a permissive 2x2 must encode");
assert_eq!(gp.protos(), 2);
let mut grids: Vec<Vec<usize>> = models(&gp.problem)
.iter()
.map(|m| {
let s = Solution::from_assignment(&gp.problem, m.clone()).expect("a model is a solution");
gp.read(&s).expect("every solution reads back as a grid")
})
.collect();
grids.sort();
assert_eq!(grids, vec![vec![0, 1, 1, 0], vec![1, 0, 0, 1]]);
}
#[test]
fn encode_honours_a_pinned_cell_the_way_grammar_solve_does() {
let (w, h) = (2usize, 2usize);
let mut initial = [0b11u32; 4];
initial[0] = 1 << 1; let gp = GridProblem::encode(&checkerboard_support(), &initial, w, h, 2)
.expect("a pinned 2x2 must encode");
let grids: Vec<Vec<usize>> = models(&gp.problem)
.iter()
.map(|m| {
let s = Solution::from_assignment(&gp.problem, m.clone()).expect("a model is a solution");
gp.read(&s).expect("reads back")
})
.collect();
assert_eq!(grids, vec![vec![1, 0, 0, 1]], "pinning must leave exactly the one checkerboard");
}
#[test]
fn encode_names_every_malformed_region_rather_than_panicking() {
let s = checkerboard_support();
let cases: [(usize, usize, usize, &[u32], &str); 5] = [
(0, 2, 2, &[0b11; 4], "no cells"),
(2, 2, 0, &[0b11; 4], "no prototypes"),
(2, 2, 33, &[0b11; 4], "too many prototypes"),
(2, 2, 2, &[0b11; 3], "too few starting domains"),
(2, 2, 3, &[0b11; 4], "a support table of the wrong width"),
];
for (w, h, protos, initial, why) in cases {
assert!(
GridProblem::encode(&s, initial, w, h, protos).is_err(),
"{why} must be refused by name"
);
}
}
#[test]
fn solve_returns_one_of_the_arrangements_the_enumerator_finds() {
let gp = GridProblem::encode(&checkerboard_support(), &[0b11; 4], 2, 2, 2).expect("encodes");
let solution = gp.problem.solve(0).expect("a satisfiable problem must solve");
let grid = gp.read(&solution).expect("and read back");
let expected: Vec<Vec<usize>> = models(&gp.problem)
.iter()
.filter_map(|m| {
let s = Solution::from_assignment(&gp.problem, m.clone()).ok()?;
gp.read(&s).ok()
})
.collect();
assert!(expected.contains(&grid), "the solver returned {grid:?}, not among {expected:?}");
}
#[test]
fn solve_refuses_an_unsatisfiable_problem_by_name() {
let gp = GridProblem::encode(&checkerboard_support(), &[0; 4], 2, 2, 2).expect("encodes");
let err = gp.problem.solve(0).expect_err("an empty domain admits nothing");
assert!(err.contains("no arrangement"), "the refusal must say what happened: {err}");
}
fn optimal_cost(p: &Problem) -> Option<u64> {
models(p).iter().map(|m| p.unmet(m)).min()
}
#[test]
fn solve_returns_the_proven_optimum_and_not_merely_a_model() {
let mut p = Problem::new();
let a = Lit::pos(p.var());
let b = Lit::pos(p.var());
let c = Lit::pos(p.var());
p.count(&[a, b], 0, 1, None); p.implies_any(p.always_true(), &[a], Some(9)); p.implies_any(p.always_true(), &[b], Some(2)); p.implies_any(p.always_true(), &[c], Some(4)); let s = p.solve(0).expect("solves");
assert_eq!(optimal_cost(&p), Some(2), "the fixture's optimum, by enumeration");
assert_eq!(s.unmet(), 2, "give up the cheap wish, not the dear one");
assert!(s.get(a.var()) && !s.get(b.var()) && s.get(c.var()));
}
#[test]
fn solve_matches_exhaustive_enumeration_over_many_random_soft_problems() {
let mut state = 0x243F_6A88_85A3_08D3u64;
let mut next = move || {
state = state.wrapping_mul(6_364_136_223_846_793_005).wrapping_add(1_442_695_040_888_963_407);
let x = state;
(x ^ (x >> 33)).wrapping_mul(0xff51_afd7_ed55_8ccd)
};
let mut checked = 0;
for case in 0..250 {
let n_vars = 3 + (next() % 4) as usize;
let mut p = Problem::new();
let vars: Vec<Lit> = (0..n_vars).map(|_| Lit::pos(p.var())).collect();
if next() % 2 == 0 {
let cap = (next() % (n_vars as u64 - 1)) as u32;
p.count(&vars, 0, cap, None);
}
for _ in 0..3 {
let i = (next() % n_vars as u64) as usize;
let j = (next() % n_vars as u64) as usize;
let (li, lj) = (vars[i], vars[j]);
let (li, lj) = (
if next() % 2 == 0 { li } else { !li },
if next() % 2 == 0 { lj } else { !lj },
);
p.add_clause(&[li, lj]);
}
for v in &vars {
let want = if next() % 2 == 0 { *v } else { !*v };
let w = 1 + (next() % 9) as u32;
p.implies_any(p.always_true(), &[want], Some(w));
}
let want = optimal_cost(&p);
match (p.solve(0), want) {
(Ok(s), Some(best)) => {
assert_eq!(s.unmet(), best, "case {case}: solve found {} not {best}", s.unmet());
checked += 1;
}
(Err(_), None) => {} (Ok(s), None) => panic!("case {case}: solved to {} where nothing is satisfiable", s.unmet()),
(Err(e), Some(best)) => panic!("case {case}: refused ({e}) where {best} was reachable"),
}
}
assert!(checked > 150, "the sweep must actually exercise satisfiable cases: {checked}/250");
}
#[test]
fn giving_up_two_wishes_from_one_core_is_priced_correctly() {
let mut p = Problem::new();
let a = Lit::pos(p.var());
let b = Lit::pos(p.var());
let c = Lit::pos(p.var());
p.count(&[a, b, c], 0, 1, None); p.implies_any(p.always_true(), &[a], Some(5));
p.implies_any(p.always_true(), &[b], Some(3));
p.implies_any(p.always_true(), &[c], Some(2));
let s = p.solve(0).expect("solves");
assert_eq!(optimal_cost(&p), Some(5), "the fixture's optimum, by enumeration");
assert_eq!(s.unmet(), 5, "two wishes from one core must cost their own weights, not a multiple");
assert!(s.get(a.var()) && !s.get(b.var()) && !s.get(c.var()));
}
#[test]
fn giving_up_three_wishes_from_one_core_is_priced_correctly() {
let mut p = Problem::new();
let vars: Vec<Lit> = (0..5).map(|_| Lit::pos(p.var())).collect();
p.count(&vars, 0, 2, None); for (i, v) in vars.iter().enumerate() {
p.implies_any(p.always_true(), &[*v], Some(1 + i as u32));
}
let s = p.solve(0).expect("solves");
assert_eq!(optimal_cost(&p), Some(6), "the fixture's optimum, by enumeration");
assert_eq!(s.unmet(), 6);
}
fn room_kit() -> Vec<Option<crate::composition::Interface>> {
use crate::composition::{Band, Interface};
let wall = || Band {
y: (0.0, 2.0),
lat: (-0.5, 0.5),
token: Some("wall".to_owned()),
};
let solid = Interface {
faces: [vec![wall()], vec![wall()], vec![wall()], vec![wall()]],
faults: Vec::new(),
};
vec![None, Some(Interface::default()), Some(solid)]
}
fn measure_grid(faces: &[Option<crate::composition::Interface>], grid: &[usize], w: usize, h: usize) -> crate::range::Measured {
let f = || crate::range::Faces::new(faces, "wall", 0.5);
crate::range::measure(w, h, grid, |p, d| f().wall(p, d), |p| f().floor(p), |p| f().doorway(p))
.expect("the fixture is well formed")
}
#[test]
fn a_solved_grid_really_measures_as_enclosed() {
let kit = room_kit();
let (w, h, protos) = (5usize, 5usize, 3usize);
let full = (1u32 << protos) - 1;
for want in [0.1f32, 0.2, 0.32] {
let mut gp = GridProblem::encode(&anything_goes(protos), &vec![full; w * h], w, h, protos)
.expect("encodes");
let faces = crate::range::Faces::new(&kit, "wall", 0.5);
enclosure_rules(&mut gp.problem, &gp.place, &faces, w, h, want, None)
.expect("the wish is well formed");
let grid = gp
.read(&gp.problem.solve(0).expect("a reachable target must solve"))
.expect("reads back");
let m = measure_grid(&kit, &grid, w, h);
assert!(
m.enclosure >= want,
"asked for {want}, the metric reports {:.3} on {grid:?}",
m.enclosure
);
assert!(m.regions > 0, "enclosure above zero must mean at least one region");
}
}
#[test]
fn the_wish_is_what_makes_the_rooms_and_the_dream_alone_does_not() {
let kit = room_kit();
let (w, h, protos) = (7usize, 7usize, 3usize);
let full = (1u32 << protos) - 1;
let want = 0.30f32;
let solve = |wish: bool| {
let mut gp = GridProblem::encode(&anything_goes(protos), &vec![full; w * h], w, h, protos)
.expect("encodes");
preference_rules(&mut gp.problem, &gp.place, &[1.0, 1.0, 1.0], 11).expect("wishes");
if wish {
let faces = crate::range::Faces::new(&kit, "wall", 0.5);
enclosure_rules(&mut gp.problem, &gp.place, &faces, w, h, want, Some(200))
.expect("well formed");
}
let grid = gp.read(&gp.problem.solve(0).expect("solves")).expect("reads");
measure_grid(&kit, &grid, w, h).enclosure
};
let dream_only = solve(false);
let with_wish = solve(true);
assert!(dream_only < want, "the fixture must start below the target: {dream_only:.3}");
assert!(with_wish >= want, "the wish must reach its target: {with_wish:.3}");
}
fn sealed_room_grid() -> Vec<usize> {
let mut g = vec![0usize; 25];
for z in 1..4 {
for x in 1..4 {
g[z * 5 + x] = 2;
}
}
g[2 * 5 + 2] = 1;
g
}
#[test]
fn the_rules_accept_exactly_the_enclosure_a_pinned_grid_really_has() {
let kit = room_kit();
let grid = sealed_room_grid();
let m = measure_grid(&kit, &grid, 5, 5);
assert_eq!(m.enclosure, 1.0, "the metric scores this room fully enclosed");
let pinned: Vec<u32> = grid.iter().map(|&p| 1u32 << p).collect();
let solvable = |want: f32| {
let mut gp = GridProblem::encode(&anything_goes(3), &pinned, 5, 5, 3).expect("encodes");
let faces = crate::range::Faces::new(&kit, "wall", 0.5);
enclosure_rules(&mut gp.problem, &gp.place, &faces, 5, 5, want, None).expect("well formed");
gp.problem.solve(0).is_ok()
};
assert!(solvable(0.36), "nine enclosed cells of twenty-five is 0.36 and must be reachable");
assert!(!solvable(0.40), "asking for ten enclosed cells where nine exist must refuse");
assert!(!solvable(0.37), "9.25 cells must round UP to ten, or the bound is not sound");
}
#[test]
fn a_leak_in_the_ring_is_seen_by_the_rules_as_it_is_by_the_metric() {
let kit = room_kit();
let mut grid = sealed_room_grid();
grid[5 + 2] = 0; assert_eq!(measure_grid(&kit, &grid, 5, 5).enclosure, 0.875, "the metric sees the leak");
let solvable = |g: &[usize], want: f32| {
let pinned: Vec<u32> = g.iter().map(|&p| 1u32 << p).collect();
let mut gp = GridProblem::encode(&anything_goes(3), &pinned, 5, 5, 3).expect("encodes");
let faces = crate::range::Faces::new(&kit, "wall", 0.5);
enclosure_rules(&mut gp.problem, &gp.place, &faces, 5, 5, want, None).expect("well formed");
gp.problem.solve(0).is_ok()
};
assert!(solvable(&sealed_room_grid(), 0.36), "the sealed room has nine enclosed cells");
assert!(!solvable(&grid, 0.36), "the leaked room has only seven and must refuse");
}
#[test]
fn a_soft_enclosure_wish_is_paid_for_rather_than_refused() {
let kit = room_kit();
let mut grid = sealed_room_grid();
grid[5 + 2] = 0;
let pinned: Vec<u32> = grid.iter().map(|&p| 1u32 << p).collect();
let mut gp = GridProblem::encode(&anything_goes(3), &pinned, 5, 5, 3).expect("encodes");
let faces = crate::range::Faces::new(&kit, "wall", 0.5);
enclosure_rules(&mut gp.problem, &gp.place, &faces, 5, 5, 0.5, Some(200)).expect("well formed");
let s = gp.problem.solve(0).expect("a soft wish must not refuse");
assert_eq!(s.unmet(), 200, "the unmeetable wish costs exactly its weight");
assert_eq!(gp.read(&s).expect("reads"), grid, "and the pinned arrangement is unchanged");
}
#[test]
fn enclosure_is_deterministic() {
let kit = room_kit();
let (w, h, protos) = (6usize, 6usize, 3usize);
let full = (1u32 << protos) - 1;
let once = || {
let mut gp = GridProblem::encode(&anything_goes(protos), &vec![full; w * h], w, h, protos)
.expect("encodes");
let faces = crate::range::Faces::new(&kit, "wall", 0.5);
enclosure_rules(&mut gp.problem, &gp.place, &faces, w, h, 0.25, None).expect("well formed");
gp.read(&gp.problem.solve(0).expect("solves")).expect("reads")
};
let first = once();
for round in 1..4 {
assert_eq!(once(), first, "round {round} enclosed a different set of cells");
}
}
#[test]
fn enclosure_rules_refuse_a_malformed_wish() {
let kit = room_kit();
let mut gp = GridProblem::encode(&anything_goes(3), &[0b111; 9], 3, 3, 3).expect("encodes");
let place = gp.place.clone();
let faces = crate::range::Faces::new(&kit, "wall", 0.5);
for bad in [-0.1f32, 1.5, f32::NAN] {
assert!(
enclosure_rules(&mut gp.problem, &place, &faces, 3, 3, bad, None).is_err(),
"{bad} is not a fraction"
);
}
assert!(
enclosure_rules(&mut gp.problem, &place, &faces, 4, 4, 0.5, None).is_err(),
"a region whose size disagrees with the placement must be refused"
);
assert!(
enclosure_rules(&mut gp.problem, &place, &faces, 0, 3, 0.5, None).is_err(),
"an empty region must be refused"
);
}
fn anything_goes(protos: usize) -> [Vec<u32>; 4] {
let full = (1u32 << protos) - 1;
let row = vec![full; protos];
[row.clone(), row.clone(), row.clone(), row]
}
#[test]
fn different_seeds_give_different_arrangements() {
let (w, h, protos) = (6usize, 6usize, 4usize);
let weights = vec![1.0; protos];
let full = (1u32 << protos) - 1;
let grids: Vec<Vec<usize>> = (1..=8u64)
.map(|seed| {
let mut gp = GridProblem::encode(&anything_goes(protos), &vec![full; w * h], w, h, protos)
.expect("encodes");
preference_rules(&mut gp.problem, &gp.place, &weights, seed).expect("wishes");
gp.read(&gp.problem.solve(0).expect("solves")).expect("reads")
})
.collect();
let distinct: std::collections::BTreeSet<&Vec<usize>> = grids.iter().collect();
assert!(distinct.len() >= 7, "8 seeds gave only {} distinct arrangements", distinct.len());
}
#[test]
fn the_same_seed_gives_the_same_arrangement() {
let (w, h, protos) = (6usize, 6usize, 4usize);
let weights = vec![1.0, 2.0, 0.5, 1.5];
let full = (1u32 << protos) - 1;
let once = |seed: u64| {
let mut gp = GridProblem::encode(&anything_goes(protos), &vec![full; w * h], w, h, protos)
.expect("encodes");
preference_rules(&mut gp.problem, &gp.place, &weights, seed).expect("wishes");
gp.read(&gp.problem.solve(0).expect("solves")).expect("reads")
};
let first = once(99);
for round in 1..5 {
assert_eq!(once(99), first, "round {round} produced a different world from one seed");
}
}
#[test]
fn an_unconstrained_grid_gets_exactly_what_it_wished_for() {
let (w, h, protos) = (5usize, 5usize, 3usize);
let weights = vec![1.0, 1.0, 1.0];
let full = (1u32 << protos) - 1;
let mut gp = GridProblem::encode(&anything_goes(protos), &vec![full; w * h], w, h, protos)
.expect("encodes");
preference_rules(&mut gp.problem, &gp.place, &weights, 4242).expect("wishes");
let s = gp.problem.solve(0).expect("solves");
assert_eq!(s.unmet(), 0, "nothing forbids any wish, so none should be overruled");
use crate::rng::DetRng;
let mut rng = crate::rng::seeded(4242);
let total: f64 = weights.iter().sum();
let dream: Vec<usize> = (0..w * h)
.map(|_| {
let mut r = rng.unit() * total;
let mut k = weights.len() - 1;
for (i, x) in weights.iter().enumerate() {
r -= x;
if r <= 0.0 {
k = i;
break;
}
}
k
})
.collect();
assert_eq!(gp.read(&s).expect("reads"), dream, "the arrangement must be the dream itself");
}
#[test]
fn the_wishes_follow_the_authors_weights() {
let (w, h) = (24usize, 24usize);
let weights = vec![10.0, 1.0];
let full = 0b11u32;
let mut gp = GridProblem::encode(&anything_goes(2), &vec![full; w * h], w, h, 2).expect("encodes");
preference_rules(&mut gp.problem, &gp.place, &weights, 7).expect("wishes");
let grid = gp.read(&gp.problem.solve(0).expect("solves")).expect("reads");
let common = grid.iter().filter(|&&p| p == 0).count();
let share = common as f64 / grid.len() as f64;
assert!((0.83..=0.99).contains(&share), "the 10:1 prototype took {share:.3} of the cells");
}
#[test]
fn a_wish_the_rules_forbid_is_overruled_without_being_charged_for() {
let (w, h) = (4usize, 4usize);
let apart = vec![0b01u32, 0b10];
let support = [apart.clone(), apart.clone(), apart.clone(), apart];
let mut gp = GridProblem::encode(&support, &vec![0b11u32; w * h], w, h, 2).expect("encodes");
preference_rules(&mut gp.problem, &gp.place, &[1.0, 1.0], 3).expect("wishes");
let s = gp.problem.solve(0).expect("solves");
let grid = gp.read(&s).expect("reads");
assert!(grid.iter().all(|&p| p == grid[0]), "the rules admit only a uniform grid: {grid:?}");
assert_eq!(s.unmet(), 0, "a hint that the rules overrule costs nothing");
}
#[test]
fn preference_rules_refuse_a_grammar_that_cannot_be_drawn_from() {
let mut gp = GridProblem::encode(&anything_goes(2), &[0b11; 4], 2, 2, 2).expect("encodes");
let place = gp.place.clone();
assert!(preference_rules(&mut gp.problem, &place, &[], 1).is_err(), "no prototypes");
assert!(preference_rules(&mut gp.problem, &place, &[0.0, 0.0], 1).is_err(), "no weight");
assert!(preference_rules(&mut gp.problem, &place, &[1.0, -1.0], 1).is_err(), "negative");
assert!(preference_rules(&mut gp.problem, &place, &[1.0, f64::NAN], 1).is_err(), "not a number");
}
#[test]
fn a_soft_constraint_that_can_be_met_costs_nothing() {
let mut p = Problem::new();
let a = Lit::pos(p.var());
let b = Lit::pos(p.var());
p.count(&[a, b], 2, 2, Some(5));
let s = p.solve(0).expect("solves");
assert_eq!(s.unmet(), 0, "a satisfiable wish must be granted, not paid for");
assert!(s.get(a.var()) && s.get(b.var()));
}
#[test]
fn an_unmeetable_soft_constraint_costs_exactly_its_weight_and_no_more() {
let mut p = Problem::new();
let a = Lit::pos(p.var());
p.add_clause(&[!a]); p.implies_any(p.always_true(), &[a], Some(7)); let s = p.solve(0).expect("a soft conflict must still solve");
assert_eq!(s.unmet(), 7);
assert!(!s.get(a.var()));
}
#[test]
fn hard_unsatisfiability_is_named_differently_from_wishes_that_conflict() {
let mut p = Problem::new();
let a = Lit::pos(p.var());
p.add_clause(&[a]);
p.add_clause(&[!a]);
p.implies_any(p.always_true(), &[a], Some(3));
let err = p.solve(0).expect_err("hard clauses that contradict cannot be bought off");
assert!(err.contains("no arrangement"), "must name the hard refusal: {err}");
}
#[test]
fn the_optimum_is_the_same_every_time() {
let mut p = Problem::new();
let vars: Vec<Lit> = (0..6).map(|_| Lit::pos(p.var())).collect();
p.count(&vars, 0, 2, None);
for (i, v) in vars.iter().enumerate() {
p.implies_any(p.always_true(), &[*v], Some(1 + i as u32));
}
let first = p.solve(0).expect("solves");
assert_eq!(optimal_cost(&p), Some(first.unmet()), "and it is the true optimum");
for round in 1..5 {
let again = p.solve(0).expect("solves");
assert_eq!(again.values(), first.values(), "round {round} chose differently");
}
}
#[test]
fn the_optimiser_carries_a_region_sized_problem_with_one_wish_per_cell() {
let (w, h, protos) = (12usize, 12usize, 3usize);
let support: [Vec<u32>; 4] = [
vec![0b011, 0b110, 0b101],
vec![0b111, 0b011, 0b110],
vec![0b110, 0b101, 0b011],
vec![0b101, 0b111, 0b110],
];
let full = (1u32 << protos) - 1;
let mut gp = GridProblem::encode(&support, &vec![full; w * h], w, h, protos).expect("encodes");
let mut state = 0x9E37_79B9_7F4A_7C15u64;
let mut next = move || {
state = state.wrapping_mul(6_364_136_223_846_793_005).wrapping_add(1_442_695_040_888_963_407);
let x = state;
(x ^ (x >> 33)).wrapping_mul(0xff51_afd7_ed55_8ccd)
};
for c in 0..w * h {
let wanted = (next() % protos as u64) as usize;
let weight = 1 + (next() % 5) as u32;
let Some(v) = gp.place.get(c).and_then(|row| row.get(wanted)) else {
continue;
};
gp.problem.implies_any(gp.problem.always_true(), &[Lit::pos(*v)], Some(weight));
}
assert_eq!(gp.problem.soft_count(), w * h, "one wish per cell");
let s = gp.problem.solve(0).expect("a region-sized optimisation must complete");
let grid = gp.read(&s).expect("and read back as a grid");
assert_eq!(grid.len(), w * h);
const STEPS: [(i32, i32); 4] = [(0, -1), (1, 0), (0, 1), (-1, 0)];
for z in 0..h {
for x in 0..w {
for (dir, (dx, dz)) in STEPS.iter().enumerate() {
let (nx, nz) = (x as i32 + dx, z as i32 + dz);
if nx < 0 || nz < 0 || nx as usize >= w || nz as usize >= h {
continue;
}
let (a, b) = (grid[z * w + x], grid[nz as usize * w + nx as usize]);
assert!(support[dir][a] & (1 << b) != 0, "({x},{z}) {a} beside {b} on {dir}");
}
}
}
println!("region-sized optimum: unmet {} over {} wishes", s.unmet(), w * h);
}
#[test]
fn weights_of_zero_are_free_to_break() {
let mut p = Problem::new();
let a = Lit::pos(p.var());
p.add_clause(&[!a]);
p.implies_any(p.always_true(), &[a], Some(0));
let s = p.solve(0).expect("solves");
assert_eq!(s.unmet(), 0, "a wish worth nothing costs nothing to abandon");
}
#[test]
fn solve_is_deterministic() {
let support: [Vec<u32>; 4] = [
vec![0b011, 0b110, 0b101],
vec![0b111, 0b011, 0b110],
vec![0b110, 0b101, 0b011],
vec![0b101, 0b111, 0b110],
];
let gp = GridProblem::encode(&support, &[0b111; 36], 6, 6, 3).expect("encodes");
let first = gp.read(&gp.problem.solve(0).expect("solves")).expect("reads");
for round in 1..6 {
let again = gp.read(&gp.problem.solve(0).expect("solves")).expect("reads");
assert_eq!(again, first, "round {round} produced a different arrangement");
}
}
#[test]
fn a_solved_grid_is_legal_under_the_support_table_it_was_built_from() {
let support: [Vec<u32>; 4] = [
vec![0b011, 0b110, 0b101],
vec![0b111, 0b011, 0b110],
vec![0b110, 0b101, 0b011],
vec![0b101, 0b111, 0b110],
];
let (w, h) = (6usize, 6usize);
let gp = GridProblem::encode(&support, &[0b111; 36], w, h, 3).expect("encodes");
let grid = gp.read(&gp.problem.solve(0).expect("solves")).expect("reads");
const STEPS: [(i32, i32); 4] = [(0, -1), (1, 0), (0, 1), (-1, 0)];
for z in 0..h {
for x in 0..w {
for (dir, (dx, dz)) in STEPS.iter().enumerate() {
let (nx, nz) = (x as i32 + dx, z as i32 + dz);
if nx < 0 || nz < 0 || nx as usize >= w || nz as usize >= h {
continue;
}
let a = grid[z * w + x];
let b = grid[nz as usize * w + nx as usize];
assert!(
support[dir][a] & (1 << b) != 0,
"({x},{z}) = {a} may not have {b} on side {dir}"
);
}
}
}
}
#[test]
fn read_refuses_a_model_that_leaves_a_cell_unfilled() {
let gp = GridProblem::encode(&checkerboard_support(), &[0b11; 4], 2, 2, 2).expect("encodes");
let elsewhere = Problem::new();
let alien = Solution::from_assignment(&elsewhere, vec![true]).expect("a trivial solution");
let err = gp.read(&alien).expect_err("a foreign solution cannot fill this grid");
assert!(err.contains("no prototype in cell"), "the refusal must name what is wrong: {err}");
}
}