problemreductions/rules/ksatisfiability_qubo.rs
1//! Reduction from KSatisfiability to QUBO (Max-K-SAT).
2//!
3//! For K=2 (quadratic penalty), each clause contributes to Q based on literal signs:
4//! - (x_i ∨ x_j): penalty (1-x_i)(1-x_j) → Q[i][i]-=1, Q[j][j]-=1, Q[i][j]+=1, const+=1
5//! - (¬x_i ∨ x_j): penalty x_i(1-x_j) → Q[i][i]+=1, Q[i][j]-=1
6//! - (x_i ∨ ¬x_j): penalty (1-x_i)x_j → Q[j][j]+=1, Q[i][j]-=1
7//! - (¬x_i ∨ ¬x_j): penalty x_i·x_j → Q[i][j]+=1
8//!
9//! For K≥3, we use the Rosenberg quadratization to reduce degree-K penalty terms
10//! to quadratic form by introducing auxiliary variables. Each clause of K literals
11//! requires K−2 auxiliary variables.
12//!
13//! CNFClause uses 1-indexed signed integers: positive = variable, negative = negated.
14
15use crate::models::algebraic::QUBO;
16use crate::models::formula::KSatisfiability;
17use crate::reduction;
18use crate::rules::traits::{ReduceTo, ReductionResult};
19use crate::variant::{K2, K3};
20/// Result of reducing KSatisfiability to QUBO.
21#[derive(Debug, Clone)]
22pub struct ReductionKSatToQUBO {
23 target: QUBO<f64>,
24 source_num_vars: usize,
25}
26
27impl ReductionResult for ReductionKSatToQUBO {
28 type Source = KSatisfiability<K2>;
29 type Target = QUBO<f64>;
30
31 fn target_problem(&self) -> &Self::Target {
32 &self.target
33 }
34
35 fn extract_solution(&self, target_solution: &[usize]) -> Vec<usize> {
36 target_solution[..self.source_num_vars].to_vec()
37 }
38}
39
40/// Result of reducing `KSatisfiability<K3>` to QUBO.
41#[derive(Debug, Clone)]
42pub struct Reduction3SATToQUBO {
43 target: QUBO<f64>,
44 source_num_vars: usize,
45}
46
47impl ReductionResult for Reduction3SATToQUBO {
48 type Source = KSatisfiability<K3>;
49 type Target = QUBO<f64>;
50
51 fn target_problem(&self) -> &Self::Target {
52 &self.target
53 }
54
55 fn extract_solution(&self, target_solution: &[usize]) -> Vec<usize> {
56 target_solution[..self.source_num_vars].to_vec()
57 }
58}
59
60/// Convert a signed literal to (0-indexed variable, is_negated).
61fn lit_to_var(lit: i32) -> (usize, bool) {
62 let var = (lit.unsigned_abs() as usize) - 1;
63 let neg = lit < 0;
64 (var, neg)
65}
66
67/// Add the quadratic penalty term for a 2-literal clause to the QUBO matrix.
68///
69/// For clause (l_i ∨ l_j), the penalty for the clause being unsatisfied is
70/// the product of the complemented literals.
71fn add_2sat_clause_penalty(matrix: &mut [Vec<f64>], lits: &[i32]) {
72 assert_eq!(lits.len(), 2, "Expected 2-literal clause");
73
74 let (var_i, neg_i) = lit_to_var(lits[0]);
75 let (var_j, neg_j) = lit_to_var(lits[1]);
76
77 // Ensure i <= j for upper-triangular form
78 let (i, j, ni, nj) = if var_i <= var_j {
79 (var_i, var_j, neg_i, neg_j)
80 } else {
81 (var_j, var_i, neg_j, neg_i)
82 };
83
84 match (ni, nj) {
85 (false, false) => {
86 // (x_i ∨ x_j): penalty = (1-x_i)(1-x_j) = 1 - x_i - x_j + x_i·x_j
87 matrix[i][i] -= 1.0;
88 matrix[j][j] -= 1.0;
89 matrix[i][j] += 1.0;
90 }
91 (true, false) => {
92 // (¬x_i ∨ x_j): penalty = x_i(1-x_j) = x_i - x_i·x_j
93 matrix[i][i] += 1.0;
94 matrix[i][j] -= 1.0;
95 }
96 (false, true) => {
97 // (x_i ∨ ¬x_j): penalty = (1-x_i)x_j = x_j - x_i·x_j
98 matrix[j][j] += 1.0;
99 matrix[i][j] -= 1.0;
100 }
101 (true, true) => {
102 // (¬x_i ∨ ¬x_j): penalty = x_i·x_j
103 matrix[i][j] += 1.0;
104 }
105 }
106}
107
108/// Add the QUBO terms for a 3-literal clause using Rosenberg quadratization.
109///
110/// For clause (l1 ∨ l2 ∨ l3), the penalty for not satisfying the clause is:
111/// P = (1-l1)(1-l2)(1-l3) = y1·y2·y3
112/// where yi = 1 - li (complement of literal).
113///
114/// We introduce one auxiliary variable `a` and quadratize using the substitution
115/// a = y1·y2, adding penalty M·(y1·y2 - 2·y1·a - 2·y2·a + 3·a) where M is a
116/// sufficiently large penalty (M = 2 suffices for Max-SAT).
117///
118/// The resulting quadratic form is:
119/// H = a·y3 + M·(y1·y2 - 2·y1·a - 2·y2·a + 3·a)
120///
121/// `aux_var` is the 0-indexed auxiliary variable.
122fn add_3sat_clause_penalty(matrix: &mut [Vec<f64>], lits: &[i32], aux_var: usize) {
123 assert_eq!(lits.len(), 3, "Expected 3-literal clause");
124 let penalty = 2.0; // Rosenberg penalty weight
125
126 let (v1, n1) = lit_to_var(lits[0]);
127 let (v2, n2) = lit_to_var(lits[1]);
128 let (v3, n3) = lit_to_var(lits[2]);
129 let a = aux_var;
130
131 // We need to express yi = (1 - li) in terms of xi:
132 // If literal is positive (li = xi): yi = 1 - xi
133 // If literal is negated (li = 1 - xi): yi = xi
134 //
135 // So yi = xi if negated, yi = 1 - xi if positive.
136 //
137 // We compute the QUBO terms for:
138 // H = a·y3 + M·(y1·y2 - 2·y1·a - 2·y2·a + 3·a)
139 //
140 // Each term is expanded using yi = xi (if negated) or yi = 1-xi (if positive).
141
142 // Helper: add coefficient * yi * yj to the matrix
143 // where yi depends on variable vi and negation ni
144 let add_yy = |matrix: &mut [Vec<f64>], vi: usize, ni: bool, vj: usize, nj: bool, coeff: f64| {
145 // yi = xi if ni (negated literal), yi = 1 - xi if !ni (positive literal)
146 // yi * yj expansion:
147 if vi == vj {
148 // Same variable: yi * yj
149 // Both complemented the same way means yi = yj, so yi*yj = yi (binary)
150 // If ni == nj: yi*yj = yi^2 = yi (binary), add coeff * yi
151 // If ni != nj: yi * yj = xi * (1-xi) = 0 (always), add nothing
152 if ni == nj {
153 // yi * yi = yi (binary)
154 if ni {
155 // yi = xi, add coeff * xi
156 matrix[vi][vi] += coeff;
157 } else {
158 // yi = 1 - xi, add coeff * (1 - xi) = coeff - coeff * xi
159 // constant term ignored in QUBO (offset), diagonal:
160 matrix[vi][vi] -= coeff;
161 }
162 }
163 // else: xi * (1-xi) = 0, nothing to add
164 return;
165 }
166 // Different variables: yi * yj
167 let (lo, hi, lo_neg, hi_neg) = if vi < vj {
168 (vi, vj, ni, nj)
169 } else {
170 (vj, vi, nj, ni)
171 };
172 // yi = xi if neg, else 1-xi
173 // yj = xj if neg, else 1-xj
174 // yi*yj = (xi if neg_i else 1-xi) * (xj if neg_j else 1-xj)
175 match (lo_neg, hi_neg) {
176 (true, true) => {
177 // xi * xj
178 matrix[lo][hi] += coeff;
179 }
180 (true, false) => {
181 // xi * (1 - xj) = xi - xi*xj
182 matrix[lo][lo] += coeff;
183 matrix[lo][hi] -= coeff;
184 }
185 (false, true) => {
186 // (1 - xi) * xj = xj - xi*xj
187 matrix[hi][hi] += coeff;
188 matrix[lo][hi] -= coeff;
189 }
190 (false, false) => {
191 // (1-xi)(1-xj) = 1 - xi - xj + xi*xj
192 // constant 1 ignored (offset)
193 matrix[lo][lo] -= coeff;
194 matrix[hi][hi] -= coeff;
195 matrix[lo][hi] += coeff;
196 }
197 }
198 };
199
200 // Helper: add coefficient * yi * a to the matrix
201 // where yi depends on variable vi and negation ni, a is aux variable
202 let add_ya = |matrix: &mut [Vec<f64>], vi: usize, ni: bool, a: usize, coeff: f64| {
203 // yi = xi if ni (negated literal), yi = 1-xi if !ni (positive literal)
204 // yi * a:
205 let (lo, hi) = if vi < a { (vi, a) } else { (a, vi) };
206 if ni {
207 // yi = xi, so yi * a = xi * a
208 matrix[lo][hi] += coeff;
209 } else {
210 // yi = 1 - xi, so yi * a = a - xi * a
211 matrix[a][a] += coeff;
212 matrix[lo][hi] -= coeff;
213 }
214 };
215
216 // Helper: add coefficient * yi to the matrix (linear term)
217 let add_y = |matrix: &mut [Vec<f64>], vi: usize, ni: bool, coeff: f64| {
218 if ni {
219 // yi = xi
220 matrix[vi][vi] += coeff;
221 } else {
222 // yi = 1 - xi, linear part: -coeff * xi (constant coeff ignored)
223 matrix[vi][vi] -= coeff;
224 }
225 };
226
227 // Term 1: a * y3 (coefficient = 1.0)
228 add_ya(matrix, v3, n3, a, 1.0);
229
230 // Term 2: M * y1 * y2
231 add_yy(matrix, v1, n1, v2, n2, penalty);
232
233 // Term 3: -2M * y1 * a
234 add_ya(matrix, v1, n1, a, -2.0 * penalty);
235
236 // Term 4: -2M * y2 * a
237 add_ya(matrix, v2, n2, a, -2.0 * penalty);
238
239 // Term 5: 3M * a (linear)
240 // a is a binary variable, a^2 = a, so linear a → diagonal
241 matrix[a][a] += 3.0 * penalty;
242
243 // We also need to add linear terms that come from constant offsets in products
244 // Actually, let's verify: the full expansion of
245 // H = a·y3 + M·(y1·y2 - 2·y1·a - 2·y2·a + 3·a)
246 // All terms are handled above.
247 //
248 // However, we need to account for the case where "add_ya" with !ni adds
249 // a linear term in `a`. Let me verify the add_ya logic handles this correctly.
250 //
251 // add_ya with ni=false: yi = 1-xi, yi*a = a - xi*a
252 // matrix[a][a] += coeff (linear in a)
253 // matrix[min(vi,a)][max(vi,a)] -= coeff (quadratic xi*a)
254 // This is correct.
255
256 // Note: We ignore constant terms (don't affect QUBO optimization).
257 let _ = add_y; // suppress unused warning - linear terms in y handled via products
258}
259
260/// Build a QUBO matrix from a KSatisfiability instance.
261///
262/// For K=2, directly encodes quadratic penalties.
263/// For K=3, uses Rosenberg quadratization with one auxiliary variable per clause.
264///
265/// Returns (matrix, num_source_vars) where matrix is (n + aux) x (n + aux).
266fn build_qubo_matrix(
267 num_vars: usize,
268 clauses: &[crate::models::formula::CNFClause],
269 k: usize,
270) -> Vec<Vec<f64>> {
271 match k {
272 2 => {
273 let mut matrix = vec![vec![0.0; num_vars]; num_vars];
274 for clause in clauses {
275 add_2sat_clause_penalty(&mut matrix, &clause.literals);
276 }
277 matrix
278 }
279 3 => {
280 let num_aux = clauses.len(); // one auxiliary per clause
281 let total = num_vars + num_aux;
282 let mut matrix = vec![vec![0.0; total]; total];
283 for (idx, clause) in clauses.iter().enumerate() {
284 let aux_var = num_vars + idx;
285 add_3sat_clause_penalty(&mut matrix, &clause.literals, aux_var);
286 }
287 matrix
288 }
289 _ => unimplemented!("KSatisfiability to QUBO only supports K=2 and K=3"),
290 }
291}
292
293#[reduction(
294 overhead = { num_vars = "num_vars" }
295)]
296impl ReduceTo<QUBO<f64>> for KSatisfiability<K2> {
297 type Result = ReductionKSatToQUBO;
298
299 fn reduce_to(&self) -> Self::Result {
300 let n = self.num_vars();
301 let matrix = build_qubo_matrix(n, self.clauses(), 2);
302
303 ReductionKSatToQUBO {
304 target: QUBO::from_matrix(matrix),
305 source_num_vars: n,
306 }
307 }
308}
309
310#[reduction(
311 overhead = { num_vars = "num_vars + num_clauses" }
312)]
313impl ReduceTo<QUBO<f64>> for KSatisfiability<K3> {
314 type Result = Reduction3SATToQUBO;
315
316 fn reduce_to(&self) -> Self::Result {
317 let n = self.num_vars();
318 let matrix = build_qubo_matrix(n, self.clauses(), 3);
319
320 Reduction3SATToQUBO {
321 target: QUBO::from_matrix(matrix),
322 source_num_vars: n,
323 }
324 }
325}
326
327#[cfg(test)]
328#[path = "../unit_tests/rules/ksatisfiability_qubo.rs"]
329mod tests;