problemreductions/models/formula/
nae_satisfiability.rs1use crate::registry::{FieldInfo, ProblemSchemaEntry};
7use crate::traits::Problem;
8use serde::{Deserialize, Serialize};
9
10use super::{sat::validate_cnf_literals, CNFClause};
11
12inventory::submit! {
13 ProblemSchemaEntry {
14 name: "NAESatisfiability",
15 display_name: "Not-All-Equal Satisfiability",
16 aliases: &["NAESAT"],
17 dimensions: &[],
18 category: crate::registry::ProblemCategory::Formula,
19 module_path: module_path!(),
20 description: "Find an assignment where every CNF clause has both a true and a false literal",
21 fields: &[
22 FieldInfo { name: "num_vars", type_name: "usize", description: "Number of Boolean variables" },
23 FieldInfo { name: "clauses", type_name: "Vec<CNFClause>", description: "Clauses in conjunctive normal form with at least two literals each" },
24 ],
25 }
26}
27
28#[derive(Debug, Clone, Serialize, Deserialize)]
34#[serde(try_from = "NAESatisfiabilityDef")]
35pub struct NAESatisfiability {
36 num_vars: usize,
38 clauses: Vec<CNFClause>,
40}
41
42impl NAESatisfiability {
43 pub fn new(num_vars: usize, clauses: Vec<CNFClause>) -> Self {
48 Self::try_new(num_vars, clauses).unwrap_or_else(|err| panic!("{err}"))
49 }
50
51 pub fn try_new(
54 num_vars: usize,
55 clauses: Vec<CNFClause>,
56 ) -> Result<Self, crate::registry::ConstructionError> {
57 validate_cnf_literals(num_vars, &clauses)?;
58 validate_clause_lengths(&clauses)?;
59 Ok(Self { num_vars, clauses })
60 }
61
62 pub fn num_vars(&self) -> usize {
64 self.num_vars
65 }
66
67 pub fn num_clauses(&self) -> usize {
69 self.clauses.len()
70 }
71
72 pub fn num_literals(&self) -> usize {
74 self.clauses.iter().map(|c| c.len()).sum()
75 }
76
77 pub fn num_literal_pairs(&self) -> usize {
81 self.clauses
82 .iter()
83 .map(|c| c.len() * (c.len() - 1) / 2)
84 .sum()
85 }
86
87 pub fn clauses(&self) -> &[CNFClause] {
89 &self.clauses
90 }
91
92 pub fn get_clause(&self, index: usize) -> Option<&CNFClause> {
94 self.clauses.get(index)
95 }
96
97 pub fn count_nae_satisfied(
99 &self,
100 assignment: &[bool],
101 ) -> Result<i64, crate::traits::EvaluationError> {
102 let count = self
103 .clauses
104 .iter()
105 .filter(|clause| Self::clause_is_nae_satisfied(clause, assignment))
106 .count();
107 i64::try_from(count).map_err(|_| {
108 crate::traits::EvaluationError::IntegerOverflow(
109 "converting NAE-satisfied-clause count to i64".into(),
110 )
111 })
112 }
113
114 pub fn is_nae_satisfying(&self, assignment: &[bool]) -> bool {
116 self.clauses
117 .iter()
118 .all(|clause| Self::clause_is_nae_satisfied(clause, assignment))
119 }
120
121 pub fn is_valid_solution(
123 &self,
124 config: &[bool],
125 ) -> Result<bool, crate::traits::EvaluationError> {
126 if config.len() != self.num_vars {
127 return Err(crate::traits::EvaluationError::InvalidConfiguration(
128 "assignment length does not match the formula variables".into(),
129 ));
130 }
131 Ok(self.is_nae_satisfying(config))
132 }
133
134 fn literal_value(lit: i64, assignment: &[bool]) -> bool {
135 let var = lit.unsigned_abs() as usize - 1;
136 let value = assignment.get(var).copied().unwrap_or(false);
137 if lit > 0 {
138 value
139 } else {
140 !value
141 }
142 }
143
144 fn clause_is_nae_satisfied(clause: &CNFClause, assignment: &[bool]) -> bool {
145 let mut has_true = false;
146 let mut has_false = false;
147
148 for &lit in &clause.literals {
149 if Self::literal_value(lit, assignment) {
150 has_true = true;
151 } else {
152 has_false = true;
153 }
154
155 if has_true && has_false {
156 return true;
157 }
158 }
159
160 false
161 }
162}
163
164impl Problem for NAESatisfiability {
165 const NAME: &'static str = "NAESatisfiability";
166 type Solution = Vec<bool>;
167 type Value = crate::types::Or;
168
169 crate::problem_parameters![
170 ("num_clauses", num_clauses),
171 ("num_literal_pairs", num_literal_pairs),
172 ("num_literals", num_literals),
173 ("num_vars", num_vars),
174 ];
175
176 fn evaluate(
177 &self,
178 config: &Self::Solution,
179 ) -> Result<crate::types::Or, crate::traits::EvaluationError> {
180 Ok(crate::types::Or(self.is_valid_solution(config)?))
181 }
182
183 fn variant() -> Vec<(&'static str, &'static str)> {
184 crate::variant_params![]
185 }
186}
187
188impl crate::solvers::BruteForceProblem for NAESatisfiability {
189 fn dimensions(&self) -> Vec<usize> {
190 vec![2; self.num_vars]
191 }
192}
193
194crate::declare_variants! {
195 default NAESatisfiability => "2^num_vars",
196}
197
198crate::register_brute_force! {
199 NAESatisfiability decode |_, indices: Vec<usize>| crate::config::config_to_bits(&indices),
200}
201
202#[derive(Debug, Clone, Deserialize)]
203struct NAESatisfiabilityDef {
204 num_vars: usize,
205 clauses: Vec<CNFClause>,
206}
207
208impl TryFrom<NAESatisfiabilityDef> for NAESatisfiability {
209 type Error = crate::registry::ConstructionError;
210
211 fn try_from(value: NAESatisfiabilityDef) -> Result<Self, Self::Error> {
212 Self::try_new(value.num_vars, value.clauses)
213 }
214}
215
216fn validate_clause_lengths(
217 clauses: &[CNFClause],
218) -> Result<(), crate::registry::ConstructionError> {
219 for (index, clause) in clauses.iter().enumerate() {
220 if clause.len() < 2 {
221 return Err(format!(
222 "Clause {} has {} literals, expected at least 2",
223 index,
224 clause.len()
225 )
226 .into());
227 }
228 }
229 Ok(())
230}
231
232#[cfg(feature = "example-db")]
233pub(crate) fn canonical_model_example_specs() -> Vec<crate::example_db::specs::ModelExampleSpec> {
234 vec![crate::example_db::specs::ModelExampleSpec {
235 id: "nae_satisfiability",
236 instance: Box::new(NAESatisfiability::new(
237 5,
238 vec![
239 CNFClause::new(vec![1, 2, -3]),
240 CNFClause::new(vec![-1, 3, 4]),
241 CNFClause::new(vec![2, -4, 5]),
242 CNFClause::new(vec![-2, 3, -5]),
243 CNFClause::new(vec![1, -3, 5]),
244 ],
245 )),
246 optimal_config: serde_json::json!(vec![false, false, false, true, true]),
247 optimal_value: serde_json::json!(true),
248 }]
249}
250
251#[cfg(test)]
252#[path = "../../unit_tests/models/formula/nae_satisfiability.rs"]
253mod tests;