From 3e136d701303063beaad13e79a8832ca0aaaa85d Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Thu, 7 Nov 2024 07:20:47 -0500 Subject: [PATCH] Add pauli_twirl_2q_gates function (#13331) * Add twirl_circuit function This commit adds a new function twirl_circuit to apply Pauli twirling to a given circuit. This function only works with a fixed set of two qubit gates and is not a general solution. For performance this new function is written in rust and uses static lookup tables for the twirling sets around the fixed two qubit gates that are randomly sampled. There is an option to perform twirling multiple times and return a list of circuits instead of just doing it once. Ideally we'd do this in parallel, but we're currently blocked on the use of OnceCell in the PackedInstruction and the ParameterTable from parallelizing with CircuitData. We can further improve the performance of this new function by using a parallel iterator once #13219 is resolved. The function is written in a way to make this simple in the future. Fixes #13325 Co-authored-by: Paul Nation * Update asv benchmarks to use new function * Apply suggestions from code review Co-authored-by: Julien Gacon * Expand testing coverage * Add capacity option to clone_empty_from * Make static twirling tables more compact * Make num_trials default to None to return a single circuit * Allow for multiple twirling gates This commit expands the twirling_gate argument to work with strings instead of classes and also specify a list of gates instead of a single gate type. It also changes the default to `None` to twirl all supported gates in the circuit. * Fix typo in max seed value * Avoid vec for qubits * Recurse into control flow for twirling This commit updates the twirl_circuits logic to recurse into a control flow operation and twirl any gates that are potentially in the block. * Avoid extra calls to interner Previously for each twirled gate we were calling the interner 4 times once for each new gate. However, there are only 2 qargs being used and we only need half of the interning calls by reusing the interned qubits between the two gates. This commit reworks the logic to make this optimization. * Rename clone_empty_from to clone_empty_like * Improve docs for new rust space methods * Unify seeding logic This commit removes the final logic branch for when we're generating > 4 circuit outputs. That was previously left in place to prepare for building the output circuits in parallel using a rayon iterator. However, we're currently blocked form doing that and having different seeding behavior for a fixed seed when you move from 3 to 5 output circuits is probably unexpected. This standardizes on the lower overhead sequential iterator path, if/when we make it parallel we can switch the seeding behavior over to the other form. * Fix lint * Preserve Python space circuit metadata in output twirled circuits * Add option to run Optimize1qGatesDecomposition This commit adds a new option to the function to enable running the 1q optimization pass as part of the twirling function. The typical workflow is to run twirling after transpilation to generate a lot of random twirled circuits, in this workflow we need to adjust the pauli gates generated during twirling to match the backend's target. The Optimize1qGatesDecomposition pass that's passed in is used as a container for the arguments, we call the pass internally via rust for lower overhead. * Handle all gates with a matrix This commit expands the twirling function to work with any gate object that has a matrix. For the gates outside the previosuly supported set of CX, ECR, CZ, and iSwap we compute the twirling set on demand for any gate when the function is called now. This lets the function work for any gate even custom ones as long as they define a matrix. * Add checking on custom gates to ensure they're valid for twirling * Fix lint * Pre-compute pauli gate products * Use square of norm to save a sqrt * Switch from taking pass to a Target Forcing users to instantiate a Optimize1qGatesDecomposition transpiler pass was a bit heavy when the twirling function wasn't actually calling the pass directly. All the pass was being used for was to pull the constraints out and pass it to Rust. It is simpler for users to just give the target to the function directly and also simplifies the function's code. * Apply suggestions from code review Co-authored-by: Julien Gacon * Adjust docstring * Update tests for new function name * Update release note for new function name too * Rename set_qargs to add_qargs --------- Co-authored-by: Paul Nation Co-authored-by: Julien Gacon --- crates/accelerate/src/lib.rs | 1 + crates/accelerate/src/twirling.rs | 484 ++++++++++++++++++ crates/circuit/src/circuit_data.rs | 60 +++ crates/circuit/src/dag_circuit.rs | 2 +- crates/pyext/src/lib.rs | 1 + qiskit/__init__.py | 1 + qiskit/circuit/__init__.py | 29 +- qiskit/circuit/quantumcircuit.py | 61 ++- qiskit/circuit/twirling.py | 145 ++++++ .../add-twirl-circuit-ff4d4437190551bc.yaml | 17 + test/benchmarks/manipulate.py | 123 +---- test/python/circuit/test_twirling.py | 212 ++++++++ 12 files changed, 981 insertions(+), 155 deletions(-) create mode 100644 crates/accelerate/src/twirling.rs create mode 100644 qiskit/circuit/twirling.py create mode 100644 releasenotes/notes/add-twirl-circuit-ff4d4437190551bc.yaml create mode 100644 test/python/circuit/test_twirling.py diff --git a/crates/accelerate/src/lib.rs b/crates/accelerate/src/lib.rs index 84e62b7a8867..45cf047a6808 100644 --- a/crates/accelerate/src/lib.rs +++ b/crates/accelerate/src/lib.rs @@ -54,6 +54,7 @@ pub mod star_prerouting; pub mod stochastic_swap; pub mod synthesis; pub mod target_transpiler; +pub mod twirling; pub mod two_qubit_decompose; pub mod uc_gate; pub mod unitary_synthesis; diff --git a/crates/accelerate/src/twirling.rs b/crates/accelerate/src/twirling.rs new file mode 100644 index 000000000000..0867bc161555 --- /dev/null +++ b/crates/accelerate/src/twirling.rs @@ -0,0 +1,484 @@ +// This code is part of Qiskit. +// +// (C) Copyright IBM 2024 +// +// This code is licensed under the Apache License, Version 2.0. You may +// obtain a copy of this license in the LICENSE.txt file in the root directory +// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +// +// Any modifications or derivative works of this code must retain this +// copyright notice, and modified files need to carry a notice indicating +// that they have been altered from the originals. + +use std::f64::consts::PI; + +use hashbrown::HashMap; +use ndarray::linalg::kron; +use ndarray::prelude::*; +use ndarray::ArrayView2; +use num_complex::Complex64; +use pyo3::intern; +use pyo3::prelude::*; +use pyo3::types::PyList; +use pyo3::wrap_pyfunction; +use pyo3::Python; +use rand::prelude::*; +use rand_pcg::Pcg64Mcg; +use smallvec::SmallVec; + +use qiskit_circuit::circuit_data::CircuitData; +use qiskit_circuit::circuit_instruction::{ExtraInstructionAttributes, OperationFromPython}; +use qiskit_circuit::converters::dag_to_circuit; +use qiskit_circuit::dag_circuit::DAGCircuit; +use qiskit_circuit::gate_matrix::ONE_QUBIT_IDENTITY; +use qiskit_circuit::imports::QUANTUM_CIRCUIT; +use qiskit_circuit::operations::StandardGate::{IGate, XGate, YGate, ZGate}; +use qiskit_circuit::operations::{Operation, OperationRef, Param, PyInstruction, StandardGate}; +use qiskit_circuit::packed_instruction::{PackedInstruction, PackedOperation}; + +use crate::euler_one_qubit_decomposer::optimize_1q_gates_decomposition; +use crate::target_transpiler::Target; +use crate::QiskitError; + +static ECR_TWIRL_SET: [([StandardGate; 4], f64); 16] = [ + ([IGate, ZGate, ZGate, YGate], 0.), + ([IGate, XGate, IGate, XGate], 0.), + ([IGate, YGate, ZGate, ZGate], PI), + ([IGate, IGate, IGate, IGate], 0.), + ([ZGate, XGate, ZGate, XGate], PI), + ([ZGate, YGate, IGate, ZGate], 0.), + ([ZGate, IGate, ZGate, IGate], PI), + ([ZGate, ZGate, IGate, YGate], PI), + ([XGate, YGate, XGate, YGate], 0.), + ([XGate, IGate, YGate, XGate], PI), + ([XGate, ZGate, XGate, ZGate], 0.), + ([XGate, XGate, YGate, IGate], PI), + ([YGate, IGate, XGate, XGate], PI), + ([YGate, ZGate, YGate, ZGate], PI), + ([YGate, XGate, XGate, IGate], PI), + ([YGate, YGate, YGate, YGate], PI), +]; + +static CX_TWIRL_SET: [([StandardGate; 4], f64); 16] = [ + ([IGate, ZGate, ZGate, ZGate], 0.), + ([IGate, XGate, IGate, XGate], 0.), + ([IGate, YGate, ZGate, YGate], 0.), + ([IGate, IGate, IGate, IGate], 0.), + ([ZGate, XGate, ZGate, XGate], 0.), + ([ZGate, YGate, IGate, YGate], 0.), + ([ZGate, IGate, ZGate, IGate], 0.), + ([ZGate, ZGate, IGate, ZGate], 0.), + ([XGate, YGate, YGate, ZGate], 0.), + ([XGate, IGate, XGate, XGate], 0.), + ([XGate, ZGate, YGate, YGate], PI), + ([XGate, XGate, XGate, IGate], 0.), + ([YGate, IGate, YGate, XGate], 0.), + ([YGate, ZGate, XGate, YGate], 0.), + ([YGate, XGate, YGate, IGate], 0.), + ([YGate, YGate, XGate, ZGate], PI), +]; + +static CZ_TWIRL_SET: [([StandardGate; 4], f64); 16] = [ + ([IGate, ZGate, IGate, ZGate], 0.), + ([IGate, XGate, ZGate, XGate], 0.), + ([IGate, YGate, ZGate, YGate], 0.), + ([IGate, IGate, IGate, IGate], 0.), + ([ZGate, XGate, IGate, XGate], 0.), + ([ZGate, YGate, IGate, YGate], 0.), + ([ZGate, IGate, ZGate, IGate], 0.), + ([ZGate, ZGate, ZGate, ZGate], 0.), + ([XGate, YGate, YGate, XGate], PI), + ([XGate, IGate, XGate, ZGate], 0.), + ([XGate, ZGate, XGate, IGate], 0.), + ([XGate, XGate, YGate, YGate], 0.), + ([YGate, IGate, YGate, ZGate], 0.), + ([YGate, ZGate, YGate, IGate], 0.), + ([YGate, XGate, XGate, YGate], PI), + ([YGate, YGate, XGate, XGate], 0.), +]; + +static ISWAP_TWIRL_SET: [([StandardGate; 4], f64); 16] = [ + ([IGate, ZGate, ZGate, IGate], 0.), + ([IGate, XGate, YGate, ZGate], 0.), + ([IGate, YGate, XGate, ZGate], PI), + ([IGate, IGate, IGate, IGate], 0.), + ([ZGate, XGate, YGate, IGate], 0.), + ([ZGate, YGate, XGate, IGate], PI), + ([ZGate, IGate, IGate, ZGate], 0.), + ([ZGate, ZGate, ZGate, ZGate], 0.), + ([XGate, YGate, YGate, XGate], 0.), + ([XGate, IGate, ZGate, YGate], 0.), + ([XGate, ZGate, IGate, YGate], 0.), + ([XGate, XGate, XGate, XGate], 0.), + ([YGate, IGate, ZGate, XGate], PI), + ([YGate, ZGate, IGate, XGate], PI), + ([YGate, XGate, XGate, YGate], 0.), + ([YGate, YGate, YGate, YGate], 0.), +]; + +static TWIRLING_SETS: [&[([StandardGate; 4], f64); 16]; 4] = [ + &CX_TWIRL_SET, + &CZ_TWIRL_SET, + &ECR_TWIRL_SET, + &ISWAP_TWIRL_SET, +]; + +const CX_MASK: u8 = 8; +const CZ_MASK: u8 = 4; +const ECR_MASK: u8 = 2; +const ISWAP_MASK: u8 = 1; + +#[inline(always)] +fn diff_frob_norm_sq(array: ArrayView2, gate_matrix: ArrayView2) -> f64 { + let mut res: f64 = 0.; + for i in 0..4 { + for j in 0..4 { + let gate = gate_matrix[[i, j]]; + let twirled = array[[i, j]]; + let diff = twirled - gate; + res += (diff.conj() * diff).re; + } + } + res +} + +fn generate_twirling_set(gate_matrix: ArrayView2) -> Vec<([StandardGate; 4], f64)> { + let mut out_vec = Vec::with_capacity(16); + let i_matrix = aview2(&ONE_QUBIT_IDENTITY); + let x_matrix = aview2(&qiskit_circuit::gate_matrix::X_GATE); + let y_matrix = aview2(&qiskit_circuit::gate_matrix::Y_GATE); + let z_matrix = aview2(&qiskit_circuit::gate_matrix::Z_GATE); + let iter_set = [IGate, XGate, YGate, ZGate]; + let kron_set: [Array2; 16] = [ + kron(&i_matrix, &i_matrix), + kron(&x_matrix, &i_matrix), + kron(&y_matrix, &i_matrix), + kron(&z_matrix, &i_matrix), + kron(&i_matrix, &x_matrix), + kron(&x_matrix, &x_matrix), + kron(&y_matrix, &x_matrix), + kron(&z_matrix, &x_matrix), + kron(&i_matrix, &y_matrix), + kron(&x_matrix, &y_matrix), + kron(&y_matrix, &y_matrix), + kron(&z_matrix, &y_matrix), + kron(&i_matrix, &z_matrix), + kron(&x_matrix, &z_matrix), + kron(&y_matrix, &z_matrix), + kron(&z_matrix, &z_matrix), + ]; + for (i_idx, i) in iter_set.iter().enumerate() { + for (j_idx, j) in iter_set.iter().enumerate() { + let before_matrix = kron_set[i_idx * 4 + j_idx].view(); + let half_twirled_matrix = gate_matrix.dot(&before_matrix); + for (k_idx, k) in iter_set.iter().enumerate() { + for (l_idx, l) in iter_set.iter().enumerate() { + let after_matrix = kron_set[k_idx * 4 + l_idx].view(); + let twirled_matrix = after_matrix.dot(&half_twirled_matrix); + let norm: f64 = diff_frob_norm_sq(twirled_matrix.view(), gate_matrix); + if norm.abs() < 1e-15 { + out_vec.push(([*i, *j, *k, *l], 0.)); + } else if (norm - 16.).abs() < 1e-15 { + out_vec.push(([*i, *j, *k, *l], PI)); + } + } + } + } + } + out_vec +} + +fn twirl_gate( + py: Python, + circ: &CircuitData, + rng: &mut Pcg64Mcg, + out_circ: &mut CircuitData, + twirl_set: &[([StandardGate; 4], f64)], + inst: &PackedInstruction, +) -> PyResult<()> { + let qubits = circ.get_qargs(inst.qubits); + let (twirl, twirl_phase) = twirl_set.choose(rng).unwrap(); + let bit_zero = out_circ.add_qargs(std::slice::from_ref(&qubits[0])); + let bit_one = out_circ.add_qargs(std::slice::from_ref(&qubits[1])); + out_circ.push( + py, + PackedInstruction { + op: PackedOperation::from_standard(twirl[0]), + qubits: bit_zero, + clbits: circ.cargs_interner().get_default(), + params: None, + extra_attrs: ExtraInstructionAttributes::new(None, None, None, None), + #[cfg(feature = "cache_pygates")] + py_op: std::cell::OnceCell::new(), + }, + )?; + out_circ.push( + py, + PackedInstruction { + op: PackedOperation::from_standard(twirl[1]), + qubits: bit_one, + clbits: circ.cargs_interner().get_default(), + params: None, + extra_attrs: ExtraInstructionAttributes::new(None, None, None, None), + #[cfg(feature = "cache_pygates")] + py_op: std::cell::OnceCell::new(), + }, + )?; + + out_circ.push(py, inst.clone())?; + out_circ.push( + py, + PackedInstruction { + op: PackedOperation::from_standard(twirl[2]), + qubits: bit_zero, + clbits: circ.cargs_interner().get_default(), + params: None, + extra_attrs: ExtraInstructionAttributes::new(None, None, None, None), + #[cfg(feature = "cache_pygates")] + py_op: std::cell::OnceCell::new(), + }, + )?; + out_circ.push( + py, + PackedInstruction { + op: PackedOperation::from_standard(twirl[3]), + qubits: bit_one, + clbits: circ.cargs_interner().get_default(), + params: None, + extra_attrs: ExtraInstructionAttributes::new(None, None, None, None), + #[cfg(feature = "cache_pygates")] + py_op: std::cell::OnceCell::new(), + }, + )?; + + if *twirl_phase != 0. { + out_circ.add_global_phase(py, &Param::Float(*twirl_phase))?; + } + Ok(()) +} + +type CustomGateTwirlingMap = HashMap>; + +fn generate_twirled_circuit( + py: Python, + circ: &CircuitData, + rng: &mut Pcg64Mcg, + twirling_mask: u8, + custom_gate_map: Option<&CustomGateTwirlingMap>, + optimizer_target: Option<&Target>, +) -> PyResult { + let mut out_circ = CircuitData::clone_empty_like(circ, None); + + for inst in circ.data() { + if let Some(custom_gate_map) = custom_gate_map { + if let Some(twirling_set) = custom_gate_map.get(inst.op.name()) { + twirl_gate(py, circ, rng, &mut out_circ, twirling_set.as_slice(), inst)?; + continue; + } + } + match inst.op.view() { + OperationRef::Standard(gate) => match gate { + StandardGate::CXGate => { + if twirling_mask & CX_MASK != 0 { + twirl_gate(py, circ, rng, &mut out_circ, TWIRLING_SETS[0], inst)?; + } else { + out_circ.push(py, inst.clone())?; + } + } + StandardGate::CZGate => { + if twirling_mask & CZ_MASK != 0 { + twirl_gate(py, circ, rng, &mut out_circ, TWIRLING_SETS[1], inst)?; + } else { + out_circ.push(py, inst.clone())?; + } + } + StandardGate::ECRGate => { + if twirling_mask & ECR_MASK != 0 { + twirl_gate(py, circ, rng, &mut out_circ, TWIRLING_SETS[2], inst)?; + } else { + out_circ.push(py, inst.clone())?; + } + } + StandardGate::ISwapGate => { + if twirling_mask & ISWAP_MASK != 0 { + twirl_gate(py, circ, rng, &mut out_circ, TWIRLING_SETS[3], inst)?; + } else { + out_circ.push(py, inst.clone())?; + } + } + _ => out_circ.push(py, inst.clone())?, + }, + OperationRef::Instruction(py_inst) => { + if py_inst.control_flow() { + let new_blocks: PyResult> = py_inst + .blocks() + .iter() + .map(|block| -> PyResult { + let new_block = generate_twirled_circuit( + py, + block, + rng, + twirling_mask, + custom_gate_map, + optimizer_target, + )?; + Ok(new_block.into_py(py)) + }) + .collect(); + let new_blocks = new_blocks?; + let blocks_list = PyList::new_bound( + py, + new_blocks.iter().map(|block| { + QUANTUM_CIRCUIT + .get_bound(py) + .call_method1(intern!(py, "_from_circuit_data"), (block,)) + .unwrap() + }), + ); + + let new_inst_obj = py_inst + .instruction + .bind(py) + .call_method1(intern!(py, "replace_blocks"), (blocks_list,))? + .unbind(); + let new_inst = PyInstruction { + qubits: py_inst.qubits, + clbits: py_inst.clbits, + params: py_inst.params, + op_name: py_inst.op_name.clone(), + control_flow: true, + instruction: new_inst_obj.clone_ref(py), + }; + let new_inst = PackedInstruction { + op: PackedOperation::from_instruction(Box::new(new_inst)), + qubits: inst.qubits, + clbits: inst.clbits, + params: Some(Box::new( + new_blocks + .iter() + .map(|x| Param::Obj(x.into_py(py))) + .collect::>(), + )), + extra_attrs: inst.extra_attrs.clone(), + #[cfg(feature = "cache_pygates")] + py_op: std::cell::OnceCell::new(), + }; + #[cfg(feature = "cache_pygates")] + new_inst.py_op.set(new_inst_obj).unwrap(); + out_circ.push(py, new_inst)?; + } else { + out_circ.push(py, inst.clone())?; + } + } + _ => { + out_circ.push(py, inst.clone())?; + } + } + } + if optimizer_target.is_some() { + let mut dag = DAGCircuit::from_circuit_data(py, out_circ, false)?; + optimize_1q_gates_decomposition(py, &mut dag, optimizer_target, None, None)?; + dag_to_circuit(py, &dag, false) + } else { + Ok(out_circ) + } +} + +#[pyfunction] +#[pyo3(signature=(circ, twirled_gate=None, custom_twirled_gates=None, seed=None, num_twirls=1, optimizer_target=None))] +pub(crate) fn twirl_circuit( + py: Python, + circ: &CircuitData, + twirled_gate: Option>, + custom_twirled_gates: Option>, + seed: Option, + num_twirls: usize, + optimizer_target: Option<&Target>, +) -> PyResult> { + let mut rng = match seed { + Some(seed) => Pcg64Mcg::seed_from_u64(seed), + None => Pcg64Mcg::from_entropy(), + }; + let twirling_mask: u8 = match twirled_gate { + Some(gates) => { + let mut out_mask = 0; + for gate in gates { + let new_mask = match gate { + StandardGate::CXGate => CX_MASK, + StandardGate::CZGate => CZ_MASK, + StandardGate::ECRGate => ECR_MASK, + StandardGate::ISwapGate => ISWAP_MASK, + _ => { + return Err(QiskitError::new_err( + format!("Provided gate to twirl, {}, is not currently supported you can only use cx, cz, ecr or iswap.", gate.name()) + )) + } + }; + out_mask |= new_mask; + } + out_mask + } + None => { + if custom_twirled_gates.is_none() { + 15 + } else { + 0 + } + } + }; + let custom_gate_twirling_sets: Option = + custom_twirled_gates.map(|gates| { + gates + .into_iter() + .filter_map(|gate| { + if gate.operation.num_qubits() != 2 { + return Some(Err(QiskitError::new_err( + format!( + "The provided gate to twirl {} operates on an invalid number of qubits {}, it can only be a two qubit gate", + gate.operation.name(), + gate.operation.num_qubits(), + ) + ))) + } + if gate.operation.num_params() != 0 { + return Some(Err(QiskitError::new_err( + format!( + "The provided gate to twirl {} takes a parameter, it can only be an unparameterized gate", + gate.operation.name(), + ) + ))) + } + let matrix = gate.operation.matrix(&gate.params); + if let Some(matrix) = matrix { + let twirl_set = generate_twirling_set(matrix.view()); + if twirl_set.is_empty() { + None + } else { + Some(Ok((gate.operation.name().to_string(), twirl_set))) + } + } else { + Some(Err(QiskitError::new_err( + format!("Provided gate to twirl, {}, does not have a matrix defined and can't be twirled", gate.operation.name()) + ))) + } + }) + .collect() + }).transpose()?; + (0..num_twirls) + .map(|_| { + generate_twirled_circuit( + py, + circ, + &mut rng, + twirling_mask, + custom_gate_twirling_sets.as_ref(), + optimizer_target, + ) + }) + .collect() +} + +pub fn twirling(m: &Bound) -> PyResult<()> { + m.add_wrapped(wrap_pyfunction!(twirl_circuit))?; + Ok(()) +} diff --git a/crates/circuit/src/circuit_data.rs b/crates/circuit/src/circuit_data.rs index f680d96e47a8..f24277e18c7e 100644 --- a/crates/circuit/src/circuit_data.rs +++ b/crates/circuit/src/circuit_data.rs @@ -17,6 +17,7 @@ use crate::bit_data::BitData; use crate::circuit_instruction::{ CircuitInstruction, ExtraInstructionAttributes, OperationFromPython, }; +use crate::dag_circuit::add_global_phase; use crate::imports::{ANNOTATED_OPERATION, CLBIT, QUANTUM_CIRCUIT, QUBIT}; use crate::interner::{Interned, Interner}; use crate::operations::{Operation, OperationRef, Param, StandardGate}; @@ -1285,6 +1286,11 @@ impl CircuitData { self.qargs_interner().get(index) } + /// Insert qargs into the interner and return the interned value + pub fn add_qargs(&mut self, qubits: &[Qubit]) -> Interned<[Qubit]> { + self.qargs_interner.insert(qubits) + } + /// Unpacks from InternerIndex to `[Clbit]` pub fn get_cargs(&self, index: Interned<[Clbit]>) -> &[Clbit] { self.cargs_interner().get(index) @@ -1498,6 +1504,60 @@ impl CircuitData { pub fn get_parameter_by_uuid(&self, uuid: ParameterUuid) -> Option<&Py> { self.param_table.py_parameter_by_uuid(uuid) } + + /// Get an immutable view of the instructions in the circuit data + pub fn data(&self) -> &[PackedInstruction] { + &self.data + } + + /// Clone an empty CircuitData from a given reference. + /// + /// The new copy will have the global properties from the provided `CircuitData`. + /// The the bit data fields and interners, global phase, etc will be copied to + /// the new returned `CircuitData`, but the `data` field's instruction list will + /// be empty. This can be useful for scenarios where you want to rebuild a copy + /// of the circuit from a reference but insert new gates in the middle. + /// + /// # Arguments + /// + /// * other - The other `CircuitData` to clone an empty `CircuitData` from. + /// * capacity - The capacity for instructions to use in the output `CircuitData` + /// If `None` the length of `other` will be used, if `Some` the integer + /// value will be used as the capacity. + pub fn clone_empty_like(other: &Self, capacity: Option) -> Self { + CircuitData { + data: Vec::with_capacity(capacity.unwrap_or(other.data.len())), + qargs_interner: other.qargs_interner.clone(), + cargs_interner: other.cargs_interner.clone(), + qubits: other.qubits.clone(), + clbits: other.clbits.clone(), + param_table: ParameterTable::new(), + global_phase: other.global_phase.clone(), + } + } + + /// Append a PackedInstruction to the circuit data. + /// + /// # Arguments + /// + /// * packed: The new packed instruction to insert to the end of the CircuitData + /// The qubits and clbits **must** already be present in the interner for this + /// function to work. If they are not this will corrupt the circuit. + pub fn push(&mut self, py: Python, packed: PackedInstruction) -> PyResult<()> { + let new_index = self.data.len(); + self.data.push(packed); + self.track_instruction_parameters(py, new_index) + } + + /// Add a param to the current global phase of the circuit + pub fn add_global_phase(&mut self, py: Python, value: &Param) -> PyResult<()> { + match value { + Param::Obj(_) => Err(PyTypeError::new_err( + "Invalid parameter type, only float and parameter expression are supported", + )), + _ => self.set_global_phase(py, add_global_phase(py, &self.global_phase, value)?), + } + } } /// Helper struct for `assign_parameters` to allow use of `Param::extract_no_coerce` in diff --git a/crates/circuit/src/dag_circuit.rs b/crates/circuit/src/dag_circuit.rs index 10551963b6e1..eb49130fcd37 100644 --- a/crates/circuit/src/dag_circuit.rs +++ b/crates/circuit/src/dag_circuit.rs @@ -7009,7 +7009,7 @@ impl DAGCircuit { /// Add to global phase. Global phase can only be Float or ParameterExpression so this /// does not handle the full possibility of parameter values. -fn add_global_phase(py: Python, phase: &Param, other: &Param) -> PyResult { +pub(crate) fn add_global_phase(py: Python, phase: &Param, other: &Param) -> PyResult { Ok(match [phase, other] { [Param::Float(a), Param::Float(b)] => Param::Float(a + b), [Param::Float(a), Param::ParameterExpression(b)] => Param::ParameterExpression( diff --git a/crates/pyext/src/lib.rs b/crates/pyext/src/lib.rs index 330705ea3305..d8e59e04e51e 100644 --- a/crates/pyext/src/lib.rs +++ b/crates/pyext/src/lib.rs @@ -62,6 +62,7 @@ fn _accelerate(m: &Bound) -> PyResult<()> { add_submodule(m, ::qiskit_accelerate::stochastic_swap::stochastic_swap, "stochastic_swap")?; add_submodule(m, ::qiskit_accelerate::synthesis::synthesis, "synthesis")?; add_submodule(m, ::qiskit_accelerate::target_transpiler::target, "target")?; + add_submodule(m, ::qiskit_accelerate::twirling::twirling, "twirling")?; add_submodule(m, ::qiskit_accelerate::two_qubit_decompose::two_qubit_decompose, "two_qubit_decompose")?; add_submodule(m, ::qiskit_accelerate::unitary_synthesis::unitary_synthesis, "unitary_synthesis")?; add_submodule(m, ::qiskit_accelerate::uc_gate::uc_gate, "uc_gate")?; diff --git a/qiskit/__init__.py b/qiskit/__init__.py index 9f3576978432..760dee682322 100644 --- a/qiskit/__init__.py +++ b/qiskit/__init__.py @@ -106,6 +106,7 @@ sys.modules["qiskit._accelerate.inverse_cancellation"] = _accelerate.inverse_cancellation sys.modules["qiskit._accelerate.check_map"] = _accelerate.check_map sys.modules["qiskit._accelerate.filter_op_nodes"] = _accelerate.filter_op_nodes +sys.modules["qiskit._accelerate.twirling"] = _accelerate.twirling sys.modules["qiskit._accelerate.high_level_synthesis"] = _accelerate.high_level_synthesis sys.modules["qiskit._accelerate.remove_identity_equiv"] = _accelerate.remove_identity_equiv diff --git a/qiskit/circuit/__init__.py b/qiskit/circuit/__init__.py index 542c927ee175..ab7ff9a84160 100644 --- a/qiskit/circuit/__init__.py +++ b/qiskit/circuit/__init__.py @@ -788,10 +788,10 @@ .. code-block:: text ┌─────────┐ ┌─────────┐ ┌─────────┐ - q_0: ┤ Rz(0.5) ├──■──┤ Rz(1.2) ├──■── q_0: ┤ Rz(1.7) ├ - └─────────┘┌─┴─┐└──┬───┬──┘┌─┴─┐ = └──┬───┬──┘ - q_1: ───────────┤ X ├───┤ X ├───┤ X ├ q_1: ───┤ X ├─── - └───┘ └───┘ └───┘ └───┘ + q_0: ┤ Rz(0.5) ├──■──┤ Rz(1.2) ├──■── q_0: ┤ Rz(1.7) ├ + └─────────┘┌─┴─┐└──┬───┬──┘┌─┴─┐ = └──┬───┬──┘ + q_1: ───────────┤ X ├───┤ X ├───┤ X ├ q_1: ───┤ X ├─── + └───┘ └───┘ └───┘ └───┘ Performing these optimizations are part of the transpiler, but the tools to investigate commutations are available in the :class:`CommutationChecker`. @@ -801,7 +801,7 @@ CommutationChecker - + .. _circuit-custom-gates: Creating custom instructions @@ -1047,6 +1047,24 @@ def __array__(self, dtype=None, copy=None): .. autofunction:: random_circuit .. currentmodule:: qiskit.circuit +Apply Pauli twirling to a circuit +--------------------------------- + +There are two primary types of noise when executing quantum circuits. The first is stochastic, +or incoherent, noise that is mainly due to the unwanted interaction between the quantum processor +and the external environment in which it resides. The second is known as coherent error, and these +errors arise due to imperfect control of a quantum system. This can be unwanted terms in a system +Hamiltonian, i.e. incorrect unitary evolution, or errors from incorrect temporal control of the +quantum system, which includes things like incorrect pulse-shapes for gates. + +Pauli twirling is a quantum error suppression technique that uses randomization to shape coherent +error into stochastic errors by combining the results from many random, but logically equivalent +circuits, together. Qiskit provides a function to apply Pauli twirling to a given circuit for +standard two qubit gates. For more details you can refer to the documentation of the function +below: + +.. autofunction:: qiskit.circuit.pauli_twirl_2q_gates + Exceptions ========== @@ -1292,3 +1310,4 @@ def __array__(self, dtype=None, copy=None): ) from .annotated_operation import AnnotatedOperation, InverseModifier, ControlModifier, PowerModifier +from .twirling import pauli_twirl_2q_gates diff --git a/qiskit/circuit/quantumcircuit.py b/qiskit/circuit/quantumcircuit.py index 5ffd352c595f..75bd85d88ce9 100644 --- a/qiskit/circuit/quantumcircuit.py +++ b/qiskit/circuit/quantumcircuit.py @@ -3752,39 +3752,13 @@ def copy_empty_like( f"invalid name for a circuit: '{name}'. The name must be a string or 'None'." ) cpy = _copy.copy(self) - # copy registers correctly, in copy.copy they are only copied via reference - cpy.qregs = self.qregs.copy() - cpy.cregs = self.cregs.copy() - cpy._builder_api = _OuterCircuitScopeInterface(cpy) - cpy._ancillas = self._ancillas.copy() - cpy._qubit_indices = self._qubit_indices.copy() - cpy._clbit_indices = self._clbit_indices.copy() - - if vars_mode == "alike": - # Note that this causes the local variables to be uninitialised, because the stores are - # not copied. This can leave the circuit in a potentially dangerous state for users if - # they don't re-add initializer stores. - cpy._vars_local = self._vars_local.copy() - cpy._vars_input = self._vars_input.copy() - cpy._vars_capture = self._vars_capture.copy() - elif vars_mode == "captures": - cpy._vars_local = {} - cpy._vars_input = {} - cpy._vars_capture = {var.name: var for var in self.iter_vars()} - elif vars_mode == "drop": - cpy._vars_local = {} - cpy._vars_input = {} - cpy._vars_capture = {} - else: # pragma: no cover - raise ValueError(f"unknown vars_mode: '{vars_mode}'") + + _copy_metadata(self, cpy, vars_mode) cpy._data = CircuitData( self._data.qubits, self._data.clbits, global_phase=self._data.global_phase ) - cpy._calibrations = _copy.deepcopy(self._calibrations) - cpy._metadata = _copy.deepcopy(self._metadata) - if name: cpy.name = name return cpy @@ -6820,3 +6794,34 @@ def _bit_argument_conversion_scalar(specifier, bit_sequence, bit_set, type_): else f"Invalid bit index: '{specifier}' of type '{type(specifier)}'" ) raise CircuitError(message) + + +def _copy_metadata(original, cpy, vars_mode): + # copy registers correctly, in copy.copy they are only copied via reference + cpy.qregs = original.qregs.copy() + cpy.cregs = original.cregs.copy() + cpy._builder_api = _OuterCircuitScopeInterface(cpy) + cpy._ancillas = original._ancillas.copy() + cpy._qubit_indices = original._qubit_indices.copy() + cpy._clbit_indices = original._clbit_indices.copy() + + if vars_mode == "alike": + # Note that this causes the local variables to be uninitialised, because the stores are + # not copied. This can leave the circuit in a potentially dangerous state for users if + # they don't re-add initializer stores. + cpy._vars_local = original._vars_local.copy() + cpy._vars_input = original._vars_input.copy() + cpy._vars_capture = original._vars_capture.copy() + elif vars_mode == "captures": + cpy._vars_local = {} + cpy._vars_input = {} + cpy._vars_capture = {var.name: var for var in original.iter_vars()} + elif vars_mode == "drop": + cpy._vars_local = {} + cpy._vars_input = {} + cpy._vars_capture = {} + else: # pragma: no cover + raise ValueError(f"unknown vars_mode: '{vars_mode}'") + + cpy._calibrations = _copy.deepcopy(original._calibrations) + cpy._metadata = _copy.deepcopy(original._metadata) diff --git a/qiskit/circuit/twirling.py b/qiskit/circuit/twirling.py new file mode 100644 index 000000000000..9cea055ddf48 --- /dev/null +++ b/qiskit/circuit/twirling.py @@ -0,0 +1,145 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2024 +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""The twirling module.""" + +from __future__ import annotations +import typing + +from qiskit._accelerate.twirling import twirl_circuit as twirl_rs +from qiskit.circuit.quantumcircuit import QuantumCircuit, _copy_metadata +from qiskit.circuit.gate import Gate +from qiskit.circuit.library.standard_gates import CXGate, ECRGate, CZGate, iSwapGate +from qiskit.exceptions import QiskitError + +if typing.TYPE_CHECKING: + from qiskit.transpiler.target import Target + + +NAME_TO_CLASS = { + "cx": CXGate._standard_gate, + "ecr": ECRGate._standard_gate, + "cz": CZGate._standard_gate, + "iswap": iSwapGate._standard_gate, +} + + +def pauli_twirl_2q_gates( + circuit: QuantumCircuit, + twirling_gate: None | str | Gate | list[str] | list[Gate] = None, + seed: int | None = None, + num_twirls: int | None = None, + target: Target | None = None, +) -> QuantumCircuit | list[QuantumCircuit]: + """Create copies of a given circuit with Pauli twirling applied around specified two qubit + gates. + + If you're running this function with the intent to twirl a circuit to run on hardware this + may not be the most efficient way to perform twirling. Especially if the hardware vendor + has implemented the :mod:`.primitives` execution interface with :class:`.SamplerV2` and + :class:`.EstimatorV2` this most likely is not the best way to apply twirling to your + circuit and you'll want to refer to the implementation of :class:`.SamplerV2` and/or + :class:`.EstimatorV2` for the specified hardware vendor. + + If the intent of this function is to be run after :func:`.transpile` or + :meth:`.PassManager.run` the optional ``target`` argument can be used + so that the inserted 1 qubit Pauli gates are synthesized to be + compatible with the given :class:`.Target` so the output circuit(s) are + still compatible. + + Args: + circuit: The circuit to twirl + twirling_gate: The gate to twirl, defaults to `None` which means twirl all default gates: + :class:`.CXGate`, :class:`.CZGate`, :class:`.ECRGate`, and :class:`.iSwapGate`. + If supplied it can either be a single gate or a list of gates either as either a gate + object or its string name. Currently only the names `"cx"`, `"cz"`, `"ecr"`, and + `"iswap"` are supported. If a gate object is provided outside the default gates it must + have a matrix defined from its :class:`~.Gate.to_matrix` method for the gate to potentially + be twirled. If a valid twirling configuration can't be computed that particular gate will + be silently ignored and not twirled. + seed: An integer seed for the random number generator used internally by this function. + If specified this must be between 0 and 18,446,744,073,709,551,615. + num_twirls: The number of twirling circuits to build. This defaults to ``None`` and will return + a single circuit. If it is an integer a list of circuits with `num_twirls` circuits + will be returned. + target: If specified an :class:`.Target` instance to use for running single qubit decomposition + as part of the Pauli twirling to optimize and map the pauli gates added to the circuit + to the specified target. + + Returns: + A copy of the given circuit with Pauli twirling applied to each + instance of the specified twirling gate. + """ + custom_gates = None + if isinstance(twirling_gate, str): + gate = NAME_TO_CLASS.get(twirling_gate, None) + if gate is None: + raise QiskitError(f"The specified gate name {twirling_gate} is not supported") + twirling_std_gate = [gate] + elif isinstance(twirling_gate, list): + custom_gates = [] + twirling_std_gate = [] + for gate in twirling_gate: + if isinstance(gate, str): + gate = NAME_TO_CLASS.get(gate, None) + if gate is None: + raise QiskitError(f"The specified gate name {twirling_gate} is not supported") + twirling_std_gate.append(gate) + else: + twirling_gate = getattr(gate, "_standard_gate", None) + + if twirling_gate is None: + custom_gates.append(gate) + else: + if twirling_gate in NAME_TO_CLASS.values(): + twirling_std_gate.append(twirling_gate) + else: + custom_gates.append(gate) + if not custom_gates: + custom_gates = None + if not twirling_std_gate: + twirling_std_gate = None + elif twirling_gate is not None: + std_gate = getattr(twirling_gate, "_standard_gate", None) + if std_gate is None: + twirling_std_gate = None + custom_gates = [twirling_gate] + else: + if std_gate in NAME_TO_CLASS.values(): + twirling_std_gate = [std_gate] + else: + twirling_std_gate = None + custom_gates = [twirling_gate] + else: + twirling_std_gate = twirling_gate + out_twirls = num_twirls + if out_twirls is None: + out_twirls = 1 + new_data = twirl_rs( + circuit._data, + twirling_std_gate, + custom_gates, + seed, + out_twirls, + target, + ) + if num_twirls is not None: + out_list = [] + for circ in new_data: + new_circ = QuantumCircuit._from_circuit_data(circ) + _copy_metadata(circuit, new_circ, "alike") + out_list.append(new_circ) + return out_list + else: + out_circ = QuantumCircuit._from_circuit_data(new_data[0]) + _copy_metadata(circuit, out_circ, "alike") + return out_circ diff --git a/releasenotes/notes/add-twirl-circuit-ff4d4437190551bc.yaml b/releasenotes/notes/add-twirl-circuit-ff4d4437190551bc.yaml new file mode 100644 index 000000000000..46e7701e26ac --- /dev/null +++ b/releasenotes/notes/add-twirl-circuit-ff4d4437190551bc.yaml @@ -0,0 +1,17 @@ +--- +features_circuits: + - | + Added a new circuit manipulation function :func:`.pauli_twirl_2q_gates` that can be used to apply + Pauli twirling to a given circuit. This only works for twirling a fixed set of two-qubit + gates, currently :class:`.CXGate`, :class:`.ECRGate`, :class:`.CZGate`, :class:`.iSwapGate`. + For example: + + .. plot:: + :include-source: + + from qiskit.circuit import QuantumCircuit, pauli_twirl_2q_gates + + qc = QuantumCircuit(2) + qc.cx(0, 1) + twirled_circuit = pauli_twirl_2q_gates(qc, seed=123456) + twirled_circuit.draw("mpl") diff --git a/test/benchmarks/manipulate.py b/test/benchmarks/manipulate.py index 0043c6c59fd4..a35f316238ea 100644 --- a/test/benchmarks/manipulate.py +++ b/test/benchmarks/manipulate.py @@ -15,124 +15,13 @@ # pylint: disable=unused-wildcard-import,wildcard-import,undefined-variable import os -import numpy as np from qiskit import QuantumCircuit -from qiskit.converters import circuit_to_dag -from qiskit.circuit import CircuitInstruction, Qubit, library -from qiskit.dagcircuit import DAGCircuit +from qiskit.circuit import twirl_circuit from qiskit.passmanager import PropertySet from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager from .utils import multi_control_circuit -GATES = { - "id": library.IGate(), - "x": library.XGate(), - "y": library.YGate(), - "z": library.ZGate(), - "cx": library.CXGate(), - "cz": library.CZGate(), -} - -TWIRLING_SETS_NAMES = { - "cx": [ - ["id", "z", "z", "z"], - ["id", "x", "id", "x"], - ["id", "y", "z", "y"], - ["id", "id", "id", "id"], - ["z", "x", "z", "x"], - ["z", "y", "id", "y"], - ["z", "id", "z", "id"], - ["z", "z", "id", "z"], - ["x", "y", "y", "z"], - ["x", "id", "x", "x"], - ["x", "z", "y", "y"], - ["x", "x", "x", "id"], - ["y", "id", "y", "x"], - ["y", "z", "x", "y"], - ["y", "x", "y", "id"], - ["y", "y", "x", "z"], - ], - "cz": [ - ["id", "z", "id", "z"], - ["id", "x", "z", "x"], - ["id", "y", "z", "y"], - ["id", "id", "id", "id"], - ["z", "x", "id", "x"], - ["z", "y", "id", "y"], - ["z", "id", "z", "id"], - ["z", "z", "z", "z"], - ["x", "y", "y", "x"], - ["x", "id", "x", "z"], - ["x", "z", "x", "id"], - ["x", "x", "y", "y"], - ["y", "id", "y", "z"], - ["y", "z", "y", "id"], - ["y", "x", "x", "y"], - ["y", "y", "x", "x"], - ], -} -TWIRLING_SETS = { - key: [[GATES[name] for name in twirl] for twirl in twirls] - for key, twirls in TWIRLING_SETS_NAMES.items() -} - - -def _dag_from_twirl(gate_2q, twirl): - dag = DAGCircuit() - # or use QuantumRegister - doesn't matter - qubits = (Qubit(), Qubit()) - dag.add_qubits(qubits) - dag.apply_operation_back(twirl[0], (qubits[0],), (), check=False) - dag.apply_operation_back(twirl[1], (qubits[1],), (), check=False) - dag.apply_operation_back(gate_2q, qubits, (), check=False) - dag.apply_operation_back(twirl[2], (qubits[0],), (), check=False) - dag.apply_operation_back(twirl[3], (qubits[1],), (), check=False) - return dag - - -def circuit_twirl(qc, twirled_gate="cx", seed=None): - rng = np.random.default_rng(seed) - twirl_set = TWIRLING_SETS.get(twirled_gate, []) - - out = qc.copy_empty_like() - for instruction in qc.data: - if instruction.operation.name != twirled_gate: - out._append(instruction) - else: - # We could also scan through `qc` outside the loop to know how many - # twirled gates we'll be dealing with, and RNG the integers ahead of - # time - that'll be faster depending on what percentage of gates are - # twirled, and how much the Numpy overhead is. - twirls = twirl_set[rng.integers(len(twirl_set))] - control, target = instruction.qubits - out._append(CircuitInstruction(twirls[0], (control,), ())) - out._append(CircuitInstruction(twirls[1], (target,), ())) - out._append(instruction) - out._append(CircuitInstruction(twirls[2], (control,), ())) - out._append(CircuitInstruction(twirls[3], (target,), ())) - return out - - -def dag_twirl(dag, twirled_gate="cx", seed=None): - # This mutates `dag` in place. - rng = np.random.default_rng(seed) - twirl_set = TWIRLING_DAGS.get(twirled_gate, []) - twirled_gate_op = GATES[twirled_gate].base_class - - to_twirl = dag.op_nodes(twirled_gate_op) - twirl_indices = rng.integers(len(twirl_set), size=(len(to_twirl),)) - - for index, op_node in zip(twirl_indices, to_twirl): - dag.substitute_node_with_dag(op_node, twirl_set[index]) - return dag - - -TWIRLING_DAGS = { - key: [_dag_from_twirl(GATES[key], twirl) for twirl in twirls] - for key, twirls in TWIRLING_SETS.items() -} - class TestCircuitManipulate: def setup(self): @@ -149,7 +38,7 @@ def time_DTC100_twirling(self): """Perform Pauli-twirling on a 100Q QV circuit """ - out = circuit_twirl(self.dtc_qc) + out = twirl_circuit(self.dtc_qc, seed=12345678942) return out def time_multi_control_decompose(self): @@ -168,11 +57,3 @@ def time_QV100_basis_change(self): self.translate.property_set = PropertySet() out = self.translate.run(self.qv_qc) return out - - def time_DTC100_twirling_dag(self): - """Perform Pauli-twirling on a 100Q QV - circuit - """ - self.translate.property_set = PropertySet() - out = self.translate.run(self.qv_qc) - return circuit_to_dag(out) diff --git a/test/python/circuit/test_twirling.py b/test/python/circuit/test_twirling.py new file mode 100644 index 000000000000..59e6b0c41fe2 --- /dev/null +++ b/test/python/circuit/test_twirling.py @@ -0,0 +1,212 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2023. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""Test Qiskit's AnnotatedOperation class.""" + +import ddt +import numpy as np + +from qiskit.circuit import QuantumCircuit, pauli_twirl_2q_gates, Gate +from qiskit.circuit.library import ( + CXGate, + ECRGate, + CZGate, + iSwapGate, + SwapGate, + PermutationGate, + XGate, + CCXGate, + RZXGate, +) +from qiskit.circuit.random import random_circuit +from qiskit.exceptions import QiskitError +from qiskit.quantum_info import Operator +from qiskit.transpiler.target import Target +from test import QiskitTestCase # pylint: disable=wrong-import-order + + +@ddt.ddt +class TestTwirling(QiskitTestCase): + """Testing qiskit.circuit.twirl_circuit""" + + @ddt.data(CXGate, ECRGate, CZGate, iSwapGate) + def test_twirl_circuit_equiv(self, gate): + """Test the twirled circuit is equivalent.""" + qc = QuantumCircuit(2) + qc.append(gate(), (0, 1)) + for i in range(100): + with self.subTest(i): + res = pauli_twirl_2q_gates(qc, gate, i) + np.testing.assert_allclose( + Operator(qc), Operator(res), err_msg=f"gate: {gate} not equiv to\n{res}" + ) + self.assertNotEqual(res, qc) + # Assert we have more than just a 2q gate in the circuit + self.assertGreater(len(res.count_ops()), 1) + + def test_twirl_circuit_None(self): + """Test the default twirl all gates.""" + qc = QuantumCircuit(2) + qc.cx(0, 1) + qc.cz(0, 1) + qc.ecr(0, 1) + qc.iswap(0, 1) + res = pauli_twirl_2q_gates(qc, seed=12345) + np.testing.assert_allclose( + Operator(qc), Operator(res), err_msg=f"{qc}\nnot equiv to\n{res}" + ) + self.assertNotEqual(res, qc) + self.assertEqual(sum(res.count_ops().values()), 20) + + def test_twirl_circuit_list(self): + """Test twirling for a circuit list of gates to twirl.""" + qc = QuantumCircuit(2) + qc.cx(0, 1) + qc.cz(0, 1) + qc.ecr(0, 1) + qc.iswap(0, 1) + res = pauli_twirl_2q_gates(qc, twirling_gate=["cx", iSwapGate()], seed=12345) + np.testing.assert_allclose( + Operator(qc), Operator(res), err_msg=f"{qc}\nnot equiv to\n{res}" + ) + self.assertNotEqual(res, qc) + self.assertEqual(sum(res.count_ops().values()), 12) + + @ddt.data(CXGate, ECRGate, CZGate, iSwapGate) + def test_many_twirls_equiv(self, gate): + """Test the twirled circuits are equivalent if num_twirls>1.""" + qc = QuantumCircuit(2) + qc.append(gate(), (0, 1)) + res = pauli_twirl_2q_gates(qc, gate, seed=424242, num_twirls=1000) + for twirled_circuit in res: + np.testing.assert_allclose( + Operator(qc), Operator(twirled_circuit), err_msg=f"gate: {gate} not equiv to\n{res}" + ) + self.assertNotEqual(twirled_circuit, qc) + + def test_invalid_gate(self): + """Test an error is raised with a non-standard gate.""" + + class MyGate(Gate): + """Custom gate.""" + + def __init__(self): + super().__init__("custom", num_qubits=2, params=[]) + + qc = QuantumCircuit(2) + qc.append(MyGate(), (0, 1)) + + with self.assertRaises(QiskitError): + pauli_twirl_2q_gates(qc, twirling_gate=MyGate()) + + def test_custom_standard_gate(self): + """Test an error is raised with an unsupported standard gate.""" + qc = QuantumCircuit(2) + qc.swap(0, 1) + res = pauli_twirl_2q_gates(qc, twirling_gate=SwapGate()) + np.testing.assert_allclose( + Operator(qc), Operator(res), err_msg=f"gate: {qc} not equiv to\n{res}" + ) + self.assertNotEqual(qc, res) + + def test_invalid_string(self): + """Test an error is raised with an unsupported standard gate.""" + qc = QuantumCircuit(2) + qc.swap(0, 1) + with self.assertRaises(QiskitError): + pauli_twirl_2q_gates(qc, twirling_gate="swap") + + def test_invalid_str_entry_in_list(self): + """Test an error is raised with an unsupported string gate in list.""" + qc = QuantumCircuit(2) + qc.swap(0, 1) + with self.assertRaises(QiskitError): + pauli_twirl_2q_gates(qc, twirling_gate=[CXGate, "swap"]) + + def test_invalid_class_entry_in_list(self): + """Test an error is raised with an unsupported string gate in list.""" + qc = QuantumCircuit(2) + qc.swap(0, 1) + res = pauli_twirl_2q_gates(qc, twirling_gate=[SwapGate(), "cx"]) + np.testing.assert_allclose( + Operator(qc), Operator(res), err_msg=f"gate: {qc} not equiv to\n{res}" + ) + self.assertNotEqual(qc, res) + + @ddt.data(CXGate, ECRGate, CZGate, iSwapGate) + def test_full_circuit(self, gate): + """Test a circuit with a random assortment of gates.""" + qc = random_circuit(5, 25, seed=12345678942) + qc.append(PermutationGate([1, 2, 0]), [0, 1, 2]) + res = pauli_twirl_2q_gates(qc) + np.testing.assert_allclose( + Operator(qc), Operator(res), err_msg=f"gate: {gate} not equiv to\n{res}" + ) + + @ddt.data(CXGate, ECRGate, CZGate, iSwapGate) + def test_control_flow(self, gate): + """Test we twirl inside control flow blocks.""" + qc = QuantumCircuit(2, 1) + with qc.if_test((qc.clbits[0], 0)): + qc.append(gate(), [0, 1]) + res = pauli_twirl_2q_gates(qc) + np.testing.assert_allclose( + Operator(res.data[0].operation.blocks[0]), + Operator(gate()), + err_msg=f"gate: {gate} not equiv to\n{res}", + ) + + def test_metadata_is_preserved(self): + """Test we preserve circuit metadata after twirling.""" + qc = QuantumCircuit(2) + qc.cx(0, 1) + qc.ecr(0, 1) + qc.iswap(0, 1) + qc.cz(0, 1) + qc.metadata = {"is_this_circuit_twirled?": True} + res = pauli_twirl_2q_gates(qc, twirling_gate=CZGate, num_twirls=5) + for out_circ in res: + self.assertEqual(out_circ.metadata, qc.metadata) + + def test_random_circuit_optimized(self): + """Test we run 1q gate optimization if specified.""" + qc = random_circuit(5, 25, seed=1234567842) + qc.barrier() + qc = qc.decompose() + target = Target.from_configuration(basis_gates=["cx", "iswap", "cz", "ecr", "r"]) + res = pauli_twirl_2q_gates(qc, seed=12345678, num_twirls=5, target=target) + for out_circ in res: + self.assertEqual( + Operator(out_circ), + Operator(qc), + f"{qc}\nnot equiv to\n{out_circ}", + ) + count_ops = out_circ.count_ops() + self.assertNotIn("x", count_ops) + self.assertNotIn("y", count_ops) + self.assertNotIn("z", count_ops) + self.assertNotIn("id", count_ops) + self.assertIn("r", count_ops) + + def test_error_on_invalid_qubit_count(self): + """Test an error is raised on non-2q gates.""" + qc = QuantumCircuit(5) + with self.assertRaises(QiskitError): + pauli_twirl_2q_gates(qc, [CCXGate()]) + with self.assertRaises(QiskitError): + pauli_twirl_2q_gates(qc, [XGate()]) + + def test_error_on_parameterized_gate(self): + """Test an error is raised on parameterized 2q gates.""" + qc = QuantumCircuit(5) + with self.assertRaises(QiskitError): + pauli_twirl_2q_gates(qc, [RZXGate(3.24)])