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}