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