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 interactions: Vec<((usize, usize), W)> = self.interactions.into_iter().collect();
283        let sg = SpinGlass::new(self.num_spins, interactions, self.fields);
284        (sg, self.variable_map)
285    }
286}
287
288/// Process a boolean expression and return the spin index of its output.
289fn process_expression<W>(expr: &BooleanExpr, builder: &mut SpinGlassBuilder<W>) -> usize
290where
291    W: Clone + Default + Zero + AddAssign + From<i32>,
292{
293    match &expr.op {
294        BooleanOp::Var(name) => builder.get_or_create_variable(name),
295
296        BooleanOp::Const(value) => {
297            let gadget: LogicGadget<W> = if *value { set1_gadget() } else { set0_gadget() };
298            let output_spin = builder.allocate_spin();
299            let spin_map = vec![output_spin];
300            builder.add_gadget(&gadget, &spin_map);
301            output_spin
302        }
303
304        BooleanOp::Not(inner) => {
305            let input_spin = process_expression(inner, builder);
306            let gadget: LogicGadget<W> = not_gadget();
307            let output_spin = builder.allocate_spin();
308            let spin_map = vec![input_spin, output_spin];
309            builder.add_gadget(&gadget, &spin_map);
310            output_spin
311        }
312
313        BooleanOp::And(args) => process_binary_chain(args, builder, and_gadget),
314
315        BooleanOp::Or(args) => process_binary_chain(args, builder, or_gadget),
316
317        BooleanOp::Xor(args) => process_binary_chain(args, builder, xor_gadget),
318    }
319}
320
321/// Process a multi-input gate by chaining binary gates.
322fn process_binary_chain<W, F>(
323    args: &[BooleanExpr],
324    builder: &mut SpinGlassBuilder<W>,
325    gadget_fn: F,
326) -> usize
327where
328    W: Clone + Default + Zero + AddAssign + From<i32>,
329    F: Fn() -> LogicGadget<W>,
330{
331    assert!(
332        !args.is_empty(),
333        "Binary gate must have at least one argument"
334    );
335
336    if args.len() == 1 {
337        // Single argument - just return its output
338        return process_expression(&args[0], builder);
339    }
340
341    // Process first two arguments
342    let mut result_spin = {
343        let input0 = process_expression(&args[0], builder);
344        let input1 = process_expression(&args[1], builder);
345        let gadget = gadget_fn();
346        let output_spin = builder.allocate_spin();
347
348        // For XOR gadget, we need to allocate the auxiliary spin too
349        let spin_map = if gadget.num_spins() == 4 {
350            // XOR: inputs [0, 1], aux at 3, output at 2
351            let aux_spin = builder.allocate_spin();
352            vec![input0, input1, output_spin, aux_spin]
353        } else {
354            // AND/OR: inputs [0, 1], output at 2
355            vec![input0, input1, output_spin]
356        };
357
358        builder.add_gadget(&gadget, &spin_map);
359        output_spin
360    };
361
362    // Chain remaining arguments
363    for arg in args.iter().skip(2) {
364        let next_input = process_expression(arg, builder);
365        let gadget = gadget_fn();
366        let output_spin = builder.allocate_spin();
367
368        let spin_map = if gadget.num_spins() == 4 {
369            let aux_spin = builder.allocate_spin();
370            vec![result_spin, next_input, output_spin, aux_spin]
371        } else {
372            vec![result_spin, next_input, output_spin]
373        };
374
375        builder.add_gadget(&gadget, &spin_map);
376        result_spin = output_spin;
377    }
378
379    result_spin
380}
381
382/// Process a circuit assignment.
383fn process_assignment<W>(assignment: &Assignment, builder: &mut SpinGlassBuilder<W>)
384where
385    W: Clone + Default + Zero + AddAssign + From<i32>,
386{
387    // Process the expression to get the output spin
388    let expr_output = process_expression(&assignment.expr, builder);
389
390    // For each output variable, we need to constrain it to equal the expression output
391    // This is done by adding a NOT gadget constraint (with J=1) to enforce equality
392    for output_name in &assignment.outputs {
393        let output_spin = builder.get_or_create_variable(output_name);
394
395        // If the output spin is different from expr_output, add equality constraint
396        if output_spin != expr_output {
397            // Add ferromagnetic coupling to enforce s_i = s_j
398            // J = -1 means aligned spins have lower energy
399            // Actually, we want to use a strong negative coupling
400            let key = if output_spin < expr_output {
401                (output_spin, expr_output)
402            } else {
403                (expr_output, output_spin)
404            };
405            builder
406                .interactions
407                .entry(key)
408                .or_insert_with(W::zero)
409                .add_assign(W::from(-4)); // Strong ferromagnetic coupling
410        }
411    }
412}
413
414#[reduction(
415    overhead = {
416        num_spins = "num_assignments",
417        num_interactions = "num_assignments",
418    }
419)]
420impl ReduceTo<SpinGlass<SimpleGraph, i32>> for CircuitSAT {
421    type Result = ReductionCircuitToSG;
422
423    fn reduce_to(&self) -> Self::Result {
424        let mut builder: SpinGlassBuilder<i32> = SpinGlassBuilder::new();
425
426        // Process each assignment in the circuit
427        for assignment in &self.circuit().assignments {
428            process_assignment(assignment, &mut builder);
429        }
430
431        let (target, variable_map) = builder.build();
432        let source_variables = self.variable_names().to_vec();
433
434        ReductionCircuitToSG {
435            target,
436            variable_map,
437            source_variables,
438        }
439    }
440}
441
442#[cfg(test)]
443#[path = "../unit_tests/rules/circuit_spinglass.rs"]
444mod tests;