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}