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::optimization::SpinGlass;
10use crate::models::specialized::{Assignment, BooleanExpr, BooleanOp, CircuitSAT};
11use crate::rules::traits::{ReduceTo, ReductionResult};
12use crate::traits::Problem;
13use crate::types::ProblemSize;
14use num_traits::{Num, 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<W>,
30    /// Input spin indices (0-indexed within the gadget).
31    pub inputs: Vec<usize>,
32    /// Output spin indices (0-indexed within the gadget).
33    pub outputs: Vec<usize>,
34}
35
36impl<W> LogicGadget<W> {
37    /// Create a new logic gadget.
38    pub fn new(problem: SpinGlass<W>, inputs: Vec<usize>, outputs: Vec<usize>) -> Self {
39        Self {
40            problem,
41            inputs,
42            outputs,
43        }
44    }
45}
46
47impl<W: Clone + Default> LogicGadget<W> {
48    /// Get the number of spins in this gadget.
49    pub fn num_spins(&self) -> usize {
50        self.problem.num_spins()
51    }
52}
53
54/// Create an AND gate gadget.
55///
56/// 3-variable SpinGlass: inputs at indices 0, 1; output at index 2.
57/// Ground states: (0,0,0), (0,1,0), (1,0,0), (1,1,1) corresponding to
58/// all valid AND truth table entries.
59///
60/// J = [1, -2, -2] for edges (0,1), (0,2), (1,2)
61/// h = [-1, -1, 2] (negated from Julia to account for different spin convention)
62///
63/// Note: Julia uses config 0 -> spin +1, 1 -> spin -1
64///       Rust uses config 0 -> spin -1, 1 -> spin +1
65///       So h values are negated to produce equivalent ground states.
66pub fn and_gadget<W>() -> LogicGadget<W>
67where
68    W: Clone + Default + From<i32>,
69{
70    let interactions = vec![
71        ((0, 1), W::from(1)),
72        ((0, 2), W::from(-2)),
73        ((1, 2), W::from(-2)),
74    ];
75    let fields = vec![W::from(-1), W::from(-1), W::from(2)];
76    let sg = SpinGlass::new(3, interactions, fields);
77    LogicGadget::new(sg, vec![0, 1], vec![2])
78}
79
80/// Create an OR gate gadget.
81///
82/// 3-variable SpinGlass: inputs at indices 0, 1; output at index 2.
83/// Ground states: (0,0,0), (0,1,1), (1,0,1), (1,1,1) corresponding to
84/// all valid OR truth table entries.
85///
86/// J = [1, -2, -2] for edges (0,1), (0,2), (1,2)
87/// h = [1, 1, -2] (negated from Julia to account for different spin convention)
88pub fn or_gadget<W>() -> LogicGadget<W>
89where
90    W: Clone + Default + From<i32>,
91{
92    let interactions = vec![
93        ((0, 1), W::from(1)),
94        ((0, 2), W::from(-2)),
95        ((1, 2), W::from(-2)),
96    ];
97    let fields = vec![W::from(1), W::from(1), W::from(-2)];
98    let sg = SpinGlass::new(3, interactions, fields);
99    LogicGadget::new(sg, vec![0, 1], vec![2])
100}
101
102/// Create a NOT gate gadget.
103///
104/// 2-variable SpinGlass: input at index 0; output at index 1.
105/// Ground states: (0,1), (1,0) corresponding to valid NOT.
106///
107/// J = [1] for edge (0,1)
108/// h = [0, 0]
109pub fn not_gadget<W>() -> LogicGadget<W>
110where
111    W: Clone + Default + From<i32> + Zero,
112{
113    let interactions = vec![((0, 1), W::from(1))];
114    let fields = vec![W::zero(), W::zero()];
115    let sg = SpinGlass::new(2, interactions, fields);
116    LogicGadget::new(sg, vec![0], vec![1])
117}
118
119/// Create an XOR gate gadget.
120///
121/// 4-variable SpinGlass: inputs at indices 0, 1; output at 2; auxiliary at 3.
122/// Ground states correspond to valid XOR truth table entries.
123///
124/// J = [1, -1, -2, -1, -2, 2] for edges (0,1), (0,2), (0,3), (1,2), (1,3), (2,3)
125/// h = [-1, -1, 1, 2] (negated from Julia to account for different spin convention)
126pub fn xor_gadget<W>() -> LogicGadget<W>
127where
128    W: Clone + Default + From<i32>,
129{
130    let interactions = vec![
131        ((0, 1), W::from(1)),
132        ((0, 2), W::from(-1)),
133        ((0, 3), W::from(-2)),
134        ((1, 2), W::from(-1)),
135        ((1, 3), W::from(-2)),
136        ((2, 3), W::from(2)),
137    ];
138    let fields = vec![W::from(-1), W::from(-1), W::from(1), W::from(2)];
139    let sg = SpinGlass::new(4, interactions, fields);
140    // Note: output is at index 2 (not 3) according to Julia code
141    // The Julia code has: LogicGadget(sg, [1, 2], [3]) which is 1-indexed
142    // In 0-indexed: inputs [0, 1], output [2]
143    LogicGadget::new(sg, vec![0, 1], vec![2])
144}
145
146/// Create a SET0 gadget (constant false).
147///
148/// 1-variable SpinGlass that prefers config 0 (spin -1 in Rust convention).
149/// h = [1] (negated from Julia's [-1] to account for different spin convention)
150pub fn set0_gadget<W>() -> LogicGadget<W>
151where
152    W: Clone + Default + From<i32>,
153{
154    let interactions = vec![];
155    let fields = vec![W::from(1)];
156    let sg = SpinGlass::new(1, interactions, fields);
157    LogicGadget::new(sg, vec![], vec![0])
158}
159
160/// Create a SET1 gadget (constant true).
161///
162/// 1-variable SpinGlass that prefers config 1 (spin +1 in Rust convention).
163/// h = [-1] (negated from Julia's [1] to account for different spin convention)
164pub fn set1_gadget<W>() -> LogicGadget<W>
165where
166    W: Clone + Default + From<i32>,
167{
168    let interactions = vec![];
169    let fields = vec![W::from(-1)];
170    let sg = SpinGlass::new(1, interactions, fields);
171    LogicGadget::new(sg, vec![], vec![0])
172}
173
174/// Result of reducing CircuitSAT to SpinGlass.
175#[derive(Debug, Clone)]
176pub struct ReductionCircuitToSG<W> {
177    /// The target SpinGlass problem.
178    target: SpinGlass<W>,
179    /// Mapping from source variable names to spin indices.
180    variable_map: HashMap<String, usize>,
181    /// Source variable names in order.
182    source_variables: Vec<String>,
183    /// Source problem size.
184    source_size: ProblemSize,
185}
186
187impl<W> ReductionResult for ReductionCircuitToSG<W>
188where
189    W: Clone + Default + PartialOrd + Num + Zero + AddAssign + From<i32> + 'static,
190{
191    type Source = CircuitSAT<W>;
192    type Target = SpinGlass<W>;
193
194    fn target_problem(&self) -> &Self::Target {
195        &self.target
196    }
197
198    fn extract_solution(&self, target_solution: &[usize]) -> Vec<usize> {
199        self.source_variables
200            .iter()
201            .map(|var| {
202                self.variable_map
203                    .get(var)
204                    .and_then(|&idx| target_solution.get(idx).copied())
205                    .unwrap_or(0)
206            })
207            .collect()
208    }
209
210    fn source_size(&self) -> ProblemSize {
211        self.source_size.clone()
212    }
213
214    fn target_size(&self) -> ProblemSize {
215        self.target.problem_size()
216    }
217}
218
219/// Builder for constructing the combined SpinGlass from circuit gadgets.
220struct SpinGlassBuilder<W> {
221    /// Current number of spins.
222    num_spins: usize,
223    /// Accumulated interactions.
224    interactions: HashMap<(usize, usize), W>,
225    /// Accumulated fields.
226    fields: Vec<W>,
227    /// Variable name to spin index mapping.
228    variable_map: HashMap<String, usize>,
229}
230
231impl<W> SpinGlassBuilder<W>
232where
233    W: Clone + Default + Zero + AddAssign + From<i32>,
234{
235    fn new() -> Self {
236        Self {
237            num_spins: 0,
238            interactions: HashMap::new(),
239            fields: Vec::new(),
240            variable_map: HashMap::new(),
241        }
242    }
243
244    /// Allocate a new spin and return its index.
245    fn allocate_spin(&mut self) -> usize {
246        let idx = self.num_spins;
247        self.num_spins += 1;
248        self.fields.push(W::zero());
249        idx
250    }
251
252    /// Get or create a spin index for a variable.
253    fn get_or_create_variable(&mut self, name: &str) -> usize {
254        if let Some(&idx) = self.variable_map.get(name) {
255            idx
256        } else {
257            let idx = self.allocate_spin();
258            self.variable_map.insert(name.to_string(), idx);
259            idx
260        }
261    }
262
263    /// Add a gadget to the builder with the given spin mapping.
264    fn add_gadget(&mut self, gadget: &LogicGadget<W>, spin_map: &[usize]) {
265        // Add interactions
266        for ((i, j), weight) in gadget.problem.interactions() {
267            let global_i = spin_map[*i];
268            let global_j = spin_map[*j];
269            let key = if global_i < global_j {
270                (global_i, global_j)
271            } else {
272                (global_j, global_i)
273            };
274            self.interactions
275                .entry(key)
276                .or_insert_with(W::zero)
277                .add_assign(weight.clone());
278        }
279
280        // Add fields
281        for (local_idx, field) in gadget.problem.fields().iter().enumerate() {
282            let global_idx = spin_map[local_idx];
283            self.fields[global_idx].add_assign(field.clone());
284        }
285    }
286
287    /// Build the final SpinGlass.
288    fn build(self) -> (SpinGlass<W>, HashMap<String, usize>) {
289        let interactions: Vec<((usize, usize), W)> =
290            self.interactions.into_iter().collect();
291        let sg = SpinGlass::new(self.num_spins, interactions, self.fields);
292        (sg, self.variable_map)
293    }
294}
295
296/// Process a boolean expression and return the spin index of its output.
297fn process_expression<W>(
298    expr: &BooleanExpr,
299    builder: &mut SpinGlassBuilder<W>,
300) -> usize
301where
302    W: Clone + Default + Zero + AddAssign + From<i32>,
303{
304    match &expr.op {
305        BooleanOp::Var(name) => builder.get_or_create_variable(name),
306
307        BooleanOp::Const(value) => {
308            let gadget: LogicGadget<W> = if *value {
309                set1_gadget()
310            } else {
311                set0_gadget()
312            };
313            let output_spin = builder.allocate_spin();
314            let spin_map = vec![output_spin];
315            builder.add_gadget(&gadget, &spin_map);
316            output_spin
317        }
318
319        BooleanOp::Not(inner) => {
320            let input_spin = process_expression(inner, builder);
321            let gadget: LogicGadget<W> = not_gadget();
322            let output_spin = builder.allocate_spin();
323            let spin_map = vec![input_spin, output_spin];
324            builder.add_gadget(&gadget, &spin_map);
325            output_spin
326        }
327
328        BooleanOp::And(args) => process_binary_chain(args, builder, and_gadget),
329
330        BooleanOp::Or(args) => process_binary_chain(args, builder, or_gadget),
331
332        BooleanOp::Xor(args) => process_binary_chain(args, builder, xor_gadget),
333    }
334}
335
336/// Process a multi-input gate by chaining binary gates.
337fn process_binary_chain<W, F>(
338    args: &[BooleanExpr],
339    builder: &mut SpinGlassBuilder<W>,
340    gadget_fn: F,
341) -> usize
342where
343    W: Clone + Default + Zero + AddAssign + From<i32>,
344    F: Fn() -> LogicGadget<W>,
345{
346    assert!(!args.is_empty(), "Binary gate must have at least one argument");
347
348    if args.len() == 1 {
349        // Single argument - just return its output
350        return process_expression(&args[0], builder);
351    }
352
353    // Process first two arguments
354    let mut result_spin = {
355        let input0 = process_expression(&args[0], builder);
356        let input1 = process_expression(&args[1], builder);
357        let gadget = gadget_fn();
358        let output_spin = builder.allocate_spin();
359
360        // For XOR gadget, we need to allocate the auxiliary spin too
361        let spin_map = if gadget.num_spins() == 4 {
362            // XOR: inputs [0, 1], aux at 3, output at 2
363            let aux_spin = builder.allocate_spin();
364            vec![input0, input1, output_spin, aux_spin]
365        } else {
366            // AND/OR: inputs [0, 1], output at 2
367            vec![input0, input1, output_spin]
368        };
369
370        builder.add_gadget(&gadget, &spin_map);
371        output_spin
372    };
373
374    // Chain remaining arguments
375    for arg in args.iter().skip(2) {
376        let next_input = process_expression(arg, builder);
377        let gadget = gadget_fn();
378        let output_spin = builder.allocate_spin();
379
380        let spin_map = if gadget.num_spins() == 4 {
381            let aux_spin = builder.allocate_spin();
382            vec![result_spin, next_input, output_spin, aux_spin]
383        } else {
384            vec![result_spin, next_input, output_spin]
385        };
386
387        builder.add_gadget(&gadget, &spin_map);
388        result_spin = output_spin;
389    }
390
391    result_spin
392}
393
394/// Process a circuit assignment.
395fn process_assignment<W>(
396    assignment: &Assignment,
397    builder: &mut SpinGlassBuilder<W>,
398) where
399    W: Clone + Default + Zero + AddAssign + From<i32>,
400{
401    // Process the expression to get the output spin
402    let expr_output = process_expression(&assignment.expr, builder);
403
404    // For each output variable, we need to constrain it to equal the expression output
405    // This is done by adding a NOT gadget constraint (with J=1) to enforce equality
406    for output_name in &assignment.outputs {
407        let output_spin = builder.get_or_create_variable(output_name);
408
409        // If the output spin is different from expr_output, add equality constraint
410        if output_spin != expr_output {
411            // Add ferromagnetic coupling to enforce s_i = s_j
412            // J = -1 means aligned spins have lower energy
413            // Actually, we want to use a strong negative coupling
414            let key = if output_spin < expr_output {
415                (output_spin, expr_output)
416            } else {
417                (expr_output, output_spin)
418            };
419            builder
420                .interactions
421                .entry(key)
422                .or_insert_with(W::zero)
423                .add_assign(W::from(-4)); // Strong ferromagnetic coupling
424        }
425    }
426}
427
428impl<W> ReduceTo<SpinGlass<W>> for CircuitSAT<W>
429where
430    W: Clone + Default + PartialOrd + Num + Zero + AddAssign + From<i32> + 'static,
431{
432    type Result = ReductionCircuitToSG<W>;
433
434    fn reduce_to(&self) -> Self::Result {
435        let mut builder = SpinGlassBuilder::new();
436
437        // Process each assignment in the circuit
438        for assignment in &self.circuit().assignments {
439            process_assignment(assignment, &mut builder);
440        }
441
442        let (target, variable_map) = builder.build();
443        let source_variables = self.variable_names().to_vec();
444        let source_size = self.problem_size();
445
446        ReductionCircuitToSG {
447            target,
448            variable_map,
449            source_variables,
450            source_size,
451        }
452    }
453}
454
455#[cfg(test)]
456mod tests {
457    use super::*;
458    use crate::models::specialized::Circuit;
459    use crate::solvers::{BruteForce, Solver};
460
461    /// Verify a gadget has the correct ground states.
462    fn verify_gadget_truth_table<W>(gadget: &LogicGadget<W>, expected: &[(Vec<usize>, Vec<usize>)])
463    where
464        W: Clone
465            + Default
466            + PartialOrd
467            + Num
468            + Zero
469            + AddAssign
470            + From<i32>
471            + std::ops::Mul<Output = W>
472            + std::fmt::Debug
473            + 'static,
474    {
475        let solver = BruteForce::new();
476        let solutions = solver.find_best(&gadget.problem);
477
478        // For each expected input/output pair, verify there's a matching ground state
479        for (inputs, outputs) in expected {
480            let found = solutions.iter().any(|sol| {
481                let input_match = gadget
482                    .inputs
483                    .iter()
484                    .zip(inputs)
485                    .all(|(&idx, &expected)| sol[idx] == expected);
486                let output_match = gadget
487                    .outputs
488                    .iter()
489                    .zip(outputs)
490                    .all(|(&idx, &expected)| sol[idx] == expected);
491                input_match && output_match
492            });
493            assert!(
494                found,
495                "Expected ground state with inputs {:?} and outputs {:?} not found in {:?}",
496                inputs, outputs, solutions
497            );
498        }
499    }
500
501    #[test]
502    fn test_and_gadget() {
503        let gadget: LogicGadget<i32> = and_gadget();
504        assert_eq!(gadget.num_spins(), 3);
505        assert_eq!(gadget.inputs, vec![0, 1]);
506        assert_eq!(gadget.outputs, vec![2]);
507
508        // AND truth table: (a, b) -> a AND b
509        let truth_table = vec![
510            (vec![0, 0], vec![0]), // 0 AND 0 = 0
511            (vec![0, 1], vec![0]), // 0 AND 1 = 0
512            (vec![1, 0], vec![0]), // 1 AND 0 = 0
513            (vec![1, 1], vec![1]), // 1 AND 1 = 1
514        ];
515        verify_gadget_truth_table(&gadget, &truth_table);
516    }
517
518    #[test]
519    fn test_or_gadget() {
520        let gadget: LogicGadget<i32> = or_gadget();
521        assert_eq!(gadget.num_spins(), 3);
522        assert_eq!(gadget.inputs, vec![0, 1]);
523        assert_eq!(gadget.outputs, vec![2]);
524
525        // OR truth table: (a, b) -> a OR b
526        let truth_table = vec![
527            (vec![0, 0], vec![0]), // 0 OR 0 = 0
528            (vec![0, 1], vec![1]), // 0 OR 1 = 1
529            (vec![1, 0], vec![1]), // 1 OR 0 = 1
530            (vec![1, 1], vec![1]), // 1 OR 1 = 1
531        ];
532        verify_gadget_truth_table(&gadget, &truth_table);
533    }
534
535    #[test]
536    fn test_not_gadget() {
537        let gadget: LogicGadget<i32> = not_gadget();
538        assert_eq!(gadget.num_spins(), 2);
539        assert_eq!(gadget.inputs, vec![0]);
540        assert_eq!(gadget.outputs, vec![1]);
541
542        // NOT truth table: a -> NOT a
543        let truth_table = vec![
544            (vec![0], vec![1]), // NOT 0 = 1
545            (vec![1], vec![0]), // NOT 1 = 0
546        ];
547        verify_gadget_truth_table(&gadget, &truth_table);
548    }
549
550    #[test]
551    fn test_xor_gadget() {
552        let gadget: LogicGadget<i32> = xor_gadget();
553        assert_eq!(gadget.num_spins(), 4);
554        assert_eq!(gadget.inputs, vec![0, 1]);
555        assert_eq!(gadget.outputs, vec![2]);
556
557        // XOR truth table: (a, b) -> a XOR b
558        let truth_table = vec![
559            (vec![0, 0], vec![0]), // 0 XOR 0 = 0
560            (vec![0, 1], vec![1]), // 0 XOR 1 = 1
561            (vec![1, 0], vec![1]), // 1 XOR 0 = 1
562            (vec![1, 1], vec![0]), // 1 XOR 1 = 0
563        ];
564        verify_gadget_truth_table(&gadget, &truth_table);
565    }
566
567    #[test]
568    fn test_set0_gadget() {
569        let gadget: LogicGadget<i32> = set0_gadget();
570        assert_eq!(gadget.num_spins(), 1);
571        assert_eq!(gadget.inputs, Vec::<usize>::new());
572        assert_eq!(gadget.outputs, vec![0]);
573
574        let solver = BruteForce::new();
575        let solutions = solver.find_best(&gadget.problem);
576        // Ground state should be spin down (0)
577        assert!(solutions.contains(&vec![0]));
578        assert!(!solutions.contains(&vec![1]));
579    }
580
581    #[test]
582    fn test_set1_gadget() {
583        let gadget: LogicGadget<i32> = set1_gadget();
584        assert_eq!(gadget.num_spins(), 1);
585        assert_eq!(gadget.inputs, Vec::<usize>::new());
586        assert_eq!(gadget.outputs, vec![0]);
587
588        let solver = BruteForce::new();
589        let solutions = solver.find_best(&gadget.problem);
590        // Ground state should be spin up (1)
591        assert!(solutions.contains(&vec![1]));
592        assert!(!solutions.contains(&vec![0]));
593    }
594
595    #[test]
596    fn test_simple_and_circuit() {
597        // c = x AND y
598        let circuit = Circuit::new(vec![Assignment::new(
599            vec!["c".to_string()],
600            BooleanExpr::and(vec![BooleanExpr::var("x"), BooleanExpr::var("y")]),
601        )]);
602        let problem = CircuitSAT::<i32>::new(circuit);
603        let reduction = problem.reduce_to();
604        let sg = reduction.target_problem();
605
606        let solver = BruteForce::new();
607        let solutions = solver.find_best(sg);
608
609        // Extract and verify solutions
610        let extracted: Vec<Vec<usize>> = solutions
611            .iter()
612            .map(|s| reduction.extract_solution(s))
613            .collect();
614
615        // Should have valid AND configurations
616        // Variables are sorted: c, x, y
617        let valid_configs = vec![
618            vec![0, 0, 0], // c=0, x=0, y=0: 0 AND 0 = 0 OK
619            vec![0, 0, 1], // c=0, x=0, y=1: 0 AND 1 = 0 OK
620            vec![0, 1, 0], // c=0, x=1, y=0: 1 AND 0 = 0 OK
621            vec![1, 1, 1], // c=1, x=1, y=1: 1 AND 1 = 1 OK
622        ];
623
624        for config in &valid_configs {
625            assert!(
626                extracted.contains(config),
627                "Expected valid config {:?} not found in {:?}",
628                config,
629                extracted
630            );
631        }
632    }
633
634    #[test]
635    fn test_simple_or_circuit() {
636        // c = x OR y
637        let circuit = Circuit::new(vec![Assignment::new(
638            vec!["c".to_string()],
639            BooleanExpr::or(vec![BooleanExpr::var("x"), BooleanExpr::var("y")]),
640        )]);
641        let problem = CircuitSAT::<i32>::new(circuit);
642        let reduction = problem.reduce_to();
643        let sg = reduction.target_problem();
644
645        let solver = BruteForce::new();
646        let solutions = solver.find_best(sg);
647
648        let extracted: Vec<Vec<usize>> = solutions
649            .iter()
650            .map(|s| reduction.extract_solution(s))
651            .collect();
652
653        // Variables sorted: c, x, y
654        let valid_configs = vec![
655            vec![0, 0, 0], // c=0, x=0, y=0: 0 OR 0 = 0 OK
656            vec![1, 0, 1], // c=1, x=0, y=1: 0 OR 1 = 1 OK
657            vec![1, 1, 0], // c=1, x=1, y=0: 1 OR 0 = 1 OK
658            vec![1, 1, 1], // c=1, x=1, y=1: 1 OR 1 = 1 OK
659        ];
660
661        for config in &valid_configs {
662            assert!(
663                extracted.contains(config),
664                "Expected valid config {:?} not found in {:?}",
665                config,
666                extracted
667            );
668        }
669    }
670
671    #[test]
672    fn test_not_circuit() {
673        // c = NOT x
674        let circuit = Circuit::new(vec![Assignment::new(
675            vec!["c".to_string()],
676            BooleanExpr::not(BooleanExpr::var("x")),
677        )]);
678        let problem = CircuitSAT::<i32>::new(circuit);
679        let reduction = problem.reduce_to();
680        let sg = reduction.target_problem();
681
682        let solver = BruteForce::new();
683        let solutions = solver.find_best(sg);
684
685        let extracted: Vec<Vec<usize>> = solutions
686            .iter()
687            .map(|s| reduction.extract_solution(s))
688            .collect();
689
690        // Variables sorted: c, x
691        let valid_configs = vec![
692            vec![1, 0], // c=1, x=0: NOT 0 = 1 OK
693            vec![0, 1], // c=0, x=1: NOT 1 = 0 OK
694        ];
695
696        for config in &valid_configs {
697            assert!(
698                extracted.contains(config),
699                "Expected valid config {:?} not found in {:?}",
700                config,
701                extracted
702            );
703        }
704    }
705
706    #[test]
707    fn test_xor_circuit() {
708        // c = x XOR y
709        let circuit = Circuit::new(vec![Assignment::new(
710            vec!["c".to_string()],
711            BooleanExpr::xor(vec![BooleanExpr::var("x"), BooleanExpr::var("y")]),
712        )]);
713        let problem = CircuitSAT::<i32>::new(circuit);
714        let reduction = problem.reduce_to();
715        let sg = reduction.target_problem();
716
717        let solver = BruteForce::new();
718        let solutions = solver.find_best(sg);
719
720        let extracted: Vec<Vec<usize>> = solutions
721            .iter()
722            .map(|s| reduction.extract_solution(s))
723            .collect();
724
725        // Variables sorted: c, x, y
726        let valid_configs = vec![
727            vec![0, 0, 0], // c=0, x=0, y=0: 0 XOR 0 = 0 OK
728            vec![1, 0, 1], // c=1, x=0, y=1: 0 XOR 1 = 1 OK
729            vec![1, 1, 0], // c=1, x=1, y=0: 1 XOR 0 = 1 OK
730            vec![0, 1, 1], // c=0, x=1, y=1: 1 XOR 1 = 0 OK
731        ];
732
733        for config in &valid_configs {
734            assert!(
735                extracted.contains(config),
736                "Expected valid config {:?} not found in {:?}",
737                config,
738                extracted
739            );
740        }
741    }
742
743    #[test]
744    fn test_constant_true() {
745        // c = true
746        let circuit = Circuit::new(vec![Assignment::new(
747            vec!["c".to_string()],
748            BooleanExpr::constant(true),
749        )]);
750        let problem = CircuitSAT::<i32>::new(circuit);
751        let reduction = problem.reduce_to();
752        let sg = reduction.target_problem();
753
754        let solver = BruteForce::new();
755        let solutions = solver.find_best(sg);
756
757        let extracted: Vec<Vec<usize>> = solutions
758            .iter()
759            .map(|s| reduction.extract_solution(s))
760            .collect();
761
762        // c should be 1
763        assert!(extracted.contains(&vec![1]), "Expected c=1 in {:?}", extracted);
764    }
765
766    #[test]
767    fn test_constant_false() {
768        // c = false
769        let circuit = Circuit::new(vec![Assignment::new(
770            vec!["c".to_string()],
771            BooleanExpr::constant(false),
772        )]);
773        let problem = CircuitSAT::<i32>::new(circuit);
774        let reduction = problem.reduce_to();
775        let sg = reduction.target_problem();
776
777        let solver = BruteForce::new();
778        let solutions = solver.find_best(sg);
779
780        let extracted: Vec<Vec<usize>> = solutions
781            .iter()
782            .map(|s| reduction.extract_solution(s))
783            .collect();
784
785        // c should be 0
786        assert!(extracted.contains(&vec![0]), "Expected c=0 in {:?}", extracted);
787    }
788
789    #[test]
790    fn test_multi_input_and() {
791        // c = x AND y AND z (3-input AND)
792        let circuit = Circuit::new(vec![Assignment::new(
793            vec!["c".to_string()],
794            BooleanExpr::and(vec![
795                BooleanExpr::var("x"),
796                BooleanExpr::var("y"),
797                BooleanExpr::var("z"),
798            ]),
799        )]);
800        let problem = CircuitSAT::<i32>::new(circuit);
801        let reduction = problem.reduce_to();
802        let sg = reduction.target_problem();
803
804        let solver = BruteForce::new();
805        let solutions = solver.find_best(sg);
806
807        let extracted: Vec<Vec<usize>> = solutions
808            .iter()
809            .map(|s| reduction.extract_solution(s))
810            .collect();
811
812        // Variables sorted: c, x, y, z
813        // Only c=1 when all inputs are 1
814        assert!(
815            extracted.contains(&vec![1, 1, 1, 1]),
816            "Expected (1,1,1,1) in {:?}",
817            extracted
818        );
819        // c=0 for all other combinations
820        assert!(
821            extracted.contains(&vec![0, 0, 0, 0]),
822            "Expected (0,0,0,0) in {:?}",
823            extracted
824        );
825    }
826
827    #[test]
828    fn test_chained_circuit() {
829        // c = x AND y
830        // d = c OR z
831        let circuit = Circuit::new(vec![
832            Assignment::new(
833                vec!["c".to_string()],
834                BooleanExpr::and(vec![BooleanExpr::var("x"), BooleanExpr::var("y")]),
835            ),
836            Assignment::new(
837                vec!["d".to_string()],
838                BooleanExpr::or(vec![BooleanExpr::var("c"), BooleanExpr::var("z")]),
839            ),
840        ]);
841        let problem = CircuitSAT::<i32>::new(circuit);
842        let reduction = problem.reduce_to();
843        let sg = reduction.target_problem();
844
845        let solver = BruteForce::new();
846        let solutions = solver.find_best(sg);
847
848        let extracted: Vec<Vec<usize>> = solutions
849            .iter()
850            .map(|s| reduction.extract_solution(s))
851            .collect();
852
853        // Verify some valid configurations
854        // Variables sorted: c, d, x, y, z
855        // c = x AND y, d = c OR z
856
857        // x=1, y=1 -> c=1, z=0 -> d=1
858        assert!(
859            extracted.contains(&vec![1, 1, 1, 1, 0]),
860            "Expected (1,1,1,1,0) in {:?}",
861            extracted
862        );
863
864        // x=0, y=0 -> c=0, z=1 -> d=1
865        assert!(
866            extracted.contains(&vec![0, 1, 0, 0, 1]),
867            "Expected (0,1,0,0,1) in {:?}",
868            extracted
869        );
870
871        // x=0, y=0 -> c=0, z=0 -> d=0
872        assert!(
873            extracted.contains(&vec![0, 0, 0, 0, 0]),
874            "Expected (0,0,0,0,0) in {:?}",
875            extracted
876        );
877    }
878
879    #[test]
880    fn test_nested_expression() {
881        // c = (x AND y) OR z
882        let circuit = Circuit::new(vec![Assignment::new(
883            vec!["c".to_string()],
884            BooleanExpr::or(vec![
885                BooleanExpr::and(vec![BooleanExpr::var("x"), BooleanExpr::var("y")]),
886                BooleanExpr::var("z"),
887            ]),
888        )]);
889        let problem = CircuitSAT::<i32>::new(circuit);
890        let reduction = problem.reduce_to();
891        let sg = reduction.target_problem();
892
893        let solver = BruteForce::new();
894        let solutions = solver.find_best(sg);
895
896        let extracted: Vec<Vec<usize>> = solutions
897            .iter()
898            .map(|s| reduction.extract_solution(s))
899            .collect();
900
901        // Variables sorted: c, x, y, z
902        // c = (x AND y) OR z
903
904        // x=1, y=1, z=0 -> c=1
905        assert!(
906            extracted.contains(&vec![1, 1, 1, 0]),
907            "Expected (1,1,1,0) in {:?}",
908            extracted
909        );
910
911        // x=0, y=0, z=1 -> c=1
912        assert!(
913            extracted.contains(&vec![1, 0, 0, 1]),
914            "Expected (1,0,0,1) in {:?}",
915            extracted
916        );
917
918        // x=0, y=0, z=0 -> c=0
919        assert!(
920            extracted.contains(&vec![0, 0, 0, 0]),
921            "Expected (0,0,0,0) in {:?}",
922            extracted
923        );
924    }
925
926    #[test]
927    fn test_reduction_result_methods() {
928        let circuit = Circuit::new(vec![Assignment::new(
929            vec!["c".to_string()],
930            BooleanExpr::var("x"),
931        )]);
932        let problem = CircuitSAT::<i32>::new(circuit);
933        let reduction = problem.reduce_to();
934
935        // Test source_size and target_size
936        let source_size = reduction.source_size();
937        let target_size = reduction.target_size();
938
939        assert!(source_size.get("num_variables").is_some());
940        assert!(target_size.get("num_spins").is_some());
941    }
942
943    #[test]
944    fn test_empty_circuit() {
945        let circuit = Circuit::new(vec![]);
946        let problem = CircuitSAT::<i32>::new(circuit);
947        let reduction = problem.reduce_to();
948        let sg = reduction.target_problem();
949
950        // Empty circuit should result in empty SpinGlass
951        assert_eq!(sg.num_spins(), 0);
952    }
953
954    #[test]
955    fn test_solution_extraction() {
956        let circuit = Circuit::new(vec![Assignment::new(
957            vec!["c".to_string()],
958            BooleanExpr::and(vec![BooleanExpr::var("x"), BooleanExpr::var("y")]),
959        )]);
960        let problem = CircuitSAT::<i32>::new(circuit);
961        let reduction = problem.reduce_to();
962
963        // The source variables are c, x, y (sorted)
964        assert_eq!(reduction.source_variables, vec!["c", "x", "y"]);
965
966        // Test extraction with a mock target solution
967        // Need to know the mapping to construct proper test
968        let sg = reduction.target_problem();
969        assert!(sg.num_spins() >= 3); // At least c, x, y
970    }
971}
972
973// Register reduction with inventory for auto-discovery
974use crate::poly;
975use crate::rules::registry::{ReductionEntry, ReductionOverhead};
976
977inventory::submit! {
978    ReductionEntry {
979        source_name: "CircuitSAT",
980        target_name: "SpinGlass",
981        source_graph: "Circuit",
982        target_graph: "SpinGlassGraph",
983        overhead_fn: || ReductionOverhead::new(vec![
984            ("num_spins", poly!(num_assignments)),
985            ("num_interactions", poly!(num_assignments)),
986        ]),
987    }
988}