Skip to content

Commit 48a9571

Browse files
authored
Merge pull request #1210 from montionugera/fix/issue-1101
Add NAESatisfiability → Satisfiability clause-doubling reduction (#1101)
2 parents 8622b3f + c963cd6 commit 48a9571

5 files changed

Lines changed: 331 additions & 0 deletions

File tree

‎docs/paper/reductions.typ‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12983,6 +12983,16 @@ where $P$ is a penalty weight large enough that any constraint violation costs m
1298312983
_Solution extraction._ If the sentinel $s = 0$, return the first $n$ variables. If $s = 1$, return the complement of the first $n$ variables.
1298412984
]
1298512985

12986+
#reduction-rule("NAESatisfiability", "Satisfiability")[
12987+
A not-all-equal clause holds exactly when it has a true literal and a false literal @schaefer1978. Each NAE clause therefore becomes two CNF clauses over the same variables, one forbidding the all-false assignment and one forbidding the all-true assignment.
12988+
][
12989+
_Construction._ Given an NAE-SAT instance with $n$ variables and $m$ clauses, keep the same $n$ variables. For each clause $C_j = (ell_1, dots, ell_r)$ with $r >= 2$, emit the CNF clauses $(ell_1 or dots or ell_r)$ and $(not ell_1 or dots or not ell_r)$. The target has $n$ variables, $2m$ clauses, and $2L$ literal occurrences, where $L$ is the number of source literal occurrences. Repeated and complementary literals are copied unchanged.
12990+
12991+
_Correctness._ For any assignment, the first clause is false exactly when every $ell_i$ is false, and the second is false exactly when every $ell_i$ is true. Both hold exactly when $C_j$ contains a true and a false literal, which is the NAE condition. Hence an assignment satisfies the target iff it satisfies the source.
12992+
12993+
_Solution extraction._ Return the target assignment unchanged.
12994+
]
12995+
1298612996
#let cs_sat = load-example("CircuitSAT", "Satisfiability")
1298712997
#let cs_sat_sol = cs_sat.solutions.at(0)
1298812998
#reduction-rule("CircuitSAT", "Satisfiability",

