1use 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#[derive(Debug, Clone)]
32pub struct LogicGadget<W> {
33 pub problem: SpinGlass<SimpleGraph, W>,
35 #[allow(dead_code)] pub inputs: Vec<usize>,
38 #[allow(dead_code)] pub outputs: Vec<usize>,
41}
42
43impl<W> LogicGadget<W> {
44 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 pub fn num_spins(&self) -> usize {
61 self.problem.num_spins()
62 }
63}
64
65pub 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
95pub 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
121pub 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
142pub 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 LogicGadget::new(
167 sg.expect("static XOR gadget must be valid"),
168 vec![0, 1],
169 vec![2],
170 )
171}
172
173pub 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
191pub 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#[derive(Debug, Clone)]
211pub struct ReductionCircuitToSG {
212 target: Decision<SpinGlass<SimpleGraph, i64>>,
214 variable_map: HashMap<String, usize>,
216 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
250struct SpinGlassBuilder {
252 num_spins: usize,
254 interactions: HashMap<(usize, usize), i64>,
256 fields: Vec<i64>,
258 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 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 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 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 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 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 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
349fn 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
382fn 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 return process_expression(&BooleanExpr::constant(empty_value), builder);
396 }
397
398 if args.len() == 1 {
399 return process_expression(&args[0], builder);
401 }
402
403 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 let spin_map = if gadget.num_spins() == 4 {
412 let aux_spin = builder.allocate_spin()?;
414 vec![input0, input1, output_spin, aux_spin]
415 } else {
416 vec![input0, input1, output_spin]
418 };
419
420 builder.add_gadget(&gadget, &spin_map, ground_energy)?;
421 output_spin
422 };
423
424 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
444fn process_assignment(
446 assignment: &Assignment,
447 builder: &mut SpinGlassBuilder,
448) -> Result<(), crate::registry::ConstructionError> {
449 let expr_output = process_expression(&assignment.expr, builder)?;
451
452 for output_name in &assignment.outputs {
455 let output_spin = builder.get_or_create_variable(output_name)?;
456
457 if output_spin != expr_output {
459 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 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;