Skip to content

Commit 43fb5e0

Browse files
committed
feat(rules): add NAESatisfiability to Satisfiability reduction (#1101)
1 parent 875b6e8 commit 43fb5e0

5 files changed

Lines changed: 329 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
@@ -115,6 +115,7 @@ pub(crate) mod minimumvertexcover_minimumsetcovering;
115115
pub(crate) mod minimumvertexcover_minimumweightandorgraph;
116116
pub(crate) mod naesatisfiability_maxcut;
117117
pub(crate) mod naesatisfiability_partitionintoperfectmatchings;
118+
pub(crate) mod naesatisfiability_satisfiability;
118119
pub(crate) mod naesatisfiability_setsplitting;
119120
pub(crate) mod numerical3dimensionalmatching_numericalmatchingwithtargetsums;
120121
pub(crate) mod optimallineararrangement_consecutiveonesmatrixaugmentation;
@@ -411,6 +412,7 @@ pub(crate) fn canonical_rule_example_specs() -> Vec<crate::example_db::specs::Ru
411412
specs.extend(rootedtreearrangement_rootedtreestorageassignment::canonical_rule_example_specs());
412413
specs.extend(naesatisfiability_maxcut::canonical_rule_example_specs());
413414
specs.extend(naesatisfiability_partitionintoperfectmatchings::canonical_rule_example_specs());
415+
specs.extend(naesatisfiability_satisfiability::canonical_rule_example_specs());
414416
specs.extend(satisfiability_maximum2satisfiability::canonical_rule_example_specs());
415417
specs.extend(ensemblecomputation_ilp::canonical_rule_example_specs());
416418
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
@@ -938,6 +938,21 @@ fn test_find_rule_example_satisfiability_to_naesatisfiability() {
938938
assert_eq!(example.target.problem, "NAESatisfiability");
939939
}
940940

941+
#[test]
942+
fn test_find_rule_example_naesatisfiability_to_satisfiability() {
943+
let source = ProblemRef {
944+
name: "NAESatisfiability".to_string(),
945+
variant: BTreeMap::new(),
946+
};
947+
let target = ProblemRef {
948+
name: "Satisfiability".to_string(),
949+
variant: BTreeMap::new(),
950+
};
951+
let example = find_rule_example(&source, &target).unwrap();
952+
assert_eq!(example.source.problem, "NAESatisfiability");
953+
assert_eq!(example.target.problem, "Satisfiability");
954+
}
955+
941956
// PR #779 rules
942957

943958
#[test]
Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
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.extract_solution(&vec![false, false, true]).unwrap(),
106+
vec![false, false, true]
107+
);
108+
// [0,1,0] makes (¬x1, x2) all true, so the excluded all-true CNF clause fails.
109+
let error = reduction
110+
.extract_solution(&vec![false, true, false])
111+
.unwrap_err();
112+
assert!(error
113+
.to_string()
114+
.contains("target assignment does not satisfy CNF clauses"));
115+
}
116+
117+
#[test]
118+
fn test_naesatisfiability_to_satisfiability_rejects_malformed_assignment() {
119+
let reduction = reduce(&canonical_source());
120+
assert!(reduction.extract_solution(&vec![false, false]).is_err());
121+
assert!(reduction
122+
.extract_solution(&vec![false, false, true, false])
123+
.is_err());
124+
}
125+
126+
#[test]
127+
fn test_naesatisfiability_to_satisfiability_empty_conjunction() {
128+
for num_vars in [0, 2] {
129+
let source = NAESatisfiability::new(num_vars, vec![]);
130+
let reduction = reduce(&source);
131+
let target = reduction.target_problem();
132+
assert_eq!(target.num_vars(), num_vars);
133+
assert_eq!(target.num_clauses(), 0);
134+
assert_eq!(target.num_literals(), 0);
135+
assert_equivalent_on_all_assignments(&source);
136+
}
137+
}
138+
139+
#[test]
140+
fn test_naesatisfiability_to_satisfiability_unsatisfiable_repeated_variable() {
141+
let source = NAESatisfiability::new(1, vec![CNFClause::new(vec![1, 1])]);
142+
let reduction = reduce(&source);
143+
let literals: Vec<Vec<i64>> = reduction
144+
.target_problem()
145+
.clauses()
146+
.iter()
147+
.map(|clause| clause.literals.clone())
148+
.collect();
149+
assert_eq!(literals, vec![vec![1, 1], vec![-1, -1]]);
150+
151+
let solver = BruteForce::new();
152+
assert!(solver.solve(&source).unwrap().is_none());
153+
assert!(solver.solve(reduction.target_problem()).unwrap().is_none());
154+
assert_equivalent_on_all_assignments(&source);
155+
}
156+
157+
#[test]
158+
fn test_naesatisfiability_to_satisfiability_repeated_literals() {
159+
// NAE(x1, x1, x2) holds exactly when x1 != x2.
160+
let source = NAESatisfiability::new(2, vec![CNFClause::new(vec![1, 1, 2])]);
161+
assert_equivalent_on_all_assignments(&source);
162+
}
163+
164+
#[test]
165+
fn test_naesatisfiability_to_satisfiability_tautological_clauses() {
166+
// (x1, ¬x1) always has one true and one false literal.
167+
let source = NAESatisfiability::new(
168+
2,
169+
vec![CNFClause::new(vec![1, -1]), CNFClause::new(vec![2, -2, 1])],
170+
);
171+
let reduction = reduce(&source);
172+
assert_eq!(
173+
reduction.target_problem().clauses()[1].literals,
174+
vec![-1, 1]
175+
);
176+
assert_equivalent_on_all_assignments(&source);
177+
}
178+
179+
#[test]
180+
fn test_naesatisfiability_to_satisfiability_arbitrary_clause_lengths() {
181+
let source = NAESatisfiability::new(
182+
5,
183+
vec![
184+
CNFClause::new(vec![1, -2]),
185+
CNFClause::new(vec![2, 3, -4]),
186+
CNFClause::new(vec![-1, 3, 4, 5]),
187+
CNFClause::new(vec![1, 2, -3, -4, 5]),
188+
],
189+
);
190+
let reduction = reduce(&source);
191+
let target = reduction.target_problem();
192+
assert_eq!(target.num_clauses(), 8);
193+
assert_eq!(target.num_literals(), 2 * 14);
194+
assert_equivalent_on_all_assignments(&source);
195+
assert_satisfaction_round_trip_from_satisfaction_target(
196+
&source,
197+
&reduction,
198+
"NAE-SAT->SAT mixed clause lengths",
199+
);
200+
}

0 commit comments

Comments
 (0)