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