sat_solver/sat/variable_selection.rs
1#![warn(clippy::all, clippy::pedantic, clippy::nursery, clippy::cargo)]
2//! Defines variable selection strategies (decision heuristics) for SAT solvers.
3//!
4//! The variable selection strategy (often called the decision heuristic) determines
5//! which unassigned variable to pick next and what truth value to assign to it
6//! during the search process. A good heuristic can significantly impact solver performance.
7//!
8//! This module provides:
9//! - The `VariableSelection` trait, defining a common interface for these strategies.
10//! - Several concrete implementations:
11//! - `VsidsHeap`: Implements VSIDS (Variable State Independent Decaying Sum) using a binary heap
12//! to efficiently find the variable with the highest activity score.
13//! - `Vsids`: A simpler VSIDS implementation that iterates through scores without a heap.
14//! - `FixedOrder`: Selects variables in a fixed, predefined order (e.g. by variable index).
15//! - `RandomOrder`: Selects variables in a (shuffled) random order.
16//! - `JeroslowWangOneSided`: Implements the Jeroslow-Wang heuristic (one-sided version),
17//! which scores literals based on their occurrence in short clauses.
18//! - `JeroslowWangTwoSided`: A two-sided version of the Jeroslow-Wang heuristic.
19
20use crate::sat::assignment::Assignment;
21use crate::sat::literal::Literal;
22use clap::ValueEnum;
23use fastrand::Rng;
24use ordered_float::OrderedFloat;
25use std::collections::BinaryHeap;
26use std::fmt::{Debug, Display, Formatter};
27use std::ops::{Index, IndexMut};
28
29/// Trait defining the interface for variable selection strategies.
30///
31/// Implementors of this trait decide which unassigned variable to choose next for a decision
32/// and, implicitly or explicitly, which polarity to assign to it.
33///
34/// # Type Parameters
35///
36/// * `L`: The `Literal` type used by the solver.
37pub trait VariableSelection<L: Literal>: Debug + Clone {
38 /// Creates a new instance of the variable selection strategy.
39 ///
40 /// # Parameters
41 ///
42 /// * `num_vars`: The total number of variables in the SAT problem.
43 /// * `vars`: A slice of all literals (often used for initial setup, though not all strategies use it).
44 /// * `clauses`: A slice of clauses in the SAT problem. Some strategies use this to
45 /// initialise scores.
46 fn new<C: AsRef<[L]>>(num_vars: usize, vars: &[L], clauses: &[C]) -> Self;
47
48 /// Picks an unassigned variable for the next decision.
49 ///
50 /// Returns `Some(L)` with the chosen literal (variable and its assigned polarity)
51 /// if an unassigned variable can be found. Returns `None` if all variables
52 /// relevant to the strategy are assigned.
53 ///
54 /// # Parameters
55 ///
56 /// * `assignment`: The current state of variable assignments in the solver.
57 fn pick<A: Assignment>(&mut self, assignment: &A) -> Option<L>;
58
59 /// Increases the activity score of the specified variables.
60 ///
61 /// This is called when a conflict is analysed, and the variables
62 /// involved in the conflict (or the learned clause) are "rewarded".
63 ///
64 /// # Parameters
65 ///
66 /// * `vars`: An iterator over literals whose activity should be bumped.
67 fn bumps<T: IntoIterator<Item = L>>(&mut self, vars: T);
68
69 /// Decays the activity scores of all variables.
70 ///
71 /// This is periodically called to give more weight to recently active variables.
72 /// The exact mechanism can vary (e.g. multiplying all scores by a factor,
73 /// or increasing an activity increment).
74 ///
75 /// # Parameters
76 ///
77 /// * `decay`: The decay factor. The interpretation of this factor depends on the specific strategy.
78 /// For VSIDS-like strategies, it's typically a value slightly less than 1 (e.g. 0.95),
79 /// and `activity_inc` is divided by this factor.
80 fn decay(&mut self, decay: f64);
81
82 /// Returns the number of decisions made by this strategy.
83 fn decisions(&self) -> usize;
84}
85/// A structure representing a score entry in the VSIDS heap.
86///
87/// This struct is used to store a variable's (literal's) activity score along with its index,
88/// primarily for use in a `BinaryHeap`. `OrderedFloat` is used for the score to ensure
89/// proper ordering in the heap, as `f64` itself does not implement `Ord`.
90#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
91struct ScoreEntry {
92 /// The activity score of the literal.
93 /// `OrderedFloat` is used to ensure that `f64` values can be correctly ordered in the `BinaryHeap`,
94 /// as `f64` itself does not implement `Ord` due to NaN.
95 score: OrderedFloat<f64>,
96 /// The index of the literal (as returned by `Literal::index()`).
97 lit_idx: usize,
98}
99
100/// A constant for the VSIDS rescale threshold.
101/// When the activity increment (`activity_inc`) in VSIDS-like heuristics exceeds this value,
102/// scores are rescaled to prevent floating-point overflow and maintain numerical stability.
103const VSIDS_RESCALE_THRESHOLD: f64 = 1e100;
104/// A constant for the VSIDS rescale factor.
105/// Scores are multiplied by this factor during rescaling to bring them back into a manageable range.
106const VSIDS_RESCALE_FACTOR: f64 = 1e-100;
107
108/// A constant for the decay factor used in VSIDS-like heuristics.
109pub const VSIDS_DECAY_FACTOR: f64 = 0.95;
110
111/// A VSIDS (Variable State Independent Decaying Sum) implementation using a binary heap.
112///
113/// This strategy maintains an activity score for each literal. Variables involved in recent
114/// conflicts get their scores "bumped". Periodically, all scores "decay".
115/// The `BinaryHeap` is used to efficiently find the unassigned literal with the highest activity score.
116/// This implementation aims for `O(log N)` pick operations on average, where N is the number of variables.
117#[derive(Debug, Clone)]
118pub struct VsidsHeap {
119 /// Stores the current activity scores for each literal. The index corresponds to `Literal::index()`.
120 /// For a variable `v`, `scores[2*v]` might be negative literal and `scores[2*v+1]` positive,
121 /// or based on `Literal::index()` directly.
122 scores: Vec<f64>,
123 /// The amount by which a literal's score is incremented during a "bump".
124 /// This value itself is adjusted during decay operations (typically increased, effectively
125 /// making newer bumps more significant relative to older scores before rescaling).
126 activity_inc: f64,
127 /// A max-heap storing `ScoreEntry` items. It allows efficient retrieval of the literal
128 /// with the highest current activity score. Entries in the heap might be stale;
129 /// they are validated against the `scores` vector upon extraction.
130 heap: BinaryHeap<ScoreEntry>,
131 /// Counter for the number of decisions made by this strategy.
132 num_decisions: usize,
133}
134
135/// `Index` trait implementation for the VSIDS heap.
136/// Allows for indexing into the scores vector using a `usize` that typically corresponds
137/// to `Literal::index()`.
138///
139/// # Safety
140/// This implementation uses `get_unchecked` for performance
141impl Index<usize> for VsidsHeap {
142 type Output = f64;
143
144 fn index(&self, index: usize) -> &Self::Output {
145 unsafe { self.scores.get_unchecked(index) }
146 }
147}
148
149/// `IndexMut` trait implementation for the VSIDS heap.
150/// Allows for mutable indexing into the scores vector using a `usize` that typically
151/// corresponds to `Literal::index()`.
152///
153/// # Safety
154/// Similar to the `Index` implementation, this uses `get_unchecked_mut`.
155impl IndexMut<usize> for VsidsHeap {
156 fn index_mut(&mut self, index: usize) -> &mut Self::Output {
157 unsafe { self.scores.get_unchecked_mut(index) }
158 }
159}
160
161impl VsidsHeap {
162 /// Bumps the activity of a literal at the given index and updates the heap.
163 ///
164 /// The score in the `scores` vector is updated by adding `activity_inc`.
165 /// A new `ScoreEntry` reflecting this updated score is pushed onto the heap.
166 /// Old entries for the same literal with outdated scores are not immediately
167 /// removed from the heap; they are filtered out during `pick` when their score
168 /// doesn't match the `scores` vector, or eventually cleared during `rescale_scores`.
169 fn bump_internal(&mut self, lit_idx: usize) {
170 let score_ref = unsafe { self.scores.get_unchecked_mut(lit_idx) };
171 *score_ref += self.activity_inc;
172 let new_score = *score_ref;
173
174 // push the new score entry into the heap.
175 // at this point we don't need to remove the old entry, that is handled when rescaling.
176 self.heap.push(ScoreEntry {
177 score: OrderedFloat(new_score),
178 lit_idx,
179 });
180 }
181
182 /// Rescales all activity scores and the activity increment.
183 ///
184 /// This operation is performed when `activity_inc` grows too large (exceeds
185 /// `VSIDS_RESCALE_THRESHOLD`). All scores in the `scores` vector are multiplied
186 /// by `VSIDS_RESCALE_FACTOR` (a value less than 1) to bring them into a smaller range,
187 /// preventing floating-point overflow. The `activity_inc` is also rescaled.
188 ///
189 /// After rescaling scores, the `heap` is rebuilt from the `scores` vector. This ensures
190 /// that the heap accurately reflects the new scores and also cleans up any stale
191 /// `ScoreEntry` items (those whose scores in the heap did not match the `scores` vector).
192 fn rescale_scores(&mut self) {
193 // rescale the scores to prevent overflow
194 self.scores
195 .iter_mut()
196 .for_each(|s| *s *= VSIDS_RESCALE_FACTOR);
197 // rescale the activity increment to prevent overflow
198 self.activity_inc *= VSIDS_RESCALE_FACTOR;
199
200 let mut score_entries: Vec<ScoreEntry> = Vec::with_capacity(self.scores.len());
201 for (lit_idx, &score) in self.scores.iter().enumerate() {
202 score_entries.push(ScoreEntry {
203 score: OrderedFloat(score),
204 lit_idx,
205 });
206 }
207 self.heap = BinaryHeap::from(score_entries);
208 }
209}
210
211/// `VariableSelection` trait implementation for the VSIDS heap.
212impl<L: Literal> VariableSelection<L> for VsidsHeap {
213 /// Creates a new instance of the `VsidsHeap` strategy.
214 ///
215 /// Initialises scores for all possible literals (2 * `num_vars`) to zero.
216 /// The `activity_inc` is initialised to 1.0.
217 /// The heap is populated with initial zero scores for all literals.
218 fn new<C: AsRef<[L]>>(num_vars: usize, _: &[L], _: &[C]) -> Self {
219 let scores = vec![0.0; num_vars * 2];
220 let mut score_entries: Vec<ScoreEntry> = Vec::with_capacity(scores.len());
221 for (lit_idx, &score) in scores.iter().enumerate() {
222 score_entries.push(ScoreEntry {
223 score: OrderedFloat(score),
224 lit_idx,
225 });
226 }
227 let heap = BinaryHeap::from(score_entries);
228
229 Self {
230 scores,
231 activity_inc: 1.0,
232 heap,
233 num_decisions: 0,
234 }
235 }
236
237 /// Picks an unassigned variable with the highest activity score.
238 ///
239 /// It repeatedly pops from the `heap` until it finds a `ScoreEntry` whose
240 /// corresponding variable is unassigned and whose score matches the current score
241 /// in the `scores` vector (to filter out stale entries).
242 ///
243 /// The complexity is typically `O(log N)` for heap pop, but can be higher if many
244 /// stale entries need to be processed.
245 fn pick<A: Assignment>(&mut self, assignment: &A) -> Option<L> {
246 while let Some(entry) = self.heap.pop() {
247 let current_score = unsafe { *self.scores.get_unchecked(entry.lit_idx) };
248
249 if entry.score != OrderedFloat(current_score) {
250 continue;
251 }
252
253 if assignment[entry.lit_idx / 2].is_unassigned() {
254 self.num_decisions = self.num_decisions.wrapping_add(1);
255 return Some(L::from_index(entry.lit_idx));
256 }
257 }
258
259 None
260 }
261
262 /// Bumps the activity scores of the specified literals.
263 ///
264 /// For each literal in `vars`, its score is incremented by `activity_inc`,
265 /// and the heap is updated accordingly via `bump_internal`.
266 fn bumps<T: IntoIterator<Item = L>>(&mut self, vars: T) {
267 for i in vars {
268 self.bump_internal(i.index());
269 }
270 }
271
272 /// Decays the activity scores.
273 ///
274 /// In this VSIDS implementation, scores are not directly decayed. Instead,
275 /// `activity_inc` is divided by the `decay` factor (typically `0 < decay < 1`,
276 /// e.g. 0.95, so `activity_inc` effectively increases). This gives more weight
277 /// to variables involved in more recent conflicts.
278 ///
279 /// If `activity_inc` exceeds `VSIDS_RESCALE_THRESHOLD`, all scores and
280 /// `activity_inc` are rescaled by multiplying with `VSIDS_RESCALE_FACTOR`
281 /// to prevent overflow and maintain precision. This rescaling step involves
282 /// rebuilding the heap, which can be an `O(N)` operation.
283 fn decay(&mut self, decay: f64) {
284 self.activity_inc /= decay;
285
286 if self.activity_inc > VSIDS_RESCALE_THRESHOLD {
287 self.rescale_scores();
288 }
289 }
290
291 /// Returns the total number of decisions made by this strategy instance.
292 fn decisions(&self) -> usize {
293 self.num_decisions
294 }
295}
296
297/// A VSIDS (Variable State Independent Decaying Sum) implementation without a binary heap.
298///
299/// This strategy maintains an activity score for each literal. When picking a variable,
300/// it iterates through all literals to find the unassigned one with the highest score.
301/// This makes the `pick` operation `O(N)` where N is the number of variables.
302///
303/// `EARLY_EXIT` is a compile-time constant. If `true`, the `pick` operation may
304/// exit early if a literal with a sufficiently high score is found, potentially
305/// saving computation but possibly leading to suboptimal choices in some cases.
306#[derive(Debug, Clone, PartialEq, Default, PartialOrd)]
307pub struct Vsids<const EARLY_EXIT: bool = false> {
308 /// Stores the current activity scores for each literal.
309 /// The index corresponds to `Literal::index()`.
310 scores: Vec<f64>,
311 /// The amount by which a literal's score is incremented during a "bump".
312 /// Adjusted during decay operations.
313 activity_inc: f64,
314 /// The total number of variables in the problem. Used to iterate appropriately.
315 num_vars: usize,
316 /// Counter for the number of decisions made by this strategy.
317 num_decisions: usize,
318}
319
320/// `Index` trait implementation for the VSIDS variable selection strategy.
321/// Allows for indexing into the scores vector.
322///
323/// # Safety
324/// Should only be used internally and with care due to the unchecked indexing.
325impl<const E: bool> Index<usize> for Vsids<E> {
326 type Output = f64;
327
328 fn index(&self, index: usize) -> &Self::Output {
329 unsafe { self.scores.get_unchecked(index) }
330 }
331}
332
333/// `IndexMut` trait implementation for the VSIDS variable selection strategy.
334/// Allows for mutable indexing into the scores vector.
335///
336/// # Safety
337/// Again, should only be used internally and with care due to the unchecked indexing.
338impl<const E: bool> IndexMut<usize> for Vsids<E> {
339 fn index_mut(&mut self, index: usize) -> &mut Self::Output {
340 unsafe { self.scores.get_unchecked_mut(index) }
341 }
342}
343
344/// `Vsids` implementation.
345impl<const E: bool> Vsids<E> {
346 /// Bumps the activity of a variable at the given literal's index.
347 /// The score of the literal `i` is incremented by `activity_inc`.
348 /// This method is internal to the `Vsids` strategy.
349 fn bump(&mut self, i: impl Literal) {
350 self[i.index()] += self.activity_inc;
351 }
352}
353
354/// `VariableSelection` trait implementation for the VSIDS variable selection strategy.
355impl<L: Literal, const E: bool> VariableSelection<L> for Vsids<E> {
356 /// Creates a new instance of the `Vsids` strategy.
357 /// Initialises scores for all literals to zero and `activity_inc` to 1.0.
358 fn new<C: AsRef<[L]>>(num_vars: usize, _: &[L], _: &[C]) -> Self {
359 Self {
360 scores: vec![0.0; num_vars * 2], // scores for positive and negative literals
361 activity_inc: 1.0,
362 num_vars,
363 num_decisions: 0,
364 }
365 }
366
367 /// Picks an unassigned variable with the highest activity score by iterating through all literals.
368 ///
369 /// This operation is `O(N)` where N is the number of variables.
370 /// If `EARLY_EXIT` (compile-time const generic `E`) is true, the iteration
371 /// might stop early if a literal's score exceeds `self.activity_inc * 5.0`.
372 fn pick<A: Assignment>(&mut self, assignment: &A) -> Option<L> {
373 let mut max_score = f64::MIN;
374 let mut max = None;
375
376 for (i, &v) in self.scores.iter().enumerate() {
377 let lit = L::from_index(i);
378 if v > max_score && assignment[lit.variable() as usize].is_unassigned() {
379 max = Some(lit);
380 max_score = v;
381
382 if E && v > self.activity_inc * 5.0 {
383 break;
384 }
385 }
386 }
387
388 if max.is_some() {
389 self.num_decisions = self.num_decisions.wrapping_add(1);
390 }
391
392 max
393 }
394
395 /// Bumps the activity scores of the specified literals.
396 /// Each literal's score is incremented by `activity_inc`.
397 fn bumps<T: IntoIterator<Item = L>>(&mut self, vars: T) {
398 for i in vars {
399 self.bump(i);
400 }
401 }
402
403 /// Decays the activity scores.
404 /// Similar to `VsidsHeap`, `activity_inc` is divided by `decay` (effectively increasing it).
405 /// If `activity_inc` exceeds `VSIDS_RESCALE_THRESHOLD`, all scores are rescaled by
406 /// multiplying with `VSIDS_RESCALE_FACTOR`, and `activity_inc` is also set to
407 /// `VSIDS_RESCALE_FACTOR` (note: this differs slightly from `VsidsHeap` which rescales `activity_inc`
408 /// by the same factor; here it's reset). This prevents overflow.
409 fn decay(&mut self, decay: f64) {
410 self.activity_inc /= decay;
411
412 if self.activity_inc > VSIDS_RESCALE_THRESHOLD {
413 self.scores
414 .iter_mut()
415 .for_each(|s| *s *= VSIDS_RESCALE_FACTOR);
416
417 self.activity_inc *= VSIDS_RESCALE_FACTOR;
418 }
419 }
420
421 /// Returns the total number of decisions made by this strategy instance.
422 fn decisions(&self) -> usize {
423 self.num_decisions
424 }
425}
426
427/// A variable selection strategy that picks variables in a fixed, predefined order.
428///
429/// It iterates through variables by their index (1 to `num_vars - 1`).
430/// The first unassigned variable encountered is chosen.
431/// The polarity (truth value) assigned to the chosen variable is selected randomly.
432///
433/// This strategy is simple and deterministic in variable choice (once an order is set)
434/// but random in polarity.
435#[derive(Debug, Clone, Default)]
436pub struct FixedOrder {
437 /// The total number of variables in the problem.
438 var_count: usize,
439 /// Random number generator used for selecting polarity.
440 rand: Rng,
441 /// Counter for the number of decisions made by this strategy.
442 num_decisions: usize,
443}
444
445impl<L: Literal> VariableSelection<L> for FixedOrder {
446 /// Creates a new `FixedOrder` strategy.
447 /// `num_vars` is the total number of variables.
448 /// The random number generator is initialised with a fixed seed (0).
449 fn new<C: AsRef<[L]>>(num_vars: usize, _: &[L], _: &[C]) -> Self {
450 Self {
451 var_count: num_vars,
452 rand: Rng::with_seed(0), // fixed seed for deterministic behavior
453 num_decisions: 0,
454 }
455 }
456
457 /// Picks the first unassigned variable according to a fixed order (variable index).
458 /// Variables are typically checked from index 1 up to `var_count - 1`.
459 /// The polarity of the chosen variable is selected randomly.
460 fn pick<A: Assignment>(&mut self, assignment: &A) -> Option<L> {
461 #![allow(clippy::cast_possible_truncation)]
462 (1..self.var_count as u32).find_map(|v| {
463 if assignment[v as usize].is_unassigned() {
464 let polarity = self.rand.bool();
465 let lit = L::new(v, polarity);
466 self.num_decisions = self.num_decisions.wrapping_add(1);
467 Some(lit)
468 } else {
469 None
470 }
471 })
472 }
473
474 /// No-op
475 fn bumps<T: IntoIterator<Item = L>>(&mut self, _: T) {}
476
477 /// No-op
478 fn decay(&mut self, _: f64) {}
479
480 /// Returns the total number of decisions made by this strategy instance.
481 fn decisions(&self) -> usize {
482 self.num_decisions
483 }
484}
485
486/// A variable selection strategy that picks variables in a random order.
487///
488/// On initialisation, it creates a shuffled list of variable indices.
489/// When `pick` is called, it iterates through this shuffled list and chooses the
490/// first unassigned variable it encounters. The polarity of the chosen variable
491/// is selected randomly.
492///
493/// The initial shuffling makes the order random but fixed for the lifetime of this instance
494#[derive(Debug, Clone, Default)]
495pub struct RandomOrder {
496 /// Random number generator for selecting polarity and initial shuffling.
497 /// `RefCell` for mutable access in `pick`.
498 rand: Rng,
499 /// A list of variable indices, shuffled once at initialisation.
500 /// The `pick` method iterates through this list.
501 /// Indices stored here are typically 0-indexed if `num_vars` in `new` is count.
502 vars: Vec<usize>,
503 /// Counter for the number of decisions made by this strategy.
504 num_decisions: usize,
505}
506
507/// `VariableSelection` trait implementation for the random order variable selection strategy.
508impl<L: Literal> VariableSelection<L> for RandomOrder {
509 /// Creates a new `RandomOrder` strategy.
510 /// `num_vars` is the total number of variables.
511 /// It initialises a list of variable indices (e.g. 0 to `num_vars - 1` or 1 to `num_vars`)
512 /// and shuffles it using an `Rng` seeded with 0 for deterministic shuffling.
513 /// A separate `Rng` (newly created) is stored for random polarity selection.
514 fn new<C: AsRef<[L]>>(num_vars: usize, _: &[L], _: &[C]) -> Self {
515 let mut shuffle_rng = Rng::with_seed(0);
516 let mut indices: Vec<usize> = (0..num_vars).collect();
517 shuffle_rng.shuffle(indices.as_mut_slice());
518
519 Self {
520 vars: indices,
521 rand: Rng::new(),
522 num_decisions: 0,
523 }
524 }
525
526 /// Picks an unassigned variable from the pre-shuffled list.
527 /// Iterates through the `vars` list. The first variable index `i` encountered
528 /// for which `assignment[i]` is unassigned is chosen.
529 /// Polarity is chosen randomly.
530 fn pick<A: Assignment>(&mut self, assignment: &A) -> Option<L> {
531 #![allow(clippy::cast_possible_truncation)]
532
533 for &var_idx in &self.vars {
534 if assignment[var_idx].is_unassigned() {
535 let polarity = self.rand.bool();
536 self.num_decisions = self.num_decisions.wrapping_add(1);
537 return Some(L::new(var_idx as u32, polarity));
538 }
539 }
540 None
541 }
542
543 /// No-op
544 fn bumps<T: IntoIterator<Item = L>>(&mut self, _: T) {}
545
546 /// No-op
547 fn decay(&mut self, _: f64) {}
548
549 /// Returns the total number of decisions made by this strategy instance.
550 fn decisions(&self) -> usize {
551 self.num_decisions
552 }
553}
554
555/// Calculates the Jeroslow-Wang weight for a clause.
556/// The weight is `2^(-clause_len)`. Returns 0.0 if `clause_len` is 0.
557///
558/// # Parameters
559/// * `clause_len`: The length (number of literals) of the clause.
560///
561/// # Returns
562/// The calculated weight as an `f64`.
563#[allow(clippy::cast_possible_wrap, clippy::cast_possible_truncation)]
564fn jw_weight(clause_len: usize) -> f64 {
565 if clause_len == 0 {
566 0.0
567 } else {
568 2.0_f64.powi(-(clause_len as i32))
569 }
570}
571
572/// Initialises scores for Jeroslow-Wang heuristics.
573///
574/// Iterates through all clauses, calculates the JW weight for each clause,
575/// and adds this weight to the score of each literal present in that clause.
576/// The scores vector is indexed by `Literal::index()`.
577///
578/// # Type Parameters
579/// * `L`: The `Literal` type.
580/// * `C`: A type that can be referenced as a slice of literals `&[L]`, typically a clause.
581///
582/// # Parameters
583/// * `num_vars`: The total number of variables. Scores vector will be `num_vars * 2`.
584/// * `clauses`: A slice of clauses to compute initial scores from.
585///
586/// # Returns
587/// A `Vec<f64>` containing the initial scores for each literal.
588fn init_jw_scores<L: Literal, C: AsRef<[L]>>(num_vars: usize, clauses: &[C]) -> Vec<f64> {
589 let mut scores = vec![0.0; num_vars * 2];
590 for clause_ref in clauses {
591 let clause = clause_ref.as_ref();
592 let len = clause.len();
593 let weight = jw_weight(len);
594 for &lit in clause {
595 scores[lit.index()] += weight;
596 }
597 }
598 scores
599}
600
601/// Implements the Jeroslow-Wang heuristic (one-sided version).
602///
603/// This heuristic scores literals based on their occurrences in clauses,
604/// giving higher weight to literals in shorter clauses (specifically, `2^(-clause_length)`).
605/// When picking a variable, it chooses the literal (variable and polarity) with the
606/// highest score among unassigned literals.
607///
608/// There's a small chance (10%) that the chosen literal's polarity will be flipped,
609/// adding an element of randomness.
610#[derive(Debug, Clone, Default)]
611pub struct JeroslowWangOneSided {
612 /// Stores the Jeroslow-Wang scores for each literal, indexed by `Literal::index()`.
613 /// These scores are typically initialised based on clause lengths and do not decay.
614 scores: Vec<f64>,
615 /// Random number generator, used for the 10% chance of flipping polarity.
616 rand: Rng,
617 /// Counter for the number of decisions made by this strategy.
618 num_decisions: usize,
619}
620
621impl<L: Literal> VariableSelection<L> for JeroslowWangOneSided {
622 /// Creates a new `JeroslowWangOneSided` strategy.
623 /// Initialises scores using `init_jw_scores` based on the provided clauses.
624 /// The random number generator is seeded with 0.
625 fn new<C: AsRef<[L]>>(num_vars: usize, _: &[L], clauses: &[C]) -> Self {
626 let scores = init_jw_scores(num_vars, clauses);
627
628 Self {
629 scores,
630 rand: Rng::with_seed(0),
631 num_decisions: 0,
632 }
633 }
634
635 /// Picks an unassigned literal with the highest Jeroslow-Wang score.
636 /// It iterates through all literals, finds the unassigned one with the maximum score.
637 /// There is a 10% chance the polarity of this chosen literal will be flipped.
638 fn pick<A: Assignment>(&mut self, assignment: &A) -> Option<L> {
639 let mut max_score = f64::MIN;
640 let mut best_lit = None;
641
642 for i in 0..self.scores.len() {
643 let lit = L::from_index(i);
644 if assignment[lit.variable() as usize].is_unassigned() {
645 let score = self.scores[i];
646 if score > max_score {
647 max_score = score;
648 best_lit = Some(lit);
649 }
650 }
651 }
652
653 best_lit.map(|lit| {
654 self.num_decisions = self.num_decisions.wrapping_add(1);
655
656 if self.rand.f64() < 0.1 {
657 lit.negated()
658 } else {
659 lit
660 }
661 })
662 }
663
664 /// JW scores are static, based on initial clause structure.
665 fn bumps<T: IntoIterator<Item = L>>(&mut self, _: T) {}
666
667 /// JW scores are static.
668 fn decay(&mut self, _: f64) {}
669
670 /// Returns the total number of decisions made by this strategy instance.
671 fn decisions(&self) -> usize {
672 self.num_decisions
673 }
674}
675
676/// Implements a two-sided version of the Jeroslow-Wang heuristic.
677///
678/// This heuristic first selects a variable based on the sum of JW scores for its
679/// positive and negative literals (`score(v) = JW(v) + JW(-v)`).
680/// Once a variable is chosen, it picks the polarity (positive or negative)
681/// that has the higher individual JW score. If scores are equal, polarity is chosen randomly.
682///
683/// Similar to the one-sided version, there's a small chance (10%) that the final
684/// chosen literal's polarity will be flipped.
685#[derive(Debug, Clone, Default)]
686pub struct JeroslowWangTwoSided {
687 /// Stores the Jeroslow-Wang scores for each literal, indexed by `Literal::index()`.
688 scores: Vec<f64>,
689 /// The total number of variables in the problem.
690 num_vars: usize,
691 /// Random number generator, used for tie-breaking polarity and the 10% polarity flip.
692 rand: Rng,
693 /// Counter for the number of decisions made by this strategy.
694 num_decisions: usize,
695}
696
697impl<L: Literal> VariableSelection<L> for JeroslowWangTwoSided {
698 /// Creates a new `JeroslowWangTwoSided` strategy.
699 /// Initialises scores using `init_jw_scores`.
700 /// The random number generator is seeded with 0.
701 fn new<C: AsRef<[L]>>(num_vars: usize, _: &[L], clauses: &[C]) -> Self {
702 let scores = init_jw_scores(num_vars, clauses);
703
704 Self {
705 scores,
706 num_vars,
707 rand: Rng::with_seed(0),
708 num_decisions: 0,
709 }
710 }
711
712 /// Picks a variable and its polarity using the two-sided Jeroslow-Wang heuristic.
713 /// 1. Finds the unassigned variable `v` maximising `JW(v_pos) + JW(v_neg)`.
714 /// 2. For the chosen `v`, selects the literal (positive or negative) with the higher JW score.
715 /// Ties are broken randomly.
716 /// 3. There is a 10% chance the polarity of this final literal is flipped.
717 fn pick<A: Assignment>(&mut self, assignment: &A) -> Option<L> {
718 let mut max_score = f64::MIN;
719 let mut best_lit = None;
720
721 for i in 0..self.num_vars {
722 if assignment[i].is_unassigned() {
723 let neg_lit_idx = 2 * i;
724 let pos_lit_idx = 2 * i + 1;
725
726 let combined_score = self.scores[neg_lit_idx] + self.scores[pos_lit_idx];
727
728 if combined_score > max_score {
729 max_score = combined_score;
730 best_lit = Some(i);
731 }
732 }
733 }
734 best_lit
735 .map(|v_dix| {
736 let ng_lit_idx = 2 * v_dix;
737 let pos_lit_idx = 2 * v_dix + 1;
738
739 let neg_score = self.scores[ng_lit_idx];
740 let pos_score = self.scores[pos_lit_idx];
741
742 if neg_score > pos_score {
743 L::from_index(ng_lit_idx)
744 } else if neg_score < pos_score {
745 L::from_index(pos_lit_idx)
746 } else if self.rand.bool() {
747 L::from_index(ng_lit_idx)
748 } else {
749 L::from_index(pos_lit_idx)
750 }
751 })
752 .map(|lit| {
753 self.num_decisions = self.num_decisions.wrapping_add(1);
754 if self.rand.f64() < 0.1 {
755 lit.negated()
756 } else {
757 lit
758 }
759 })
760 }
761
762 /// Bumps scores. NO-OP for `JeroslowWangTwoSided`.
763 /// JW scores are static.
764 fn bumps<T: IntoIterator<Item = L>>(&mut self, _: T) {}
765
766 /// Decays scores. NO-OP for `JeroslowWangTwoSided`.
767 /// JW scores are static.
768 fn decay(&mut self, _: f64) {}
769
770 /// Returns the total number of decisions made by this strategy instance.
771 fn decisions(&self) -> usize {
772 self.num_decisions
773 }
774}
775
776/// Enum to encapsulate different variable selection implementations.
777#[derive(Debug, Clone)]
778pub enum VariableSelectionImpls {
779 /// VSIDS with a vector.
780 Vsids(Vsids),
781 /// VSIDS with a binary heap.
782 VsidsHeap(VsidsHeap),
783 /// Fixed order variable selection.
784 FixedOrder(FixedOrder),
785 /// Random order variable selection.
786 RandomOrder(RandomOrder),
787 /// Jeroslow-Wang one-sided variable selection.
788 JeroslowWangOneSided(JeroslowWangOneSided),
789 /// Jeroslow-Wang two-sided variable selection.
790 JeroslowWangTwoSided(JeroslowWangTwoSided),
791}
792
793impl<L: Literal> VariableSelection<L> for VariableSelectionImpls {
794 fn new<C: AsRef<[L]>>(num_vars: usize, lits: &[L], clauses: &[C]) -> Self {
795 Self::Vsids(Vsids::<false>::new(num_vars, lits, clauses))
796 }
797
798 fn pick<A: Assignment>(&mut self, assignment: &A) -> Option<L> {
799 match self {
800 Self::Vsids(vsids) => vsids.pick(assignment),
801 Self::VsidsHeap(vsids_heap) => vsids_heap.pick(assignment),
802 Self::FixedOrder(fixed_order) => fixed_order.pick(assignment),
803 Self::RandomOrder(random_order) => random_order.pick(assignment),
804 Self::JeroslowWangOneSided(jw_one_sided) => jw_one_sided.pick(assignment),
805 Self::JeroslowWangTwoSided(jw_two_sided) => jw_two_sided.pick(assignment),
806 }
807 }
808
809 fn bumps<T: IntoIterator<Item = L>>(&mut self, vars: T) {
810 match self {
811 Self::Vsids(vsids) => vsids.bumps(vars),
812 Self::VsidsHeap(vsids_heap) => vsids_heap.bumps(vars),
813 Self::FixedOrder(fixed_order) => fixed_order.bumps(vars),
814 Self::RandomOrder(random_order) => random_order.bumps(vars),
815 Self::JeroslowWangOneSided(jw_one_sided) => jw_one_sided.bumps(vars),
816 Self::JeroslowWangTwoSided(jw_two_sided) => jw_two_sided.bumps(vars),
817 }
818 }
819
820 fn decay(&mut self, decay: f64) {
821 match self {
822 Self::Vsids(vsids) => <Vsids as VariableSelection<L>>::decay(vsids, decay),
823 Self::VsidsHeap(vsids_heap) => {
824 <VsidsHeap as VariableSelection<L>>::decay(vsids_heap, decay);
825 }
826 Self::FixedOrder(fixed_order) => {
827 <FixedOrder as VariableSelection<L>>::decay(fixed_order, decay);
828 }
829 Self::RandomOrder(random_order) => {
830 <RandomOrder as VariableSelection<L>>::decay(random_order, decay);
831 }
832 Self::JeroslowWangOneSided(jw_one_sided) => {
833 <JeroslowWangOneSided as VariableSelection<L>>::decay(jw_one_sided, decay);
834 }
835 Self::JeroslowWangTwoSided(jw_two_sided) => {
836 <JeroslowWangTwoSided as VariableSelection<L>>::decay(jw_two_sided, decay);
837 }
838 }
839 }
840
841 fn decisions(&self) -> usize {
842 match self {
843 Self::Vsids(vsids) => <Vsids as VariableSelection<L>>::decisions(vsids),
844 Self::VsidsHeap(vsids_heap) => {
845 <VsidsHeap as VariableSelection<L>>::decisions(vsids_heap)
846 }
847 Self::FixedOrder(fixed_order) => {
848 <FixedOrder as VariableSelection<L>>::decisions(fixed_order)
849 }
850 Self::RandomOrder(random_order) => {
851 <RandomOrder as VariableSelection<L>>::decisions(random_order)
852 }
853 Self::JeroslowWangOneSided(jw_one_sided) => {
854 <JeroslowWangOneSided as VariableSelection<L>>::decisions(jw_one_sided)
855 }
856 Self::JeroslowWangTwoSided(jw_two_sided) => {
857 <JeroslowWangTwoSided as VariableSelection<L>>::decisions(jw_two_sided)
858 }
859 }
860 }
861}
862
863/// Enum representing different variable selection strategies.
864#[derive(Debug, Clone, PartialEq, Eq, Copy, PartialOrd, Ord, Hash, Default, ValueEnum)]
865pub enum VariableSelectionType {
866 /// VSIDS (Variable State Independent Decaying Sum) selection strategy.
867 #[default]
868 Vsids,
869 /// VSIDS with a binary heap for efficient selection.
870 VsidsHeap,
871 /// Fixed order selection strategy, iterating through variables in a fixed order.
872 FixedOrder,
873 /// Random order selection strategy, picking variables in a random order.
874 RandomOrder,
875 /// Jeroslow-Wang one-sided selection strategy, scoring literals based on clause lengths.
876 JeroslowWangOneSided,
877 /// Jeroslow-Wang two-sided selection strategy, scoring variables based on both polarities.
878 JeroslowWangTwoSided,
879}
880
881impl Display for VariableSelectionType {
882 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
883 match self {
884 Self::Vsids => write!(f, "vsids"),
885 Self::VsidsHeap => write!(f, "vsids_heap"),
886 Self::FixedOrder => write!(f, "fixed_order"),
887 Self::RandomOrder => write!(f, "random_order"),
888 Self::JeroslowWangOneSided => write!(f, "jw_one_sided"),
889 Self::JeroslowWangTwoSided => write!(f, "jw_two_sided"),
890 }
891 }
892}
893
894impl VariableSelectionType {
895 /// Converts the `VariableSelectionType` into a concrete implementation of `VariableSelection`.
896 pub fn to_impl<L: Literal>(
897 self,
898 num_vars: usize,
899 lits: &[L],
900 clauses: &[impl AsRef<[L]>],
901 ) -> VariableSelectionImpls {
902 match self {
903 Self::Vsids => {
904 VariableSelectionImpls::Vsids(Vsids::<false>::new(num_vars, lits, clauses))
905 }
906 Self::VsidsHeap => {
907 VariableSelectionImpls::VsidsHeap(VsidsHeap::new(num_vars, lits, clauses))
908 }
909 Self::FixedOrder => {
910 VariableSelectionImpls::FixedOrder(FixedOrder::new(num_vars, lits, clauses))
911 }
912 Self::RandomOrder => {
913 VariableSelectionImpls::RandomOrder(RandomOrder::new(num_vars, lits, clauses))
914 }
915 Self::JeroslowWangOneSided => VariableSelectionImpls::JeroslowWangOneSided(
916 JeroslowWangOneSided::new(num_vars, lits, clauses),
917 ),
918 Self::JeroslowWangTwoSided => VariableSelectionImpls::JeroslowWangTwoSided(
919 JeroslowWangTwoSided::new(num_vars, lits, clauses),
920 ),
921 }
922 }
923}
924
925#[cfg(test)]
926mod tests {
927 use super::*;
928 use crate::sat::assignment::VecAssignment;
929 use crate::sat::clause::Clause;
930 use crate::sat::literal::{PackedLiteral, Variable};
931 use smallvec::SmallVec;
932
933 type TestLiteral = PackedLiteral;
934 type TestClause = Clause<TestLiteral, SmallVec<[TestLiteral; 8]>>;
935 type TestAssignment = VecAssignment;
936
937 fn test_selector_behavior<VS: VariableSelection<TestLiteral>>(
938 mut selector: VS,
939 num_vars_for_assignment: usize,
940 expected_decisions_if_all_picked: usize,
941 ) {
942 let mut assignment = TestAssignment::new(num_vars_for_assignment);
943 let mut picked_literals = Vec::new();
944
945 for _ in 0..num_vars_for_assignment {
946 if let Some(picked_lit) = selector.pick(&assignment) {
947 assert!(
948 assignment[picked_lit.variable() as usize].is_unassigned(),
949 "Picked an already assigned variable's literal form. Var: {}",
950 picked_lit.variable()
951 );
952 assignment.assign(picked_lit);
953 picked_literals.push(picked_lit);
954 } else {
955 break;
956 }
957 }
958
959 assert_eq!(
960 picked_literals.len(),
961 expected_decisions_if_all_picked,
962 "Number of picked literals/decisions mismatch"
963 );
964 assert_eq!(
965 selector.decisions(),
966 expected_decisions_if_all_picked,
967 "Internal decision count mismatch"
968 );
969
970 let picked_vars_set: std::collections::HashSet<Variable> =
971 picked_literals.iter().map(|l| l.variable()).collect();
972 assert_eq!(
973 picked_vars_set.len(),
974 picked_literals.len(),
975 "Picked variables are not unique"
976 );
977
978 selector.bumps(picked_literals.clone());
979 selector.decay(0.95);
980 }
981
982 const NUM_VARS_TEST: usize = 5;
983 const EMPTY_LITS_SLICE: [TestLiteral; 0] = [];
984 const EMPTY_CLAUSES_SLICE: [TestClause; 0] = [];
985
986 #[test]
987 fn test_vsids_heap() {
988 let selector = VsidsHeap::new(NUM_VARS_TEST, &EMPTY_LITS_SLICE, &EMPTY_CLAUSES_SLICE);
989 test_selector_behavior(selector, NUM_VARS_TEST, NUM_VARS_TEST);
990 }
991
992 #[test]
993 fn test_vsids_no_heap() {
994 let selector = Vsids::<false>::new(NUM_VARS_TEST, &EMPTY_LITS_SLICE, &EMPTY_CLAUSES_SLICE);
995 test_selector_behavior(selector, NUM_VARS_TEST, NUM_VARS_TEST);
996 }
997
998 #[test]
999 fn test_vsids_no_heap_early_exit() {
1000 let selector = Vsids::<true>::new(NUM_VARS_TEST, &EMPTY_LITS_SLICE, &EMPTY_CLAUSES_SLICE);
1001 test_selector_behavior(selector, NUM_VARS_TEST, NUM_VARS_TEST);
1002 }
1003
1004 // #[test]
1005 // fn test_fixed_order() {
1006 // let selector = FixedOrder::new(NUM_VARS_TEST, &EMPTY_LITS_SLICE, &EMPTY_CLAUSES_SLICE);
1007 // test_selector_behavior(selector, NUM_VARS_TEST, NUM_VARS_TEST);
1008 // }
1009
1010 #[test]
1011 fn test_random_order() {
1012 let selector = RandomOrder::new(NUM_VARS_TEST, &EMPTY_LITS_SLICE, &EMPTY_CLAUSES_SLICE);
1013 test_selector_behavior(selector, NUM_VARS_TEST, NUM_VARS_TEST);
1014 }
1015
1016 // #[test]
1017 // fn test_jeroslow_wang_one_sided() {
1018 // let clauses = vec![clause(&[1, 2]), clause(&[-1, 3]), clause(&[2])];
1019 // let num_problem_vars = 3;
1020 // let selector = JeroslowWangOneSided::new(num_problem_vars , &EMPTY_LITS_SLICE, &clauses);
1021 // test_selector_behavior(selector, num_problem_vars, num_problem_vars);
1022 // }
1023 //
1024 // #[test]
1025 // fn test_jeroslow_wang_two_sided() {
1026 // let clauses = vec![clause(&[1, 2]), clause(&[-1, 3]), clause(&[2])];
1027 // let num_problem_vars = 3;
1028 // let selector = JeroslowWangTwoSided::new(num_problem_vars + 1, &EMPTY_LITS_SLICE, &clauses);
1029 // test_selector_behavior(selector, num_problem_vars, num_problem_vars);
1030 // }
1031
1032 #[test]
1033 fn test_vsids_heap_bumping_and_decay() {
1034 let mut selector: VsidsHeap = VariableSelection::<TestLiteral>::new(
1035 NUM_VARS_TEST,
1036 &EMPTY_LITS_SLICE,
1037 &EMPTY_CLAUSES_SLICE,
1038 );
1039 let mut assignment = TestAssignment::new(NUM_VARS_TEST);
1040
1041 let lit1_opt = selector.pick(&assignment);
1042 assert!(lit1_opt.is_some(), "Should be able to pick a literal");
1043 let lit1: TestLiteral = lit1_opt.unwrap();
1044 assignment.assign(lit1);
1045 let lit1_score_before_bump = selector.scores[lit1.index()];
1046
1047 selector.bumps(std::iter::once(lit1));
1048 let lit1_score_after_bump = selector.scores[lit1.index()];
1049 assert!(
1050 lit1_score_after_bump > lit1_score_before_bump || selector.activity_inc == 0.0,
1051 "Score should increase after bump"
1052 );
1053 assert!(
1054 (lit1_score_after_bump - (lit1_score_before_bump + selector.activity_inc)).abs() < 1e-9,
1055 "Score did not increase by activity_inc"
1056 );
1057
1058 let _lit2_opt: Option<TestLiteral> = selector.pick(&assignment);
1059 }
1060
1061 #[test]
1062 fn test_vsids_heap_rescaling() {
1063 let mut selector: VsidsHeap =
1064 VariableSelection::<PackedLiteral>::new(1, &EMPTY_LITS_SLICE, &EMPTY_CLAUSES_SLICE);
1065 selector.activity_inc = VSIDS_RESCALE_THRESHOLD * 2.0;
1066
1067 let lit_to_bump = PackedLiteral::new(0, true);
1068 assert!((selector.scores[lit_to_bump.index()] - 0.0).abs() < 1e-9);
1069
1070 selector.bumps(std::iter::once(lit_to_bump));
1071 let score_before_decay_and_rescale = selector.scores[lit_to_bump.index()];
1072 assert!(
1073 VSIDS_RESCALE_THRESHOLD
1074 .mul_add(-2.0, score_before_decay_and_rescale)
1075 .abs()
1076 < 1e-9,
1077 );
1078
1079 <VsidsHeap as VariableSelection<TestLiteral>>::decay(&mut selector, 2.0);
1080 assert!((selector.activity_inc - VSIDS_RESCALE_THRESHOLD).abs() < 1e-9);
1081 assert!(
1082 (selector.scores[lit_to_bump.index()] - score_before_decay_and_rescale).abs() < 1e-9,
1083 );
1084
1085 selector.activity_inc = VSIDS_RESCALE_THRESHOLD * 10.0;
1086 <VsidsHeap as VariableSelection<TestLiteral>>::decay(&mut selector, 2.0);
1087
1088 let score_after_rescale = selector.scores[lit_to_bump.index()];
1089 let activity_inc_after_rescale = selector.activity_inc;
1090
1091 let expected_rescaled_score = score_before_decay_and_rescale * VSIDS_RESCALE_FACTOR;
1092 assert!(
1093 (score_after_rescale - expected_rescaled_score).abs() < 1e-9,
1094 "Score not rescaled correctly"
1095 );
1096
1097 let expected_rescaled_activity_inc = (VSIDS_RESCALE_THRESHOLD * 5.0) * VSIDS_RESCALE_FACTOR;
1098 assert!(
1099 (activity_inc_after_rescale - expected_rescaled_activity_inc).abs() < 1e-9,
1100 "Activity increment not rescaled correctly"
1101 );
1102 assert!(
1103 activity_inc_after_rescale < VSIDS_RESCALE_THRESHOLD,
1104 "Activity increment should be below threshold after rescale"
1105 );
1106 }
1107}