diff --git a/qiskit/compiler/transpiler.py b/qiskit/compiler/transpiler.py index b5f1b444158c..b34a54ea8a0f 100644 --- a/qiskit/compiler/transpiler.py +++ b/qiskit/compiler/transpiler.py @@ -35,6 +35,7 @@ from qiskit.transpiler.passmanager_config import PassManagerConfig from qiskit.transpiler.preset_passmanagers import ( level_0_pass_manager, + pulse_efficient_pass_manager, level_1_pass_manager, level_2_pass_manager, level_3_pass_manager, @@ -58,7 +59,8 @@ def transpile( dt: Optional[float] = None, approximation_degree: Optional[float] = None, seed_transpiler: Optional[int] = None, - optimization_level: Optional[int] = None, + inst_map: Dict[str, Dict[Tuple[int], Schedule]] = None, + optimization_level: Union[Optional[int], Optional[str]] = None, pass_manager: Optional[PassManager] = None, callback: Optional[Callable[[BasePass, DAGCircuit, float, PropertySet, int], Any]] = None, output_name: Optional[Union[str, List[str]]] = None, @@ -186,6 +188,7 @@ def callback_func(**kwargs): output_name: A list with strings to identify the output circuits. The length of the list should be exactly the length of the ``circuits`` parameter. + inst_map: Instruction schedule map. Returns: The transpiled circuit(s). @@ -221,6 +224,7 @@ def callback_func(**kwargs): translation_method=translation_method, approximation_degree=approximation_degree, backend=backend, + inst_map=inst_map, ) warnings.warn( @@ -260,6 +264,7 @@ def callback_func(**kwargs): dt, approximation_degree, seed_transpiler, + inst_map, optimization_level, callback, output_name, @@ -352,8 +357,12 @@ def _transpile_circuit(circuit_config_tuple: Tuple[QuantumCircuit, Dict]) -> Qua pass_manager = level_2_pass_manager(pass_manager_config) elif level == 3: pass_manager = level_3_pass_manager(pass_manager_config) + elif level == "pulse_efficient": + pass_manager = pulse_efficient_pass_manager(pass_manager_config) else: - raise TranspilerError("optimization_level can range from 0 to 3.") + raise TranspilerError( + "optimization_level can range from 0 to 3, or can be 'pulse_efficient'." + ) result = pass_manager.run( circuit, callback=transpile_config["callback"], output_name=transpile_config["output_name"] @@ -440,6 +449,7 @@ def _parse_transpile_args( dt, approximation_degree, seed_transpiler, + inst_map, optimization_level, callback, output_name, @@ -475,6 +485,7 @@ def _parse_transpile_args( translation_method = _parse_translation_method(translation_method, num_circuits) approximation_degree = _parse_approximation_degree(approximation_degree, num_circuits) seed_transpiler = _parse_seed_transpiler(seed_transpiler, num_circuits) + inst_map = _parse_inst_map(inst_map, backend, num_circuits) optimization_level = _parse_optimization_level(optimization_level, num_circuits) output_name = _parse_output_name(output_name, circuits) callback = _parse_callback(callback, num_circuits) @@ -499,6 +510,7 @@ def _parse_transpile_args( durations, approximation_degree, seed_transpiler, + inst_map, optimization_level, output_name, callback, @@ -518,12 +530,13 @@ def _parse_transpile_args( instruction_durations=args[8], approximation_degree=args[9], seed_transpiler=args[10], + inst_map=args[11], ), - "optimization_level": args[11], - "output_name": args[12], - "callback": args[13], - "backend_num_qubits": args[14], - "faulty_qubits_map": args[15], + "optimization_level": args[12], + "output_name": args[13], + "callback": args[14], + "backend_num_qubits": args[15], + "faulty_qubits_map": args[16], } list_transpile_args.append(transpile_args) @@ -770,6 +783,16 @@ def _parse_seed_transpiler(seed_transpiler, num_circuits): return seed_transpiler +def _parse_inst_map(inst_map, backend, num_circuits): + if inst_map is None: + if backend.defaults(): + backend_defaults = backend.defaults() + inst_map = backend_defaults.instruction_schedule_map + if not isinstance(inst_map, list): + inst_map = [inst_map] * num_circuits + return inst_map + + def _parse_optimization_level(optimization_level, num_circuits): if not isinstance(optimization_level, list): optimization_level = [optimization_level] * num_circuits diff --git a/qiskit/quantum_info/synthesis/two_qubit_decompose.py b/qiskit/quantum_info/synthesis/two_qubit_decompose.py index 7ebe0b3c8de7..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 @@ -129,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, @@ -544,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 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 diff --git a/qiskit/transpiler/passes/scheduling/calibration_creators.py b/qiskit/transpiler/passes/scheduling/calibration_creators.py index 5b44e9cffbf5..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: """ @@ -328,6 +339,7 @@ def get_calibration(self, params: List, qubits: List) -> Schedule: 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/qiskit/transpiler/passmanager_config.py b/qiskit/transpiler/passmanager_config.py index e91ed5ed623d..311cce90c145 100644 --- a/qiskit/transpiler/passmanager_config.py +++ b/qiskit/transpiler/passmanager_config.py @@ -29,6 +29,7 @@ def __init__( backend_properties=None, approximation_degree=None, seed_transpiler=None, + inst_map=None, ): """Initialize a PassManagerConfig object @@ -54,6 +55,7 @@ def __init__( (1.0=no approximation, 0.0=maximal approximation) seed_transpiler (int): Sets random seed for the stochastic parts of the transpiler. + inst_map (Dict[str, Dict[Tuple[int], Schedule]]): Schedule instructions """ self.initial_layout = initial_layout self.basis_gates = basis_gates @@ -66,3 +68,4 @@ def __init__( self.backend_properties = backend_properties self.approximation_degree = approximation_degree self.seed_transpiler = seed_transpiler + self.inst_map = inst_map diff --git a/qiskit/transpiler/preset_passmanagers/__init__.py b/qiskit/transpiler/preset_passmanagers/__init__.py index f005cb0c54a2..2f7730317bbd 100644 --- a/qiskit/transpiler/preset_passmanagers/__init__.py +++ b/qiskit/transpiler/preset_passmanagers/__init__.py @@ -30,3 +30,4 @@ from .level1 import level_1_pass_manager from .level2 import level_2_pass_manager from .level3 import level_3_pass_manager +from .pulse_efficient import pulse_efficient_pass_manager diff --git a/qiskit/transpiler/preset_passmanagers/pulse_efficient.py b/qiskit/transpiler/preset_passmanagers/pulse_efficient.py new file mode 100644 index 000000000000..ebc25e02773b --- /dev/null +++ b/qiskit/transpiler/preset_passmanagers/pulse_efficient.py @@ -0,0 +1,92 @@ +# 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. + +"""Pass manager for pulse-efficient, cross-resonance gate based circuits. + +Pulse-efficient pass manager: Computes Weyl parameters of two-qubit operations in a circuit +and translates the circuit into a pulse-efficient, cross-resonance gate based circuit by creating +calibrations for the cross-resonance gates. Reference: https://arxiv.org/pdf/2105.01063.pdf +""" + +from qiskit.transpiler.passmanager_config import PassManagerConfig +from qiskit.transpiler.passmanager import PassManager + +from qiskit.transpiler.passes import Collect2qBlocks +from qiskit.transpiler.passes import ConsolidateBlocks + +from qiskit.transpiler.passes.scheduling.calibration_creators import RZXCalibrationBuilderNoEcho +from qiskit.transpiler.passes.optimization.echo_rzx_weyl_decomposition import ( + EchoRZXWeylDecomposition, +) +from qiskit.circuit.library.standard_gates.equivalence_library import ( + StandardEquivalenceLibrary as std_eqlib, +) +from qiskit.transpiler.passes.basis import BasisTranslator, UnrollCustomDefinitions +from qiskit.transpiler.passes import Optimize1qGatesDecomposition + +from qiskit.transpiler.preset_passmanagers.level3 import level_3_pass_manager + +from qiskit.transpiler.preset_passmanagers.level0 import level_0_pass_manager + + +def pulse_efficient_pass_manager(pass_manager_config: PassManagerConfig) -> PassManager: + """Pulse-efficient pass manager. + + This pass manager consolidates all consecutive two-qubit operations in a quantum circuit and + computes the Weyl decomposition of the corresponding unitaries. It rewrites the unitary operations + in terms of cross-resonance gates and creates calibrations for these gates. Lastly, the circuit + is rewritten in the hardware-native basis with 'rzx'. + + Note: + This pass manager does currently not support the transpilation of two-qubit operations + between qubits that are not connected on the hardware. + + Args: + pass_manager_config: configuration of the pass manager. + + Returns: + a pulse-efficient pass manager. + """ + inst_map = pass_manager_config.inst_map + basis_gates = pass_manager_config.basis_gates + + # 1. Consolidate all consecutive two-qubit operations + _collect_2q_blocks = Collect2qBlocks() + _consolidate_blocks = ConsolidateBlocks(basis_gates=["rz", "sx", "x", "rxx"]) + + # 2. Decompose two-qubit unitaries in terms of echoed RZX gates according to the Weyl decomposition + _echo_rzx_weyl_decomposition = EchoRZXWeylDecomposition(inst_map=inst_map) + + # 3. Add calibrations + _rzx_calibrations = RZXCalibrationBuilderNoEcho(inst_map=inst_map) + + # 4. Unroll to backend basis with rzx + basis_gates = list(set(basis_gates) | {"rzx"}) + + _unroll = [ + UnrollCustomDefinitions(std_eqlib, basis_gates), + BasisTranslator(std_eqlib, basis_gates), + ] + + # 5. Optimize one-qubit decomposition + _optimize_1q_decomposition = Optimize1qGatesDecomposition(basis_gates) + + # Build pass manager + # pm = PassManager() + pm = level_3_pass_manager(pass_manager_config) + pm.append(_collect_2q_blocks) + pm.append(_consolidate_blocks) + pm.append(_echo_rzx_weyl_decomposition) + pm.append(_rzx_calibrations) + pm.append(_unroll) + pm.append(_optimize_1q_decomposition) + return pm 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() diff --git a/test/python/transpiler/test_pulse_efficient_passmanager.py b/test/python/transpiler/test_pulse_efficient_passmanager.py new file mode 100644 index 000000000000..5834cf164d44 --- /dev/null +++ b/test/python/transpiler/test_pulse_efficient_passmanager.py @@ -0,0 +1,247 @@ +# 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. + +"""Tests pulse-efficient pass manager API""" + +import unittest + +from qiskit.converters import circuit_to_dag + +from qiskit import QuantumCircuit, QuantumRegister +from qiskit.quantum_info.synthesis.two_qubit_decompose import TwoQubitWeylDecomposition +from qiskit.compiler import transpile +from qiskit.test import QiskitTestCase +from qiskit.test.mock import ( + FakeParis, + FakeAthens, +) + +from math import pi +import numpy as np + +import qiskit.quantum_info as qi + + +class TestPulseEfficientPassManager(QiskitTestCase): + """Test the pulse-efficient pass manager""" + + def setUp(self): + super().setUp() + + def test_empty_circuit(self): + """Test empty circuit""" + + circuit = QuantumCircuit() + + unitary_circuit = qi.Operator(circuit).data + + result = transpile(circuit, FakeAthens(), optimization_level="pulse_efficient", seed_transpiler=100) + + transpile_level_3 = transpile(circuit, FakeAthens(), optimization_level=3, seed_transpiler=100) + + unitary_result = qi.Operator(result).data + + unitary_3 = qi.Operator(transpile_level_3).data + + self.assertTrue(np.allclose(unitary_3, unitary_result)) + + def test_2q_circuit_transpilation(self): + """Test random two-qubit circuit""" + + theta = 0.2 + epsilon = pi / 3 + qr = QuantumRegister(5, "qr") + circuit = QuantumCircuit(qr) + circuit.cx(qr[1], qr[0]) + circuit.rzx(theta, qr[1], qr[0]) + circuit.sx(qr[1]) + circuit.rzz(epsilon, qr[1], qr[0]) + circuit.swap(qr[1], qr[0]) + circuit.h(qr[1]) + + result = transpile(circuit, FakeAthens(), optimization_level="pulse_efficient", seed_transpiler=100) + + transpile_level_3 = transpile(circuit, FakeAthens(), optimization_level=3, seed_transpiler=100) + + unitary_result = qi.Operator(result).data + + unitary_3 = qi.Operator(transpile_level_3).data + + self.assertTrue(np.allclose(unitary_3, unitary_result)) + + def test_correct_basis(self): + """Test random two-qubit circuit""" + + theta = 0.2 + epsilon = pi / 3 + qr = QuantumRegister(2, "qr") + circuit = QuantumCircuit(qr) + circuit.cx(qr[1], qr[0]) + circuit.rzx(theta, qr[1], qr[0]) + circuit.sx(qr[1]) + circuit.rzz(epsilon, qr[1], qr[0]) + circuit.swap(qr[1], qr[0]) + circuit.h(qr[1]) + + result = transpile(circuit, FakeAthens(), optimization_level="pulse_efficient", seed_transpiler=100) + + self.assertEqual(set(result.count_ops().keys()), {'rz', 'rzx', 'sx', 'x'}) + + # def test_2q_circuit_rzx_number(self): + # """Test correct number of rzx gates""" + # + # theta = -2 + # epsilon = pi / 9 + # qr = QuantumRegister(2, "qr") + # circuit = QuantumCircuit(qr) + # circuit.cx(qr[0], qr[1]) + # circuit.rxx(theta, qr[1], qr[0]) + # circuit.x(qr[1]) + # circuit.cx(qr[1], qr[0]) + # circuit.rzz(epsilon, qr[1], qr[0]) + # circuit.cx(qr[1], qr[0]) + # circuit.h(qr[1]) + # + # unitary_circuit = qi.Operator(circuit).data + # + # result = transpile(circuit, FakeAthens(), optimization_level="pulse_efficient", seed_transpiler=10) + # + # 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(result)["rzx"] + # + # self.assertEqual(expected_rzx_number, circuit_rzx_number) + + # def test_alpha_beta_gamma(self): + # """Check if the Weyl parameters match the expected ones for rzz""" + # + # theta = 1 + # qr = QuantumRegister(2, "qr") + # circuit = QuantumCircuit(qr) + # circuit.rzz(theta, qr[1], qr[0]) + # + # result = transpile(circuit, FakeAthens(), optimization_level="pulse_efficient", seed_transpiler=10) + # + # unitary_result = qi.Operator(result).data + # + # alpha = TwoQubitWeylDecomposition(unitary_result).a + # beta = TwoQubitWeylDecomposition(unitary_result).b + # gamma = TwoQubitWeylDecomposition(unitary_result).c + # + # self.assertEqual((alpha, beta, gamma), (0.5, 0, 0)) + # + def test_5q_circuit_weyl_decomposition(self): + """Test random five-qubit circuit""" + + delta = 6 * pi / 5 + epsilon = pi / 3 + zeta = -2.1 + theta = 0.2 + qr = QuantumRegister(5, "qr") + circuit = QuantumCircuit(qr) + circuit.cx(qr[1], qr[0]) + circuit.rzx(theta, qr[1], qr[0]) + circuit.rzz(epsilon, qr[1], qr[0]) + circuit.swap(qr[1], qr[0]) + circuit.rzz(delta, qr[0], qr[1]) + circuit.swap(qr[1], qr[0]) + circuit.cx(qr[1], qr[0]) + circuit.swap(qr[1], qr[0]) + circuit.h(qr[1]) + circuit.rxx(zeta, qr[1], qr[0]) + circuit.rzz(theta, qr[0], qr[1]) + circuit.swap(qr[3], qr[2]) + circuit.cx(qr[1], qr[2]) + circuit.swap(qr[1], qr[4]) + circuit.h(qr[3]) + + result = transpile(circuit, FakeAthens(), optimization_level="pulse_efficient", seed_transpiler=10) + + transpile_level_3 = transpile(circuit, FakeAthens(), optimization_level=3, seed_transpiler=100) + + unitary_result = qi.Operator(result).data + + unitary_3 = qi.Operator(transpile_level_3).data + + self.assertTrue(np.allclose(unitary_3, unitary_result)) + + def test_rzx_calibrations(self): + """Test whether there exist calibrations for rzx""" + + delta = 0.001 + theta = 3.8 + qr = QuantumRegister(5, "qr") + circuit = QuantumCircuit(qr) + circuit.rzz(delta, qr[0], qr[1]) + circuit.swap(qr[1], qr[0]) + circuit.cx(qr[1], qr[0]) + circuit.swap(qr[1], qr[0]) + circuit.s(qr[1]) + circuit.rzz(theta, qr[0], qr[1]) + circuit.swap(qr[3], qr[2]) + circuit.cx(qr[1], qr[2]) + circuit.sx(qr[4]) + circuit.swap(qr[1], qr[4]) + circuit.h(qr[3]) + + result = transpile(circuit, FakeAthens(), optimization_level="pulse_efficient", seed_transpiler=10) + + self.assertIn("rzx", result.calibrations) + + def test_params_values(self): + """Test whether absolute value of rzx angles is smaller than or equals pi""" + + delta = 8 * pi / 5 + epsilon = pi / 2 + zeta = -5.1 + theta = 0.02 + qr = QuantumRegister(5, "qr") + circuit = QuantumCircuit(qr) + circuit.cx(qr[1], qr[0]) + circuit.rzx(theta, qr[1], qr[0]) + circuit.rzz(epsilon, qr[1], qr[0]) + circuit.swap(qr[1], qr[0]) + circuit.rzz(delta, qr[0], qr[1]) + circuit.swap(qr[1], qr[0]) + circuit.cx(qr[1], qr[0]) + circuit.swap(qr[1], qr[0]) + circuit.h(qr[1]) + circuit.rxx(zeta, qr[1], qr[0]) + circuit.rzz(theta, qr[0], qr[1]) + circuit.swap(qr[3], qr[2]) + circuit.cx(qr[1], qr[2]) + circuit.swap(qr[1], qr[4]) + circuit.h(qr[3]) + + result = transpile(circuit, FakeAthens(), optimization_level="pulse_efficient", seed_transpiler=10) + + after_dag = circuit_to_dag(result) + + for node in after_dag.nodes(): + if node.type == "op" and node.op.name == "rzx": + params = node.op.params + self.assertTrue(abs(params[0]) <= np.pi) + + +if __name__ == "__main__": + unittest.main()