‎src/rules/mod.rs‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,7 @@ mod minmaxmulticenter_casts;
133133
mod mixedchinesepostman_casts;
134134
pub(crate) mod naesatisfiability_maxcut;
135135
pub(crate) mod naesatisfiability_partitionintoperfectmatchings;
136+
pub(crate) mod naesatisfiability_satisfiability;
136137
pub(crate) mod naesatisfiability_setsplitting;
137138
pub(crate) mod numerical3dimensionalmatching_numericalmatchingwithtargetsums;
138139
pub(crate) mod optimallineararrangement_consecutiveonesmatrixaugmentation;
@@ -431,6 +432,7 @@ pub(crate) fn canonical_rule_example_specs() -> Vec<crate::example_db::specs::Ru
431432
specs.extend(rootedtreearrangement_rootedtreestorageassignment::canonical_rule_example_specs());
432433
specs.extend(naesatisfiability_maxcut::canonical_rule_example_specs());
433434
specs.extend(naesatisfiability_partitionintoperfectmatchings::canonical_rule_example_specs());
435+
specs.extend(naesatisfiability_satisfiability::canonical_rule_example_specs());
434436
specs.extend(satisfiability_maximum2satisfiability::canonical_rule_example_specs());
435437
specs.extend(ensemblecomputation_ilp::canonical_rule_example_specs());
436438
specs.extend(exactcoverby3sets_maximumsetpacking::canonical_rule_example_specs());
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
//! Reduction from NAE-Satisfiability to Satisfiability.
2+
//!
3+
//! A not-all-equal clause `(l_1, ..., l_r)` holds exactly when at least one
4+
//! literal is true and at least one literal is false. Each NAE clause therefore
5+
//! becomes two CNF clauses over the same variables: `(l_1 ∨ ... ∨ l_r)` excludes
6+
//! the all-false assignment and `(¬l_1 ∨ ... ∨ ¬l_r)` excludes the all-true one.
7+
8+
use crate::models::formula::{CNFClause, NAESatisfiability, Satisfiability};
9+
use crate::reduction;
10+
use crate::rules::traits::{ReduceTo, ReductionResult};
11+
12+
/// Result of reducing NAE-Satisfiability to Satisfiability.
13+
#[derive(Debug, Clone)]
14+
pub struct ReductionNAESATToSAT {
15+
/// The target SAT problem.
16+
target: Satisfiability,
17+
}
18+
19+
impl ReductionResult for ReductionNAESATToSAT {
20+
type Source = NAESatisfiability;
21+
type Target = Satisfiability;
22+
23+
fn target_problem(&self) -> &Self::Target {
24+
&self.target
25+
}
26+
27+
fn extract_solution(
28+
&self,
29+
target_solution: &<Self::Target as crate::traits::Problem>::Solution,
30+
) -> crate::rules::ExtractionResult<<Self::Source as crate::traits::Problem>::Solution> {
31+
crate::rules::traits::validate_target_witness(
32+
self.target_problem(),
33+
target_solution,
34+
|value| value.0,
35+
"target assignment does not satisfy CNF clauses",
36+
)?;
37+
38+
Ok(target_solution.clone())
39+
}
40+
}
41+
42+
#[crate::aggregate_reduction(identity)]
43+
impl crate::rules::AggregateReductionResult for ReductionNAESATToSAT {}
44+
45+
#[reduction(transform = exact {
46+
num_vars = "num_vars",
47+
num_clauses = "2 * num_clauses",
48+
num_literals = "2 * num_literals",
49+
})]
50+
impl ReduceTo<Satisfiability> for NAESatisfiability {
51+
type Result = ReductionNAESATToSAT;
52+
53+
fn reduce_to(&self) -> Result<Self::Result, crate::rules::ReductionError> {
54+
let clauses: Vec<CNFClause> = self
55+
.clauses()
56+
.iter()
57+
.flat_map(|clause| {
58+
let negated = clause.literals.iter().map(|&lit| -lit).collect();
59+
[
60+
CNFClause::new(clause.literals.clone()),
61+
CNFClause::new(negated),
62+
]
63+
})
64+
.collect();
65+
66+
let target = Satisfiability::try_new(self.num_vars(), clauses).map_err(
67+
crate::rules::ReductionError::construction::<NAESatisfiability, Satisfiability>,
68+
)?;
69+
70+
Ok(ReductionNAESATToSAT { target })
71+
}
72+
}
73+
74+
#[cfg(feature = "example-db")]
75+
pub(crate) fn canonical_rule_example_specs() -> Vec<crate::example_db::specs::RuleExampleSpec> {
76+
use crate::export::SolutionPair;
77+
78+
vec![crate::example_db::specs::RuleExampleSpec {
79+
id: "naesatisfiability_to_satisfiability",
80+
build: || {
81+
let source = NAESatisfiability::new(
82+
3,
83+
vec![
84+
CNFClause::new(vec![1, 2, 3]),
85+
CNFClause::new(vec![-1, 2]),
86+
CNFClause::new(vec![1, -2, -3]),
87+
],
88+
);
89+
crate::example_db::specs::rule_example_with_witness::<_, Satisfiability>(
90+
source,
91+
SolutionPair {
92+
source_config: serde_json::json!(vec![false, false, true]),
93+
target_config: serde_json::json!(vec![false, false, true]),
94+
},
95+
)
96+
},
97+
}]
98+
}
99+
100+
#[cfg(test)]
101+
#[path = "../unit_tests/rules/naesatisfiability_satisfiability.rs"]
102+
mod tests;

‎src/unit_tests/example_db.rs‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -974,6 +974,21 @@ fn test_find_rule_example_satisfiability_to_naesatisfiability() {
974974
assert_eq!(example.target.problem, "NAESatisfiability");
975975
}
976976

977+
#[test]
978+
fn test_find_rule_example_naesatisfiability_to_satisfiability() {
979+
let source = ProblemRef {
980+
name: "NAESatisfiability".to_string(),
981+
variant: BTreeMap::new(),
982+
};
983+
let target = ProblemRef {
984+
name: "Satisfiability".to_string(),
985+
variant: BTreeMap::new(),
986+
};
987+
let example = find_rule_example(&source, &target).unwrap();
988+
assert_eq!(example.source.problem, "NAESatisfiability");
989+
assert_eq!(example.target.problem, "Satisfiability");
990+
}
991+
977992
#[test]
978993
fn test_find_rule_example_ksatisfiability_to_decisionminimumvertexcover() {
979994
let source = ProblemRef {
Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
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

Comments
 (0)