From 55514cd5b4d3e8ca854e5813bb99600ad1c41be0 Mon Sep 17 00:00:00 2001 From: catornow Date: Wed, 21 Jul 2021 17:24:07 +0200 Subject: [PATCH 01/57] Added new EchoRZXWeylDecomposition transpiler pass --- .../echo_rzx_weyl_decomposition.py | 84 +++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 qiskit/transpiler/passes/optimization/echo_rzx_weyl_decomposition.py 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..149f2f0f3771 --- /dev/null +++ b/qiskit/transpiler/passes/optimization/echo_rzx_weyl_decomposition.py @@ -0,0 +1,84 @@ +# 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 qiskit.transpiler.basepasses import TransformationPass +from qiskit.transpiler.exceptions import TranspilerError + +from qiskit.dagcircuit import DAGCircuit +from qiskit.converters import circuit_to_dag + +from qiskit.transpiler.layout import Layout + +import qiskit.quantum_info as qi + +from qiskit.quantum_info.synthesis.two_qubit_decompose import TwoQubitWeylEchoRZX + + +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, inst_map): + """EchoRZXWeylDecomposition pass.""" + self.inst_map = inst_map + super().__init__() + + def run(self, dag): + """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 map. + + Returns: + DAGCircuit: The rearranged dag. + + Raises: + TranspilerError: If the circuit cannot be mapped. + """ + + 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()) + + for node in dag.two_qubit_ops(): + if node.type == "op": + control = node.qargs[0] + target = node.qargs[1] + + physical_q0 = trivial_layout[control] + physical_q1 = trivial_layout[target] + + qubit_pair = (physical_q0, physical_q1) + + unitary = qi.Operator(node.op).data + dag_weyl = circuit_to_dag( + TwoQubitWeylEchoRZX( + unitary, inst_map=self.inst_map, qubit_pair=qubit_pair + ).circuit() + ) + dag.substitute_node_with_dag(node, dag_weyl) + + return dag From b60171d1254da10db4e32a368d1a1efdcd310622 Mon Sep 17 00:00:00 2001 From: catornow Date: Wed, 21 Jul 2021 17:27:27 +0200 Subject: [PATCH 02/57] Adapted two_qubit_decompose and calibration_creators and added tests --- .../synthesis/two_qubit_decompose.py | 361 ++++-------------- .../passes/scheduling/calibration_creators.py | 53 +-- .../test_echo_rzx_weyl_decomposition.py | 202 ++++++++++ 3 files changed, 305 insertions(+), 311 deletions(-) create mode 100644 test/python/transpiler/test_echo_rzx_weyl_decomposition.py diff --git a/qiskit/quantum_info/synthesis/two_qubit_decompose.py b/qiskit/quantum_info/synthesis/two_qubit_decompose.py index 61b5da2f1006..9d22ece73947 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, Tuple import logging @@ -41,10 +41,7 @@ from qiskit.exceptions import QiskitError from qiskit.quantum_info.operators import Operator from qiskit.quantum_info.synthesis.weyl import weyl_coordinates -from qiskit.quantum_info.synthesis.one_qubit_decompose import ( - OneQubitEulerDecomposer, - DEFAULT_ATOL, -) +from qiskit.quantum_info.synthesis.one_qubit_decompose import OneQubitEulerDecomposer, DEFAULT_ATOL logger = logging.getLogger(__name__) @@ -132,12 +129,12 @@ def __init_subclass__(cls, **kwargs): Make explicitly-instantiated subclass __new__ call base __new__ with fidelity=None""" super().__init_subclass__(**kwargs) - cls.__new__ = lambda cls, *a, fidelity=None, **k: TwoQubitWeylDecomposition.__new__( - cls, *a, fidelity=None, **k + cls.__new__ = lambda cls, *a, fidelity=None, inst_map=None, qubit_pair=None, **k: TwoQubitWeylDecomposition.__new__( + cls, *a, fidelity=None, inst_map=None, qubit_pair=None, **k ) @staticmethod - def __new__(cls, unitary_matrix, *, fidelity=(1.0 - 1.0e-9)): + def __new__(cls, unitary_matrix, *, fidelity=(1.0 - 1.0e-9), inst_map=None, qubit_pair=None): """Perform the Weyl chamber decomposition, and optionally choose a specialized subclass. The flip into the Weyl Chamber is described in B. Kraus and J. I. Cirac, Phys. Rev. A 63, @@ -547,6 +544,73 @@ def specialize(self): self.K2r = np.asarray(RYGate(k2rtheta)) @ np.asarray(RXGate(k2rlambda)) +class TwoQubitWeylEchoRZX(TwoQubitWeylDecomposition): + """Decompose two-qubit unitary in terms of echoed cross-resonance gates.""" + + def __init__(self, unitary, inst_map, qubit_pair: Tuple): + self.inst_map = inst_map + self.qubit_pair = qubit_pair + super().__init__(unitary) + + def specialize(self): + pass # Nothing to do + + @staticmethod + def _apply_rzx(circ: QuantumCircuit, angle: float): + """Echoed RZX gate""" + circ.rzx(-angle, 0, 1) + circ.x(0) + circ.rzx(angle, 0, 1) + circ.x(0) + + @staticmethod + def _apply_reverse_rzx(circ: QuantumCircuit, angle: float): + """Reverse direction of the echoed RZX gate""" + circ.h(0) + circ.h(1) + circ.rzx(-angle, 1, 0) + circ.x(1) + circ.rzx(angle, 1, 0) + circ.x(1) + circ.h(0) + circ.h(1) + + def is_native_cx(self, qubit_pair: Tuple) -> bool: + """Check that a CX for a qubit pair is native.""" + cx1 = self.inst_map.get("cx", qubit_pair) + cx2 = self.inst_map.get("cx", qubit_pair[::-1]) + return cx1.duration < cx2.duration + + def _weyl_gate(self, simplify, circ: QuantumCircuit, atol): + """Appends Ud(a, b, c) to the circuit.""" + del simplify + circ.h(0) + if abs(self.a) > atol: + if self.is_native_cx(self.qubit_pair): + self._apply_rzx(circ, self.a) + else: + self._apply_reverse_rzx(circ, self.a) + circ.h(0) + circ.sdg(0) + circ.h(0) + circ.sdg(1) + if abs(self.b) > atol: + if self.is_native_cx(self.qubit_pair): + self._apply_rzx(circ, self.b) + else: + self._apply_reverse_rzx(circ, self.b) + circ.h(0) + circ.s(0) + circ.s(1) + circ.h(1) + if abs(self.c) > atol: + if self.is_native_cx(self.qubit_pair): + self._apply_rzx(circ, self.c) + else: + self._apply_reverse_rzx(circ, self.c) + circ.h(1) + + class TwoQubitWeylMirrorControlledEquiv(TwoQubitWeylDecomposition): """U ~ Ud(𝜋/4, 𝜋/4, α) ~ SWAP . Ctrl-U @@ -678,17 +742,11 @@ class TwoQubitBasisDecomposer: euler_basis (str): Basis string to be provided to OneQubitEulerDecomposer for 1Q synthesis. Valid options are ['ZYZ', 'ZXZ', 'XYX', 'U', 'U3', 'U1X', 'PSX', 'ZSX', 'RR']. Default 'U3'. - pulse_optimize (None or bool): If True, try to do decomposition which minimizes - local unitaries in between entangling gates. This will raise an exception if an - optimal decomposition is not implemented. Currently, only [{CX, SX, RZ}] is known. - If False, don't attempt optimization. If None, attempt optimization but don't raise - if unknown. """ - def __init__(self, gate, basis_fidelity=1.0, euler_basis=None, pulse_optimize=None): + def __init__(self, gate, basis_fidelity=1.0, euler_basis=None): self.gate = gate self.basis_fidelity = basis_fidelity - self.pulse_optimize = pulse_optimize basis = self.basis = TwoQubitWeylDecomposition(Operator(gate).data) if euler_basis is not None: @@ -812,7 +870,6 @@ def __init__(self, gate, basis_fidelity=1.0, euler_basis=None, pulse_optimize=No self.decomp2_supercontrolled, self.decomp3_supercontrolled, ] - self._rqc = None def traces(self, target): """Give the expected traces :math:`|Tr(U \\cdot Utarget^dag)|` for different number of @@ -922,22 +979,9 @@ def __call__(self, target, basis_fidelity=None, *, _num_basis_uses=None) -> Quan if _num_basis_uses is not None: best_nbasis = _num_basis_uses decomposition = self.decomposition_fns[best_nbasis](target_decomposed) + decomposition_euler = [self._decomposer1q._decompose(x) for x in decomposition] - # attempt pulse optimal decomposition - try: - if self.pulse_optimize in {None, True}: - return_circuit = self._pulse_optimal_chooser( - best_nbasis, decomposition, target_decomposed - ) - if return_circuit: - return return_circuit - except QiskitError: - if self.pulse_optimize: - raise - - # do default decomposition q = QuantumRegister(2) - decomposition_euler = [self._decomposer1q._decompose(x) for x in decomposition] return_circuit = QuantumCircuit(q) return_circuit.global_phase = target_decomposed.global_phase return_circuit.global_phase -= best_nbasis * self.basis.global_phase @@ -949,263 +993,8 @@ def __call__(self, target, basis_fidelity=None, *, _num_basis_uses=None) -> Quan return_circuit.append(self.gate, [q[0], q[1]]) return_circuit.compose(decomposition_euler[2 * best_nbasis], [q[0]], inplace=True) return_circuit.compose(decomposition_euler[2 * best_nbasis + 1], [q[1]], inplace=True) - return return_circuit - - def _pulse_optimal_chooser(self, best_nbasis, decomposition, target_decomposed): - """Determine method to find pulse optimal circuit. This method may be - removed once a more general approach is used. - - Returns: - QuantumCircuit: pulse optimal quantum circuit. - None: Probably nbasis=1 and original circuit is fine. - - Raises: - QiskitError: Decomposition for selected basis not implemented. - """ - circuit = None - if self.pulse_optimize and best_nbasis in {0, 1}: - # already pulse optimal - return None - elif self.pulse_optimize and best_nbasis > 3: - raise QiskitError( - f"Unexpected number of entangling gates ({best_nbasis}) in decomposition." - ) - if self._decomposer1q.basis in {"ZSX", "ZSXX"}: - if isinstance(self.gate, CXGate): - if best_nbasis == 3: - circuit = self._get_sx_vz_3cx_efficient_euler(decomposition, target_decomposed) - elif best_nbasis == 2: - circuit = self._get_sx_vz_2cx_efficient_euler(decomposition, target_decomposed) - else: - raise QiskitError("pulse_optimizer currently only works with CNOT entangling gate") - else: - raise QiskitError( - '"pulse_optimize" currently only works with ZSX basis ' - f"({self._decomposer1q.basis} used)" - ) - return circuit - - def _get_sx_vz_2cx_efficient_euler(self, decomposition, target_decomposed): - """ - Decomposition of SU(4) gate for device with SX, virtual RZ, and CNOT gates assuming - two CNOT gates are needed. - - This first decomposes each unitary from the KAK decomposition into ZXZ on the source - qubit of the CNOTs and XZX on the targets in order to commute operators to beginning and - end of decomposition. The beginning and ending single qubit gates are then - collapsed and re-decomposed with the single qubit decomposer. This last step could be avoided - if performance is a concern. - """ - best_nbasis = 2 # by assumption - num_1q_uni = len(decomposition) - # list of euler angle decompositions on qubits 0 and 1 - euler_q0 = np.empty((num_1q_uni // 2, 3), dtype=float) - euler_q1 = np.empty((num_1q_uni // 2, 3), dtype=float) - global_phase = 0.0 - - # decompose source unitaries to zxz - zxz_decomposer = OneQubitEulerDecomposer("ZXZ") - for iqubit, decomp in enumerate(decomposition[0::2]): - euler_angles = zxz_decomposer.angles_and_phase(decomp) - euler_q0[iqubit, [1, 2, 0]] = euler_angles[:3] - global_phase += euler_angles[3] - # decompose target unitaries to xzx - xzx_decomposer = OneQubitEulerDecomposer("XZX") - for iqubit, decomp in enumerate(decomposition[1::2]): - euler_angles = xzx_decomposer.angles_and_phase(decomp) - euler_q1[iqubit, [1, 2, 0]] = euler_angles[:3] - global_phase += euler_angles[3] - qc = QuantumCircuit(2) - qc.global_phase = target_decomposed.global_phase - qc.global_phase -= best_nbasis * self.basis.global_phase - qc.global_phase += global_phase - - # TODO: make this more effecient to avoid double decomposition - # prepare beginning 0th qubit local unitary - circ = QuantumCircuit(1) - circ.rz(euler_q0[0][0], 0) - circ.rx(euler_q0[0][1], 0) - circ.rz(euler_q0[0][2] + euler_q0[1][0] + math.pi / 2, 0) - # re-decompose to basis of 1q decomposer - qceuler = self._decomposer1q(Operator(circ).data) - qc.compose(qceuler, [0], inplace=True) - - # prepare beginning 1st qubit local unitary - circ = QuantumCircuit(1) - circ.rx(euler_q1[0][0], 0) - circ.rz(euler_q1[0][1], 0) - circ.rx(euler_q1[0][2] + euler_q1[1][0], 0) - qceuler = self._decomposer1q(Operator(circ).data) - qc.compose(qceuler, [1], inplace=True) - - qc.cx(0, 1) - # the central decompositions are dependent on the specific form of the - # unitaries coming out of the two qubit decomposer which have some flexibility - # of choice. - qc.sx(0) - qc.rz(euler_q0[1][1] - math.pi, 0) - qc.sx(0) - qc.rz(euler_q1[1][1], 1) - qc.global_phase += math.pi / 2 - - qc.cx(0, 1) - - circ = QuantumCircuit(1) - circ.rz(euler_q0[1][2] + euler_q0[2][0] + math.pi / 2, 0) - circ.rx(euler_q0[2][1], 0) - circ.rz(euler_q0[2][2], 0) - qceuler = self._decomposer1q(Operator(circ).data) - qc.compose(qceuler, [0], inplace=True) - circ = QuantumCircuit(1) - circ.rx(euler_q1[1][2] + euler_q1[2][0], 0) - circ.rz(euler_q1[2][1], 0) - circ.rx(euler_q1[2][2], 0) - qceuler = self._decomposer1q(Operator(circ).data) - qc.compose(qceuler, [1], inplace=True) - - return qc - - def _get_sx_vz_3cx_efficient_euler(self, decomposition, target_decomposed): - """ - Decomposition of SU(4) gate for device with SX, virtual RZ, and CNOT gates assuming - three CNOT gates are needed. - - This first decomposes each unitary from the KAK decomposition into ZXZ on the source - qubit of the CNOTs and XZX on the targets in order commute operators to beginning and - end of decomposition. Inserting Hadamards reverses the direction of the CNOTs and transforms - a variable Rx -> variable virtual Rz. The beginning and ending single qubit gates are then - collapsed and re-decomposed with the single qubit decomposer. This last step could be avoided - if performance is a concern. - """ - best_nbasis = 3 # by assumption - num_1q_uni = len(decomposition) - # create structure to hold euler angles: 1st index represents unitary "group" wrt cx - # 2nd index represents index of euler triple. - euler_q0 = np.empty((num_1q_uni // 2, 3), dtype=float) - euler_q1 = np.empty((num_1q_uni // 2, 3), dtype=float) - global_phase = 0.0 - atol = 1e-10 # absolute tolerance for floats - - # decompose source unitaries to zxz - zxz_decomposer = OneQubitEulerDecomposer("ZXZ") - for iqubit, decomp in enumerate(decomposition[0::2]): - euler_angles = zxz_decomposer.angles_and_phase(decomp) - euler_q0[iqubit, [1, 2, 0]] = euler_angles[:3] - global_phase += euler_angles[3] - # decompose target unitaries to xzx - xzx_decomposer = OneQubitEulerDecomposer("XZX") - for iqubit, decomp in enumerate(decomposition[1::2]): - euler_angles = xzx_decomposer.angles_and_phase(decomp) - euler_q1[iqubit, [1, 2, 0]] = euler_angles[:3] - global_phase += euler_angles[3] - - qc = QuantumCircuit(2) - qc.global_phase = target_decomposed.global_phase - qc.global_phase -= best_nbasis * self.basis.global_phase - qc.global_phase += global_phase - - x12 = euler_q0[1][2] + euler_q0[2][0] - x12_isNonZero = not math.isclose(x12, 0, abs_tol=atol) - x12_isOddMult = None - x12_isPiMult = math.isclose(math.sin(x12), 0, abs_tol=atol) - if x12_isPiMult: - x12_isOddMult = math.isclose(math.cos(x12), -1, abs_tol=atol) - x12_phase = math.pi * math.cos(x12) - x02_add = x12 - euler_q0[1][0] - x12_isHalfPi = math.isclose(x12, math.pi / 2, abs_tol=atol) - - # TODO: make this more effecient to avoid double decomposition - circ = QuantumCircuit(1) - circ.rz(euler_q0[0][0], 0) - circ.rx(euler_q0[0][1], 0) - if x12_isNonZero and x12_isPiMult: - circ.rz(euler_q0[0][2] - x02_add, 0) - else: - circ.rz(euler_q0[0][2] + euler_q0[1][0], 0) - circ.h(0) - qceuler = self._decomposer1q(Operator(circ).data) - qc.compose(qceuler, [0], inplace=True) - - circ = QuantumCircuit(1) - circ.rx(euler_q1[0][0], 0) - circ.rz(euler_q1[0][1], 0) - circ.rx(euler_q1[0][2] + euler_q1[1][0], 0) - circ.h(0) - qceuler = self._decomposer1q(Operator(circ).data) - qc.compose(qceuler, [1], inplace=True) - - qc.cx(1, 0) - if x12_isPiMult: - # even or odd multiple - if x12_isNonZero: - qc.global_phase += x12_phase - if x12_isNonZero and x12_isOddMult: - qc.rz(-euler_q0[1][1], 0) - else: - qc.rz(euler_q0[1][1], 0) - qc.global_phase += math.pi - if x12_isHalfPi: - qc.sx(0) - qc.global_phase -= math.pi / 4 - elif x12_isNonZero and not x12_isPiMult: - # this is non-optimal but doesn't seem to occur currently - if self.pulse_optimize is None: - qc.compose(self._decomposer1q(Operator(RXGate(x12)).data), [0], inplace=True) - else: - raise QiskitError("possible non-pulse-optimal decomposition encountered") - if math.isclose(euler_q1[1][1], math.pi / 2, abs_tol=atol): - qc.sx(1) - qc.global_phase -= math.pi / 4 - else: - # this is non-optimal but doesn't seem to occur currently - if self.pulse_optimize is None: - qc.compose( - self._decomposer1q(Operator(RXGate(euler_q1[1][1])).data), [1], inplace=True - ) - else: - raise QiskitError("possible non-pulse-optimal decomposition encountered") - qc.rz(euler_q1[1][2] + euler_q1[2][0], 1) - - qc.cx(1, 0) - - qc.rz(euler_q0[2][1], 0) - if math.isclose(euler_q1[2][1], math.pi / 2, abs_tol=atol): - qc.sx(1) - qc.global_phase -= math.pi / 4 - else: - # this is non-optimal but doesn't seem to occur currently - if self.pulse_optimize is None: - qc.compose( - self._decomposer1q(Operator(RXGate(euler_q1[2][1])).data), [1], inplace=True - ) - else: - raise QiskitError("possible non-pulse-optimal decomposition encountered") - - qc.cx(1, 0) - - circ = QuantumCircuit(1) - circ.h(0) - circ.rz(euler_q0[2][2] + euler_q0[3][0], 0) - circ.rx(euler_q0[3][1], 0) - circ.rz(euler_q0[3][2], 0) - qceuler = self._decomposer1q(Operator(circ).data) - qc.compose(qceuler, [0], inplace=True) - - circ = QuantumCircuit(1) - circ.h(0) - circ.rx(euler_q1[2][2] + euler_q1[3][0], 0) - circ.rz(euler_q1[3][1], 0) - circ.rx(euler_q1[3][2], 0) - qceuler = self._decomposer1q(Operator(circ).data) - qc.compose(qceuler, [1], inplace=True) - - # TODO: fix the sign problem to avoid correction here - if cmath.isclose( - target_decomposed.unitary_matrix[0, 0], -(Operator(qc).data[0, 0]), abs_tol=atol - ): - qc.global_phase += math.pi - return qc + return return_circuit def num_basis_gates(self, unitary): """Computes the number of basis gates needed in diff --git a/qiskit/transpiler/passes/scheduling/calibration_creators.py b/qiskit/transpiler/passes/scheduling/calibration_creators.py index 4c138de25739..94278ffc9381 100644 --- a/qiskit/transpiler/passes/scheduling/calibration_creators.py +++ b/qiskit/transpiler/passes/scheduling/calibration_creators.py @@ -13,7 +13,7 @@ """Calibration creators.""" import math -from typing import List, Union +from typing import List, Union, Optional, Dict, Tuple from abc import abstractmethod import numpy as np @@ -28,7 +28,7 @@ ) from qiskit.pulse.instructions.instruction import Instruction from qiskit.exceptions import QiskitError -from qiskit.providers import basebackend +from qiskit.providers import BaseBackend from qiskit.dagcircuit import DAGNode from qiskit.circuit.library.standard_gates import RZXGate from qiskit.transpiler.basepasses import TransformationPass @@ -64,7 +64,7 @@ def run(self, dag): schedule = self.get_calibration(params, qubits) - dag.add_calibration(node.op, qubits, schedule, params=params) + dag.add_calibration(node.op.name, qubits, schedule, params=params) return dag @@ -82,26 +82,37 @@ class RZXCalibrationBuilder(CalibrationCreator): angle. Additional details can be found in https://arxiv.org/abs/2012.11660. """ - def __init__(self, backend: basebackend): + def __init__( + self, backend: Optional[BaseBackend] = None, + inst_map: Optional[Dict[str, Dict[Tuple[int], Schedule]]] = None + ): """ Initializes a RZXGate calibration builder. Args: + inst_map: Instruction schedule map. backend: Backend for which to construct the gates. Raises: QiskitError: if open pulse is not supported by the backend. """ + super().__init__() - if not backend.configuration().open_pulse: + + if backend is not None: + self._inst_map = backend.defaults().instruction_schedule_map + if not backend.configuration().open_pulse: + raise QiskitError( + "Calibrations can only be added to Pulse-enabled backends, " + "but {} is not enabled with Pulse.".format(backend.name()) + ) + elif inst_map is not None: + self._inst_map = inst_map + else: raise QiskitError( - "Calibrations can only be added to Pulse-enabled backends, " - "but {} is not enabled with Pulse.".format(backend.name()) - ) + "Either a backend or an instruction schedule map must be specified.") - self._inst_map = backend.defaults().instruction_schedule_map - self._config = backend.configuration() - self._channel_map = backend.configuration().qubit_channel_mapping + # self._inst_map = inst_map def supported(self, node_op: DAGNode) -> bool: """ @@ -269,13 +280,11 @@ class RZXCalibrationBuilderNoEcho(RZXCalibrationBuilder): """ Creates calibrations for RZXGate(theta) by stretching and compressing Gaussian square pulses in the CX gate. - - The ``RZXCalibrationBuilderNoEcho`` is a variation of the - :class:`~qiskit.transpiler.passes.RZXCalibrationBuilder` pass - that creates calibrations for the cross-resonance pulses without inserting + The RZXCalibrationBuilderNoEcho is a variation of the RZXCalibrationBuilder + as it creates calibrations for the cross-resonance pulses without inserting the echo pulses in the pulse schedule. This enables exposing the echo in the cross-resonance sequence as gates so that the transpiler can simplify them. - The ``RZXCalibrationBuilderNoEcho`` only supports the hardware-native direction + The RZXCalibrationBuilderNoEcho only supports the hardware-native direction of the CX gate. """ @@ -283,13 +292,11 @@ class RZXCalibrationBuilderNoEcho(RZXCalibrationBuilder): def _filter_control(inst: (int, Union["Schedule", Instruction])) -> bool: """ Looks for Gaussian square pulses applied to control channels. - Args: inst: Instructions to be filtered. - Returns: match: True if the instruction is a Play instruction with - a Gaussian square pulse on the ControlChannel. + a Gaussian square pulse on the ControlChannel. """ if isinstance(inst[1], Play): if isinstance(inst[1].pulse, GaussianSquare) and isinstance( @@ -303,13 +310,11 @@ def _filter_control(inst: (int, Union["Schedule", Instruction])) -> bool: def _filter_drive(inst: (int, Union["Schedule", Instruction])) -> bool: """ Looks for Gaussian square pulses applied to drive channels. - Args: inst: Instructions to be filtered. - Returns: match: True if the instruction is a Play instruction with - a Gaussian square pulse on the DriveChannel. + a Gaussian square pulse on the DriveChannel. """ if isinstance(inst[1], Play): if isinstance(inst[1].pulse, GaussianSquare) and isinstance( @@ -322,21 +327,19 @@ def _filter_drive(inst: (int, Union["Schedule", Instruction])) -> bool: def get_calibration(self, params: List, qubits: List) -> Schedule: """ Builds the calibration schedule for the RZXGate(theta) without echos. - Args: params: Parameters of the RZXGate(theta). I.e. params[0] is theta. qubits: List of qubits for which to get the schedules. The first qubit is the control and the second is the target. - Returns: schedule: The calibration schedule for the RZXGate(theta). - Raises: QiskitError: If the control and target qubits cannot be identified, or the backend does not support a cx gate between the qubits, or the backend does not natively support the specified direction of the cx. """ theta = params[0] + q1, q2 = qubits[0], qubits[1] if not self._inst_map.has("cx", qubits): 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..b22bc00fa243 --- /dev/null +++ b/test/python/transpiler/test_echo_rzx_weyl_decomposition.py @@ -0,0 +1,202 @@ +# 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 and the TwoQubitWeylEchoRZX class""" +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, + TwoQubitWeylEchoRZX, +) + + +class TestEchoRZXWeylDecomposition(QiskitTestCase): + """Tests the EchoRZXWeylDecomposition pass and the TwoQubitWeylEchoRZX class.""" + + def setUp(self): + super().setUp() + self.backend = FakeParis() + self.inst_map = self.backend.defaults().instruction_schedule_map + + def test_native_weyl_decomposition(self): + """The CX is in the hardware-native direction""" + qr = QuantumRegister(2, "qr") + circuit = QuantumCircuit(qr) + circuit.cx(qr[0], qr[1]) + + unitary_circuit = qi.Operator(circuit).data + + after = EchoRZXWeylDecomposition(self.inst_map)(circuit) + + unitary_after = qi.Operator(after).data + + self.assertTrue(np.allclose(unitary_circuit, unitary_after)) + + alpha = TwoQubitWeylDecomposition(unitary_circuit).a + beta = TwoQubitWeylDecomposition(unitary_circuit).b + gamma = TwoQubitWeylDecomposition(unitary_circuit).c + + # check whether after circuit has correct number of rzx gates + 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 = after.count_ops()["rzx"] + + self.assertEqual(expected_rzx_number, circuit_rzx_number) + + def test_non_native_weyl_decomposition(self): + """The RZZ is not in the hardware-native direction""" + theta = pi / 9 + qr = QuantumRegister(2, "qr") + circuit = QuantumCircuit(qr) + circuit.rzz(theta, qr[1], qr[0]) + + unitary_circuit = qi.Operator(circuit).data + + dag = circuit_to_dag(circuit) + pass_ = EchoRZXWeylDecomposition(self.inst_map) + after = dag_to_circuit(pass_.run(dag)) + + unitary_after = qi.Operator(after).data + + self.assertTrue(np.allclose(unitary_circuit, unitary_after)) + + self.assertRZXgates(unitary_circuit, after) + + def assertRZXgates(self, unitary_circuit, after): + alpha = TwoQubitWeylDecomposition(unitary_circuit).a + beta = TwoQubitWeylDecomposition(unitary_circuit).b + gamma = TwoQubitWeylDecomposition(unitary_circuit).c + + # check whether after circuit has correct number of rzx gates + 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) + + def test_weyl_unitaries_random_circuit(self): + """Weyl decomposition for 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.inst_map) + after = dag_to_circuit(pass_.run(dag)) + + unitary_after = qi.Operator(after).data + + self.assertTrue(np.allclose(unitary_circuit, unitary_after)) + + def test_weyl_parameters(self): + """Computation of the correct RZX Weyl parameters""" + theta = pi / 3 + qr = QuantumRegister(2, "qr") + circuit = QuantumCircuit(qr) + qubit_pair = (qr[0], qr[1]) + + circuit.rzz(theta, qubit_pair[0], qubit_pair[1]) + circuit.swap(qubit_pair[0], qubit_pair[1]) + unitary_circuit = qi.Operator(circuit).data + + # Weyl parameters (alpha, beta, gamma) + alpha = TwoQubitWeylDecomposition(unitary_circuit).a + beta = TwoQubitWeylDecomposition(unitary_circuit).b + gamma = TwoQubitWeylDecomposition(unitary_circuit).c + + # RZX Weyl parameters (rzx_alpha, rzx_beta, rzx_gamma) + rzx_alpha = TwoQubitWeylEchoRZX( + unitary_circuit, inst_map=self.inst_map, qubit_pair=qubit_pair + ).a + rzx_beta = TwoQubitWeylEchoRZX( + unitary_circuit, inst_map=self.inst_map, qubit_pair=qubit_pair + ).b + rzx_gamma = TwoQubitWeylEchoRZX( + unitary_circuit, inst_map=self.inst_map, qubit_pair=qubit_pair + ).c + + self.assertEqual((alpha, beta, gamma), (rzx_alpha, rzx_beta, rzx_gamma)) + + def test_non_native_weyl_parameters(self): + """Weyl parameters for a non-hardware-native CX direction""" + qr = QuantumRegister(2, "qr") + circuit = QuantumCircuit(qr) + qubit_pair = (qr[1], qr[0]) + circuit.cx(qubit_pair[1], qubit_pair[0]) + unitary_circuit = qi.Operator(circuit).data + + # Weyl parameters (alpha, beta, gamma) + alpha = TwoQubitWeylDecomposition(unitary_circuit).a + beta = TwoQubitWeylDecomposition(unitary_circuit).b + gamma = TwoQubitWeylDecomposition(unitary_circuit).c + + # RZX Weyl parameters (rzx_alpha, rzx_beta, rzx_gamma) + rzx_alpha = TwoQubitWeylEchoRZX( + unitary_circuit, inst_map=self.inst_map, qubit_pair=qubit_pair + ).a + rzx_beta = TwoQubitWeylEchoRZX( + unitary_circuit, inst_map=self.inst_map, qubit_pair=qubit_pair + ).b + rzx_gamma = TwoQubitWeylEchoRZX( + unitary_circuit, inst_map=self.inst_map, qubit_pair=qubit_pair + ).c + + self.assertEqual((alpha, beta, gamma), (rzx_alpha, rzx_beta, rzx_gamma)) + + +if __name__ == "__main__": + unittest.main() From 73de71bc189def846d3f2bdd7aae7ab0dccbcc68 Mon Sep 17 00:00:00 2001 From: catornow Date: Wed, 21 Jul 2021 20:28:04 +0200 Subject: [PATCH 03/57] Black --- .../passes/optimization/echo_rzx_weyl_decomposition.py | 2 +- .../passes/scheduling/calibration_creators.py | 10 ++++------ .../transpiler/test_echo_rzx_weyl_decomposition.py | 2 +- 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/qiskit/transpiler/passes/optimization/echo_rzx_weyl_decomposition.py b/qiskit/transpiler/passes/optimization/echo_rzx_weyl_decomposition.py index 149f2f0f3771..8a0e46a5cf0b 100644 --- a/qiskit/transpiler/passes/optimization/echo_rzx_weyl_decomposition.py +++ b/qiskit/transpiler/passes/optimization/echo_rzx_weyl_decomposition.py @@ -35,7 +35,7 @@ class EchoRZXWeylDecomposition(TransformationPass): def __init__(self, inst_map): """EchoRZXWeylDecomposition pass.""" - self.inst_map = inst_map + self.inst_map = inst_map super().__init__() def run(self, dag): diff --git a/qiskit/transpiler/passes/scheduling/calibration_creators.py b/qiskit/transpiler/passes/scheduling/calibration_creators.py index 94278ffc9381..192c93ac29a3 100644 --- a/qiskit/transpiler/passes/scheduling/calibration_creators.py +++ b/qiskit/transpiler/passes/scheduling/calibration_creators.py @@ -83,8 +83,9 @@ class RZXCalibrationBuilder(CalibrationCreator): """ def __init__( - self, backend: Optional[BaseBackend] = None, - inst_map: Optional[Dict[str, Dict[Tuple[int], Schedule]]] = None + self, + backend: Optional[BaseBackend] = None, + inst_map: Optional[Dict[str, Dict[Tuple[int], Schedule]]] = None, ): """ Initializes a RZXGate calibration builder. @@ -109,10 +110,7 @@ def __init__( elif inst_map is not None: self._inst_map = inst_map else: - raise QiskitError( - "Either a backend or an instruction schedule map must be specified.") - - # self._inst_map = inst_map + raise QiskitError("Either a backend or an instruction schedule map must be specified.") def supported(self, node_op: DAGNode) -> bool: """ diff --git a/test/python/transpiler/test_echo_rzx_weyl_decomposition.py b/test/python/transpiler/test_echo_rzx_weyl_decomposition.py index b22bc00fa243..7ae48d0274de 100644 --- a/test/python/transpiler/test_echo_rzx_weyl_decomposition.py +++ b/test/python/transpiler/test_echo_rzx_weyl_decomposition.py @@ -105,7 +105,7 @@ def assertRZXgates(self, unitary_circuit, after): expected_rzx_number += 2 circuit_rzx_number = QuantumCircuit.count_ops(after)["rzx"] - + self.assertEqual(expected_rzx_number, circuit_rzx_number) def test_weyl_unitaries_random_circuit(self): From 8f4ebf73d5fc6d370c170c958c0160a77019d52a Mon Sep 17 00:00:00 2001 From: catornow Date: Wed, 21 Jul 2021 20:40:08 +0200 Subject: [PATCH 04/57] Small modification --- .../passes/optimization/echo_rzx_weyl_decomposition.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiskit/transpiler/passes/optimization/echo_rzx_weyl_decomposition.py b/qiskit/transpiler/passes/optimization/echo_rzx_weyl_decomposition.py index 8a0e46a5cf0b..cebc028cfe60 100644 --- a/qiskit/transpiler/passes/optimization/echo_rzx_weyl_decomposition.py +++ b/qiskit/transpiler/passes/optimization/echo_rzx_weyl_decomposition.py @@ -38,7 +38,7 @@ def __init__(self, inst_map): self.inst_map = inst_map super().__init__() - def run(self, 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 From add541320e576ebdf860c02009596bdf84c01370 Mon Sep 17 00:00:00 2001 From: catornow Date: Fri, 23 Jul 2021 10:16:11 +0200 Subject: [PATCH 05/57] Adapted two_qubit_decompose to match latest upstream/main version --- .../synthesis/two_qubit_decompose.py | 281 +++++++++++++++++- 1 file changed, 278 insertions(+), 3 deletions(-) diff --git a/qiskit/quantum_info/synthesis/two_qubit_decompose.py b/qiskit/quantum_info/synthesis/two_qubit_decompose.py index 9d22ece73947..6e37987eeb87 100644 --- a/qiskit/quantum_info/synthesis/two_qubit_decompose.py +++ b/qiskit/quantum_info/synthesis/two_qubit_decompose.py @@ -742,11 +742,17 @@ class TwoQubitBasisDecomposer: euler_basis (str): Basis string to be provided to OneQubitEulerDecomposer for 1Q synthesis. Valid options are ['ZYZ', 'ZXZ', 'XYX', 'U', 'U3', 'U1X', 'PSX', 'ZSX', 'RR']. Default 'U3'. + pulse_optimize (None or bool): If True, try to do decomposition which minimizes + local unitaries in between entangling gates. This will raise an exception if an + optimal decomposition is not implemented. Currently, only [{CX, SX, RZ}] is known. + If False, don't attempt optimization. If None, attempt optimization but don't raise + if unknown. """ - def __init__(self, gate, basis_fidelity=1.0, euler_basis=None): + def __init__(self, gate, basis_fidelity=1.0, euler_basis=None, pulse_optimize=None): self.gate = gate self.basis_fidelity = basis_fidelity + self.pulse_optimize = pulse_optimize basis = self.basis = TwoQubitWeylDecomposition(Operator(gate).data) if euler_basis is not None: @@ -870,6 +876,7 @@ def __init__(self, gate, basis_fidelity=1.0, euler_basis=None): self.decomp2_supercontrolled, self.decomp3_supercontrolled, ] + self._rqc = None def traces(self, target): """Give the expected traces :math:`|Tr(U \\cdot Utarget^dag)|` for different number of @@ -979,9 +986,22 @@ def __call__(self, target, basis_fidelity=None, *, _num_basis_uses=None) -> Quan if _num_basis_uses is not None: best_nbasis = _num_basis_uses decomposition = self.decomposition_fns[best_nbasis](target_decomposed) - decomposition_euler = [self._decomposer1q._decompose(x) for x in decomposition] + # attempt pulse optimal decomposition + try: + if self.pulse_optimize in {None, True}: + return_circuit = self._pulse_optimal_chooser( + best_nbasis, decomposition, target_decomposed + ) + if return_circuit: + return return_circuit + except QiskitError: + if self.pulse_optimize: + raise + + # do default decomposition q = QuantumRegister(2) + decomposition_euler = [self._decomposer1q._decompose(x) for x in decomposition] return_circuit = QuantumCircuit(q) return_circuit.global_phase = target_decomposed.global_phase return_circuit.global_phase -= best_nbasis * self.basis.global_phase @@ -993,9 +1013,264 @@ def __call__(self, target, basis_fidelity=None, *, _num_basis_uses=None) -> Quan return_circuit.append(self.gate, [q[0], q[1]]) return_circuit.compose(decomposition_euler[2 * best_nbasis], [q[0]], inplace=True) return_circuit.compose(decomposition_euler[2 * best_nbasis + 1], [q[1]], inplace=True) - return return_circuit + def _pulse_optimal_chooser(self, best_nbasis, decomposition, target_decomposed): + """Determine method to find pulse optimal circuit. This method may be + removed once a more general approach is used. + + Returns: + QuantumCircuit: pulse optimal quantum circuit. + None: Probably nbasis=1 and original circuit is fine. + + Raises: + QiskitError: Decomposition for selected basis not implemented. + """ + circuit = None + if self.pulse_optimize and best_nbasis in {0, 1}: + # already pulse optimal + return None + elif self.pulse_optimize and best_nbasis > 3: + raise QiskitError( + f"Unexpected number of entangling gates ({best_nbasis}) in decomposition." + ) + if self._decomposer1q.basis in {"ZSX", "ZSXX"}: + if isinstance(self.gate, CXGate): + if best_nbasis == 3: + circuit = self._get_sx_vz_3cx_efficient_euler(decomposition, target_decomposed) + elif best_nbasis == 2: + circuit = self._get_sx_vz_2cx_efficient_euler(decomposition, target_decomposed) + else: + raise QiskitError("pulse_optimizer currently only works with CNOT entangling gate") + else: + raise QiskitError( + '"pulse_optimize" currently only works with ZSX basis ' + f"({self._decomposer1q.basis} used)" + ) + return circuit + + def _get_sx_vz_2cx_efficient_euler(self, decomposition, target_decomposed): + """ + Decomposition of SU(4) gate for device with SX, virtual RZ, and CNOT gates assuming + two CNOT gates are needed. + + This first decomposes each unitary from the KAK decomposition into ZXZ on the source + qubit of the CNOTs and XZX on the targets in order to commute operators to beginning and + end of decomposition. The beginning and ending single qubit gates are then + collapsed and re-decomposed with the single qubit decomposer. This last step could be avoided + if performance is a concern. + """ + best_nbasis = 2 # by assumption + num_1q_uni = len(decomposition) + # list of euler angle decompositions on qubits 0 and 1 + euler_q0 = np.empty((num_1q_uni // 2, 3), dtype=float) + euler_q1 = np.empty((num_1q_uni // 2, 3), dtype=float) + global_phase = 0.0 + + # decompose source unitaries to zxz + zxz_decomposer = OneQubitEulerDecomposer("ZXZ") + for iqubit, decomp in enumerate(decomposition[0::2]): + euler_angles = zxz_decomposer.angles_and_phase(decomp) + euler_q0[iqubit, [1, 2, 0]] = euler_angles[:3] + global_phase += euler_angles[3] + # decompose target unitaries to xzx + xzx_decomposer = OneQubitEulerDecomposer("XZX") + for iqubit, decomp in enumerate(decomposition[1::2]): + euler_angles = xzx_decomposer.angles_and_phase(decomp) + euler_q1[iqubit, [1, 2, 0]] = euler_angles[:3] + global_phase += euler_angles[3] + qc = QuantumCircuit(2) + qc.global_phase = target_decomposed.global_phase + qc.global_phase -= best_nbasis * self.basis.global_phase + qc.global_phase += global_phase + + # TODO: make this more effecient to avoid double decomposition + # prepare beginning 0th qubit local unitary + circ = QuantumCircuit(1) + circ.rz(euler_q0[0][0], 0) + circ.rx(euler_q0[0][1], 0) + circ.rz(euler_q0[0][2] + euler_q0[1][0] + math.pi / 2, 0) + # re-decompose to basis of 1q decomposer + qceuler = self._decomposer1q(Operator(circ).data) + qc.compose(qceuler, [0], inplace=True) + + # prepare beginning 1st qubit local unitary + circ = QuantumCircuit(1) + circ.rx(euler_q1[0][0], 0) + circ.rz(euler_q1[0][1], 0) + circ.rx(euler_q1[0][2] + euler_q1[1][0], 0) + qceuler = self._decomposer1q(Operator(circ).data) + qc.compose(qceuler, [1], inplace=True) + + qc.cx(0, 1) + # the central decompositions are dependent on the specific form of the + # unitaries coming out of the two qubit decomposer which have some flexibility + # of choice. + qc.sx(0) + qc.rz(euler_q0[1][1] - math.pi, 0) + qc.sx(0) + qc.rz(euler_q1[1][1], 1) + qc.global_phase += math.pi / 2 + + qc.cx(0, 1) + + circ = QuantumCircuit(1) + circ.rz(euler_q0[1][2] + euler_q0[2][0] + math.pi / 2, 0) + circ.rx(euler_q0[2][1], 0) + circ.rz(euler_q0[2][2], 0) + qceuler = self._decomposer1q(Operator(circ).data) + qc.compose(qceuler, [0], inplace=True) + circ = QuantumCircuit(1) + circ.rx(euler_q1[1][2] + euler_q1[2][0], 0) + circ.rz(euler_q1[2][1], 0) + circ.rx(euler_q1[2][2], 0) + qceuler = self._decomposer1q(Operator(circ).data) + qc.compose(qceuler, [1], inplace=True) + + return qc + + def _get_sx_vz_3cx_efficient_euler(self, decomposition, target_decomposed): + """ + Decomposition of SU(4) gate for device with SX, virtual RZ, and CNOT gates assuming + three CNOT gates are needed. + + This first decomposes each unitary from the KAK decomposition into ZXZ on the source + qubit of the CNOTs and XZX on the targets in order commute operators to beginning and + end of decomposition. Inserting Hadamards reverses the direction of the CNOTs and transforms + a variable Rx -> variable virtual Rz. The beginning and ending single qubit gates are then + collapsed and re-decomposed with the single qubit decomposer. This last step could be avoided + if performance is a concern. + """ + best_nbasis = 3 # by assumption + num_1q_uni = len(decomposition) + # create structure to hold euler angles: 1st index represents unitary "group" wrt cx + # 2nd index represents index of euler triple. + euler_q0 = np.empty((num_1q_uni // 2, 3), dtype=float) + euler_q1 = np.empty((num_1q_uni // 2, 3), dtype=float) + global_phase = 0.0 + atol = 1e-10 # absolute tolerance for floats + + # decompose source unitaries to zxz + zxz_decomposer = OneQubitEulerDecomposer("ZXZ") + for iqubit, decomp in enumerate(decomposition[0::2]): + euler_angles = zxz_decomposer.angles_and_phase(decomp) + euler_q0[iqubit, [1, 2, 0]] = euler_angles[:3] + global_phase += euler_angles[3] + # decompose target unitaries to xzx + xzx_decomposer = OneQubitEulerDecomposer("XZX") + for iqubit, decomp in enumerate(decomposition[1::2]): + euler_angles = xzx_decomposer.angles_and_phase(decomp) + euler_q1[iqubit, [1, 2, 0]] = euler_angles[:3] + global_phase += euler_angles[3] + + qc = QuantumCircuit(2) + qc.global_phase = target_decomposed.global_phase + qc.global_phase -= best_nbasis * self.basis.global_phase + qc.global_phase += global_phase + + x12 = euler_q0[1][2] + euler_q0[2][0] + x12_isNonZero = not math.isclose(x12, 0, abs_tol=atol) + x12_isOddMult = None + x12_isPiMult = math.isclose(math.sin(x12), 0, abs_tol=atol) + if x12_isPiMult: + x12_isOddMult = math.isclose(math.cos(x12), -1, abs_tol=atol) + x12_phase = math.pi * math.cos(x12) + x02_add = x12 - euler_q0[1][0] + x12_isHalfPi = math.isclose(x12, math.pi / 2, abs_tol=atol) + + # TODO: make this more effecient to avoid double decomposition + circ = QuantumCircuit(1) + circ.rz(euler_q0[0][0], 0) + circ.rx(euler_q0[0][1], 0) + if x12_isNonZero and x12_isPiMult: + circ.rz(euler_q0[0][2] - x02_add, 0) + else: + circ.rz(euler_q0[0][2] + euler_q0[1][0], 0) + circ.h(0) + qceuler = self._decomposer1q(Operator(circ).data) + qc.compose(qceuler, [0], inplace=True) + + circ = QuantumCircuit(1) + circ.rx(euler_q1[0][0], 0) + circ.rz(euler_q1[0][1], 0) + circ.rx(euler_q1[0][2] + euler_q1[1][0], 0) + circ.h(0) + qceuler = self._decomposer1q(Operator(circ).data) + qc.compose(qceuler, [1], inplace=True) + + qc.cx(1, 0) + + if x12_isPiMult: + # even or odd multiple + if x12_isNonZero: + qc.global_phase += x12_phase + if x12_isNonZero and x12_isOddMult: + qc.rz(-euler_q0[1][1], 0) + else: + qc.rz(euler_q0[1][1], 0) + qc.global_phase += math.pi + if x12_isHalfPi: + qc.sx(0) + qc.global_phase -= math.pi / 4 + elif x12_isNonZero and not x12_isPiMult: + # this is non-optimal but doesn't seem to occur currently + if self.pulse_optimize is None: + qc.compose(self._decomposer1q(Operator(RXGate(x12)).data), [0], inplace=True) + else: + raise QiskitError("possible non-pulse-optimal decomposition encountered") + if math.isclose(euler_q1[1][1], math.pi / 2, abs_tol=atol): + qc.sx(1) + qc.global_phase -= math.pi / 4 + else: + # this is non-optimal but doesn't seem to occur currently + if self.pulse_optimize is None: + qc.compose( + self._decomposer1q(Operator(RXGate(euler_q1[1][1])).data), [1], inplace=True + ) + else: + raise QiskitError("possible non-pulse-optimal decomposition encountered") + qc.rz(euler_q1[1][2] + euler_q1[2][0], 1) + + qc.cx(1, 0) + + qc.rz(euler_q0[2][1], 0) + if math.isclose(euler_q1[2][1], math.pi / 2, abs_tol=atol): + qc.sx(1) + qc.global_phase -= math.pi / 4 + else: + # this is non-optimal but doesn't seem to occur currently + if self.pulse_optimize is None: + qc.compose( + self._decomposer1q(Operator(RXGate(euler_q1[2][1])).data), [1], inplace=True + ) + else: + raise QiskitError("possible non-pulse-optimal decomposition encountered") + + qc.cx(1, 0) + + circ = QuantumCircuit(1) + circ.h(0) + circ.rz(euler_q0[2][2] + euler_q0[3][0], 0) + circ.rx(euler_q0[3][1], 0) + circ.rz(euler_q0[3][2], 0) + qceuler = self._decomposer1q(Operator(circ).data) + qc.compose(qceuler, [0], inplace=True) + + circ = QuantumCircuit(1) + circ.h(0) + circ.rx(euler_q1[2][2] + euler_q1[3][0], 0) + circ.rz(euler_q1[3][1], 0) + circ.rx(euler_q1[3][2], 0) + qceuler = self._decomposer1q(Operator(circ).data) + qc.compose(qceuler, [1], inplace=True) + + # TODO: fix the sign problem to avoid correction here + if cmath.isclose( + target_decomposed.unitary_matrix[0, 0], -(Operator(qc).data[0, 0]), abs_tol=atol + ): + qc.global_phase += math.pi + return qc + def num_basis_gates(self, unitary): """Computes the number of basis gates needed in a decomposition of input unitary From 76936f6217ab5f909f939e2d192853e418f43085 Mon Sep 17 00:00:00 2001 From: catornow <79633854+catornow@users.noreply.github.com> Date: Sun, 25 Jul 2021 17:36:30 +0200 Subject: [PATCH 06/57] Update qiskit/quantum_info/synthesis/two_qubit_decompose.py Co-authored-by: Daniel Egger <38065505+eggerdj@users.noreply.github.com> --- qiskit/quantum_info/synthesis/two_qubit_decompose.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/qiskit/quantum_info/synthesis/two_qubit_decompose.py b/qiskit/quantum_info/synthesis/two_qubit_decompose.py index 6e37987eeb87..bd458c1ae9d0 100644 --- a/qiskit/quantum_info/synthesis/two_qubit_decompose.py +++ b/qiskit/quantum_info/synthesis/two_qubit_decompose.py @@ -575,11 +575,6 @@ def _apply_reverse_rzx(circ: QuantumCircuit, angle: float): circ.h(0) circ.h(1) - def is_native_cx(self, qubit_pair: Tuple) -> bool: - """Check that a CX for a qubit pair is native.""" - cx1 = self.inst_map.get("cx", qubit_pair) - cx2 = self.inst_map.get("cx", qubit_pair[::-1]) - return cx1.duration < cx2.duration def _weyl_gate(self, simplify, circ: QuantumCircuit, atol): """Appends Ud(a, b, c) to the circuit.""" From 294804a17b860ff84a5750371c8f7db6cc558675 Mon Sep 17 00:00:00 2001 From: catornow <79633854+catornow@users.noreply.github.com> Date: Sun, 25 Jul 2021 17:36:48 +0200 Subject: [PATCH 07/57] Update qiskit/transpiler/passes/optimization/echo_rzx_weyl_decomposition.py Co-authored-by: Daniel Egger <38065505+eggerdj@users.noreply.github.com> --- .../passes/optimization/echo_rzx_weyl_decomposition.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/qiskit/transpiler/passes/optimization/echo_rzx_weyl_decomposition.py b/qiskit/transpiler/passes/optimization/echo_rzx_weyl_decomposition.py index cebc028cfe60..bc9c5ae9adf3 100644 --- a/qiskit/transpiler/passes/optimization/echo_rzx_weyl_decomposition.py +++ b/qiskit/transpiler/passes/optimization/echo_rzx_weyl_decomposition.py @@ -38,6 +38,12 @@ def __init__(self, inst_map): self.inst_map = inst_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 + def run(self, dag: DAGCircuit): """Run the EchoRZXWeylDecomposition pass on `dag`. From c13538d08a5c92d08ce29b118835a917169b18c6 Mon Sep 17 00:00:00 2001 From: catornow <79633854+catornow@users.noreply.github.com> Date: Sun, 25 Jul 2021 17:36:56 +0200 Subject: [PATCH 08/57] Update qiskit/transpiler/passes/optimization/echo_rzx_weyl_decomposition.py Co-authored-by: Daniel Egger <38065505+eggerdj@users.noreply.github.com> --- .../passes/optimization/echo_rzx_weyl_decomposition.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiskit/transpiler/passes/optimization/echo_rzx_weyl_decomposition.py b/qiskit/transpiler/passes/optimization/echo_rzx_weyl_decomposition.py index bc9c5ae9adf3..80c8a95ad9d8 100644 --- a/qiskit/transpiler/passes/optimization/echo_rzx_weyl_decomposition.py +++ b/qiskit/transpiler/passes/optimization/echo_rzx_weyl_decomposition.py @@ -77,7 +77,7 @@ def run(self, dag: DAGCircuit): physical_q0 = trivial_layout[control] physical_q1 = trivial_layout[target] - qubit_pair = (physical_q0, physical_q1) + is_native= self._is_native((physical_q0, physical_q1)) unitary = qi.Operator(node.op).data dag_weyl = circuit_to_dag( From 8bd3c225d4d032ef8ccd2cd3e12b021c14ae18d1 Mon Sep 17 00:00:00 2001 From: catornow <79633854+catornow@users.noreply.github.com> Date: Sun, 25 Jul 2021 17:37:01 +0200 Subject: [PATCH 09/57] Update qiskit/transpiler/passes/optimization/echo_rzx_weyl_decomposition.py Co-authored-by: Daniel Egger <38065505+eggerdj@users.noreply.github.com> --- .../passes/optimization/echo_rzx_weyl_decomposition.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiskit/transpiler/passes/optimization/echo_rzx_weyl_decomposition.py b/qiskit/transpiler/passes/optimization/echo_rzx_weyl_decomposition.py index 80c8a95ad9d8..b4c6f23db587 100644 --- a/qiskit/transpiler/passes/optimization/echo_rzx_weyl_decomposition.py +++ b/qiskit/transpiler/passes/optimization/echo_rzx_weyl_decomposition.py @@ -82,7 +82,7 @@ def run(self, dag: DAGCircuit): unitary = qi.Operator(node.op).data dag_weyl = circuit_to_dag( TwoQubitWeylEchoRZX( - unitary, inst_map=self.inst_map, qubit_pair=qubit_pair + unitary, is_native=is_native ).circuit() ) dag.substitute_node_with_dag(node, dag_weyl) From a8fc088f2707a85da8a8679841363e112f0dc545 Mon Sep 17 00:00:00 2001 From: catornow <79633854+catornow@users.noreply.github.com> Date: Sun, 25 Jul 2021 17:37:10 +0200 Subject: [PATCH 10/57] Update qiskit/quantum_info/synthesis/two_qubit_decompose.py Co-authored-by: Daniel Egger <38065505+eggerdj@users.noreply.github.com> --- qiskit/quantum_info/synthesis/two_qubit_decompose.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/qiskit/quantum_info/synthesis/two_qubit_decompose.py b/qiskit/quantum_info/synthesis/two_qubit_decompose.py index bd458c1ae9d0..7583f447e873 100644 --- a/qiskit/quantum_info/synthesis/two_qubit_decompose.py +++ b/qiskit/quantum_info/synthesis/two_qubit_decompose.py @@ -547,9 +547,13 @@ def specialize(self): class TwoQubitWeylEchoRZX(TwoQubitWeylDecomposition): """Decompose two-qubit unitary in terms of echoed cross-resonance gates.""" - def __init__(self, unitary, inst_map, qubit_pair: Tuple): - self.inst_map = inst_map - self.qubit_pair = qubit_pair + def __init__(self, unitary, is_native: bool): + """Initialize the KAK decomposition. + + Args: + is_native: If True then the CX schedule on qubits (q0, q1) is shorter than the schedule on (q1, q0). + """ + self.is_native = is_native super().__init__(unitary) def specialize(self): From e71d0980f42c18b63419ef138e7c759aea19ed2c Mon Sep 17 00:00:00 2001 From: catornow <79633854+catornow@users.noreply.github.com> Date: Sun, 25 Jul 2021 17:37:21 +0200 Subject: [PATCH 11/57] Update qiskit/quantum_info/synthesis/two_qubit_decompose.py Co-authored-by: Daniel Egger <38065505+eggerdj@users.noreply.github.com> --- qiskit/quantum_info/synthesis/two_qubit_decompose.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiskit/quantum_info/synthesis/two_qubit_decompose.py b/qiskit/quantum_info/synthesis/two_qubit_decompose.py index 7583f447e873..8f7c187c7c58 100644 --- a/qiskit/quantum_info/synthesis/two_qubit_decompose.py +++ b/qiskit/quantum_info/synthesis/two_qubit_decompose.py @@ -585,7 +585,7 @@ def _weyl_gate(self, simplify, circ: QuantumCircuit, atol): del simplify circ.h(0) if abs(self.a) > atol: - if self.is_native_cx(self.qubit_pair): + if self.is_native: self._apply_rzx(circ, self.a) else: self._apply_reverse_rzx(circ, self.a) From f93d5e5d2e61c6d52aeabcc8f8865d3d843bb101 Mon Sep 17 00:00:00 2001 From: catornow <79633854+catornow@users.noreply.github.com> Date: Sun, 25 Jul 2021 17:37:30 +0200 Subject: [PATCH 12/57] Update qiskit/quantum_info/synthesis/two_qubit_decompose.py Co-authored-by: Daniel Egger <38065505+eggerdj@users.noreply.github.com> --- qiskit/quantum_info/synthesis/two_qubit_decompose.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiskit/quantum_info/synthesis/two_qubit_decompose.py b/qiskit/quantum_info/synthesis/two_qubit_decompose.py index 8f7c187c7c58..44ccc3774325 100644 --- a/qiskit/quantum_info/synthesis/two_qubit_decompose.py +++ b/qiskit/quantum_info/synthesis/two_qubit_decompose.py @@ -594,7 +594,7 @@ def _weyl_gate(self, simplify, circ: QuantumCircuit, atol): circ.h(0) circ.sdg(1) if abs(self.b) > atol: - if self.is_native_cx(self.qubit_pair): + if self.is_native: self._apply_rzx(circ, self.b) else: self._apply_reverse_rzx(circ, self.b) From 43de98b70055871dfa54ad1532dd3bd11696d56f Mon Sep 17 00:00:00 2001 From: catornow <79633854+catornow@users.noreply.github.com> Date: Sun, 25 Jul 2021 17:37:54 +0200 Subject: [PATCH 13/57] Update qiskit/quantum_info/synthesis/two_qubit_decompose.py Co-authored-by: Daniel Egger <38065505+eggerdj@users.noreply.github.com> --- qiskit/quantum_info/synthesis/two_qubit_decompose.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiskit/quantum_info/synthesis/two_qubit_decompose.py b/qiskit/quantum_info/synthesis/two_qubit_decompose.py index 44ccc3774325..6610a4ae2d0b 100644 --- a/qiskit/quantum_info/synthesis/two_qubit_decompose.py +++ b/qiskit/quantum_info/synthesis/two_qubit_decompose.py @@ -603,7 +603,7 @@ def _weyl_gate(self, simplify, circ: QuantumCircuit, atol): circ.s(1) circ.h(1) if abs(self.c) > atol: - if self.is_native_cx(self.qubit_pair): + if self.is_native: self._apply_rzx(circ, self.c) else: self._apply_reverse_rzx(circ, self.c) From 5b7523eb59039d18c401c301910551a59d4f9324 Mon Sep 17 00:00:00 2001 From: catornow <79633854+catornow@users.noreply.github.com> Date: Sun, 25 Jul 2021 17:38:23 +0200 Subject: [PATCH 14/57] Update qiskit/quantum_info/synthesis/two_qubit_decompose.py Co-authored-by: Daniel Egger <38065505+eggerdj@users.noreply.github.com> --- qiskit/quantum_info/synthesis/two_qubit_decompose.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiskit/quantum_info/synthesis/two_qubit_decompose.py b/qiskit/quantum_info/synthesis/two_qubit_decompose.py index 6610a4ae2d0b..5e293cebacd0 100644 --- a/qiskit/quantum_info/synthesis/two_qubit_decompose.py +++ b/qiskit/quantum_info/synthesis/two_qubit_decompose.py @@ -134,7 +134,7 @@ def __init_subclass__(cls, **kwargs): ) @staticmethod - def __new__(cls, unitary_matrix, *, fidelity=(1.0 - 1.0e-9), inst_map=None, qubit_pair=None): + def __new__(cls, unitary_matrix, *, fidelity=(1.0 - 1.0e-9), is_native=None): """Perform the Weyl chamber decomposition, and optionally choose a specialized subclass. The flip into the Weyl Chamber is described in B. Kraus and J. I. Cirac, Phys. Rev. A 63, From 713b5364f98129cde658b3335669a097ebab4af3 Mon Sep 17 00:00:00 2001 From: catornow <79633854+catornow@users.noreply.github.com> Date: Sun, 25 Jul 2021 17:38:38 +0200 Subject: [PATCH 15/57] Update qiskit/quantum_info/synthesis/two_qubit_decompose.py Co-authored-by: Daniel Egger <38065505+eggerdj@users.noreply.github.com> --- qiskit/quantum_info/synthesis/two_qubit_decompose.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/qiskit/quantum_info/synthesis/two_qubit_decompose.py b/qiskit/quantum_info/synthesis/two_qubit_decompose.py index 5e293cebacd0..120b3c9b2fdf 100644 --- a/qiskit/quantum_info/synthesis/two_qubit_decompose.py +++ b/qiskit/quantum_info/synthesis/two_qubit_decompose.py @@ -129,8 +129,8 @@ def __init_subclass__(cls, **kwargs): Make explicitly-instantiated subclass __new__ call base __new__ with fidelity=None""" super().__init_subclass__(**kwargs) - cls.__new__ = lambda cls, *a, fidelity=None, inst_map=None, qubit_pair=None, **k: TwoQubitWeylDecomposition.__new__( - cls, *a, fidelity=None, inst_map=None, qubit_pair=None, **k + cls.__new__ = lambda cls, *a, fidelity=None, is_native=None, **k: TwoQubitWeylDecomposition.__new__( + cls, *a, fidelity=None, is_native=None, **k ) @staticmethod From a484c9c07b9472be7a32a633b5bcb3342de254c7 Mon Sep 17 00:00:00 2001 From: catornow Date: Sun, 25 Jul 2021 18:03:35 +0200 Subject: [PATCH 16/57] Implemented Daniel's suggestions --- .../synthesis/two_qubit_decompose.py | 11 +++++---- .../echo_rzx_weyl_decomposition.py | 8 +++---- .../passes/scheduling/calibration_creators.py | 3 +++ .../test_echo_rzx_weyl_decomposition.py | 24 +++++-------------- 4 files changed, 19 insertions(+), 27 deletions(-) diff --git a/qiskit/quantum_info/synthesis/two_qubit_decompose.py b/qiskit/quantum_info/synthesis/two_qubit_decompose.py index 120b3c9b2fdf..31a016c2f6e0 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, Tuple +from typing import ClassVar, Optional import logging @@ -129,8 +129,10 @@ def __init_subclass__(cls, **kwargs): Make explicitly-instantiated subclass __new__ call base __new__ with fidelity=None""" super().__init_subclass__(**kwargs) - cls.__new__ = lambda cls, *a, fidelity=None, is_native=None, **k: TwoQubitWeylDecomposition.__new__( - cls, *a, fidelity=None, is_native=None, **k + cls.__new__ = ( + lambda cls, *a, fidelity=None, is_native=None, **k: TwoQubitWeylDecomposition.__new__( + cls, *a, fidelity=None, is_native=None, **k + ) ) @staticmethod @@ -549,7 +551,7 @@ class TwoQubitWeylEchoRZX(TwoQubitWeylDecomposition): def __init__(self, unitary, is_native: bool): """Initialize the KAK decomposition. - + Args: is_native: If True then the CX schedule on qubits (q0, q1) is shorter than the schedule on (q1, q0). """ @@ -579,7 +581,6 @@ def _apply_reverse_rzx(circ: QuantumCircuit, angle: float): circ.h(0) circ.h(1) - def _weyl_gate(self, simplify, circ: QuantumCircuit, atol): """Appends Ud(a, b, c) to the circuit.""" del simplify diff --git a/qiskit/transpiler/passes/optimization/echo_rzx_weyl_decomposition.py b/qiskit/transpiler/passes/optimization/echo_rzx_weyl_decomposition.py index b4c6f23db587..84ddd0b2a5f2 100644 --- a/qiskit/transpiler/passes/optimization/echo_rzx_weyl_decomposition.py +++ b/qiskit/transpiler/passes/optimization/echo_rzx_weyl_decomposition.py @@ -12,6 +12,8 @@ """Weyl decomposition of two-qubit gates in terms of echoed cross-resonance gates.""" +from typing import Tuple + from qiskit.transpiler.basepasses import TransformationPass from qiskit.transpiler.exceptions import TranspilerError @@ -77,13 +79,11 @@ def run(self, dag: DAGCircuit): physical_q0 = trivial_layout[control] physical_q1 = trivial_layout[target] - is_native= self._is_native((physical_q0, physical_q1)) + is_native = self._is_native((physical_q0, physical_q1)) unitary = qi.Operator(node.op).data dag_weyl = circuit_to_dag( - TwoQubitWeylEchoRZX( - unitary, is_native=is_native - ).circuit() + TwoQubitWeylEchoRZX(unitary, is_native=is_native).circuit() ) dag.substitute_node_with_dag(node, dag_weyl) diff --git a/qiskit/transpiler/passes/scheduling/calibration_creators.py b/qiskit/transpiler/passes/scheduling/calibration_creators.py index 192c93ac29a3..cab1079fff59 100644 --- a/qiskit/transpiler/passes/scheduling/calibration_creators.py +++ b/qiskit/transpiler/passes/scheduling/calibration_creators.py @@ -290,6 +290,7 @@ class RZXCalibrationBuilderNoEcho(RZXCalibrationBuilder): def _filter_control(inst: (int, Union["Schedule", Instruction])) -> bool: """ Looks for Gaussian square pulses applied to control channels. + Args: inst: Instructions to be filtered. Returns: @@ -308,6 +309,7 @@ def _filter_control(inst: (int, Union["Schedule", Instruction])) -> bool: def _filter_drive(inst: (int, Union["Schedule", Instruction])) -> bool: """ Looks for Gaussian square pulses applied to drive channels. + Args: inst: Instructions to be filtered. Returns: @@ -325,6 +327,7 @@ def _filter_drive(inst: (int, Union["Schedule", Instruction])) -> bool: def get_calibration(self, params: List, qubits: List) -> Schedule: """ Builds the calibration schedule for the RZXGate(theta) without echos. + Args: params: Parameters of the RZXGate(theta). I.e. params[0] is theta. qubits: List of qubits for which to get the schedules. The first qubit is diff --git a/test/python/transpiler/test_echo_rzx_weyl_decomposition.py b/test/python/transpiler/test_echo_rzx_weyl_decomposition.py index 7ae48d0274de..54f692e118b0 100644 --- a/test/python/transpiler/test_echo_rzx_weyl_decomposition.py +++ b/test/python/transpiler/test_echo_rzx_weyl_decomposition.py @@ -159,15 +159,9 @@ def test_weyl_parameters(self): gamma = TwoQubitWeylDecomposition(unitary_circuit).c # RZX Weyl parameters (rzx_alpha, rzx_beta, rzx_gamma) - rzx_alpha = TwoQubitWeylEchoRZX( - unitary_circuit, inst_map=self.inst_map, qubit_pair=qubit_pair - ).a - rzx_beta = TwoQubitWeylEchoRZX( - unitary_circuit, inst_map=self.inst_map, qubit_pair=qubit_pair - ).b - rzx_gamma = TwoQubitWeylEchoRZX( - unitary_circuit, inst_map=self.inst_map, qubit_pair=qubit_pair - ).c + rzx_alpha = TwoQubitWeylEchoRZX(unitary_circuit, is_native=True).a + rzx_beta = TwoQubitWeylEchoRZX(unitary_circuit, is_native=True).b + rzx_gamma = TwoQubitWeylEchoRZX(unitary_circuit, is_native=True).c self.assertEqual((alpha, beta, gamma), (rzx_alpha, rzx_beta, rzx_gamma)) @@ -185,15 +179,9 @@ def test_non_native_weyl_parameters(self): gamma = TwoQubitWeylDecomposition(unitary_circuit).c # RZX Weyl parameters (rzx_alpha, rzx_beta, rzx_gamma) - rzx_alpha = TwoQubitWeylEchoRZX( - unitary_circuit, inst_map=self.inst_map, qubit_pair=qubit_pair - ).a - rzx_beta = TwoQubitWeylEchoRZX( - unitary_circuit, inst_map=self.inst_map, qubit_pair=qubit_pair - ).b - rzx_gamma = TwoQubitWeylEchoRZX( - unitary_circuit, inst_map=self.inst_map, qubit_pair=qubit_pair - ).c + rzx_alpha = TwoQubitWeylEchoRZX(unitary_circuit, is_native=False).a + rzx_beta = TwoQubitWeylEchoRZX(unitary_circuit, is_native=False).b + rzx_gamma = TwoQubitWeylEchoRZX(unitary_circuit, is_native=False).c self.assertEqual((alpha, beta, gamma), (rzx_alpha, rzx_beta, rzx_gamma)) From d600452a6ad6da9ce6ab5e5650a8fa167163de23 Mon Sep 17 00:00:00 2001 From: catornow Date: Sun, 25 Jul 2021 18:14:04 +0200 Subject: [PATCH 17/57] Small modifications in calibration_creators --- .../transpiler/passes/scheduling/calibration_creators.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/qiskit/transpiler/passes/scheduling/calibration_creators.py b/qiskit/transpiler/passes/scheduling/calibration_creators.py index cab1079fff59..0bc0f163b3d4 100644 --- a/qiskit/transpiler/passes/scheduling/calibration_creators.py +++ b/qiskit/transpiler/passes/scheduling/calibration_creators.py @@ -278,7 +278,9 @@ class RZXCalibrationBuilderNoEcho(RZXCalibrationBuilder): """ Creates calibrations for RZXGate(theta) by stretching and compressing Gaussian square pulses in the CX gate. - The RZXCalibrationBuilderNoEcho is a variation of the RZXCalibrationBuilder + + The ``RZXCalibrationBuilderNoEcho`` is a variation of the + :class:`~qiskit.transpiler.passes.RZXCalibrationBuilder` pass as it creates calibrations for the cross-resonance pulses without inserting the echo pulses in the pulse schedule. This enables exposing the echo in the cross-resonance sequence as gates so that the transpiler can simplify them. @@ -293,6 +295,7 @@ def _filter_control(inst: (int, Union["Schedule", Instruction])) -> bool: Args: inst: Instructions to be filtered. + Returns: match: True if the instruction is a Play instruction with a Gaussian square pulse on the ControlChannel. @@ -312,6 +315,7 @@ def _filter_drive(inst: (int, Union["Schedule", Instruction])) -> bool: Args: inst: Instructions to be filtered. + Returns: match: True if the instruction is a Play instruction with a Gaussian square pulse on the DriveChannel. @@ -332,15 +336,16 @@ def get_calibration(self, params: List, qubits: List) -> Schedule: params: Parameters of the RZXGate(theta). I.e. params[0] is theta. qubits: List of qubits for which to get the schedules. The first qubit is the control and the second is the target. + Returns: schedule: The calibration schedule for the RZXGate(theta). + Raises: QiskitError: If the control and target qubits cannot be identified, or the backend does not support a cx gate between the qubits, or the backend does not natively support the specified direction of the cx. """ theta = params[0] - q1, q2 = qubits[0], qubits[1] if not self._inst_map.has("cx", qubits): From 2bcb15e5394ffcb9064316e6d6d48574cda3a7e3 Mon Sep 17 00:00:00 2001 From: catornow Date: Mon, 26 Jul 2021 10:21:18 +0200 Subject: [PATCH 18/57] Style and lint errors --- qiskit/quantum_info/synthesis/two_qubit_decompose.py | 3 ++- test/python/transpiler/test_echo_rzx_weyl_decomposition.py | 5 ++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/qiskit/quantum_info/synthesis/two_qubit_decompose.py b/qiskit/quantum_info/synthesis/two_qubit_decompose.py index 31a016c2f6e0..e93d8ae3f076 100644 --- a/qiskit/quantum_info/synthesis/two_qubit_decompose.py +++ b/qiskit/quantum_info/synthesis/two_qubit_decompose.py @@ -553,7 +553,8 @@ def __init__(self, unitary, is_native: bool): """Initialize the KAK decomposition. Args: - is_native: If True then the CX schedule on qubits (q0, q1) is shorter than the schedule on (q1, q0). + is_native: If True then the CX schedule on qubits (q0, q1) + is shorter than the schedule on (q1, q0). """ self.is_native = is_native super().__init__(unitary) diff --git a/test/python/transpiler/test_echo_rzx_weyl_decomposition.py b/test/python/transpiler/test_echo_rzx_weyl_decomposition.py index 54f692e118b0..4f85aaf6f00d 100644 --- a/test/python/transpiler/test_echo_rzx_weyl_decomposition.py +++ b/test/python/transpiler/test_echo_rzx_weyl_decomposition.py @@ -91,6 +91,7 @@ def test_non_native_weyl_decomposition(self): self.assertRZXgates(unitary_circuit, after) 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 @@ -183,7 +184,9 @@ def test_non_native_weyl_parameters(self): rzx_beta = TwoQubitWeylEchoRZX(unitary_circuit, is_native=False).b rzx_gamma = TwoQubitWeylEchoRZX(unitary_circuit, is_native=False).c - self.assertEqual((alpha, beta, gamma), (rzx_alpha, rzx_beta, rzx_gamma)) + self.assertAlmostEqual(alpha, rzx_alpha) + self.assertAlmostEqual(beta, rzx_beta) + self.assertAlmostEqual(gamma, rzx_gamma) if __name__ == "__main__": From 1aa8f803e91691d4942b3b8812cd3295883c5ede Mon Sep 17 00:00:00 2001 From: catornow Date: Mon, 26 Jul 2021 12:59:58 +0200 Subject: [PATCH 19/57] Rewrote TwoQubitWeylEchoRZX to remove lint error --- .../synthesis/two_qubit_decompose.py | 56 +++++++++++++------ .../test_echo_rzx_weyl_decomposition.py | 12 ++-- 2 files changed, 44 insertions(+), 24 deletions(-) diff --git a/qiskit/quantum_info/synthesis/two_qubit_decompose.py b/qiskit/quantum_info/synthesis/two_qubit_decompose.py index e93d8ae3f076..62b8e0dd45f9 100644 --- a/qiskit/quantum_info/synthesis/two_qubit_decompose.py +++ b/qiskit/quantum_info/synthesis/two_qubit_decompose.py @@ -130,13 +130,13 @@ def __init_subclass__(cls, **kwargs): Make explicitly-instantiated subclass __new__ call base __new__ with fidelity=None""" super().__init_subclass__(**kwargs) cls.__new__ = ( - lambda cls, *a, fidelity=None, is_native=None, **k: TwoQubitWeylDecomposition.__new__( - cls, *a, fidelity=None, is_native=None, **k + lambda cls, *a, fidelity=None, **k: TwoQubitWeylDecomposition.__new__( + cls, *a, fidelity=None, **k ) ) @staticmethod - def __new__(cls, unitary_matrix, *, fidelity=(1.0 - 1.0e-9), is_native=None): + def __new__(cls, unitary_matrix, *, fidelity=(1.0 - 1.0e-9)): """Perform the Weyl chamber decomposition, and optionally choose a specialized subclass. The flip into the Weyl Chamber is described in B. Kraus and J. I. Cirac, Phys. Rev. A 63, @@ -546,9 +546,11 @@ def specialize(self): self.K2r = np.asarray(RYGate(k2rtheta)) @ np.asarray(RXGate(k2rlambda)) -class TwoQubitWeylEchoRZX(TwoQubitWeylDecomposition): +class TwoQubitWeylEchoRZX: """Decompose two-qubit unitary in terms of echoed cross-resonance gates.""" + _default_1q_basis = "XYX" + def __init__(self, unitary, is_native: bool): """Initialize the KAK decomposition. @@ -557,10 +559,29 @@ def __init__(self, unitary, is_native: bool): is shorter than the schedule on (q1, q0). """ self.is_native = is_native - super().__init__(unitary) - def specialize(self): - pass # Nothing to do + self.decomposer = TwoQubitWeylDecomposition(unitary) + + def circuit( + self, *, euler_basis: Optional[str] = None, atol=DEFAULT_ATOL + ) -> QuantumCircuit: + """Returns Weyl decomposition in circuit form. + + simplify, atol arguments are passed to OneQubitEulerDecomposer""" + if euler_basis is None: + euler_basis = self._default_1q_basis + oneq_decompose = OneQubitEulerDecomposer(euler_basis) + 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, atol) + circ.compose(c1r, [0], inplace=True) + circ.compose(c1l, [1], inplace=True) + return circ @staticmethod def _apply_rzx(circ: QuantumCircuit, angle: float): @@ -582,33 +603,32 @@ def _apply_reverse_rzx(circ: QuantumCircuit, angle: float): circ.h(0) circ.h(1) - def _weyl_gate(self, simplify, circ: QuantumCircuit, atol): + def _weyl_gate(self, circ: QuantumCircuit, atol=1.0e-13): """Appends Ud(a, b, c) to the circuit.""" - del simplify circ.h(0) - if abs(self.a) > atol: + if abs(self.decomposer.a) > atol: if self.is_native: - self._apply_rzx(circ, self.a) + self._apply_rzx(circ, self.decomposer.a) else: - self._apply_reverse_rzx(circ, self.a) + self._apply_reverse_rzx(circ, self.decomposer.a) circ.h(0) circ.sdg(0) circ.h(0) circ.sdg(1) - if abs(self.b) > atol: + if abs(self.decomposer.b) > atol: if self.is_native: - self._apply_rzx(circ, self.b) + self._apply_rzx(circ, self.decomposer.b) else: - self._apply_reverse_rzx(circ, self.b) + self._apply_reverse_rzx(circ, self.decomposer.b) circ.h(0) circ.s(0) circ.s(1) circ.h(1) - if abs(self.c) > atol: + if abs(self.decomposer.c) > atol: if self.is_native: - self._apply_rzx(circ, self.c) + self._apply_rzx(circ, self.decomposer.c) else: - self._apply_reverse_rzx(circ, self.c) + self._apply_reverse_rzx(circ, self.decomposer.c) circ.h(1) diff --git a/test/python/transpiler/test_echo_rzx_weyl_decomposition.py b/test/python/transpiler/test_echo_rzx_weyl_decomposition.py index 4f85aaf6f00d..e38198992b6b 100644 --- a/test/python/transpiler/test_echo_rzx_weyl_decomposition.py +++ b/test/python/transpiler/test_echo_rzx_weyl_decomposition.py @@ -160,9 +160,9 @@ def test_weyl_parameters(self): gamma = TwoQubitWeylDecomposition(unitary_circuit).c # RZX Weyl parameters (rzx_alpha, rzx_beta, rzx_gamma) - rzx_alpha = TwoQubitWeylEchoRZX(unitary_circuit, is_native=True).a - rzx_beta = TwoQubitWeylEchoRZX(unitary_circuit, is_native=True).b - rzx_gamma = TwoQubitWeylEchoRZX(unitary_circuit, is_native=True).c + rzx_alpha = TwoQubitWeylEchoRZX(unitary_circuit, is_native=True).decomposer.a + rzx_beta = TwoQubitWeylEchoRZX(unitary_circuit, is_native=True).decomposer.b + rzx_gamma = TwoQubitWeylEchoRZX(unitary_circuit, is_native=True).decomposer.c self.assertEqual((alpha, beta, gamma), (rzx_alpha, rzx_beta, rzx_gamma)) @@ -180,9 +180,9 @@ def test_non_native_weyl_parameters(self): gamma = TwoQubitWeylDecomposition(unitary_circuit).c # RZX Weyl parameters (rzx_alpha, rzx_beta, rzx_gamma) - rzx_alpha = TwoQubitWeylEchoRZX(unitary_circuit, is_native=False).a - rzx_beta = TwoQubitWeylEchoRZX(unitary_circuit, is_native=False).b - rzx_gamma = TwoQubitWeylEchoRZX(unitary_circuit, is_native=False).c + rzx_alpha = TwoQubitWeylEchoRZX(unitary_circuit, is_native=False).decomposer.a + rzx_beta = TwoQubitWeylEchoRZX(unitary_circuit, is_native=False).decomposer.b + rzx_gamma = TwoQubitWeylEchoRZX(unitary_circuit, is_native=False).decomposer.c self.assertAlmostEqual(alpha, rzx_alpha) self.assertAlmostEqual(beta, rzx_beta) From e1cefd01edc48a4bb37f73e71c13c5ec1e9b449f Mon Sep 17 00:00:00 2001 From: catornow Date: Mon, 26 Jul 2021 13:03:30 +0200 Subject: [PATCH 20/57] Black --- .../synthesis/two_qubit_decompose.py | 22 +++++++++++-------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/qiskit/quantum_info/synthesis/two_qubit_decompose.py b/qiskit/quantum_info/synthesis/two_qubit_decompose.py index 62b8e0dd45f9..b32f45c56433 100644 --- a/qiskit/quantum_info/synthesis/two_qubit_decompose.py +++ b/qiskit/quantum_info/synthesis/two_qubit_decompose.py @@ -41,7 +41,10 @@ from qiskit.exceptions import QiskitError from qiskit.quantum_info.operators import Operator from qiskit.quantum_info.synthesis.weyl import weyl_coordinates -from qiskit.quantum_info.synthesis.one_qubit_decompose import OneQubitEulerDecomposer, DEFAULT_ATOL +from qiskit.quantum_info.synthesis.one_qubit_decompose import ( + OneQubitEulerDecomposer, + DEFAULT_ATOL, +) logger = logging.getLogger(__name__) @@ -129,10 +132,8 @@ def __init_subclass__(cls, **kwargs): Make explicitly-instantiated subclass __new__ call base __new__ with fidelity=None""" super().__init_subclass__(**kwargs) - cls.__new__ = ( - lambda cls, *a, fidelity=None, **k: TwoQubitWeylDecomposition.__new__( - cls, *a, fidelity=None, **k - ) + cls.__new__ = lambda cls, *a, fidelity=None, **k: TwoQubitWeylDecomposition.__new__( + cls, *a, fidelity=None, **k ) @staticmethod @@ -562,9 +563,7 @@ def __init__(self, unitary, is_native: bool): self.decomposer = TwoQubitWeylDecomposition(unitary) - def circuit( - self, *, euler_basis: Optional[str] = None, atol=DEFAULT_ATOL - ) -> QuantumCircuit: + def circuit(self, *, euler_basis: Optional[str] = None, atol=DEFAULT_ATOL) -> QuantumCircuit: """Returns Weyl decomposition in circuit form. simplify, atol arguments are passed to OneQubitEulerDecomposer""" @@ -573,7 +572,12 @@ def circuit( oneq_decompose = OneQubitEulerDecomposer(euler_basis) c1l, c1r, c2l, c2r = ( oneq_decompose(k, atol=atol) - for k in (self.decomposer.K1l, self.decomposer.K1r, self.decomposer.K2l, self.decomposer.K2r) + 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) From 9bcb060e6422c57bbb7dd2485b3c51046aace46f Mon Sep 17 00:00:00 2001 From: Daniel Egger <38065505+eggerdj@users.noreply.github.com> Date: Wed, 28 Jul 2021 09:59:57 +0200 Subject: [PATCH 21/57] Update qiskit/quantum_info/synthesis/two_qubit_decompose.py --- qiskit/quantum_info/synthesis/two_qubit_decompose.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiskit/quantum_info/synthesis/two_qubit_decompose.py b/qiskit/quantum_info/synthesis/two_qubit_decompose.py index b32f45c56433..52d9a9b13423 100644 --- a/qiskit/quantum_info/synthesis/two_qubit_decompose.py +++ b/qiskit/quantum_info/synthesis/two_qubit_decompose.py @@ -557,7 +557,7 @@ def __init__(self, unitary, is_native: bool): Args: is_native: If True then the CX schedule on qubits (q0, q1) - is shorter than the schedule on (q1, q0). + is shorter than the schedule on (q1, q0). """ self.is_native = is_native From 0e9f6376a945cc3653e13ce96c0ac51bf3d04a2b Mon Sep 17 00:00:00 2001 From: Daniel Egger <38065505+eggerdj@users.noreply.github.com> Date: Wed, 28 Jul 2021 10:02:38 +0200 Subject: [PATCH 22/57] Update qiskit/quantum_info/synthesis/two_qubit_decompose.py --- qiskit/quantum_info/synthesis/two_qubit_decompose.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/qiskit/quantum_info/synthesis/two_qubit_decompose.py b/qiskit/quantum_info/synthesis/two_qubit_decompose.py index 52d9a9b13423..44528c1a24f2 100644 --- a/qiskit/quantum_info/synthesis/two_qubit_decompose.py +++ b/qiskit/quantum_info/synthesis/two_qubit_decompose.py @@ -564,9 +564,10 @@ def __init__(self, unitary, is_native: bool): self.decomposer = TwoQubitWeylDecomposition(unitary) def circuit(self, *, euler_basis: Optional[str] = None, atol=DEFAULT_ATOL) -> QuantumCircuit: - """Returns Weyl decomposition in circuit form. + """Returns the Weyl decomposition in circuit form. - simplify, atol arguments are passed to OneQubitEulerDecomposer""" + Note: atol ist passed to OneQubitEulerDecomposer. + """ if euler_basis is None: euler_basis = self._default_1q_basis oneq_decompose = OneQubitEulerDecomposer(euler_basis) From 4139093acd3c3db837ba96aa732ed470cc3dc70c Mon Sep 17 00:00:00 2001 From: catornow Date: Wed, 28 Jul 2021 10:40:22 +0200 Subject: [PATCH 23/57] Implemented additional tests --- .../test_echo_rzx_weyl_decomposition.py | 65 +++++++++++++++++-- 1 file changed, 61 insertions(+), 4 deletions(-) diff --git a/test/python/transpiler/test_echo_rzx_weyl_decomposition.py b/test/python/transpiler/test_echo_rzx_weyl_decomposition.py index e38198992b6b..630dfe522ab1 100644 --- a/test/python/transpiler/test_echo_rzx_weyl_decomposition.py +++ b/test/python/transpiler/test_echo_rzx_weyl_decomposition.py @@ -40,8 +40,8 @@ def setUp(self): self.backend = FakeParis() self.inst_map = self.backend.defaults().instruction_schedule_map - def test_native_weyl_decomposition(self): - """The CX is in the hardware-native direction""" + 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]) @@ -91,7 +91,7 @@ def test_non_native_weyl_decomposition(self): self.assertRZXgates(unitary_circuit, after) def assertRZXgates(self, unitary_circuit, after): - """Check the number of rzx gates""" + """"Check the number of rzx gates""" alpha = TwoQubitWeylDecomposition(unitary_circuit).a beta = TwoQubitWeylDecomposition(unitary_circuit).b gamma = TwoQubitWeylDecomposition(unitary_circuit).c @@ -109,6 +109,63 @@ def assertRZXgates(self, unitary_circuit, after): self.assertEqual(expected_rzx_number, circuit_rzx_number) + def test_h_number_non_native_weyl_decomposition_1(self): + """Check the number of added Hadamard gates for an 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.inst_map) + after = dag_to_circuit(pass_.run(dag)) + + dag_non_native = circuit_to_dag(circuit_non_native) + pass_ = EchoRZXWeylDecomposition(self.inst_map) + after_non_native = dag_to_circuit(pass_.run(dag_non_native)) + + circuit_rzx_number = QuantumCircuit.count_ops(after)["rzx"] + + circuit_h_number = QuantumCircuit.count_ops(after)["h"] + circuit_non_native_h_number = QuantumCircuit.count_ops(after_non_native)["h"] + + # 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.inst_map) + after = dag_to_circuit(pass_.run(dag)) + + dag_non_native = circuit_to_dag(circuit_non_native) + pass_ = EchoRZXWeylDecomposition(self.inst_map) + after_non_native = dag_to_circuit(pass_.run(dag_non_native)) + + circuit_rzx_number = QuantumCircuit.count_ops(after)["rzx"] + + circuit_h_number = QuantumCircuit.count_ops(after)["h"] + circuit_non_native_h_number = QuantumCircuit.count_ops(after_non_native)["h"] + + # 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_unitaries_random_circuit(self): """Weyl decomposition for random two-qubit circuit.""" theta = pi / 9 @@ -167,7 +224,7 @@ def test_weyl_parameters(self): self.assertEqual((alpha, beta, gamma), (rzx_alpha, rzx_beta, rzx_gamma)) def test_non_native_weyl_parameters(self): - """Weyl parameters for a non-hardware-native CX direction""" + """Weyl parameters for non-hardware-native CX direction""" qr = QuantumRegister(2, "qr") circuit = QuantumCircuit(qr) qubit_pair = (qr[1], qr[0]) From e7d06e2a298298139c1c8d810abb64bac86ffed4 Mon Sep 17 00:00:00 2001 From: catornow Date: Wed, 28 Jul 2021 10:46:02 +0200 Subject: [PATCH 24/57] Black --- .../transpiler/test_echo_rzx_weyl_decomposition.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/test/python/transpiler/test_echo_rzx_weyl_decomposition.py b/test/python/transpiler/test_echo_rzx_weyl_decomposition.py index 630dfe522ab1..f1e95c3949cd 100644 --- a/test/python/transpiler/test_echo_rzx_weyl_decomposition.py +++ b/test/python/transpiler/test_echo_rzx_weyl_decomposition.py @@ -91,7 +91,7 @@ def test_non_native_weyl_decomposition(self): self.assertRZXgates(unitary_circuit, after) def assertRZXgates(self, unitary_circuit, after): - """"Check the number of rzx gates""" + """ "Check the number of rzx gates""" alpha = TwoQubitWeylDecomposition(unitary_circuit).a beta = TwoQubitWeylDecomposition(unitary_circuit).b gamma = TwoQubitWeylDecomposition(unitary_circuit).c @@ -136,7 +136,9 @@ def test_h_number_non_native_weyl_decomposition_1(self): # 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) + 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""" @@ -164,7 +166,9 @@ def test_h_number_non_native_weyl_decomposition_2(self): # 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) + self.assertEqual( + (circuit_rzx_number / 2) * 4, circuit_non_native_h_number - circuit_h_number + ) def test_weyl_unitaries_random_circuit(self): """Weyl decomposition for random two-qubit circuit.""" From c148aaa188e5b4eb190d9ffaee592c11c1a14468 Mon Sep 17 00:00:00 2001 From: catornow Date: Wed, 28 Jul 2021 10:55:43 +0200 Subject: [PATCH 25/57] Small modifications of the tests --- .../test_echo_rzx_weyl_decomposition.py | 31 ++++++------------- 1 file changed, 9 insertions(+), 22 deletions(-) diff --git a/test/python/transpiler/test_echo_rzx_weyl_decomposition.py b/test/python/transpiler/test_echo_rzx_weyl_decomposition.py index f1e95c3949cd..cd514554ba6a 100644 --- a/test/python/transpiler/test_echo_rzx_weyl_decomposition.py +++ b/test/python/transpiler/test_echo_rzx_weyl_decomposition.py @@ -41,7 +41,7 @@ def setUp(self): self.inst_map = self.backend.defaults().instruction_schedule_map def test_rzx_number_native_weyl_decomposition(self): - """Check the number of RZX gates for a hardware-native CX""" + """Check the number of RZX gates for a hardware-native cx""" qr = QuantumRegister(2, "qr") circuit = QuantumCircuit(qr) circuit.cx(qr[0], qr[1]) @@ -54,25 +54,11 @@ def test_rzx_number_native_weyl_decomposition(self): self.assertTrue(np.allclose(unitary_circuit, unitary_after)) - alpha = TwoQubitWeylDecomposition(unitary_circuit).a - beta = TwoQubitWeylDecomposition(unitary_circuit).b - gamma = TwoQubitWeylDecomposition(unitary_circuit).c - # check whether after circuit has correct number of rzx gates - 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 = after.count_ops()["rzx"] - - self.assertEqual(expected_rzx_number, circuit_rzx_number) + self.assertRZXgates(unitary_circuit, after) def test_non_native_weyl_decomposition(self): - """The RZZ is not in the hardware-native direction""" + """Check the number of RZX gates for a non-hardware-native rzz""" theta = pi / 9 qr = QuantumRegister(2, "qr") circuit = QuantumCircuit(qr) @@ -88,10 +74,11 @@ def test_non_native_weyl_decomposition(self): self.assertTrue(np.allclose(unitary_circuit, unitary_after)) + # check whether after circuit has correct number of rzx gates self.assertRZXgates(unitary_circuit, after) def assertRZXgates(self, unitary_circuit, after): - """ "Check the number of rzx gates""" + """Check the number of rzx gates""" alpha = TwoQubitWeylDecomposition(unitary_circuit).a beta = TwoQubitWeylDecomposition(unitary_circuit).b gamma = TwoQubitWeylDecomposition(unitary_circuit).c @@ -135,7 +122,7 @@ def test_h_number_non_native_weyl_decomposition_1(self): circuit_non_native_h_number = QuantumCircuit.count_ops(after_non_native)["h"] # for each pair of rzx gates four hadamard gates have to be added in - # the case of a non-hardware native directed gate. + # the case of a non-hardware-native directed gate. self.assertEqual( (circuit_rzx_number / 2) * 4, circuit_non_native_h_number - circuit_h_number ) @@ -165,7 +152,7 @@ def test_h_number_non_native_weyl_decomposition_2(self): circuit_non_native_h_number = QuantumCircuit.count_ops(after_non_native)["h"] # for each pair of rzx gates four hadamard gates have to be added in - # the case of a non-hardware native directed gate. + # the case of a non-hardware-native directed gate. self.assertEqual( (circuit_rzx_number / 2) * 4, circuit_non_native_h_number - circuit_h_number ) @@ -205,7 +192,7 @@ def test_weyl_unitaries_random_circuit(self): self.assertTrue(np.allclose(unitary_circuit, unitary_after)) def test_weyl_parameters(self): - """Computation of the correct RZX Weyl parameters""" + """Check whether rzx Weyl parameters are correct""" theta = pi / 3 qr = QuantumRegister(2, "qr") circuit = QuantumCircuit(qr) @@ -228,7 +215,7 @@ def test_weyl_parameters(self): self.assertEqual((alpha, beta, gamma), (rzx_alpha, rzx_beta, rzx_gamma)) def test_non_native_weyl_parameters(self): - """Weyl parameters for non-hardware-native CX direction""" + """Weyl parameters for non-hardware-native cx direction""" qr = QuantumRegister(2, "qr") circuit = QuantumCircuit(qr) qubit_pair = (qr[1], qr[0]) From ae7df3bf0efde95e403c2970a179a2a12c93ad33 Mon Sep 17 00:00:00 2001 From: catornow Date: Wed, 28 Jul 2021 15:57:10 +0200 Subject: [PATCH 26/57] Added release note --- .../notes/echo-rzx-weyl-decomposition-ef72345a58bea9e0.yaml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 releasenotes/notes/echo-rzx-weyl-decomposition-ef72345a58bea9e0.yaml 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. From 562af6ac7c9cc2fbc6e8d866bcf8adbe55422711 Mon Sep 17 00:00:00 2001 From: catornow Date: Wed, 18 Aug 2021 15:58:06 +0200 Subject: [PATCH 27/57] Added the class TwoQubitControlledUDecomposer to generalize TwoQubitWeylEchoRZX. --- .../synthesis/two_qubit_decompose.py | 118 +++++++++++++++++- 1 file changed, 115 insertions(+), 3 deletions(-) diff --git a/qiskit/quantum_info/synthesis/two_qubit_decompose.py b/qiskit/quantum_info/synthesis/two_qubit_decompose.py index 44528c1a24f2..ae0c57f6ac06 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,8 +36,8 @@ import scipy.linalg as la from qiskit.circuit.quantumregister import QuantumRegister -from qiskit.circuit.quantumcircuit import QuantumCircuit -from qiskit.circuit.library.standard_gates import CXGate, RXGate, RYGate, RZGate +from qiskit.circuit.quantumcircuit import QuantumCircuit, Gate +from qiskit.circuit.library.standard_gates import CXGate, RXGate, RYGate, RZGate, RXXGate from qiskit.exceptions import QiskitError from qiskit.quantum_info.operators import Operator from qiskit.quantum_info.synthesis.weyl import weyl_coordinates @@ -547,6 +547,118 @@ 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.""" + + _default_1q_basis = "ZYZ" + + def __init__(self, unitary, rxx_equivalent_gate: Type[Gate]): + """Initialize the KAK decomposition. + + Args: + unitary: The unitary to be decomposed. + rxx_equivalent_gate: Gate that is locally equivalent to an RXXGate: + U ~ Ud(α, 0, 0) ~ Ctrl-U gate. + """ + self.rxx_equivalent_gate = rxx_equivalent_gate + self.decomposer = TwoQubitWeylDecomposition(unitary) + + def circuit(self, *, euler_basis: Optional[str] = None, atol=DEFAULT_ATOL) -> QuantumCircuit: + """Returns the Weyl decomposition in circuit form. + + Note: atol ist passed to OneQubitEulerDecomposer. + """ + + if euler_basis is None: + euler_basis = self._default_1q_basis + + oneq_decompose = OneQubitEulerDecomposer(euler_basis) + 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, euler_basis=None): + """ + 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. + """ + circ = QuantumCircuit(2) + circ.append(self.rxx_equivalent_gate(angle), qargs=[0, 1]) + + unitary = Operator(circ).data + + decomposer_rxx = TwoQubitWeylControlledEquiv(unitary) + + if euler_basis is None: + euler_basis = self._default_1q_basis + oneq_decompose = OneQubitEulerDecomposer(euler_basis) + + rxx_circ = QuantumCircuit(2, global_phase=-decomposer_rxx.global_phase) + rxx_circ.compose(oneq_decompose(decomposer_rxx.K2r).inverse(), inplace=True, qubits=[0]) + rxx_circ.compose(oneq_decompose(decomposer_rxx.K2l).inverse(), inplace=True, qubits=[1]) + rxx_circ.compose(circ, inplace=True) + rxx_circ.compose(oneq_decompose(decomposer_rxx.K1r).inverse(), inplace=True, qubits=[0]) + rxx_circ.compose(oneq_decompose(decomposer_rxx.K1l).inverse(), inplace=True, qubits=[1]) + + # The circuit should be equivalent to an RXXGate. + qc_rxx = QuantumCircuit(2) + qc_rxx.append(RXXGate(angle), qargs=[0, 1]) + if not np.allclose(Operator(rxx_circ).data, Operator(qc_rxx).data): + raise QiskitError("Failed to reproduce RXXGate.") + + 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: + circ_rzz = QuantumCircuit(2) + circ_rzz.h(0) + circ_rzz.h(1) + circ_rzz.compose(self._to_rxx_gate(-2 * self.decomposer.c), inplace=True) + circ_rzz.h(0) + circ_rzz.h(1) + circ.compose(circ_rzz, inplace=True) + + return circ + + class TwoQubitWeylEchoRZX: """Decompose two-qubit unitary in terms of echoed cross-resonance gates.""" From 148e8e956586879e964e025ba2e721e6be6ef54f Mon Sep 17 00:00:00 2001 From: catornow <79633854+catornow@users.noreply.github.com> Date: Tue, 24 Aug 2021 11:14:26 +0200 Subject: [PATCH 28/57] Update qiskit/quantum_info/synthesis/two_qubit_decompose.py Co-authored-by: Daniel Egger <38065505+eggerdj@users.noreply.github.com> --- qiskit/quantum_info/synthesis/two_qubit_decompose.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiskit/quantum_info/synthesis/two_qubit_decompose.py b/qiskit/quantum_info/synthesis/two_qubit_decompose.py index ae0c57f6ac06..82ca9eefdfd3 100644 --- a/qiskit/quantum_info/synthesis/two_qubit_decompose.py +++ b/qiskit/quantum_info/synthesis/two_qubit_decompose.py @@ -553,7 +553,7 @@ class TwoQubitControlledUDecomposer: _default_1q_basis = "ZYZ" - def __init__(self, unitary, rxx_equivalent_gate: Type[Gate]): + def __init__(self, rxx_equivalent_gate: Type[Gate]): """Initialize the KAK decomposition. Args: From 2210b73e1069180169f65a8d436641485058fedb Mon Sep 17 00:00:00 2001 From: catornow <79633854+catornow@users.noreply.github.com> Date: Tue, 24 Aug 2021 11:14:34 +0200 Subject: [PATCH 29/57] Update qiskit/quantum_info/synthesis/two_qubit_decompose.py Co-authored-by: Daniel Egger <38065505+eggerdj@users.noreply.github.com> --- qiskit/quantum_info/synthesis/two_qubit_decompose.py | 1 - 1 file changed, 1 deletion(-) diff --git a/qiskit/quantum_info/synthesis/two_qubit_decompose.py b/qiskit/quantum_info/synthesis/two_qubit_decompose.py index 82ca9eefdfd3..bb6368a4f3f2 100644 --- a/qiskit/quantum_info/synthesis/two_qubit_decompose.py +++ b/qiskit/quantum_info/synthesis/two_qubit_decompose.py @@ -562,7 +562,6 @@ def __init__(self, rxx_equivalent_gate: Type[Gate]): U ~ Ud(α, 0, 0) ~ Ctrl-U gate. """ self.rxx_equivalent_gate = rxx_equivalent_gate - self.decomposer = TwoQubitWeylDecomposition(unitary) def circuit(self, *, euler_basis: Optional[str] = None, atol=DEFAULT_ATOL) -> QuantumCircuit: """Returns the Weyl decomposition in circuit form. From e5a4370da3f9a891c009b4de93dfd2a889a92105 Mon Sep 17 00:00:00 2001 From: catornow <79633854+catornow@users.noreply.github.com> Date: Tue, 24 Aug 2021 11:14:42 +0200 Subject: [PATCH 30/57] Update qiskit/quantum_info/synthesis/two_qubit_decompose.py Co-authored-by: Daniel Egger <38065505+eggerdj@users.noreply.github.com> --- qiskit/quantum_info/synthesis/two_qubit_decompose.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiskit/quantum_info/synthesis/two_qubit_decompose.py b/qiskit/quantum_info/synthesis/two_qubit_decompose.py index bb6368a4f3f2..3ec1b78b10c6 100644 --- a/qiskit/quantum_info/synthesis/two_qubit_decompose.py +++ b/qiskit/quantum_info/synthesis/two_qubit_decompose.py @@ -563,7 +563,7 @@ def __init__(self, rxx_equivalent_gate: Type[Gate]): """ self.rxx_equivalent_gate = rxx_equivalent_gate - def circuit(self, *, euler_basis: Optional[str] = None, atol=DEFAULT_ATOL) -> QuantumCircuit: + def __call__(self, unitary, *, euler_basis: Optional[str] = None, atol=DEFAULT_ATOL) -> QuantumCircuit: """Returns the Weyl decomposition in circuit form. Note: atol ist passed to OneQubitEulerDecomposer. From 7ed3b1a1cb4cca328f60bb43e76bbe10f661d37c Mon Sep 17 00:00:00 2001 From: catornow <79633854+catornow@users.noreply.github.com> Date: Tue, 24 Aug 2021 11:15:10 +0200 Subject: [PATCH 31/57] Update qiskit/quantum_info/synthesis/two_qubit_decompose.py Co-authored-by: Daniel Egger <38065505+eggerdj@users.noreply.github.com> --- qiskit/quantum_info/synthesis/two_qubit_decompose.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/qiskit/quantum_info/synthesis/two_qubit_decompose.py b/qiskit/quantum_info/synthesis/two_qubit_decompose.py index 3ec1b78b10c6..e115851b3327 100644 --- a/qiskit/quantum_info/synthesis/two_qubit_decompose.py +++ b/qiskit/quantum_info/synthesis/two_qubit_decompose.py @@ -569,6 +569,8 @@ def __call__(self, unitary, *, euler_basis: Optional[str] = None, atol=DEFAULT_A Note: atol ist passed to OneQubitEulerDecomposer. """ +self.decomposer = TwoQubitWeylDecomposition(unitary) + if euler_basis is None: euler_basis = self._default_1q_basis From d996963dfa35fafcde874630511610c600d6c800 Mon Sep 17 00:00:00 2001 From: catornow <79633854+catornow@users.noreply.github.com> Date: Tue, 24 Aug 2021 11:15:23 +0200 Subject: [PATCH 32/57] Update qiskit/quantum_info/synthesis/two_qubit_decompose.py Co-authored-by: Daniel Egger <38065505+eggerdj@users.noreply.github.com> --- qiskit/quantum_info/synthesis/two_qubit_decompose.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/qiskit/quantum_info/synthesis/two_qubit_decompose.py b/qiskit/quantum_info/synthesis/two_qubit_decompose.py index e115851b3327..dda249b8f27c 100644 --- a/qiskit/quantum_info/synthesis/two_qubit_decompose.py +++ b/qiskit/quantum_info/synthesis/two_qubit_decompose.py @@ -561,6 +561,12 @@ def __init__(self, rxx_equivalent_gate: Type[Gate]): rxx_equivalent_gate: Gate that is locally equivalent to an RXXGate: U ~ Ud(α, 0, 0) ~ Ctrl-U gate. """ + # Do a few spot checks that the KAK decomposition gives (x, 0, 0) + for test_angle in [0.2, 0.3, np.pi / 2]: + decomp = TwoQubitWeylDecomposition(Operator(rxx_equivalent_gate(test_angle)).data) + if not np.allclose([decomp.b, decomp.c], [0, 0]): + raise QiskitError(f"{rxx_equivalent_gate.__name__} is not equivalent to an RXXGate.") + self.rxx_equivalent_gate = rxx_equivalent_gate def __call__(self, unitary, *, euler_basis: Optional[str] = None, atol=DEFAULT_ATOL) -> QuantumCircuit: From 6903d22d5102b47089880764d57ab09a288f72a4 Mon Sep 17 00:00:00 2001 From: catornow <79633854+catornow@users.noreply.github.com> Date: Tue, 24 Aug 2021 11:15:38 +0200 Subject: [PATCH 33/57] Update qiskit/transpiler/passes/optimization/echo_rzx_weyl_decomposition.py Co-authored-by: Daniel Egger <38065505+eggerdj@users.noreply.github.com> --- .../passes/optimization/echo_rzx_weyl_decomposition.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/qiskit/transpiler/passes/optimization/echo_rzx_weyl_decomposition.py b/qiskit/transpiler/passes/optimization/echo_rzx_weyl_decomposition.py index 84ddd0b2a5f2..d9c78c3d6e50 100644 --- a/qiskit/transpiler/passes/optimization/echo_rzx_weyl_decomposition.py +++ b/qiskit/transpiler/passes/optimization/echo_rzx_weyl_decomposition.py @@ -71,6 +71,8 @@ def run(self, dag: DAGCircuit): trivial_layout = Layout.generate_trivial_layout(*dag.qregs.values()) +decomposer = TwoQubitControlledUDecomposer(RZXGate) + for node in dag.two_qubit_ops(): if node.type == "op": control = node.qargs[0] From 97e0221f374c5040ad2e84862f0aef0feaaadf5f Mon Sep 17 00:00:00 2001 From: catornow <79633854+catornow@users.noreply.github.com> Date: Tue, 24 Aug 2021 11:15:56 +0200 Subject: [PATCH 34/57] Update qiskit/transpiler/passes/optimization/echo_rzx_weyl_decomposition.py Co-authored-by: Daniel Egger <38065505+eggerdj@users.noreply.github.com> --- .../passes/optimization/echo_rzx_weyl_decomposition.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiskit/transpiler/passes/optimization/echo_rzx_weyl_decomposition.py b/qiskit/transpiler/passes/optimization/echo_rzx_weyl_decomposition.py index d9c78c3d6e50..8e90625e43da 100644 --- a/qiskit/transpiler/passes/optimization/echo_rzx_weyl_decomposition.py +++ b/qiskit/transpiler/passes/optimization/echo_rzx_weyl_decomposition.py @@ -85,7 +85,7 @@ def run(self, dag: DAGCircuit): unitary = qi.Operator(node.op).data dag_weyl = circuit_to_dag( - TwoQubitWeylEchoRZX(unitary, is_native=is_native).circuit() + decomposer(unitary) ) dag.substitute_node_with_dag(node, dag_weyl) From 49bc0ded0ee0d4f161b9a1ef474a4b91035bbad0 Mon Sep 17 00:00:00 2001 From: catornow Date: Fri, 27 Aug 2021 16:38:39 +0200 Subject: [PATCH 35/57] Modified TwoQubitControlledUDecomposer to work for CPhaseGate, CRZGate; removed TwoQubitWeylEchoRZX --- .../synthesis/two_qubit_decompose.py | 151 ++++++------------ 1 file changed, 48 insertions(+), 103 deletions(-) diff --git a/qiskit/quantum_info/synthesis/two_qubit_decompose.py b/qiskit/quantum_info/synthesis/two_qubit_decompose.py index dda249b8f27c..63ae8c4a4dcb 100644 --- a/qiskit/quantum_info/synthesis/two_qubit_decompose.py +++ b/qiskit/quantum_info/synthesis/two_qubit_decompose.py @@ -560,22 +560,28 @@ def __init__(self, rxx_equivalent_gate: Type[Gate]): unitary: The unitary to be decomposed. 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. """ # Do a few spot checks that the KAK decomposition gives (x, 0, 0) for test_angle in [0.2, 0.3, np.pi / 2]: decomp = TwoQubitWeylDecomposition(Operator(rxx_equivalent_gate(test_angle)).data) if not np.allclose([decomp.b, decomp.c], [0, 0]): - raise QiskitError(f"{rxx_equivalent_gate.__name__} is not equivalent to an RXXGate.") + raise QiskitError( + f"{rxx_equivalent_gate.__name__} is not equivalent to an RXXGate." + ) self.rxx_equivalent_gate = rxx_equivalent_gate - def __call__(self, unitary, *, euler_basis: Optional[str] = None, atol=DEFAULT_ATOL) -> QuantumCircuit: + def __call__( + self, unitary, *, euler_basis: Optional[str] = None, atol=DEFAULT_ATOL + ) -> QuantumCircuit: """Returns the Weyl decomposition in circuit form. Note: atol ist passed to OneQubitEulerDecomposer. """ -self.decomposer = TwoQubitWeylDecomposition(unitary) + self.decomposer = TwoQubitWeylDecomposition(unitary) if euler_basis is None: euler_basis = self._default_1q_basis @@ -602,6 +608,7 @@ def _to_rxx_gate(self, angle: float, euler_basis=None): """ 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) @@ -611,29 +618,43 @@ def _to_rxx_gate(self, angle: float, euler_basis=None): 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.rxx(angle, 0, 1) + decomposer_rxx = TwoQubitWeylControlledEquiv(Operator(circ).data) + circ = QuantumCircuit(2) circ.append(self.rxx_equivalent_gate(angle), qargs=[0, 1]) + decomposer_equiv = TwoQubitWeylControlledEquiv(Operator(circ).data) - unitary = Operator(circ).data + scale = decomposer_rxx.a / decomposer_equiv.a - decomposer_rxx = TwoQubitWeylControlledEquiv(unitary) + circ = QuantumCircuit(2) + circ.append(self.rxx_equivalent_gate(scale * angle), qargs=[0, 1]) + decomposer_inv = TwoQubitWeylControlledEquiv(Operator(circ).data) if euler_basis is None: euler_basis = self._default_1q_basis oneq_decompose = OneQubitEulerDecomposer(euler_basis) - rxx_circ = QuantumCircuit(2, global_phase=-decomposer_rxx.global_phase) - rxx_circ.compose(oneq_decompose(decomposer_rxx.K2r).inverse(), inplace=True, qubits=[0]) - rxx_circ.compose(oneq_decompose(decomposer_rxx.K2l).inverse(), inplace=True, qubits=[1]) + # 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_rxx.K1r).inverse(), inplace=True, qubits=[0]) - rxx_circ.compose(oneq_decompose(decomposer_rxx.K1l).inverse(), inplace=True, qubits=[1]) + 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]) - # The circuit should be equivalent to an RXXGate. + # Ensure the circuit is equivalent to an RXXGate. qc_rxx = QuantumCircuit(2) qc_rxx.append(RXXGate(angle), qargs=[0, 1]) if not np.allclose(Operator(rxx_circ).data, Operator(qc_rxx).data): - raise QiskitError("Failed to reproduce RXXGate.") + raise QiskitError(f"Failed to reproduce RXXGate for angle {angle}.") return rxx_circ @@ -655,106 +676,30 @@ def _weyl_gate(self, circ: QuantumCircuit, atol=1.0e-13): # translate the RZZGate(c) into a circuit based on the desired Ctrl-U gate. if abs(self.decomposer.c) > atol: + # Since the Weyl chember 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(-2 * self.decomposer.c), inplace=True) + circ_rzz.compose(self._to_rxx_gate(gamma), inplace=True) circ_rzz.h(0) circ_rzz.h(1) - circ.compose(circ_rzz, inplace=True) - - return circ - - -class TwoQubitWeylEchoRZX: - """Decompose two-qubit unitary in terms of echoed cross-resonance gates.""" - - _default_1q_basis = "XYX" - def __init__(self, unitary, is_native: bool): - """Initialize the KAK decomposition. - - Args: - is_native: If True then the CX schedule on qubits (q0, q1) - is shorter than the schedule on (q1, q0). - """ - self.is_native = is_native - - self.decomposer = TwoQubitWeylDecomposition(unitary) - - def circuit(self, *, euler_basis: Optional[str] = None, atol=DEFAULT_ATOL) -> QuantumCircuit: - """Returns the Weyl decomposition in circuit form. + if invert: + circ.compose(circ_rzz.inverse(), inplace=True) + else: + circ.compose(circ_rzz, inplace=True) - Note: atol ist passed to OneQubitEulerDecomposer. - """ - if euler_basis is None: - euler_basis = self._default_1q_basis - oneq_decompose = OneQubitEulerDecomposer(euler_basis) - 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, atol) - circ.compose(c1r, [0], inplace=True) - circ.compose(c1l, [1], inplace=True) return circ - @staticmethod - def _apply_rzx(circ: QuantumCircuit, angle: float): - """Echoed RZX gate""" - circ.rzx(-angle, 0, 1) - circ.x(0) - circ.rzx(angle, 0, 1) - circ.x(0) - - @staticmethod - def _apply_reverse_rzx(circ: QuantumCircuit, angle: float): - """Reverse direction of the echoed RZX gate""" - circ.h(0) - circ.h(1) - circ.rzx(-angle, 1, 0) - circ.x(1) - circ.rzx(angle, 1, 0) - circ.x(1) - circ.h(0) - circ.h(1) - - def _weyl_gate(self, circ: QuantumCircuit, atol=1.0e-13): - """Appends Ud(a, b, c) to the circuit.""" - circ.h(0) - if abs(self.decomposer.a) > atol: - if self.is_native: - self._apply_rzx(circ, self.decomposer.a) - else: - self._apply_reverse_rzx(circ, self.decomposer.a) - circ.h(0) - circ.sdg(0) - circ.h(0) - circ.sdg(1) - if abs(self.decomposer.b) > atol: - if self.is_native: - self._apply_rzx(circ, self.decomposer.b) - else: - self._apply_reverse_rzx(circ, self.decomposer.b) - circ.h(0) - circ.s(0) - circ.s(1) - circ.h(1) - if abs(self.decomposer.c) > atol: - if self.is_native: - self._apply_rzx(circ, self.decomposer.c) - else: - self._apply_reverse_rzx(circ, self.decomposer.c) - circ.h(1) - class TwoQubitWeylMirrorControlledEquiv(TwoQubitWeylDecomposition): """U ~ Ud(𝜋/4, 𝜋/4, α) ~ SWAP . Ctrl-U From 60316e6ed47e72a09bb6a9a9010166e65f13eaa1 Mon Sep 17 00:00:00 2001 From: catornow Date: Fri, 27 Aug 2021 16:42:33 +0200 Subject: [PATCH 36/57] Added tests for TwoQubitControlledUDecomposer --- test/python/quantum_info/test_synthesis.py | 37 ++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/test/python/quantum_info/test_synthesis.py b/test/python/quantum_info/test_synthesis.py index ae59793b276e..5fb36c446a1e 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,36 @@ 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_rxx_not_reproducible(self): + """Test that an exception is raised if RXXGate cannot be reproduced""" + unitary = random_unitary(4) + for gate in [CXGate, CZGate]: + decomposer = TwoQubitControlledUDecomposer(gate) + with self.assertRaises(QiskitError) as exc: + decomposer(unitary) + self.assertIn("Failed to reproduce RXXGate for angle", exc.exception.message) + + 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("is not equivalent to an RXXGate.", exc.exception.message) + + class TestDecomposeProductRaises(QiskitTestCase): """Check that exceptions are raised when 2q matrix is not a product of 1q unitaries""" From b359a4d6dacc958509f4f7440da455bc3c8aa67a Mon Sep 17 00:00:00 2001 From: catornow Date: Fri, 27 Aug 2021 16:43:49 +0200 Subject: [PATCH 37/57] Moved is_native to EchoRZXWeylDecomposition transpiler pass --- .../echo_rzx_weyl_decomposition.py | 51 ++++++++++++++++--- 1 file changed, 43 insertions(+), 8 deletions(-) diff --git a/qiskit/transpiler/passes/optimization/echo_rzx_weyl_decomposition.py b/qiskit/transpiler/passes/optimization/echo_rzx_weyl_decomposition.py index 8e90625e43da..1ba4c9e28351 100644 --- a/qiskit/transpiler/passes/optimization/echo_rzx_weyl_decomposition.py +++ b/qiskit/transpiler/passes/optimization/echo_rzx_weyl_decomposition.py @@ -14,6 +14,9 @@ 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 @@ -23,8 +26,7 @@ from qiskit.transpiler.layout import Layout import qiskit.quantum_info as qi - -from qiskit.quantum_info.synthesis.two_qubit_decompose import TwoQubitWeylEchoRZX +from qiskit.quantum_info.synthesis.two_qubit_decompose import TwoQubitControlledUDecomposer class EchoRZXWeylDecomposition(TransformationPass): @@ -46,6 +48,32 @@ def _is_native(self, qubit_pair: Tuple) -> bool: 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`. @@ -71,10 +99,17 @@ def run(self, dag: DAGCircuit): trivial_layout = Layout.generate_trivial_layout(*dag.qregs.values()) -decomposer = TwoQubitControlledUDecomposer(RZXGate) + decomposer = TwoQubitControlledUDecomposer(RZXGate) for node in dag.two_qubit_ops(): if node.type == "op": + + 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.type == "op" and node.name == "rzx": control = node.qargs[0] target = node.qargs[1] @@ -83,10 +118,10 @@ def run(self, dag: DAGCircuit): is_native = self._is_native((physical_q0, physical_q1)) - unitary = qi.Operator(node.op).data - dag_weyl = circuit_to_dag( - decomposer(unitary) - ) - dag.substitute_node_with_dag(node, dag_weyl) + 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 From 2c26d6ff8910cfb12105781d5d6738e128374965 Mon Sep 17 00:00:00 2001 From: catornow Date: Fri, 27 Aug 2021 16:49:14 +0200 Subject: [PATCH 38/57] Removed TwoQubitWeylEchoRZX tests (since this class does not exist anymore) --- .../test_echo_rzx_weyl_decomposition.py | 71 +++++-------------- 1 file changed, 17 insertions(+), 54 deletions(-) diff --git a/test/python/transpiler/test_echo_rzx_weyl_decomposition.py b/test/python/transpiler/test_echo_rzx_weyl_decomposition.py index cd514554ba6a..f962f95246b5 100644 --- a/test/python/transpiler/test_echo_rzx_weyl_decomposition.py +++ b/test/python/transpiler/test_echo_rzx_weyl_decomposition.py @@ -10,7 +10,7 @@ # copyright notice, and modified files need to carry a notice indicating # that they have been altered from the originals. -"""Test the EchoRZXWeylDecomposition pass and the TwoQubitWeylEchoRZX class""" +"""Test the EchoRZXWeylDecomposition pass""" import unittest from math import pi import numpy as np @@ -28,12 +28,11 @@ from qiskit.quantum_info.synthesis.two_qubit_decompose import ( TwoQubitWeylDecomposition, - TwoQubitWeylEchoRZX, ) class TestEchoRZXWeylDecomposition(QiskitTestCase): - """Tests the EchoRZXWeylDecomposition pass and the TwoQubitWeylEchoRZX class.""" + """Tests the EchoRZXWeylDecomposition pass.""" def setUp(self): super().setUp() @@ -96,6 +95,15 @@ def assertRZXgates(self, unitary_circuit, after): 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_h_number_non_native_weyl_decomposition_1(self): """Check the number of added Hadamard gates for an rzz gate""" theta = pi / 11 @@ -116,10 +124,10 @@ def test_h_number_non_native_weyl_decomposition_1(self): pass_ = EchoRZXWeylDecomposition(self.inst_map) after_non_native = dag_to_circuit(pass_.run(dag_non_native)) - circuit_rzx_number = QuantumCircuit.count_ops(after)["rzx"] + circuit_rzx_number = self.count_gate_number("rzx", after) - circuit_h_number = QuantumCircuit.count_ops(after)["h"] - circuit_non_native_h_number = QuantumCircuit.count_ops(after_non_native)["h"] + 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. @@ -146,10 +154,10 @@ def test_h_number_non_native_weyl_decomposition_2(self): pass_ = EchoRZXWeylDecomposition(self.inst_map) after_non_native = dag_to_circuit(pass_.run(dag_non_native)) - circuit_rzx_number = QuantumCircuit.count_ops(after)["rzx"] + circuit_rzx_number = self.count_gate_number("rzx", after) - circuit_h_number = QuantumCircuit.count_ops(after)["h"] - circuit_non_native_h_number = QuantumCircuit.count_ops(after_non_native)["h"] + 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. @@ -191,51 +199,6 @@ def test_weyl_unitaries_random_circuit(self): self.assertTrue(np.allclose(unitary_circuit, unitary_after)) - def test_weyl_parameters(self): - """Check whether rzx Weyl parameters are correct""" - theta = pi / 3 - qr = QuantumRegister(2, "qr") - circuit = QuantumCircuit(qr) - qubit_pair = (qr[0], qr[1]) - - circuit.rzz(theta, qubit_pair[0], qubit_pair[1]) - circuit.swap(qubit_pair[0], qubit_pair[1]) - unitary_circuit = qi.Operator(circuit).data - - # Weyl parameters (alpha, beta, gamma) - alpha = TwoQubitWeylDecomposition(unitary_circuit).a - beta = TwoQubitWeylDecomposition(unitary_circuit).b - gamma = TwoQubitWeylDecomposition(unitary_circuit).c - - # RZX Weyl parameters (rzx_alpha, rzx_beta, rzx_gamma) - rzx_alpha = TwoQubitWeylEchoRZX(unitary_circuit, is_native=True).decomposer.a - rzx_beta = TwoQubitWeylEchoRZX(unitary_circuit, is_native=True).decomposer.b - rzx_gamma = TwoQubitWeylEchoRZX(unitary_circuit, is_native=True).decomposer.c - - self.assertEqual((alpha, beta, gamma), (rzx_alpha, rzx_beta, rzx_gamma)) - - def test_non_native_weyl_parameters(self): - """Weyl parameters for non-hardware-native cx direction""" - qr = QuantumRegister(2, "qr") - circuit = QuantumCircuit(qr) - qubit_pair = (qr[1], qr[0]) - circuit.cx(qubit_pair[1], qubit_pair[0]) - unitary_circuit = qi.Operator(circuit).data - - # Weyl parameters (alpha, beta, gamma) - alpha = TwoQubitWeylDecomposition(unitary_circuit).a - beta = TwoQubitWeylDecomposition(unitary_circuit).b - gamma = TwoQubitWeylDecomposition(unitary_circuit).c - - # RZX Weyl parameters (rzx_alpha, rzx_beta, rzx_gamma) - rzx_alpha = TwoQubitWeylEchoRZX(unitary_circuit, is_native=False).decomposer.a - rzx_beta = TwoQubitWeylEchoRZX(unitary_circuit, is_native=False).decomposer.b - rzx_gamma = TwoQubitWeylEchoRZX(unitary_circuit, is_native=False).decomposer.c - - self.assertAlmostEqual(alpha, rzx_alpha) - self.assertAlmostEqual(beta, rzx_beta) - self.assertAlmostEqual(gamma, rzx_gamma) - if __name__ == "__main__": unittest.main() From a7018a4817126360ee58863dbd09bd861dfeadaf Mon Sep 17 00:00:00 2001 From: catornow Date: Fri, 27 Aug 2021 17:35:57 +0200 Subject: [PATCH 39/57] Small modification --- qiskit/transpiler/passes/scheduling/calibration_creators.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiskit/transpiler/passes/scheduling/calibration_creators.py b/qiskit/transpiler/passes/scheduling/calibration_creators.py index 005e1356ddbe..deac93fdc943 100644 --- a/qiskit/transpiler/passes/scheduling/calibration_creators.py +++ b/qiskit/transpiler/passes/scheduling/calibration_creators.py @@ -29,7 +29,7 @@ from qiskit.pulse.instructions.instruction import Instruction from qiskit.exceptions import QiskitError from qiskit.providers import BaseBackend -from qiskit.dagcircuit import DAGNode +from qiskit.dagcircuit import DAGOpNode from qiskit.circuit.library.standard_gates import RZXGate from qiskit.transpiler.basepasses import TransformationPass From 80517539b9ed968271dfc7b82c35b6a7b32885b1 Mon Sep 17 00:00:00 2001 From: catornow <79633854+catornow@users.noreply.github.com> Date: Fri, 27 Aug 2021 18:03:10 +0200 Subject: [PATCH 40/57] Update qiskit/quantum_info/synthesis/two_qubit_decompose.py Co-authored-by: Daniel Egger <38065505+eggerdj@users.noreply.github.com> --- qiskit/quantum_info/synthesis/two_qubit_decompose.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/qiskit/quantum_info/synthesis/two_qubit_decompose.py b/qiskit/quantum_info/synthesis/two_qubit_decompose.py index ee883cc912f2..c968bb4eee84 100644 --- a/qiskit/quantum_info/synthesis/two_qubit_decompose.py +++ b/qiskit/quantum_info/synthesis/two_qubit_decompose.py @@ -582,9 +582,7 @@ def __init__(self, rxx_equivalent_gate: Type[Gate]): self.rxx_equivalent_gate = rxx_equivalent_gate - def __call__( - self, unitary, *, euler_basis: Optional[str] = None, atol=DEFAULT_ATOL - ) -> QuantumCircuit: + def __call__(self, unitary, *, atol=DEFAULT_ATOL) -> QuantumCircuit: """Returns the Weyl decomposition in circuit form. Note: atol ist passed to OneQubitEulerDecomposer. From 5cf54dacafac8a32cb29d871733fc3e11892f5d4 Mon Sep 17 00:00:00 2001 From: catornow <79633854+catornow@users.noreply.github.com> Date: Fri, 27 Aug 2021 18:03:19 +0200 Subject: [PATCH 41/57] Update qiskit/quantum_info/synthesis/two_qubit_decompose.py Co-authored-by: Daniel Egger <38065505+eggerdj@users.noreply.github.com> --- qiskit/quantum_info/synthesis/two_qubit_decompose.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiskit/quantum_info/synthesis/two_qubit_decompose.py b/qiskit/quantum_info/synthesis/two_qubit_decompose.py index c968bb4eee84..59d748d867e0 100644 --- a/qiskit/quantum_info/synthesis/two_qubit_decompose.py +++ b/qiskit/quantum_info/synthesis/two_qubit_decompose.py @@ -593,7 +593,7 @@ def __call__(self, unitary, *, atol=DEFAULT_ATOL) -> QuantumCircuit: if euler_basis is None: euler_basis = self._default_1q_basis - oneq_decompose = OneQubitEulerDecomposer(euler_basis) + oneq_decompose = OneQubitEulerDecomposer("ZYZ") c1l, c1r, c2l, c2r = ( oneq_decompose(k, atol=atol) for k in ( From 2dd8097c2cf68ffc87815ae166aee76ed068738f Mon Sep 17 00:00:00 2001 From: catornow <79633854+catornow@users.noreply.github.com> Date: Fri, 27 Aug 2021 18:03:27 +0200 Subject: [PATCH 42/57] Update qiskit/quantum_info/synthesis/two_qubit_decompose.py Co-authored-by: Daniel Egger <38065505+eggerdj@users.noreply.github.com> --- qiskit/quantum_info/synthesis/two_qubit_decompose.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/qiskit/quantum_info/synthesis/two_qubit_decompose.py b/qiskit/quantum_info/synthesis/two_qubit_decompose.py index 59d748d867e0..1cd1df6608bf 100644 --- a/qiskit/quantum_info/synthesis/two_qubit_decompose.py +++ b/qiskit/quantum_info/synthesis/two_qubit_decompose.py @@ -590,9 +590,6 @@ def __call__(self, unitary, *, atol=DEFAULT_ATOL) -> QuantumCircuit: self.decomposer = TwoQubitWeylDecomposition(unitary) - if euler_basis is None: - euler_basis = self._default_1q_basis - oneq_decompose = OneQubitEulerDecomposer("ZYZ") c1l, c1r, c2l, c2r = ( oneq_decompose(k, atol=atol) From cb33ef5101184f56e0970d307abcfd5a9120cdbe Mon Sep 17 00:00:00 2001 From: catornow <79633854+catornow@users.noreply.github.com> Date: Fri, 27 Aug 2021 18:03:34 +0200 Subject: [PATCH 43/57] Update qiskit/quantum_info/synthesis/two_qubit_decompose.py Co-authored-by: Daniel Egger <38065505+eggerdj@users.noreply.github.com> --- qiskit/quantum_info/synthesis/two_qubit_decompose.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/qiskit/quantum_info/synthesis/two_qubit_decompose.py b/qiskit/quantum_info/synthesis/two_qubit_decompose.py index 1cd1df6608bf..e5f69102d67b 100644 --- a/qiskit/quantum_info/synthesis/two_qubit_decompose.py +++ b/qiskit/quantum_info/synthesis/two_qubit_decompose.py @@ -560,8 +560,6 @@ 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.""" - _default_1q_basis = "ZYZ" - def __init__(self, rxx_equivalent_gate: Type[Gate]): """Initialize the KAK decomposition. From a2f52d5968beae8c2685211ad894724f9821fcf0 Mon Sep 17 00:00:00 2001 From: catornow Date: Fri, 27 Aug 2021 20:28:48 +0200 Subject: [PATCH 44/57] Fixed style and lint error --- qiskit/quantum_info/synthesis/two_qubit_decompose.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/qiskit/quantum_info/synthesis/two_qubit_decompose.py b/qiskit/quantum_info/synthesis/two_qubit_decompose.py index e5f69102d67b..a7cd18cc1d2c 100644 --- a/qiskit/quantum_info/synthesis/two_qubit_decompose.py +++ b/qiskit/quantum_info/synthesis/two_qubit_decompose.py @@ -564,7 +564,6 @@ def __init__(self, rxx_equivalent_gate: Type[Gate]): """Initialize the KAK decomposition. Args: - unitary: The unitary to be decomposed. rxx_equivalent_gate: Gate that is locally equivalent to an RXXGate: U ~ Ud(α, 0, 0) ~ Ctrl-U gate. Raises: @@ -586,6 +585,7 @@ def __call__(self, unitary, *, atol=DEFAULT_ATOL) -> QuantumCircuit: Note: atol ist passed to OneQubitEulerDecomposer. """ + # pylint: disable=attribute-defined-outside-init self.decomposer = TwoQubitWeylDecomposition(unitary) oneq_decompose = OneQubitEulerDecomposer("ZYZ") @@ -606,7 +606,7 @@ def __call__(self, unitary, *, atol=DEFAULT_ATOL) -> QuantumCircuit: circ.compose(c1l, [1], inplace=True) return circ - def _to_rxx_gate(self, angle: float, euler_basis=None): + 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. @@ -640,9 +640,7 @@ def _to_rxx_gate(self, angle: float, euler_basis=None): circ.append(self.rxx_equivalent_gate(scale * angle), qargs=[0, 1]) decomposer_inv = TwoQubitWeylControlledEquiv(Operator(circ).data) - if euler_basis is None: - euler_basis = self._default_1q_basis - oneq_decompose = OneQubitEulerDecomposer(euler_basis) + 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) From 69bf841291d3d1bcbf689362a1a0be31b59bc674 Mon Sep 17 00:00:00 2001 From: catornow Date: Fri, 27 Aug 2021 21:57:18 +0200 Subject: [PATCH 45/57] Removed ```node.type == "op"``` in EchoRZXWeylDecomposition transpiler pass to remove deprecation warning --- .../passes/optimization/echo_rzx_weyl_decomposition.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/qiskit/transpiler/passes/optimization/echo_rzx_weyl_decomposition.py b/qiskit/transpiler/passes/optimization/echo_rzx_weyl_decomposition.py index 1ba4c9e28351..139f61e75967 100644 --- a/qiskit/transpiler/passes/optimization/echo_rzx_weyl_decomposition.py +++ b/qiskit/transpiler/passes/optimization/echo_rzx_weyl_decomposition.py @@ -102,14 +102,13 @@ def run(self, dag: DAGCircuit): decomposer = TwoQubitControlledUDecomposer(RZXGate) for node in dag.two_qubit_ops(): - if node.type == "op": - unitary = qi.Operator(node.op).data - dag_weyl = circuit_to_dag(decomposer(unitary)) - dag.substitute_node_with_dag(node, dag_weyl) + 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.type == "op" and node.name == "rzx": + if node.name == "rzx": control = node.qargs[0] target = node.qargs[1] From 8dff1061adc4ad03adc44f972d55f229be8eafd2 Mon Sep 17 00:00:00 2001 From: catornow Date: Wed, 1 Sep 2021 15:28:08 +0200 Subject: [PATCH 46/57] Changed ```gate=node.op.name``` back to ```gate=node.op``` in CalibrationBuilder --- qiskit/transpiler/passes/calibration/builders.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiskit/transpiler/passes/calibration/builders.py b/qiskit/transpiler/passes/calibration/builders.py index 68eee47f2235..ee5355126538 100644 --- a/qiskit/transpiler/passes/calibration/builders.py +++ b/qiskit/transpiler/passes/calibration/builders.py @@ -84,7 +84,7 @@ def run(self, dag: DAGCircuit) -> DAGCircuit: # add calibration if it is not backend default if publisher != CalibrationPublisher.BACKEND_PROVIDER: - dag.add_calibration(gate=node.op.name, qubits=qubits, schedule=schedule) + dag.add_calibration(gate=node.op, qubits=qubits, schedule=schedule) return dag From e0f0c2e3452f8dd880aa16018529131f5b449385 Mon Sep 17 00:00:00 2001 From: catornow Date: Wed, 1 Sep 2021 15:42:12 +0200 Subject: [PATCH 47/57] Changed the argument of EchoRZXWeylDecomposition from inst_map to backend --- .../optimization/echo_rzx_weyl_decomposition.py | 11 ++++++----- .../test_echo_rzx_weyl_decomposition.py | 15 +++++++-------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/qiskit/transpiler/passes/optimization/echo_rzx_weyl_decomposition.py b/qiskit/transpiler/passes/optimization/echo_rzx_weyl_decomposition.py index 139f61e75967..2b38911451fb 100644 --- a/qiskit/transpiler/passes/optimization/echo_rzx_weyl_decomposition.py +++ b/qiskit/transpiler/passes/optimization/echo_rzx_weyl_decomposition.py @@ -19,11 +19,12 @@ 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.transpiler.layout import Layout +from qiskit.providers import basebackend import qiskit.quantum_info as qi from qiskit.quantum_info.synthesis.two_qubit_decompose import TwoQubitControlledUDecomposer @@ -37,15 +38,15 @@ class EchoRZXWeylDecomposition(TransformationPass): Each pair of RZXGates forms an echoed RZXGate. """ - def __init__(self, inst_map): + def __init__(self, backend: basebackend): """EchoRZXWeylDecomposition pass.""" - self.inst_map = inst_map + 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]) + cx1 = self._inst_map.get("cx", qubit_pair) + cx2 = self._inst_map.get("cx", qubit_pair[::-1]) return cx1.duration < cx2.duration @staticmethod diff --git a/test/python/transpiler/test_echo_rzx_weyl_decomposition.py b/test/python/transpiler/test_echo_rzx_weyl_decomposition.py index f962f95246b5..b8a05f243fea 100644 --- a/test/python/transpiler/test_echo_rzx_weyl_decomposition.py +++ b/test/python/transpiler/test_echo_rzx_weyl_decomposition.py @@ -37,7 +37,6 @@ class TestEchoRZXWeylDecomposition(QiskitTestCase): def setUp(self): super().setUp() self.backend = FakeParis() - self.inst_map = self.backend.defaults().instruction_schedule_map def test_rzx_number_native_weyl_decomposition(self): """Check the number of RZX gates for a hardware-native cx""" @@ -47,7 +46,7 @@ def test_rzx_number_native_weyl_decomposition(self): unitary_circuit = qi.Operator(circuit).data - after = EchoRZXWeylDecomposition(self.inst_map)(circuit) + after = EchoRZXWeylDecomposition(self.backend)(circuit) unitary_after = qi.Operator(after).data @@ -66,7 +65,7 @@ def test_non_native_weyl_decomposition(self): unitary_circuit = qi.Operator(circuit).data dag = circuit_to_dag(circuit) - pass_ = EchoRZXWeylDecomposition(self.inst_map) + pass_ = EchoRZXWeylDecomposition(self.backend) after = dag_to_circuit(pass_.run(dag)) unitary_after = qi.Operator(after).data @@ -117,11 +116,11 @@ def test_h_number_non_native_weyl_decomposition_1(self): circuit_non_native.rzz(theta, qr[1], qr[0]) dag = circuit_to_dag(circuit) - pass_ = EchoRZXWeylDecomposition(self.inst_map) + pass_ = EchoRZXWeylDecomposition(self.backend) after = dag_to_circuit(pass_.run(dag)) dag_non_native = circuit_to_dag(circuit_non_native) - pass_ = EchoRZXWeylDecomposition(self.inst_map) + pass_ = EchoRZXWeylDecomposition(self.backend) after_non_native = dag_to_circuit(pass_.run(dag_non_native)) circuit_rzx_number = self.count_gate_number("rzx", after) @@ -147,11 +146,11 @@ def test_h_number_non_native_weyl_decomposition_2(self): circuit_non_native.swap(qr[1], qr[0]) dag = circuit_to_dag(circuit) - pass_ = EchoRZXWeylDecomposition(self.inst_map) + pass_ = EchoRZXWeylDecomposition(self.backend) after = dag_to_circuit(pass_.run(dag)) dag_non_native = circuit_to_dag(circuit_non_native) - pass_ = EchoRZXWeylDecomposition(self.inst_map) + pass_ = EchoRZXWeylDecomposition(self.backend) after_non_native = dag_to_circuit(pass_.run(dag_non_native)) circuit_rzx_number = self.count_gate_number("rzx", after) @@ -192,7 +191,7 @@ def test_weyl_unitaries_random_circuit(self): unitary_circuit = qi.Operator(circuit).data dag = circuit_to_dag(circuit) - pass_ = EchoRZXWeylDecomposition(self.inst_map) + pass_ = EchoRZXWeylDecomposition(self.backend) after = dag_to_circuit(pass_.run(dag)) unitary_after = qi.Operator(after).data From 4fe00f8f601558b98c9b4ad6fd77e54d5d8dcdbb Mon Sep 17 00:00:00 2001 From: catornow <79633854+catornow@users.noreply.github.com> Date: Mon, 6 Sep 2021 13:51:23 +0200 Subject: [PATCH 48/57] Update qiskit/quantum_info/synthesis/two_qubit_decompose.py Co-authored-by: Lev Bishop <18673315+levbishop@users.noreply.github.com> --- qiskit/quantum_info/synthesis/two_qubit_decompose.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/qiskit/quantum_info/synthesis/two_qubit_decompose.py b/qiskit/quantum_info/synthesis/two_qubit_decompose.py index a7cd18cc1d2c..65cffd53a178 100644 --- a/qiskit/quantum_info/synthesis/two_qubit_decompose.py +++ b/qiskit/quantum_info/synthesis/two_qubit_decompose.py @@ -571,8 +571,8 @@ def __init__(self, rxx_equivalent_gate: Type[Gate]): """ # Do a few spot checks that the KAK decomposition gives (x, 0, 0) for test_angle in [0.2, 0.3, np.pi / 2]: - decomp = TwoQubitWeylDecomposition(Operator(rxx_equivalent_gate(test_angle)).data) - if not np.allclose([decomp.b, decomp.c], [0, 0]): + decomp = TwoQubitWeylDecomposition(rxx_equivalent_gate(test_angle)) + if not isinstance(decomp, TwoQubitWeylControlledEquiv) or abs(decomp.a - self.scale * test_angle) > atol: raise QiskitError( f"{rxx_equivalent_gate.__name__} is not equivalent to an RXXGate." ) From 4630b11d0d24788a0096120f8c9d8422b65432b3 Mon Sep 17 00:00:00 2001 From: catornow <79633854+catornow@users.noreply.github.com> Date: Mon, 6 Sep 2021 13:51:43 +0200 Subject: [PATCH 49/57] Update qiskit/quantum_info/synthesis/two_qubit_decompose.py Co-authored-by: Lev Bishop <18673315+levbishop@users.noreply.github.com> --- qiskit/quantum_info/synthesis/two_qubit_decompose.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/qiskit/quantum_info/synthesis/two_qubit_decompose.py b/qiskit/quantum_info/synthesis/two_qubit_decompose.py index 65cffd53a178..1f6a9a5d2881 100644 --- a/qiskit/quantum_info/synthesis/two_qubit_decompose.py +++ b/qiskit/quantum_info/synthesis/two_qubit_decompose.py @@ -650,11 +650,6 @@ def _to_rxx_gate(self, angle: float): 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]) - # Ensure the circuit is equivalent to an RXXGate. - qc_rxx = QuantumCircuit(2) - qc_rxx.append(RXXGate(angle), qargs=[0, 1]) - if not np.allclose(Operator(rxx_circ).data, Operator(qc_rxx).data): - raise QiskitError(f"Failed to reproduce RXXGate for angle {angle}.") return rxx_circ From c328dd08a988328f45218efd7b5fefa70b2d0c4d Mon Sep 17 00:00:00 2001 From: catornow <79633854+catornow@users.noreply.github.com> Date: Mon, 6 Sep 2021 13:51:55 +0200 Subject: [PATCH 50/57] Update qiskit/quantum_info/synthesis/two_qubit_decompose.py Co-authored-by: Lev Bishop <18673315+levbishop@users.noreply.github.com> --- qiskit/quantum_info/synthesis/two_qubit_decompose.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/qiskit/quantum_info/synthesis/two_qubit_decompose.py b/qiskit/quantum_info/synthesis/two_qubit_decompose.py index 1f6a9a5d2881..0bd044eea0c4 100644 --- a/qiskit/quantum_info/synthesis/two_qubit_decompose.py +++ b/qiskit/quantum_info/synthesis/two_qubit_decompose.py @@ -569,6 +569,12 @@ def __init__(self, rxx_equivalent_gate: Type[Gate]): Raises: QiskitError: If the gate is not locally equivalent to an RXXGate. """ + # Check that gate takes a single angle parameter + try: + rxx_equivalent_gate(test_angle, label='foo') + except TypeError: + raise QiskitError() + # Do a few spot checks that the KAK decomposition gives (x, 0, 0) for test_angle in [0.2, 0.3, np.pi / 2]: decomp = TwoQubitWeylDecomposition(rxx_equivalent_gate(test_angle)) From a778fd3228b8125a6dd90092c955c6d2b3387461 Mon Sep 17 00:00:00 2001 From: catornow <79633854+catornow@users.noreply.github.com> Date: Mon, 6 Sep 2021 13:52:11 +0200 Subject: [PATCH 51/57] Update qiskit/quantum_info/synthesis/two_qubit_decompose.py Co-authored-by: Lev Bishop <18673315+levbishop@users.noreply.github.com> --- qiskit/quantum_info/synthesis/two_qubit_decompose.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiskit/quantum_info/synthesis/two_qubit_decompose.py b/qiskit/quantum_info/synthesis/two_qubit_decompose.py index 0bd044eea0c4..c52ec07b2ff4 100644 --- a/qiskit/quantum_info/synthesis/two_qubit_decompose.py +++ b/qiskit/quantum_info/synthesis/two_qubit_decompose.py @@ -677,7 +677,7 @@ def _weyl_gate(self, circ: QuantumCircuit, atol=1.0e-13): # translate the RZZGate(c) into a circuit based on the desired Ctrl-U gate. if abs(self.decomposer.c) > atol: - # Since the Weyl chember is here defined as a > b > |c| we may have + # 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 From f3a08881d6c611e5b5532a01b1bc70d53744f67c Mon Sep 17 00:00:00 2001 From: catornow Date: Mon, 6 Sep 2021 16:10:30 +0200 Subject: [PATCH 52/57] Fixed bug and slightly modified correct decomposition check --- .../synthesis/two_qubit_decompose.py | 70 +++++++++++++------ 1 file changed, 48 insertions(+), 22 deletions(-) diff --git a/qiskit/quantum_info/synthesis/two_qubit_decompose.py b/qiskit/quantum_info/synthesis/two_qubit_decompose.py index c52ec07b2ff4..bc38d67f2d86 100644 --- a/qiskit/quantum_info/synthesis/two_qubit_decompose.py +++ b/qiskit/quantum_info/synthesis/two_qubit_decompose.py @@ -37,7 +37,7 @@ from qiskit.circuit.quantumregister import QuantumRegister from qiskit.circuit.quantumcircuit import QuantumCircuit, Gate -from qiskit.circuit.library.standard_gates import CXGate, RXGate, RYGate, RZGate, RXXGate +from qiskit.circuit.library.standard_gates import CXGate, RXGate, RYGate, RZGate from qiskit.exceptions import QiskitError from qiskit.quantum_info.operators import Operator from qiskit.quantum_info.synthesis.weyl import weyl_coordinates, transform_to_magic_basis @@ -569,20 +569,47 @@ def __init__(self, rxx_equivalent_gate: Type[Gate]): Raises: QiskitError: If the gate is not locally equivalent to an RXXGate. """ - # Check that gate takes a single angle parameter - try: - rxx_equivalent_gate(test_angle, label='foo') - except TypeError: - raise QiskitError() - - # Do a few spot checks that the KAK decomposition gives (x, 0, 0) - for test_angle in [0.2, 0.3, np.pi / 2]: - decomp = TwoQubitWeylDecomposition(rxx_equivalent_gate(test_angle)) - if not isinstance(decomp, TwoQubitWeylControlledEquiv) or abs(decomp.a - self.scale * test_angle) > atol: + 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: + raise QiskitError("Equivalent gate needs to take exactly 1 angle parameter.") + 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: @@ -632,18 +659,18 @@ def _to_rxx_gate(self, angle: float): # 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.rxx(angle, 0, 1) - decomposer_rxx = TwoQubitWeylControlledEquiv(Operator(circ).data) - - circ = QuantumCircuit(2) - circ.append(self.rxx_equivalent_gate(angle), qargs=[0, 1]) - decomposer_equiv = TwoQubitWeylControlledEquiv(Operator(circ).data) - - scale = decomposer_rxx.a / decomposer_equiv.a + # circ = QuantumCircuit(2) + # circ.rxx(angle, 0, 1) + # decomposer_rxx = TwoQubitWeylControlledEquiv(Operator(circ).data) + # + # circ = QuantumCircuit(2) + # circ.append(self.rxx_equivalent_gate(angle), qargs=[0, 1]) + # decomposer_equiv = TwoQubitWeylControlledEquiv(Operator(circ).data) + # + # scale = decomposer_rxx.a / decomposer_equiv.a circ = QuantumCircuit(2) - circ.append(self.rxx_equivalent_gate(scale * angle), qargs=[0, 1]) + circ.append(self.rxx_equivalent_gate(self.scale * angle), qargs=[0, 1]) decomposer_inv = TwoQubitWeylControlledEquiv(Operator(circ).data) oneq_decompose = OneQubitEulerDecomposer("ZYZ") @@ -656,7 +683,6 @@ def _to_rxx_gate(self, angle: float): 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): From 3506c531b2bb5f8ccf845d00fac056a8e8c030f6 Mon Sep 17 00:00:00 2001 From: catornow Date: Tue, 7 Sep 2021 08:42:56 +0200 Subject: [PATCH 53/57] Removed (raise-missing-from) pylint error --- qiskit/quantum_info/synthesis/two_qubit_decompose.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/qiskit/quantum_info/synthesis/two_qubit_decompose.py b/qiskit/quantum_info/synthesis/two_qubit_decompose.py index bc38d67f2d86..f217d81ba433 100644 --- a/qiskit/quantum_info/synthesis/two_qubit_decompose.py +++ b/qiskit/quantum_info/synthesis/two_qubit_decompose.py @@ -577,8 +577,8 @@ def __init__(self, rxx_equivalent_gate: Type[Gate]): # Check that gate takes a single angle parameter try: rxx_equivalent_gate(test_angle, label="foo") - except TypeError: - raise QiskitError("Equivalent gate needs to take exactly 1 angle parameter.") + 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) From 382ab9ae3a1d8c79c911647fbb1e9b2afb10288d Mon Sep 17 00:00:00 2001 From: catornow Date: Tue, 7 Sep 2021 15:43:52 +0200 Subject: [PATCH 54/57] Adapted tests. --- test/python/quantum_info/test_synthesis.py | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/test/python/quantum_info/test_synthesis.py b/test/python/quantum_info/test_synthesis.py index 5fb36c446a1e..9218e69ecab6 100644 --- a/test/python/quantum_info/test_synthesis.py +++ b/test/python/quantum_info/test_synthesis.py @@ -1322,21 +1322,12 @@ def test_correct_unitary(self, seed): circ = decomposer(unitary) self.assertEqual(Operator(unitary), Operator(circ)) - def test_rxx_not_reproducible(self): - """Test that an exception is raised if RXXGate cannot be reproduced""" - unitary = random_unitary(4) - for gate in [CXGate, CZGate]: - decomposer = TwoQubitControlledUDecomposer(gate) - with self.assertRaises(QiskitError) as exc: - decomposer(unitary) - self.assertIn("Failed to reproduce RXXGate for angle", exc.exception.message) - 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("is not equivalent to an RXXGate.", exc.exception.message) + self.assertIn("Equivalent gate needs to take exactly 1 angle parameter.", exc.exception.message) class TestDecomposeProductRaises(QiskitTestCase): From 9cb9b6cc20eca47093e7e77f44982485bc26de1e Mon Sep 17 00:00:00 2001 From: catornow Date: Tue, 7 Sep 2021 16:25:35 +0200 Subject: [PATCH 55/57] Black --- qiskit/quantum_info/synthesis/two_qubit_decompose.py | 9 --------- test/python/quantum_info/test_synthesis.py | 4 +++- 2 files changed, 3 insertions(+), 10 deletions(-) diff --git a/qiskit/quantum_info/synthesis/two_qubit_decompose.py b/qiskit/quantum_info/synthesis/two_qubit_decompose.py index f217d81ba433..2d209ecf97ed 100644 --- a/qiskit/quantum_info/synthesis/two_qubit_decompose.py +++ b/qiskit/quantum_info/synthesis/two_qubit_decompose.py @@ -659,15 +659,6 @@ def _to_rxx_gate(self, angle: float): # 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.rxx(angle, 0, 1) - # decomposer_rxx = TwoQubitWeylControlledEquiv(Operator(circ).data) - # - # circ = QuantumCircuit(2) - # circ.append(self.rxx_equivalent_gate(angle), qargs=[0, 1]) - # decomposer_equiv = TwoQubitWeylControlledEquiv(Operator(circ).data) - # - # scale = decomposer_rxx.a / decomposer_equiv.a circ = QuantumCircuit(2) circ.append(self.rxx_equivalent_gate(self.scale * angle), qargs=[0, 1]) diff --git a/test/python/quantum_info/test_synthesis.py b/test/python/quantum_info/test_synthesis.py index 9218e69ecab6..c9d85a9a4290 100644 --- a/test/python/quantum_info/test_synthesis.py +++ b/test/python/quantum_info/test_synthesis.py @@ -1327,7 +1327,9 @@ def test_not_rxx_equivalent(self): gate = SwapGate with self.assertRaises(QiskitError) as exc: TwoQubitControlledUDecomposer(gate) - self.assertIn("Equivalent gate needs to take exactly 1 angle parameter.", exc.exception.message) + self.assertIn( + "Equivalent gate needs to take exactly 1 angle parameter.", exc.exception.message + ) class TestDecomposeProductRaises(QiskitTestCase): From de3622bc188a19c7df6b90a7210149b2e9a69597 Mon Sep 17 00:00:00 2001 From: catornow Date: Wed, 29 Sep 2021 19:13:55 +0200 Subject: [PATCH 56/57] Modified docstrings --- .../passes/optimization/echo_rzx_weyl_decomposition.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/qiskit/transpiler/passes/optimization/echo_rzx_weyl_decomposition.py b/qiskit/transpiler/passes/optimization/echo_rzx_weyl_decomposition.py index 2b38911451fb..76e0ecc13ac9 100644 --- a/qiskit/transpiler/passes/optimization/echo_rzx_weyl_decomposition.py +++ b/qiskit/transpiler/passes/optimization/echo_rzx_weyl_decomposition.py @@ -83,13 +83,13 @@ def run(self, dag: DAGCircuit): input dag. Args: - dag (DAGCircuit): DAG to map. + dag (DAGCircuit): DAG to rewrite. Returns: - DAGCircuit: The rearranged dag. + DAGCircuit: The modified dag. Raises: - TranspilerError: If the circuit cannot be mapped. + TranspilerError: If the circuit cannot be rewritten. """ if len(dag.qregs) > 1: From bd41b3fc723eb3e87a63d2c4edf34bf8be87ed75 Mon Sep 17 00:00:00 2001 From: catornow Date: Thu, 30 Sep 2021 12:16:59 +0200 Subject: [PATCH 57/57] Modified and added tests to also check the RZX gate angles --- .../test_echo_rzx_weyl_decomposition.py | 115 +++++++++++------- 1 file changed, 72 insertions(+), 43 deletions(-) diff --git a/test/python/transpiler/test_echo_rzx_weyl_decomposition.py b/test/python/transpiler/test_echo_rzx_weyl_decomposition.py index b8a05f243fea..b9a38871bbad 100644 --- a/test/python/transpiler/test_echo_rzx_weyl_decomposition.py +++ b/test/python/transpiler/test_echo_rzx_weyl_decomposition.py @@ -11,6 +11,7 @@ # that they have been altered from the originals. """Test the EchoRZXWeylDecomposition pass""" + import unittest from math import pi import numpy as np @@ -38,50 +39,12 @@ def setUp(self): super().setUp() self.backend = FakeParis() - 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 after circuit has correct number of rzx gates - self.assertRZXgates(unitary_circuit, after) - - def test_non_native_weyl_decomposition(self): - """Check the number of RZX gates for a non-hardware-native rzz""" - theta = pi / 9 - qr = QuantumRegister(2, "qr") - circuit = QuantumCircuit(qr) - circuit.rzz(theta, qr[1], qr[0]) - - 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)) - - # check whether after circuit has correct number of rzx gates - self.assertRZXgates(unitary_circuit, after) - 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 - # check whether after circuit has correct number of rzx gates expected_rzx_number = 0 if not alpha == 0: expected_rzx_number += 2 @@ -103,8 +66,25 @@ def count_gate_number(gate, circuit): 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 an rzz gate""" + """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 @@ -137,11 +117,11 @@ def test_h_number_non_native_weyl_decomposition_1(self): 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 + # swap gate in native direction. circuit = QuantumCircuit(qr) circuit.swap(qr[0], qr[1]) - # swap gate in non-native direction + # swap gate in non-native direction. circuit_non_native = QuantumCircuit(qr) circuit_non_native.swap(qr[1], qr[0]) @@ -164,8 +144,57 @@ def test_h_number_non_native_weyl_decomposition_2(self): (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 random two-qubit circuit.""" + """Weyl decomposition for a random two-qubit circuit.""" theta = pi / 9 epsilon = 5 delta = -1 @@ -173,7 +202,7 @@ def test_weyl_unitaries_random_circuit(self): qr = QuantumRegister(2, "qr") circuit = QuantumCircuit(qr) - # random two-qubit circuit + # random two-qubit circuit. circuit.rzx(theta, 0, 1) circuit.rzz(epsilon, 0, 1) circuit.rz(eta, 0)