xlog_solve/instance.rs
1//! SAT instance representation types.
2//!
3//! This module provides the core types for representing SAT and MaxSAT instances
4//! in Conjunctive Normal Form (CNF). The representation is designed for efficient
5//! evaluation and GPU-friendly memory layouts.
6//!
7//! # Types
8//!
9//! - [`Literal`]: A propositional variable with optional negation
10//! - [`Clause`]: A disjunction (OR) of literals
11//! - [`Objective`]: Optimization objective (satisfaction, MaxSAT, etc.)
12//! - [`SolveInstance`]: A complete SAT/MaxSAT instance
13//!
14//! # Example
15//!
16//! ```
17//! use xlog_solve::{SolveInstance, Clause, Literal};
18//!
19//! // Encode: (x0 OR NOT x1) AND (x1 OR x2)
20//! let instance = SolveInstance::new(3, vec![
21//! Clause::new(vec![Literal::positive(0), Literal::negative(1)]),
22//! Clause::new(vec![Literal::positive(1), Literal::positive(2)]),
23//! ]);
24//!
25//! // Check if an assignment satisfies the formula
26//! let assignment = vec![true, false, true];
27//! assert!(instance.is_satisfied(&assignment));
28//! ```
29
30use std::cmp::Ordering;
31
32// =============================================================================
33// Literal - A propositional variable with optional negation
34// =============================================================================
35
36/// A propositional variable with optional negation.
37///
38/// Literals are the atomic units of SAT formulas. A literal is either a variable
39/// (positive literal) or the negation of a variable (negative literal).
40///
41/// Variables are 0-indexed internally but convert to 1-indexed for DIMACS format.
42///
43/// # Memory Layout
44///
45/// The struct is designed for efficient GPU transfer with a compact representation:
46/// - `var`: 4 bytes (u32 variable index)
47/// - `negated`: 1 byte (bool), with potential padding
48///
49/// For bulk GPU operations, consider using the packed representation via `to_packed()`.
50#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
51pub struct Literal {
52 /// The variable index (0-indexed).
53 pub var: u32,
54 /// Whether this literal is negated.
55 pub negated: bool,
56}
57
58impl Literal {
59 /// Creates a positive literal (variable without negation).
60 ///
61 /// # Arguments
62 ///
63 /// * `var` - The variable index (0-indexed)
64 ///
65 /// # Example
66 ///
67 /// ```
68 /// use xlog_solve::Literal;
69 /// let lit = Literal::positive(3);
70 /// assert_eq!(lit.var, 3);
71 /// assert!(!lit.negated);
72 /// ```
73 #[inline]
74 pub const fn positive(var: u32) -> Self {
75 Self {
76 var,
77 negated: false,
78 }
79 }
80
81 /// Creates a negative literal (negated variable).
82 ///
83 /// # Arguments
84 ///
85 /// * `var` - The variable index (0-indexed)
86 ///
87 /// # Example
88 ///
89 /// ```
90 /// use xlog_solve::Literal;
91 /// let lit = Literal::negative(3);
92 /// assert_eq!(lit.var, 3);
93 /// assert!(lit.negated);
94 /// ```
95 #[inline]
96 pub const fn negative(var: u32) -> Self {
97 Self { var, negated: true }
98 }
99
100 /// Creates a literal from a variable and sign.
101 ///
102 /// # Arguments
103 ///
104 /// * `var` - The variable index (0-indexed)
105 /// * `negated` - Whether the literal is negated
106 #[inline]
107 pub const fn new(var: u32, negated: bool) -> Self {
108 Self { var, negated }
109 }
110
111 /// Returns the negation of this literal.
112 ///
113 /// # Example
114 ///
115 /// ```
116 /// use xlog_solve::Literal;
117 /// let pos = Literal::positive(5);
118 /// let neg = pos.negate();
119 /// assert!(neg.negated);
120 /// assert_eq!(neg.var, 5);
121 /// ```
122 #[inline]
123 pub const fn negate(self) -> Self {
124 Self {
125 var: self.var,
126 negated: !self.negated,
127 }
128 }
129
130 /// Evaluates this literal under a given assignment.
131 ///
132 /// # Arguments
133 ///
134 /// * `assignment` - A slice of boolean values where index i represents variable i
135 ///
136 /// # Returns
137 ///
138 /// `true` if the literal is satisfied by the assignment, `false` otherwise.
139 ///
140 /// # Panics
141 ///
142 /// Panics if `self.var >= assignment.len()` (variable index out of bounds).
143 ///
144 /// # Example
145 ///
146 /// ```
147 /// use xlog_solve::Literal;
148 /// let lit = Literal::positive(1);
149 /// assert!(lit.eval(&[false, true, false])); // x1 = true
150 /// ```
151 #[inline]
152 pub fn eval(self, assignment: &[bool]) -> bool {
153 let value = assignment[self.var as usize];
154 if self.negated {
155 !value
156 } else {
157 value
158 }
159 }
160
161 /// Converts this literal to DIMACS format.
162 ///
163 /// DIMACS format uses 1-indexed variables, with negative numbers for negated literals.
164 ///
165 /// # Returns
166 ///
167 /// A signed integer where:
168 /// - Positive values represent positive literals (var + 1)
169 /// - Negative values represent negative literals (-(var + 1))
170 ///
171 /// # Example
172 ///
173 /// ```
174 /// use xlog_solve::Literal;
175 /// assert_eq!(Literal::positive(0).to_dimacs(), 1);
176 /// assert_eq!(Literal::negative(2).to_dimacs(), -3);
177 /// ```
178 #[inline]
179 pub fn to_dimacs(self) -> i32 {
180 let var_1indexed = (self.var + 1) as i32;
181 if self.negated {
182 -var_1indexed
183 } else {
184 var_1indexed
185 }
186 }
187
188 /// Creates a literal from DIMACS format.
189 ///
190 /// DIMACS format uses 1-indexed variables, with negative numbers for negated literals.
191 ///
192 /// # Arguments
193 ///
194 /// * `dimacs` - A non-zero signed integer in DIMACS format
195 ///
196 /// # Panics
197 ///
198 /// Panics if `dimacs == 0` (zero is not a valid DIMACS literal).
199 ///
200 /// # Example
201 ///
202 /// ```
203 /// use xlog_solve::Literal;
204 /// let pos = Literal::from_dimacs(3);
205 /// assert_eq!(pos.var, 2);
206 /// assert!(!pos.negated);
207 ///
208 /// let neg = Literal::from_dimacs(-5);
209 /// assert_eq!(neg.var, 4);
210 /// assert!(neg.negated);
211 /// ```
212 #[inline]
213 pub fn from_dimacs(dimacs: i32) -> Self {
214 assert!(dimacs != 0, "DIMACS literal cannot be zero");
215 let negated = dimacs < 0;
216 let var = dimacs.unsigned_abs() - 1;
217 Self { var, negated }
218 }
219
220 /// Returns a packed 32-bit representation suitable for GPU transfer.
221 ///
222 /// The packed format stores the variable in the lower 31 bits and the
223 /// negation flag in the most significant bit.
224 ///
225 /// # Returns
226 ///
227 /// A u32 where bit 31 is the negation flag and bits 0-30 are the variable.
228 #[inline]
229 pub fn to_packed(self) -> u32 {
230 if self.negated {
231 self.var | 0x8000_0000
232 } else {
233 self.var
234 }
235 }
236
237 /// Creates a literal from a packed 32-bit representation.
238 ///
239 /// # Arguments
240 ///
241 /// * `packed` - A u32 with negation in bit 31 and variable in bits 0-30
242 #[inline]
243 pub fn from_packed(packed: u32) -> Self {
244 let negated = (packed & 0x8000_0000) != 0;
245 let var = packed & 0x7FFF_FFFF;
246 Self { var, negated }
247 }
248}
249
250impl PartialOrd for Literal {
251 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
252 Some(self.cmp(other))
253 }
254}
255
256impl Ord for Literal {
257 /// Orders literals first by variable index, then by negation (positive before negative).
258 fn cmp(&self, other: &Self) -> Ordering {
259 match self.var.cmp(&other.var) {
260 Ordering::Equal => self.negated.cmp(&other.negated),
261 ord => ord,
262 }
263 }
264}
265
266// =============================================================================
267// Clause - A disjunction (OR) of literals
268// =============================================================================
269
270/// A clause is a disjunction (OR) of literals.
271///
272/// In CNF (Conjunctive Normal Form), a formula is a conjunction (AND) of clauses,
273/// where each clause is a disjunction (OR) of literals.
274///
275/// # Properties
276///
277/// - An empty clause is always unsatisfied (represents `false`)
278/// - A clause is satisfied if at least one of its literals is satisfied
279/// - Unit clauses (single literal) are important for unit propagation
280///
281/// # Memory Layout
282///
283/// The clause stores literals in a `Vec` which allows efficient iteration
284/// and provides good cache locality for clause evaluation.
285#[derive(Debug, Clone, PartialEq, Eq, Hash)]
286pub struct Clause {
287 /// The literals in this clause.
288 pub literals: Vec<Literal>,
289}
290
291impl Clause {
292 /// Creates a new clause from a vector of literals.
293 ///
294 /// # Arguments
295 ///
296 /// * `literals` - The literals forming this disjunction
297 ///
298 /// # Example
299 ///
300 /// ```
301 /// use xlog_solve::{Clause, Literal};
302 /// // Create clause: (x0 OR NOT x1)
303 /// let clause = Clause::new(vec![
304 /// Literal::positive(0),
305 /// Literal::negative(1),
306 /// ]);
307 /// ```
308 #[inline]
309 pub fn new(literals: Vec<Literal>) -> Self {
310 Self { literals }
311 }
312
313 /// Creates a unit clause (single literal).
314 ///
315 /// Unit clauses are important in SAT solving for unit propagation.
316 ///
317 /// # Arguments
318 ///
319 /// * `literal` - The single literal in this clause
320 #[inline]
321 pub fn unit(literal: Literal) -> Self {
322 Self {
323 literals: vec![literal],
324 }
325 }
326
327 /// Creates a binary clause (two literals).
328 ///
329 /// Binary clauses are common in many SAT encodings (implications, at-most-one, etc.).
330 ///
331 /// # Arguments
332 ///
333 /// * `a` - The first literal
334 /// * `b` - The second literal
335 #[inline]
336 pub fn binary(a: Literal, b: Literal) -> Self {
337 Self {
338 literals: vec![a, b],
339 }
340 }
341
342 /// Creates a ternary clause (three literals).
343 ///
344 /// # Arguments
345 ///
346 /// * `a` - The first literal
347 /// * `b` - The second literal
348 /// * `c` - The third literal
349 #[inline]
350 pub fn ternary(a: Literal, b: Literal, c: Literal) -> Self {
351 Self {
352 literals: vec![a, b, c],
353 }
354 }
355
356 /// Returns the number of literals in this clause.
357 #[inline]
358 pub fn len(&self) -> usize {
359 self.literals.len()
360 }
361
362 /// Returns true if this clause has no literals.
363 ///
364 /// An empty clause is always unsatisfied.
365 #[inline]
366 pub fn is_empty(&self) -> bool {
367 self.literals.is_empty()
368 }
369
370 /// Checks if this clause is satisfied by the given assignment.
371 ///
372 /// A clause is satisfied if at least one of its literals evaluates to true.
373 /// An empty clause is never satisfied.
374 ///
375 /// # Arguments
376 ///
377 /// * `assignment` - A slice of boolean values for each variable
378 ///
379 /// # Returns
380 ///
381 /// `true` if the clause is satisfied, `false` otherwise.
382 ///
383 /// # Example
384 ///
385 /// ```
386 /// use xlog_solve::{Clause, Literal};
387 /// let clause = Clause::new(vec![
388 /// Literal::positive(0),
389 /// Literal::negative(1),
390 /// ]);
391 /// assert!(clause.is_satisfied(&[true, true])); // x0=true satisfies
392 /// assert!(!clause.is_satisfied(&[false, true])); // neither satisfied
393 /// ```
394 #[inline]
395 pub fn is_satisfied(&self, assignment: &[bool]) -> bool {
396 self.literals.iter().any(|lit| lit.eval(assignment))
397 }
398
399 /// Counts how many literals in this clause are satisfied by the assignment.
400 ///
401 /// This is useful for CLS solvers and MaxSAT evaluation.
402 ///
403 /// # Arguments
404 ///
405 /// * `assignment` - A slice of boolean values for each variable
406 ///
407 /// # Returns
408 ///
409 /// The number of literals that evaluate to true.
410 #[inline]
411 pub fn count_satisfied(&self, assignment: &[bool]) -> usize {
412 self.literals
413 .iter()
414 .filter(|lit| lit.eval(assignment))
415 .count()
416 }
417
418 /// Returns an iterator over the literals in this clause.
419 #[inline]
420 pub fn iter(&self) -> impl Iterator<Item = &Literal> {
421 self.literals.iter()
422 }
423
424 /// Returns an iterator over the variable indices in this clause.
425 #[inline]
426 pub fn vars(&self) -> impl Iterator<Item = u32> + '_ {
427 self.literals.iter().map(|lit| lit.var)
428 }
429}
430
431impl IntoIterator for Clause {
432 type Item = Literal;
433 type IntoIter = std::vec::IntoIter<Literal>;
434
435 fn into_iter(self) -> Self::IntoIter {
436 self.literals.into_iter()
437 }
438}
439
440impl<'a> IntoIterator for &'a Clause {
441 type Item = &'a Literal;
442 type IntoIter = std::slice::Iter<'a, Literal>;
443
444 fn into_iter(self) -> Self::IntoIter {
445 self.literals.iter()
446 }
447}
448
449impl FromIterator<Literal> for Clause {
450 fn from_iter<I: IntoIterator<Item = Literal>>(iter: I) -> Self {
451 Self {
452 literals: iter.into_iter().collect(),
453 }
454 }
455}
456
457// =============================================================================
458// Objective - Optimization objective for solving
459// =============================================================================
460
461/// The optimization objective for a SAT/MaxSAT instance.
462///
463/// This determines what the solver should optimize for:
464/// - `Satisfaction`: Find any satisfying assignment (standard SAT)
465/// - `MaxSat`: Maximize the number/weight of satisfied clauses
466/// - `MinUnsat`: Minimize the number/weight of unsatisfied clauses
467#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
468pub enum Objective {
469 /// Find any satisfying assignment (decision problem).
470 ///
471 /// The solver returns SAT if an assignment exists that satisfies all clauses,
472 /// or UNSAT if no such assignment exists.
473 #[default]
474 Satisfaction,
475
476 /// Maximize the total weight of satisfied clauses (optimization).
477 ///
478 /// For unweighted instances, this maximizes the count of satisfied clauses.
479 /// The solver finds an assignment that maximizes the objective.
480 MaxSat,
481
482 /// Minimize the total weight of unsatisfied clauses (optimization).
483 ///
484 /// This is equivalent to MaxSAT for complete solvers but may differ
485 /// for incomplete/approximate solvers.
486 MinUnsat,
487}
488
489// =============================================================================
490// SolveInstance - A complete SAT/MaxSAT instance
491// =============================================================================
492
493/// A SAT or MaxSAT instance in Conjunctive Normal Form (CNF).
494///
495/// This struct represents the complete problem to be solved:
496/// - A set of propositional variables (indexed 0 to num_vars-1)
497/// - A conjunction of clauses (each clause is a disjunction of literals)
498/// - Optional weights for MaxSAT
499/// - An optimization objective
500///
501/// # CNF Semantics
502///
503/// The formula represented is:
504/// ```text
505/// clause[0] AND clause[1] AND ... AND clause[n-1]
506/// ```
507///
508/// where each clause is:
509/// ```text
510/// lit[0] OR lit[1] OR ... OR lit[k-1]
511/// ```
512///
513/// # Memory Layout
514///
515/// The instance is designed for efficient GPU transfer:
516/// - Clauses can be flattened to a contiguous literal array with offset indices
517/// - Weights align with clause indices for vectorized operations
518///
519/// # Example
520///
521/// ```
522/// use xlog_solve::{SolveInstance, Clause, Literal, Objective};
523///
524/// // (x0 OR NOT x1) AND (x1 OR x2)
525/// let instance = SolveInstance::new(3, vec![
526/// Clause::new(vec![Literal::positive(0), Literal::negative(1)]),
527/// Clause::new(vec![Literal::positive(1), Literal::positive(2)]),
528/// ]);
529///
530/// assert_eq!(instance.num_vars, 3);
531/// assert_eq!(instance.clauses.len(), 2);
532/// assert_eq!(instance.objective, Objective::Satisfaction);
533/// ```
534#[derive(Debug, Clone)]
535pub struct SolveInstance {
536 /// Number of propositional variables in this instance.
537 ///
538 /// Variables are indexed from 0 to `num_vars - 1`.
539 pub num_vars: u32,
540
541 /// The clauses forming this CNF formula.
542 ///
543 /// The formula is satisfied when ALL clauses are satisfied.
544 pub clauses: Vec<Clause>,
545
546 /// Optional weights for each clause (for weighted MaxSAT).
547 ///
548 /// If `Some`, must have the same length as `clauses`.
549 /// If `None`, all clauses have implicit weight 1.0.
550 pub weights: Option<Vec<f64>>,
551
552 /// The optimization objective.
553 pub objective: Objective,
554}
555
556impl SolveInstance {
557 /// Creates a new SAT instance with the given variables and clauses.
558 ///
559 /// The instance uses `Objective::Satisfaction` (standard SAT).
560 ///
561 /// # Arguments
562 ///
563 /// * `num_vars` - The number of propositional variables
564 /// * `clauses` - The clauses forming the CNF formula
565 ///
566 /// # Example
567 ///
568 /// ```
569 /// use xlog_solve::{SolveInstance, Clause, Literal};
570 ///
571 /// let instance = SolveInstance::new(3, vec![
572 /// Clause::new(vec![Literal::positive(0), Literal::negative(1)]),
573 /// Clause::new(vec![Literal::positive(1), Literal::positive(2)]),
574 /// ]);
575 /// ```
576 #[inline]
577 pub fn new(num_vars: u32, clauses: Vec<Clause>) -> Self {
578 Self {
579 num_vars,
580 clauses,
581 weights: None,
582 objective: Objective::Satisfaction,
583 }
584 }
585
586 /// Creates a weighted MaxSAT instance.
587 ///
588 /// # Arguments
589 ///
590 /// * `num_vars` - The number of propositional variables
591 /// * `clauses` - The clauses forming the CNF formula
592 /// * `weights` - Weights for each clause (must match clause count)
593 ///
594 /// # Panics
595 ///
596 /// Panics if `weights.len() != clauses.len()`.
597 ///
598 /// # Example
599 ///
600 /// ```
601 /// use xlog_solve::{SolveInstance, Clause, Literal, Objective};
602 ///
603 /// let instance = SolveInstance::with_weights(
604 /// 2,
605 /// vec![
606 /// Clause::new(vec![Literal::positive(0)]),
607 /// Clause::new(vec![Literal::positive(1)]),
608 /// ],
609 /// vec![1.0, 2.0], // Clause 1 has twice the weight
610 /// );
611 ///
612 /// assert_eq!(instance.objective, Objective::MaxSat);
613 /// ```
614 #[inline]
615 pub fn with_weights(num_vars: u32, clauses: Vec<Clause>, weights: Vec<f64>) -> Self {
616 assert_eq!(
617 clauses.len(),
618 weights.len(),
619 "Number of weights ({}) must match number of clauses ({})",
620 weights.len(),
621 clauses.len()
622 );
623 Self {
624 num_vars,
625 clauses,
626 weights: Some(weights),
627 objective: Objective::MaxSat,
628 }
629 }
630
631 /// Adds a clause to this instance.
632 ///
633 /// # Arguments
634 ///
635 /// * `clause` - The clause to add
636 ///
637 /// # Note
638 ///
639 /// If the instance has weights, the new clause gets weight 1.0.
640 #[inline]
641 pub fn add_clause(&mut self, clause: Clause) {
642 self.clauses.push(clause);
643 if let Some(ref mut weights) = self.weights {
644 weights.push(1.0);
645 }
646 }
647
648 /// Adds a weighted clause to this instance.
649 ///
650 /// If the instance doesn't have weights yet, this initializes weights
651 /// with 1.0 for all existing clauses.
652 ///
653 /// # Arguments
654 ///
655 /// * `clause` - The clause to add
656 /// * `weight` - The weight for this clause
657 #[inline]
658 pub fn add_weighted_clause(&mut self, clause: Clause, weight: f64) {
659 if self.weights.is_none() {
660 self.weights = Some(vec![1.0; self.clauses.len()]);
661 }
662 self.clauses.push(clause);
663 self.weights.as_mut().unwrap().push(weight);
664 }
665
666 /// Returns the number of clauses in this instance.
667 #[inline]
668 pub fn num_clauses(&self) -> usize {
669 self.clauses.len()
670 }
671
672 /// Checks if all clauses are satisfied by the given assignment.
673 ///
674 /// # Arguments
675 ///
676 /// * `assignment` - A slice of boolean values, one per variable
677 ///
678 /// # Returns
679 ///
680 /// `true` if all clauses are satisfied, `false` otherwise.
681 ///
682 /// # Example
683 ///
684 /// ```
685 /// use xlog_solve::{SolveInstance, Clause, Literal};
686 ///
687 /// let instance = SolveInstance::new(2, vec![
688 /// Clause::new(vec![Literal::positive(0)]),
689 /// Clause::new(vec![Literal::positive(1)]),
690 /// ]);
691 ///
692 /// assert!(instance.is_satisfied(&[true, true]));
693 /// assert!(!instance.is_satisfied(&[true, false]));
694 /// ```
695 #[inline]
696 pub fn is_satisfied(&self, assignment: &[bool]) -> bool {
697 self.clauses
698 .iter()
699 .all(|clause| clause.is_satisfied(assignment))
700 }
701
702 /// Counts how many clauses are satisfied by the given assignment.
703 ///
704 /// # Arguments
705 ///
706 /// * `assignment` - A slice of boolean values, one per variable
707 ///
708 /// # Returns
709 ///
710 /// The number of satisfied clauses.
711 #[inline]
712 pub fn count_satisfied(&self, assignment: &[bool]) -> usize {
713 self.clauses
714 .iter()
715 .filter(|clause| clause.is_satisfied(assignment))
716 .count()
717 }
718
719 /// Computes the weighted satisfaction score for the given assignment.
720 ///
721 /// If weights are specified, returns the sum of weights of satisfied clauses.
722 /// If no weights are specified, each clause has implicit weight 1.0.
723 ///
724 /// # Arguments
725 ///
726 /// * `assignment` - A slice of boolean values, one per variable
727 ///
728 /// # Returns
729 ///
730 /// The total weight of satisfied clauses.
731 #[inline]
732 pub fn weighted_satisfaction(&self, assignment: &[bool]) -> f64 {
733 match &self.weights {
734 Some(weights) => self
735 .clauses
736 .iter()
737 .zip(weights.iter())
738 .filter(|(clause, _)| clause.is_satisfied(assignment))
739 .map(|(_, weight)| *weight)
740 .sum(),
741 None => self.count_satisfied(assignment) as f64,
742 }
743 }
744
745 /// Returns the total weight of all clauses.
746 ///
747 /// For unweighted instances, returns the number of clauses.
748 #[inline]
749 pub fn total_weight(&self) -> f64 {
750 match &self.weights {
751 Some(weights) => weights.iter().sum(),
752 None => self.clauses.len() as f64,
753 }
754 }
755
756 /// Returns the satisfaction ratio (satisfied weight / total weight).
757 ///
758 /// # Arguments
759 ///
760 /// * `assignment` - A slice of boolean values, one per variable
761 ///
762 /// # Returns
763 ///
764 /// A value in [0.0, 1.0] representing the fraction of weight satisfied.
765 /// Returns 0.0 for empty instances (no clauses).
766 #[inline]
767 pub fn satisfaction_ratio(&self, assignment: &[bool]) -> f64 {
768 let total = self.total_weight();
769 if total == 0.0 {
770 0.0
771 } else {
772 self.weighted_satisfaction(assignment) / total
773 }
774 }
775
776 /// Returns an iterator over all clauses.
777 #[inline]
778 pub fn iter_clauses(&self) -> impl Iterator<Item = &Clause> {
779 self.clauses.iter()
780 }
781
782 /// Returns an iterator over clauses with their weights.
783 ///
784 /// If no weights are specified, uses 1.0 for each clause.
785 pub fn iter_weighted(&self) -> impl Iterator<Item = (&Clause, f64)> {
786 let weights = self.weights.as_ref();
787 self.clauses.iter().enumerate().map(move |(i, clause)| {
788 let weight = weights.map_or(1.0, |w| w[i]);
789 (clause, weight)
790 })
791 }
792
793 /// Validates that all variable indices in clauses are within bounds.
794 ///
795 /// # Returns
796 ///
797 /// `true` if all variable indices are less than `num_vars`, `false` otherwise.
798 pub fn validate(&self) -> bool {
799 self.clauses
800 .iter()
801 .all(|clause| clause.literals.iter().all(|lit| lit.var < self.num_vars))
802 }
803
804 /// Returns the maximum variable index used in any clause.
805 ///
806 /// Returns `None` if there are no clauses or all clauses are empty.
807 pub fn max_var(&self) -> Option<u32> {
808 self.clauses
809 .iter()
810 .flat_map(|c| c.literals.iter())
811 .map(|lit| lit.var)
812 .max()
813 }
814
815 /// Returns the total number of literals across all clauses.
816 pub fn total_literals(&self) -> usize {
817 self.clauses.iter().map(|c| c.len()).sum()
818 }
819}
820
821impl Default for SolveInstance {
822 fn default() -> Self {
823 Self {
824 num_vars: 0,
825 clauses: Vec::new(),
826 weights: None,
827 objective: Objective::Satisfaction,
828 }
829 }
830}
831
832// Tests first (TDD approach)
833#[cfg(test)]
834mod tests {
835 use super::*;
836
837 #[test]
838 fn test_literal_new() {
839 let pos = Literal::positive(5);
840 assert_eq!(pos.var, 5);
841 assert!(!pos.negated);
842
843 let neg = Literal::negative(3);
844 assert_eq!(neg.var, 3);
845 assert!(neg.negated);
846 }
847
848 #[test]
849 fn test_literal_eval() {
850 let pos = Literal::positive(1);
851 let neg = Literal::negative(1);
852
853 // Assignment: x0=true, x1=false, x2=true
854 let assignment = vec![true, false, true];
855
856 // x1 is false, so positive literal evaluates to false
857 assert!(!pos.eval(&assignment));
858 // NOT x1 evaluates to true (since x1=false)
859 assert!(neg.eval(&assignment));
860
861 // Test with x2 (index 2, value true)
862 let pos2 = Literal::positive(2);
863 let neg2 = Literal::negative(2);
864 assert!(pos2.eval(&assignment));
865 assert!(!neg2.eval(&assignment));
866 }
867
868 #[test]
869 fn test_literal_to_dimacs() {
870 let pos = Literal::positive(3);
871 let neg = Literal::negative(7);
872
873 // DIMACS uses 1-indexed variables
874 assert_eq!(pos.to_dimacs(), 4); // var 3 -> 4 in DIMACS (1-indexed)
875 assert_eq!(neg.to_dimacs(), -8); // var 7 negated -> -8 in DIMACS
876 }
877
878 #[test]
879 fn test_literal_from_dimacs() {
880 // DIMACS variable 4 (positive) -> var index 3
881 let pos = Literal::from_dimacs(4);
882 assert_eq!(pos.var, 3);
883 assert!(!pos.negated);
884
885 // DIMACS variable -8 (negative) -> var index 7
886 let neg = Literal::from_dimacs(-8);
887 assert_eq!(neg.var, 7);
888 assert!(neg.negated);
889 }
890
891 #[test]
892 fn test_clause_new() {
893 let clause = Clause::new(vec![Literal::positive(1), Literal::negative(2)]);
894 assert_eq!(clause.literals.len(), 2);
895 }
896
897 #[test]
898 fn test_clause_is_satisfied() {
899 // Clause: (x0 OR NOT x1)
900 let clause = Clause::new(vec![Literal::positive(0), Literal::negative(1)]);
901
902 // x0=true, x1=false -> satisfied (both literals true)
903 assert!(clause.is_satisfied(&[true, false]));
904
905 // x0=true, x1=true -> satisfied (x0 is true)
906 assert!(clause.is_satisfied(&[true, true]));
907
908 // x0=false, x1=false -> satisfied (NOT x1 is true)
909 assert!(clause.is_satisfied(&[false, false]));
910
911 // x0=false, x1=true -> NOT satisfied (both literals false)
912 assert!(!clause.is_satisfied(&[false, true]));
913 }
914
915 #[test]
916 fn test_clause_count_satisfied() {
917 // Clause: (x0 OR NOT x1 OR x2)
918 let clause = Clause::new(vec![
919 Literal::positive(0),
920 Literal::negative(1),
921 Literal::positive(2),
922 ]);
923
924 // x0=true, x1=false, x2=true -> all 3 literals satisfied
925 assert_eq!(clause.count_satisfied(&[true, false, true]), 3);
926
927 // x0=true, x1=true, x2=false -> only x0 satisfied
928 assert_eq!(clause.count_satisfied(&[true, true, false]), 1);
929
930 // x0=false, x1=true, x2=false -> none satisfied
931 assert_eq!(clause.count_satisfied(&[false, true, false]), 0);
932 }
933
934 #[test]
935 fn test_clause_empty() {
936 // Empty clause is always unsatisfied (false)
937 let empty = Clause::new(vec![]);
938 assert!(!empty.is_satisfied(&[true, false, true]));
939 assert_eq!(empty.count_satisfied(&[true, false, true]), 0);
940 }
941
942 #[test]
943 fn test_clause_unit() {
944 // Unit clause (single literal)
945 let unit_pos = Clause::unit(Literal::positive(0));
946 assert!(unit_pos.is_satisfied(&[true]));
947 assert!(!unit_pos.is_satisfied(&[false]));
948
949 let unit_neg = Clause::unit(Literal::negative(0));
950 assert!(!unit_neg.is_satisfied(&[true]));
951 assert!(unit_neg.is_satisfied(&[false]));
952 }
953
954 #[test]
955 fn test_objective_default() {
956 let obj = Objective::default();
957 assert!(matches!(obj, Objective::Satisfaction));
958 }
959
960 #[test]
961 fn test_instance_from_cnf() {
962 // (x1 OR NOT x2) AND (x2 OR x3)
963 let instance = SolveInstance::new(
964 3,
965 vec![
966 Clause::new(vec![Literal::positive(0), Literal::negative(1)]),
967 Clause::new(vec![Literal::positive(1), Literal::positive(2)]),
968 ],
969 );
970 assert_eq!(instance.num_vars, 3);
971 assert_eq!(instance.clauses.len(), 2);
972 }
973
974 #[test]
975 fn test_instance_is_satisfied() {
976 let instance = SolveInstance::new(
977 3,
978 vec![
979 Clause::new(vec![Literal::positive(0), Literal::negative(1)]),
980 Clause::new(vec![Literal::positive(1), Literal::positive(2)]),
981 ],
982 );
983
984 // x0=true, x1=false, x2=true should satisfy both clauses
985 let assignment = vec![true, false, true];
986 assert!(instance.is_satisfied(&assignment));
987
988 // x0=false, x1=true, x2=false should fail clause 2
989 let assignment2 = vec![false, true, false];
990 assert!(!instance.is_satisfied(&assignment2));
991 }
992
993 #[test]
994 fn test_instance_count_satisfied() {
995 let instance = SolveInstance::new(
996 3,
997 vec![
998 Clause::new(vec![Literal::positive(0), Literal::negative(1)]),
999 Clause::new(vec![Literal::positive(1), Literal::positive(2)]),
1000 Clause::new(vec![Literal::negative(0), Literal::negative(2)]),
1001 ],
1002 );
1003
1004 // x0=true, x1=false, x2=true
1005 // Clause 1: x0=T or NOT x1=T -> satisfied
1006 // Clause 2: x1=F or x2=T -> satisfied
1007 // Clause 3: NOT x0=F or NOT x2=F -> NOT satisfied
1008 assert_eq!(instance.count_satisfied(&[true, false, true]), 2);
1009
1010 // x0=false, x1=false, x2=false
1011 // Clause 1: x0=F or NOT x1=T -> satisfied
1012 // Clause 2: x1=F or x2=F -> NOT satisfied
1013 // Clause 3: NOT x0=T or NOT x2=T -> satisfied
1014 assert_eq!(instance.count_satisfied(&[false, false, false]), 2);
1015 }
1016
1017 #[test]
1018 fn test_instance_with_weights() {
1019 let instance = SolveInstance::with_weights(
1020 3,
1021 vec![
1022 Clause::new(vec![Literal::positive(0)]),
1023 Clause::new(vec![Literal::positive(1)]),
1024 Clause::new(vec![Literal::positive(2)]),
1025 ],
1026 vec![1.0, 2.0, 3.0],
1027 );
1028
1029 assert!(instance.weights.is_some());
1030 assert_eq!(instance.weights.as_ref().unwrap().len(), 3);
1031 }
1032
1033 #[test]
1034 fn test_instance_weighted_satisfaction() {
1035 let instance = SolveInstance::with_weights(
1036 3,
1037 vec![
1038 Clause::new(vec![Literal::positive(0)]), // weight 1.0
1039 Clause::new(vec![Literal::positive(1)]), // weight 2.0
1040 Clause::new(vec![Literal::positive(2)]), // weight 3.0
1041 ],
1042 vec![1.0, 2.0, 3.0],
1043 );
1044
1045 // All true: 1.0 + 2.0 + 3.0 = 6.0
1046 assert_eq!(instance.weighted_satisfaction(&[true, true, true]), 6.0);
1047
1048 // Only x1=true: 2.0
1049 assert_eq!(instance.weighted_satisfaction(&[false, true, false]), 2.0);
1050
1051 // None satisfied: 0.0
1052 assert_eq!(instance.weighted_satisfaction(&[false, false, false]), 0.0);
1053 }
1054
1055 #[test]
1056 fn test_instance_weighted_satisfaction_unweighted() {
1057 // When no weights provided, each clause has implicit weight 1.0
1058 let instance = SolveInstance::new(
1059 2,
1060 vec![
1061 Clause::new(vec![Literal::positive(0)]),
1062 Clause::new(vec![Literal::positive(1)]),
1063 ],
1064 );
1065
1066 assert_eq!(instance.weighted_satisfaction(&[true, true]), 2.0);
1067 assert_eq!(instance.weighted_satisfaction(&[true, false]), 1.0);
1068 }
1069
1070 #[test]
1071 fn test_instance_empty() {
1072 let instance = SolveInstance::new(0, vec![]);
1073 assert!(instance.is_satisfied(&[]));
1074 assert_eq!(instance.count_satisfied(&[]), 0);
1075 assert_eq!(instance.weighted_satisfaction(&[]), 0.0);
1076 }
1077
1078 #[test]
1079 fn test_literal_negate() {
1080 let pos = Literal::positive(5);
1081 let neg = pos.negate();
1082 assert_eq!(neg.var, 5);
1083 assert!(neg.negated);
1084
1085 let pos_again = neg.negate();
1086 assert_eq!(pos_again.var, 5);
1087 assert!(!pos_again.negated);
1088 }
1089
1090 #[test]
1091 fn test_clause_binary() {
1092 let clause = Clause::binary(Literal::positive(0), Literal::negative(1));
1093 assert_eq!(clause.literals.len(), 2);
1094 assert!(clause.is_satisfied(&[true, true]));
1095 assert!(clause.is_satisfied(&[true, false]));
1096 assert!(clause.is_satisfied(&[false, false]));
1097 assert!(!clause.is_satisfied(&[false, true]));
1098 }
1099
1100 #[test]
1101 fn test_clause_ternary() {
1102 let clause = Clause::ternary(
1103 Literal::positive(0),
1104 Literal::positive(1),
1105 Literal::positive(2),
1106 );
1107 assert_eq!(clause.literals.len(), 3);
1108
1109 // Only satisfied when at least one is true
1110 assert!(clause.is_satisfied(&[true, false, false]));
1111 assert!(clause.is_satisfied(&[false, true, false]));
1112 assert!(clause.is_satisfied(&[false, false, true]));
1113 assert!(!clause.is_satisfied(&[false, false, false]));
1114 }
1115
1116 #[test]
1117 fn test_instance_add_clause() {
1118 let mut instance = SolveInstance::new(3, vec![Clause::new(vec![Literal::positive(0)])]);
1119 assert_eq!(instance.clauses.len(), 1);
1120
1121 instance.add_clause(Clause::new(vec![Literal::positive(1)]));
1122 assert_eq!(instance.clauses.len(), 2);
1123 }
1124
1125 #[test]
1126 fn test_instance_total_weight() {
1127 let instance = SolveInstance::with_weights(
1128 3,
1129 vec![
1130 Clause::new(vec![Literal::positive(0)]),
1131 Clause::new(vec![Literal::positive(1)]),
1132 Clause::new(vec![Literal::positive(2)]),
1133 ],
1134 vec![1.0, 2.0, 3.0],
1135 );
1136 assert_eq!(instance.total_weight(), 6.0);
1137
1138 // Unweighted instance: total weight = number of clauses
1139 let unweighted = SolveInstance::new(
1140 2,
1141 vec![
1142 Clause::new(vec![Literal::positive(0)]),
1143 Clause::new(vec![Literal::positive(1)]),
1144 ],
1145 );
1146 assert_eq!(unweighted.total_weight(), 2.0);
1147 }
1148
1149 #[test]
1150 fn test_instance_satisfaction_ratio() {
1151 let instance = SolveInstance::new(
1152 3,
1153 vec![
1154 Clause::new(vec![Literal::positive(0)]),
1155 Clause::new(vec![Literal::positive(1)]),
1156 Clause::new(vec![Literal::positive(2)]),
1157 Clause::new(vec![Literal::negative(0)]),
1158 ],
1159 );
1160
1161 // x0=true satisfies clauses 0, but not 3 -> 1 satisfied
1162 // x1, x2 satisfy clauses 1, 2
1163 // Total: 3 out of 4
1164 assert_eq!(instance.satisfaction_ratio(&[true, true, true]), 0.75);
1165 }
1166
1167 #[test]
1168 fn test_literal_ordering() {
1169 let a = Literal::positive(1);
1170 let b = Literal::positive(2);
1171 let c = Literal::negative(1);
1172
1173 assert!(a < b); // Lower var comes first
1174 assert!(a < c); // Same var, positive before negative
1175 }
1176
1177 #[test]
1178 fn test_clause_len_and_is_empty() {
1179 let empty = Clause::new(vec![]);
1180 assert!(empty.is_empty());
1181 assert_eq!(empty.len(), 0);
1182
1183 let unit = Clause::unit(Literal::positive(0));
1184 assert!(!unit.is_empty());
1185 assert_eq!(unit.len(), 1);
1186
1187 let binary = Clause::binary(Literal::positive(0), Literal::positive(1));
1188 assert_eq!(binary.len(), 2);
1189 }
1190
1191 #[test]
1192 fn test_instance_num_clauses() {
1193 let instance = SolveInstance::new(
1194 3,
1195 vec![
1196 Clause::new(vec![Literal::positive(0)]),
1197 Clause::new(vec![Literal::positive(1)]),
1198 ],
1199 );
1200 assert_eq!(instance.num_clauses(), 2);
1201 }
1202}
1203
1204// Implementation will go here after tests fail