Skip to main content

tensorlogic_quantrs_hooks/
quantum_circuit.rs

1//! Quantum circuit integration for probabilistic graphical models.
2//!
3//! This module provides conversion between TensorLogic expressions and quantum circuits
4//! for QAOA (Quantum Approximate Optimization Algorithm) and VQE (Variational Quantum
5//! Eigensolver) applications.
6//!
7//! # Overview
8//!
9//! The key insight is that many optimization problems can be encoded as:
10//! 1. Classical constraints → QUBO (Quadratic Unconstrained Binary Optimization)
11//! 2. QUBO → Ising Hamiltonian
12//! 3. Ising Hamiltonian → Parameterized quantum circuit
13//!
14//! # Architecture
15//!
16//! ```text
17//! TLExpr → FactorGraph → QUBO → Ising → QAOA Circuit
18//!    ↓          ↓          ↓       ↓          ↓
19//! Predicates  Factors   Matrix  Hamiltonian  Gates
20//! ```
21//!
22//! # Example
23//!
24//! ```no_run
25//! use tensorlogic_quantrs_hooks::quantum_circuit::{
26//!     QuantumCircuitBuilder, QUBOProblem, constraint_to_qubo,
27//! };
28//! use tensorlogic_ir::TLExpr;
29//!
30//! // Create a QUBO from constraints
31//! let constraints = vec![
32//!     TLExpr::pred("edge", vec![tensorlogic_ir::Term::var("x"), tensorlogic_ir::Term::var("y")]),
33//! ];
34//! let qubo = constraint_to_qubo(&constraints, 4).unwrap();
35//!
36//! // Build a QAOA circuit
37//! let builder = QuantumCircuitBuilder::new(4);
38//! let circuit = builder.build_qaoa_circuit(&qubo, 2).unwrap();
39//! ```
40
41use crate::error::{PgmError, Result};
42use crate::graph::FactorGraph;
43use quantrs2_sim::circuit_optimizer::{Circuit, Gate, GateType};
44use scirs2_core::ndarray::{Array1, Array2};
45use serde::{Deserialize, Serialize};
46use std::collections::HashMap;
47use std::f64::consts::PI;
48use tensorlogic_ir::TLExpr;
49
50/// A Quadratic Unconstrained Binary Optimization (QUBO) problem.
51///
52/// QUBO represents the optimization problem:
53/// minimize x^T Q x + c^T x
54///
55/// where x ∈ {0, 1}^n is a binary vector.
56#[derive(Debug, Clone, Serialize, Deserialize)]
57pub struct QUBOProblem {
58    /// Number of binary variables
59    pub num_variables: usize,
60    /// Variable names for reference
61    pub variable_names: Vec<String>,
62    /// Quadratic coefficients matrix Q (upper triangular)
63    pub quadratic: Array2<f64>,
64    /// Linear coefficients c
65    pub linear: Array1<f64>,
66    /// Constant offset
67    pub offset: f64,
68}
69
70impl QUBOProblem {
71    /// Create a new QUBO problem with given number of variables.
72    pub fn new(num_variables: usize) -> Self {
73        Self {
74            num_variables,
75            variable_names: (0..num_variables).map(|i| format!("x_{}", i)).collect(),
76            quadratic: Array2::zeros((num_variables, num_variables)),
77            linear: Array1::zeros(num_variables),
78            offset: 0.0,
79        }
80    }
81
82    /// Create a QUBO problem with named variables.
83    pub fn with_names(variable_names: Vec<String>) -> Self {
84        let n = variable_names.len();
85        Self {
86            num_variables: n,
87            variable_names,
88            quadratic: Array2::zeros((n, n)),
89            linear: Array1::zeros(n),
90            offset: 0.0,
91        }
92    }
93
94    /// Set a linear coefficient.
95    pub fn set_linear(&mut self, i: usize, value: f64) {
96        if i < self.num_variables {
97            self.linear[i] = value;
98        }
99    }
100
101    /// Set a quadratic coefficient (ensures upper triangular).
102    pub fn set_quadratic(&mut self, i: usize, j: usize, value: f64) {
103        if i < self.num_variables && j < self.num_variables {
104            if i <= j {
105                self.quadratic[[i, j]] = value;
106            } else {
107                self.quadratic[[j, i]] = value;
108            }
109        }
110    }
111
112    /// Add to a linear coefficient.
113    pub fn add_linear(&mut self, i: usize, value: f64) {
114        if i < self.num_variables {
115            self.linear[i] += value;
116        }
117    }
118
119    /// Add to a quadratic coefficient.
120    pub fn add_quadratic(&mut self, i: usize, j: usize, value: f64) {
121        if i < self.num_variables && j < self.num_variables {
122            if i <= j {
123                self.quadratic[[i, j]] += value;
124            } else {
125                self.quadratic[[j, i]] += value;
126            }
127        }
128    }
129
130    /// Evaluate the QUBO objective for a binary assignment.
131    pub fn evaluate(&self, assignment: &[usize]) -> f64 {
132        let mut value = self.offset;
133
134        // Linear terms
135        for (i, &xi) in assignment.iter().enumerate() {
136            if i < self.num_variables {
137                value += self.linear[i] * xi as f64;
138            }
139        }
140
141        // Quadratic terms
142        for i in 0..self.num_variables {
143            for j in i..self.num_variables {
144                let xi = if i < assignment.len() {
145                    assignment[i]
146                } else {
147                    0
148                };
149                let xj = if j < assignment.len() {
150                    assignment[j]
151                } else {
152                    0
153                };
154                value += self.quadratic[[i, j]] * (xi * xj) as f64;
155            }
156        }
157
158        value
159    }
160
161    /// Convert QUBO to Ising model (h, J).
162    ///
163    /// The Ising model is: H = Σᵢ hᵢ σᵢ + Σᵢⱼ Jᵢⱼ σᵢσⱼ
164    /// where σ ∈ {-1, +1}.
165    ///
166    /// The transformation is: x = (1 + σ) / 2
167    pub fn to_ising(&self) -> IsingModel {
168        let n = self.num_variables;
169        let mut h = Array1::zeros(n);
170        let mut j_matrix = Array2::zeros((n, n));
171        let mut offset = self.offset;
172
173        // Transform linear terms
174        for i in 0..n {
175            h[i] = self.linear[i] / 2.0;
176            offset += self.linear[i] / 2.0;
177        }
178
179        // Transform quadratic terms
180        for i in 0..n {
181            for j in i..n {
182                let qij = self.quadratic[[i, j]];
183                if i == j {
184                    // Diagonal: x² = x for binary variables
185                    h[i] += qij / 2.0;
186                    offset += qij / 2.0;
187                } else {
188                    // Off-diagonal: xᵢxⱼ → (1/4)(1 + σᵢ)(1 + σⱼ)
189                    j_matrix[[i, j]] = qij / 4.0;
190                    h[i] += qij / 4.0;
191                    h[j] += qij / 4.0;
192                    offset += qij / 4.0;
193                }
194            }
195        }
196
197        IsingModel {
198            num_spins: n,
199            h,
200            j: j_matrix,
201            offset,
202        }
203    }
204
205    /// Get the index of a variable by name.
206    pub fn variable_index(&self, name: &str) -> Option<usize> {
207        self.variable_names.iter().position(|n| n == name)
208    }
209}
210
211/// Ising model representation.
212///
213/// H = Σᵢ hᵢ σᵢ + Σᵢⱼ Jᵢⱼ σᵢσⱼ + offset
214#[derive(Debug, Clone)]
215pub struct IsingModel {
216    /// Number of spins
217    pub num_spins: usize,
218    /// Local fields
219    pub h: Array1<f64>,
220    /// Coupling strengths (upper triangular)
221    pub j: Array2<f64>,
222    /// Constant offset
223    pub offset: f64,
224}
225
226impl IsingModel {
227    /// Evaluate the Ising Hamiltonian for a spin configuration.
228    ///
229    /// Spins should be in {-1, +1}.
230    pub fn evaluate(&self, spins: &[i32]) -> f64 {
231        let mut energy = self.offset;
232
233        // Local field terms
234        for i in 0..self.num_spins {
235            if i < spins.len() {
236                energy += self.h[i] * spins[i] as f64;
237            }
238        }
239
240        // Coupling terms
241        for i in 0..self.num_spins {
242            for j in (i + 1)..self.num_spins {
243                if i < spins.len() && j < spins.len() {
244                    energy += self.j[[i, j]] * (spins[i] * spins[j]) as f64;
245                }
246            }
247        }
248
249        energy
250    }
251}
252
253/// Convert TensorLogic constraints to a QUBO problem.
254///
255/// This function interprets logical constraints as penalty terms in an optimization problem.
256///
257/// # Constraint Interpretation
258///
259/// - `Pred("edge", [x, y])`: Penalizes configurations where x and y differ
260/// - `And(a, b)`: Sum of penalties
261/// - `Not(a)`: Inverts the penalty
262///
263/// # Arguments
264///
265/// * `constraints` - List of TLExpr constraints
266/// * `num_variables` - Number of binary variables
267///
268/// # Returns
269///
270/// A QUBO problem encoding the constraints.
271pub fn constraint_to_qubo(constraints: &[TLExpr], num_variables: usize) -> Result<QUBOProblem> {
272    let mut qubo = QUBOProblem::new(num_variables);
273    let mut var_map: HashMap<String, usize> = HashMap::new();
274    let mut next_idx = 0;
275
276    // First pass: collect all variables
277    for expr in constraints {
278        collect_variables(expr, &mut var_map, &mut next_idx, num_variables)?;
279    }
280
281    // Update variable names
282    qubo.variable_names = vec![String::new(); num_variables];
283    for (name, &idx) in &var_map {
284        if idx < num_variables {
285            qubo.variable_names[idx] = name.clone();
286        }
287    }
288
289    // Second pass: add constraints as penalties
290    for expr in constraints {
291        add_constraint_penalty(expr, &var_map, &mut qubo)?;
292    }
293
294    Ok(qubo)
295}
296
297/// Collect variables from a TLExpr.
298fn collect_variables(
299    expr: &TLExpr,
300    var_map: &mut HashMap<String, usize>,
301    next_idx: &mut usize,
302    max_vars: usize,
303) -> Result<()> {
304    match expr {
305        TLExpr::Pred { args, .. } => {
306            for term in args {
307                if let tensorlogic_ir::Term::Var(v) = term {
308                    if !var_map.contains_key(v) && *next_idx < max_vars {
309                        var_map.insert(v.clone(), *next_idx);
310                        *next_idx += 1;
311                    }
312                }
313            }
314        }
315        TLExpr::And(left, right) | TLExpr::Or(left, right) | TLExpr::Imply(left, right) => {
316            collect_variables(left, var_map, next_idx, max_vars)?;
317            collect_variables(right, var_map, next_idx, max_vars)?;
318        }
319        TLExpr::Not(inner) => {
320            collect_variables(inner, var_map, next_idx, max_vars)?;
321        }
322        TLExpr::Exists { body, .. } | TLExpr::ForAll { body, .. } => {
323            collect_variables(body, var_map, next_idx, max_vars)?;
324        }
325        _ => {}
326    }
327    Ok(())
328}
329
330/// Add a constraint as a penalty term to the QUBO.
331fn add_constraint_penalty(
332    expr: &TLExpr,
333    var_map: &HashMap<String, usize>,
334    qubo: &mut QUBOProblem,
335) -> Result<()> {
336    match expr {
337        TLExpr::Pred { name, args } => {
338            // Extract variable indices from arguments
339            let mut var_indices = Vec::new();
340            for term in args {
341                if let tensorlogic_ir::Term::Var(v) = term {
342                    if let Some(&idx) = var_map.get(v) {
343                        var_indices.push(idx);
344                    }
345                }
346            }
347
348            // Add penalty based on predicate name and arity
349            match name.as_str() {
350                "edge" | "connected" | "equal" if var_indices.len() >= 2 => {
351                    // Penalize when x ≠ y: penalty = (x - y)² = x + y - 2xy
352                    let i = var_indices[0];
353                    let j = var_indices[1];
354                    qubo.add_linear(i, 1.0);
355                    qubo.add_linear(j, 1.0);
356                    qubo.add_quadratic(i, j, -2.0);
357                }
358                "conflict" | "different" | "not_equal" if var_indices.len() >= 2 => {
359                    // Penalize when x = y: penalty = xy
360                    let i = var_indices[0];
361                    let j = var_indices[1];
362                    qubo.add_quadratic(i, j, 1.0);
363                }
364                "select" | "chosen" if !var_indices.is_empty() => {
365                    // Encourage selection: penalty = -x
366                    let i = var_indices[0];
367                    qubo.add_linear(i, -1.0);
368                }
369                "exclude" | "reject" if !var_indices.is_empty() => {
370                    // Discourage selection: penalty = x
371                    let i = var_indices[0];
372                    qubo.add_linear(i, 1.0);
373                }
374                _ => {
375                    // Generic interaction penalty for any pair of variables
376                    for i in 0..var_indices.len() {
377                        for j in (i + 1)..var_indices.len() {
378                            qubo.add_quadratic(var_indices[i], var_indices[j], 0.5);
379                        }
380                    }
381                }
382            }
383        }
384        TLExpr::And(left, right) => {
385            add_constraint_penalty(left, var_map, qubo)?;
386            add_constraint_penalty(right, var_map, qubo)?;
387        }
388        TLExpr::Not(inner) => {
389            // For NOT, we would invert the penalty sign (simplified approach)
390            // In practice, this requires creating auxiliary variables
391            add_constraint_penalty(inner, var_map, qubo)?;
392        }
393        _ => {
394            // Other expressions are ignored for now
395        }
396    }
397    Ok(())
398}
399
400/// Convert a TensorLogic expression to a QAOA circuit.
401///
402/// This is a high-level function that:
403/// 1. Converts the expression to a QUBO
404/// 2. Converts QUBO to Ising model
405/// 3. Builds a QAOA circuit
406///
407/// # Arguments
408///
409/// * `expr` - The TensorLogic expression
410/// * `num_layers` - Number of QAOA layers (p parameter)
411/// * `num_qubits` - Number of qubits to use
412///
413/// # Returns
414///
415/// A quantum circuit implementing QAOA.
416pub fn tlexpr_to_qaoa_circuit(
417    expr: &TLExpr,
418    num_layers: usize,
419    num_qubits: usize,
420) -> Result<Circuit> {
421    // Convert expression to constraints
422    let constraints = vec![expr.clone()];
423    let qubo = constraint_to_qubo(&constraints, num_qubits)?;
424
425    // Build QAOA circuit
426    let builder = QuantumCircuitBuilder::new(num_qubits);
427    builder.build_qaoa_circuit(&qubo, num_layers)
428}
429
430/// Convert a factor graph to a QUBO problem.
431///
432/// Each factor becomes a penalty term in the QUBO.
433pub fn factor_graph_to_qubo(graph: &FactorGraph) -> Result<QUBOProblem> {
434    let num_vars = graph.num_variables();
435    let mut qubo = QUBOProblem::new(num_vars);
436
437    // Map variable names to indices
438    let mut var_to_idx: HashMap<String, usize> = HashMap::new();
439    for (idx, var_name) in graph.variable_names().enumerate() {
440        var_to_idx.insert(var_name.clone(), idx);
441        if idx < qubo.variable_names.len() {
442            qubo.variable_names[idx] = var_name.clone();
443        }
444    }
445
446    // Convert factors to QUBO terms
447    for factor in graph.factors() {
448        let factor_vars: Vec<usize> = factor
449            .variables
450            .iter()
451            .filter_map(|v| var_to_idx.get(v).copied())
452            .collect();
453
454        // For binary variables, convert factor values to QUBO coefficients
455        if factor_vars.len() == 1 {
456            // Unary factor: linear term
457            let i = factor_vars[0];
458            if factor.values.len() >= 2 {
459                // Penalty for value 1 vs value 0
460                let penalty = -(factor.values[[1]].ln() - factor.values[[0]].ln());
461                if penalty.is_finite() {
462                    qubo.add_linear(i, penalty);
463                }
464            }
465        } else if factor_vars.len() == 2 {
466            // Binary factor: quadratic term
467            let i = factor_vars[0];
468            let j = factor_vars[1];
469            if factor.values.len() >= 4 {
470                // Extract coupling strength from factor values
471                // J_ij ≈ log(f(0,0) * f(1,1)) - log(f(0,1) * f(1,0))
472                let f00 = factor.values[[0, 0]].max(1e-10);
473                let f01 = factor.values[[0, 1]].max(1e-10);
474                let f10 = factor.values[[1, 0]].max(1e-10);
475                let f11 = factor.values[[1, 1]].max(1e-10);
476
477                let coupling = ((f00 * f11).ln() - (f01 * f10).ln()) / 4.0;
478                if coupling.is_finite() {
479                    qubo.add_quadratic(i, j, -coupling);
480                }
481            }
482        }
483        // Higher-order factors require auxiliary variables (not implemented)
484    }
485
486    Ok(qubo)
487}
488
489/// Builder for constructing quantum circuits.
490///
491/// This builder creates parameterized circuits for variational algorithms
492/// like QAOA and VQE.
493pub struct QuantumCircuitBuilder {
494    /// Number of qubits
495    num_qubits: usize,
496    /// Default parameter values (gamma, beta for QAOA)
497    default_params: Vec<f64>,
498}
499
500impl QuantumCircuitBuilder {
501    /// Create a new circuit builder.
502    pub fn new(num_qubits: usize) -> Self {
503        Self {
504            num_qubits,
505            default_params: vec![PI / 4.0, PI / 4.0], // Default gamma, beta
506        }
507    }
508
509    /// Set default parameters.
510    pub fn with_params(mut self, params: Vec<f64>) -> Self {
511        self.default_params = params;
512        self
513    }
514
515    /// Build an initial state circuit (|+⟩^⊗n).
516    pub fn build_initial_state(&self) -> Result<Circuit> {
517        let mut circuit = Circuit::new(self.num_qubits);
518
519        // Apply Hadamard to all qubits
520        for qubit in 0..self.num_qubits {
521            let _ = circuit.add_gate(Gate::new(GateType::H, vec![qubit]));
522        }
523
524        Ok(circuit)
525    }
526
527    /// Build a QAOA circuit for a QUBO problem.
528    ///
529    /// QAOA consists of alternating layers:
530    /// 1. Problem unitary: U_C(γ) = exp(-iγC) where C is the cost function
531    /// 2. Mixer unitary: U_B(β) = exp(-iβB) where B = Σ Xᵢ
532    ///
533    /// # Arguments
534    ///
535    /// * `qubo` - The QUBO problem to optimize
536    /// * `num_layers` - Number of QAOA layers (p)
537    ///
538    /// # Returns
539    ///
540    /// A quantum circuit implementing QAOA.
541    pub fn build_qaoa_circuit(&self, qubo: &QUBOProblem, num_layers: usize) -> Result<Circuit> {
542        if qubo.num_variables > self.num_qubits {
543            return Err(PgmError::InvalidGraph(format!(
544                "QUBO has {} variables but circuit has {} qubits",
545                qubo.num_variables, self.num_qubits
546            )));
547        }
548
549        // Convert QUBO to Ising model for circuit construction
550        let ising = qubo.to_ising();
551
552        let mut circuit = Circuit::new(self.num_qubits);
553
554        // Initial state: |+⟩^⊗n
555        for qubit in 0..self.num_qubits {
556            let _ = circuit.add_gate(Gate::new(GateType::H, vec![qubit]));
557        }
558
559        // QAOA layers
560        for layer in 0..num_layers {
561            // Get parameters for this layer
562            let gamma = if layer * 2 < self.default_params.len() {
563                self.default_params[layer * 2]
564            } else {
565                PI / (4.0 * (layer + 1) as f64)
566            };
567
568            let beta = if layer * 2 + 1 < self.default_params.len() {
569                self.default_params[layer * 2 + 1]
570            } else {
571                PI / (4.0 * (layer + 1) as f64)
572            };
573
574            // Problem unitary: exp(-iγC)
575            self.add_problem_unitary(&mut circuit, &ising, gamma);
576
577            // Mixer unitary: exp(-iβB)
578            self.add_mixer_unitary(&mut circuit, beta);
579        }
580
581        Ok(circuit)
582    }
583
584    /// Add the problem unitary U_C(γ) to the circuit.
585    fn add_problem_unitary(&self, circuit: &mut Circuit, ising: &IsingModel, gamma: f64) {
586        let n = ising.num_spins.min(self.num_qubits);
587
588        // Local field terms: exp(-iγhᵢZᵢ) = Rz(2γhᵢ)
589        for i in 0..n {
590            if ising.h[i].abs() > 1e-10 {
591                let angle = 2.0 * gamma * ising.h[i];
592                let _ = circuit.add_gate(Gate::with_parameters(GateType::RZ, vec![i], vec![angle]));
593            }
594        }
595
596        // Coupling terms: exp(-iγJᵢⱼZᵢZⱼ) = Rzz(2γJᵢⱼ)
597        for i in 0..n {
598            for j in (i + 1)..n {
599                let jij = ising.j[[i, j]];
600                if jij.abs() > 1e-10 {
601                    self.add_rzz_gate(circuit, i, j, 2.0 * gamma * jij);
602                }
603            }
604        }
605    }
606
607    /// Add the mixer unitary U_B(β) to the circuit.
608    fn add_mixer_unitary(&self, circuit: &mut Circuit, beta: f64) {
609        // X-mixer: exp(-iβΣXᵢ) = ⊗ᵢ Rx(2β)
610        for qubit in 0..self.num_qubits {
611            let _ = circuit.add_gate(Gate::with_parameters(
612                GateType::RX,
613                vec![qubit],
614                vec![2.0 * beta],
615            ));
616        }
617    }
618
619    /// Add an Rzz gate (ZZ rotation) to the circuit.
620    ///
621    /// Rzz(θ) = exp(-iθZZ/2) decomposed into native gates:
622    /// CNOT(i, j) - Rz(θ)@j - CNOT(i, j)
623    fn add_rzz_gate(&self, circuit: &mut Circuit, qubit1: usize, qubit2: usize, angle: f64) {
624        let _ = circuit.add_gate(Gate::new(GateType::CNOT, vec![qubit1, qubit2]));
625        let _ = circuit.add_gate(Gate::with_parameters(
626            GateType::RZ,
627            vec![qubit2],
628            vec![angle],
629        ));
630        let _ = circuit.add_gate(Gate::new(GateType::CNOT, vec![qubit1, qubit2]));
631    }
632
633    /// Build a hardware-efficient ansatz circuit.
634    ///
635    /// This circuit uses alternating layers of single-qubit rotations
636    /// and entangling gates.
637    pub fn build_hardware_efficient_ansatz(&self, num_layers: usize) -> Result<Circuit> {
638        let mut circuit = Circuit::new(self.num_qubits);
639
640        for _layer in 0..num_layers {
641            // Single-qubit rotations: Ry-Rz
642            for qubit in 0..self.num_qubits {
643                let _ = circuit.add_gate(Gate::with_parameters(
644                    GateType::RY,
645                    vec![qubit],
646                    vec![PI / 4.0],
647                ));
648                let _ = circuit.add_gate(Gate::with_parameters(
649                    GateType::RZ,
650                    vec![qubit],
651                    vec![PI / 4.0],
652                ));
653            }
654
655            // Entangling layer: linear connectivity
656            for qubit in 0..(self.num_qubits - 1) {
657                let _ = circuit.add_gate(Gate::new(GateType::CNOT, vec![qubit, qubit + 1]));
658            }
659        }
660
661        // Final rotation layer
662        for qubit in 0..self.num_qubits {
663            let _ = circuit.add_gate(Gate::with_parameters(
664                GateType::RY,
665                vec![qubit],
666                vec![PI / 4.0],
667            ));
668        }
669
670        Ok(circuit)
671    }
672}
673
674/// QAOA result containing the optimal parameters and solution.
675#[derive(Debug, Clone)]
676pub struct QAOAResult {
677    /// Optimal gamma parameters
678    pub gamma: Vec<f64>,
679    /// Optimal beta parameters
680    pub beta: Vec<f64>,
681    /// Best solution found
682    pub best_solution: Vec<usize>,
683    /// Best objective value
684    pub best_value: f64,
685    /// Number of optimization iterations
686    pub iterations: usize,
687}
688
689/// Configuration for QAOA optimization.
690#[derive(Debug, Clone)]
691pub struct QAOAConfig {
692    /// Number of QAOA layers (p)
693    pub num_layers: usize,
694    /// Number of measurement shots
695    pub num_shots: usize,
696    /// Maximum optimization iterations
697    pub max_iterations: usize,
698    /// Convergence tolerance
699    pub tolerance: f64,
700}
701
702impl Default for QAOAConfig {
703    fn default() -> Self {
704        Self {
705            num_layers: 2,
706            num_shots: 1000,
707            max_iterations: 100,
708            tolerance: 1e-6,
709        }
710    }
711}
712
713impl QAOAConfig {
714    /// Create a new QAOA configuration.
715    pub fn new(num_layers: usize) -> Self {
716        Self {
717            num_layers,
718            ..Default::default()
719        }
720    }
721
722    /// Set the number of measurement shots.
723    pub fn with_shots(mut self, shots: usize) -> Self {
724        self.num_shots = shots;
725        self
726    }
727
728    /// Set the maximum iterations.
729    pub fn with_max_iterations(mut self, iterations: usize) -> Self {
730        self.max_iterations = iterations;
731        self
732    }
733}
734
735#[cfg(test)]
736mod tests {
737    use super::*;
738    use approx::assert_abs_diff_eq;
739
740    #[test]
741    fn test_qubo_creation() {
742        let qubo = QUBOProblem::new(3);
743        assert_eq!(qubo.num_variables, 3);
744        assert_eq!(qubo.variable_names.len(), 3);
745    }
746
747    #[test]
748    fn test_qubo_with_names() {
749        let qubo = QUBOProblem::with_names(vec!["x".to_string(), "y".to_string(), "z".to_string()]);
750        assert_eq!(qubo.num_variables, 3);
751        assert_eq!(qubo.variable_names[0], "x");
752        assert_eq!(qubo.variable_index("y"), Some(1));
753    }
754
755    #[test]
756    fn test_qubo_evaluation() {
757        let mut qubo = QUBOProblem::new(2);
758        qubo.set_linear(0, 1.0);
759        qubo.set_linear(1, 2.0);
760        qubo.set_quadratic(0, 1, 3.0);
761
762        // x=0, y=0: 0
763        assert_abs_diff_eq!(qubo.evaluate(&[0, 0]), 0.0);
764
765        // x=1, y=0: 1
766        assert_abs_diff_eq!(qubo.evaluate(&[1, 0]), 1.0);
767
768        // x=0, y=1: 2
769        assert_abs_diff_eq!(qubo.evaluate(&[0, 1]), 2.0);
770
771        // x=1, y=1: 1 + 2 + 3 = 6
772        assert_abs_diff_eq!(qubo.evaluate(&[1, 1]), 6.0);
773    }
774
775    #[test]
776    fn test_qubo_to_ising() {
777        let mut qubo = QUBOProblem::new(2);
778        qubo.set_linear(0, 1.0);
779        qubo.set_quadratic(0, 1, 2.0);
780
781        let ising = qubo.to_ising();
782        assert_eq!(ising.num_spins, 2);
783
784        // Verify Ising evaluation matches QUBO for corresponding configurations
785        // x=0 → σ=-1, x=1 → σ=+1
786        let qubo_00 = qubo.evaluate(&[0, 0]);
787        let qubo_11 = qubo.evaluate(&[1, 1]);
788
789        let ising_mm = ising.evaluate(&[-1, -1]);
790        let ising_pp = ising.evaluate(&[1, 1]);
791
792        // The values should match (up to offset)
793        assert_abs_diff_eq!(qubo_11 - qubo_00, ising_pp - ising_mm, epsilon = 1e-6);
794    }
795
796    #[test]
797    fn test_constraint_to_qubo() {
798        use tensorlogic_ir::Term;
799
800        let constraints = vec![TLExpr::pred("edge", vec![Term::var("x"), Term::var("y")])];
801
802        let qubo = constraint_to_qubo(&constraints, 4).ok();
803        assert!(qubo.is_some());
804
805        let qubo = qubo.expect("QUBO creation failed");
806        assert_eq!(qubo.num_variables, 4);
807    }
808
809    #[test]
810    fn test_circuit_builder_initial_state() {
811        let builder = QuantumCircuitBuilder::new(3);
812        let circuit = builder.build_initial_state();
813
814        assert!(circuit.is_ok());
815        let circuit = circuit.expect("Circuit creation failed");
816        assert_eq!(circuit.n_qubits, 3);
817    }
818
819    #[test]
820    fn test_qaoa_circuit_creation() {
821        let mut qubo = QUBOProblem::new(2);
822        qubo.set_quadratic(0, 1, 1.0);
823
824        let builder = QuantumCircuitBuilder::new(2);
825        let circuit = builder.build_qaoa_circuit(&qubo, 1);
826
827        assert!(circuit.is_ok());
828        let circuit = circuit.expect("Circuit creation failed");
829        assert_eq!(circuit.n_qubits, 2);
830    }
831
832    #[test]
833    fn test_hardware_efficient_ansatz() {
834        let builder = QuantumCircuitBuilder::new(3);
835        let circuit = builder.build_hardware_efficient_ansatz(2);
836
837        assert!(circuit.is_ok());
838        let circuit = circuit.expect("Circuit creation failed");
839        assert_eq!(circuit.n_qubits, 3);
840    }
841
842    #[test]
843    fn test_qaoa_config() {
844        let config = QAOAConfig::new(3).with_shots(2000).with_max_iterations(50);
845
846        assert_eq!(config.num_layers, 3);
847        assert_eq!(config.num_shots, 2000);
848        assert_eq!(config.max_iterations, 50);
849    }
850
851    #[test]
852    fn test_tlexpr_to_qaoa() {
853        use tensorlogic_ir::Term;
854
855        let expr = TLExpr::pred("conflict", vec![Term::var("a"), Term::var("b")]);
856
857        let circuit = tlexpr_to_qaoa_circuit(&expr, 1, 4);
858        assert!(circuit.is_ok());
859    }
860}