Skip to main content

cqlib_core/circuit/ansatz/
two_local.rs

1// This code is part of Cqlib.
2//
3// (C) Copyright China Telecom Quantum Group 2026
4//
5// This code is licensed under the Apache License, Version 2.0. You may
6// obtain a copy of this license in the LICENSE.txt file in the root directory
7// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
8//
9// Any modifications or derivative works of this code must retain this
10// copyright notice, and modified files need to carry a notice indicating
11// that they have been altered from the originals.
12
13//! Hardware-efficient ansatze with alternating rotation and entanglement layers.
14//!
15//! This module provides the [`TwoLocal`] ansatz and related components. TwoLocal
16//! is one of the most widely used ansatz architectures in variational quantum
17//! algorithms due to its flexibility and hardware efficiency.
18//!
19//! # TwoLocal Architecture
20//!
21//! The TwoLocal ansatz consists of alternating layers:
22//!
23//! 1. **Rotation Layers**: Single-qubit parameterized gates (e.g., `RY`, `RZ`, `RX`).
24//! 2. **Entanglement Layers**: Two-qubit gates creating correlations between qubits
25//!    (e.g., `CX`, `CZ`).
26//!
27//! The pattern is: `[Rotation] → [Entanglement] → [Rotation] → [Entanglement] → ...`
28//!
29//! # Entanglement Topologies
30//!
31//! Different problem structures benefit from different entanglement patterns:
32//!
33//! - [`Linear`][`EntanglementTopology::Linear`]: Nearest-neighbor interactions.
34//!   Best for near-term devices with limited connectivity.
35//!
36//! - [`Circular`][`EntanglementTopology::Circular`]: Linear + wrap-around edge.
37//!   Adds periodic boundary conditions.
38//!
39//! - [`Full`][`EntanglementTopology::Full`]: All-to-all connectivity.
40//!   Most expressive but requires many gates.
41//!
42//! - [`Custom`][`EntanglementTopology::Custom`]: User-defined qubit pairs.
43//!   Allows problem-specific tailoring.
44//!
45//! # Example
46//!
47//! ```
48//! use cqlib_core::circuit::ansatz::{Ansatz, TwoLocal, EntanglementTopology};
49//! use cqlib_core::circuit::gate::StandardGate;
50//!
51//! // Create an EfficientSU2-style ansatz
52//! let ansatz = TwoLocal::new(4)
53//!     .reps(3)
54//!     .rotation_gates(vec![StandardGate::RY, StandardGate::RZ])
55//!     .entanglement(EntanglementTopology::Circular)
56//!     .entanglement_gate(StandardGate::CX);
57//!
58//! let circuit = ansatz.build_circuit("theta").unwrap();
59//! ```
60
61use super::traits::Ansatz;
62use crate::circuit::Parameter;
63use crate::circuit::ParameterValue;
64use crate::circuit::circuit_impl::Circuit;
65use crate::circuit::error::CircuitError;
66use crate::circuit::gate::StandardGate;
67use crate::circuit::{Instruction, Qubit};
68
69/// Returns all k-combinations of `{0, 1, ..., n-1}` in lexicographic order.
70fn k_combinations(n: usize, k: usize) -> Vec<Vec<usize>> {
71    let mut result = Vec::new();
72    let mut combo = Vec::with_capacity(k);
73    k_combinations_helper(0, n, k, &mut combo, &mut result);
74    result
75}
76
77fn k_combinations_helper(
78    start: usize,
79    n: usize,
80    remaining: usize,
81    combo: &mut Vec<usize>,
82    result: &mut Vec<Vec<usize>>,
83) {
84    if remaining == 0 {
85        result.push(combo.clone());
86        return;
87    }
88    for i in start..=(n - remaining) {
89        combo.push(i);
90        k_combinations_helper(i + 1, n, remaining - 1, combo, result);
91        combo.pop();
92    }
93}
94
95/// Defines the topology of the entanglement layer in a TwoLocal ansatz.
96///
97/// The entanglement topology determines which pairs of qubits are connected
98/// by two-qubit gates in each entanglement layer. This choice significantly
99/// affects the expressibility of the ansatz and the hardware resources required.
100#[derive(Debug, Clone, PartialEq, Eq)]
101pub enum EntanglementTopology {
102    /// Linear nearest-neighbor entanglement.
103    ///
104    /// Creates entanglement between adjacent qubits: (0,1), (1,2), ..., (n-2, n-1).
105    /// This topology is hardware-efficient for devices with linear connectivity.
106    Linear,
107    /// Circular nearest-neighbor entanglement.
108    ///
109    /// Like [`Linear`](Self::Linear) but adds a wrap-around edge (n-1, 0).
110    /// Creates a ring topology suitable for periodic boundary conditions.
111    /// For 2 qubits, this is identical to Linear.
112    Circular,
113    /// Full all-to-all entanglement.
114    ///
115    /// Creates entanglement between every pair of qubits.
116    /// Most expressive but requires O(n²) gates per layer.
117    Full,
118    /// Custom explicit entanglement pairs.
119    ///
120    /// Allows users to specify exactly which qubit pairs should be entangled.
121    /// Useful for problem-specific ansatz design or hardware-aware compilation.
122    Custom(Vec<(usize, usize)>),
123}
124
125impl EntanglementTopology {
126    /// Generates all k-tuples of qubit indices based on the entanglement topology.
127    ///
128    /// This generalizes [`generate_pairs`](Self::generate_pairs) (k=2) to arbitrary
129    /// locality k. It is used by [`PauliFeatureMap`][super::feature_map::PauliFeatureMap]
130    /// to apply k-local Pauli evolution gates.
131    ///
132    /// # Behavior per topology
133    ///
134    /// | Topology | k=1 | k=2 | k≥3 |
135    /// |----------|-----|-----|-----|
136    /// | Linear | each qubit | (0,1),(1,2),… | consecutive windows of width k |
137    /// | Circular | each qubit | linear + wrap | all rotational windows mod n |
138    /// | Full | each qubit | all C(n,2) | all C(n,k) |
139    /// | Custom | each qubit | custom pairs | falls back to Full (C(n,k)) |
140    ///
141    /// Returns an empty vec if `k == 0`, `num_qubits == 0`, or `k > num_qubits`.
142    ///
143    /// # Examples
144    ///
145    /// ```
146    /// use cqlib_core::circuit::ansatz::EntanglementTopology;
147    ///
148    /// let full = EntanglementTopology::Full;
149    /// let triples = full.generate_k_tuples(3, 4);
150    /// assert_eq!(triples, vec![
151    ///     vec![0, 1, 2],
152    ///     vec![0, 1, 3],
153    ///     vec![0, 2, 3],
154    ///     vec![1, 2, 3],
155    /// ]);
156    ///
157    /// let linear = EntanglementTopology::Linear;
158    /// let triples = linear.generate_k_tuples(3, 4);
159    /// assert_eq!(triples, vec![vec![0, 1, 2], vec![1, 2, 3]]);
160    /// ```
161    pub fn generate_k_tuples(&self, k: usize, num_qubits: usize) -> Vec<Vec<usize>> {
162        if k == 0 || num_qubits == 0 || k > num_qubits {
163            return vec![];
164        }
165        if k == 1 {
166            return (0..num_qubits).map(|i| vec![i]).collect();
167        }
168        if k == 2 {
169            return self
170                .generate_pairs(num_qubits)
171                .into_iter()
172                .map(|(a, b)| vec![a, b])
173                .collect();
174        }
175        // k >= 3
176        match self {
177            EntanglementTopology::Linear => {
178                (0..=num_qubits - k).map(|i| (i..i + k).collect()).collect()
179            }
180            // For Circular k≥3, generates n rotational windows: [i, i+1, ..., i+k-1] (mod n).
181            // Note: when k == n, all n windows contain the same set of qubits (just rotated),
182            // so they produce the same Pauli string and each applies an identical evolution gate.
183            // This rotational symmetry is intentional for periodic-boundary feature maps.
184            EntanglementTopology::Circular => (0..num_qubits)
185                .map(|i| (0..k).map(|j| (i + j) % num_qubits).collect())
186                .collect(),
187            // Custom topology only defines pairs; fall back to Full for k > 2.
188            EntanglementTopology::Full | EntanglementTopology::Custom(_) => {
189                k_combinations(num_qubits, k)
190            }
191        }
192    }
193
194    /// Generates a list of qubit pairs based on the specified topology.
195    ///
196    /// # Arguments
197    ///
198    /// * `num_qubits` - The total number of qubits in the circuit.
199    ///
200    /// # Returns
201    ///
202    /// A vector of tuples `(control, target)` representing qubit pairs to entangle.
203    ///
204    /// # Examples
205    ///
206    /// ```
207    /// use cqlib_core::circuit::ansatz::EntanglementTopology;
208    ///
209    /// let topology = EntanglementTopology::Linear;
210    /// let pairs = topology.generate_pairs(4);
211    /// assert_eq!(pairs, vec![(0, 1), (1, 2), (2, 3)]);
212    ///
213    /// let circular = EntanglementTopology::Circular;
214    /// let pairs = circular.generate_pairs(4);
215    /// assert_eq!(pairs, vec![(0, 1), (1, 2), (2, 3), (3, 0)]);
216    /// ```
217    pub fn generate_pairs(&self, num_qubits: usize) -> Vec<(usize, usize)> {
218        match self {
219            EntanglementTopology::Linear => (0..num_qubits.saturating_sub(1))
220                .map(|i| (i, i + 1))
221                .collect(),
222            EntanglementTopology::Circular => {
223                let mut p: Vec<(usize, usize)> = (0..num_qubits.saturating_sub(1))
224                    .map(|i| (i, i + 1))
225                    .collect();
226                if num_qubits > 2 {
227                    p.push((num_qubits - 1, 0));
228                }
229                p
230            }
231            EntanglementTopology::Full => {
232                let mut p = Vec::new();
233                for i in 0..num_qubits {
234                    for j in (i + 1)..num_qubits {
235                        p.push((i, j));
236                    }
237                }
238                p
239            }
240            EntanglementTopology::Custom(pairs) => pairs.clone(),
241        }
242    }
243}
244
245/// The TwoLocal ansatz, a versatile hardware-efficient parameterized circuit.
246///
247/// TwoLocal is a popular ansatz architecture consisting of alternating layers
248/// of single-qubit rotations and two-qubit entanglement gates. It serves as
249/// the foundation for many common ansatz patterns including RealAmplitudes
250/// and EfficientSU2.
251///
252/// # Architecture
253///
254/// The circuit structure follows this pattern:
255///
256/// ```text
257/// Layer 0:    [R]───────[R]───────[R]───────[R]    (Rotation)
258///               │       │       │       │
259/// Layer 0.5:   └─[E]───┘       └─[E]───┘        (Entanglement)
260///
261/// Layer 1:    [R]───────[R]───────[R]───────[R]
262///               │       │       │       │
263/// Layer 1.5:   └─[E]───┘       └─[E]───┘
264/// ...
265/// ```
266///
267/// Where `[R]` represents parameterized rotation gates and `[E]` represents
268/// entanglement gates.
269///
270/// # Configuration
271///
272/// Use the builder pattern to configure the ansatz:
273///
274/// - [`reps`](Self::reps): Number of repetition layers
275/// - [`rotation_gates`](Self::rotation_gates): Single-qubit gates (e.g., `[RY]`, `[RY, RZ]`)
276/// - [`entanglement_gate`](Self::entanglement_gate): Two-qubit gate (e.g., `CX`, `CZ`)
277/// - [`entanglement`](Self::entanglement): Connectivity pattern
278/// - [`skip_final_rotation_layer`](Self::skip_final_rotation_layer): Omit final rotation
279///
280/// # Parameter Count
281///
282/// The total number of parameters is:
283/// `layers × num_qubits × rotation_gates.len()`
284///
285/// where `layers = reps` if `skip_final_rotation_layer` is `true`,
286/// otherwise `layers = reps + 1`.
287#[derive(Debug, Clone)]
288pub struct TwoLocal {
289    num_qubits: usize,
290    reps: usize,
291    rotation_gates: Vec<StandardGate>,
292    entanglement_gate: StandardGate,
293    entanglement: EntanglementTopology,
294    skip_final_rotation_layer: bool,
295}
296
297impl TwoLocal {
298    /// Creates a new TwoLocal ansatz with sensible defaults.
299    ///
300    /// # Default Configuration
301    ///
302    /// - `reps`: 3 layers
303    /// - `rotation_gates`: `[RY]`
304    /// - `entanglement_gate`: `CX`
305    /// - `entanglement`: [`Linear`](EntanglementTopology::Linear)
306    /// - `skip_final_rotation_layer`: false
307    ///
308    /// # Arguments
309    ///
310    /// * `num_qubits` - The number of qubits in the ansatz.
311    ///
312    /// # Example
313    ///
314    /// ```
315    /// use cqlib_core::circuit::ansatz::{Ansatz, TwoLocal};
316    ///
317    /// let ansatz = TwoLocal::new(5);
318    /// assert_eq!(ansatz.num_qubits(), 5);
319    /// ```
320    pub fn new(num_qubits: usize) -> Self {
321        Self {
322            num_qubits,
323            reps: 3,
324            rotation_gates: vec![StandardGate::RY],
325            entanglement_gate: StandardGate::CX,
326            entanglement: EntanglementTopology::Linear,
327            skip_final_rotation_layer: false,
328        }
329    }
330
331    /// Sets the number of repetition layers.
332    ///
333    /// Each repetition consists of one rotation layer followed by one
334    /// entanglement layer. The default is 3.
335    ///
336    /// # Arguments
337    ///
338    /// * `reps` - The number of repetitions (must be >= 0).
339    pub fn reps(mut self, reps: usize) -> Self {
340        self.reps = reps;
341        self
342    }
343
344    /// Sets the rotation gates used in the rotation layers.
345    ///
346    /// Each qubit receives all specified rotation gates in sequence.
347    /// Valid choices are single-parameter single-qubit rotation gates:
348    /// - `[RX]`, `[RY]`, `[RZ]` individually
349    /// - `[RY, RZ]` for EfficientSU2 ansatz
350    /// - `[RX, RY, RZ]` for full SU(2) coverage
351    ///
352    /// # Arguments
353    ///
354    /// * `gates` - A vector of single-qubit parameterized gates (RX, RY, or RZ).
355    ///
356    /// # Errors
357    ///
358    /// Validation is performed in [`build_circuit`](Ansatz::build_circuit).
359    /// Returns `CircuitError::InvalidOperation` if any gate is not RX, RY, or RZ.
360    pub fn rotation_gates(mut self, gates: Vec<StandardGate>) -> Self {
361        self.rotation_gates = gates;
362        self
363    }
364
365    /// Sets the entanglement gate used in the entanglement layers.
366    ///
367    /// Valid choices are two-qubit controlled gates that create entanglement:
368    /// - `CX` (CNOT): Standard choice, universal for entanglement (default)
369    /// - `CY`: Controlled-Y gate
370    /// - `CZ`: Symmetric, native on some hardware
371    ///
372    /// # Arguments
373    ///
374    /// * `gate` - A two-qubit controlled entanglement gate (CX, CY, or CZ).
375    ///
376    /// # Errors
377    ///
378    /// Validation is performed in [`build_circuit`](Ansatz::build_circuit).
379    /// Returns `CircuitError::InvalidOperation` if the gate is not CX, CY, or CZ.
380    pub fn entanglement_gate(mut self, gate: StandardGate) -> Self {
381        self.entanglement_gate = gate;
382        self
383    }
384
385    /// Sets the entanglement topology.
386    ///
387    /// See [`EntanglementTopology`] for available options.
388    ///
389    /// # Arguments
390    ///
391    /// * `topology` - The desired connectivity pattern.
392    pub fn entanglement(mut self, topology: EntanglementTopology) -> Self {
393        self.entanglement = topology;
394        self
395    }
396
397    /// Sets whether to skip the final rotation layer.
398    ///
399    /// The default TwoLocal architecture adds a final rotation layer after
400    /// the last entanglement layer (total `reps + 1` rotation layers).
401    /// When enabled, this final rotation layer is omitted, meaning the
402    /// ansatz ends with an entanglement layer and has exactly `reps`
403    /// rotation layers.
404    ///
405    /// # Arguments
406    ///
407    /// * `skip` - If `true`, omit the final rotation layer.
408    pub fn skip_final_rotation_layer(mut self, skip: bool) -> Self {
409        self.skip_final_rotation_layer = skip;
410        self
411    }
412}
413
414impl Ansatz for TwoLocal {
415    fn validate(&self) -> Result<(), CircuitError> {
416        if self.num_qubits == 0 {
417            return Err(CircuitError::InvalidOperation(
418                "TwoLocal requires at least 1 qubit".to_string(),
419            ));
420        }
421
422        // Validate rotation gates: must be RX, RY, or RZ
423        for gate in &self.rotation_gates {
424            if !matches!(gate, StandardGate::RX | StandardGate::RY | StandardGate::RZ) {
425                return Err(CircuitError::InvalidOperation(format!(
426                    "Rotation gates must be RX, RY, or RZ, got {:?}",
427                    gate
428                )));
429            }
430        }
431
432        // Validate entanglement gate: must be CX, CY, or CZ
433        if !matches!(
434            self.entanglement_gate,
435            StandardGate::CX | StandardGate::CY | StandardGate::CZ
436        ) {
437            return Err(CircuitError::InvalidOperation(format!(
438                "Entanglement gate must be CX, CY, or CZ, got {:?}",
439                self.entanglement_gate
440            )));
441        }
442
443        // Validate Custom topology pairs
444        if let EntanglementTopology::Custom(pairs) = &self.entanglement {
445            use std::collections::HashSet;
446            let mut seen = HashSet::new();
447            for (c, t) in pairs {
448                // Check for self-loop
449                if c == t {
450                    return Err(CircuitError::InvalidOperation(format!(
451                        "EntanglementTopology::Custom contains self-loop ({}, {})",
452                        c, t
453                    )));
454                }
455                // Check index bounds
456                if *c >= self.num_qubits || *t >= self.num_qubits {
457                    return Err(CircuitError::InvalidOperation(format!(
458                        "EntanglementTopology::Custom contains out-of-bounds index: ({}, {}) for {} qubits",
459                        c, t, self.num_qubits
460                    )));
461                }
462                // Check for duplicate edges (undirected: (c,t) == (t,c))
463                let edge = if c < t { (*c, *t) } else { (*t, *c) };
464                if !seen.insert(edge) {
465                    return Err(CircuitError::InvalidOperation(format!(
466                        "EntanglementTopology::Custom contains duplicate edge ({}, {})",
467                        c, t
468                    )));
469                }
470            }
471        }
472
473        Ok(())
474    }
475
476    fn build_circuit(&self, prefix: &str) -> Result<Circuit, CircuitError> {
477        // Validate configuration first
478        self.validate()?;
479
480        let mut circuit = Circuit::new(self.num_qubits);
481        let mut param_idx = 0;
482
483        let num_layers = if self.skip_final_rotation_layer {
484            self.reps
485        } else {
486            self.reps + 1
487        };
488
489        for layer in 0..num_layers {
490            // Rotation Layer
491            for q in 0..self.num_qubits {
492                for gate in &self.rotation_gates {
493                    let param_name = format!("{}_{}", prefix, param_idx);
494                    let param = Parameter::try_from(param_name.as_str())
495                        .map_err(|_| CircuitError::InvalidParameterValue(param_idx, f64::NAN))?;
496
497                    circuit.append(
498                        Instruction::Standard(*gate),
499                        vec![Qubit::new(q as u32)],
500                        vec![ParameterValue::Param(param)],
501                        None,
502                    )?;
503                    param_idx += 1;
504                }
505            }
506
507            // Entanglement Layer
508            if layer < self.reps {
509                let pairs = self.entanglement.generate_pairs(self.num_qubits);
510
511                for (c, t) in pairs {
512                    circuit.append(
513                        Instruction::Standard(self.entanglement_gate),
514                        vec![Qubit::new(c as u32), Qubit::new(t as u32)],
515                        vec![],
516                        None,
517                    )?;
518                }
519            }
520        }
521
522        Ok(circuit)
523    }
524
525    fn num_parameters(&self) -> usize {
526        let layers = if self.skip_final_rotation_layer {
527            self.reps
528        } else {
529            self.reps + 1
530        };
531        layers * self.num_qubits * self.rotation_gates.len()
532    }
533
534    fn num_qubits(&self) -> usize {
535        self.num_qubits
536    }
537}