Skip to main content

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::decision::Decision;
17use crate::models::formula::KSatisfiability;
18use crate::reduction;
19use crate::rules::traits::{ReduceTo, ReductionResult};
20use crate::variant::{K2, K3};
21/// Result of reducing KSatisfiability to QUBO.
22#[derive(Debug, Clone)]
23pub struct ReductionKSatToQUBO {
24    target: Decision<QUBO<i64>>,
25    source_num_vars: usize,
26}
27
28impl ReductionResult for ReductionKSatToQUBO {
29    type Source = KSatisfiability<K2>;
30    type Target = Decision<QUBO<i64>>;
31
32    fn target_problem(&self) -> &Self::Target {
33        &self.target
34    }
35
36    fn extract_solution(
37        &self,
38        target_solution: &<Self::Target as crate::traits::Problem>::Solution,
39    ) -> crate::rules::ExtractionResult<<Self::Source as crate::traits::Problem>::Solution> {
40        crate::rules::traits::validate_target_witness(
41            self.target_problem(),
42            target_solution,
43            |value| value.0,
44            "QUBO energy does not meet the SAT zero-penalty threshold",
45        )?;
46        Ok(target_solution[..self.source_num_vars].to_vec())
47    }
48}
49
50/// Result of reducing `KSatisfiability<K3>` to QUBO.
51#[derive(Debug, Clone)]
52pub struct Reduction3SATToQUBO {
53    target: Decision<QUBO<i64>>,
54    source_num_vars: usize,
55}
56
57impl ReductionResult for Reduction3SATToQUBO {
58    type Source = KSatisfiability<K3>;
59    type Target = Decision<QUBO<i64>>;
60
61    fn target_problem(&self) -> &Self::Target {
62        &self.target
63    }
64
65    fn extract_solution(
66        &self,
67        target_solution: &<Self::Target as crate::traits::Problem>::Solution,
68    ) -> crate::rules::ExtractionResult<<Self::Source as crate::traits::Problem>::Solution> {
69        crate::rules::traits::validate_target_witness(
70            self.target_problem(),
71            target_solution,
72            |value| value.0,
73            "QUBO energy does not meet the SAT zero-penalty threshold",
74        )?;
75        Ok(target_solution[..self.source_num_vars].to_vec())
76    }
77}
78
79/// Add the quadratic penalty term for a 2-literal clause to the QUBO matrix.
80///
81/// For clause (l_i ∨ l_j), the penalty for the clause being unsatisfied is
82/// the product of the complemented literals.
83fn add_coefficient(
84    matrix: &mut [Vec<i64>],
85    row: usize,
86    column: usize,
87    coefficient: i64,
88) -> Result<(), &'static str> {
89    matrix[row][column] = matrix[row][column]
90        .checked_add(coefficient)
91        .ok_or("adding a SAT QUBO coefficient")?;
92    Ok(())
93}
94
95fn add_2sat_clause_penalty(
96    matrix: &mut [Vec<i64>],
97    lits: &[(usize, bool)],
98) -> Result<(), &'static str> {
99    assert_eq!(lits.len(), 2, "Expected 2-literal clause");
100
101    let (var_i, neg_i) = lits[0];
102    let (var_j, neg_j) = lits[1];
103
104    // Ensure i <= j for upper-triangular form
105    let (i, j, ni, nj) = if var_i <= var_j {
106        (var_i, var_j, neg_i, neg_j)
107    } else {
108        (var_j, var_i, neg_j, neg_i)
109    };
110
111    match (ni, nj) {
112        (false, false) => {
113            // (x_i ∨ x_j): penalty = (1-x_i)(1-x_j) = 1 - x_i - x_j + x_i·x_j
114            add_coefficient(matrix, i, i, -1)?;
115            add_coefficient(matrix, j, j, -1)?;
116            add_coefficient(matrix, i, j, 1)?;
117        }
118        (true, false) => {
119            // (¬x_i ∨ x_j): penalty = x_i(1-x_j) = x_i - x_i·x_j
120            add_coefficient(matrix, i, i, 1)?;
121            add_coefficient(matrix, i, j, -1)?;
122        }
123        (false, true) => {
124            // (x_i ∨ ¬x_j): penalty = (1-x_i)x_j = x_j - x_i·x_j
125            add_coefficient(matrix, j, j, 1)?;
126            add_coefficient(matrix, i, j, -1)?;
127        }
128        (true, true) => {
129            // (¬x_i ∨ ¬x_j): penalty = x_i·x_j
130            add_coefficient(matrix, i, j, 1)?;
131        }
132    }
133    Ok(())
134}
135
136/// Add the QUBO terms for a 3-literal clause using Rosenberg quadratization.
137///
138/// For clause (l1 ∨ l2 ∨ l3), the penalty for not satisfying the clause is:
139///   P = (1-l1)(1-l2)(1-l3) = y1·y2·y3
140/// where yi = 1 - li (complement of literal).
141///
142/// We introduce one auxiliary variable `a` and quadratize using the substitution
143/// a = y1·y2, adding penalty M·(y1·y2 - 2·y1·a - 2·y2·a + 3·a) where M is a
144/// sufficiently large penalty (M = 2 suffices for Max-SAT).
145///
146/// The resulting quadratic form is:
147///   H = a·y3 + M·(y1·y2 - 2·y1·a - 2·y2·a + 3·a)
148///
149/// `aux_var` is the 0-indexed auxiliary variable.
150fn add_3sat_clause_penalty(
151    matrix: &mut [Vec<i64>],
152    lits: &[(usize, bool)],
153    aux_var: usize,
154) -> Result<(), &'static str> {
155    assert_eq!(lits.len(), 3, "Expected 3-literal clause");
156    let penalty = 2; // Rosenberg penalty weight
157
158    let (v1, n1) = lits[0];
159    let (v2, n2) = lits[1];
160    let (v3, n3) = lits[2];
161    let a = aux_var;
162
163    // We need to express yi = (1 - li) in terms of xi:
164    //   If literal is positive (li = xi): yi = 1 - xi
165    //   If literal is negated (li = 1 - xi): yi = xi
166    //
167    // So yi = xi if negated, yi = 1 - xi if positive.
168    //
169    // We compute the QUBO terms for:
170    //   H = a·y3 + M·(y1·y2 - 2·y1·a - 2·y2·a + 3·a)
171    //
172    // Each term is expanded using yi = xi (if negated) or yi = 1-xi (if positive).
173
174    // Helper: add coefficient * yi * yj to the matrix
175    // where yi depends on variable vi and negation ni
176    let add_yy = |matrix: &mut [Vec<i64>],
177                  vi: usize,
178                  ni: bool,
179                  vj: usize,
180                  nj: bool,
181                  coeff: i64|
182     -> Result<(), &'static str> {
183        // yi = xi if ni (negated literal), yi = 1 - xi if !ni (positive literal)
184        // yi * yj expansion:
185        if vi == vj {
186            // Same variable: yi * yj
187            // Both complemented the same way means yi = yj, so yi*yj = yi (binary)
188            // If ni == nj: yi*yj = yi^2 = yi (binary), add coeff * yi
189            // If ni != nj: yi * yj = xi * (1-xi) = 0 (always), add nothing
190            if ni == nj {
191                // yi * yi = yi (binary)
192                if ni {
193                    // yi = xi, add coeff * xi
194                    add_coefficient(matrix, vi, vi, coeff)?;
195                } else {
196                    // yi = 1 - xi, add coeff * (1 - xi) = coeff - coeff * xi
197                    // constant term ignored in QUBO (offset), diagonal:
198                    add_coefficient(matrix, vi, vi, -coeff)?;
199                }
200            }
201            // else: xi * (1-xi) = 0, nothing to add
202            return Ok(());
203        }
204        // Different variables: yi * yj
205        let (lo, hi, lo_neg, hi_neg) = if vi < vj {
206            (vi, vj, ni, nj)
207        } else {
208            (vj, vi, nj, ni)
209        };
210        // yi = xi if neg, else 1-xi
211        // yj = xj if neg, else 1-xj
212        // yi*yj = (xi if neg_i else 1-xi) * (xj if neg_j else 1-xj)
213        match (lo_neg, hi_neg) {
214            (true, true) => {
215                // xi * xj
216                add_coefficient(matrix, lo, hi, coeff)?;
217            }
218            (true, false) => {
219                // xi * (1 - xj) = xi - xi*xj
220                add_coefficient(matrix, lo, lo, coeff)?;
221                add_coefficient(matrix, lo, hi, -coeff)?;
222            }
223            (false, true) => {
224                // (1 - xi) * xj = xj - xi*xj
225                add_coefficient(matrix, hi, hi, coeff)?;
226                add_coefficient(matrix, lo, hi, -coeff)?;
227            }
228            (false, false) => {
229                // (1-xi)(1-xj) = 1 - xi - xj + xi*xj
230                // constant 1 ignored (offset)
231                add_coefficient(matrix, lo, lo, -coeff)?;
232                add_coefficient(matrix, hi, hi, -coeff)?;
233                add_coefficient(matrix, lo, hi, coeff)?;
234            }
235        }
236        Ok(())
237    };
238
239    // Helper: add coefficient * yi * a to the matrix
240    // where yi depends on variable vi and negation ni, a is aux variable
241    let add_ya = |matrix: &mut [Vec<i64>],
242                  vi: usize,
243                  ni: bool,
244                  a: usize,
245                  coeff: i64|
246     -> Result<(), &'static str> {
247        // yi = xi if ni (negated literal), yi = 1-xi if !ni (positive literal)
248        // yi * a:
249        let (lo, hi) = if vi < a { (vi, a) } else { (a, vi) };
250        if ni {
251            // yi = xi, so yi * a = xi * a
252            add_coefficient(matrix, lo, hi, coeff)?;
253        } else {
254            // yi = 1 - xi, so yi * a = a - xi * a
255            add_coefficient(matrix, a, a, coeff)?;
256            add_coefficient(matrix, lo, hi, -coeff)?;
257        }
258        Ok(())
259    };
260
261    // Term 1: a * y3 (coefficient = 1)
262    add_ya(matrix, v3, n3, a, 1)?;
263
264    // Term 2: M * y1 * y2
265    add_yy(matrix, v1, n1, v2, n2, penalty)?;
266
267    // Term 3: -2M * y1 * a
268    add_ya(matrix, v1, n1, a, -2 * penalty)?;
269
270    // Term 4: -2M * y2 * a
271    add_ya(matrix, v2, n2, a, -2 * penalty)?;
272
273    // Term 5: 3M * a (linear)
274    // a is a binary variable, a^2 = a, so linear a → diagonal
275    add_coefficient(matrix, a, a, 3 * penalty)?;
276
277    Ok(())
278}
279
280/// Expand clause penalties and retain the constant omitted by QUBO.
281/// K3 reserves one auxiliary per clause, including free auxiliaries for short
282/// clauses; K2 reserves none. The source constructor validates clause widths.
283fn build_qubo_matrix(
284    num_vars: usize,
285    clauses: &[crate::models::formula::CNFClause],
286    num_aux: usize,
287) -> Result<(Vec<Vec<i64>>, i64), &'static str> {
288    let total = num_vars
289        .checked_add(num_aux)
290        .ok_or("computing the number of SAT QUBO variables")?;
291    total
292        .checked_mul(total)
293        .ok_or("computing the SAT QUBO dense matrix entry count")?;
294    let mut matrix = vec![vec![0; total]; total];
295    let mut constant = 0i64;
296    for (idx, clause) in clauses.iter().enumerate() {
297        let literals: Vec<_> = clause
298            .variables()
299            .into_iter()
300            .zip(&clause.literals)
301            .map(|(variable, &literal)| (variable, literal < 0))
302            .collect();
303        let offset = match literals.as_slice() {
304            [] => 1,
305            &[(v, neg)] => {
306                add_coefficient(&mut matrix, v, v, if neg { 1 } else { -1 })?;
307                i64::from(!neg)
308            }
309            &[(_, n1), (_, n2)] => {
310                add_2sat_clause_penalty(&mut matrix, &literals)?;
311                i64::from(!n1 && !n2)
312            }
313            &[(_, n1), (_, n2), _] => {
314                add_3sat_clause_penalty(&mut matrix, &literals, num_vars + idx)?;
315                2 * i64::from(!n1 && !n2)
316            }
317            _ => unreachable!("the source validates clause width at most three"),
318        };
319        constant = constant
320            .checked_add(offset)
321            .ok_or("accumulating the SAT QUBO constant")?;
322    }
323    Ok((matrix, constant))
324}
325
326#[crate::aggregate_reduction(identity)]
327impl crate::rules::AggregateReductionResult for ReductionKSatToQUBO {}
328
329#[crate::aggregate_reduction(identity)]
330impl crate::rules::AggregateReductionResult for Reduction3SATToQUBO {}
331
332#[reduction(
333    transform = {
334        exact { num_vars = "num_vars" },
335        upper_bound { num_quadratic_terms = "num_vars * (num_vars - 1) / 2" },
336    }
337)]
338impl ReduceTo<Decision<QUBO<i64>>> for KSatisfiability<K2> {
339    type Result = ReductionKSatToQUBO;
340
341    fn reduce_to(&self) -> Result<Self::Result, crate::rules::ReductionError> {
342        let n = self.num_vars();
343        let (matrix, constant) =
344            build_qubo_matrix(n, self.clauses(), 0).map_err(|operation| {
345                crate::rules::ReductionError::integer_overflow::<
346                    KSatisfiability<K2>,
347                    Decision<QUBO<i64>>,
348                >(operation)
349            })?;
350
351        Ok(ReductionKSatToQUBO {
352            target: Decision::new(
353                QUBO::from_matrix(matrix).map_err(|message| {
354                    crate::rules::ReductionError::construction::<
355                        KSatisfiability<K2>,
356                        Decision<QUBO<i64>>,
357                    >(message)
358                })?,
359                -constant,
360            ),
361            source_num_vars: n,
362        })
363    }
364}
365
366#[reduction(transform = {
367    exact {
368        num_vars = "num_vars + num_clauses",
369    },
370    upper_bound {
371        num_quadratic_terms = "(num_vars + num_clauses) * ((num_vars + num_clauses) - 1) / 2",
372    },
373})]
374impl ReduceTo<Decision<QUBO<i64>>> for KSatisfiability<K3> {
375    type Result = Reduction3SATToQUBO;
376
377    fn reduce_to(&self) -> Result<Self::Result, crate::rules::ReductionError> {
378        let n = self.num_vars();
379        let (matrix, constant) =
380            build_qubo_matrix(n, self.clauses(), self.num_clauses()).map_err(|operation| {
381                crate::rules::ReductionError::integer_overflow::<
382                    KSatisfiability<K3>,
383                    Decision<QUBO<i64>>,
384                >(operation)
385            })?;
386
387        Ok(Reduction3SATToQUBO {
388            target: Decision::new(
389                QUBO::from_matrix(matrix).map_err(|message| {
390                    crate::rules::ReductionError::construction::<
391                        KSatisfiability<K3>,
392                        Decision<QUBO<i64>>,
393                    >(message)
394                })?,
395                -constant,
396            ),
397            source_num_vars: n,
398        })
399    }
400}
401
402#[cfg(feature = "example-db")]
403pub(crate) fn canonical_rule_example_specs() -> Vec<crate::example_db::specs::RuleExampleSpec> {
404    use crate::export::SolutionPair;
405    use crate::models::algebraic::QUBO;
406    use crate::models::formula::CNFClause;
407
408    vec![
409        crate::example_db::specs::RuleExampleSpec {
410            id: "ksatisfiability_k2_to_qubo",
411            build: || {
412                let source = KSatisfiability::<K2>::new(
413                    4,
414                    vec![
415                        CNFClause::new(vec![1, 2]),
416                        CNFClause::new(vec![-1, 3]),
417                        CNFClause::new(vec![-2, 4]),
418                        CNFClause::new(vec![-3, -4]),
419                    ],
420                );
421                crate::example_db::specs::rule_example_with_witness::<_, Decision<QUBO<i64>>>(
422                    source,
423                    SolutionPair {
424                        source_config: serde_json::json!(vec![false, true, false, true]),
425                        target_config: serde_json::json!(vec![false, true, false, true]),
426                    },
427                )
428            },
429        },
430        crate::example_db::specs::RuleExampleSpec {
431            id: "ksatisfiability_to_qubo",
432            build: || {
433                let source = KSatisfiability::<K3>::new(
434                    5,
435                    vec![
436                        CNFClause::new(vec![1, 2, -3]),
437                        CNFClause::new(vec![-1, 3, 4]),
438                        CNFClause::new(vec![2, -4, 5]),
439                        CNFClause::new(vec![-2, 3, -5]),
440                        CNFClause::new(vec![1, -3, 5]),
441                        CNFClause::new(vec![-1, -2, 4]),
442                        CNFClause::new(vec![3, -4, -5]),
443                    ],
444                );
445                crate::example_db::specs::rule_example_with_witness::<_, Decision<QUBO<i64>>>(
446                    source,
447                    SolutionPair {
448                        source_config: serde_json::json!(vec![false, false, false, false, false]),
449                        target_config: serde_json::json!(vec![
450                            false, false, false, false, false, true, false, false, false, false,
451                            false, false
452                        ]),
453                    },
454                )
455            },
456        },
457    ]
458}
459
460#[cfg(test)]
461#[path = "../unit_tests/rules/ksatisfiability_qubo.rs"]
462mod tests;