|
| 1 | +use super::*; |
| 2 | +use crate::rules::test_helpers::assert_satisfaction_round_trip_from_satisfaction_target; |
| 3 | +use crate::solvers::BruteForce; |
| 4 | +use crate::traits::Problem; |
| 5 | + |
| 6 | +fn reduce(source: &NAESatisfiability) -> ReductionNAESATToSAT { |
| 7 | + ReduceTo::<Satisfiability>::reduce_to(source).expect("reduction should succeed") |
| 8 | +} |
| 9 | + |
| 10 | +fn assignments(num_vars: usize) -> impl Iterator<Item = Vec<bool>> { |
| 11 | + (0..1usize << num_vars).map(move |mask| (0..num_vars).map(|i| (mask >> i) & 1 == 1).collect()) |
| 12 | +} |
| 13 | + |
| 14 | +/// Checks that source and target agree on every assignment, that satisfying |
| 15 | +/// assignments extract unchanged, and that all others are rejected. |
| 16 | +fn assert_equivalent_on_all_assignments(source: &NAESatisfiability) { |
| 17 | + let reduction = reduce(source); |
| 18 | + let target = reduction.target_problem(); |
| 19 | + assert_eq!(target.num_vars(), source.num_vars()); |
| 20 | + for assignment in assignments(source.num_vars()) { |
| 21 | + let source_ok = source.evaluate(&assignment).unwrap().0; |
| 22 | + assert_eq!( |
| 23 | + source_ok, |
| 24 | + target.evaluate(&assignment).unwrap().0, |
| 25 | + "disagreement on {assignment:?}" |
| 26 | + ); |
| 27 | + match reduction.extract_solution(&assignment) { |
| 28 | + Ok(extracted) => { |
| 29 | + assert!(source_ok, "accepted non-witness {assignment:?}"); |
| 30 | + assert_eq!(extracted, assignment); |
| 31 | + } |
| 32 | + Err(_) => assert!(!source_ok, "rejected witness {assignment:?}"), |
| 33 | + } |
| 34 | + } |
| 35 | +} |
| 36 | + |
| 37 | +fn canonical_source() -> NAESatisfiability { |
| 38 | + NAESatisfiability::new( |
| 39 | + 3, |
| 40 | + vec![ |
| 41 | + CNFClause::new(vec![1, 2, 3]), |
| 42 | + CNFClause::new(vec![-1, 2]), |
| 43 | + CNFClause::new(vec![1, -2, -3]), |
| 44 | + ], |
| 45 | + ) |
| 46 | +} |
| 47 | + |
| 48 | +#[test] |
| 49 | +fn test_naesatisfiability_to_satisfiability_closed_loop() { |
| 50 | + let source = canonical_source(); |
| 51 | + let reduction = reduce(&source); |
| 52 | + assert_satisfaction_round_trip_from_satisfaction_target( |
| 53 | + &source, |
| 54 | + &reduction, |
| 55 | + "NAE-SAT->SAT closed loop", |
| 56 | + ); |
| 57 | +} |
| 58 | + |
| 59 | +#[test] |
| 60 | +fn test_naesatisfiability_to_satisfiability_structure() { |
| 61 | + let source = canonical_source(); |
| 62 | + let reduction = reduce(&source); |
| 63 | + let target = reduction.target_problem(); |
| 64 | + |
| 65 | + assert_eq!(target.num_vars(), 3); |
| 66 | + assert_eq!(target.num_clauses(), 2 * source.num_clauses()); |
| 67 | + assert_eq!(target.num_literals(), 2 * source.num_literals()); |
| 68 | + let literals: Vec<Vec<i64>> = target |
| 69 | + .clauses() |
| 70 | + .iter() |
| 71 | + .map(|clause| clause.literals.clone()) |
| 72 | + .collect(); |
| 73 | + assert_eq!( |
| 74 | + literals, |
| 75 | + vec![ |
| 76 | + vec![1, 2, 3], |
| 77 | + vec![-1, -2, -3], |
| 78 | + vec![-1, 2], |
| 79 | + vec![1, -2], |
| 80 | + vec![1, -2, -3], |
| 81 | + vec![-1, 2, 3], |
| 82 | + ] |
| 83 | + ); |
| 84 | +} |
| 85 | + |
| 86 | +#[test] |
| 87 | +fn test_naesatisfiability_to_satisfiability_canonical_all_assignments() { |
| 88 | + let source = canonical_source(); |
| 89 | + assert_equivalent_on_all_assignments(&source); |
| 90 | + |
| 91 | + let witnesses: Vec<Vec<bool>> = assignments(3) |
| 92 | + .filter(|assignment| source.evaluate(assignment).unwrap().0) |
| 93 | + .collect(); |
| 94 | + assert_eq!( |
| 95 | + witnesses, |
| 96 | + vec![vec![true, true, false], vec![false, false, true]] |
| 97 | + ); |
| 98 | +} |
| 99 | + |
| 100 | +#[test] |
| 101 | +fn test_naesatisfiability_to_satisfiability_witness_and_near_witness() { |
| 102 | + let reduction = reduce(&canonical_source()); |
| 103 | + |
| 104 | + assert_eq!( |
| 105 | + reduction |
| 106 | + .extract_solution(&vec![false, false, true]) |
| 107 | + .unwrap(), |
| 108 | + vec![false, false, true] |
| 109 | + ); |
| 110 | + // [0,1,0] makes (¬x1, x2) all true, so the excluded all-true CNF clause fails. |
| 111 | + let error = reduction |
| 112 | + .extract_solution(&vec![false, true, false]) |
| 113 | + .unwrap_err(); |
| 114 | + assert!(error |
| 115 | + .to_string() |
| 116 | + .contains("target assignment does not satisfy CNF clauses")); |
| 117 | +} |
| 118 | + |
| 119 | +#[test] |
| 120 | +fn test_naesatisfiability_to_satisfiability_rejects_malformed_assignment() { |
| 121 | + let reduction = reduce(&canonical_source()); |
| 122 | + assert!(reduction.extract_solution(&vec![false, false]).is_err()); |
| 123 | + assert!(reduction |
| 124 | + .extract_solution(&vec![false, false, true, false]) |
| 125 | + .is_err()); |
| 126 | +} |
| 127 | + |
| 128 | +#[test] |
| 129 | +fn test_naesatisfiability_to_satisfiability_empty_conjunction() { |
| 130 | + for num_vars in [0, 2] { |
| 131 | + let source = NAESatisfiability::new(num_vars, vec![]); |
| 132 | + let reduction = reduce(&source); |
| 133 | + let target = reduction.target_problem(); |
| 134 | + assert_eq!(target.num_vars(), num_vars); |
| 135 | + assert_eq!(target.num_clauses(), 0); |
| 136 | + assert_eq!(target.num_literals(), 0); |
| 137 | + assert_equivalent_on_all_assignments(&source); |
| 138 | + } |
| 139 | +} |
| 140 | + |
| 141 | +#[test] |
| 142 | +fn test_naesatisfiability_to_satisfiability_unsatisfiable_repeated_variable() { |
| 143 | + let source = NAESatisfiability::new(1, vec![CNFClause::new(vec![1, 1])]); |
| 144 | + let reduction = reduce(&source); |
| 145 | + let literals: Vec<Vec<i64>> = reduction |
| 146 | + .target_problem() |
| 147 | + .clauses() |
| 148 | + .iter() |
| 149 | + .map(|clause| clause.literals.clone()) |
| 150 | + .collect(); |
| 151 | + assert_eq!(literals, vec![vec![1, 1], vec![-1, -1]]); |
| 152 | + |
| 153 | + let solver = BruteForce::new(); |
| 154 | + assert!(solver.solve(&source).unwrap().is_none()); |
| 155 | + assert!(solver.solve(reduction.target_problem()).unwrap().is_none()); |
| 156 | + assert_equivalent_on_all_assignments(&source); |
| 157 | +} |
| 158 | + |
| 159 | +#[test] |
| 160 | +fn test_naesatisfiability_to_satisfiability_repeated_literals() { |
| 161 | + // NAE(x1, x1, x2) holds exactly when x1 != x2. |
| 162 | + let source = NAESatisfiability::new(2, vec![CNFClause::new(vec![1, 1, 2])]); |
| 163 | + assert_equivalent_on_all_assignments(&source); |
| 164 | +} |
| 165 | + |
| 166 | +#[test] |
| 167 | +fn test_naesatisfiability_to_satisfiability_tautological_clauses() { |
| 168 | + // (x1, ¬x1) always has one true and one false literal. |
| 169 | + let source = NAESatisfiability::new( |
| 170 | + 2, |
| 171 | + vec![CNFClause::new(vec![1, -1]), CNFClause::new(vec![2, -2, 1])], |
| 172 | + ); |
| 173 | + let reduction = reduce(&source); |
| 174 | + assert_eq!( |
| 175 | + reduction.target_problem().clauses()[1].literals, |
| 176 | + vec![-1, 1] |
| 177 | + ); |
| 178 | + assert_equivalent_on_all_assignments(&source); |
| 179 | +} |
| 180 | + |
| 181 | +#[test] |
| 182 | +fn test_naesatisfiability_to_satisfiability_arbitrary_clause_lengths() { |
| 183 | + let source = NAESatisfiability::new( |
| 184 | + 5, |
| 185 | + vec![ |
| 186 | + CNFClause::new(vec![1, -2]), |
| 187 | + CNFClause::new(vec![2, 3, -4]), |
| 188 | + CNFClause::new(vec![-1, 3, 4, 5]), |
| 189 | + CNFClause::new(vec![1, 2, -3, -4, 5]), |
| 190 | + ], |
| 191 | + ); |
| 192 | + let reduction = reduce(&source); |
| 193 | + let target = reduction.target_problem(); |
| 194 | + assert_eq!(target.num_clauses(), 8); |
| 195 | + assert_eq!(target.num_literals(), 2 * 14); |
| 196 | + assert_equivalent_on_all_assignments(&source); |
| 197 | + assert_satisfaction_round_trip_from_satisfaction_target( |
| 198 | + &source, |
| 199 | + &reduction, |
| 200 | + "NAE-SAT->SAT mixed clause lengths", |
| 201 | + ); |
| 202 | +} |
0 commit comments