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)])