Skip to main content

problemreductions/rules/
circuit_spinglass.rs

1//! Reduction from CircuitSAT to SpinGlass.
2//!
3//! This module implements the reduction from boolean circuit satisfiability
4//! to the Spin Glass (Ising model) problem using logic gadgets.
5//!
6//! Each logic gate is encoded as a SpinGlass Hamiltonian where the ground
7//! states correspond to valid input/output combinations.
8
9use crate::models::formula::{Assignment, BooleanExpr, BooleanOp, CircuitSAT};
10use crate::models::graph::SpinGlass;
11use crate::reduction;
12use crate::rules::traits::{ReduceTo, ReductionResult};
13use crate::topology::SimpleGraph;
14use num_traits::Zero;
15use std::collections::HashMap;
16use std::ops::AddAssign;
17
18/// A logic gadget represented as a SpinGlass problem.
19///
20/// Each gadget encodes a logic gate where the ground states of the
21/// Hamiltonian correspond to valid input/output combinations.
22///
23/// # References
24/// - [What are the cost function for NAND and NOR gates?](https://support.dwavesys.com/hc/en-us/community/posts/1500000470701-What-are-the-cost-function-for-NAND-and-NOR-gates)
25/// - Nguyen, M.-T., Liu, J.-G., et al., PRX Quantum 4, 010316 (2023)
26#[derive(Debug, Clone)]
27pub struct LogicGadget<W> {
28    /// The SpinGlass problem encoding the gate.
29    pub problem: SpinGlass<SimpleGraph, W>,
30    /// Input spin indices (0-indexed within the gadget).
31    #[allow(dead_code)] // read in tests
32    pub inputs: Vec<usize>,
33    /// Output spin indices (0-indexed within the gadget).
34    #[allow(dead_code)] // read in tests
35    pub outputs: Vec<usize>,
36}
37
38impl<W> LogicGadget<W> {
39    /// Create a new logic gadget.
40    pub fn new(
41        problem: SpinGlass<SimpleGraph, W>,
42        inputs: Vec<usize>,
43        outputs: Vec<usize>,
44    ) -> Self {
45        Self {
46            problem,
47            inputs,
48            outputs,
49        }
50    }
51}
52
53impl<W: Clone + Default> LogicGadget<W> {
54    /// Get the number of spins in this gadget.
55    pub fn num_spins(&self) -> usize {
56        self.problem.num_spins()
57    }
58}
59
60/// Create an AND gate gadget.
61///
62/// 3-variable SpinGlass: inputs at indices 0, 1; output at index 2.
63/// Ground states: (0,0,0), (0,1,0), (1,0,0), (1,1,1) corresponding to
64/// all valid AND truth table entries.
65///
66/// J = [1, -2, -2] for edges (0,1), (0,2), (1,2)
67/// h = [-1, -1, 2] (negated from Julia to account for different spin convention)
68///
69/// Note: Julia uses config 0 -> spin +1, 1 -> spin -1
70///       Rust uses config 0 -> spin -1, 1 -> spin +1
71///       So h values are negated to produce equivalent ground states.
72pub fn and_gadget<W>() -> LogicGadget<W>
73where
74    W: Clone + Default + From<i32>,
75{
76    let interactions = vec![
77        ((0, 1), W::from(1)),
78        ((0, 2), W::from(-2)),
79        ((1, 2), W::from(-2)),
80    ];
81    let fields = vec![W::from(-1), W::from(-1), W::from(2)];
82    let sg = SpinGlass::new(3, interactions, fields);
83    LogicGadget::new(sg, vec![0, 1], vec![2])
84}
85
86/// Create an OR gate gadget.
87///
88/// 3-variable SpinGlass: inputs at indices 0, 1; output at index 2.
89/// Ground states: (0,0,0), (0,1,1), (1,0,1), (1,1,1) corresponding to
90/// all valid OR truth table entries.
91///
92/// J = [1, -2, -2] for edges (0,1), (0,2), (1,2)
93/// h = [1, 1, -2] (negated from Julia to account for different spin convention)
94pub fn or_gadget<W>() -> LogicGadget<W>
95where
96    W: Clone + Default + From<i32>,
97{
98    let interactions = vec![
99        ((0, 1), W::from(1)),
100        ((0, 2), W::from(-2)),
101        ((1, 2), W::from(-2)),
102    ];
103    let fields = vec![W::from(1), W::from(1), W::from(-2)];
104    let sg = SpinGlass::new(3, interactions, fields);
105    LogicGadget::new(sg, vec![0, 1], vec![2])
106}
107
108/// Create a NOT gate gadget.
109///
110/// 2-variable SpinGlass: input at index 0; output at index 1.
111/// Ground states: (0,1), (1,0) corresponding to valid NOT.
112///
113/// J = \[1\] for edge (0,1)
114/// h = \[0, 0\]
115pub fn not_gadget<W>() -> LogicGadget<W>
116where
117    W: Clone + Default + From<i32> + Zero,
118{
119    let interactions = vec![((0, 1), W::from(1))];
120    let fields = vec![W::zero(), W::zero()];
121    let sg = SpinGlass::new(2, interactions, fields);
122    LogicGadget::new(sg, vec![0], vec![1])
123}
124
125/// Create an XOR gate gadget.
126///
127/// 4-variable SpinGlass: inputs at indices 0, 1; output at 2; auxiliary at 3.
128/// Ground states correspond to valid XOR truth table entries.
129///
130/// J = [1, -1, -2, -1, -2, 2] for edges (0,1), (0,2), (0,3), (1,2), (1,3), (2,3)
131/// h = [-1, -1, 1, 2] (negated from Julia to account for different spin convention)
132pub fn xor_gadget<W>() -> LogicGadget<W>
133where
134    W: Clone + Default + From<i32>,
135{
136    let interactions = vec![
137        ((0, 1), W::from(1)),
138        ((0, 2), W::from(-1)),
139        ((0, 3), W::from(-2)),
140        ((1, 2), W::from(-1)),
141        ((1, 3), W::from(-2)),
142        ((2, 3), W::from(2)),
143    ];
144    let fields = vec![W::from(-1), W::from(-1), W::from(1), W::from(2)];
145    let sg = SpinGlass::new(4, interactions, fields);
146    // Note: output is at index 2 (not 3) according to Julia code
147    // The Julia code has: LogicGadget(sg, [1, 2], [3]) which is 1-indexed
148    // In 0-indexed: inputs [0, 1], output [2]
149    LogicGadget::new(sg, vec![0, 1], vec![2])
150}
151
152/// Create a SET0 gadget (constant false).
153///
154/// 1-variable SpinGlass that prefers config 0 (spin -1 in Rust convention).
155/// h = \[1\] (negated from Julia's \[-1\] to account for different spin convention)
156pub fn set0_gadget<W>() -> LogicGadget<W>
157where
158    W: Clone + Default + From<i32>,
159{
160    let interactions = vec![];
161    let fields = vec![W::from(1)];
162    let sg = SpinGlass::new(1, interactions, fields);
163    LogicGadget::new(sg, vec![], vec![0])
164}
165
166/// Create a SET1 gadget (constant true).
167///
168/// 1-variable SpinGlass that prefers config 1 (spin +1 in Rust convention).
169/// h = \[-1\] (negated from Julia's \[1\] to account for different spin convention)
170pub fn set1_gadget<W>() -> LogicGadget<W>
171where
172    W: Clone + Default + From<i32>,
173{
174    let interactions = vec![];
175    let fields = vec![W::from(-1)];
176    let sg = SpinGlass::new(1, interactions, fields);
177    LogicGadget::new(sg, vec![], vec![0])
178}
179
180/// Result of reducing CircuitSAT to SpinGlass.
181#[derive(Debug, Clone)]
182pub struct ReductionCircuitToSG {
183    /// The target SpinGlass problem.
184    target: SpinGlass<SimpleGraph, i32>,
185    /// Mapping from source variable names to spin indices.
186    variable_map: HashMap<String, usize>,
187    /// Source variable names in order.
188    source_variables: Vec<String>,
189}
190
191impl ReductionResult for ReductionCircuitToSG {
192    type Source = CircuitSAT;
193    type Target = SpinGlass<SimpleGraph, i32>;
194
195    fn target_problem(&self) -> &Self::Target {
196        &self.target
197    }
198
199    fn extract_solution(&self, target_solution: &[usize]) -> Vec<usize> {
200        self.source_variables
201            .iter()
202            .map(|var| {
203                self.variable_map
204                    .get(var)
205                    .and_then(|&idx| target_solution.get(idx).copied())
206                    .unwrap_or(0)
207            })
208            .collect()
209    }
210}
211
212/// Builder for constructing the combined SpinGlass from circuit gadgets.
213struct SpinGlassBuilder<W> {
214    /// Current number of spins.
215    num_spins: usize,
216    /// Accumulated interactions.
217    interactions: HashMap<(usize, usize), W>,
218    /// Accumulated fields.
219    fields: Vec<W>,
220    /// Variable name to spin index mapping.
221    variable_map: HashMap<String, usize>,
222}
223
224impl<W> SpinGlassBuilder<W>
225where
226    W: Clone + Default + Zero + AddAssign + From<i32>,
227{
228    fn new() -> Self {
229        Self {
230            num_spins: 0,
231            interactions: HashMap::new(),
232            fields: Vec::new(),
233            variable_map: HashMap::new(),
234        }
235    }
236
237    /// Allocate a new spin and return its index.
238    fn allocate_spin(&mut self) -> usize {
239        let idx = self.num_spins;
240        self.num_spins += 1;
241        self.fields.push(W::zero());
242        idx
243    }
244
245    /// Get or create a spin index for a variable.
246    fn get_or_create_variable(&mut self, name: &str) -> usize {
247        if let Some(&idx) = self.variable_map.get(name) {
248            idx
249        } else {
250            let idx = self.allocate_spin();
251            self.variable_map.insert(name.to_string(), idx);
252            idx
253        }
254    }
255
256    /// Add a gadget to the builder with the given spin mapping.
257    fn add_gadget(&mut self, gadget: &LogicGadget<W>, spin_map: &[usize]) {
258        // Add interactions
259        for ((i, j), weight) in gadget.problem.interactions() {
260            let global_i = spin_map[i];
261            let global_j = spin_map[j];
262            let key = if global_i < global_j {
263                (global_i, global_j)
264            } else {
265                (global_j, global_i)
266            };
267            self.interactions
268                .entry(key)
269                .or_insert_with(W::zero)
270                .add_assign(weight.clone());
271        }
272
273        // Add fields
274        for (local_idx, field) in gadget.problem.fields().iter().enumerate() {
275            let global_idx = spin_map[local_idx];
276            self.fields[global_idx].add_assign(field.clone());
277        }
278    }
279
280    /// Build the final SpinGlass.
281    fn build(self) -> (SpinGlass<SimpleGraph, W>, HashMap<String, usize>) {
282        let mut interactions: Vec<((usize, usize), W)> = self.interactions.into_iter().collect();
283        interactions.sort_by_key(|((u, v), _)| (*u, *v));
284        let sg = SpinGlass::new(self.num_spins, interactions, self.fields);
285        (sg, self.variable_map)
286    }
287}
288
289/// Process a boolean expression and return the spin index of its output.
290fn process_expression<W>(expr: &BooleanExpr, builder: &mut SpinGlassBuilder<W>) -> usize
291where
292    W: Clone + Default + Zero + AddAssign + From<i32>,
293{
294    match &expr.op {
295        BooleanOp::Var(name) => builder.get_or_create_variable(name),
296
297        BooleanOp::Const(value) => {
298            let gadget: LogicGadget<W> = if *value { set1_gadget() } else { set0_gadget() };
299            let output_spin = builder.allocate_spin();
300            let spin_map = vec![output_spin];
301            builder.add_gadget(&gadget, &spin_map);
302            output_spin
303        }
304
305        BooleanOp::Not(inner) => {
306            let input_spin = process_expression(inner, builder);
307            let gadget: LogicGadget<W> = not_gadget();
308            let output_spin = builder.allocate_spin();
309            let spin_map = vec![input_spin, output_spin];
310            builder.add_gadget(&gadget, &spin_map);
311            output_spin
312        }
313
314        BooleanOp::And(args) => process_binary_chain(args, builder, and_gadget),
315
316        BooleanOp::Or(args) => process_binary_chain(args, builder, or_gadget),
317
318        BooleanOp::Xor(args) => process_binary_chain(args, builder, xor_gadget),
319    }
320}
321
322/// Process a multi-input gate by chaining binary gates.
323fn process_binary_chain<W, F>(
324    args: &[BooleanExpr],
325    builder: &mut SpinGlassBuilder<W>,
326    gadget_fn: F,
327) -> usize
328where
329    W: Clone + Default + Zero + AddAssign + From<i32>,
330    F: Fn() -> LogicGadget<W>,
331{
332    assert!(
333        !args.is_empty(),
334        "Binary gate must have at least one argument"
335    );
336
337    if args.len() == 1 {
338        // Single argument - just return its output
339        return process_expression(&args[0], builder);
340    }
341
342    // Process first two arguments
343    let mut result_spin = {
344        let input0 = process_expression(&args[0], builder);
345        let input1 = process_expression(&args[1], builder);
346        let gadget = gadget_fn();
347        let output_spin = builder.allocate_spin();
348
349        // For XOR gadget, we need to allocate the auxiliary spin too
350        let spin_map = if gadget.num_spins() == 4 {
351            // XOR: inputs [0, 1], aux at 3, output at 2
352            let aux_spin = builder.allocate_spin();
353            vec![input0, input1, output_spin, aux_spin]
354        } else {
355            // AND/OR: inputs [0, 1], output at 2
356            vec![input0, input1, output_spin]
357        };
358
359        builder.add_gadget(&gadget, &spin_map);
360        output_spin
361    };
362
363    // Chain remaining arguments
364    for arg in args.iter().skip(2) {
365        let next_input = process_expression(arg, builder);
366        let gadget = gadget_fn();
367        let output_spin = builder.allocate_spin();
368
369        let spin_map = if gadget.num_spins() == 4 {
370            let aux_spin = builder.allocate_spin();
371            vec![result_spin, next_input, output_spin, aux_spin]
372        } else {
373            vec![result_spin, next_input, output_spin]
374        };
375
376        builder.add_gadget(&gadget, &spin_map);
377        result_spin = output_spin;
378    }
379
380    result_spin
381}
382
383/// Process a circuit assignment.
384fn process_assignment<W>(assignment: &Assignment, builder: &mut SpinGlassBuilder<W>)
385where
386    W: Clone + Default + Zero + AddAssign + From<i32>,
387{
388    // Process the expression to get the output spin
389    let expr_output = process_expression(&assignment.expr, builder);
390
391    // For each output variable, we need to constrain it to equal the expression output
392    // This is done by adding a NOT gadget constraint (with J=1) to enforce equality
393    for output_name in &assignment.outputs {
394        let output_spin = builder.get_or_create_variable(output_name);
395
396        // If the output spin is different from expr_output, add equality constraint
397        if output_spin != expr_output {
398            // Add ferromagnetic coupling to enforce s_i = s_j
399            // J = -1 means aligned spins have lower energy
400            // Actually, we want to use a strong negative coupling
401            let key = if output_spin < expr_output {
402                (output_spin, expr_output)
403            } else {
404                (expr_output, output_spin)
405            };
406            builder
407                .interactions
408                .entry(key)
409                .or_insert_with(W::zero)
410                .add_assign(W::from(-4)); // Strong ferromagnetic coupling
411        }
412    }
413}
414
415#[reduction(
416    overhead = {
417        num_spins = "num_assignments * num_variables",
418        num_interactions = "num_assignments * num_variables",
419    }
420)]
421impl ReduceTo<SpinGlass<SimpleGraph, i32>> for CircuitSAT {
422    type Result = ReductionCircuitToSG;
423
424    fn reduce_to(&self) -> Self::Result {
425        let mut builder: SpinGlassBuilder<i32> = SpinGlassBuilder::new();
426
427        // Process each assignment in the circuit
428        for assignment in &self.circuit().assignments {
429            process_assignment(assignment, &mut builder);
430        }
431
432        let (target, variable_map) = builder.build();
433        let source_variables = self.variable_names().to_vec();
434
435        ReductionCircuitToSG {
436            target,
437            variable_map,
438            source_variables,
439        }
440    }
441}
442
443#[cfg(feature = "example-db")]
444pub(crate) fn canonical_rule_example_specs() -> Vec<crate::example_db::specs::RuleExampleSpec> {
445    use crate::export::SolutionPair;
446    use crate::models::formula::{Assignment, BooleanExpr, Circuit, CircuitSAT};
447
448    fn full_adder_circuit_sat() -> CircuitSAT {
449        let circuit = Circuit::new(vec![
450            Assignment::new(
451                vec!["t".to_string()],
452                BooleanExpr::xor(vec![BooleanExpr::var("a"), BooleanExpr::var("b")]),
453            ),
454            Assignment::new(
455                vec!["sum".to_string()],
456                BooleanExpr::xor(vec![BooleanExpr::var("t"), BooleanExpr::var("cin")]),
457            ),
458            Assignment::new(
459                vec!["ab".to_string()],
460                BooleanExpr::and(vec![BooleanExpr::var("a"), BooleanExpr::var("b")]),
461            ),
462            Assignment::new(
463                vec!["cin_t".to_string()],
464                BooleanExpr::and(vec![BooleanExpr::var("cin"), BooleanExpr::var("t")]),
465            ),
466            Assignment::new(
467                vec!["cout".to_string()],
468                BooleanExpr::or(vec![BooleanExpr::var("ab"), BooleanExpr::var("cin_t")]),
469            ),
470        ]);
471        CircuitSAT::new(circuit)
472    }
473
474    vec![crate::example_db::specs::RuleExampleSpec {
475        id: "circuitsat_to_spinglass",
476        build: || {
477            crate::example_db::specs::rule_example_with_witness::<_, SpinGlass<SimpleGraph, i32>>(
478                full_adder_circuit_sat(),
479                SolutionPair {
480                    source_config: vec![0, 0, 0, 0, 0, 0, 0, 0],
481                    target_config: vec![0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
482                },
483            )
484        },
485    }]
486}
487
488#[cfg(test)]
489#[path = "../unit_tests/rules/circuit_spinglass.rs"]
490mod tests;