diff --git a/crates/synthesis/src/two_qubit_decompose/basis_decomposer.rs b/crates/synthesis/src/two_qubit_decompose/basis_decomposer.rs index 42db5797fe9a..9c8a65127c57 100644 --- a/crates/synthesis/src/two_qubit_decompose/basis_decomposer.rs +++ b/crates/synthesis/src/two_qubit_decompose/basis_decomposer.rs @@ -25,7 +25,7 @@ use numpy::{IntoPyArray, ToPyArray}; use pyo3::exceptions::PyValueError; use pyo3::prelude::*; -use super::common::{DEFAULT_FIDELITY, IPZ, TraceToFidelity, rx_matrix, rz_matrix}; +use super::common::{DEFAULT_FIDELITY, HGATE, IPZ, TraceToFidelity, rx_matrix, rz_matrix}; use super::gate_sequence::{TwoQubitGateSequence, TwoQubitSequenceVec}; use super::weyl_decomposition::{__num_basis_gates, _num_basis_gates, TwoQubitWeylDecomposition}; @@ -41,7 +41,7 @@ use qiskit_circuit::bit::ShareableQubit; use qiskit_circuit::circuit_data::{CircuitData, PyCircuitData}; use qiskit_circuit::circuit_instruction::OperationFromPython; use qiskit_circuit::dag_circuit::DAGCircuit; -use qiskit_circuit::gate_matrix::{CX_GATE, H_GATE, ONE_QUBIT_IDENTITY}; +use qiskit_circuit::gate_matrix::{CX_GATE, ONE_QUBIT_IDENTITY}; use qiskit_circuit::instruction::{Instruction, Parameters}; use qiskit_circuit::operations::{Operation, OperationRef, Param, StandardGate}; use qiskit_circuit::packed_instruction::PackedOperation; @@ -56,9 +56,6 @@ use qiskit_util::complex::{C_M_ONE, C_ONE, IM, M_IM, c64}; // Python space. const TWO_QUBIT_SEQUENCE_DEFAULT_CAPACITY: usize = 21; -static HGATE: Matrix2 = - Matrix2::new(H_GATE[0][0], H_GATE[0][1], H_GATE[1][0], H_GATE[1][1]); - static K12R: Matrix2 = Matrix2::new( c64(0., FRAC_1_SQRT_2), c64(FRAC_1_SQRT_2, 0.), diff --git a/crates/synthesis/src/two_qubit_decompose/common.rs b/crates/synthesis/src/two_qubit_decompose/common.rs index b4203ac3c1bb..809420212441 100644 --- a/crates/synthesis/src/two_qubit_decompose/common.rs +++ b/crates/synthesis/src/two_qubit_decompose/common.rs @@ -19,6 +19,7 @@ use numpy::{IntoPyArray, PyArray2, PyReadonlyArray1, PyReadonlyArray2}; use pyo3::prelude::*; use crate::linalg::ndarray_to_faer; +use qiskit_circuit::gate_matrix::{H_GATE, S_GATE, SDG_GATE}; use qiskit_util::alias::GateArray2Q; use qiskit_util::complex::{C_M_ONE, C_ONE, C_ZERO, IM, M_IM, c64}; pub(super) const DEFAULT_FIDELITY: f64 = 1.0 - 1.0e-9; @@ -231,3 +232,14 @@ pub(super) fn ndarray_to_matrix2(view: ArrayView2) -> Matrix2 { pub(super) fn ndarray_to_matrix4(view: ArrayView2) -> Matrix4 { Matrix4::from_row_iterator(view.iter().copied()) } + +pub static HGATE: Matrix2 = + Matrix2::new(H_GATE[0][0], H_GATE[0][1], H_GATE[1][0], H_GATE[1][1]); +pub static SGATE: Matrix2 = + Matrix2::new(S_GATE[0][0], S_GATE[0][1], S_GATE[1][0], S_GATE[1][1]); +pub static SDGGATE: Matrix2 = Matrix2::new( + SDG_GATE[0][0], + SDG_GATE[0][1], + SDG_GATE[1][0], + SDG_GATE[1][1], +); diff --git a/crates/synthesis/src/two_qubit_decompose/controlled_u_decomposer.rs b/crates/synthesis/src/two_qubit_decompose/controlled_u_decomposer.rs index b0ebc9450549..de7718c5ca19 100644 --- a/crates/synthesis/src/two_qubit_decompose/controlled_u_decomposer.rs +++ b/crates/synthesis/src/two_qubit_decompose/controlled_u_decomposer.rs @@ -15,7 +15,7 @@ use num_complex::Complex64; use smallvec::{SmallVec, smallvec}; use std::f64::consts::FRAC_PI_2; -use nalgebra::U2; +use nalgebra::{Matrix2, MatrixView2, U2}; use ndarray::prelude::*; use numpy::PyReadonlyArray2; @@ -31,34 +31,20 @@ use crate::linalg::nalgebra_array_view; use qiskit_circuit::circuit_data::{CircuitData, PyCircuitData}; use qiskit_circuit::circuit_instruction::OperationFromPython; -use qiskit_circuit::gate_matrix::{H_GATE, S_GATE, SDG_GATE}; use qiskit_circuit::operations::{Operation, OperationRef, Param, StandardGate}; use qiskit_circuit::packed_instruction::PackedOperation; use qiskit_circuit::{NoBlocks, Qubit}; -use super::common::DEFAULT_FIDELITY; -use super::gate_sequence::TwoQubitGateSequence; +use super::common::{DEFAULT_FIDELITY, HGATE, SDGGATE, SGATE}; +use super::gate_sequence::{TwoQubitGateSequence, TwoQubitSequenceVec}; use super::weyl_decomposition::{Specialization, TwoQubitWeylDecomposition}; -/// invert 1q gate sequence -fn invert_1q_gate( - gate: (StandardGate, SmallVec<[f64; 3]>), -) -> (PackedOperation, SmallVec<[f64; 3]>) { - let gate_params = gate.1.into_iter().map(Param::Float).collect::>(); - let inv_gate = gate - .0 - .inverse(&gate_params) - .expect("An unexpected standard gate was inverted"); - let inv_gate_params = inv_gate - .1 - .into_iter() - .map(|param| match param { - Param::Float(val) => val, - _ => unreachable!("Parameterized inverse generated from non-parameterized gate."), - }) - .collect::>(); - (inv_gate.0.into(), inv_gate_params) -} +type TwoQubitUnitary = ( + PackedOperation, + SmallVec<[f64; 3]>, + f64, + [Matrix2; 4], +); #[derive(Clone, Debug, FromPyObject)] pub enum RXXEquivalent { @@ -175,11 +161,29 @@ impl TwoQubitControlledUDecomposer { /// RXX equivalent gate as the two-qubit unitary. /// Args: /// angle: Rotation angle (in this case one of the Weyl parameters a, b, or c) + /// is_inv_rxx: Whether the RXX equivalent gate should be inverted or not /// Returns: - /// Circuit: Circuit equivalent to an RXXGate. + /// out_gate_name: An equivalent 2-qubit gate (or its inverse) + /// out_gate_params: The equivalent 2-qubit gate params + /// global_phase: The global phase of the equivalent circuit + /// k_mats: Four 1-qubit gates in the equivalent circuit (before and after the 2-qubit gate) /// Raises: /// QiskitError: If the circuit is not equivalent to an RXXGate. - fn to_rxx_gate(&self, angle: f64) -> PyResult { + /// + /// Example: + /// If Equiv is an RXX equivalent gate and `is_inv_rxx=false` then the output circuit is of the form: + /// ┌─────────┐┌────────┐┌─────────┐ + /// q_0: ┤ k2r_inv ├┤0 ├┤ k1r_inv ├ + /// ├─────────┤│ Equiv │├─────────┤ + /// q_1: ┤ k2l_inv ├┤1 ├┤ k1l_inv ├ + /// └─────────┘└────────┘└─────────┘ + /// If Equiv is an RXX equivalent gate and `is_inv_rxx=true` then the output circuit is of the form: + /// ┌─────┐┌────────────┐┌─────┐ + /// q_0: ┤ k1r ├┤0 ├┤ k2r ├ + /// ├─────┤│ Equiv_inv │├─────┤ + /// q_1: ┤ k1l ├┤1 ├┤ k2l ├ + /// └─────┘└────────────┘└─────┘ + fn to_rxx_gate(&self, angle: f64, is_inv_rxx: bool) -> PyResult { // The user-provided RXX equivalent gate may be locally equivalent to the RXX gate // but with some scaling in the rotation angle. For example, RXX(angle) has Weyl // parameters (angle, 0, 0) for angle in [0, pi/2] but the user provided gate, i.e. @@ -190,40 +194,33 @@ impl TwoQubitControlledUDecomposer { let decomposer_inv = TwoQubitWeylDecomposition::new_inner(mat.view(), Some(DEFAULT_FIDELITY), None)?; - let mut target_1q_basis_list = EulerBasisSet::new(); - target_1q_basis_list.add_basis(self.euler_basis); - // Express the RXX in terms of the user-provided RXX equivalent gate. - let mut gates = Vec::with_capacity(13); - let mut global_phase = -decomposer_inv.global_phase; - - let decomp_k1r = nalgebra_array_view::(decomposer_inv.K1r.as_view()); - let decomp_k2r = nalgebra_array_view::(decomposer_inv.K2r.as_view()); - let decomp_k1l = nalgebra_array_view::(decomposer_inv.K1l.as_view()); - let decomp_k2l = nalgebra_array_view::(decomposer_inv.K2l.as_view()); - - let unitary_k1r = - unitary_to_gate_sequence_inner(decomp_k1r, &target_1q_basis_list, 0, None, true, None); - let unitary_k2r = - unitary_to_gate_sequence_inner(decomp_k2r, &target_1q_basis_list, 0, None, true, None); - let unitary_k1l = - unitary_to_gate_sequence_inner(decomp_k1l, &target_1q_basis_list, 0, None, true, None); - let unitary_k2l = - unitary_to_gate_sequence_inner(decomp_k2l, &target_1q_basis_list, 0, None, true, None); - - if let Some(unitary_k2r) = unitary_k2r { - global_phase -= unitary_k2r.global_phase; - for gate in unitary_k2r.gates.into_iter().rev() { - let (inv_gate_name, inv_gate_params) = invert_1q_gate(gate); - gates.push((inv_gate_name, inv_gate_params, smallvec![0])); + let global_phase = -decomposer_inv.global_phase; + + let k1r = decomposer_inv.K1r; + let k2r = decomposer_inv.K2r; + let k1l = decomposer_inv.K1l; + let k2l = decomposer_inv.K2l; + + // the k matrices where RXX is inverted + let mut k_mats = [k1r, k1l, k2r, k2l]; + + if !is_inv_rxx { + if !k_mats[2].try_inverse_mut() { + panic!("TwoQubitWeylDecomposition failed. Matrix K2r is not unitary"); } - } - if let Some(unitary_k2l) = unitary_k2l { - global_phase -= unitary_k2l.global_phase; - for gate in unitary_k2l.gates.into_iter().rev() { - let (inv_gate_name, inv_gate_params) = invert_1q_gate(gate); - gates.push((inv_gate_name, inv_gate_params, smallvec![1])); + if !k_mats[3].try_inverse_mut() { + panic!("TwoQubitWeylDecomposition failed. Matrix K2l is not unitary"); + } + if !k_mats[0].try_inverse_mut() { + panic!("TwoQubitWeylDecomposition failed. Matrix K1R is not unitary"); } + if !k_mats[1].try_inverse_mut() { + panic!("TwoQubitWeylDecomposition failed. Matrix K1l is not unitary"); + } + // k_mats = [k2r_inv, k2l_inv, k1r_inv, k1l_inv]; + k_mats.swap(0, 2); + k_mats.swap(1, 3); } let rxx_op = match &self.rxx_equivalent_gate { RXXEquivalent::Standard(gate) => PackedOperation::from_standard_gate(*gate), @@ -235,91 +232,155 @@ impl TwoQubitControlledUDecomposer { })? } }; - gates.push((rxx_op, smallvec![self.scale * angle], smallvec![0, 1])); - - if let Some(unitary_k1r) = unitary_k1r { - global_phase -= unitary_k1r.global_phase; - for gate in unitary_k1r.gates.into_iter().rev() { - let (inv_gate_name, inv_gate_params) = invert_1q_gate(gate); - gates.push((inv_gate_name, inv_gate_params, smallvec![0])); - } - } - if let Some(unitary_k1l) = unitary_k1l { - global_phase -= unitary_k1l.global_phase; - for gate in unitary_k1l.gates.into_iter().rev() { - let (inv_gate_name, inv_gate_params) = invert_1q_gate(gate); - gates.push((inv_gate_name, inv_gate_params, smallvec![1])); - } + let mut out_gate_name = rxx_op; + let mut out_gate_params = smallvec![self.scale * angle]; + + if is_inv_rxx { + // invert the rxx_op + let (inv_gate_name, inv_gate_params, _inv_gate_qubits) = + self.invert_2q_gate((out_gate_name, out_gate_params, smallvec![0, 1]))?; + out_gate_name = inv_gate_name; + out_gate_params = inv_gate_params; } - Ok(TwoQubitGateSequence { - gates, - global_phase, - }) + Ok((out_gate_name, out_gate_params, global_phase, k_mats)) } /// Appends U_d(a, b, c) to the circuit. + /// + /// Any 2-qubit unitary can be decompose into a Weyl gate and four 1-qubit unitary gates: + /// ┌─────┐┌───────┐┌─────┐ + /// q_0: ┤ c2r ├┤0 ├┤ c1r ├ + /// ├─────┤│ Weyl │├─────┤ + /// q_1: ┤ c2l ├┤1 ├┤ c1l ├ + /// └─────┘└───────┘└─────┘ + /// The Weyl gate can be decomposed into the following product of 2-qubit gates + /// ┌─────────┐┌─────────┐ + /// q_0: ┤0 ├┤0 ├─■────── + /// │ Rxx(a) ││ Ryy(b) │ │ZZ(c) + /// q_1: ┤1 ├┤1 ├─■────── + /// └─────────┘└─────────┘ + /// RYY(b) is decomposed as + /// ┌─────┐┌─────────┐┌───┐ + /// q_0: ┤ Sdg ├┤0 ├┤ S ├ + /// ├─────┤│ Rxx(b) │├───┤ + /// q_1: ┤ Sdg ├┤1 ├┤ S ├ + /// └─────┘└─────────┘└───┘ + /// and RZZ(c) is decomposed as + /// ┌───┐┌─────────┐┌───┐ + /// q_0: ┤ H ├┤0 ├┤ H ├ + /// ├───┤│ Rxx(c) │├───┤ + /// q_1: ┤ H ├┤1 ├┤ H ├ + /// └───┘└─────────┘└───┘ + /// So the original 2-qubit unitary is decomposed as + /// ┌─────┐┌─────────┐┌─────┐┌─────────┐┌───┐┌───┐┌─────────┐┌───┐┌─────┐ + /// q_0: ┤ c2r ├┤0 ├┤ Sdg ├┤0 ├┤ S ├┤ H ├┤0 ├┤ H ├┤ c1r ├ + /// ├─────┤│ Rxx(a) │├─────┤│ Rxx(b) │├───┤├───┤│ Rxx(c) │├───┤├─────┤ + /// q_1: ┤ c2l ├┤1 ├┤ Sdg ├┤1 ├┤ S ├┤ H ├┤1 ├┤ H ├┤ c1l ├ + /// └─────┘└─────────┘└─────┘└─────────┘└───┘└───┘└─────────┘└───┘└─────┘ + /// + /// Now, RXX(a), RXX(b) and RXX(c) are decomposed into the RXX Equivalent gate, + /// so the final circuit obtained contains three 2-qubit gates and 24 1-qubit gates. + /// ┌─────┐┌─────────┐┌───────────┐┌─────────┐┌─────┐┌─────────┐┌───────────┐» + /// q_0: ┤ c2r ├┤ rxx_k2r ├┤0 ├┤ rxx_k1r ├┤ Sdg ├┤ ryy_k2r ├┤0 ├» + /// ├─────┤├─────────┤│ Equiv(a) │├─────────┤├─────┤├─────────┤│ Equiv(b) │» + /// q_1: ┤ c2l ├┤ rxx_k2l ├┤1 ├┤ rxx_k1l ├┤ Sdg ├┤ ryy_k2l ├┤1 ├» + /// └─────┘└─────────┘└───────────┘└─────────┘└─────┘└─────────┘└───────────┘» + /// « ┌─────────┐┌───┐┌───┐┌─────────┐┌───────────┐┌─────────┐┌───┐┌─────┐ + /// «q_0: ┤ ryy_k1r ├┤ S ├┤ H ├┤ rzz_k2r ├┤0 ├┤ rzz_k1r ├┤ H ├┤ c1r ├ + /// « ├─────────┤├───┤├───┤├─────────┤│ Equiv(c) │├─────────┤├───┤├─────┤ + /// «q_1: ┤ ryy_k1l ├┤ S ├┤ H ├┤ rzz_k2l ├┤1 ├┤ rzz_k1l ├┤ H ├┤ c1l ├ + /// « └─────────┘└───┘└───┘└─────────┘└───────────┘└─────────┘└───┘└─────┘ fn weyl_gate( &self, circ: &mut TwoQubitGateSequence, - target_decomposed: TwoQubitWeylDecomposition, + target_decomposed: &TwoQubitWeylDecomposition, atol: f64, + c_mats: [Matrix2; 4], + target_1q_basis_list: EulerBasisSet, ) -> PyResult<()> { - let circ_a = self.to_rxx_gate(-2.0 * target_decomposed.a)?; - circ.gates.extend(circ_a.gates); - let mut global_phase = circ_a.global_phase; - - let mut target_1q_basis_list = EulerBasisSet::new(); - target_1q_basis_list.add_basis(self.euler_basis); - - let s_decomp = unitary_to_gate_sequence_inner( - aview2(&S_GATE), - &target_1q_basis_list, + // RXX(a) + let (circ_a_name, circ_a_params, global_phase_a, rxx_mats) = + self.to_rxx_gate(-2.0 * target_decomposed.a, false)?; + let mut global_phase = global_phase_a; + + let mut c2r = c_mats[0]; // before weyl_gate, qubit 0 + let mut c2l = c_mats[1]; // before weyl_gate, qubit 1 + let mut c1r = c_mats[2]; // after weyl_gate, qubit 0 + let mut c1l = c_mats[3]; // after weyl_gate, qubit 1 + + let rxx_k2r = rxx_mats[0]; // before RXX(a), qubit 0 + let rxx_k2l = rxx_mats[1]; // before RXX(a), qubit 1 + let rxx_k1r = rxx_mats[2]; // after RXX(a), qubit 0 + let rxx_k1l = rxx_mats[3]; // after RXX(a), qubit 1 + + let mut ryy_k2r: Matrix2; // before RXX(b), qubit 0 + let mut ryy_k2l: Matrix2; // before RXX(b), qubit 1 + let mut ryy_k1r: Matrix2; // after RXX(b), qubit 0 + let mut ryy_k1l: Matrix2; // after RXX(b), qubit 1 + + let mut rzz_k2r: Matrix2; // before RXX(c), qubit 0 + let mut rzz_k2l: Matrix2; // before RXX(c), qubit 1 + let mut rzz_k1r: Matrix2; // after RXX(c), qubit 0 + let mut rzz_k1l: Matrix2; // after RXX(c), qubit 1 + + // before the weyl_gate + c2r = rxx_k2r * c2r; + c2l = rxx_k2l * c2l; + + self.append_1q_sequence( + &mut circ.gates, + &mut global_phase, + c2r.as_view(), 0, - None, - true, - None, + target_1q_basis_list, ); - let sdg_decomp = unitary_to_gate_sequence_inner( - aview2(&SDG_GATE), - &target_1q_basis_list, - 0, - None, - true, - None, - ); - let h_decomp = unitary_to_gate_sequence_inner( - aview2(&H_GATE), - &target_1q_basis_list, - 0, - None, - true, - None, + self.append_1q_sequence( + &mut circ.gates, + &mut global_phase, + c2l.as_view(), + 1, + target_1q_basis_list, ); + circ.gates + .push((circ_a_name, circ_a_params, smallvec![0, 1])); // translate RYY(b) into a circuit based on the desired Ctrl-U gate. if (target_decomposed.b).abs() > atol { - let circ_b = self.to_rxx_gate(-2.0 * target_decomposed.b)?; - global_phase += circ_b.global_phase; - if let Some(sdg_decomp) = sdg_decomp { - global_phase += 2.0 * sdg_decomp.global_phase; - for gate in sdg_decomp.gates.into_iter() { - let gate_params = gate.1; - circ.gates - .push((gate.0.into(), gate_params.clone(), smallvec![0])); - circ.gates.push((gate.0.into(), gate_params, smallvec![1])); - } - } - circ.gates.extend(circ_b.gates); - if let Some(s_decomp) = s_decomp { - global_phase += 2.0 * s_decomp.global_phase; - for gate in s_decomp.gates.into_iter() { - let gate_params = gate.1; - circ.gates - .push((gate.0.into(), gate_params.clone(), smallvec![0])); - circ.gates.push((gate.0.into(), gate_params, smallvec![1])); - } - } + let (circ_b_name, circ_b_params, global_phase_b, ryy_mats) = + self.to_rxx_gate(-2.0 * target_decomposed.b, false)?; + global_phase += global_phase_b; + + ryy_k2r = ryy_mats[0]; // before RXX(b), qubit 0 + ryy_k2l = ryy_mats[1]; // before RXX(b), qubit 1 + ryy_k1r = ryy_mats[2]; // after RXX(b), qubit 0 + ryy_k1l = ryy_mats[3]; // after RXX(b), qubit 1 + + ryy_k2r = ryy_k2r * SDGGATE * rxx_k1r; // between RXX(a) and RXX(b), qubit 0 + ryy_k2l = ryy_k2l * SDGGATE * rxx_k1l; // between RXX(a) and RXX(b), qubit 1 + ryy_k1r = SGATE * ryy_k1r; // between RXX(b) and RXX(c), qubit 0 + ryy_k1l = SGATE * ryy_k1l; // between RXX(b) and RXX(c), qubit 1 + + self.append_1q_sequence( + &mut circ.gates, + &mut global_phase, + ryy_k2r.as_view(), + 0, + target_1q_basis_list, + ); + self.append_1q_sequence( + &mut circ.gates, + &mut global_phase, + ryy_k2l.as_view(), + 1, + target_1q_basis_list, + ); + circ.gates + .push((circ_b_name, circ_b_params, smallvec![0, 1])); + } else { + // no circ_b + ryy_k1r = rxx_k1r; + ryy_k1l = rxx_k1l; } // translate RZZ(c) into a circuit based on the desired Ctrl-U gate. @@ -331,60 +392,97 @@ impl TwoQubitControlledUDecomposer { // circuit if c < 0. let mut gamma = -2.0 * target_decomposed.c; if gamma <= 0.0 { - let circ_c = self.to_rxx_gate(gamma)?; - global_phase += circ_c.global_phase; - - if let Some(ref h_decomp) = h_decomp { - global_phase += 2.0 * h_decomp.global_phase; - for gate in h_decomp.gates.clone().into_iter() { - let gate_params = gate.1; - circ.gates - .push((gate.0.into(), gate_params.clone(), smallvec![0])); - circ.gates.push((gate.0.into(), gate_params, smallvec![1])); - } - } - circ.gates.extend(circ_c.gates); - if let Some(ref h_decomp) = h_decomp { - global_phase += 2.0 * h_decomp.global_phase; - for gate in h_decomp.gates.clone().into_iter() { - let gate_params = gate.1; - circ.gates - .push((gate.0.into(), gate_params.clone(), smallvec![0])); - circ.gates.push((gate.0.into(), gate_params, smallvec![1])); - } - } + let (circ_c_name, circ_c_params, global_phase_c, rzz_mats) = + self.to_rxx_gate(gamma, false)?; + global_phase += global_phase_c; + + rzz_k2r = rzz_mats[0]; // before RXX(c), qubit 0 + rzz_k2l = rzz_mats[1]; // before RXX(c), qubit 1 + rzz_k1r = rzz_mats[2]; // after RXX(c), qubit 0 + rzz_k1l = rzz_mats[3]; // after RXX(c), qubit 1 + + rzz_k2r = rzz_k2r * HGATE * ryy_k1r; // between RXX(b) and RXX(c), qubit 0 + rzz_k2l = rzz_k2l * HGATE * ryy_k1l; // between RXX(b) and RXX(c), qubit 1 + rzz_k1r = HGATE * rzz_k1r; // after RXX(c), qubit 0 + rzz_k1l = HGATE * rzz_k1l; // after RXX(c), qubit 1 + + self.append_1q_sequence( + &mut circ.gates, + &mut global_phase, + rzz_k2r.as_view(), + 0, + target_1q_basis_list, + ); + self.append_1q_sequence( + &mut circ.gates, + &mut global_phase, + rzz_k2l.as_view(), + 1, + target_1q_basis_list, + ); + circ.gates + .push((circ_c_name, circ_c_params, smallvec![0, 1])); } else { // invert the circuit above gamma *= -1.0; - let circ_c = self.to_rxx_gate(gamma)?; - global_phase -= circ_c.global_phase; - if let Some(ref h_decomp) = h_decomp { - global_phase += 2.0 * h_decomp.global_phase; - for gate in h_decomp.gates.clone().into_iter() { - let gate_params = gate.1; - circ.gates - .push((gate.0.into(), gate_params.clone(), smallvec![0])); - circ.gates.push((gate.0.into(), gate_params, smallvec![1])); - } - } - for gate in circ_c.gates.into_iter().rev() { - let (inv_gate_name, inv_gate_params, inv_gate_qubits) = - self.invert_2q_gate(gate)?; - circ.gates - .push((inv_gate_name, inv_gate_params, inv_gate_qubits)); - } - if let Some(ref h_decomp) = h_decomp { - global_phase += 2.0 * h_decomp.global_phase; - for gate in h_decomp.gates.clone().into_iter() { - let gate_params = gate.1; - circ.gates - .push((gate.0.into(), gate_params.clone(), smallvec![0])); - circ.gates.push((gate.0.into(), gate_params, smallvec![1])); - } - } + // the inverted 2-qubit RXX gate + let (circ_c_name, circ_c_params, global_phase_c, rzz_mats) = + self.to_rxx_gate(gamma, true)?; + global_phase -= global_phase_c; + + // the inverted 1-qubit matrices + rzz_k2r = rzz_mats[0]; // before RXX(c), qubit 0 + rzz_k2l = rzz_mats[1]; // before RXX(c), qubit 1 + rzz_k1r = rzz_mats[2]; // after RXX(c), qubit 0 + rzz_k1l = rzz_mats[3]; // after RXX(c), qubit 1 + + rzz_k2r = rzz_k2r * HGATE * ryy_k1r; // between RXX(b) and RXX(c), qubit 0 + rzz_k2l = rzz_k2l * HGATE * ryy_k1l; // between RXX(b) and RXX(c), qubit 1 + rzz_k1r = HGATE * rzz_k1r; // after RXX(c), qubit 0 + rzz_k1l = HGATE * rzz_k1l; // after RXX(c), qubit 1 + + self.append_1q_sequence( + &mut circ.gates, + &mut global_phase, + rzz_k2r.as_view(), + 0, + target_1q_basis_list, + ); + self.append_1q_sequence( + &mut circ.gates, + &mut global_phase, + rzz_k2l.as_view(), + 1, + target_1q_basis_list, + ); + circ.gates + .push((circ_c_name, circ_c_params, smallvec![0, 1])); } + } else { + // no circ_c + rzz_k1r = ryy_k1r; + rzz_k1l = ryy_k1l; } + // after the weyl_gate + c1r *= rzz_k1r; + c1l *= rzz_k1l; + + self.append_1q_sequence( + &mut circ.gates, + &mut global_phase, + c1r.as_view(), + 0, + target_1q_basis_list, + ); + self.append_1q_sequence( + &mut circ.gates, + &mut global_phase, + c1l.as_view(), + 1, + target_1q_basis_list, + ); + circ.global_phase = global_phase; Ok(()) } @@ -402,57 +500,32 @@ impl TwoQubitControlledUDecomposer { let mut target_1q_basis_list = EulerBasisSet::new(); target_1q_basis_list.add_basis(self.euler_basis); - let c1r = nalgebra_array_view::(target_decomposed.K1r.as_view()); - let c2r = nalgebra_array_view::(target_decomposed.K2r.as_view()); - let c1l = nalgebra_array_view::(target_decomposed.K1l.as_view()); - let c2l = nalgebra_array_view::(target_decomposed.K2l.as_view()); - - let unitary_c1r = - unitary_to_gate_sequence_inner(c1r, &target_1q_basis_list, 0, None, true, None); - let unitary_c2r = - unitary_to_gate_sequence_inner(c2r, &target_1q_basis_list, 0, None, true, None); - let unitary_c1l = - unitary_to_gate_sequence_inner(c1l, &target_1q_basis_list, 0, None, true, None); - let unitary_c2l = - unitary_to_gate_sequence_inner(c2l, &target_1q_basis_list, 0, None, true, None); - - let mut gates = Vec::with_capacity(59); - let mut global_phase = target_decomposed.global_phase; - - if let Some(unitary_c2r) = unitary_c2r { - global_phase += unitary_c2r.global_phase; - for gate in unitary_c2r.gates.into_iter() { - gates.push((gate.0.into(), gate.1, smallvec![0])); - } - } - if let Some(unitary_c2l) = unitary_c2l { - global_phase += unitary_c2l.global_phase; - for gate in unitary_c2l.gates.into_iter() { - gates.push((gate.0.into(), gate.1, smallvec![1])); - } - } - let mut gates1 = TwoQubitGateSequence { + let c2r: Matrix2 = target_decomposed.K2r; // qubit 0, before weyl_gate + let c2l: Matrix2 = target_decomposed.K2l; // qubit 1, before weyl_gate + let c1r: Matrix2 = target_decomposed.K1r; // qubit 0, after weyl_gate + let c1l: Matrix2 = target_decomposed.K1l; // qubit 1, after weyl_gate + + // Capacity = 5*8 + 3 = 43 + // 3 2-qubit gates + // 8 1-qubit unitaries + // max 5 1-qubit gates per 1-qubit unitary + let gates = Vec::with_capacity(43); + let global_phase = target_decomposed.global_phase; + + let mut weyl_gates = TwoQubitGateSequence { gates, global_phase, }; - self.weyl_gate(&mut gates1, target_decomposed, atol.unwrap_or(DEFAULT_ATOL))?; - global_phase += gates1.global_phase; - - if let Some(unitary_c1r) = unitary_c1r { - global_phase += unitary_c1r.global_phase; - for gate in unitary_c1r.gates.into_iter() { - gates1.gates.push((gate.0.into(), gate.1, smallvec![0])); - } - } - if let Some(unitary_c1l) = unitary_c1l { - global_phase += unitary_c1l.global_phase; - for gate in unitary_c1l.gates.into_iter() { - gates1.gates.push((gate.0.into(), gate.1, smallvec![1])); - } - } - - gates1.global_phase = global_phase; - Ok(gates1) + self.weyl_gate( + &mut weyl_gates, + &target_decomposed, + atol.unwrap_or(DEFAULT_ATOL), + [c2r, c2l, c1r, c1l], + target_1q_basis_list, + )?; + weyl_gates.global_phase += global_phase; + + Ok(weyl_gates) } /// Initialize the KAK decomposition. @@ -526,6 +599,31 @@ impl TwoQubitControlledUDecomposer { euler_basis: EulerBasis::__new__(euler_basis)?, }) } + + // Note: this function also appears in TwoQubitBasisDecomposer) + fn append_1q_sequence( + &self, + gates: &mut TwoQubitSequenceVec, + global_phase: &mut f64, + unitary: MatrixView2, + qubit: u8, + target_1q_basis_list: EulerBasisSet, + ) { + let sequence = unitary_to_gate_sequence_inner( + nalgebra_array_view::(unitary), + &target_1q_basis_list, + qubit as usize, + None, + true, + None, + ); + if let Some(sequence) = sequence { + *global_phase += sequence.global_phase; + for gate in sequence.gates { + gates.push((gate.0.into(), gate.1, smallvec![qubit])); + } + } + } } #[pymethods] @@ -538,8 +636,8 @@ impl TwoQubitControlledUDecomposer { /// Args: /// rxx_equivalent_gate: Gate that is locally equivalent to an :class:`.RXXGate`: /// :math:`U \sim U_d(\alpha, 0, 0) \sim \text{Ctrl-U}` gate. - /// euler_basis: Basis string to be provided to :class:`.OneQubitEulerDecomposer` - /// for 1Q synthesis. + /// euler_basis: Basis string to be provided to :class:`.OneQubitEulerDecomposer` + /// for 1Q synthesis. /// Raises: /// QiskitError: If the gate is not locally equivalent to an :class:`.RXXGate`. #[new] diff --git a/releasenotes/notes/improve_two_qubit_controlled_u_decomposer_1q_gate_count-2d3fe2e4c84f78df.yaml b/releasenotes/notes/improve_two_qubit_controlled_u_decomposer_1q_gate_count-2d3fe2e4c84f78df.yaml new file mode 100644 index 000000000000..280d69c9ebc9 --- /dev/null +++ b/releasenotes/notes/improve_two_qubit_controlled_u_decomposer_1q_gate_count-2d3fe2e4c84f78df.yaml @@ -0,0 +1,6 @@ +--- +features_synthesis: + - Improve the single-qubit gate count in the :class:`.TwoQubitControlledUDecomposer`. + A general two-qubit unitary is now decomposed into a circuit with 3 two-qubit gates + and only 8 single-qubit unitary gates, improving on the previous decomposition + which required 24 single-qubit unitary gates. diff --git a/test/python/synthesis/test_synthesis.py b/test/python/synthesis/test_synthesis.py index a02ea5a2b24e..eb244941f0a6 100644 --- a/test/python/synthesis/test_synthesis.py +++ b/test/python/synthesis/test_synthesis.py @@ -1458,6 +1458,35 @@ def test_correct_unitary(self, seed, gate, euler_basis): circ = decomposer(unitary) self.assertEqual(Operator(unitary), Operator(circ)) + # bound on the number of 2-qubit gates + num2q = circ.size(filter_function=lambda x: x.operation.num_qubits == 2) + self.assertLessEqual(num2q, 3) + + # bound on the number of 1-qubit gates + self.assertLessEqual(circ.count_ops().get("u", 0), 8) + self.assertLessEqual(circ.count_ops().get("sx", 0), 14) + self.assertLessEqual(circ.count_ops().get("rx", 0), 14) + self.assertLessEqual(circ.count_ops().get("ry", 0), 8) + self.assertLessEqual(circ.count_ops().get("rz", 0), 22) + + @combine(gate=[RXXGate, RYYGate, RZZGate, RZXGate, CPhaseGate, CRZGate, CRXGate, CRYGate]) + def test_one_controlled_u_gate_in_unitary(self, gate): + """Verify a unitary with one controlled-u gate for different gates in the decomposition""" + unitary = RZZGate(-0.3).to_matrix() + + decomposer = TwoQubitControlledUDecomposer(gate, euler_basis="U") + circ = decomposer(unitary) + self.assertEqual(Operator(unitary), Operator(circ)) + + # bound on the number of 2-qubit gates + num2q = circ.size(filter_function=lambda x: x.operation.num_qubits == 2) + self.assertEqual(num2q, 1) + + # bound on the number of 1-qubit gates + self.assertLessEqual(circ.count_ops().get("u", 0), 4) + if gate(0.1).name == "rzz": + self.assertEqual(circ.size(), 1) + def test_not_rxx_equivalent(self): """Test that an exception is raised if the gate is not equivalent to an RXXGate""" gate = SwapGate