diff --git a/qiskit/quantum_info/synthesis/two_qubit_decompose.py b/qiskit/quantum_info/synthesis/two_qubit_decompose.py index 0d3d89cc64b9..2d209ecf97ed 100644 --- a/qiskit/quantum_info/synthesis/two_qubit_decompose.py +++ b/qiskit/quantum_info/synthesis/two_qubit_decompose.py @@ -28,7 +28,7 @@ import io import base64 import warnings -from typing import ClassVar, Optional +from typing import ClassVar, Optional, Type import logging @@ -36,7 +36,7 @@ import scipy.linalg as la from qiskit.circuit.quantumregister import QuantumRegister -from qiskit.circuit.quantumcircuit import QuantumCircuit +from qiskit.circuit.quantumcircuit import QuantumCircuit, Gate from qiskit.circuit.library.standard_gates import CXGate, RXGate, RYGate, RZGate from qiskit.exceptions import QiskitError from qiskit.quantum_info.operators import Operator @@ -556,6 +556,169 @@ def specialize(self): self.K2r = np.asarray(RYGate(k2rtheta)) @ np.asarray(RXGate(k2rlambda)) +class TwoQubitControlledUDecomposer: + """Decompose two-qubit unitary in terms of a desired U ~ Ud(α, 0, 0) ~ Ctrl-U gate + that is locally equivalent to an RXXGate.""" + + def __init__(self, rxx_equivalent_gate: Type[Gate]): + """Initialize the KAK decomposition. + + Args: + rxx_equivalent_gate: Gate that is locally equivalent to an RXXGate: + U ~ Ud(α, 0, 0) ~ Ctrl-U gate. + Raises: + QiskitError: If the gate is not locally equivalent to an RXXGate. + """ + atol = DEFAULT_ATOL + + scales, test_angles, scale = [], [0.2, 0.3, np.pi / 2], None + + for test_angle in test_angles: + # Check that gate takes a single angle parameter + try: + rxx_equivalent_gate(test_angle, label="foo") + except TypeError as _: + raise QiskitError("Equivalent gate needs to take exactly 1 angle parameter.") from _ + decomp = TwoQubitWeylDecomposition(rxx_equivalent_gate(test_angle)) + + circ = QuantumCircuit(2) + circ.rxx(test_angle, 0, 1) + decomposer_rxx = TwoQubitWeylControlledEquiv(Operator(circ).data) + + circ = QuantumCircuit(2) + circ.append(rxx_equivalent_gate(test_angle), qargs=[0, 1]) + decomposer_equiv = TwoQubitWeylControlledEquiv(Operator(circ).data) + + scale = decomposer_rxx.a / decomposer_equiv.a + + if ( + not isinstance(decomp, TwoQubitWeylControlledEquiv) + or abs(decomp.a * 2 - test_angle / scale) > atol + ): + raise QiskitError( + f"{rxx_equivalent_gate.__name__} is not equivalent to an RXXGate." + ) + + scales.append(scale) + + # Check that all three tested angles give the same scale + if not np.allclose(scales, [scale] * len(test_angles)): + raise QiskitError( + f"Cannot initialize {self.__class__.__name__}: with gate {rxx_equivalent_gate}. " + "Inconsistent scaling parameters in checks." + ) + + self.scale = scales[0] + + self.rxx_equivalent_gate = rxx_equivalent_gate + + def __call__(self, unitary, *, atol=DEFAULT_ATOL) -> QuantumCircuit: + """Returns the Weyl decomposition in circuit form. + + Note: atol ist passed to OneQubitEulerDecomposer. + """ + + # pylint: disable=attribute-defined-outside-init + self.decomposer = TwoQubitWeylDecomposition(unitary) + + oneq_decompose = OneQubitEulerDecomposer("ZYZ") + c1l, c1r, c2l, c2r = ( + oneq_decompose(k, atol=atol) + for k in ( + self.decomposer.K1l, + self.decomposer.K1r, + self.decomposer.K2l, + self.decomposer.K2r, + ) + ) + circ = QuantumCircuit(2, global_phase=self.decomposer.global_phase) + circ.compose(c2r, [0], inplace=True) + circ.compose(c2l, [1], inplace=True) + self._weyl_gate(circ) + circ.compose(c1r, [0], inplace=True) + circ.compose(c1l, [1], inplace=True) + return circ + + def _to_rxx_gate(self, angle: float): + """ + Takes an angle and returns the circuit equivalent to an RXXGate with the + RXX equivalent gate as the two-qubit unitary. + + Args: + angle: Rotation angle (in this case one of the Weyl parameters a, b, or c) + + Returns: + Circuit: Circuit equivalent to an RXXGate. + + Raises: + QiskitError: If the circuit is not equivalent to an RXXGate. + """ + + # The user-provided RXXGate equivalent gate may be locally equivalent to the RXXGate + # but with some scaling in the rotation angle. For example, RXXGate(angle) has Weyl + # parameters (angle, 0, 0) for angle in [0, pi/2] but the user provided gate, i.e. + # :code:`self.rxx_equivalent_gate(angle)` might produce the Weyl parameters + # (scale * angle, 0, 0) where scale != 1. This is the case for the CPhaseGate. + + circ = QuantumCircuit(2) + circ.append(self.rxx_equivalent_gate(self.scale * angle), qargs=[0, 1]) + decomposer_inv = TwoQubitWeylControlledEquiv(Operator(circ).data) + + oneq_decompose = OneQubitEulerDecomposer("ZYZ") + + # Express the RXXGate in terms of the user-provided RXXGate equivalent gate. + rxx_circ = QuantumCircuit(2, global_phase=-decomposer_inv.global_phase) + rxx_circ.compose(oneq_decompose(decomposer_inv.K2r).inverse(), inplace=True, qubits=[0]) + rxx_circ.compose(oneq_decompose(decomposer_inv.K2l).inverse(), inplace=True, qubits=[1]) + rxx_circ.compose(circ, inplace=True) + rxx_circ.compose(oneq_decompose(decomposer_inv.K1r).inverse(), inplace=True, qubits=[0]) + rxx_circ.compose(oneq_decompose(decomposer_inv.K1l).inverse(), inplace=True, qubits=[1]) + + return rxx_circ + + def _weyl_gate(self, circ: QuantumCircuit, atol=1.0e-13): + """Appends Ud(a, b, c) to the circuit.""" + + circ_rxx = self._to_rxx_gate(-2 * self.decomposer.a) + circ.compose(circ_rxx, inplace=True) + + # translate the RYYGate(b) into a circuit based on the desired Ctrl-U gate. + if abs(self.decomposer.b) > atol: + circ_ryy = QuantumCircuit(2) + circ_ryy.sdg(0) + circ_ryy.sdg(1) + circ_ryy.compose(self._to_rxx_gate(-2 * self.decomposer.b), inplace=True) + circ_ryy.s(0) + circ_ryy.s(1) + circ.compose(circ_ryy, inplace=True) + + # translate the RZZGate(c) into a circuit based on the desired Ctrl-U gate. + if abs(self.decomposer.c) > atol: + # Since the Weyl chamber is here defined as a > b > |c| we may have + # negative c. This will cause issues in _to_rxx_gate + # as TwoQubitWeylControlledEquiv will map (c, 0, 0) to (|c|, 0, 0). + # We therefore produce RZZGate(|c|) and append its inverse to the + # circuit if c < 0. + gamma, invert = -2 * self.decomposer.c, False + if gamma > 0: + gamma *= -1 + invert = True + + circ_rzz = QuantumCircuit(2) + circ_rzz.h(0) + circ_rzz.h(1) + circ_rzz.compose(self._to_rxx_gate(gamma), inplace=True) + circ_rzz.h(0) + circ_rzz.h(1) + + if invert: + circ.compose(circ_rzz.inverse(), inplace=True) + else: + circ.compose(circ_rzz, inplace=True) + + return circ + + class TwoQubitWeylMirrorControlledEquiv(TwoQubitWeylDecomposition): """U ~ Ud(𝜋/4, 𝜋/4, α) ~ SWAP . Ctrl-U diff --git a/qiskit/transpiler/passes/optimization/echo_rzx_weyl_decomposition.py b/qiskit/transpiler/passes/optimization/echo_rzx_weyl_decomposition.py new file mode 100644 index 000000000000..76e0ecc13ac9 --- /dev/null +++ b/qiskit/transpiler/passes/optimization/echo_rzx_weyl_decomposition.py @@ -0,0 +1,127 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2017, 2021. +# +# 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. + +"""Weyl decomposition of two-qubit gates in terms of echoed cross-resonance gates.""" + +from typing import Tuple + +from qiskit import QuantumRegister +from qiskit.circuit.library.standard_gates import RZXGate, HGate, XGate + +from qiskit.transpiler.basepasses import TransformationPass +from qiskit.transpiler.exceptions import TranspilerError +from qiskit.transpiler.layout import Layout + +from qiskit.dagcircuit import DAGCircuit +from qiskit.converters import circuit_to_dag + +from qiskit.providers import basebackend + +import qiskit.quantum_info as qi +from qiskit.quantum_info.synthesis.two_qubit_decompose import TwoQubitControlledUDecomposer + + +class EchoRZXWeylDecomposition(TransformationPass): + """Rewrite two-qubit gates using the Weyl decomposition. + + This transpiler pass rewrites two-qubit gates in terms of echoed cross-resonance gates according + to the Weyl decomposition. A two-qubit gate will be replaced with at most six non-echoed RZXGates. + Each pair of RZXGates forms an echoed RZXGate. + """ + + def __init__(self, backend: basebackend): + """EchoRZXWeylDecomposition pass.""" + self._inst_map = backend.defaults().instruction_schedule_map + super().__init__() + + def _is_native(self, qubit_pair: Tuple) -> bool: + """Return the direction of the qubit pair that is native, i.e. with the shortest schedule.""" + cx1 = self._inst_map.get("cx", qubit_pair) + cx2 = self._inst_map.get("cx", qubit_pair[::-1]) + return cx1.duration < cx2.duration + + @staticmethod + def _echo_rzx_dag(theta): + rzx_dag = DAGCircuit() + qr = QuantumRegister(2) + rzx_dag.add_qreg(qr) + rzx_dag.apply_operation_back(RZXGate(theta / 2), [qr[0], qr[1]], []) + rzx_dag.apply_operation_back(XGate(), [qr[0]], []) + rzx_dag.apply_operation_back(RZXGate(-theta / 2), [qr[0], qr[1]], []) + rzx_dag.apply_operation_back(XGate(), [qr[0]], []) + return rzx_dag + + @staticmethod + def _reverse_echo_rzx_dag(theta): + reverse_rzx_dag = DAGCircuit() + qr = QuantumRegister(2) + reverse_rzx_dag.add_qreg(qr) + reverse_rzx_dag.apply_operation_back(HGate(), [qr[0]], []) + reverse_rzx_dag.apply_operation_back(HGate(), [qr[1]], []) + reverse_rzx_dag.apply_operation_back(RZXGate(theta / 2), [qr[1], qr[0]], []) + reverse_rzx_dag.apply_operation_back(XGate(), [qr[1]], []) + reverse_rzx_dag.apply_operation_back(RZXGate(-theta / 2), [qr[1], qr[0]], []) + reverse_rzx_dag.apply_operation_back(XGate(), [qr[1]], []) + reverse_rzx_dag.apply_operation_back(HGate(), [qr[0]], []) + reverse_rzx_dag.apply_operation_back(HGate(), [qr[1]], []) + return reverse_rzx_dag + + def run(self, dag: DAGCircuit): + """Run the EchoRZXWeylDecomposition pass on `dag`. + + Rewrites two-qubit gates in an arbitrary circuit in terms of echoed cross-resonance + gates by computing the Weyl decomposition of the corresponding unitary. Modifies the + input dag. + + Args: + dag (DAGCircuit): DAG to rewrite. + + Returns: + DAGCircuit: The modified dag. + + Raises: + TranspilerError: If the circuit cannot be rewritten. + """ + + if len(dag.qregs) > 1: + raise TranspilerError( + "EchoRZXWeylDecomposition expects a single qreg input DAG," + f"but input DAG had qregs: {dag.qregs}." + ) + + trivial_layout = Layout.generate_trivial_layout(*dag.qregs.values()) + + decomposer = TwoQubitControlledUDecomposer(RZXGate) + + for node in dag.two_qubit_ops(): + + unitary = qi.Operator(node.op).data + dag_weyl = circuit_to_dag(decomposer(unitary)) + dag.substitute_node_with_dag(node, dag_weyl) + + for node in dag.two_qubit_ops(): + if node.name == "rzx": + control = node.qargs[0] + target = node.qargs[1] + + physical_q0 = trivial_layout[control] + physical_q1 = trivial_layout[target] + + is_native = self._is_native((physical_q0, physical_q1)) + + theta = node.op.params[0] + if is_native: + dag.substitute_node_with_dag(node, self._echo_rzx_dag(theta)) + else: + dag.substitute_node_with_dag(node, self._reverse_echo_rzx_dag(theta)) + + return dag diff --git a/releasenotes/notes/echo-rzx-weyl-decomposition-ef72345a58bea9e0.yaml b/releasenotes/notes/echo-rzx-weyl-decomposition-ef72345a58bea9e0.yaml new file mode 100644 index 000000000000..44fcb9695ea5 --- /dev/null +++ b/releasenotes/notes/echo-rzx-weyl-decomposition-ef72345a58bea9e0.yaml @@ -0,0 +1,4 @@ +--- +features: + - | + Added a new transpiler pass :class:`qiskit.transpiler.passes.optimization.EchoRZXWeylDecomposition` that allows users to decompose an arbitrary two-qubit gate in terms of echoed RZX-gates by leveraging Cartan's decomposition. In combination with other transpiler passes this can be used to transpile arbitrary circuits to RZX-gate-based and pulse-efficient circuits that implement the same unitary. diff --git a/test/python/quantum_info/test_synthesis.py b/test/python/quantum_info/test_synthesis.py index ae59793b276e..c9d85a9a4290 100644 --- a/test/python/quantum_info/test_synthesis.py +++ b/test/python/quantum_info/test_synthesis.py @@ -37,7 +37,13 @@ CXGate, CZGate, iSwapGate, + SwapGate, RXXGate, + RYYGate, + RZZGate, + RZXGate, + CPhaseGate, + CRZGate, RXGate, RYGate, RZGate, @@ -60,6 +66,7 @@ TwoQubitWeylGeneral, two_qubit_cnot_decompose, TwoQubitBasisDecomposer, + TwoQubitControlledUDecomposer, Ud, decompose_two_qubit_product_gate, ) @@ -1302,6 +1309,29 @@ def test_approx_supercontrolled_decompose_phase_3_use_random(self, seed, delta=0 self.check_approx_decomposition(tgt_unitary, decomposer, num_basis_uses=3) +@ddt +class TestTwoQubitControlledUDecompose(CheckDecompositions): + """Test TwoQubitControlledUDecomposer() for exact decompositions and raised exceptions""" + + @combine(seed=range(10), name="seed_{seed}") + def test_correct_unitary(self, seed): + """Verify unitary for different gates in the decomposition""" + unitary = random_unitary(4, seed=seed) + for gate in [RXXGate, RYYGate, RZZGate, RZXGate, CPhaseGate, CRZGate]: + decomposer = TwoQubitControlledUDecomposer(gate) + circ = decomposer(unitary) + self.assertEqual(Operator(unitary), Operator(circ)) + + def test_not_rxx_equivalent(self): + """Test that an exception is raised if the gate is not equivalent to an RXXGate""" + gate = SwapGate + with self.assertRaises(QiskitError) as exc: + TwoQubitControlledUDecomposer(gate) + self.assertIn( + "Equivalent gate needs to take exactly 1 angle parameter.", exc.exception.message + ) + + class TestDecomposeProductRaises(QiskitTestCase): """Check that exceptions are raised when 2q matrix is not a product of 1q unitaries""" diff --git a/test/python/transpiler/test_echo_rzx_weyl_decomposition.py b/test/python/transpiler/test_echo_rzx_weyl_decomposition.py new file mode 100644 index 000000000000..b9a38871bbad --- /dev/null +++ b/test/python/transpiler/test_echo_rzx_weyl_decomposition.py @@ -0,0 +1,232 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2017, 2021. +# +# 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 the EchoRZXWeylDecomposition pass""" + +import unittest +from math import pi +import numpy as np + +from qiskit import QuantumRegister, QuantumCircuit + +from qiskit.transpiler.passes.optimization.echo_rzx_weyl_decomposition import ( + EchoRZXWeylDecomposition, +) +from qiskit.converters import circuit_to_dag, dag_to_circuit +from qiskit.test import QiskitTestCase +from qiskit.test.mock import FakeParis + +import qiskit.quantum_info as qi + +from qiskit.quantum_info.synthesis.two_qubit_decompose import ( + TwoQubitWeylDecomposition, +) + + +class TestEchoRZXWeylDecomposition(QiskitTestCase): + """Tests the EchoRZXWeylDecomposition pass.""" + + def setUp(self): + super().setUp() + self.backend = FakeParis() + + def assertRZXgates(self, unitary_circuit, after): + """Check the number of rzx gates""" + alpha = TwoQubitWeylDecomposition(unitary_circuit).a + beta = TwoQubitWeylDecomposition(unitary_circuit).b + gamma = TwoQubitWeylDecomposition(unitary_circuit).c + + expected_rzx_number = 0 + if not alpha == 0: + expected_rzx_number += 2 + if not beta == 0: + expected_rzx_number += 2 + if not gamma == 0: + expected_rzx_number += 2 + + circuit_rzx_number = QuantumCircuit.count_ops(after)["rzx"] + + self.assertEqual(expected_rzx_number, circuit_rzx_number) + + @staticmethod + def count_gate_number(gate, circuit): + """Count the number of a specific gate type in a circuit""" + if gate not in QuantumCircuit.count_ops(circuit): + gate_number = 0 + else: + gate_number = QuantumCircuit.count_ops(circuit)[gate] + return gate_number + + def test_rzx_number_native_weyl_decomposition(self): + """Check the number of RZX gates for a hardware-native cx""" + qr = QuantumRegister(2, "qr") + circuit = QuantumCircuit(qr) + circuit.cx(qr[0], qr[1]) + + unitary_circuit = qi.Operator(circuit).data + + after = EchoRZXWeylDecomposition(self.backend)(circuit) + + unitary_after = qi.Operator(after).data + + self.assertTrue(np.allclose(unitary_circuit, unitary_after)) + + # check whether the after circuit has the correct number of rzx gates. + self.assertRZXgates(unitary_circuit, after) + + def test_h_number_non_native_weyl_decomposition_1(self): + """Check the number of added Hadamard gates for a native and non-native rzz gate""" + theta = pi / 11 + qr = QuantumRegister(2, "qr") + # rzz gate in native direction + circuit = QuantumCircuit(qr) + circuit.rzz(theta, qr[0], qr[1]) + + # rzz gate in non-native direction + circuit_non_native = QuantumCircuit(qr) + circuit_non_native.rzz(theta, qr[1], qr[0]) + + dag = circuit_to_dag(circuit) + pass_ = EchoRZXWeylDecomposition(self.backend) + after = dag_to_circuit(pass_.run(dag)) + + dag_non_native = circuit_to_dag(circuit_non_native) + pass_ = EchoRZXWeylDecomposition(self.backend) + after_non_native = dag_to_circuit(pass_.run(dag_non_native)) + + circuit_rzx_number = self.count_gate_number("rzx", after) + + circuit_h_number = self.count_gate_number("h", after) + circuit_non_native_h_number = self.count_gate_number("h", after_non_native) + + # for each pair of rzx gates four hadamard gates have to be added in + # the case of a non-hardware-native directed gate. + self.assertEqual( + (circuit_rzx_number / 2) * 4, circuit_non_native_h_number - circuit_h_number + ) + + def test_h_number_non_native_weyl_decomposition_2(self): + """Check the number of added Hadamard gates for a swap gate""" + qr = QuantumRegister(2, "qr") + # swap gate in native direction. + circuit = QuantumCircuit(qr) + circuit.swap(qr[0], qr[1]) + + # swap gate in non-native direction. + circuit_non_native = QuantumCircuit(qr) + circuit_non_native.swap(qr[1], qr[0]) + + dag = circuit_to_dag(circuit) + pass_ = EchoRZXWeylDecomposition(self.backend) + after = dag_to_circuit(pass_.run(dag)) + + dag_non_native = circuit_to_dag(circuit_non_native) + pass_ = EchoRZXWeylDecomposition(self.backend) + after_non_native = dag_to_circuit(pass_.run(dag_non_native)) + + circuit_rzx_number = self.count_gate_number("rzx", after) + + circuit_h_number = self.count_gate_number("h", after) + circuit_non_native_h_number = self.count_gate_number("h", after_non_native) + + # for each pair of rzx gates four hadamard gates have to be added in + # the case of a non-hardware-native directed gate. + self.assertEqual( + (circuit_rzx_number / 2) * 4, circuit_non_native_h_number - circuit_h_number + ) + + def test_weyl_decomposition_gate_angles(self): + """Check the number and angles of the RZX gates for different gates""" + thetas = [pi / 9, 2.1, -0.2] + + qr = QuantumRegister(2, "qr") + circuit_rxx = QuantumCircuit(qr) + circuit_rxx.rxx(thetas[0], qr[1], qr[0]) + + circuit_ryy = QuantumCircuit(qr) + circuit_ryy.ryy(thetas[1], qr[0], qr[1]) + + circuit_rzz = QuantumCircuit(qr) + circuit_rzz.rzz(thetas[2], qr[1], qr[0]) + + circuits = [circuit_rxx, circuit_ryy, circuit_rzz] + + for circuit in circuits: + + unitary_circuit = qi.Operator(circuit).data + + dag = circuit_to_dag(circuit) + pass_ = EchoRZXWeylDecomposition(self.backend) + after = dag_to_circuit(pass_.run(dag)) + dag_after = circuit_to_dag(after) + + unitary_after = qi.Operator(after).data + + # check whether the unitaries are equivalent. + self.assertTrue(np.allclose(unitary_circuit, unitary_after)) + + # check whether the after circuit has the correct number of rzx gates. + self.assertRZXgates(unitary_circuit, after) + + alpha = TwoQubitWeylDecomposition(unitary_circuit).a + + rzx_angles = [] + for node in dag_after.two_qubit_ops(): + if node.name == "rzx": + rzx_angle = node.op.params[0] + # check whether the absolute values of the RZX gate angles + # are equivalent to the corresponding Weyl parameter. + self.assertAlmostEqual(np.abs(rzx_angle), alpha) + rzx_angles.append(rzx_angle) + + # check whether the angles of every RZX gate pair of an echoed RZX gate + # have opposite signs. + for idx in range(1, len(rzx_angles), 2): + self.assertAlmostEqual(rzx_angles[idx - 1], -rzx_angles[idx]) + + def test_weyl_unitaries_random_circuit(self): + """Weyl decomposition for a random two-qubit circuit.""" + theta = pi / 9 + epsilon = 5 + delta = -1 + eta = 0.2 + qr = QuantumRegister(2, "qr") + circuit = QuantumCircuit(qr) + + # random two-qubit circuit. + circuit.rzx(theta, 0, 1) + circuit.rzz(epsilon, 0, 1) + circuit.rz(eta, 0) + circuit.swap(1, 0) + circuit.h(0) + circuit.rzz(delta, 1, 0) + circuit.swap(0, 1) + circuit.cx(1, 0) + circuit.swap(0, 1) + circuit.h(1) + circuit.rxx(theta, 0, 1) + circuit.ryy(theta, 1, 0) + circuit.ecr(0, 1) + + unitary_circuit = qi.Operator(circuit).data + + dag = circuit_to_dag(circuit) + pass_ = EchoRZXWeylDecomposition(self.backend) + after = dag_to_circuit(pass_.run(dag)) + + unitary_after = qi.Operator(after).data + + self.assertTrue(np.allclose(unitary_circuit, unitary_after)) + + +if __name__ == "__main__": + unittest.main()