Skip to main content

ruqu_core/
transpiler.rs

1//! Noise-aware transpiler for quantum circuits.
2//!
3//! Decomposes arbitrary gates into hardware-native basis gate sets, routes
4//! two-qubit gates onto constrained coupling topologies via SWAP insertion,
5//! and applies peephole gate-cancellation optimizations.
6
7use std::collections::VecDeque;
8
9use crate::circuit::QuantumCircuit;
10use crate::gate::Gate;
11
12use std::f64::consts::{FRAC_PI_2, FRAC_PI_4, PI};
13
14// ---------------------------------------------------------------------------
15// Configuration types
16// ---------------------------------------------------------------------------
17
18/// Hardware-native basis gate sets supported by the transpiler.
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20pub enum BasisGateSet {
21    /// IBM Eagle: CX, ID, RZ, SX (= Rx(pi/2)), X
22    IbmEagle,
23    /// IonQ Aria: GPI, GPI2, MS -- mapped to Rx, Ry, Rzz
24    IonQAria,
25    /// Rigetti Aspen: CZ, RX, RZ
26    RigettiAspen,
27    /// Universal: any gate passes through without decomposition.
28    Universal,
29}
30
31/// Transpiler configuration.
32#[derive(Debug, Clone)]
33pub struct TranspilerConfig {
34    /// Target basis gate set.
35    pub basis: BasisGateSet,
36    /// Optional coupling map describing which qubit pairs support two-qubit
37    /// gates.  Edges are undirected -- `(a, b)` implies `(b, a)`.
38    pub coupling_map: Option<Vec<(u32, u32)>>,
39    /// Optimization level: 0 = none, 1 = inverse-pair cancellation,
40    /// 2 = also merge adjacent Rz rotations.
41    pub optimization_level: u8,
42}
43
44// ---------------------------------------------------------------------------
45// Top-level entry point
46// ---------------------------------------------------------------------------
47
48/// Transpile a circuit through the full pipeline:
49/// decompose -> route -> optimize.
50pub fn transpile(circuit: &QuantumCircuit, config: &TranspilerConfig) -> QuantumCircuit {
51    // Step 1: decompose to basis gate set
52    let decomposed = decompose(circuit, config.basis);
53
54    // Step 2: route onto coupling map (if provided)
55    let routed = match &config.coupling_map {
56        Some(map) => route_circuit(&decomposed, map),
57        None => decomposed,
58    };
59
60    // Step 3: optimize
61    optimize_gates(&routed, config.optimization_level)
62}
63
64// ---------------------------------------------------------------------------
65// Decomposition dispatcher
66// ---------------------------------------------------------------------------
67
68fn decompose(circuit: &QuantumCircuit, basis: BasisGateSet) -> QuantumCircuit {
69    if basis == BasisGateSet::Universal {
70        return circuit.clone();
71    }
72    let mut result = QuantumCircuit::new(circuit.num_qubits());
73    for gate in circuit.gates() {
74        let decomposed = match basis {
75            BasisGateSet::IbmEagle => decompose_to_ibm(gate),
76            BasisGateSet::IonQAria => decompose_to_ionq(gate),
77            BasisGateSet::RigettiAspen => decompose_to_rigetti(gate),
78            BasisGateSet::Universal => unreachable!(),
79        };
80        for g in decomposed {
81            result.add_gate(g);
82        }
83    }
84    result
85}
86
87// ---------------------------------------------------------------------------
88// IBM Eagle decomposition: basis = {CNOT, Rz, SX (Rx(pi/2)), X}
89// ---------------------------------------------------------------------------
90//
91// SX = Rx(pi/2).  The IBM ID gate is a no-op and never needs to be emitted.
92
93/// Decompose a single gate into the IBM Eagle basis {CNOT, Rz, Rx(pi/2), X}.
94///
95/// The SX gate is represented as `Rx(q, PI/2)`.
96pub fn decompose_to_ibm(gate: &Gate) -> Vec<Gate> {
97    match gate {
98        // --- already in basis ---
99        Gate::CNOT(c, t) => vec![Gate::CNOT(*c, *t)],
100        Gate::X(q) => vec![Gate::X(*q)],
101        Gate::Rz(q, theta) => vec![Gate::Rz(*q, *theta)],
102
103        // --- single-qubit Cliffords ---
104        // H = Rz(pi) SX Rz(pi)
105        Gate::H(q) => vec![
106            Gate::Rz(*q, PI),
107            Gate::Rx(*q, FRAC_PI_2), // SX
108            Gate::Rz(*q, PI),
109        ],
110
111        // S = Rz(pi/2)
112        Gate::S(q) => vec![Gate::Rz(*q, FRAC_PI_2)],
113
114        // Sdg = Rz(-pi/2)
115        Gate::Sdg(q) => vec![Gate::Rz(*q, -FRAC_PI_2)],
116
117        // T = Rz(pi/4)
118        Gate::T(q) => vec![Gate::Rz(*q, FRAC_PI_4)],
119
120        // Tdg = Rz(-pi/4)
121        Gate::Tdg(q) => vec![Gate::Rz(*q, -FRAC_PI_4)],
122
123        // Y = X Rz(pi)  (global phase ignored)
124        Gate::Y(q) => vec![Gate::X(*q), Gate::Rz(*q, PI)],
125
126        // Z = Rz(pi)
127        Gate::Z(q) => vec![Gate::Rz(*q, PI)],
128
129        // Phase(theta) = Rz(theta)  (differs by global phase only)
130        Gate::Phase(q, theta) => vec![Gate::Rz(*q, *theta)],
131
132        // Rx(theta): Rz(-pi/2) SX Rz(pi - theta) SX Rz(-pi/2)
133        // Simplified: for arbitrary Rx we use Rz(-pi/2) SX Rz(pi) Rz(-theta) SX Rz(-pi/2)
134        // But a simpler exact decomposition is:
135        //   Rx(theta) = Rz(-pi/2) Rx(pi/2) Rz(theta) Rx(pi/2) Rz(-pi/2)
136        // keeping only basis gates
137        Gate::Rx(q, theta) => {
138            if (*theta - FRAC_PI_2).abs() < 1e-12 {
139                // Already SX
140                vec![Gate::Rx(*q, FRAC_PI_2)]
141            } else {
142                // Rx(theta) = Rz(-pi/2) SX Rz(PI - theta) SX Rz(-pi/2)
143                vec![
144                    Gate::Rz(*q, -FRAC_PI_2),
145                    Gate::Rx(*q, FRAC_PI_2),
146                    Gate::Rz(*q, PI - theta),
147                    Gate::Rx(*q, FRAC_PI_2),
148                    Gate::Rz(*q, -FRAC_PI_2),
149                ]
150            }
151        }
152
153        // Ry(theta) = Rz(-pi/2) SX Rz(theta) SX^dag Rz(pi/2)
154        // SX^dag = Rx(-pi/2) but that is not in basis, so use X SX = Rx(-pi/2)
155        // Actually: Ry(theta) = SX Rz(theta) SX^dag
156        //   where SX^dag = Rz(pi) SX Rz(pi)  (since Rx(-pi/2) = Rz(pi) Rx(pi/2) Rz(pi))
157        // Simpler: Ry(theta) = Rz(pi/2) Rx(pi/2) Rz(theta) Rx(pi/2) Rz(-pi/2)
158        // We map to: Rz(-pi/2) SX Rz(theta + pi) SX Rz(pi/2)
159        Gate::Ry(q, theta) => vec![
160            Gate::Rz(*q, -FRAC_PI_2),
161            Gate::Rx(*q, FRAC_PI_2),
162            Gate::Rz(*q, theta + PI),
163            Gate::Rx(*q, FRAC_PI_2),
164            Gate::Rz(*q, FRAC_PI_2),
165        ],
166
167        // --- two-qubit gates ---
168        // CZ = H(target) CNOT H(target)
169        Gate::CZ(q1, q2) => {
170            let mut gates = Vec::new();
171            gates.extend(decompose_to_ibm(&Gate::H(*q2)));
172            gates.push(Gate::CNOT(*q1, *q2));
173            gates.extend(decompose_to_ibm(&Gate::H(*q2)));
174            gates
175        }
176
177        // SWAP = CNOT(a,b) CNOT(b,a) CNOT(a,b)
178        Gate::SWAP(a, b) => vec![
179            Gate::CNOT(*a, *b),
180            Gate::CNOT(*b, *a),
181            Gate::CNOT(*a, *b),
182        ],
183
184        // Rzz(theta) = CNOT(a,b) Rz(b, theta) CNOT(a,b)
185        Gate::Rzz(a, b, theta) => vec![
186            Gate::CNOT(*a, *b),
187            Gate::Rz(*b, *theta),
188            Gate::CNOT(*a, *b),
189        ],
190
191        // --- non-unitary / pass-through ---
192        Gate::Measure(q) => vec![Gate::Measure(*q)],
193        Gate::Reset(q) => vec![Gate::Reset(*q)],
194        Gate::Barrier => vec![Gate::Barrier],
195
196        // Unitary1Q: decompose via ZYZ Euler angles and then map Ry
197        // For simplicity, keep as-is since custom unitaries are an edge case
198        // and the user can re-synthesize them.
199        Gate::Unitary1Q(q, m) => vec![Gate::Unitary1Q(*q, *m)],
200    }
201}
202
203// ---------------------------------------------------------------------------
204// Rigetti Aspen decomposition: basis = {CZ, Rx, Rz}
205// ---------------------------------------------------------------------------
206
207/// Decompose a single gate into the Rigetti Aspen basis {CZ, Rx, Rz}.
208pub fn decompose_to_rigetti(gate: &Gate) -> Vec<Gate> {
209    match gate {
210        // --- already in basis ---
211        Gate::CZ(q1, q2) => vec![Gate::CZ(*q1, *q2)],
212        Gate::Rx(q, theta) => vec![Gate::Rx(*q, *theta)],
213        Gate::Rz(q, theta) => vec![Gate::Rz(*q, *theta)],
214
215        // --- single-qubit Cliffords ---
216        // H = Rz(pi) Rx(pi/2)  (up to global phase)
217        Gate::H(q) => vec![Gate::Rz(*q, PI), Gate::Rx(*q, FRAC_PI_2)],
218
219        Gate::X(q) => vec![Gate::Rx(*q, PI)],
220        Gate::Y(q) => vec![Gate::Rx(*q, PI), Gate::Rz(*q, PI)],
221        Gate::Z(q) => vec![Gate::Rz(*q, PI)],
222        Gate::S(q) => vec![Gate::Rz(*q, FRAC_PI_2)],
223        Gate::Sdg(q) => vec![Gate::Rz(*q, -FRAC_PI_2)],
224        Gate::T(q) => vec![Gate::Rz(*q, FRAC_PI_4)],
225        Gate::Tdg(q) => vec![Gate::Rz(*q, -FRAC_PI_4)],
226        Gate::Phase(q, theta) => vec![Gate::Rz(*q, *theta)],
227
228        // Ry(theta) = Rz(-pi/2) Rx(theta) Rz(pi/2)
229        Gate::Ry(q, theta) => vec![
230            Gate::Rz(*q, -FRAC_PI_2),
231            Gate::Rx(*q, *theta),
232            Gate::Rz(*q, FRAC_PI_2),
233        ],
234
235        // --- two-qubit gates ---
236        // CNOT = H(target) CZ H(target)
237        //      = [Rz(pi) Rx(pi/2)] CZ [Rz(pi) Rx(pi/2)]  on target
238        Gate::CNOT(c, t) => {
239            let mut gates = Vec::new();
240            gates.extend(decompose_to_rigetti(&Gate::H(*t)));
241            gates.push(Gate::CZ(*c, *t));
242            gates.extend(decompose_to_rigetti(&Gate::H(*t)));
243            gates
244        }
245
246        // SWAP = CNOT(a,b) CNOT(b,a) CNOT(a,b) -- each CNOT further decomposed
247        Gate::SWAP(a, b) => {
248            let mut gates = Vec::new();
249            gates.extend(decompose_to_rigetti(&Gate::CNOT(*a, *b)));
250            gates.extend(decompose_to_rigetti(&Gate::CNOT(*b, *a)));
251            gates.extend(decompose_to_rigetti(&Gate::CNOT(*a, *b)));
252            gates
253        }
254
255        // Rzz(theta) = CNOT(a,b) Rz(b, theta) CNOT(a,b)
256        Gate::Rzz(a, b, theta) => {
257            let mut gates = Vec::new();
258            gates.extend(decompose_to_rigetti(&Gate::CNOT(*a, *b)));
259            gates.push(Gate::Rz(*b, *theta));
260            gates.extend(decompose_to_rigetti(&Gate::CNOT(*a, *b)));
261            gates
262        }
263
264        // --- non-unitary / pass-through ---
265        Gate::Measure(q) => vec![Gate::Measure(*q)],
266        Gate::Reset(q) => vec![Gate::Reset(*q)],
267        Gate::Barrier => vec![Gate::Barrier],
268        Gate::Unitary1Q(q, m) => vec![Gate::Unitary1Q(*q, *m)],
269    }
270}
271
272// ---------------------------------------------------------------------------
273// IonQ Aria decomposition: basis = {Rx, Ry, Rzz}
274// ---------------------------------------------------------------------------
275
276/// Decompose a single gate into the IonQ Aria basis {Rx, Ry, Rzz}.
277///
278/// IonQ native gates are GPI, GPI2, and MS, which map naturally to rotations
279/// in the {Rx, Ry, Rzz} family.
280pub fn decompose_to_ionq(gate: &Gate) -> Vec<Gate> {
281    match gate {
282        // --- already in basis ---
283        Gate::Rx(q, theta) => vec![Gate::Rx(*q, *theta)],
284        Gate::Ry(q, theta) => vec![Gate::Ry(*q, *theta)],
285        Gate::Rzz(a, b, theta) => vec![Gate::Rzz(*a, *b, *theta)],
286
287        // --- single-qubit Cliffords (decomposed via Rx / Ry) ---
288        // H = Ry(pi/2) Rx(pi)  (= Y^{1/2} X up to global phase)
289        Gate::H(q) => vec![Gate::Ry(*q, FRAC_PI_2), Gate::Rx(*q, PI)],
290
291        Gate::X(q) => vec![Gate::Rx(*q, PI)],
292        Gate::Y(q) => vec![Gate::Ry(*q, PI)],
293
294        // Z = Rx(pi) Ry(pi)  (up to global phase)
295        Gate::Z(q) => vec![Gate::Rx(*q, PI), Gate::Ry(*q, PI)],
296
297        // S = Rz(pi/2) = Rx(-pi/2) Ry(pi/2) Rx(pi/2)
298        Gate::S(q) => vec![
299            Gate::Rx(*q, -FRAC_PI_2),
300            Gate::Ry(*q, FRAC_PI_2),
301            Gate::Rx(*q, FRAC_PI_2),
302        ],
303
304        // Sdg = Rz(-pi/2) = Rx(-pi/2) Ry(-pi/2) Rx(pi/2)
305        Gate::Sdg(q) => vec![
306            Gate::Rx(*q, -FRAC_PI_2),
307            Gate::Ry(*q, -FRAC_PI_2),
308            Gate::Rx(*q, FRAC_PI_2),
309        ],
310
311        // T = Rz(pi/4) = Rx(-pi/2) Ry(pi/4) Rx(pi/2)
312        Gate::T(q) => vec![
313            Gate::Rx(*q, -FRAC_PI_2),
314            Gate::Ry(*q, FRAC_PI_4),
315            Gate::Rx(*q, FRAC_PI_2),
316        ],
317
318        // Tdg = Rz(-pi/4)
319        Gate::Tdg(q) => vec![
320            Gate::Rx(*q, -FRAC_PI_2),
321            Gate::Ry(*q, -FRAC_PI_4),
322            Gate::Rx(*q, FRAC_PI_2),
323        ],
324
325        // Rz(theta) = Rx(-pi/2) Ry(theta) Rx(pi/2)
326        Gate::Rz(q, theta) => vec![
327            Gate::Rx(*q, -FRAC_PI_2),
328            Gate::Ry(*q, *theta),
329            Gate::Rx(*q, FRAC_PI_2),
330        ],
331
332        // Phase(theta) maps to Rz(theta)
333        Gate::Phase(q, theta) => decompose_to_ionq(&Gate::Rz(*q, *theta)),
334
335        // --- two-qubit gates ---
336        // CNOT via Rzz + single-qubit rotations:
337        //   CNOT(c, t) = Ry(t, -pi/2) Rzz(c, t, pi/2) Rx(c, -pi/2) Rx(t, -pi/2) Ry(t, pi/2)
338        // This is the standard MS-based CNOT decomposition.
339        Gate::CNOT(c, t) => vec![
340            Gate::Ry(*t, -FRAC_PI_2),
341            Gate::Rzz(*c, *t, FRAC_PI_2),
342            Gate::Rx(*c, -FRAC_PI_2),
343            Gate::Rx(*t, -FRAC_PI_2),
344            Gate::Ry(*t, FRAC_PI_2),
345        ],
346
347        // CZ = H(target) CNOT H(target) -- decompose recursively
348        Gate::CZ(q1, q2) => {
349            let mut gates = Vec::new();
350            gates.extend(decompose_to_ionq(&Gate::H(*q2)));
351            gates.extend(decompose_to_ionq(&Gate::CNOT(*q1, *q2)));
352            gates.extend(decompose_to_ionq(&Gate::H(*q2)));
353            gates
354        }
355
356        // SWAP = 3 CNOTs -- decompose recursively
357        Gate::SWAP(a, b) => {
358            let mut gates = Vec::new();
359            gates.extend(decompose_to_ionq(&Gate::CNOT(*a, *b)));
360            gates.extend(decompose_to_ionq(&Gate::CNOT(*b, *a)));
361            gates.extend(decompose_to_ionq(&Gate::CNOT(*a, *b)));
362            gates
363        }
364
365        // --- non-unitary / pass-through ---
366        Gate::Measure(q) => vec![Gate::Measure(*q)],
367        Gate::Reset(q) => vec![Gate::Reset(*q)],
368        Gate::Barrier => vec![Gate::Barrier],
369        Gate::Unitary1Q(q, m) => vec![Gate::Unitary1Q(*q, *m)],
370    }
371}
372
373// ---------------------------------------------------------------------------
374// Qubit routing via SWAP insertion
375// ---------------------------------------------------------------------------
376
377/// Route a circuit onto the given coupling map by inserting SWAP gates so that
378/// every two-qubit gate operates on adjacent (coupled) qubits.
379///
380/// The coupling map is treated as undirected: `(a, b)` implies `(b, a)`.
381///
382/// Uses a simple greedy strategy: for each two-qubit gate on non-adjacent
383/// qubits, find the shortest path via BFS and insert SWAPs along the path,
384/// updating the logical-to-physical qubit mapping.
385pub fn route_circuit(circuit: &QuantumCircuit, coupling_map: &[(u32, u32)]) -> QuantumCircuit {
386    let n = circuit.num_qubits() as usize;
387
388    // Build adjacency list (undirected).
389    let adj = build_adjacency_list(coupling_map, n);
390
391    // logical -> physical mapping (starts as identity)
392    let mut log2phys: Vec<u32> = (0..n as u32).collect();
393    // physical -> logical mapping (inverse)
394    let mut phys2log: Vec<u32> = (0..n as u32).collect();
395
396    let mut result = QuantumCircuit::new(circuit.num_qubits());
397
398    for gate in circuit.gates() {
399        let qubits = gate.qubits();
400        if qubits.len() == 2 {
401            let logical_a = qubits[0];
402            let logical_b = qubits[1];
403            let mut phys_a = log2phys[logical_a as usize];
404            let mut phys_b = log2phys[logical_b as usize];
405
406            // Check if already adjacent.
407            if !are_adjacent(&adj, phys_a, phys_b) {
408                // BFS to find shortest path from phys_a to phys_b.
409                let path = bfs_shortest_path(&adj, phys_a, phys_b, n);
410
411                // Insert SWAPs along the path to bring phys_a next to phys_b.
412                // We move qubit A along the path towards B.
413                // After swapping along path[0..path.len()-2], the logical qubit
414                // that was at phys_a ends up adjacent to phys_b.
415                for i in 0..path.len() - 2 {
416                    let p1 = path[i];
417                    let p2 = path[i + 1];
418
419                    // Insert physical SWAP
420                    result.add_gate(Gate::SWAP(p1, p2));
421
422                    // Update mappings
423                    let log1 = phys2log[p1 as usize];
424                    let log2 = phys2log[p2 as usize];
425                    log2phys[log1 as usize] = p2;
426                    log2phys[log2 as usize] = p1;
427                    phys2log[p1 as usize] = log2;
428                    phys2log[p2 as usize] = log1;
429                }
430
431                // Recompute physical positions after routing.
432                phys_a = log2phys[logical_a as usize];
433                phys_b = log2phys[logical_b as usize];
434            }
435
436            // Emit the two-qubit gate on the (now adjacent) physical qubits.
437            result.add_gate(remap_gate(gate, &log2phys));
438
439            // Sanity check: the physical qubits should now be adjacent.
440            debug_assert!(
441                are_adjacent(&adj, phys_a, phys_b),
442                "routing failed: qubits {} and {} are not adjacent after SWAP insertion",
443                phys_a,
444                phys_b
445            );
446        } else if qubits.len() == 1 {
447            // Single-qubit gate: remap to physical qubit.
448            result.add_gate(remap_gate(gate, &log2phys));
449        } else {
450            // Barrier, etc.
451            result.add_gate(gate.clone());
452        }
453    }
454
455    result
456}
457
458/// Build an adjacency list from a coupling map.
459fn build_adjacency_list(coupling_map: &[(u32, u32)], n: usize) -> Vec<Vec<u32>> {
460    let mut adj: Vec<Vec<u32>> = vec![Vec::new(); n];
461    for &(a, b) in coupling_map {
462        if (a as usize) < n && (b as usize) < n {
463            if !adj[a as usize].contains(&b) {
464                adj[a as usize].push(b);
465            }
466            if !adj[b as usize].contains(&a) {
467                adj[b as usize].push(a);
468            }
469        }
470    }
471    adj
472}
473
474/// Check whether two physical qubits are directly connected.
475fn are_adjacent(adj: &[Vec<u32>], a: u32, b: u32) -> bool {
476    adj.get(a as usize)
477        .map(|neighbors| neighbors.contains(&b))
478        .unwrap_or(false)
479}
480
481/// BFS shortest path between two nodes in the coupling graph.
482/// Returns the sequence of physical qubit indices from `start` to `end`
483/// (inclusive of both endpoints).
484fn bfs_shortest_path(adj: &[Vec<u32>], start: u32, end: u32, n: usize) -> Vec<u32> {
485    if start == end {
486        return vec![start];
487    }
488
489    let mut visited = vec![false; n];
490    let mut parent: Vec<Option<u32>> = vec![None; n];
491    let mut queue = VecDeque::new();
492
493    visited[start as usize] = true;
494    queue.push_back(start);
495
496    while let Some(current) = queue.pop_front() {
497        if current == end {
498            break;
499        }
500        for &neighbor in &adj[current as usize] {
501            if !visited[neighbor as usize] {
502                visited[neighbor as usize] = true;
503                parent[neighbor as usize] = Some(current);
504                queue.push_back(neighbor);
505            }
506        }
507    }
508
509    // Reconstruct path from end to start.
510    let mut path = Vec::new();
511    let mut current = end;
512    path.push(current);
513    while let Some(p) = parent[current as usize] {
514        path.push(p);
515        current = p;
516        if current == start {
517            break;
518        }
519    }
520    path.reverse();
521    path
522}
523
524/// Remap a gate's qubit indices using the logical-to-physical mapping.
525fn remap_gate(gate: &Gate, log2phys: &[u32]) -> Gate {
526    match gate {
527        Gate::H(q) => Gate::H(log2phys[*q as usize]),
528        Gate::X(q) => Gate::X(log2phys[*q as usize]),
529        Gate::Y(q) => Gate::Y(log2phys[*q as usize]),
530        Gate::Z(q) => Gate::Z(log2phys[*q as usize]),
531        Gate::S(q) => Gate::S(log2phys[*q as usize]),
532        Gate::Sdg(q) => Gate::Sdg(log2phys[*q as usize]),
533        Gate::T(q) => Gate::T(log2phys[*q as usize]),
534        Gate::Tdg(q) => Gate::Tdg(log2phys[*q as usize]),
535        Gate::Rx(q, theta) => Gate::Rx(log2phys[*q as usize], *theta),
536        Gate::Ry(q, theta) => Gate::Ry(log2phys[*q as usize], *theta),
537        Gate::Rz(q, theta) => Gate::Rz(log2phys[*q as usize], *theta),
538        Gate::Phase(q, theta) => Gate::Phase(log2phys[*q as usize], *theta),
539        Gate::CNOT(c, t) => Gate::CNOT(log2phys[*c as usize], log2phys[*t as usize]),
540        Gate::CZ(a, b) => Gate::CZ(log2phys[*a as usize], log2phys[*b as usize]),
541        Gate::SWAP(a, b) => Gate::SWAP(log2phys[*a as usize], log2phys[*b as usize]),
542        Gate::Rzz(a, b, theta) => {
543            Gate::Rzz(log2phys[*a as usize], log2phys[*b as usize], *theta)
544        }
545        Gate::Measure(q) => Gate::Measure(log2phys[*q as usize]),
546        Gate::Reset(q) => Gate::Reset(log2phys[*q as usize]),
547        Gate::Barrier => Gate::Barrier,
548        Gate::Unitary1Q(q, m) => Gate::Unitary1Q(log2phys[*q as usize], *m),
549    }
550}
551
552// ---------------------------------------------------------------------------
553// Gate cancellation / optimization
554// ---------------------------------------------------------------------------
555
556/// Optimize a circuit by cancelling and merging gates.
557///
558/// * Level 0: no optimization (pass-through).
559/// * Level 1: cancel adjacent self-inverse pairs
560///   (H-H, X-X, Y-Y, Z-Z, S-Sdg, T-Tdg, CNOT-CNOT on same qubits).
561/// * Level 2: level 1 plus merge adjacent Rz gates on the same qubit
562///   (Rz(a) Rz(b) -> Rz(a+b)).
563pub fn optimize_gates(circuit: &QuantumCircuit, level: u8) -> QuantumCircuit {
564    if level == 0 {
565        return circuit.clone();
566    }
567
568    let mut gates: Vec<Gate> = circuit.gates().to_vec();
569
570    // Apply cancellation passes iteratively until no more changes occur.
571    let mut changed = true;
572    while changed {
573        changed = false;
574
575        // Level 1: cancel inverse pairs
576        let (new_gates, did_cancel) = cancel_inverse_pairs(&gates);
577        if did_cancel {
578            gates = new_gates;
579            changed = true;
580        }
581
582        // Level 2: merge adjacent Rz
583        if level >= 2 {
584            let (new_gates, did_merge) = merge_adjacent_rz(&gates);
585            if did_merge {
586                gates = new_gates;
587                changed = true;
588            }
589        }
590    }
591
592    let mut result = QuantumCircuit::new(circuit.num_qubits());
593    for g in gates {
594        result.add_gate(g);
595    }
596    result
597}
598
599/// Cancel adjacent self-inverse gate pairs.
600///
601/// Returns the new gate list and whether any cancellation occurred.
602fn cancel_inverse_pairs(gates: &[Gate]) -> (Vec<Gate>, bool) {
603    let mut result: Vec<Gate> = Vec::with_capacity(gates.len());
604    let mut changed = false;
605    let mut i = 0;
606
607    while i < gates.len() {
608        if i + 1 < gates.len() && is_inverse_pair(&gates[i], &gates[i + 1]) {
609            // Skip both gates -- they cancel.
610            changed = true;
611            i += 2;
612        } else {
613            result.push(gates[i].clone());
614            i += 1;
615        }
616    }
617
618    (result, changed)
619}
620
621/// Check whether two gates form an inverse pair that cancels to identity.
622fn is_inverse_pair(a: &Gate, b: &Gate) -> bool {
623    match (a, b) {
624        // Self-inverse single-qubit gates
625        (Gate::H(q1), Gate::H(q2)) if q1 == q2 => true,
626        (Gate::X(q1), Gate::X(q2)) if q1 == q2 => true,
627        (Gate::Y(q1), Gate::Y(q2)) if q1 == q2 => true,
628        (Gate::Z(q1), Gate::Z(q2)) if q1 == q2 => true,
629
630        // Adjoint pairs
631        (Gate::S(q1), Gate::Sdg(q2)) if q1 == q2 => true,
632        (Gate::Sdg(q1), Gate::S(q2)) if q1 == q2 => true,
633        (Gate::T(q1), Gate::Tdg(q2)) if q1 == q2 => true,
634        (Gate::Tdg(q1), Gate::T(q2)) if q1 == q2 => true,
635
636        // Self-inverse two-qubit gates (same qubit order)
637        (Gate::CNOT(c1, t1), Gate::CNOT(c2, t2)) if c1 == c2 && t1 == t2 => true,
638        (Gate::CZ(a1, b1), Gate::CZ(a2, b2))
639            if (a1 == a2 && b1 == b2) || (a1 == b2 && b1 == a2) =>
640        {
641            true
642        }
643        (Gate::SWAP(a1, b1), Gate::SWAP(a2, b2))
644            if (a1 == a2 && b1 == b2) || (a1 == b2 && b1 == a2) =>
645        {
646            true
647        }
648
649        _ => false,
650    }
651}
652
653/// Merge adjacent Rz gates on the same qubit: Rz(a) Rz(b) -> Rz(a+b).
654///
655/// If the merged angle is effectively zero (|a+b| < epsilon), the gate is
656/// dropped entirely.
657///
658/// Returns the new gate list and whether any merge occurred.
659fn merge_adjacent_rz(gates: &[Gate]) -> (Vec<Gate>, bool) {
660    let mut result: Vec<Gate> = Vec::with_capacity(gates.len());
661    let mut changed = false;
662    let mut i = 0;
663    let epsilon = 1e-12;
664
665    while i < gates.len() {
666        if let Gate::Rz(q1, a) = &gates[i] {
667            // Accumulate consecutive Rz on the same qubit.
668            let mut total_angle = *a;
669            let qubit = *q1;
670            let mut count = 1;
671
672            while i + count < gates.len() {
673                if let Gate::Rz(q2, b) = &gates[i + count] {
674                    if *q2 == qubit {
675                        total_angle += b;
676                        count += 1;
677                        continue;
678                    }
679                }
680                break;
681            }
682
683            if count > 1 {
684                changed = true;
685                if total_angle.abs() > epsilon {
686                    result.push(Gate::Rz(qubit, total_angle));
687                }
688                // else: angle is zero, drop entirely
689            } else {
690                result.push(gates[i].clone());
691            }
692            i += count;
693        } else {
694            result.push(gates[i].clone());
695            i += 1;
696        }
697    }
698
699    (result, changed)
700}
701
702// ---------------------------------------------------------------------------
703// Tests
704// ---------------------------------------------------------------------------
705
706#[cfg(test)]
707mod tests {
708    use super::*;
709    use std::f64::consts::{FRAC_PI_2, FRAC_PI_4, PI};
710
711    // -- Decomposition tests --
712
713    #[test]
714    fn test_decompose_h_to_ibm() {
715        let gates = decompose_to_ibm(&Gate::H(0));
716        // H -> Rz(pi) SX Rz(pi) = 3 gates
717        assert_eq!(gates.len(), 3);
718        assert!(matches!(gates[0], Gate::Rz(0, _)));
719        assert!(matches!(gates[1], Gate::Rx(0, _)));
720        assert!(matches!(gates[2], Gate::Rz(0, _)));
721
722        // The Rx should be pi/2 (SX)
723        if let Gate::Rx(_, theta) = &gates[1] {
724            assert!((theta - FRAC_PI_2).abs() < 1e-12);
725        } else {
726            panic!("expected Rx");
727        }
728    }
729
730    #[test]
731    fn test_decompose_s_to_ibm() {
732        let gates = decompose_to_ibm(&Gate::S(0));
733        assert_eq!(gates.len(), 1);
734        if let Gate::Rz(0, theta) = &gates[0] {
735            assert!((theta - FRAC_PI_2).abs() < 1e-12);
736        } else {
737            panic!("expected Rz(pi/2)");
738        }
739    }
740
741    #[test]
742    fn test_decompose_t_to_ibm() {
743        let gates = decompose_to_ibm(&Gate::T(0));
744        assert_eq!(gates.len(), 1);
745        if let Gate::Rz(0, theta) = &gates[0] {
746            assert!((theta - FRAC_PI_4).abs() < 1e-12);
747        } else {
748            panic!("expected Rz(pi/4)");
749        }
750    }
751
752    #[test]
753    fn test_decompose_swap_to_ibm() {
754        let gates = decompose_to_ibm(&Gate::SWAP(0, 1));
755        // SWAP -> 3 CNOTs
756        assert_eq!(gates.len(), 3);
757        assert!(gates.iter().all(|g| matches!(g, Gate::CNOT(_, _))));
758    }
759
760    #[test]
761    fn test_decompose_cz_to_ibm() {
762        let gates = decompose_to_ibm(&Gate::CZ(0, 1));
763        // CZ -> H(1) CNOT H(1) = 3 + 1 + 3 = 7 gates
764        assert_eq!(gates.len(), 7);
765        // The middle gate should be CNOT
766        assert!(matches!(gates[3], Gate::CNOT(0, 1)));
767    }
768
769    #[test]
770    fn test_decompose_cnot_to_rigetti_produces_cz() {
771        let gates = decompose_to_rigetti(&Gate::CNOT(0, 1));
772        // CNOT -> H(target) CZ H(target)
773        // H(target) = Rz(pi) Rx(pi/2) = 2 gates
774        // So total = 2 + 1 + 2 = 5 gates
775        assert_eq!(gates.len(), 5);
776        // There should be exactly one CZ
777        let cz_count = gates.iter().filter(|g| matches!(g, Gate::CZ(_, _))).count();
778        assert_eq!(cz_count, 1);
779        assert!(matches!(gates[2], Gate::CZ(0, 1)));
780    }
781
782    #[test]
783    fn test_decompose_h_to_rigetti() {
784        let gates = decompose_to_rigetti(&Gate::H(0));
785        // H -> Rz(pi) Rx(pi/2)
786        assert_eq!(gates.len(), 2);
787        assert!(matches!(gates[0], Gate::Rz(0, _)));
788        assert!(matches!(gates[1], Gate::Rx(0, _)));
789    }
790
791    #[test]
792    fn test_decompose_cnot_to_ionq() {
793        let gates = decompose_to_ionq(&Gate::CNOT(0, 1));
794        // Should contain exactly one Rzz gate
795        let rzz_count = gates
796            .iter()
797            .filter(|g| matches!(g, Gate::Rzz(_, _, _)))
798            .count();
799        assert_eq!(rzz_count, 1);
800        // Total: Ry(-pi/2) Rzz(pi/2) Rx(-pi/2) Rx(-pi/2) Ry(pi/2) = 5 gates
801        assert_eq!(gates.len(), 5);
802    }
803
804    #[test]
805    fn test_decompose_preserves_non_unitary() {
806        let measure_ibm = decompose_to_ibm(&Gate::Measure(0));
807        assert_eq!(measure_ibm.len(), 1);
808        assert!(matches!(measure_ibm[0], Gate::Measure(0)));
809
810        let barrier_rigetti = decompose_to_rigetti(&Gate::Barrier);
811        assert_eq!(barrier_rigetti.len(), 1);
812        assert!(matches!(barrier_rigetti[0], Gate::Barrier));
813
814        let reset_ionq = decompose_to_ionq(&Gate::Reset(2));
815        assert_eq!(reset_ionq.len(), 1);
816        assert!(matches!(reset_ionq[0], Gate::Reset(2)));
817    }
818
819    // -- Routing tests --
820
821    #[test]
822    fn test_route_adjacent_cnot_no_swaps() {
823        // Linear chain: 0-1-2
824        let coupling = vec![(0, 1), (1, 2)];
825        let mut circuit = QuantumCircuit::new(3);
826        circuit.cnot(0, 1);
827
828        let routed = route_circuit(&circuit, &coupling);
829        // Already adjacent -- no SWAPs needed.
830        let swap_count = routed
831            .gates()
832            .iter()
833            .filter(|g| matches!(g, Gate::SWAP(_, _)))
834            .count();
835        assert_eq!(swap_count, 0);
836        assert_eq!(routed.gates().len(), 1);
837    }
838
839    #[test]
840    fn test_route_non_adjacent_cnot_inserts_swaps() {
841        // Linear chain: 0-1-2
842        let coupling = vec![(0, 1), (1, 2)];
843        let mut circuit = QuantumCircuit::new(3);
844        circuit.cnot(0, 2); // not adjacent
845
846        let routed = route_circuit(&circuit, &coupling);
847        // Should have inserted at least one SWAP.
848        let swap_count = routed
849            .gates()
850            .iter()
851            .filter(|g| matches!(g, Gate::SWAP(_, _)))
852            .count();
853        assert!(swap_count >= 1, "expected at least 1 SWAP, got {}", swap_count);
854    }
855
856    #[test]
857    fn test_route_single_qubit_gate_remapped() {
858        // Linear chain: 0-1-2
859        let coupling = vec![(0, 1), (1, 2)];
860        let mut circuit = QuantumCircuit::new(3);
861        circuit.h(0);
862
863        let routed = route_circuit(&circuit, &coupling);
864        // Single-qubit gate should pass through (mapped to physical qubit 0
865        // since no SWAPs happened).
866        assert_eq!(routed.gates().len(), 1);
867        assert!(matches!(routed.gates()[0], Gate::H(0)));
868    }
869
870    #[test]
871    fn test_bfs_shortest_path_linear() {
872        // 0 - 1 - 2 - 3
873        let coupling = vec![(0, 1), (1, 2), (2, 3)];
874        let adj = build_adjacency_list(&coupling, 4);
875        let path = bfs_shortest_path(&adj, 0, 3, 4);
876        assert_eq!(path, vec![0, 1, 2, 3]);
877    }
878
879    #[test]
880    fn test_bfs_shortest_path_branching() {
881        // Star topology: 0-1, 0-2, 0-3
882        let coupling = vec![(0, 1), (0, 2), (0, 3)];
883        let adj = build_adjacency_list(&coupling, 4);
884        let path = bfs_shortest_path(&adj, 1, 3, 4);
885        // Shortest path: 1 -> 0 -> 3 (length 3 nodes)
886        assert_eq!(path.len(), 3);
887        assert_eq!(path[0], 1);
888        assert_eq!(*path.last().unwrap(), 3);
889    }
890
891    #[test]
892    fn test_bfs_same_node() {
893        let coupling = vec![(0, 1)];
894        let adj = build_adjacency_list(&coupling, 2);
895        let path = bfs_shortest_path(&adj, 0, 0, 2);
896        assert_eq!(path, vec![0]);
897    }
898
899    // -- Optimization tests --
900
901    #[test]
902    fn test_cancel_hh_produces_empty() {
903        let mut circuit = QuantumCircuit::new(1);
904        circuit.h(0);
905        circuit.h(0);
906
907        let optimized = optimize_gates(&circuit, 1);
908        assert_eq!(optimized.gate_count(), 0);
909    }
910
911    #[test]
912    fn test_cancel_xx() {
913        let mut circuit = QuantumCircuit::new(1);
914        circuit.x(0);
915        circuit.x(0);
916
917        let optimized = optimize_gates(&circuit, 1);
918        assert_eq!(optimized.gate_count(), 0);
919    }
920
921    #[test]
922    fn test_cancel_zz() {
923        let mut circuit = QuantumCircuit::new(1);
924        circuit.z(0);
925        circuit.z(0);
926
927        let optimized = optimize_gates(&circuit, 1);
928        assert_eq!(optimized.gate_count(), 0);
929    }
930
931    #[test]
932    fn test_cancel_s_sdg() {
933        let mut circuit = QuantumCircuit::new(1);
934        circuit.s(0);
935        circuit.add_gate(Gate::Sdg(0));
936
937        let optimized = optimize_gates(&circuit, 1);
938        assert_eq!(optimized.gate_count(), 0);
939    }
940
941    #[test]
942    fn test_cancel_t_tdg() {
943        let mut circuit = QuantumCircuit::new(1);
944        circuit.t(0);
945        circuit.add_gate(Gate::Tdg(0));
946
947        let optimized = optimize_gates(&circuit, 1);
948        assert_eq!(optimized.gate_count(), 0);
949    }
950
951    #[test]
952    fn test_cancel_cnot_cnot() {
953        let mut circuit = QuantumCircuit::new(2);
954        circuit.cnot(0, 1);
955        circuit.cnot(0, 1);
956
957        let optimized = optimize_gates(&circuit, 1);
958        assert_eq!(optimized.gate_count(), 0);
959    }
960
961    #[test]
962    fn test_no_cancel_different_qubits() {
963        let mut circuit = QuantumCircuit::new(2);
964        circuit.h(0);
965        circuit.h(1);
966
967        let optimized = optimize_gates(&circuit, 1);
968        assert_eq!(optimized.gate_count(), 2);
969    }
970
971    #[test]
972    fn test_merge_rz_level2() {
973        let mut circuit = QuantumCircuit::new(1);
974        circuit.rz(0, FRAC_PI_4);
975        circuit.rz(0, FRAC_PI_4);
976
977        let optimized = optimize_gates(&circuit, 2);
978        assert_eq!(optimized.gate_count(), 1);
979        if let Gate::Rz(0, theta) = &optimized.gates()[0] {
980            assert!((theta - FRAC_PI_2).abs() < 1e-12);
981        } else {
982            panic!("expected merged Rz(pi/2)");
983        }
984    }
985
986    #[test]
987    fn test_merge_rz_to_zero_eliminates() {
988        let mut circuit = QuantumCircuit::new(1);
989        circuit.rz(0, PI);
990        circuit.rz(0, -PI);
991
992        let optimized = optimize_gates(&circuit, 2);
993        assert_eq!(optimized.gate_count(), 0);
994    }
995
996    #[test]
997    fn test_merge_three_rz() {
998        let mut circuit = QuantumCircuit::new(1);
999        circuit.rz(0, FRAC_PI_4);
1000        circuit.rz(0, FRAC_PI_4);
1001        circuit.rz(0, FRAC_PI_4);
1002
1003        let optimized = optimize_gates(&circuit, 2);
1004        assert_eq!(optimized.gate_count(), 1);
1005        if let Gate::Rz(0, theta) = &optimized.gates()[0] {
1006            assert!((theta - 3.0 * FRAC_PI_4).abs() < 1e-12);
1007        } else {
1008            panic!("expected merged Rz(3*pi/4)");
1009        }
1010    }
1011
1012    #[test]
1013    fn test_level0_no_optimization() {
1014        let mut circuit = QuantumCircuit::new(1);
1015        circuit.h(0);
1016        circuit.h(0);
1017
1018        let optimized = optimize_gates(&circuit, 0);
1019        assert_eq!(optimized.gate_count(), 2);
1020    }
1021
1022    #[test]
1023    fn test_level1_does_not_merge_rz() {
1024        let mut circuit = QuantumCircuit::new(1);
1025        circuit.rz(0, FRAC_PI_4);
1026        circuit.rz(0, FRAC_PI_4);
1027
1028        let optimized = optimize_gates(&circuit, 1);
1029        // Level 1 only cancels inverses, not merges.
1030        assert_eq!(optimized.gate_count(), 2);
1031    }
1032
1033    // -- Full pipeline tests --
1034
1035    #[test]
1036    fn test_transpile_universal_passthrough() {
1037        let mut circuit = QuantumCircuit::new(2);
1038        circuit.h(0);
1039        circuit.cnot(0, 1);
1040
1041        let config = TranspilerConfig {
1042            basis: BasisGateSet::Universal,
1043            coupling_map: None,
1044            optimization_level: 0,
1045        };
1046
1047        let result = transpile(&circuit, &config);
1048        assert_eq!(result.gate_count(), 2);
1049    }
1050
1051    #[test]
1052    fn test_transpile_ibm_decomposes_then_optimizes() {
1053        let mut circuit = QuantumCircuit::new(1);
1054        // H H should decompose to 6 gates then cancel to 0
1055        circuit.h(0);
1056        circuit.h(0);
1057
1058        let config = TranspilerConfig {
1059            basis: BasisGateSet::IbmEagle,
1060            coupling_map: None,
1061            optimization_level: 2,
1062        };
1063
1064        let result = transpile(&circuit, &config);
1065        // After decomposition: Rz(pi) Rx(pi/2) Rz(pi) Rz(pi) Rx(pi/2) Rz(pi)
1066        // Level 2 merges adjacent Rz: Rz(pi) Rx(pi/2) Rz(2*pi) Rx(pi/2) Rz(pi)
1067        // Rz(2*pi) is not zero so it stays (it is 2*pi, not 0).
1068        // This tests that the pipeline runs without error.
1069        assert!(result.gate_count() < 6, "expected some optimization");
1070    }
1071
1072    #[test]
1073    fn test_transpile_with_routing() {
1074        // 3-qubit linear chain, CNOT(0,2) should get routed
1075        let mut circuit = QuantumCircuit::new(3);
1076        circuit.cnot(0, 2);
1077
1078        let config = TranspilerConfig {
1079            basis: BasisGateSet::Universal,
1080            coupling_map: Some(vec![(0, 1), (1, 2)]),
1081            optimization_level: 0,
1082        };
1083
1084        let result = transpile(&circuit, &config);
1085        // Should have inserted SWAPs
1086        let swap_count = result
1087            .gates()
1088            .iter()
1089            .filter(|g| matches!(g, Gate::SWAP(_, _)))
1090            .count();
1091        assert!(swap_count >= 1);
1092    }
1093
1094    #[test]
1095    fn test_transpile_rigetti_bell_state() {
1096        // Bell state: H(0), CNOT(0,1)
1097        let mut circuit = QuantumCircuit::new(2);
1098        circuit.h(0);
1099        circuit.cnot(0, 1);
1100
1101        let config = TranspilerConfig {
1102            basis: BasisGateSet::RigettiAspen,
1103            coupling_map: None,
1104            optimization_level: 0,
1105        };
1106
1107        let result = transpile(&circuit, &config);
1108        // All gates should be in {CZ, Rx, Rz}
1109        for gate in result.gates() {
1110            match gate {
1111                Gate::CZ(_, _) | Gate::Rx(_, _) | Gate::Rz(_, _) => {}
1112                Gate::Measure(_) | Gate::Reset(_) | Gate::Barrier => {}
1113                other => panic!("gate {:?} not in Rigetti basis", other),
1114            }
1115        }
1116    }
1117
1118    #[test]
1119    fn test_transpile_ionq_single_qubit() {
1120        let mut circuit = QuantumCircuit::new(1);
1121        circuit.h(0);
1122
1123        let config = TranspilerConfig {
1124            basis: BasisGateSet::IonQAria,
1125            coupling_map: None,
1126            optimization_level: 0,
1127        };
1128
1129        let result = transpile(&circuit, &config);
1130        // All gates should be in {Rx, Ry, Rzz}
1131        for gate in result.gates() {
1132            match gate {
1133                Gate::Rx(_, _) | Gate::Ry(_, _) | Gate::Rzz(_, _, _) => {}
1134                Gate::Measure(_) | Gate::Reset(_) | Gate::Barrier => {}
1135                other => panic!("gate {:?} not in IonQ basis", other),
1136            }
1137        }
1138    }
1139
1140    #[test]
1141    fn test_iterative_cancellation() {
1142        // After cancelling the inner pair, the outer pair should also cancel.
1143        // X H H X -> X (cancel) X -> (cancel) -> empty
1144        let mut circuit = QuantumCircuit::new(1);
1145        circuit.x(0);
1146        circuit.h(0);
1147        circuit.h(0);
1148        circuit.x(0);
1149
1150        let optimized = optimize_gates(&circuit, 1);
1151        assert_eq!(optimized.gate_count(), 0);
1152    }
1153
1154    #[test]
1155    fn test_routing_updates_mapping_correctly() {
1156        // Linear chain: 0-1-2-3
1157        // Two CNOTs: CNOT(0,3) then CNOT(0,1)
1158        // After routing CNOT(0,3), the mapping changes due to SWAPs.
1159        let coupling = vec![(0, 1), (1, 2), (2, 3)];
1160        let mut circuit = QuantumCircuit::new(4);
1161        circuit.cnot(0, 3);
1162        circuit.h(0);
1163
1164        let routed = route_circuit(&circuit, &coupling);
1165        // The circuit should compile without panicking and contain SWAPs.
1166        let swap_count = routed
1167            .gates()
1168            .iter()
1169            .filter(|g| matches!(g, Gate::SWAP(_, _)))
1170            .count();
1171        assert!(swap_count >= 1);
1172        // The H gate should also be present (on the remapped physical qubit).
1173        let h_count = routed
1174            .gates()
1175            .iter()
1176            .filter(|g| matches!(g, Gate::H(_)))
1177            .count();
1178        assert_eq!(h_count, 1);
1179    }
1180
1181    #[test]
1182    fn test_decompose_rzz_to_ibm() {
1183        let gates = decompose_to_ibm(&Gate::Rzz(0, 1, FRAC_PI_4));
1184        // Rzz -> CNOT Rz CNOT = 3 gates
1185        assert_eq!(gates.len(), 3);
1186        assert!(matches!(gates[0], Gate::CNOT(0, 1)));
1187        assert!(matches!(gates[1], Gate::Rz(1, _)));
1188        assert!(matches!(gates[2], Gate::CNOT(0, 1)));
1189    }
1190
1191    #[test]
1192    fn test_basis_gate_set_variants() {
1193        // Ensure all variants are distinct and constructible.
1194        let variants = [
1195            BasisGateSet::IbmEagle,
1196            BasisGateSet::IonQAria,
1197            BasisGateSet::RigettiAspen,
1198            BasisGateSet::Universal,
1199        ];
1200        for (i, a) in variants.iter().enumerate() {
1201            for (j, b) in variants.iter().enumerate() {
1202                if i == j {
1203                    assert_eq!(a, b);
1204                } else {
1205                    assert_ne!(a, b);
1206                }
1207            }
1208        }
1209    }
1210}