diff --git a/crates/accelerate/src/two_qubit_decompose.rs b/crates/accelerate/src/two_qubit_decompose.rs index ed4d32bd3b85..dfee4c5b0bff 100644 --- a/crates/accelerate/src/two_qubit_decompose.rs +++ b/crates/accelerate/src/two_qubit_decompose.rs @@ -481,66 +481,12 @@ impl TwoQubitWeylDecomposition { } } } -} - -static IPZ: GateArray1Q = [[IM, C_ZERO], [C_ZERO, M_IM]]; -static IPY: GateArray1Q = [[C_ZERO, C_ONE], [C_M_ONE, C_ZERO]]; -static IPX: GateArray1Q = [[C_ZERO, IM], [IM, C_ZERO]]; - -#[pymethods] -impl TwoQubitWeylDecomposition { - #[staticmethod] - fn _from_state( - angles: [f64; 4], - matrices: [PyReadonlyArray2; 5], - specialization: Specialization, - default_euler_basis: EulerBasis, - calculated_fidelity: f64, - requested_fidelity: Option, - ) -> Self { - let [a, b, c, global_phase] = angles; - Self { - a, - b, - c, - global_phase, - K1l: matrices[0].as_array().to_owned(), - K1r: matrices[1].as_array().to_owned(), - K2l: matrices[2].as_array().to_owned(), - K2r: matrices[3].as_array().to_owned(), - specialization, - default_euler_basis, - calculated_fidelity, - requested_fidelity, - unitary_matrix: matrices[4].as_array().to_owned(), - } - } - fn __reduce__(&self, py: Python) -> PyResult> { - Ok(( - py.get_type_bound::().getattr("_from_state")?, - ( - [self.a, self.b, self.c, self.global_phase], - [ - self.K1l.to_pyarray_bound(py), - self.K1r.to_pyarray_bound(py), - self.K2l.to_pyarray_bound(py), - self.K2r.to_pyarray_bound(py), - self.unitary_matrix.to_pyarray_bound(py), - ], - self.specialization, - self.default_euler_basis, - self.calculated_fidelity, - self.requested_fidelity, - ), - ) - .into_py(py)) - } + /// Instantiate a new TwoQubitWeylDecomposition with rust native + /// data structures + fn new_inner( + unitary_matrix: ArrayView2, - #[new] - #[pyo3(signature=(unitary_matrix, fidelity=DEFAULT_FIDELITY, _specialization=None))] - fn new( - unitary_matrix: PyReadonlyArray2, fidelity: Option, _specialization: Option, ) -> PyResult { @@ -548,8 +494,8 @@ impl TwoQubitWeylDecomposition { let ipy: ArrayView2 = aview2(&IPY); let ipx: ArrayView2 = aview2(&IPX); - let mut u = unitary_matrix.as_array().to_owned(); - let unitary_matrix = unitary_matrix.as_array().to_owned(); + let mut u = unitary_matrix.to_owned(); + let unitary_matrix = unitary_matrix.to_owned(); let det_u = u.view().into_faer_complex().determinant().to_num_complex(); let det_pow = det_u.powf(-0.25); u.mapv_inplace(|x| x * det_pow); @@ -1015,6 +961,71 @@ impl TwoQubitWeylDecomposition { specialized.global_phase += tr.arg(); Ok(specialized) } +} + +static IPZ: GateArray1Q = [[IM, C_ZERO], [C_ZERO, M_IM]]; +static IPY: GateArray1Q = [[C_ZERO, C_ONE], [C_M_ONE, C_ZERO]]; +static IPX: GateArray1Q = [[C_ZERO, IM], [IM, C_ZERO]]; + +#[pymethods] +impl TwoQubitWeylDecomposition { + #[staticmethod] + fn _from_state( + angles: [f64; 4], + matrices: [PyReadonlyArray2; 5], + specialization: Specialization, + default_euler_basis: EulerBasis, + calculated_fidelity: f64, + requested_fidelity: Option, + ) -> Self { + let [a, b, c, global_phase] = angles; + Self { + a, + b, + c, + global_phase, + K1l: matrices[0].as_array().to_owned(), + K1r: matrices[1].as_array().to_owned(), + K2l: matrices[2].as_array().to_owned(), + K2r: matrices[3].as_array().to_owned(), + specialization, + default_euler_basis, + calculated_fidelity, + requested_fidelity, + unitary_matrix: matrices[4].as_array().to_owned(), + } + } + + fn __reduce__(&self, py: Python) -> PyResult> { + Ok(( + py.get_type_bound::().getattr("_from_state")?, + ( + [self.a, self.b, self.c, self.global_phase], + [ + self.K1l.to_pyarray_bound(py), + self.K1r.to_pyarray_bound(py), + self.K2l.to_pyarray_bound(py), + self.K2r.to_pyarray_bound(py), + self.unitary_matrix.to_pyarray_bound(py), + ], + self.specialization, + self.default_euler_basis, + self.calculated_fidelity, + self.requested_fidelity, + ), + ) + .into_py(py)) + } + + #[new] + #[pyo3(signature=(unitary_matrix, fidelity=DEFAULT_FIDELITY, _specialization=None))] + fn new( + unitary_matrix: PyReadonlyArray2, + fidelity: Option, + _specialization: Option, + ) -> PyResult { + TwoQubitWeylDecomposition::new_inner(unitary_matrix.as_array(), fidelity, _specialization) + } #[allow(non_snake_case)] #[getter] @@ -1568,49 +1579,17 @@ impl TwoQubitBasisDecomposer { } Ok(res) } -} - -static K12R_ARR: GateArray1Q = [ - [c64(0., FRAC_1_SQRT_2), c64(FRAC_1_SQRT_2, 0.)], - [c64(-FRAC_1_SQRT_2, 0.), c64(0., -FRAC_1_SQRT_2)], -]; - -static K12L_ARR: GateArray1Q = [ - [c64(0.5, 0.5), c64(0.5, 0.5)], - [c64(-0.5, 0.5), c64(0.5, -0.5)], -]; -fn decomp0_inner(target: &TwoQubitWeylDecomposition) -> SmallVec<[Array2; 8]> { - smallvec![target.K1r.dot(&target.K2r), target.K1l.dot(&target.K2l),] -} - -#[pymethods] -impl TwoQubitBasisDecomposer { - fn __getnewargs__(&self, py: Python) -> (String, PyObject, f64, &str, Option) { - ( - self.gate.clone(), - self.basis_decomposer - .unitary_matrix - .to_pyarray_bound(py) - .into(), - self.basis_fidelity, - self.euler_basis.as_str(), - self.pulse_optimize, - ) - } - - #[new] - #[pyo3(signature=(gate, gate_matrix, basis_fidelity=1.0, euler_basis="U", pulse_optimize=None))] - fn new( + fn new_inner( gate: String, - gate_matrix: PyReadonlyArray2, + gate_matrix: ArrayView2, basis_fidelity: f64, euler_basis: &str, pulse_optimize: Option, ) -> PyResult { let ipz: ArrayView2 = aview2(&IPZ); let basis_decomposer = - TwoQubitWeylDecomposition::new(gate_matrix, Some(DEFAULT_FIDELITY), None)?; + TwoQubitWeylDecomposition::new_inner(gate_matrix, Some(DEFAULT_FIDELITY), None)?; let super_controlled = relative_eq!(basis_decomposer.a, PI4, max_relative = 1e-09) && relative_eq!(basis_decomposer.c, 0.0, max_relative = 1e-09); @@ -1739,6 +1718,153 @@ impl TwoQubitBasisDecomposer { }) } + fn call_inner( + &self, + unitary: ArrayView2, + basis_fidelity: Option, + approximate: bool, + _num_basis_uses: Option, + ) -> PyResult { + let basis_fidelity = if !approximate { + 1.0 + } else { + basis_fidelity.unwrap_or(self.basis_fidelity) + }; + let target_decomposed = + TwoQubitWeylDecomposition::new_inner(unitary, Some(DEFAULT_FIDELITY), None)?; + let traces = self.traces(&target_decomposed); + let best_nbasis = _num_basis_uses.unwrap_or_else(|| { + traces + .into_iter() + .enumerate() + .map(|(idx, trace)| (idx, trace.trace_to_fid() * basis_fidelity.powi(idx as i32))) + .min_by(|(_idx1, fid1), (_idx2, fid2)| fid2.partial_cmp(fid1).unwrap()) + .unwrap() + .0 as u8 + }); + let decomposition = match best_nbasis { + 0 => decomp0_inner(&target_decomposed), + 1 => self.decomp1_inner(&target_decomposed), + 2 => self.decomp2_supercontrolled_inner(&target_decomposed), + 3 => self.decomp3_supercontrolled_inner(&target_decomposed), + _ => unreachable!("Invalid basis to use"), + }; + let pulse_optimize = self.pulse_optimize.unwrap_or(true); + let sequence = if pulse_optimize { + self.pulse_optimal_chooser(best_nbasis, &decomposition, &target_decomposed)? + } else { + None + }; + if let Some(seq) = sequence { + return Ok(seq); + } + let target_1q_basis_list = vec![self.euler_basis]; + let euler_decompositions: SmallVec<[Option; 8]> = decomposition + .iter() + .map(|decomp| { + unitary_to_gate_sequence_inner( + decomp.view(), + &target_1q_basis_list, + 0, + None, + true, + None, + ) + }) + .collect(); + // Worst case length is 5x 1q gates for each 1q decomposition + 1x 2q gate + // We might overallocate a bit if the euler basis is different but + // the worst case is just 16 extra elements with just a String and 2 smallvecs + // each. This is only transient though as the circuit sequences aren't long lived + // and are just used to create a QuantumCircuit or DAGCircuit when we return to + // Python space. + let mut gates = Vec::with_capacity(21); + let mut global_phase = target_decomposed.global_phase; + global_phase -= best_nbasis as f64 * self.basis_decomposer.global_phase; + if best_nbasis == 2 { + global_phase += PI; + } + for i in 0..best_nbasis as usize { + if let Some(euler_decomp) = &euler_decompositions[2 * i] { + for gate in &euler_decomp.gates { + gates.push((Some(gate.0), gate.1.clone(), smallvec![0])); + } + global_phase += euler_decomp.global_phase + } + if let Some(euler_decomp) = &euler_decompositions[2 * i + 1] { + for gate in &euler_decomp.gates { + gates.push((Some(gate.0), gate.1.clone(), smallvec![1])); + } + global_phase += euler_decomp.global_phase + } + gates.push((None, smallvec![], smallvec![0, 1])); + } + if let Some(euler_decomp) = &euler_decompositions[2 * best_nbasis as usize] { + for gate in &euler_decomp.gates { + gates.push((Some(gate.0), gate.1.clone(), smallvec![0])); + } + global_phase += euler_decomp.global_phase + } + if let Some(euler_decomp) = &euler_decompositions[2 * best_nbasis as usize + 1] { + for gate in &euler_decomp.gates { + gates.push((Some(gate.0), gate.1.clone(), smallvec![1])); + } + global_phase += euler_decomp.global_phase + } + Ok(TwoQubitGateSequence { + gates, + global_phase, + }) + } +} + +static K12R_ARR: GateArray1Q = [ + [c64(0., FRAC_1_SQRT_2), c64(FRAC_1_SQRT_2, 0.)], + [c64(-FRAC_1_SQRT_2, 0.), c64(0., -FRAC_1_SQRT_2)], +]; + +static K12L_ARR: GateArray1Q = [ + [c64(0.5, 0.5), c64(0.5, 0.5)], + [c64(-0.5, 0.5), c64(0.5, -0.5)], +]; + +fn decomp0_inner(target: &TwoQubitWeylDecomposition) -> SmallVec<[Array2; 8]> { + smallvec![target.K1r.dot(&target.K2r), target.K1l.dot(&target.K2l),] +} + +#[pymethods] +impl TwoQubitBasisDecomposer { + fn __getnewargs__(&self, py: Python) -> (String, PyObject, f64, &str, Option) { + ( + self.gate.clone(), + self.basis_decomposer + .unitary_matrix + .to_pyarray_bound(py) + .into(), + self.basis_fidelity, + self.euler_basis.as_str(), + self.pulse_optimize, + ) + } + + #[new] + #[pyo3(signature=(gate, gate_matrix, basis_fidelity=1.0, euler_basis="U", pulse_optimize=None))] + fn new( + gate: String, + gate_matrix: PyReadonlyArray2, + basis_fidelity: f64, + euler_basis: &str, + pulse_optimize: Option, + ) -> PyResult { + TwoQubitBasisDecomposer::new_inner( + gate, + gate_matrix.as_array(), + basis_fidelity, + euler_basis, + pulse_optimize, + ) + } + fn traces(&self, target: &TwoQubitWeylDecomposition) -> [Complex64; 4] { [ 4. * c64( @@ -1972,6 +2098,63 @@ impl TwoQubitBasisDecomposer { } } +fn u4_to_su4(u4: ArrayView2) -> (Array2, f64) { + let det_u = u4.into_faer_complex().determinant().to_num_complex(); + let phase_factor = det_u.powf(-0.25).conj(); + let su4 = u4.mapv(|x| x / phase_factor); + (su4, phase_factor.arg()) +} + +fn real_trace_transform(mat: ArrayView2) -> Array2 { + let a1 = -mat[[1, 3]] * mat[[2, 0]] + mat[[1, 2]] * mat[[2, 1]] + mat[[1, 1]] * mat[[2, 2]] + - mat[[1, 0]] * mat[[2, 3]]; + let a2 = mat[[0, 3]] * mat[[3, 0]] - mat[[0, 2]] * mat[[3, 1]] - mat[[0, 1]] * mat[[3, 2]] + + mat[[0, 0]] * mat[[3, 3]]; + let theta = 0.; // Arbitrary! + let phi = 0.; // This is extra arbitrary! + let psi = f64::atan2(a1.im + a2.im, a1.re - a2.re) - phi; + let im = Complex64::new(0., -1.); + let temp = [ + (theta * im).exp(), + (phi * im).exp(), + (psi * im).exp(), + (-(theta + phi + psi) * im).exp(), + ]; + Array2::from_diag(&arr1(&temp)) +} + +#[pyfunction] +fn two_qubit_decompose_up_to_diagonal( + py: Python, + mat: PyReadonlyArray2, +) -> PyResult<(PyObject, CircuitData)> { + let mat_arr: ArrayView2 = mat.as_array(); + let (su4, phase) = u4_to_su4(mat_arr); + let mut real_map = real_trace_transform(su4.view()); + let mapped_su4 = real_map.dot(&su4.view()); + let decomp = + TwoQubitBasisDecomposer::new_inner("cx".to_string(), aview2(&CX_GATE), 1.0, "U", None)?; + + let circ_seq = decomp.call_inner(mapped_su4.view(), None, true, None)?; + let circ = CircuitData::from_standard_gates( + py, + 2, + circ_seq + .gates + .into_iter() + .map(|(gate, param_floats, qubit_index)| { + let params: SmallVec<[Param; 3]> = + param_floats.into_iter().map(Param::Float).collect(); + let qubits: SmallVec<[Qubit; 2]> = + qubit_index.into_iter().map(|x| Qubit(x as u32)).collect(); + (gate.unwrap_or(StandardGate::CXGate), params, qubits) + }), + Param::Float(circ_seq.global_phase + phase), + )?; + real_map.mapv_inplace(|x| x.conj()); + Ok((real_map.into_pyarray_bound(py).into(), circ)) +} + static MAGIC: GateArray2Q = [ [ c64(FRAC_1_SQRT_2, 0.), @@ -2074,6 +2257,7 @@ pub fn local_equivalence(weyl: PyReadonlyArray1) -> PyResult<[f64; 3]> { #[pymodule] pub fn two_qubit_decompose(m: &Bound) -> PyResult<()> { m.add_wrapped(wrap_pyfunction!(_num_basis_gates))?; + m.add_wrapped(wrap_pyfunction!(two_qubit_decompose_up_to_diagonal))?; m.add_wrapped(wrap_pyfunction!(two_qubit_local_invariants))?; m.add_wrapped(wrap_pyfunction!(local_equivalence))?; m.add_class::()?; diff --git a/qiskit/synthesis/two_qubit/two_qubit_decompose.py b/qiskit/synthesis/two_qubit/two_qubit_decompose.py index 80a454cc6bd4..86c5cba8295f 100644 --- a/qiskit/synthesis/two_qubit/two_qubit_decompose.py +++ b/qiskit/synthesis/two_qubit/two_qubit_decompose.py @@ -699,95 +699,6 @@ def traces(self, target): return self._inner_decomposer.traces(target._inner_decomposition) -class TwoQubitDecomposeUpToDiagonal: - """ - Class to decompose two qubit unitaries into the product of a diagonal gate - and another unitary gate which can be represented by two CX gates instead of the - usual three. This can be used when neighboring gates commute with the diagonal to - potentially reduce overall CX count. - """ - - def __init__(self): - sy = np.array([[0, -1j], [1j, 0]]) - self.sysy = np.kron(sy, sy) - - def _u4_to_su4(self, u4): - phase_factor = np.conj(np.linalg.det(u4) ** (-1 / u4.shape[0])) - su4 = u4 / phase_factor - return su4, cmath.phase(phase_factor) - - def _gamma(self, mat): - """ - proposition II.1: this invariant characterizes when two operators in U(4), - say u, v, are equivalent up to single qubit gates: - - u ≡ v -> Det(γ(u)) = Det(±(γ(v))) - """ - sumat, _ = self._u4_to_su4(mat) - sysy = self.sysy - return sumat @ sysy @ sumat.T @ sysy - - def _cx0_test(self, mat): - # proposition III.1: zero cx sufficient - gamma = self._gamma(mat) - evals = np.linalg.eigvals(gamma) - return np.all(np.isclose(evals, np.ones(4))) - - def _cx1_test(self, mat): - # proposition III.2: one cx sufficient - gamma = self._gamma(mat) - evals = np.linalg.eigvals(gamma) - uvals, ucnts = np.unique(np.round(evals, 10), return_counts=True) - return ( - len(uvals) == 2 - and all(ucnts == 2) - and all((np.isclose(x, 1j)) or np.isclose(x, -1j) for x in uvals) - ) - - def _cx2_test(self, mat): - # proposition III.3: two cx sufficient - gamma = self._gamma(mat) - return np.isclose(np.trace(gamma).imag, 0) - - def _real_trace_transform(self, mat): - """ - Determine diagonal gate such that - - U3 = D U2 - - Where U3 is a general two-qubit gate which takes 3 cnots, D is a - diagonal gate, and U2 is a gate which takes 2 cnots. - """ - a1 = ( - -mat[1, 3] * mat[2, 0] - + mat[1, 2] * mat[2, 1] - + mat[1, 1] * mat[2, 2] - - mat[1, 0] * mat[2, 3] - ) - a2 = ( - mat[0, 3] * mat[3, 0] - - mat[0, 2] * mat[3, 1] - - mat[0, 1] * mat[3, 2] - + mat[0, 0] * mat[3, 3] - ) - theta = 0 # arbitrary - phi = 0 # arbitrary - psi = np.arctan2(a1.imag + a2.imag, a1.real - a2.real) - phi - diag = np.diag(np.exp(-1j * np.array([theta, phi, psi, -(theta + phi + psi)]))) - return diag - - def __call__(self, mat): - """do the decomposition""" - su4, phase = self._u4_to_su4(mat) - real_map = self._real_trace_transform(su4) - mapped_su4 = real_map @ su4 - if not self._cx2_test(mapped_su4): - warnings.warn("Unitary decomposition up to diagonal may use an additionl CX gate.") - circ = two_qubit_cnot_decompose(mapped_su4) - circ.global_phase += phase - return real_map.conj(), circ - - # This weird duplicated lazy structure is for backwards compatibility; Qiskit has historically # always made ``two_qubit_cnot_decompose`` available publicly immediately on import, but it's quite # expensive to construct, and we want to defer the object's creation until it's actually used. We diff --git a/qiskit/synthesis/unitary/qsd.py b/qiskit/synthesis/unitary/qsd.py index 525daa3caf15..b6b31aaa4fec 100644 --- a/qiskit/synthesis/unitary/qsd.py +++ b/qiskit/synthesis/unitary/qsd.py @@ -30,6 +30,7 @@ from qiskit.circuit.library.generalized_gates.uc_pauli_rot import UCPauliRotGate, _EPS from qiskit.circuit.library.generalized_gates.ucry import UCRYGate from qiskit.circuit.library.generalized_gates.ucrz import UCRZGate +from qiskit._accelerate.two_qubit_decompose import two_qubit_decompose_up_to_diagonal def qs_decomposition( @@ -253,7 +254,7 @@ def _apply_a2(circ): from qiskit.quantum_info import Operator from qiskit.circuit.library.generalized_gates.unitary import UnitaryGate - decomposer = two_qubit_decompose.TwoQubitDecomposeUpToDiagonal() + decomposer = two_qubit_decompose_up_to_diagonal ccirc = transpile(circ, basis_gates=["u", "cx", "qsd2q"], optimization_level=0) ind2q = [] # collect 2q instrs @@ -275,7 +276,8 @@ def _apply_a2(circ): instr2 = ccirc.data[ind2] mat2 = Operator(instr2.operation).data # rollover - dmat, qc2cx = decomposer(mat1) + dmat, qc2cx_data = decomposer(mat1) + qc2cx = QuantumCircuit._from_circuit_data(qc2cx_data) ccirc.data[ind1] = instr1.replace(operation=qc2cx.to_gate()) mat2 = mat2 @ dmat ccirc.data[ind2] = instr2.replace(UnitaryGate(mat2)) diff --git a/test/python/synthesis/test_synthesis.py b/test/python/synthesis/test_synthesis.py index 025b9accf227..05739db249b7 100644 --- a/test/python/synthesis/test_synthesis.py +++ b/test/python/synthesis/test_synthesis.py @@ -16,7 +16,6 @@ import unittest import contextlib import logging -import math import numpy as np import scipy import scipy.stats @@ -62,8 +61,8 @@ TwoQubitControlledUDecomposer, Ud, decompose_two_qubit_product_gate, - TwoQubitDecomposeUpToDiagonal, ) +from qiskit._accelerate.two_qubit_decompose import two_qubit_decompose_up_to_diagonal from qiskit._accelerate.two_qubit_decompose import Specialization from qiskit.synthesis.unitary import qsd from test import combine # pylint: disable=wrong-import-order @@ -1673,149 +1672,23 @@ def test_a2_opt_single_2q(self): class TestTwoQubitDecomposeUpToDiagonal(QiskitTestCase): """test TwoQubitDecomposeUpToDiagonal class""" - def test_prop31(self): - """test proposition III.1: no CNOTs needed""" - dec = TwoQubitDecomposeUpToDiagonal() - # test identity - mat = np.identity(4) - self.assertTrue(dec._cx0_test(mat)) - - sz = np.array([[1, 0], [0, -1]]) - zz = np.kron(sz, sz) - self.assertTrue(dec._cx0_test(zz)) - - had = np.matrix([[1, 1], [1, -1]]) / np.sqrt(2) - hh = np.kron(had, had) - self.assertTrue(dec._cx0_test(hh)) - - sy = np.array([[0, -1j], [1j, 0]]) - hy = np.kron(had, sy) - self.assertTrue(dec._cx0_test(hy)) - - qc = QuantumCircuit(2) - qc.cx(0, 1) - self.assertFalse(dec._cx0_test(Operator(qc).data)) - - def test_prop32_true(self): - """test proposition III.2: 1 CNOT sufficient""" - dec = TwoQubitDecomposeUpToDiagonal() - qc = QuantumCircuit(2) - qc.ry(np.pi / 4, 0) - qc.ry(np.pi / 3, 1) - qc.cx(0, 1) - qc.ry(np.pi / 4, 0) - qc.y(1) - mat = Operator(qc).data - self.assertTrue(dec._cx1_test(mat)) - - qc = QuantumCircuit(2) - qc.ry(np.pi / 5, 0) - qc.ry(np.pi / 3, 1) - qc.cx(1, 0) - qc.ry(np.pi / 2, 0) - qc.y(1) - mat = Operator(qc).data - self.assertTrue(dec._cx1_test(mat)) - - # this SU4 is non-local - mat = scipy.stats.unitary_group.rvs(4, random_state=84) - self.assertFalse(dec._cx1_test(mat)) - - def test_prop32_false(self): - """test proposition III.2: 1 CNOT not sufficient""" - dec = TwoQubitDecomposeUpToDiagonal() - qc = QuantumCircuit(2) - qc.ry(np.pi / 4, 0) - qc.ry(np.pi / 3, 1) - qc.cx(0, 1) - qc.ry(np.pi / 4, 0) - qc.y(1) - qc.cx(0, 1) - qc.ry(np.pi / 3, 0) - qc.rx(np.pi / 2, 1) - mat = Operator(qc).data - self.assertFalse(dec._cx1_test(mat)) - - def test_prop33_true(self): - """test proposition III.3: 2 CNOT sufficient""" - dec = TwoQubitDecomposeUpToDiagonal() - qc = QuantumCircuit(2) - qc.rx(np.pi / 4, 0) - qc.ry(np.pi / 2, 1) - qc.cx(0, 1) - qc.rx(np.pi / 4, 0) - qc.ry(np.pi / 2, 1) - qc.cx(0, 1) - qc.rx(np.pi / 4, 0) - qc.y(1) - mat = Operator(qc).data - self.assertTrue(dec._cx2_test(mat)) - - def test_prop33_false(self): - """test whether circuit which requires 3 cx fails 2 cx test""" - dec = TwoQubitDecomposeUpToDiagonal() - qc = QuantumCircuit(2) - qc.u(0.1, 0.2, 0.3, 0) - qc.u(0.4, 0.5, 0.6, 1) - qc.cx(0, 1) - qc.u(0.1, 0.2, 0.3, 0) - qc.u(0.4, 0.5, 0.6, 1) - qc.cx(0, 1) - qc.u(0.5, 0.2, 0.3, 0) - qc.u(0.2, 0.4, 0.1, 1) - qc.cx(1, 0) - qc.u(0.1, 0.2, 0.3, 0) - qc.u(0.4, 0.5, 0.6, 1) - mat = Operator(qc).data - self.assertFalse(dec._cx2_test(mat)) - - def test_ortho_local_map(self): - """test map of SO4 to SU2⊗SU2""" - dec = TwoQubitDecomposeUpToDiagonal() - emap = np.array([[1, 1j, 0, 0], [0, 0, 1j, 1], [0, 0, 1j, -1], [1, -1j, 0, 0]]) / math.sqrt( - 2 - ) - so4 = scipy.stats.ortho_group.rvs(4, random_state=284) - sy = np.array([[0, -1j], [1j, 0]]) - self.assertTrue(np.allclose(-np.kron(sy, sy), emap @ emap.T)) - self.assertFalse(dec._cx0_test(so4)) - self.assertTrue(dec._cx0_test(emap @ so4 @ emap.T.conj())) - - def test_ortho_local_map2(self): - """test map of SO4 to SU2⊗SU2""" - dec = TwoQubitDecomposeUpToDiagonal() - emap = np.array([[1, 0, 0, 1j], [0, 1j, 1, 0], [0, 1j, -1, 0], [1, 0, 0, -1j]]) / math.sqrt( - 2 - ) - so4 = scipy.stats.ortho_group.rvs(4, random_state=284) - sy = np.array([[0, -1j], [1j, 0]]) - self.assertTrue(np.allclose(-np.kron(sy, sy), emap @ emap.T)) - self.assertFalse(dec._cx0_test(so4)) - self.assertTrue(dec._cx0_test(emap @ so4 @ emap.T.conj())) - - def test_real_trace_transform(self): - """test finding diagonal factor of unitary""" - dec = TwoQubitDecomposeUpToDiagonal() - u4 = scipy.stats.unitary_group.rvs(4, random_state=83) - su4, _ = dec._u4_to_su4(u4) - real_map = dec._real_trace_transform(su4) - self.assertTrue(dec._cx2_test(real_map @ su4)) - def test_call_decompose(self): """ test __call__ method to decompose """ - dec = TwoQubitDecomposeUpToDiagonal() + dec = two_qubit_decompose_up_to_diagonal u4 = scipy.stats.unitary_group.rvs(4, random_state=47) - dmat, circ2cx = dec(u4) + dmat, circ2cx_data = dec(u4) + circ2cx = QuantumCircuit._from_circuit_data(circ2cx_data) dec_diag = dmat @ Operator(circ2cx).data self.assertTrue(Operator(u4) == Operator(dec_diag)) def test_circuit_decompose(self): """test applying decomposed gates as circuit elements""" - dec = TwoQubitDecomposeUpToDiagonal() + dec = two_qubit_decompose_up_to_diagonal u4 = scipy.stats.unitary_group.rvs(4, random_state=47) - dmat, circ2cx = dec(u4) + dmat, circ2cx_data = dec(u4) + circ2cx = QuantumCircuit._from_circuit_data(circ2cx_data) qc1 = QuantumCircuit(2) qc1.append(UnitaryGate(u4), range(2))