#![warn(clippy::all, clippy::pedantic, clippy::nursery, clippy::cargo)]
use crate::sat::assignment::Assignment;
use crate::sat::literal::Literal;
use clap::ValueEnum;
use fastrand::Rng;
use ordered_float::OrderedFloat;
use std::collections::BinaryHeap;
use std::fmt::{Debug, Display, Formatter};
use std::ops::{Index, IndexMut};
pub trait VariableSelection<L: Literal>: Debug + Clone {
fn new<C: AsRef<[L]>>(num_vars: usize, vars: &[L], clauses: &[C]) -> Self;
fn pick<A: Assignment>(&mut self, assignment: &A) -> Option<L>;
fn bumps<T: IntoIterator<Item = L>>(&mut self, vars: T);
fn decay(&mut self, decay: f64);
fn decisions(&self) -> usize;
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
struct ScoreEntry {
score: OrderedFloat<f64>,
lit_idx: usize,
}
const VSIDS_RESCALE_THRESHOLD: f64 = 1e100;
const VSIDS_RESCALE_FACTOR: f64 = 1e-100;
pub const VSIDS_DECAY_FACTOR: f64 = 0.95;
#[derive(Debug, Clone)]
pub struct VsidsHeap {
scores: Vec<f64>,
activity_inc: f64,
heap: BinaryHeap<ScoreEntry>,
num_decisions: usize,
}
impl Index<usize> for VsidsHeap {
type Output = f64;
fn index(&self, index: usize) -> &Self::Output {
unsafe { self.scores.get_unchecked(index) }
}
}
impl IndexMut<usize> for VsidsHeap {
fn index_mut(&mut self, index: usize) -> &mut Self::Output {
unsafe { self.scores.get_unchecked_mut(index) }
}
}
impl VsidsHeap {
fn bump_internal(&mut self, lit_idx: usize) {
let score_ref = unsafe { self.scores.get_unchecked_mut(lit_idx) };
*score_ref += self.activity_inc;
let new_score = *score_ref;
self.heap.push(ScoreEntry {
score: OrderedFloat(new_score),
lit_idx,
});
}
fn rescale_scores(&mut self) {
self.scores
.iter_mut()
.for_each(|s| *s *= VSIDS_RESCALE_FACTOR);
self.activity_inc *= VSIDS_RESCALE_FACTOR;
let mut score_entries: Vec<ScoreEntry> = Vec::with_capacity(self.scores.len());
for (lit_idx, &score) in self.scores.iter().enumerate() {
score_entries.push(ScoreEntry {
score: OrderedFloat(score),
lit_idx,
});
}
self.heap = BinaryHeap::from(score_entries);
}
}
impl<L: Literal> VariableSelection<L> for VsidsHeap {
fn new<C: AsRef<[L]>>(num_vars: usize, _: &[L], _: &[C]) -> Self {
let scores = vec![0.0; num_vars * 2];
let mut score_entries: Vec<ScoreEntry> = Vec::with_capacity(scores.len());
for (lit_idx, &score) in scores.iter().enumerate() {
score_entries.push(ScoreEntry {
score: OrderedFloat(score),
lit_idx,
});
}
let heap = BinaryHeap::from(score_entries);
Self {
scores,
activity_inc: 1.0,
heap,
num_decisions: 0,
}
}
fn pick<A: Assignment>(&mut self, assignment: &A) -> Option<L> {
while let Some(entry) = self.heap.pop() {
let current_score = unsafe { *self.scores.get_unchecked(entry.lit_idx) };
if entry.score != OrderedFloat(current_score) {
continue;
}
if assignment[entry.lit_idx / 2].is_unassigned() {
self.num_decisions = self.num_decisions.wrapping_add(1);
return Some(L::from_index(entry.lit_idx));
}
}
None
}
fn bumps<T: IntoIterator<Item = L>>(&mut self, vars: T) {
for i in vars {
self.bump_internal(i.index());
}
}
fn decay(&mut self, decay: f64) {
self.activity_inc /= decay;
if self.activity_inc > VSIDS_RESCALE_THRESHOLD {
self.rescale_scores();
}
}
fn decisions(&self) -> usize {
self.num_decisions
}
}
#[derive(Debug, Clone, PartialEq, Default, PartialOrd)]
pub struct Vsids<const EARLY_EXIT: bool = false> {
scores: Vec<f64>,
activity_inc: f64,
num_vars: usize,
num_decisions: usize,
}
impl<const E: bool> Index<usize> for Vsids<E> {
type Output = f64;
fn index(&self, index: usize) -> &Self::Output {
unsafe { self.scores.get_unchecked(index) }
}
}
impl<const E: bool> IndexMut<usize> for Vsids<E> {
fn index_mut(&mut self, index: usize) -> &mut Self::Output {
unsafe { self.scores.get_unchecked_mut(index) }
}
}
impl<const E: bool> Vsids<E> {
fn bump(&mut self, i: impl Literal) {
self[i.index()] += self.activity_inc;
}
}
impl<L: Literal, const E: bool> VariableSelection<L> for Vsids<E> {
fn new<C: AsRef<[L]>>(num_vars: usize, _: &[L], _: &[C]) -> Self {
Self {
scores: vec![0.0; num_vars * 2], activity_inc: 1.0,
num_vars,
num_decisions: 0,
}
}
fn pick<A: Assignment>(&mut self, assignment: &A) -> Option<L> {
let mut max_score = f64::MIN;
let mut max = None;
for (i, &v) in self.scores.iter().enumerate() {
let lit = L::from_index(i);
if v > max_score && assignment[lit.variable() as usize].is_unassigned() {
max = Some(lit);
max_score = v;
if E && v > self.activity_inc * 5.0 {
break;
}
}
}
if max.is_some() {
self.num_decisions = self.num_decisions.wrapping_add(1);
}
max
}
fn bumps<T: IntoIterator<Item = L>>(&mut self, vars: T) {
for i in vars {
self.bump(i);
}
}
fn decay(&mut self, decay: f64) {
self.activity_inc /= decay;
if self.activity_inc > VSIDS_RESCALE_THRESHOLD {
self.scores
.iter_mut()
.for_each(|s| *s *= VSIDS_RESCALE_FACTOR);
self.activity_inc *= VSIDS_RESCALE_FACTOR;
}
}
fn decisions(&self) -> usize {
self.num_decisions
}
}
#[derive(Debug, Clone, Default)]
pub struct FixedOrder {
var_count: usize,
rand: Rng,
num_decisions: usize,
}
impl<L: Literal> VariableSelection<L> for FixedOrder {
fn new<C: AsRef<[L]>>(num_vars: usize, _: &[L], _: &[C]) -> Self {
Self {
var_count: num_vars,
rand: Rng::with_seed(0), num_decisions: 0,
}
}
fn pick<A: Assignment>(&mut self, assignment: &A) -> Option<L> {
#![allow(clippy::cast_possible_truncation)]
(1..self.var_count as u32).find_map(|v| {
if assignment[v as usize].is_unassigned() {
let polarity = self.rand.bool();
let lit = L::new(v, polarity);
self.num_decisions = self.num_decisions.wrapping_add(1);
Some(lit)
} else {
None
}
})
}
fn bumps<T: IntoIterator<Item = L>>(&mut self, _: T) {}
fn decay(&mut self, _: f64) {}
fn decisions(&self) -> usize {
self.num_decisions
}
}
#[derive(Debug, Clone, Default)]
pub struct RandomOrder {
rand: Rng,
vars: Vec<usize>,
num_decisions: usize,
}
impl<L: Literal> VariableSelection<L> for RandomOrder {
fn new<C: AsRef<[L]>>(num_vars: usize, _: &[L], _: &[C]) -> Self {
let mut shuffle_rng = Rng::with_seed(0);
let mut indices: Vec<usize> = (0..num_vars).collect();
shuffle_rng.shuffle(indices.as_mut_slice());
Self {
vars: indices,
rand: Rng::new(),
num_decisions: 0,
}
}
fn pick<A: Assignment>(&mut self, assignment: &A) -> Option<L> {
#![allow(clippy::cast_possible_truncation)]
for &var_idx in &self.vars {
if assignment[var_idx].is_unassigned() {
let polarity = self.rand.bool();
self.num_decisions = self.num_decisions.wrapping_add(1);
return Some(L::new(var_idx as u32, polarity));
}
}
None
}
fn bumps<T: IntoIterator<Item = L>>(&mut self, _: T) {}
fn decay(&mut self, _: f64) {}
fn decisions(&self) -> usize {
self.num_decisions
}
}
#[allow(clippy::cast_possible_wrap, clippy::cast_possible_truncation)]
fn jw_weight(clause_len: usize) -> f64 {
if clause_len == 0 {
0.0
} else {
2.0_f64.powi(-(clause_len as i32))
}
}
fn init_jw_scores<L: Literal, C: AsRef<[L]>>(num_vars: usize, clauses: &[C]) -> Vec<f64> {
let mut scores = vec![0.0; num_vars * 2];
for clause_ref in clauses {
let clause = clause_ref.as_ref();
let len = clause.len();
let weight = jw_weight(len);
for &lit in clause {
scores[lit.index()] += weight;
}
}
scores
}
#[derive(Debug, Clone, Default)]
pub struct JeroslowWangOneSided {
scores: Vec<f64>,
rand: Rng,
num_decisions: usize,
}
impl<L: Literal> VariableSelection<L> for JeroslowWangOneSided {
fn new<C: AsRef<[L]>>(num_vars: usize, _: &[L], clauses: &[C]) -> Self {
let scores = init_jw_scores(num_vars, clauses);
Self {
scores,
rand: Rng::with_seed(0),
num_decisions: 0,
}
}
fn pick<A: Assignment>(&mut self, assignment: &A) -> Option<L> {
let mut max_score = f64::MIN;
let mut best_lit = None;
for i in 0..self.scores.len() {
let lit = L::from_index(i);
if assignment[lit.variable() as usize].is_unassigned() {
let score = self.scores[i];
if score > max_score {
max_score = score;
best_lit = Some(lit);
}
}
}
best_lit.map(|lit| {
self.num_decisions = self.num_decisions.wrapping_add(1);
if self.rand.f64() < 0.1 {
lit.negated()
} else {
lit
}
})
}
fn bumps<T: IntoIterator<Item = L>>(&mut self, _: T) {}
fn decay(&mut self, _: f64) {}
fn decisions(&self) -> usize {
self.num_decisions
}
}
#[derive(Debug, Clone, Default)]
pub struct JeroslowWangTwoSided {
scores: Vec<f64>,
num_vars: usize,
rand: Rng,
num_decisions: usize,
}
impl<L: Literal> VariableSelection<L> for JeroslowWangTwoSided {
fn new<C: AsRef<[L]>>(num_vars: usize, _: &[L], clauses: &[C]) -> Self {
let scores = init_jw_scores(num_vars, clauses);
Self {
scores,
num_vars,
rand: Rng::with_seed(0),
num_decisions: 0,
}
}
fn pick<A: Assignment>(&mut self, assignment: &A) -> Option<L> {
let mut max_score = f64::MIN;
let mut best_lit = None;
for i in 0..self.num_vars {
if assignment[i].is_unassigned() {
let neg_lit_idx = 2 * i;
let pos_lit_idx = 2 * i + 1;
let combined_score = self.scores[neg_lit_idx] + self.scores[pos_lit_idx];
if combined_score > max_score {
max_score = combined_score;
best_lit = Some(i);
}
}
}
best_lit
.map(|v_dix| {
let ng_lit_idx = 2 * v_dix;
let pos_lit_idx = 2 * v_dix + 1;
let neg_score = self.scores[ng_lit_idx];
let pos_score = self.scores[pos_lit_idx];
if neg_score > pos_score {
L::from_index(ng_lit_idx)
} else if neg_score < pos_score {
L::from_index(pos_lit_idx)
} else if self.rand.bool() {
L::from_index(ng_lit_idx)
} else {
L::from_index(pos_lit_idx)
}
})
.map(|lit| {
self.num_decisions = self.num_decisions.wrapping_add(1);
if self.rand.f64() < 0.1 {
lit.negated()
} else {
lit
}
})
}
fn bumps<T: IntoIterator<Item = L>>(&mut self, _: T) {}
fn decay(&mut self, _: f64) {}
fn decisions(&self) -> usize {
self.num_decisions
}
}
#[derive(Debug, Clone)]
pub enum VariableSelectionImpls {
Vsids(Vsids),
VsidsHeap(VsidsHeap),
FixedOrder(FixedOrder),
RandomOrder(RandomOrder),
JeroslowWangOneSided(JeroslowWangOneSided),
JeroslowWangTwoSided(JeroslowWangTwoSided),
}
impl<L: Literal> VariableSelection<L> for VariableSelectionImpls {
fn new<C: AsRef<[L]>>(num_vars: usize, lits: &[L], clauses: &[C]) -> Self {
Self::Vsids(Vsids::<false>::new(num_vars, lits, clauses))
}
fn pick<A: Assignment>(&mut self, assignment: &A) -> Option<L> {
match self {
Self::Vsids(vsids) => vsids.pick(assignment),
Self::VsidsHeap(vsids_heap) => vsids_heap.pick(assignment),
Self::FixedOrder(fixed_order) => fixed_order.pick(assignment),
Self::RandomOrder(random_order) => random_order.pick(assignment),
Self::JeroslowWangOneSided(jw_one_sided) => jw_one_sided.pick(assignment),
Self::JeroslowWangTwoSided(jw_two_sided) => jw_two_sided.pick(assignment),
}
}
fn bumps<T: IntoIterator<Item = L>>(&mut self, vars: T) {
match self {
Self::Vsids(vsids) => vsids.bumps(vars),
Self::VsidsHeap(vsids_heap) => vsids_heap.bumps(vars),
Self::FixedOrder(fixed_order) => fixed_order.bumps(vars),
Self::RandomOrder(random_order) => random_order.bumps(vars),
Self::JeroslowWangOneSided(jw_one_sided) => jw_one_sided.bumps(vars),
Self::JeroslowWangTwoSided(jw_two_sided) => jw_two_sided.bumps(vars),
}
}
fn decay(&mut self, decay: f64) {
match self {
Self::Vsids(vsids) => <Vsids as VariableSelection<L>>::decay(vsids, decay),
Self::VsidsHeap(vsids_heap) => {
<VsidsHeap as VariableSelection<L>>::decay(vsids_heap, decay);
}
Self::FixedOrder(fixed_order) => {
<FixedOrder as VariableSelection<L>>::decay(fixed_order, decay);
}
Self::RandomOrder(random_order) => {
<RandomOrder as VariableSelection<L>>::decay(random_order, decay);
}
Self::JeroslowWangOneSided(jw_one_sided) => {
<JeroslowWangOneSided as VariableSelection<L>>::decay(jw_one_sided, decay);
}
Self::JeroslowWangTwoSided(jw_two_sided) => {
<JeroslowWangTwoSided as VariableSelection<L>>::decay(jw_two_sided, decay);
}
}
}
fn decisions(&self) -> usize {
match self {
Self::Vsids(vsids) => <Vsids as VariableSelection<L>>::decisions(vsids),
Self::VsidsHeap(vsids_heap) => {
<VsidsHeap as VariableSelection<L>>::decisions(vsids_heap)
}
Self::FixedOrder(fixed_order) => {
<FixedOrder as VariableSelection<L>>::decisions(fixed_order)
}
Self::RandomOrder(random_order) => {
<RandomOrder as VariableSelection<L>>::decisions(random_order)
}
Self::JeroslowWangOneSided(jw_one_sided) => {
<JeroslowWangOneSided as VariableSelection<L>>::decisions(jw_one_sided)
}
Self::JeroslowWangTwoSided(jw_two_sided) => {
<JeroslowWangTwoSided as VariableSelection<L>>::decisions(jw_two_sided)
}
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Copy, PartialOrd, Ord, Hash, Default, ValueEnum)]
pub enum VariableSelectionType {
#[default]
Vsids,
VsidsHeap,
FixedOrder,
RandomOrder,
JeroslowWangOneSided,
JeroslowWangTwoSided,
}
impl Display for VariableSelectionType {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
Self::Vsids => write!(f, "vsids"),
Self::VsidsHeap => write!(f, "vsids_heap"),
Self::FixedOrder => write!(f, "fixed_order"),
Self::RandomOrder => write!(f, "random_order"),
Self::JeroslowWangOneSided => write!(f, "jw_one_sided"),
Self::JeroslowWangTwoSided => write!(f, "jw_two_sided"),
}
}
}
impl VariableSelectionType {
pub fn to_impl<L: Literal>(
self,
num_vars: usize,
lits: &[L],
clauses: &[impl AsRef<[L]>],
) -> VariableSelectionImpls {
match self {
Self::Vsids => {
VariableSelectionImpls::Vsids(Vsids::<false>::new(num_vars, lits, clauses))
}
Self::VsidsHeap => {
VariableSelectionImpls::VsidsHeap(VsidsHeap::new(num_vars, lits, clauses))
}
Self::FixedOrder => {
VariableSelectionImpls::FixedOrder(FixedOrder::new(num_vars, lits, clauses))
}
Self::RandomOrder => {
VariableSelectionImpls::RandomOrder(RandomOrder::new(num_vars, lits, clauses))
}
Self::JeroslowWangOneSided => VariableSelectionImpls::JeroslowWangOneSided(
JeroslowWangOneSided::new(num_vars, lits, clauses),
),
Self::JeroslowWangTwoSided => VariableSelectionImpls::JeroslowWangTwoSided(
JeroslowWangTwoSided::new(num_vars, lits, clauses),
),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::sat::assignment::VecAssignment;
use crate::sat::clause::Clause;
use crate::sat::literal::{PackedLiteral, Variable};
use smallvec::SmallVec;
type TestLiteral = PackedLiteral;
type TestClause = Clause<TestLiteral, SmallVec<[TestLiteral; 8]>>;
type TestAssignment = VecAssignment;
fn test_selector_behavior<VS: VariableSelection<TestLiteral>>(
mut selector: VS,
num_vars_for_assignment: usize,
expected_decisions_if_all_picked: usize,
) {
let mut assignment = TestAssignment::new(num_vars_for_assignment);
let mut picked_literals = Vec::new();
for _ in 0..num_vars_for_assignment {
if let Some(picked_lit) = selector.pick(&assignment) {
assert!(
assignment[picked_lit.variable() as usize].is_unassigned(),
"Picked an already assigned variable's literal form. Var: {}",
picked_lit.variable()
);
assignment.assign(picked_lit);
picked_literals.push(picked_lit);
} else {
break;
}
}
assert_eq!(
picked_literals.len(),
expected_decisions_if_all_picked,
"Number of picked literals/decisions mismatch"
);
assert_eq!(
selector.decisions(),
expected_decisions_if_all_picked,
"Internal decision count mismatch"
);
let picked_vars_set: std::collections::HashSet<Variable> =
picked_literals.iter().map(|l| l.variable()).collect();
assert_eq!(
picked_vars_set.len(),
picked_literals.len(),
"Picked variables are not unique"
);
selector.bumps(picked_literals.clone());
selector.decay(0.95);
}
const NUM_VARS_TEST: usize = 5;
const EMPTY_LITS_SLICE: [TestLiteral; 0] = [];
const EMPTY_CLAUSES_SLICE: [TestClause; 0] = [];
#[test]
fn test_vsids_heap() {
let selector = VsidsHeap::new(NUM_VARS_TEST, &EMPTY_LITS_SLICE, &EMPTY_CLAUSES_SLICE);
test_selector_behavior(selector, NUM_VARS_TEST, NUM_VARS_TEST);
}
#[test]
fn test_vsids_no_heap() {
let selector = Vsids::<false>::new(NUM_VARS_TEST, &EMPTY_LITS_SLICE, &EMPTY_CLAUSES_SLICE);
test_selector_behavior(selector, NUM_VARS_TEST, NUM_VARS_TEST);
}
#[test]
fn test_vsids_no_heap_early_exit() {
let selector = Vsids::<true>::new(NUM_VARS_TEST, &EMPTY_LITS_SLICE, &EMPTY_CLAUSES_SLICE);
test_selector_behavior(selector, NUM_VARS_TEST, NUM_VARS_TEST);
}
#[test]
fn test_random_order() {
let selector = RandomOrder::new(NUM_VARS_TEST, &EMPTY_LITS_SLICE, &EMPTY_CLAUSES_SLICE);
test_selector_behavior(selector, NUM_VARS_TEST, NUM_VARS_TEST);
}
#[test]
fn test_vsids_heap_bumping_and_decay() {
let mut selector: VsidsHeap = VariableSelection::<TestLiteral>::new(
NUM_VARS_TEST,
&EMPTY_LITS_SLICE,
&EMPTY_CLAUSES_SLICE,
);
let mut assignment = TestAssignment::new(NUM_VARS_TEST);
let lit1_opt = selector.pick(&assignment);
assert!(lit1_opt.is_some(), "Should be able to pick a literal");
let lit1: TestLiteral = lit1_opt.unwrap();
assignment.assign(lit1);
let lit1_score_before_bump = selector.scores[lit1.index()];
selector.bumps(std::iter::once(lit1));
let lit1_score_after_bump = selector.scores[lit1.index()];
assert!(
lit1_score_after_bump > lit1_score_before_bump || selector.activity_inc == 0.0,
"Score should increase after bump"
);
assert!(
(lit1_score_after_bump - (lit1_score_before_bump + selector.activity_inc)).abs() < 1e-9,
"Score did not increase by activity_inc"
);
let _lit2_opt: Option<TestLiteral> = selector.pick(&assignment);
}
#[test]
fn test_vsids_heap_rescaling() {
let mut selector: VsidsHeap =
VariableSelection::<PackedLiteral>::new(1, &EMPTY_LITS_SLICE, &EMPTY_CLAUSES_SLICE);
selector.activity_inc = VSIDS_RESCALE_THRESHOLD * 2.0;
let lit_to_bump = PackedLiteral::new(0, true);
assert!((selector.scores[lit_to_bump.index()] - 0.0).abs() < 1e-9);
selector.bumps(std::iter::once(lit_to_bump));
let score_before_decay_and_rescale = selector.scores[lit_to_bump.index()];
assert!(
VSIDS_RESCALE_THRESHOLD
.mul_add(-2.0, score_before_decay_and_rescale)
.abs()
< 1e-9,
);
<VsidsHeap as VariableSelection<TestLiteral>>::decay(&mut selector, 2.0);
assert!((selector.activity_inc - VSIDS_RESCALE_THRESHOLD).abs() < 1e-9);
assert!(
(selector.scores[lit_to_bump.index()] - score_before_decay_and_rescale).abs() < 1e-9,
);
selector.activity_inc = VSIDS_RESCALE_THRESHOLD * 10.0;
<VsidsHeap as VariableSelection<TestLiteral>>::decay(&mut selector, 2.0);
let score_after_rescale = selector.scores[lit_to_bump.index()];
let activity_inc_after_rescale = selector.activity_inc;
let expected_rescaled_score = score_before_decay_and_rescale * VSIDS_RESCALE_FACTOR;
assert!(
(score_after_rescale - expected_rescaled_score).abs() < 1e-9,
"Score not rescaled correctly"
);
let expected_rescaled_activity_inc = (VSIDS_RESCALE_THRESHOLD * 5.0) * VSIDS_RESCALE_FACTOR;
assert!(
(activity_inc_after_rescale - expected_rescaled_activity_inc).abs() < 1e-9,
"Activity increment not rescaled correctly"
);
assert!(
activity_inc_after_rescale < VSIDS_RESCALE_THRESHOLD,
"Activity increment should be below threshold after rescale"
);
}
}