From 450922b3a4c7cb65bd8723f861d5dd3c94e2e7e4 Mon Sep 17 00:00:00 2001 From: Thomas Alexander Date: Wed, 6 Jul 2022 00:26:55 -0300 Subject: [PATCH 01/70] Fix lint for Qiskit main branch. --- qiskit_ibm_provider/jupyter/__init__.py | 2 +- qiskit_ibm_provider/jupyter/backend_info.py | 2 +- qiskit_ibm_provider/jupyter/config_widget.py | 2 +- qiskit_ibm_provider/jupyter/gates_widget.py | 2 +- qiskit_ibm_provider/jupyter/jobs_widget.py | 2 +- qiskit_ibm_provider/jupyter/live_data_widget.py | 3 ++- qiskit_ibm_provider/jupyter/qubits_widget.py | 2 +- qiskit_ibm_provider/jupyter/utils.py | 2 +- requirements-dev.txt | 2 +- requirements.txt | 2 +- test/fake_account_client.py | 4 +++- test/integration/test_ibm_qasm_simulator.py | 4 +++- test/unit/mock/fake_account_client.py | 2 +- test/unit/test_ibm_job_states.py | 3 ++- 14 files changed, 20 insertions(+), 14 deletions(-) diff --git a/qiskit_ibm_provider/jupyter/__init__.py b/qiskit_ibm_provider/jupyter/__init__.py index e775a54ac..c8991a9b4 100644 --- a/qiskit_ibm_provider/jupyter/__init__.py +++ b/qiskit_ibm_provider/jupyter/__init__.py @@ -72,7 +72,7 @@ from IPython import get_ipython # pylint: disable=import-error from .dashboard.dashboard import IBMDashboardMagic - from qiskit.test.mock import FakeBackendV2 as FakeBackend + from qiskit.providers.fake_provider.fake_backend import FakeBackendV2 as FakeBackend from ..ibm_backend import IBMBackend from .backend_info import backend_widget diff --git a/qiskit_ibm_provider/jupyter/backend_info.py b/qiskit_ibm_provider/jupyter/backend_info.py index 605163b1d..9eeeaa49a 100644 --- a/qiskit_ibm_provider/jupyter/backend_info.py +++ b/qiskit_ibm_provider/jupyter/backend_info.py @@ -18,7 +18,7 @@ import ipyvuetify as vue from IPython.display import display # pylint: disable=import-error -from qiskit.test.mock import FakeBackendV2 as FakeBackend +from qiskit.providers.fake_provider.fake_backend import FakeBackendV2 as FakeBackend from qiskit_ibm_provider.ibm_backend import IBMBackend from .config_widget import config_tab diff --git a/qiskit_ibm_provider/jupyter/config_widget.py b/qiskit_ibm_provider/jupyter/config_widget.py index a30b1d55d..92b9e22d8 100644 --- a/qiskit_ibm_provider/jupyter/config_widget.py +++ b/qiskit_ibm_provider/jupyter/config_widget.py @@ -16,7 +16,7 @@ from typing import Union import ipywidgets as wid -from qiskit.test.mock import FakeBackendV2 as FakeBackend +from qiskit.providers.fake_provider.fake_backend import FakeBackendV2 as FakeBackend from qiskit_ibm_provider.ibm_backend import IBMBackend from qiskit_ibm_provider.utils.converters import duration_difference diff --git a/qiskit_ibm_provider/jupyter/gates_widget.py b/qiskit_ibm_provider/jupyter/gates_widget.py index 81c007e8e..21fa28bdc 100644 --- a/qiskit_ibm_provider/jupyter/gates_widget.py +++ b/qiskit_ibm_provider/jupyter/gates_widget.py @@ -17,7 +17,7 @@ from typing import Union import ipywidgets as wid -from qiskit.test.mock import FakeBackendV2 as FakeBackend +from qiskit.providers.fake_provider.fake_backend import FakeBackendV2 as FakeBackend from qiskit_ibm_provider.ibm_backend import IBMBackend diff --git a/qiskit_ibm_provider/jupyter/jobs_widget.py b/qiskit_ibm_provider/jupyter/jobs_widget.py index 470c6f0ca..ab6b5e795 100644 --- a/qiskit_ibm_provider/jupyter/jobs_widget.py +++ b/qiskit_ibm_provider/jupyter/jobs_widget.py @@ -18,7 +18,7 @@ import ipywidgets as wid import plotly.graph_objects as go -from qiskit.test.mock import FakeBackendV2 as FakeBackend +from qiskit.providers.fake_provider.fake_backend import FakeBackendV2 as FakeBackend from ..ibm_backend import IBMBackend from ..visualization.interactive.plotly_wrapper import PlotlyWidget diff --git a/qiskit_ibm_provider/jupyter/live_data_widget.py b/qiskit_ibm_provider/jupyter/live_data_widget.py index 5c6e435db..a3cb06d22 100644 --- a/qiskit_ibm_provider/jupyter/live_data_widget.py +++ b/qiskit_ibm_provider/jupyter/live_data_widget.py @@ -23,11 +23,12 @@ import logging import pytz from websocket import WebSocketApp + import ipywidgets as widgets import numpy as np from sklearn.decomposition import PCA from qiskit.providers.jobstatus import JobStatus -from qiskit.test.mock import FakeBackendV2 as FakeBackend +from qiskit.providers.fake_provider.fake_backend import FakeBackendV2 as FakeBackend # PLOTS ENABLE_LEVEL_0 = False diff --git a/qiskit_ibm_provider/jupyter/qubits_widget.py b/qiskit_ibm_provider/jupyter/qubits_widget.py index 9608073a4..8e5845dcf 100644 --- a/qiskit_ibm_provider/jupyter/qubits_widget.py +++ b/qiskit_ibm_provider/jupyter/qubits_widget.py @@ -16,7 +16,7 @@ from typing import Union import ipywidgets as wid -from qiskit.test.mock import FakeBackendV2 as FakeBackend +from qiskit.providers.fake_provider.fake_backend import FakeBackendV2 as FakeBackend from qiskit_ibm_provider.ibm_backend import IBMBackend diff --git a/qiskit_ibm_provider/jupyter/utils.py b/qiskit_ibm_provider/jupyter/utils.py index 4fda67c2c..3b2dfcae7 100644 --- a/qiskit_ibm_provider/jupyter/utils.py +++ b/qiskit_ibm_provider/jupyter/utils.py @@ -15,7 +15,7 @@ from datetime import datetime, timedelta from typing import Optional, Union -from qiskit.test.mock import FakeBackendV2 as FakeBackend +from qiskit.providers.fake_provider.fake_backend import FakeBackendV2 as FakeBackend from qiskit_ibm_provider.backendreservation import BackendReservation from qiskit_ibm_provider.ibm_backend import IBMBackend diff --git a/requirements-dev.txt b/requirements-dev.txt index 8268c9288..83a255774 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -20,7 +20,7 @@ reno>=2.11.0 scipy>=1.0 nbformat>=4.4.0 nbconvert>=5.3.1 -qiskit-aer +qiskit-aer>=0.10.3 websockets>=8 black==22.3.0 coverage>=6.3 diff --git a/requirements.txt b/requirements.txt index bfd37098b..c02804cf8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,4 +5,4 @@ numpy>=1.13 urllib3>=1.21.1 python-dateutil>=2.8.0 websocket-client>=1.0.1 -websockets>=10.0 \ No newline at end of file +websockets>=10.0 diff --git a/test/fake_account_client.py b/test/fake_account_client.py index 043efc31b..f5eca6013 100644 --- a/test/fake_account_client.py +++ b/test/fake_account_client.py @@ -23,7 +23,9 @@ from random import randrange from typing import Dict, Any -from qiskit.test.mock.backends.poughkeepsie.fake_poughkeepsie import FakePoughkeepsie +from qiskit.providers.fake_provider.backends.poughkeepsie.fake_poughkeepsie import ( + FakePoughkeepsie, +) from qiskit_ibm_provider.api.exceptions import ( RequestsApiError, diff --git a/test/integration/test_ibm_qasm_simulator.py b/test/integration/test_ibm_qasm_simulator.py index 6d023c1f2..469a9cc07 100644 --- a/test/integration/test_ibm_qasm_simulator.py +++ b/test/integration/test_ibm_qasm_simulator.py @@ -17,7 +17,9 @@ from qiskit import ClassicalRegister, QuantumCircuit, QuantumRegister from qiskit.compiler import transpile -from qiskit.providers.aer.noise import NoiseModel +from qiskit.providers.aer.noise import ( + NoiseModel, +) # pylint: disable=import-error,no-name-in-module from qiskit.test.reference_circuits import ReferenceCircuits from qiskit_ibm_provider import IBMBackend diff --git a/test/unit/mock/fake_account_client.py b/test/unit/mock/fake_account_client.py index 872831e7b..a21a726d3 100644 --- a/test/unit/mock/fake_account_client.py +++ b/test/unit/mock/fake_account_client.py @@ -15,7 +15,7 @@ from datetime import datetime as python_datetime from typing import List, Dict, Any, Optional -from qiskit.test.mock.backends import FakeLima +from qiskit.providers.fake_provider.backends.lima.fake_lima import FakeLima class FakeApiBackend: diff --git a/test/unit/test_ibm_job_states.py b/test/unit/test_ibm_job_states.py index 831a8ef2e..4631177f8 100644 --- a/test/unit/test_ibm_job_states.py +++ b/test/unit/test_ibm_job_states.py @@ -25,7 +25,8 @@ from qiskit import transpile from qiskit.providers import JobTimeoutError from qiskit.providers.jobstatus import JobStatus -from qiskit.test.mock.backends.bogota.fake_bogota import FakeBogota +from qiskit.providers.fake_provider.backends.bogota.fake_bogota import FakeBogota + from qiskit.test.reference_circuits import ReferenceCircuits from qiskit_ibm_provider.api.exceptions import ( From 5c6144829e999c3da8ad1bbf68e8e7e8f1a7e75e Mon Sep 17 00:00:00 2001 From: Thomas Alexander Date: Wed, 6 Jul 2022 00:36:20 -0300 Subject: [PATCH 02/70] Fix sphinx language configuration. --- docs/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/conf.py b/docs/conf.py index bfd6b61ff..d88a3f92d 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -106,7 +106,7 @@ # # This is also used if you do content translation via gettext catalogs. # Usually you set "language" from the command line for these cases. -language = None +language = 'en' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. From 0658c7a233e00c3a99c4c0880b169dce5a316d69 Mon Sep 17 00:00:00 2001 From: Thomas Alexander Date: Fri, 17 Jun 2022 10:37:31 -0300 Subject: [PATCH 03/70] Add initial scaffolding for dynamic_circuits module. --- .../dynamic_circuits/__init__.py | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 qiskit_ibm_provider/dynamic_circuits/__init__.py diff --git a/qiskit_ibm_provider/dynamic_circuits/__init__.py b/qiskit_ibm_provider/dynamic_circuits/__init__.py new file mode 100644 index 000000000..09c169da8 --- /dev/null +++ b/qiskit_ibm_provider/dynamic_circuits/__init__.py @@ -0,0 +1,51 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2022. +# +# 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. + +""" +============================================================== +Dynamic Circuits (:mod:`qiskit_ibm_provider.dynamic_circuits`) +============================================================== + +.. currentmodule:: qiskit_ibm_provider.dynamic_circuits + +A collection of tools for working with IBM Quantum's next-generation +backends that support advanced "dynamic circuit" capabilities. Ie., +circuits with support for classical control-flow/feedback based off +of measurement results. + +Example Usage on a Supporting Backend +===================================== + +.. jupyter-execute:: + :hide-code: + :hide-output: + + from qiskit_ibm_provider.test.ibm_provider_mock import mock_get_backend + mock_get_backend('FakePerth') + +.. jupyter-execute:: + + from qiskit_ibm_provider import IBMProvider + import qiskit_ibm_provider.jupyter + + provider = IBMProvider(hub='ibm-q') + backend = provider.get_backend('ibm_perth') + + + +Scheduling & Dynamical Decoupling +================================= +.. autosummary:: + :toctree: ../stubs/ + + +""" From 4dbe92a6c1436df1899c434b9ebedce5e4ff6c4a Mon Sep 17 00:00:00 2001 From: Thomas Alexander Date: Fri, 17 Jun 2022 10:43:47 -0300 Subject: [PATCH 04/70] Add a dummy test. --- .../dynamic_circuits/__init__.py | 2 ++ test/dynamic_circuits/__init__.py | 11 ++++++++++ test/dynamic_circuits/test_tests.py | 20 +++++++++++++++++++ 3 files changed, 33 insertions(+) create mode 100644 test/dynamic_circuits/__init__.py create mode 100644 test/dynamic_circuits/test_tests.py diff --git a/qiskit_ibm_provider/dynamic_circuits/__init__.py b/qiskit_ibm_provider/dynamic_circuits/__init__.py index 09c169da8..bf1d122b6 100644 --- a/qiskit_ibm_provider/dynamic_circuits/__init__.py +++ b/qiskit_ibm_provider/dynamic_circuits/__init__.py @@ -49,3 +49,5 @@ """ + +foo = 1 diff --git a/test/dynamic_circuits/__init__.py b/test/dynamic_circuits/__init__.py new file mode 100644 index 000000000..fdb172d36 --- /dev/null +++ b/test/dynamic_circuits/__init__.py @@ -0,0 +1,11 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2022. +# +# 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. diff --git a/test/dynamic_circuits/test_tests.py b/test/dynamic_circuits/test_tests.py new file mode 100644 index 000000000..91b42c978 --- /dev/null +++ b/test/dynamic_circuits/test_tests.py @@ -0,0 +1,20 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2022. +# +# 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. + +from qiskit_ibm_provider import dynamic_circuits + +from ..ibm_test_case import IBMTestCase + +class TestBasicServerPaths(IBMTestCase): + + def test_tests(self): + self.assertEqual(dynamic_circuits.foo, 1) From 44a7afb3d9b9d9327d5d8837ca9f1eb9e1e5754f Mon Sep 17 00:00:00 2001 From: Thomas Alexander Date: Fri, 17 Jun 2022 11:36:28 -0300 Subject: [PATCH 05/70] Add reno. --- .../add-dynamic-circuits-module-af6f530072c82f44.yaml | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 releasenotes/notes/add-dynamic-circuits-module-af6f530072c82f44.yaml diff --git a/releasenotes/notes/add-dynamic-circuits-module-af6f530072c82f44.yaml b/releasenotes/notes/add-dynamic-circuits-module-af6f530072c82f44.yaml new file mode 100644 index 000000000..de65fd8ef --- /dev/null +++ b/releasenotes/notes/add-dynamic-circuits-module-af6f530072c82f44.yaml @@ -0,0 +1,7 @@ +--- +features: + - | + A new dynamic circuits module has been added to :mod:`qiskit_ibm_provider.dynamic_circuits`. + It will contain all specialized Qiskit routines for running applications on IBM's next-generation + quantum devices that support dynamic capabilities such as control-flow(feedforward) and classical + compute. From 8aa33655493261272e0d877c130accdb8f046f8c Mon Sep 17 00:00:00 2001 From: Thomas Alexander Date: Fri, 17 Jun 2022 17:25:27 -0300 Subject: [PATCH 06/70] Add initial qiskit DynamicalDecoupling scheduling scaffolding and tests. --- .../dynamic_circuits/__init__.py | 2 - .../dynamic_circuits/schedule.py | 142 ++++ requirements-dev.txt | 1 + test/dynamic_circuits/test_schedule.py | 626 ++++++++++++++++++ 4 files changed, 769 insertions(+), 2 deletions(-) create mode 100644 qiskit_ibm_provider/dynamic_circuits/schedule.py create mode 100644 test/dynamic_circuits/test_schedule.py diff --git a/qiskit_ibm_provider/dynamic_circuits/__init__.py b/qiskit_ibm_provider/dynamic_circuits/__init__.py index bf1d122b6..09c169da8 100644 --- a/qiskit_ibm_provider/dynamic_circuits/__init__.py +++ b/qiskit_ibm_provider/dynamic_circuits/__init__.py @@ -49,5 +49,3 @@ """ - -foo = 1 diff --git a/qiskit_ibm_provider/dynamic_circuits/schedule.py b/qiskit_ibm_provider/dynamic_circuits/schedule.py new file mode 100644 index 000000000..6e652941d --- /dev/null +++ b/qiskit_ibm_provider/dynamic_circuits/schedule.py @@ -0,0 +1,142 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2022. +# +# 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. + +"""Scheduler for dynamic circuit backends.""" + +from qiskit.circuit import Measure +from qiskit.transpiler.exceptions import TranspilerError + +from qiskit.transpiler.passes.scheduling.scheduling.base_scheduler import BaseScheduler + + +class DynamicCircuitScheduleAnalysis(BaseScheduler): + """Dynamic circuits scheduling analysis pass. + + This is a scheduler designed to work for the unique scheduling constraints of the dynamic circuits + backends due to the limitations imposed by hardware. This is expected to evolve overtime as the + dynamic circuit backends also change. + + In its current form this is slow to Qiskit's ASAP scheduler in which instructions start asas early as possible. + + The primary differences are that: + + * Measurements currently trigger the end of a "quantum block". The period between the end of the block and the next is *indeterministic* + ie., we do not know when the next block will begin (as we could be evaluating a classical function of indeterministic length) and + therefore the next block starts at a *relative* t=0. + * It is possible to apply gates during a measurement. + * Measurements on disjoint qubits happen simulataneously and are part of the same block. Measurements that are not lexigraphically + neighbors in the generated QASM3 will happen in separate blocks. + + """ + + def run(self, dag): + """Run the ASAPSchedule pass on `dag`. + Args: + dag (DAGCircuit): DAG to schedule. + Returns: + DAGCircuit: A scheduled DAG. + Raises: + TranspilerError: if the circuit is not mapped on physical qubits. + TranspilerError: if conditional bit is added to non-supported instruction. + """ + if len(dag.qregs) != 1 or dag.qregs.get("q", None) is None: + raise TranspilerError("ASAP schedule runs on physical circuits only") + + conditional_latency = self.property_set.get("conditional_latency", 0) + clbit_write_latency = self.property_set.get("clbit_write_latency", 0) + + node_start_time = dict() + idle_after = {q: 0 for q in dag.qubits + dag.clbits} + bit_indices = {q: index for index, q in enumerate(dag.qubits)} + for node in dag.topological_op_nodes(): + op_duration = self._get_node_duration(node, bit_indices, dag) + + # compute t0, t1: instruction interval, note that + # t0: start time of instruction + # t1: end time of instruction + if isinstance(node.op, self.CONDITIONAL_SUPPORTED): + t0q = max(idle_after[q] for q in node.qargs) + if node.op.condition_bits: + # conditional is bit tricky due to conditional_latency + t0c = max(idle_after[bit] for bit in node.op.condition_bits) + if t0q > t0c: + # This is situation something like below + # + # |t0q + # Q ▒▒▒▒▒▒▒▒▒░░ + # C ▒▒▒░░░░░░░░ + # |t0c + # + # In this case, you can insert readout access before tq0 + # + # |t0q + # Q ▒▒▒▒▒▒▒▒▒▒▒ + # C ▒▒▒░░░▒▒░░░ + # |t0q - conditional_latency + # + t0c = max(t0q - conditional_latency, t0c) + t1c = t0c + conditional_latency + for bit in node.op.condition_bits: + # Lock clbit until state is read + idle_after[bit] = t1c + # It starts after register read access + t0 = max(t0q, t1c) + else: + t0 = t0q + t1 = t0 + op_duration + else: + if node.op.condition_bits: + raise TranspilerError( + f"Conditional instruction {node.op.name} is not supported in ASAP scheduler." + ) + + if isinstance(node.op, Measure): + # measure instruction handling is bit tricky due to clbit_write_latency + t0q = max(idle_after[q] for q in node.qargs) + t0c = max(idle_after[c] for c in node.cargs) + # Assume following case (t0c > t0q) + # + # |t0q + # Q ▒▒▒▒░░░░░░░░░░░░ + # C ▒▒▒▒▒▒▒▒░░░░░░░░ + # |t0c + # + # In this case, there is no actual clbit access until clbit_write_latency. + # The node t0 can be push backward by this amount. + # + # |t0q' = t0c - clbit_write_latency + # Q ▒▒▒▒░░▒▒▒▒▒▒▒▒▒▒ + # C ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ + # |t0c' = t0c + # + # rather than naively doing + # + # |t0q' = t0c + # Q ▒▒▒▒░░░░▒▒▒▒▒▒▒▒ + # C ▒▒▒▒▒▒▒▒░░░▒▒▒▒▒ + # |t0c' = t0c + clbit_write_latency + # + t0 = max(t0q, t0c - clbit_write_latency) + t1 = t0 + op_duration + for clbit in node.cargs: + idle_after[clbit] = t1 + else: + # It happens to be directives such as barrier + t0 = max(idle_after[bit] for bit in node.qargs + node.cargs) + t1 = t0 + op_duration + + for bit in node.qargs: + idle_after[bit] = t1 + + node_start_time[node] = t0 + + self.property_set["node_start_time"] = node_start_time diff --git a/requirements-dev.txt b/requirements-dev.txt index 83a255774..d417921ff 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -25,3 +25,4 @@ websockets>=8 black==22.3.0 coverage>=6.3 scikit-learn>=0.20.0 +ddt>=1.2.0,!=1.4.0,!=1.4.3 diff --git a/test/dynamic_circuits/test_schedule.py b/test/dynamic_circuits/test_schedule.py new file mode 100644 index 000000000..8e805e6d8 --- /dev/null +++ b/test/dynamic_circuits/test_schedule.py @@ -0,0 +1,626 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2022. +# +# 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 dynamic circuits scheduling analysis""" + +import unittest + +from ddt import ddt, data, unpack +from qiskit import QuantumCircuit +from qiskit.pulse import Schedule, Play, Constant, DriveChannel +from qiskit.test import QiskitTestCase +from qiskit.transpiler.instruction_durations import InstructionDurations +from qiskit.transpiler.passes import ( + PadDelay, + SetIOLatency, +) +from qiskit.transpiler.passmanager import PassManager +from qiskit.transpiler.exceptions import TranspilerError + +from qiskit_ibm_provider.dynamic_circuits.schedule import DynamicCircuitScheduleAnalysis + +@ddt +class TestSchedulingAndPaddingPass(QiskitTestCase): + """Tests the Scheduling passes""" + + def test_classically_controlled_gate_after_measure(self): + """Test if schedules circuits with c_if after measure with a common clbit. + See: https://github.com/Qiskit/qiskit-terra/issues/7654 + (input) + ┌─┐ + q_0: ┤M├─────────── + └╥┘ ┌───┐ + q_1: ─╫────┤ X ├─── + ║ └─╥─┘ + ║ ┌────╨────┐ + c: 1/═╩═╡ c_0 = T ╞ + 0 └─────────┘ + (scheduled) + ┌─┐┌────────────────┐ + q_0: ───────────────────┤M├┤ Delay(200[dt]) ├ + ┌─────────────────┐└╥┘└─────┬───┬──────┘ + q_1: ┤ Delay(1000[dt]) ├─╫───────┤ X ├─────── + └─────────────────┘ ║ └─╥─┘ + ║ ┌────╨────┐ + c: 1/════════════════════╩════╡ c_0=0x1 ╞════ + 0 └─────────┘ + """ + qc = QuantumCircuit(2, 1) + qc.measure(0, 0) + qc.x(1).c_if(0, True) + + durations = InstructionDurations([("x", None, 200), ("measure", None, 1000)]) + pm = PassManager([DynamicCircuitScheduleAnalysis(durations), PadDelay()]) + scheduled = pm.run(qc) + + expected = QuantumCircuit(2, 1) + expected.measure(0, 0) + expected.delay(1000, 1) # x.c_if starts after measure + expected.x(1).c_if(0, True) + expected.delay(200, 0) + + self.assertEqual(expected, scheduled) + + def test_measure_after_measure(self): + """Test if schedules circuits with measure after measure with a common clbit. + See: https://github.com/Qiskit/qiskit-terra/issues/7654 + (input) + ┌───┐┌─┐ + q_0: ┤ X ├┤M├─── + └───┘└╥┘┌─┐ + q_1: ──────╫─┤M├ + ║ └╥┘ + c: 1/══════╩══╩═ + 0 0 + (scheduled) + ┌───┐ ┌─┐┌─────────────────┐ + q_0: ───────┤ X ├───────┤M├┤ Delay(1000[dt]) ├ + ┌──────┴───┴──────┐└╥┘└───────┬─┬───────┘ + q_1: ┤ Delay(1200[dt]) ├─╫─────────┤M├──────── + └─────────────────┘ ║ └╥┘ + c: 1/════════════════════╩══════════╩═════════ + 0 0 + """ + qc = QuantumCircuit(2, 1) + qc.x(0) + qc.measure(0, 0) + qc.measure(1, 0) + + durations = InstructionDurations([("x", None, 200), ("measure", None, 1000)]) + pm = PassManager([DynamicCircuitScheduleAnalysis(durations), PadDelay()]) + scheduled = pm.run(qc) + + expected = QuantumCircuit(2, 1) + expected.x(0) + expected.measure(0, 0) + expected.delay(1200, 1) + expected.measure(1, 0) + expected.delay(1000, 0) + + self.assertEqual(expected, scheduled) + + def test_c_if_on_different_qubits(self): + """Test if schedules circuits with `c_if`s on different qubits. + (input) + ┌─┐ + q_0: ┤M├────────────────────── + └╥┘ ┌───┐ + q_1: ─╫────┤ X ├────────────── + ║ └─╥─┘ ┌───┐ + q_2: ─╫──────╫────────┤ X ├─── + ║ ║ └─╥─┘ + ║ ┌────╨────┐┌────╨────┐ + c: 1/═╩═╡ c_0 = T ╞╡ c_0 = T ╞ + 0 └─────────┘└─────────┘ + (scheduled) + ┌─┐┌────────────────┐ + q_0: ───────────────────┤M├┤ Delay(200[dt]) ├─────────── + ┌─────────────────┐└╥┘└─────┬───┬──────┘ + q_1: ┤ Delay(1000[dt]) ├─╫───────┤ X ├────────────────── + ├─────────────────┤ ║ └─╥─┘ ┌───┐ + q_2: ┤ Delay(1000[dt]) ├─╫─────────╫────────────┤ X ├─── + └─────────────────┘ ║ ║ └─╥─┘ + ║ ┌────╨────┐ ┌────╨────┐ + c: 1/════════════════════╩════╡ c_0=0x1 ╞════╡ c_0=0x1 ╞ + 0 └─────────┘ └─────────┘ + """ + qc = QuantumCircuit(3, 1) + qc.measure(0, 0) + qc.x(1).c_if(0, True) + qc.x(2).c_if(0, True) + + durations = InstructionDurations([("x", None, 200), ("measure", None, 1000)]) + pm = PassManager([DynamicCircuitScheduleAnalysis(durations), PadDelay()]) + scheduled = pm.run(qc) + + expected = QuantumCircuit(3, 1) + expected.measure(0, 0) + expected.delay(1000, 1) + expected.delay(1000, 2) + expected.x(1).c_if(0, True) + expected.x(2).c_if(0, True) + expected.delay(200, 0) + + self.assertEqual(expected, scheduled) + + def test_shorter_measure_after_measure(self): + """Test if schedules circuits with shorter measure after measure with a common clbit. + (input) + ┌─┐ + q_0: ┤M├─── + └╥┘┌─┐ + q_1: ─╫─┤M├ + ║ └╥┘ + c: 1/═╩══╩═ + 0 0 + (scheduled) + ┌─┐┌────────────────┐ + q_0: ───────────────────┤M├┤ Delay(700[dt]) ├ + ┌─────────────────┐└╥┘└──────┬─┬───────┘ + q_1: ┤ Delay(1000[dt]) ├─╫────────┤M├──────── + └─────────────────┘ ║ └╥┘ + c: 1/════════════════════╩═════════╩═════════ + 0 0 + """ + qc = QuantumCircuit(2, 1) + qc.measure(0, 0) + qc.measure(1, 0) + + durations = InstructionDurations([("measure", [0], 1000), ("measure", [1], 700)]) + pm = PassManager([DynamicCircuitScheduleAnalysis(durations), PadDelay()]) + scheduled = pm.run(qc) + + expected = QuantumCircuit(2, 1) + expected.measure(0, 0) + expected.delay(1000, 1) + expected.measure(1, 0) + expected.delay(700, 0) + + self.assertEqual(expected, scheduled) + + def test_measure_after_c_if(self): + """Test if schedules circuits with c_if after measure with a common clbit. + (input) + ┌─┐ + q_0: ┤M├────────────── + └╥┘ ┌───┐ + q_1: ─╫────┤ X ├────── + ║ └─╥─┘ ┌─┐ + q_2: ─╫──────╫─────┤M├ + ║ ┌────╨────┐└╥┘ + c: 1/═╩═╡ c_0 = T ╞═╩═ + 0 └─────────┘ 0 + (scheduled) + ┌─┐┌─────────────────┐ + q_0: ───────────────────┤M├┤ Delay(1000[dt]) ├────────────────── + ┌─────────────────┐└╥┘└──────┬───┬──────┘┌────────────────┐ + q_1: ┤ Delay(1000[dt]) ├─╫────────┤ X ├───────┤ Delay(800[dt]) ├ + ├─────────────────┤ ║ └─╥─┘ └──────┬─┬───────┘ + q_2: ┤ Delay(1000[dt]) ├─╫──────────╫────────────────┤M├──────── + └─────────────────┘ ║ ┌────╨────┐ └╥┘ + c: 1/════════════════════╩═════╡ c_0=0x1 ╞════════════╩═════════ + 0 └─────────┘ 0 + """ + qc = QuantumCircuit(3, 1) + qc.measure(0, 0) + qc.x(1).c_if(0, 1) + qc.measure(2, 0) + + durations = InstructionDurations([("x", None, 200), ("measure", None, 1000)]) + pm = PassManager([DynamicCircuitScheduleAnalysis(durations), PadDelay()]) + scheduled = pm.run(qc) + + expected = QuantumCircuit(3, 1) + expected.delay(1000, 1) + expected.delay(1000, 2) + expected.measure(0, 0) + expected.x(1).c_if(0, 1) + expected.measure(2, 0) + expected.delay(1000, 0) + expected.delay(800, 1) + + self.assertEqual(expected, scheduled) + + def test_parallel_gate_different_length(self): + """Test circuit having two parallel instruction with different length. + (input) + ┌───┐┌─┐ + q_0: ┤ X ├┤M├─── + ├───┤└╥┘┌─┐ + q_1: ┤ X ├─╫─┤M├ + └───┘ ║ └╥┘ + c: 2/══════╩══╩═ + 0 1 + (expected) + ┌───┐┌─┐┌────────────────┐ + q_0: ┤ X ├┤M├┤ Delay(200[dt]) ├ + ├───┤└╥┘└──────┬─┬───────┘ + q_1: ┤ X ├─╫────────┤M├──────── + └───┘ ║ └╥┘ + c: 2/══════╩═════════╩═════════ + 0 1 + """ + qc = QuantumCircuit(2, 2) + qc.x(0) + qc.x(1) + qc.measure(0, 0) + qc.measure(1, 1) + + durations = InstructionDurations( + [("x", [0], 200), ("x", [1], 400), ("measure", None, 1000)] + ) + + pm = PassManager([DynamicCircuitScheduleAnalysis(durations), PadDelay()]) + qc_sched = pm.run(qc) + + sched_expected = QuantumCircuit(2, 2) + sched_expected.x(0) + sched_expected.x(1) + sched_expected.measure(0, 0) # immediately start after X gate + sched_expected.measure(1, 1) + sched_expected.delay(200, 0) + + self.assertEqual(qc_sched, sched_expected) + + def test_parallel_gate_different_length_with_barrier(self): + """Test circuit having two parallel instruction with different length with barrier. + (input) + ┌───┐┌─┐ + q_0: ┤ X ├┤M├─── + ├───┤└╥┘┌─┐ + q_1: ┤ X ├─╫─┤M├ + └───┘ ║ └╥┘ + c: 2/══════╩══╩═ + 0 1 + (expected) + ┌───┐┌────────────────┐ ░ ┌─┐ + q_0: ┤ X ├┤ Delay(200[dt]) ├─░─┤M├─── + ├───┤└────────────────┘ ░ └╥┘┌─┐ + q_1: ┤ X ├───────────────────░──╫─┤M├ + └───┘ ░ ║ └╥┘ + c: 2/═══════════════════════════╩══╩═ + 0 1 + """ + qc = QuantumCircuit(2, 2) + qc.x(0) + qc.x(1) + qc.barrier() + qc.measure(0, 0) + qc.measure(1, 1) + + durations = InstructionDurations( + [("x", [0], 200), ("x", [1], 400), ("measure", None, 1000)] + ) + + pm = PassManager([DynamicCircuitScheduleAnalysis(durations), PadDelay()]) + qc_sched = pm.run(qc) + + sched_expected = QuantumCircuit(2, 2) + sched_expected.x(0) + sched_expected.delay(200, 0) + sched_expected.x(1) + sched_expected.barrier() + sched_expected.measure(0, 0) + sched_expected.measure(1, 1) + + self.assertEqual(qc_sched, sched_expected) + + def test_measure_after_c_if_on_edge_locking(self): + """Test if schedules circuits with c_if after measure with a common clbit. + The scheduler is configured to reproduce behavior of the 0.20.0, + in which clbit lock is applied to the end-edge of measure instruction. + See https://github.com/Qiskit/qiskit-terra/pull/7655 + (input) + ┌─┐ + q_0: ┤M├────────────── + └╥┘ ┌───┐ + q_1: ─╫────┤ X ├────── + ║ └─╥─┘ ┌─┐ + q_2: ─╫──────╫─────┤M├ + ║ ┌────╨────┐└╥┘ + c: 1/═╩═╡ c_0 = T ╞═╩═ + 0 └─────────┘ 0 + (scheduled) + ┌─┐┌────────────────┐ + q_0: ───────────────────┤M├┤ Delay(200[dt]) ├───────────────────── + ┌─────────────────┐└╥┘└─────┬───┬──────┘ + q_1: ┤ Delay(1000[dt]) ├─╫───────┤ X ├──────────────────────────── + └─────────────────┘ ║ └─╥─┘ ┌─┐┌────────────────┐ + q_2: ────────────────────╫─────────╫─────────┤M├┤ Delay(200[dt]) ├ + ║ ┌────╨────┐ └╥┘└────────────────┘ + c: 1/════════════════════╩════╡ c_0=0x1 ╞═════╩═══════════════════ + 0 └─────────┘ 0 + """ + qc = QuantumCircuit(3, 1) + qc.measure(0, 0) + qc.x(1).c_if(0, 1) + qc.measure(2, 0) + + durations = InstructionDurations([("x", None, 200), ("measure", None, 1000)]) + + # lock at the end edge + actual_sched = PassManager( + [ + SetIOLatency(clbit_write_latency=1000), + DynamicCircuitScheduleAnalysis(durations), + PadDelay(), + ] + ).run(qc) + + expected_sched = QuantumCircuit(3, 1) + expected_sched.measure(0, 0) + expected_sched.delay(1000, 1) + expected_sched.x(1).c_if(0, 1) + expected_sched.measure(2, 0) + expected_sched.delay(200, 0) + expected_sched.delay(200, 2) + self.assertEqual(expected_sched, actual_sched) + + @data([100, 200], [500, 0], [1000, 200]) + @unpack + def test_active_reset_circuit(self, write_lat, cond_lat): + """Test practical example of reset circuit. + Because of the stimulus pulse overlap with the previous XGate on the q register, + measure instruction is always triggered after XGate regardless of write latency. + Thus only conditional latency matters in the scheduling. + (input) + ┌─┐ ┌───┐ ┌─┐ ┌───┐ ┌─┐ ┌───┐ + q: ┤M├───┤ X ├───┤M├───┤ X ├───┤M├───┤ X ├─── + └╥┘ └─╥─┘ └╥┘ └─╥─┘ └╥┘ └─╥─┘ + ║ ┌────╨────┐ ║ ┌────╨────┐ ║ ┌────╨────┐ + c: 1/═╩═╡ c_0=0x1 ╞═╩═╡ c_0=0x1 ╞═╩═╡ c_0=0x1 ╞ + 0 └─────────┘ 0 └─────────┘ 0 └─────────┘ + """ + qc = QuantumCircuit(1, 1) + qc.measure(0, 0) + qc.x(0).c_if(0, 1) + qc.measure(0, 0) + qc.x(0).c_if(0, 1) + qc.measure(0, 0) + qc.x(0).c_if(0, 1) + + durations = InstructionDurations([("x", None, 100), ("measure", None, 1000)]) + + actual_sched = PassManager( + [ + SetIOLatency(clbit_write_latency=write_lat, conditional_latency=cond_lat), + DynamicCircuitScheduleAnalysis(durations), + PadDelay(), + ] + ).run(qc) + + expected = QuantumCircuit(1, 1) + expected.measure(0, 0) + if cond_lat > 0: + expected.delay(cond_lat, 0) + expected.x(0).c_if(0, 1) + expected.measure(0, 0) + if cond_lat > 0: + expected.delay(cond_lat, 0) + expected.x(0).c_if(0, 1) + expected.measure(0, 0) + if cond_lat > 0: + expected.delay(cond_lat, 0) + expected.x(0).c_if(0, 1) + + self.assertEqual(expected, actual_sched) + + def test_random_complicated_circuit(self): + """Test scheduling complicated circuit with control flow. + (input) + ┌────────────────┐ ┌───┐ ░ ┌───┐ » + q_0: ┤ Delay(100[dt]) ├───┤ X ├────░──────────────────┤ X ├───» + └────────────────┘ └─╥─┘ ░ ┌───┐ └─╥─┘ » + q_1: ───────────────────────╫──────░───────┤ X ├────────╫─────» + ║ ░ ┌─┐ └─╥─┘ ║ » + q_2: ───────────────────────╫──────░─┤M├─────╫──────────╫─────» + ┌────╨────┐ ░ └╥┘┌────╨────┐┌────╨────┐» + c: 1/══════════════════╡ c_0=0x1 ╞════╩═╡ c_0=0x0 ╞╡ c_0=0x0 ╞» + └─────────┘ 0 └─────────┘└─────────┘» + « ┌────────────────┐┌───┐ + «q_0: ┤ Delay(300[dt]) ├┤ X ├─────■───── + « └────────────────┘└───┘ ┌─┴─┐ + «q_1: ────────■─────────────────┤ X ├─── + « ┌─┴─┐ ┌─┐ └─╥─┘ + «q_2: ──────┤ X ├────────┤M├──────╫───── + « └───┘ └╥┘ ┌────╨────┐ + «c: 1/════════════════════╩══╡ c_0=0x0 ╞ + « 0 └─────────┘ + (scheduled) duration = 2800 dt + ┌────────────────┐ ┌───┐ ░ ┌─────────────────┐ » + q_0: ┤ Delay(200[dt]) ├───┤ X ├────░─┤ Delay(1400[dt]) ├───────────» + ├────────────────┤ └─╥─┘ ░ ├─────────────────┤ ┌───┐ » + q_1: ┤ Delay(300[dt]) ├─────╫──────░─┤ Delay(1200[dt]) ├───┤ X ├───» + ├────────────────┤ ║ ░ └───────┬─┬───────┘ └─╥─┘ » + q_2: ┤ Delay(300[dt]) ├─────╫──────░─────────┤M├─────────────╫─────» + └────────────────┘┌────╨────┐ ░ └╥┘ ┌────╨────┐» + c: 1/══════════════════╡ c_0=0x1 ╞════════════╩═════════╡ c_0=0x0 ╞» + └─────────┘ 0 └─────────┘» + « ┌───┐ ┌────────────────┐ ┌───┐ » + «q_0: ─────────────────────┤ X ├───┤ Delay(300[dt]) ├──────┤ X ├───────» + « └─╥─┘ └────────────────┘┌─────┴───┴──────┐» + «q_1: ───────────────────────╫─────────────■─────────┤ Delay(400[dt]) ├» + « ┌────────────────┐ ║ ┌─┴─┐ ├────────────────┤» + «q_2: ┤ Delay(300[dt]) ├─────╫───────────┤ X ├───────┤ Delay(300[dt]) ├» + « └────────────────┘┌────╨────┐ └───┘ └────────────────┘» + «c: 1/══════════════════╡ c_0=0x0 ╞════════════════════════════════════» + « └─────────┘ » + « ┌────────────────┐ + «q_0: ─────■─────┤ Delay(700[dt]) ├ + « ┌─┴─┐ ├────────────────┤ + «q_1: ───┤ X ├───┤ Delay(700[dt]) ├ + « └─╥─┘ └──────┬─┬───────┘ + «q_2: ─────╫────────────┤M├──────── + « ┌────╨────┐ └╥┘ + «c: 1/╡ c_0=0x0 ╞════════╩═════════ + « └─────────┘ 0 + """ + qc = QuantumCircuit(3, 1) + qc.delay(100, 0) + qc.x(0).c_if(0, 1) + qc.barrier() + qc.measure(2, 0) + qc.x(1).c_if(0, 0) + qc.x(0).c_if(0, 0) + qc.delay(300, 0) + qc.cx(1, 2) + qc.x(0) + qc.cx(0, 1).c_if(0, 0) + qc.measure(2, 0) + + durations = InstructionDurations( + [("x", None, 100), ("measure", None, 1000), ("cx", None, 200)] + ) + + actual_sched = PassManager( + [ + SetIOLatency(clbit_write_latency=100, conditional_latency=200), + DynamicCircuitScheduleAnalysis(durations), + PadDelay(), + ] + ).run(qc) + + expected_sched = QuantumCircuit(3, 1) + expected_sched.delay(200, 0) # due to conditional latency of 200dt + expected_sched.delay(300, 1) + expected_sched.delay(300, 2) + expected_sched.x(0).c_if(0, 1) + expected_sched.barrier() + expected_sched.delay(1400, 0) + expected_sched.delay(1200, 1) + expected_sched.measure(2, 0) + expected_sched.x(1).c_if(0, 0) + expected_sched.x(0).c_if(0, 0) + expected_sched.delay(300, 0) + expected_sched.x(0) + expected_sched.delay(300, 2) + expected_sched.cx(1, 2) + expected_sched.delay(400, 1) + expected_sched.cx(0, 1).c_if(0, 0) + expected_sched.delay(700, 0) # creg is released at t0 of cx(0,1).c_if(0,0) + expected_sched.delay( + 700, 1 + ) # no creg write until 100dt. thus measure can move left by 300dt. + expected_sched.delay(300, 2) + expected_sched.measure(2, 0) + self.assertEqual(expected_sched, actual_sched) + self.assertEqual(actual_sched.duration, 3100) + + def test_dag_introduces_extra_dependency_between_conditionals(self): + """Test dependency between conditional operations in the scheduling. + In the below example circuit, the conditional x on q1 could start at time 0, + however it must be scheduled after the conditional x on q0 in scheduling. + That is because circuit model used in the transpiler passes (DAGCircuit) + interprets instructions acting on common clbits must be run in the order + given by the original circuit (QuantumCircuit). + (input) + ┌────────────────┐ ┌───┐ + q_0: ┤ Delay(100[dt]) ├───┤ X ├─── + └─────┬───┬──────┘ └─╥─┘ + q_1: ──────┤ X ├────────────╫───── + └─╥─┘ ║ + ┌────╨────┐ ┌────╨────┐ + c: 1/═══╡ c_0=0x1 ╞════╡ c_0=0x1 ╞ + └─────────┘ └─────────┘ + (scheduled) + ┌────────────────┐ ┌───┐ + q_0: ┤ Delay(100[dt]) ├───┤ X ├────────────── + ├────────────────┤ └─╥─┘ ┌───┐ + q_1: ┤ Delay(100[dt]) ├─────╫────────┤ X ├─── + └────────────────┘ ║ └─╥─┘ + ┌────╨────┐┌────╨────┐ + c: 1/══════════════════╡ c_0=0x1 ╞╡ c_0=0x1 ╞ + └─────────┘└─────────┘ + """ + qc = QuantumCircuit(2, 1) + qc.delay(100, 0) + qc.x(0).c_if(0, True) + qc.x(1).c_if(0, True) + + durations = InstructionDurations([("x", None, 160)]) + pm = PassManager([DynamicCircuitScheduleAnalysis(durations), PadDelay()]) + scheduled = pm.run(qc) + + expected = QuantumCircuit(2, 1) + expected.delay(100, 0) + expected.delay(100, 1) # due to extra dependency on clbits + expected.x(0).c_if(0, True) + expected.x(1).c_if(0, True) + + self.assertEqual(expected, scheduled) + + def test_scheduling_with_calibration(self): + """Test if calibrated instruction can update node duration.""" + qc = QuantumCircuit(2) + qc.x(0) + qc.cx(0, 1) + qc.x(1) + qc.cx(0, 1) + + xsched = Schedule(Play(Constant(300, 0.1), DriveChannel(0))) + qc.add_calibration("x", (0,), xsched) + + durations = InstructionDurations([("x", None, 160), ("cx", None, 600)]) + pm = PassManager([DynamicCircuitScheduleAnalysis(durations), PadDelay()]) + scheduled = pm.run(qc) + + expected = QuantumCircuit(2) + expected.x(0) + expected.delay(300, 1) + expected.cx(0, 1) + expected.x(1) + expected.delay(160, 0) + expected.cx(0, 1) + expected.add_calibration("x", (0,), xsched) + + self.assertEqual(expected, scheduled) + + def test_padding_not_working_without_scheduling(self): + """Test padding fails when un-scheduled DAG is input.""" + qc = QuantumCircuit(1, 1) + qc.delay(100, 0) + qc.x(0) + qc.measure(0, 0) + + with self.assertRaises(TranspilerError): + PassManager(PadDelay()).run(qc) + + def test_no_pad_very_end_of_circuit(self): + """Test padding option that inserts no delay at the very end of circuit. + This circuit will be unchanged after scheduling/padding. + ┌────────────────┐┌─┐ + q_0: ┤ Delay(100[dt]) ├┤M├ + └─────┬───┬──────┘└╥┘ + q_1: ──────┤ X ├────────╫─ + └───┘ ║ + c: 1/═══════════════════╩═ + 0 + """ + qc = QuantumCircuit(2, 1) + qc.delay(100, 0) + qc.x(1) + qc.measure(0, 0) + + durations = InstructionDurations([("x", None, 160), ("measure", None, 1000)]) + + scheduled = PassManager( + [ + DynamicCircuitScheduleAnalysis(durations), + PadDelay(fill_very_end=False), + ] + ).run(qc) + + self.assertEqual(scheduled, qc) + + +if __name__ == "__main__": + unittest.main() From 06f9d86305a7a2a28f158316f47371e202f7d8b6 Mon Sep 17 00:00:00 2001 From: Thomas Alexander Date: Sun, 19 Jun 2022 15:26:26 -0300 Subject: [PATCH 07/70] Refactor scheduler with visitor pattern. --- .../dynamic_circuits/schedule.py | 230 +++++++++++------- 1 file changed, 141 insertions(+), 89 deletions(-) diff --git a/qiskit_ibm_provider/dynamic_circuits/schedule.py b/qiskit_ibm_provider/dynamic_circuits/schedule.py index 6e652941d..38810644c 100644 --- a/qiskit_ibm_provider/dynamic_circuits/schedule.py +++ b/qiskit_ibm_provider/dynamic_circuits/schedule.py @@ -12,9 +12,9 @@ """Scheduler for dynamic circuit backends.""" +import qiskit from qiskit.circuit import Measure from qiskit.transpiler.exceptions import TranspilerError - from qiskit.transpiler.passes.scheduling.scheduling.base_scheduler import BaseScheduler @@ -38,6 +38,24 @@ class DynamicCircuitScheduleAnalysis(BaseScheduler): """ + def __init__(self, durations: qiskit.transpiler.instruction_durations.InstructionDurations): + """Scheduler for dynamic circuit backends. + + Args: + durations: Durations of instructions to be used in scheduling. + """ + + self._dag = None + + self._conditional_latency = 0 + self._clbit_write_latency = 0 + + self._node_start_time = None + self._idle_after = None + self._bit_indices = None + + super().__init__(durations) + def run(self, dag): """Run the ASAPSchedule pass on `dag`. Args: @@ -48,95 +66,129 @@ def run(self, dag): TranspilerError: if the circuit is not mapped on physical qubits. TranspilerError: if conditional bit is added to non-supported instruction. """ + self._init_run(dag) + + for node in dag.topological_op_nodes(): + self._visit_node(node) + + self.property_set["node_start_time"] = self._node_start_time + + def _init_run(self, dag): + """Setup for initial run.""" + + self._dag = dag + if len(dag.qregs) != 1 or dag.qregs.get("q", None) is None: raise TranspilerError("ASAP schedule runs on physical circuits only") - conditional_latency = self.property_set.get("conditional_latency", 0) - clbit_write_latency = self.property_set.get("clbit_write_latency", 0) - - node_start_time = dict() - idle_after = {q: 0 for q in dag.qubits + dag.clbits} - bit_indices = {q: index for index, q in enumerate(dag.qubits)} - for node in dag.topological_op_nodes(): - op_duration = self._get_node_duration(node, bit_indices, dag) - - # compute t0, t1: instruction interval, note that - # t0: start time of instruction - # t1: end time of instruction - if isinstance(node.op, self.CONDITIONAL_SUPPORTED): - t0q = max(idle_after[q] for q in node.qargs) - if node.op.condition_bits: - # conditional is bit tricky due to conditional_latency - t0c = max(idle_after[bit] for bit in node.op.condition_bits) - if t0q > t0c: - # This is situation something like below - # - # |t0q - # Q ▒▒▒▒▒▒▒▒▒░░ - # C ▒▒▒░░░░░░░░ - # |t0c - # - # In this case, you can insert readout access before tq0 - # - # |t0q - # Q ▒▒▒▒▒▒▒▒▒▒▒ - # C ▒▒▒░░░▒▒░░░ - # |t0q - conditional_latency - # - t0c = max(t0q - conditional_latency, t0c) - t1c = t0c + conditional_latency - for bit in node.op.condition_bits: - # Lock clbit until state is read - idle_after[bit] = t1c - # It starts after register read access - t0 = max(t0q, t1c) - else: - t0 = t0q - t1 = t0 + op_duration + self._conditional_latency = self.property_set.get("conditional_latency", 0) + self._clbit_write_latency = self.property_set.get("clbit_write_latency", 0) + + self._node_start_time = dict() + self._idle_after = {q: 0 for q in dag.qubits + dag.clbits} + self._bit_indices = {q: index for index, q in enumerate(dag.qubits)} + + def _get_node_duration(self, node): + return super()._get_node_duration(node, self._bit_indices, self._dag) + + def _visit_node(self, node): + # compute t0, t1: instruction interval, note that + # t0: start time of instruction + # t1: end time of instruction + if isinstance(node.op, self.CONDITIONAL_SUPPORTED): + t0, t1 = self._visit_conditional_node(node) + else: + if node.op.condition_bits: + raise TranspilerError( + f"Conditional instruction {node.op.name} is not supported in ASAP scheduler." + ) + + if isinstance(node.op, Measure): + t0, t1 = self._visit_measure(node) else: - if node.op.condition_bits: - raise TranspilerError( - f"Conditional instruction {node.op.name} is not supported in ASAP scheduler." - ) - - if isinstance(node.op, Measure): - # measure instruction handling is bit tricky due to clbit_write_latency - t0q = max(idle_after[q] for q in node.qargs) - t0c = max(idle_after[c] for c in node.cargs) - # Assume following case (t0c > t0q) - # - # |t0q - # Q ▒▒▒▒░░░░░░░░░░░░ - # C ▒▒▒▒▒▒▒▒░░░░░░░░ - # |t0c - # - # In this case, there is no actual clbit access until clbit_write_latency. - # The node t0 can be push backward by this amount. - # - # |t0q' = t0c - clbit_write_latency - # Q ▒▒▒▒░░▒▒▒▒▒▒▒▒▒▒ - # C ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ - # |t0c' = t0c - # - # rather than naively doing - # - # |t0q' = t0c - # Q ▒▒▒▒░░░░▒▒▒▒▒▒▒▒ - # C ▒▒▒▒▒▒▒▒░░░▒▒▒▒▒ - # |t0c' = t0c + clbit_write_latency - # - t0 = max(t0q, t0c - clbit_write_latency) - t1 = t0 + op_duration - for clbit in node.cargs: - idle_after[clbit] = t1 - else: - # It happens to be directives such as barrier - t0 = max(idle_after[bit] for bit in node.qargs + node.cargs) - t1 = t0 + op_duration - - for bit in node.qargs: - idle_after[bit] = t1 - - node_start_time[node] = t0 - - self.property_set["node_start_time"] = node_start_time + t0, t1 = self._visit_generic(node) + + for bit in node.qargs: + self._idle_after[bit] = t1 + + self._node_start_time[node] = t0 + + def _visit_conditional_node(self, node): + op_duration = self._get_node_duration(node) + + t0q = max(self._idle_after[q] for q in node.qargs) + if node.op.condition_bits: + # conditional is bit tricky due to conditional_latency + t0c = max(self._idle_after[bit] for bit in node.op.condition_bits) + if t0q > t0c: + # This is situation something like below + # + # |t0q + # Q ▒▒▒▒▒▒▒▒▒░░ + # C ▒▒▒░░░░░░░░ + # |t0c + # + # In this case, you can insert readout access before tq0 + # + # |t0q + # Q ▒▒▒▒▒▒▒▒▒▒▒ + # C ▒▒▒░░░▒▒░░░ + # |t0q - conditional_latency + # + t0c = max(t0q - self._conditional_latency, t0c) + t1c = t0c + self._conditional_latency + for bit in node.op.condition_bits: + # Lock clbit until state is read + self._idle_after[bit] = t1c + # It starts after register read access + t0 = max(t0q, t1c) + else: + t0 = t0q + t1 = t0 + op_duration + + t1 = t0 + op_duration + return t0, t1 + + def _visit_measure(self, node): + op_duration = self._get_node_duration(node) + + # measure instruction handling is bit tricky due to clbit_write_latency + t0q = max(self._idle_after[q] for q in node.qargs) + t0c = max(self._idle_after[c] for c in node.cargs) + # Assume following case (t0c > t0q) + # + # |t0q + # Q ▒▒▒▒░░░░░░░░░░░░ + # C ▒▒▒▒▒▒▒▒░░░░░░░░ + # |t0c + # + # In this case, there is no actual clbit access until clbit_write_latency. + # The node t0 can be push backward by this amount. + # + # |t0q' = t0c - clbit_write_latency + # Q ▒▒▒▒░░▒▒▒▒▒▒▒▒▒▒ + # C ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ + # |t0c' = t0c + # + # rather than naively doing + # + # |t0q' = t0c + # Q ▒▒▒▒░░░░▒▒▒▒▒▒▒▒ + # C ▒▒▒▒▒▒▒▒░░░▒▒▒▒▒ + # |t0c' = t0c + clbit_write_latency + # + t0 = max(t0q, t0c - self._clbit_write_latency) + t1 = t0 + op_duration + for clbit in node.cargs: + self._idle_after[clbit] = t1 + + return t0, t1 + + def _visit_generic(self, node): + """Visit a generic node such as a gate or barrier.""" + op_duration = self._get_node_duration(node) + + # It happens to be directives such as barrier + t0 = max(self._idle_after[bit] for bit in node.qargs + node.cargs) + t1 = t0 + op_duration + return t0, t1 From a518281ce7a863da3572825753145b9d3c165995 Mon Sep 17 00:00:00 2001 From: Thomas Alexander Date: Sun, 19 Jun 2022 16:04:17 -0300 Subject: [PATCH 08/70] Refactor nodes to no longer return. Remove special handling for classical bits. --- .../dynamic_circuits/schedule.py | 78 +++++++++---------- 1 file changed, 37 insertions(+), 41 deletions(-) diff --git a/qiskit_ibm_provider/dynamic_circuits/schedule.py b/qiskit_ibm_provider/dynamic_circuits/schedule.py index 38810644c..039f882bb 100644 --- a/qiskit_ibm_provider/dynamic_circuits/schedule.py +++ b/qiskit_ibm_provider/dynamic_circuits/schedule.py @@ -96,7 +96,7 @@ def _visit_node(self, node): # t0: start time of instruction # t1: end time of instruction if isinstance(node.op, self.CONDITIONAL_SUPPORTED): - t0, t1 = self._visit_conditional_node(node) + self._visit_conditional_node(node) else: if node.op.condition_bits: raise TranspilerError( @@ -104,20 +104,25 @@ def _visit_node(self, node): ) if isinstance(node.op, Measure): - t0, t1 = self._visit_measure(node) + self._visit_measure(node) else: - t0, t1 = self._visit_generic(node) - - for bit in node.qargs: - self._idle_after[bit] = t1 - - self._node_start_time[node] = t0 + self._visit_generic(node) def _visit_conditional_node(self, node): - op_duration = self._get_node_duration(node) + """Handling case of a conditional execution. - t0q = max(self._idle_after[q] for q in node.qargs) + Conditional execution durations are currently non-deterministic. as we do not know + the time it will take to begin executing the block. We do however know when the + duration of the block contents execution (provided it does not also contain + conditional executions). + + TODO: Update for support of general control-flow, not just single conditional operations. + """ if node.op.condition_bits: + # Special processing required to resolve conditional scheduling dependencies + op_duration = self._get_node_duration(node) + + t0q = max(self._idle_after[q] for q in node.qargs) # conditional is bit tricky due to conditional_latency t0c = max(self._idle_after[bit] for bit in node.op.condition_bits) if t0q > t0c: @@ -142,47 +147,32 @@ def _visit_conditional_node(self, node): self._idle_after[bit] = t1c # It starts after register read access t0 = max(t0q, t1c) - else: - t0 = t0q - t1 = t0 + op_duration - t1 = t0 + op_duration - return t0, t1 + t1 = t0 + op_duration + self._update_idles(node, t0, t1) + else: + # Fall through to generic case if not conditional + self._visit_generic(node) def _visit_measure(self, node): + """Visit a measurement node. + + Measurement currently triggers the end of a pulse block in IBM dynamic circuits hardware. + This means that it is possible to schedule *up to* a measurement (and during its pulses) + but the measurement will be followed by a period of indeterminism. + All measurements on disjoing qubits will be collected on the same qubits to be run simulataneously. + + """ op_duration = self._get_node_duration(node) # measure instruction handling is bit tricky due to clbit_write_latency t0q = max(self._idle_after[q] for q in node.qargs) - t0c = max(self._idle_after[c] for c in node.cargs) - # Assume following case (t0c > t0q) - # - # |t0q - # Q ▒▒▒▒░░░░░░░░░░░░ - # C ▒▒▒▒▒▒▒▒░░░░░░░░ - # |t0c - # - # In this case, there is no actual clbit access until clbit_write_latency. - # The node t0 can be push backward by this amount. - # - # |t0q' = t0c - clbit_write_latency - # Q ▒▒▒▒░░▒▒▒▒▒▒▒▒▒▒ - # C ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ - # |t0c' = t0c - # - # rather than naively doing - # - # |t0q' = t0c - # Q ▒▒▒▒░░░░▒▒▒▒▒▒▒▒ - # C ▒▒▒▒▒▒▒▒░░░▒▒▒▒▒ - # |t0c' = t0c + clbit_write_latency - # - t0 = max(t0q, t0c - self._clbit_write_latency) + t0 = t0q t1 = t0 + op_duration for clbit in node.cargs: self._idle_after[clbit] = t1 - return t0, t1 + self._update_idles(node, t0, t1) def _visit_generic(self, node): """Visit a generic node such as a gate or barrier.""" @@ -191,4 +181,10 @@ def _visit_generic(self, node): # It happens to be directives such as barrier t0 = max(self._idle_after[bit] for bit in node.qargs + node.cargs) t1 = t0 + op_duration - return t0, t1 + self._update_idles(node, t0, t1) + + def _update_idles(self, node, t0, t1): + for bit in node.qargs: + self._idle_after[bit] = t1 + + self._node_start_time[node] = t0 From bfcfe2e7d19e8c055ce894e125cdddb4f0e496ef Mon Sep 17 00:00:00 2001 From: Thomas Alexander Date: Mon, 27 Jun 2022 11:12:42 -0300 Subject: [PATCH 09/70] Add block based scheduling and updated tests for dynamic circuits type scheduling. --- .../dynamic_circuits/base_padding.py | 230 +++++++++++++++++ .../dynamic_circuits/pad_delay.py | 76 ++++++ .../dynamic_circuits/schedule.py | 28 ++- test/dynamic_circuits/test_schedule.py | 233 ++++++------------ 4 files changed, 398 insertions(+), 169 deletions(-) create mode 100644 qiskit_ibm_provider/dynamic_circuits/base_padding.py create mode 100644 qiskit_ibm_provider/dynamic_circuits/pad_delay.py diff --git a/qiskit_ibm_provider/dynamic_circuits/base_padding.py b/qiskit_ibm_provider/dynamic_circuits/base_padding.py new file mode 100644 index 000000000..736b5ab95 --- /dev/null +++ b/qiskit_ibm_provider/dynamic_circuits/base_padding.py @@ -0,0 +1,230 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2022. +# +# 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. + +"""Padding pass to fill timeslots for the dynamic circuits backends.""" + +from typing import List, Optional, Union + +from qiskit.circuit import Qubit, Clbit, Instruction +from qiskit.circuit.delay import Delay +from qiskit.dagcircuit import DAGCircuit, DAGNode +from qiskit.transpiler.basepasses import TransformationPass +from qiskit.transpiler.exceptions import TranspilerError + + +class BasePadding(TransformationPass): + """The base class of padding pass. + + This pass requires one of scheduling passes to be executed before itself. + Since there are multiple scheduling strategies, the selection of scheduling + pass is left in the hands of the pass manager designer. + Once a scheduling analysis pass is run, ``node_start_time`` is generated + in the :attr:`property_set`. This information is represented by a python dictionary of + the expected instruction execution times keyed on the node instances. + Entries in the dictionary are only created for non-delay nodes. + The padding pass expects all ``DAGOpNode`` in the circuit to be scheduled. + + This base class doesn't define any sequence to interleave, but it manages + the location where the sequence is inserted, and provides a set of information necessary + to construct the proper sequence. Thus, a subclass of this pass just needs to implement + :meth:`_pad` method, in which the subclass constructs a circuit block to insert. + This mechanism removes lots of boilerplate logic to manage whole DAG circuits. + + Note that padding pass subclasses should define interleaving sequences satisfying: + + - Interleaved sequence does not change start time of other nodes + - Interleaved sequence should have total duration of the provided ``time_interval``. + + Any manipulation violating these constraints may prevent this base pass from correctly + tracking the start time of each instruction, + which may result in violation of hardware alignment constraints. + """ + + def run(self, dag: DAGCircuit): + """Run the padding pass on ``dag``. + + Args: + dag: DAG to be checked. + + Returns: + DAGCircuit: DAG with idle time filled with instructions. + + Raises: + TranspilerError: When a particular node is not scheduled, likely some transform pass + is inserted before this node is called. + """ + self._pre_runhook(dag) + + node_start_time = self.property_set["node_start_time"].copy() + + new_dag = DAGCircuit() + for qreg in dag.qregs.values(): + new_dag.add_qreg(qreg) + for creg in dag.cregs.values(): + new_dag.add_creg(creg) + + # Update start time dictionary for the new_dag. + # This information may be used for further scheduling tasks, + # but this is immediately invalidated becasue node id is updated in the new_dag. + self.property_set["node_start_time"].clear() + + new_dag.name = dag.name + new_dag.metadata = dag.metadata + new_dag.unit = self.property_set["time_unit"] + new_dag.calibrations = dag.calibrations + new_dag.global_phase = dag.global_phase + + idle_after = {bit: 0 for bit in dag.qubits} + + # Compute fresh circuit duration from the node start time dictionary and op duration. + # Note that pre-scheduled duration may change within the alignment passes, i.e. + # if some instruction time t0 violating the hardware alignment constraint, + # the alignment pass may delay t0 and accordingly the circuit duration changes. + circuit_duration = 0 + for node in dag.topological_op_nodes(): + if node in node_start_time: + block_idx, t0 = node_start_time[node] + t1 = t0 + node.op.duration + circuit_duration = max(circuit_duration, t1) + + if isinstance(node.op, Delay): + # The padding class considers a delay instruction as idle time + # rather than instruction. Delay node is removed so that + # we can extract non-delay predecessors. + dag.remove_op_node(node) + continue + + for bit in node.qargs: + + # Fill idle time with some sequence + if t0 - idle_after[bit] > 0: + # Find previous node on the wire, i.e. always the latest node on the wire + prev_node = next(new_dag.predecessors(new_dag.output_map[bit])) + self._pad( + dag=new_dag, + block_idx=block_idx, + qubit=bit, + t_start=idle_after[bit], + t_end=t0, + next_node=node, + prev_node=prev_node, + ) + + idle_after[bit] = t1 + + self._apply_scheduled_op(new_dag, block_idx, t0, node.op, node.qargs, node.cargs) + else: + raise TranspilerError( + f"Operation {repr(node)} is likely added after the circuit is scheduled. " + "Schedule the circuit again if you transformed it." + ) + + # Add delays until the end of circuit. + for bit in new_dag.qubits: + if circuit_duration - idle_after[bit] > 0: + node = new_dag.output_map[bit] + prev_node = next(new_dag.predecessors(node)) + self._pad( + dag=new_dag, + block_idx=block_idx, + qubit=bit, + t_start=idle_after[bit], + t_end=circuit_duration, + next_node=node, + prev_node=prev_node, + ) + + new_dag.duration = circuit_duration + + return new_dag + + def _pre_runhook(self, dag: DAGCircuit): + """Extra routine inserted before running the padding pass. + + Args: + dag: DAG circuit on which the sequence is applied. + + Raises: + TranspilerError: If the whole circuit or instruction is not scheduled. + """ + if "node_start_time" not in self.property_set: + raise TranspilerError( + f"The input circuit {dag.name} is not scheduled. Call one of scheduling passes " + f"before running the {self.__class__.__name__} pass." + ) + + def _apply_scheduled_op( + self, + dag: DAGCircuit, + block_idx: int, + t_start: int, + oper: Instruction, + qubits: Union[Qubit, List[Qubit]], + clbits: Optional[Union[Clbit, List[Clbit]]] = None, + ): + """Add new operation to DAG with scheduled information. + + This is identical to apply_operation_back + updating the node_start_time propety. + + Args: + dag: DAG circuit on which the sequence is applied. + block_idx: Execution block index for this node. + t_start: Start time of new node. + oper: New operation that is added to the DAG circuit. + qubits: The list of qubits that the operation acts on. + clbits: The list of clbits that the operation acts on. + """ + if isinstance(qubits, Qubit): + qubits = [qubits] + if isinstance(clbits, Clbit): + clbits = [clbits] + + new_node = dag.apply_operation_back(oper, qargs=qubits, cargs=clbits) + self.property_set["node_start_time"][new_node] = (block_idx, t_start) + + def _pad( + self, + dag: DAGCircuit, + block_idx: int, + qubit: Qubit, + t_start: int, + t_end: int, + next_node: DAGNode, + prev_node: DAGNode, + ): + """Interleave instruction sequence in between two nodes. + + .. note:: + If a DAGOpNode is added here, it should update node_start_time property + in the property set so that the added node is also scheduled. + This is achieved by adding operation via :meth:`_apply_scheduled_op`. + + .. note:: + + This method doesn't check if the total duration of new DAGOpNode added here + is identical to the interval (``t_end - t_start``). + A developer of the pass must guarantee this is satisfied. + If the duration is greater than the interval, your circuit may be + compiled down to the target code with extra duration on the backend compiler, + which is then played normally without error. However, the outcome of your circuit + might be unexpected due to erroneous scheduling. + + Args: + dag: DAG circuit that sequence is applied. + block_idx: Execution block index for this node. + qubit: The wire that the sequence is applied on. + t_start: Absolute start time of this interval. + t_end: Absolute end time of this interval. + next_node: Node that follows the sequence. + prev_node: Node ahead of the sequence. + """ + raise NotImplementedError diff --git a/qiskit_ibm_provider/dynamic_circuits/pad_delay.py b/qiskit_ibm_provider/dynamic_circuits/pad_delay.py new file mode 100644 index 000000000..d816200d6 --- /dev/null +++ b/qiskit_ibm_provider/dynamic_circuits/pad_delay.py @@ -0,0 +1,76 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2022. +# +# 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. + +"""Padding pass to insert Delay into empty timeslots for dynamic circuit backends.""" + +from qiskit.circuit import Qubit +from qiskit.circuit.delay import Delay +from qiskit.dagcircuit import DAGCircuit, DAGNode, DAGOutNode + +from qiskit_ibm_provider.dynamic_circuits.base_padding import BasePadding + + +class PadDelay(BasePadding): + """Padding idle time with Delay instructions. + + Consecutive delays will be merged in the output of this pass. + + .. code-block::python + + durations = InstructionDurations([("x", None, 160), ("cx", None, 800)]) + + qc = QuantumCircuit(2) + qc.delay(100, 0) + qc.x(1) + qc.cx(0, 1) + + The ASAP-scheduled circuit output may become + + .. parsed-literal:: + + ┌────────────────┐ + q_0: ┤ Delay(160[dt]) ├──■── + └─────┬───┬──────┘┌─┴─┐ + q_1: ──────┤ X ├───────┤ X ├ + └───┘ └───┘ + + Note that the additional idle time of 60dt on the ``q_0`` wire coming from the duration difference + between ``Delay`` of 100dt (``q_0``) and ``XGate`` of 160 dt (``q_1``) is absorbed in + the delay instruction on the ``q_0`` wire, i.e. in total 160 dt. + + See :class:`BasePadding` pass for details. + """ + + def __init__(self, fill_very_end: bool = True): + """Create new padding delay pass. + + Args: + fill_very_end: Set ``True`` to fill the end of circuit with delay. + """ + super().__init__() + self.fill_very_end = fill_very_end + + def _pad( + self, + block_idx: int, + dag: DAGCircuit, + qubit: Qubit, + t_start: int, + t_end: int, + next_node: DAGNode, + prev_node: DAGNode, + ): + if not self.fill_very_end and isinstance(next_node, DAGOutNode): + return + + time_interval = t_end - t_start + self._apply_scheduled_op(dag, block_idx, t_start, Delay(time_interval, dag.unit), qubit) diff --git a/qiskit_ibm_provider/dynamic_circuits/schedule.py b/qiskit_ibm_provider/dynamic_circuits/schedule.py index 039f882bb..babc338d1 100644 --- a/qiskit_ibm_provider/dynamic_circuits/schedule.py +++ b/qiskit_ibm_provider/dynamic_circuits/schedule.py @@ -47,8 +47,7 @@ def __init__(self, durations: qiskit.transpiler.instruction_durations.Instructio self._dag = None - self._conditional_latency = 0 - self._clbit_write_latency = 0 + self._current_block_idx = 0 self._node_start_time = None self._idle_after = None @@ -85,7 +84,7 @@ def _init_run(self, dag): self._clbit_write_latency = self.property_set.get("clbit_write_latency", 0) self._node_start_time = dict() - self._idle_after = {q: 0 for q in dag.qubits + dag.clbits} + self._idle_after = {q: (0, 0) for q in dag.qubits + dag.clbits} self._bit_indices = {q: index for index, q in enumerate(dag.qubits)} def _get_node_duration(self, node): @@ -122,9 +121,9 @@ def _visit_conditional_node(self, node): # Special processing required to resolve conditional scheduling dependencies op_duration = self._get_node_duration(node) - t0q = max(self._idle_after[q] for q in node.qargs) + t0q = max(self._idle_after[q][1] for q in node.qargs) # conditional is bit tricky due to conditional_latency - t0c = max(self._idle_after[bit] for bit in node.op.condition_bits) + t0c = max(self._idle_after[bit][1] for bit in node.op.condition_bits) if t0q > t0c: # This is situation something like below # @@ -144,7 +143,7 @@ def _visit_conditional_node(self, node): t1c = t0c + self._conditional_latency for bit in node.op.condition_bits: # Lock clbit until state is read - self._idle_after[bit] = t1c + self._idle_after[bit] = (0, t1c) # It starts after register read access t0 = max(t0q, t1c) @@ -166,25 +165,32 @@ def _visit_measure(self, node): op_duration = self._get_node_duration(node) # measure instruction handling is bit tricky due to clbit_write_latency - t0q = max(self._idle_after[q] for q in node.qargs) + t0q = max(self._idle_after[q][1] for q in node.qargs) t0 = t0q t1 = t0 + op_duration for clbit in node.cargs: - self._idle_after[clbit] = t1 + self._idle_after[clbit] = (0, t1) self._update_idles(node, t0, t1) + #self._begin_new_circuit_block() def _visit_generic(self, node): """Visit a generic node such as a gate or barrier.""" op_duration = self._get_node_duration(node) # It happens to be directives such as barrier - t0 = max(self._idle_after[bit] for bit in node.qargs + node.cargs) + t0 = max(self._idle_after[bit][1] for bit in node.qargs + node.cargs) t1 = t0 + op_duration self._update_idles(node, t0, t1) def _update_idles(self, node, t0, t1): for bit in node.qargs: - self._idle_after[bit] = t1 + self._idle_after[bit] = (self._current_block_idx, t1) - self._node_start_time[node] = t0 + self._node_start_time[node] = (self._current_block_idx, t0) + + def _begin_new_circuit_block(self): + """Create a new timed circuit block completing the previous block. + + """ + self._current_block_idx += 1 diff --git a/test/dynamic_circuits/test_schedule.py b/test/dynamic_circuits/test_schedule.py index 8e805e6d8..8eab9ba5b 100644 --- a/test/dynamic_circuits/test_schedule.py +++ b/test/dynamic_circuits/test_schedule.py @@ -26,6 +26,7 @@ from qiskit.transpiler.passmanager import PassManager from qiskit.transpiler.exceptions import TranspilerError +from qiskit_ibm_provider.dynamic_circuits.pad_delay import PadDelay from qiskit_ibm_provider.dynamic_circuits.schedule import DynamicCircuitScheduleAnalysis @ddt @@ -72,23 +73,26 @@ def test_classically_controlled_gate_after_measure(self): def test_measure_after_measure(self): """Test if schedules circuits with measure after measure with a common clbit. - See: https://github.com/Qiskit/qiskit-terra/issues/7654 + + Note: There is no delay to write into the same clbit with IBM backends. + (input) - ┌───┐┌─┐ - q_0: ┤ X ├┤M├─── - └───┘└╥┘┌─┐ - q_1: ──────╫─┤M├ - ║ └╥┘ - c: 1/══════╩══╩═ - 0 0 + ┌───┐┌─┐ + q_0: ┤ X ├┤M├─── + └───┘└╥┘┌─┐ + q_1: ──────╫─┤M├ + ║ └╥┘ + c: 1/══════╩══╩═ + 0 0 (scheduled) - ┌───┐ ┌─┐┌─────────────────┐ - q_0: ───────┤ X ├───────┤M├┤ Delay(1000[dt]) ├ - ┌──────┴───┴──────┐└╥┘└───────┬─┬───────┘ - q_1: ┤ Delay(1200[dt]) ├─╫─────────┤M├──────── - └─────────────────┘ ║ └╥┘ - c: 1/════════════════════╩══════════╩═════════ - 0 0 + ┌───┐ ┌─┐ ░ + q_0: ──────┤ X ├───────┤M├────░─ + ┌─────┴───┴──────┐└╥┘┌─┐ ░ + q_1: ┤ Delay(200[dt]) ├─╫─┤M├─░─ + └────────────────┘ ║ └╥┘ ░ + c: 1/═══════════════════╩══╩════ + 0 0 + """ qc = QuantumCircuit(2, 1) qc.x(0) @@ -101,11 +105,10 @@ def test_measure_after_measure(self): expected = QuantumCircuit(2, 1) expected.x(0) + expected.delay(200, 1) expected.measure(0, 0) - expected.delay(1200, 1) expected.measure(1, 0) - expected.delay(1000, 0) - + expected.barrier() self.assertEqual(expected, scheduled) def test_c_if_on_different_qubits(self): @@ -154,24 +157,32 @@ def test_c_if_on_different_qubits(self): def test_shorter_measure_after_measure(self): """Test if schedules circuits with shorter measure after measure with a common clbit. + + Note: For dynamic circuits support we currently group measurements to start at the same time which in turn trigger + the end of a block. + (input) - ┌─┐ - q_0: ┤M├─── - └╥┘┌─┐ - q_1: ─╫─┤M├ - ║ └╥┘ - c: 1/═╩══╩═ - 0 0 + ┌─┐ + q_0: ┤M├─── + └╥┘┌─┐ + q_1: ─╫─┤M├ + ║ └╥┘ + q_2: ─╫──╫─ + ║ ║ + c: 1/═╩══╩═ + 0 0 (scheduled) - ┌─┐┌────────────────┐ - q_0: ───────────────────┤M├┤ Delay(700[dt]) ├ - ┌─────────────────┐└╥┘└──────┬─┬───────┘ - q_1: ┤ Delay(1000[dt]) ├─╫────────┤M├──────── - └─────────────────┘ ║ └╥┘ - c: 1/════════════════════╩═════════╩═════════ - 0 0 + ┌─┐ ░ + q_0: ┤M├─────────────────────────────────────────░─ + └╥┘ ┌─┐┌────────────────┐ ░ + q_1: ─╫────────────────────┤M├┤ Delay(300[dt]) ├─░─ + ║ ┌─────────────────┐└╥┘└────────────────┘ ░ + q_2: ─╫─┤ Delay(1000[dt]) ├─╫────────────────────░─ + ║ └─────────────────┘ ║ ░ + c: 1/═╩═════════════════════╩══════════════════════ + 0 0 """ - qc = QuantumCircuit(2, 1) + qc = QuantumCircuit(3, 1) qc.measure(0, 0) qc.measure(1, 0) @@ -179,36 +190,41 @@ def test_shorter_measure_after_measure(self): pm = PassManager([DynamicCircuitScheduleAnalysis(durations), PadDelay()]) scheduled = pm.run(qc) - expected = QuantumCircuit(2, 1) + expected = QuantumCircuit(3, 1) expected.measure(0, 0) - expected.delay(1000, 1) expected.measure(1, 0) - expected.delay(700, 0) + expected.delay(300, 1) + expected.delay(1000, 2) + expected.barrier() self.assertEqual(expected, scheduled) def test_measure_after_c_if(self): """Test if schedules circuits with c_if after measure with a common clbit. + + Note: This test is not yet correct as we should schedule the conditional block + qubits with delays as well. + (input) - ┌─┐ - q_0: ┤M├────────────── - └╥┘ ┌───┐ - q_1: ─╫────┤ X ├────── - ║ └─╥─┘ ┌─┐ - q_2: ─╫──────╫─────┤M├ - ║ ┌────╨────┐└╥┘ - c: 1/═╩═╡ c_0 = T ╞═╩═ - 0 └─────────┘ 0 + ┌─┐ + q_0: ┤M├────────────── + └╥┘ ┌───┐ + q_1: ─╫────┤ X ├────── + ║ └─╥─┘ ┌─┐ + q_2: ─╫──────╫─────┤M├ + ║ ┌────╨────┐└╥┘ + c: 1/═╩═╡ c_0 = T ╞═╩═ + 0 └─────────┘ 0 (scheduled) - ┌─┐┌─────────────────┐ - q_0: ───────────────────┤M├┤ Delay(1000[dt]) ├────────────────── - ┌─────────────────┐└╥┘└──────┬───┬──────┘┌────────────────┐ - q_1: ┤ Delay(1000[dt]) ├─╫────────┤ X ├───────┤ Delay(800[dt]) ├ - ├─────────────────┤ ║ └─╥─┘ └──────┬─┬───────┘ - q_2: ┤ Delay(1000[dt]) ├─╫──────────╫────────────────┤M├──────── - └─────────────────┘ ║ ┌────╨────┐ └╥┘ - c: 1/════════════════════╩═════╡ c_0=0x1 ╞════════════╩═════════ - 0 └─────────┘ 0 + ┌─┐ ░ ░ ┌─────────────────┐ + q_0: ───────────────────┤M├─░─────────────░─┤ Delay(1000[dt]) ├ + ┌─────────────────┐└╥┘ ░ ┌───┐ ░ ├─────────────────┤ + q_1: ┤ Delay(1000[dt]) ├─╫──░────┤ X ├────░─┤ Delay(1000[dt]) ├ + ├─────────────────┤ ║ ░ └─╥─┘ ░ └───────┬─┬───────┘ + q_2: ┤ Delay(1000[dt]) ├─╫──░──────╫──────░─────────┤M├──────── + └─────────────────┘ ║ ░ ┌────╨────┐ ░ └╥┘ + c: 1/════════════════════╩════╡ c_0=0x1 ╞════════════╩═════════ + 0 └─────────┘ 0 """ qc = QuantumCircuit(3, 1) qc.measure(0, 0) @@ -223,10 +239,12 @@ def test_measure_after_c_if(self): expected.delay(1000, 1) expected.delay(1000, 2) expected.measure(0, 0) - expected.x(1).c_if(0, 1) - expected.measure(2, 0) + expected.barrier() + expected.x(1).c_if(0, 1) # Not yet correct as we should insert delays for idle qubits in conditional. + expected.barrier() expected.delay(1000, 0) - expected.delay(800, 1) + expected.measure(2, 0) + expected.delay(1000, 1) self.assertEqual(expected, scheduled) @@ -414,107 +432,6 @@ def test_active_reset_circuit(self, write_lat, cond_lat): self.assertEqual(expected, actual_sched) - def test_random_complicated_circuit(self): - """Test scheduling complicated circuit with control flow. - (input) - ┌────────────────┐ ┌───┐ ░ ┌───┐ » - q_0: ┤ Delay(100[dt]) ├───┤ X ├────░──────────────────┤ X ├───» - └────────────────┘ └─╥─┘ ░ ┌───┐ └─╥─┘ » - q_1: ───────────────────────╫──────░───────┤ X ├────────╫─────» - ║ ░ ┌─┐ └─╥─┘ ║ » - q_2: ───────────────────────╫──────░─┤M├─────╫──────────╫─────» - ┌────╨────┐ ░ └╥┘┌────╨────┐┌────╨────┐» - c: 1/══════════════════╡ c_0=0x1 ╞════╩═╡ c_0=0x0 ╞╡ c_0=0x0 ╞» - └─────────┘ 0 └─────────┘└─────────┘» - « ┌────────────────┐┌───┐ - «q_0: ┤ Delay(300[dt]) ├┤ X ├─────■───── - « └────────────────┘└───┘ ┌─┴─┐ - «q_1: ────────■─────────────────┤ X ├─── - « ┌─┴─┐ ┌─┐ └─╥─┘ - «q_2: ──────┤ X ├────────┤M├──────╫───── - « └───┘ └╥┘ ┌────╨────┐ - «c: 1/════════════════════╩══╡ c_0=0x0 ╞ - « 0 └─────────┘ - (scheduled) duration = 2800 dt - ┌────────────────┐ ┌───┐ ░ ┌─────────────────┐ » - q_0: ┤ Delay(200[dt]) ├───┤ X ├────░─┤ Delay(1400[dt]) ├───────────» - ├────────────────┤ └─╥─┘ ░ ├─────────────────┤ ┌───┐ » - q_1: ┤ Delay(300[dt]) ├─────╫──────░─┤ Delay(1200[dt]) ├───┤ X ├───» - ├────────────────┤ ║ ░ └───────┬─┬───────┘ └─╥─┘ » - q_2: ┤ Delay(300[dt]) ├─────╫──────░─────────┤M├─────────────╫─────» - └────────────────┘┌────╨────┐ ░ └╥┘ ┌────╨────┐» - c: 1/══════════════════╡ c_0=0x1 ╞════════════╩═════════╡ c_0=0x0 ╞» - └─────────┘ 0 └─────────┘» - « ┌───┐ ┌────────────────┐ ┌───┐ » - «q_0: ─────────────────────┤ X ├───┤ Delay(300[dt]) ├──────┤ X ├───────» - « └─╥─┘ └────────────────┘┌─────┴───┴──────┐» - «q_1: ───────────────────────╫─────────────■─────────┤ Delay(400[dt]) ├» - « ┌────────────────┐ ║ ┌─┴─┐ ├────────────────┤» - «q_2: ┤ Delay(300[dt]) ├─────╫───────────┤ X ├───────┤ Delay(300[dt]) ├» - « └────────────────┘┌────╨────┐ └───┘ └────────────────┘» - «c: 1/══════════════════╡ c_0=0x0 ╞════════════════════════════════════» - « └─────────┘ » - « ┌────────────────┐ - «q_0: ─────■─────┤ Delay(700[dt]) ├ - « ┌─┴─┐ ├────────────────┤ - «q_1: ───┤ X ├───┤ Delay(700[dt]) ├ - « └─╥─┘ └──────┬─┬───────┘ - «q_2: ─────╫────────────┤M├──────── - « ┌────╨────┐ └╥┘ - «c: 1/╡ c_0=0x0 ╞════════╩═════════ - « └─────────┘ 0 - """ - qc = QuantumCircuit(3, 1) - qc.delay(100, 0) - qc.x(0).c_if(0, 1) - qc.barrier() - qc.measure(2, 0) - qc.x(1).c_if(0, 0) - qc.x(0).c_if(0, 0) - qc.delay(300, 0) - qc.cx(1, 2) - qc.x(0) - qc.cx(0, 1).c_if(0, 0) - qc.measure(2, 0) - - durations = InstructionDurations( - [("x", None, 100), ("measure", None, 1000), ("cx", None, 200)] - ) - - actual_sched = PassManager( - [ - SetIOLatency(clbit_write_latency=100, conditional_latency=200), - DynamicCircuitScheduleAnalysis(durations), - PadDelay(), - ] - ).run(qc) - - expected_sched = QuantumCircuit(3, 1) - expected_sched.delay(200, 0) # due to conditional latency of 200dt - expected_sched.delay(300, 1) - expected_sched.delay(300, 2) - expected_sched.x(0).c_if(0, 1) - expected_sched.barrier() - expected_sched.delay(1400, 0) - expected_sched.delay(1200, 1) - expected_sched.measure(2, 0) - expected_sched.x(1).c_if(0, 0) - expected_sched.x(0).c_if(0, 0) - expected_sched.delay(300, 0) - expected_sched.x(0) - expected_sched.delay(300, 2) - expected_sched.cx(1, 2) - expected_sched.delay(400, 1) - expected_sched.cx(0, 1).c_if(0, 0) - expected_sched.delay(700, 0) # creg is released at t0 of cx(0,1).c_if(0,0) - expected_sched.delay( - 700, 1 - ) # no creg write until 100dt. thus measure can move left by 300dt. - expected_sched.delay(300, 2) - expected_sched.measure(2, 0) - self.assertEqual(expected_sched, actual_sched) - self.assertEqual(actual_sched.duration, 3100) - def test_dag_introduces_extra_dependency_between_conditionals(self): """Test dependency between conditional operations in the scheduling. In the below example circuit, the conditional x on q1 could start at time 0, From 9a0955c725f0eafd11826f4b6f83c054f23b287c Mon Sep 17 00:00:00 2001 From: Thomas Alexander Date: Tue, 5 Jul 2022 09:50:39 -0300 Subject: [PATCH 10/70] Refactor padder class. --- .../dynamic_circuits/base_padding.py | 201 ++++++++++-------- .../dynamic_circuits/pad_delay.py | 3 +- .../dynamic_circuits/schedule.py | 47 +++- test/dynamic_circuits/test_schedule.py | 67 +++++- test/dynamic_circuits/test_tests.py | 20 -- 5 files changed, 215 insertions(+), 123 deletions(-) delete mode 100644 test/dynamic_circuits/test_tests.py diff --git a/qiskit_ibm_provider/dynamic_circuits/base_padding.py b/qiskit_ibm_provider/dynamic_circuits/base_padding.py index 736b5ab95..2eeeb8ff1 100644 --- a/qiskit_ibm_provider/dynamic_circuits/base_padding.py +++ b/qiskit_ibm_provider/dynamic_circuits/base_padding.py @@ -49,6 +49,16 @@ class BasePadding(TransformationPass): which may result in violation of hardware alignment constraints. """ + def __init__(self): + self._node_start_time = None + self._idle_after = None + self._dag = None + self._circuit_duration = 0 + self._current_block_idx = 0 + + super().__init__() + + def run(self, dag: DAGCircuit): """Run the padding pass on ``dag``. @@ -64,88 +74,50 @@ def run(self, dag: DAGCircuit): """ self._pre_runhook(dag) - node_start_time = self.property_set["node_start_time"].copy() + self._node_start_time = self.property_set["node_start_time"].copy() + self._idle_after = {bit: 0 for bit in dag.qubits} + self._current_block_idx = 0 - new_dag = DAGCircuit() + # Prepare DAG to pad + self._dag = DAGCircuit() for qreg in dag.qregs.values(): - new_dag.add_qreg(qreg) + self._dag.add_qreg(qreg) for creg in dag.cregs.values(): - new_dag.add_creg(creg) + self._dag.add_creg(creg) # Update start time dictionary for the new_dag. # This information may be used for further scheduling tasks, # but this is immediately invalidated becasue node id is updated in the new_dag. self.property_set["node_start_time"].clear() - new_dag.name = dag.name - new_dag.metadata = dag.metadata - new_dag.unit = self.property_set["time_unit"] - new_dag.calibrations = dag.calibrations - new_dag.global_phase = dag.global_phase + self._dag.name = dag.name + self._dag.metadata = dag.metadata + self._dag.unit = self.property_set["time_unit"] + self._dag.calibrations = dag.calibrations + self._dag.global_phase = dag.global_phase - idle_after = {bit: 0 for bit in dag.qubits} # Compute fresh circuit duration from the node start time dictionary and op duration. # Note that pre-scheduled duration may change within the alignment passes, i.e. # if some instruction time t0 violating the hardware alignment constraint, # the alignment pass may delay t0 and accordingly the circuit duration changes. - circuit_duration = 0 + self._circuit_duration = 0 for node in dag.topological_op_nodes(): - if node in node_start_time: - block_idx, t0 = node_start_time[node] - t1 = t0 + node.op.duration - circuit_duration = max(circuit_duration, t1) - + if node in self._node_start_time: if isinstance(node.op, Delay): - # The padding class considers a delay instruction as idle time - # rather than instruction. Delay node is removed so that - # we can extract non-delay predecessors. - dag.remove_op_node(node) - continue - - for bit in node.qargs: - - # Fill idle time with some sequence - if t0 - idle_after[bit] > 0: - # Find previous node on the wire, i.e. always the latest node on the wire - prev_node = next(new_dag.predecessors(new_dag.output_map[bit])) - self._pad( - dag=new_dag, - block_idx=block_idx, - qubit=bit, - t_start=idle_after[bit], - t_end=t0, - next_node=node, - prev_node=prev_node, - ) - - idle_after[bit] = t1 - - self._apply_scheduled_op(new_dag, block_idx, t0, node.op, node.qargs, node.cargs) + self._visit_delay(node) + else: + self._visit_generic(node) + else: raise TranspilerError( f"Operation {repr(node)} is likely added after the circuit is scheduled. " "Schedule the circuit again if you transformed it." ) - # Add delays until the end of circuit. - for bit in new_dag.qubits: - if circuit_duration - idle_after[bit] > 0: - node = new_dag.output_map[bit] - prev_node = next(new_dag.predecessors(node)) - self._pad( - dag=new_dag, - block_idx=block_idx, - qubit=bit, - t_start=idle_after[bit], - t_end=circuit_duration, - next_node=node, - prev_node=prev_node, - ) - - new_dag.duration = circuit_duration + self._dag.duration = self._circuit_duration - return new_dag + return self._dag def _pre_runhook(self, dag: DAGCircuit): """Extra routine inserted before running the padding pass. @@ -162,38 +134,8 @@ def _pre_runhook(self, dag: DAGCircuit): f"before running the {self.__class__.__name__} pass." ) - def _apply_scheduled_op( - self, - dag: DAGCircuit, - block_idx: int, - t_start: int, - oper: Instruction, - qubits: Union[Qubit, List[Qubit]], - clbits: Optional[Union[Clbit, List[Clbit]]] = None, - ): - """Add new operation to DAG with scheduled information. - - This is identical to apply_operation_back + updating the node_start_time propety. - - Args: - dag: DAG circuit on which the sequence is applied. - block_idx: Execution block index for this node. - t_start: Start time of new node. - oper: New operation that is added to the DAG circuit. - qubits: The list of qubits that the operation acts on. - clbits: The list of clbits that the operation acts on. - """ - if isinstance(qubits, Qubit): - qubits = [qubits] - if isinstance(clbits, Clbit): - clbits = [clbits] - - new_node = dag.apply_operation_back(oper, qargs=qubits, cargs=clbits) - self.property_set["node_start_time"][new_node] = (block_idx, t_start) - def _pad( self, - dag: DAGCircuit, block_idx: int, qubit: Qubit, t_start: int, @@ -219,7 +161,6 @@ def _pad( might be unexpected due to erroneous scheduling. Args: - dag: DAG circuit that sequence is applied. block_idx: Execution block index for this node. qubit: The wire that the sequence is applied on. t_start: Absolute start time of this interval. @@ -228,3 +169,85 @@ def _pad( prev_node: Node ahead of the sequence. """ raise NotImplementedError + + def _visit_delay(self, node): + """The padding class considers a delay instruction as idle time + rather than instruction. Delay node is not added so that + we can extract non-delay predecessors. + """ + pass + + def _visit_generic(self, node): + """Visit a generic node to pad.""" + block_idx, t0 = self._node_start_time[node] + + # Trigger the end of a block + if block_idx > self._current_block_idx: + self._pad_until_block_end(self._circuit_duration, self._current_block_idx) + + # Now set the current block index. + self._current_block_idx = block_idx + + + t1 = t0 + node.op.duration + self._circuit_duration = max(self._circuit_duration, t1) + for bit in node.qargs: + + # Fill idle time with some sequence + if t0 - self._idle_after[bit] > 0: + # Find previous node on the wire, i.e. always the latest node on the wire + prev_node = next(self._dag.predecessors(self._dag.output_map[bit])) + self._pad( + block_idx=block_idx, + qubit=bit, + t_start=self._idle_after[bit], + t_end=t0, + next_node=node, + prev_node=prev_node, + ) + + self._idle_after[bit] = t1 + + self._apply_scheduled_op(block_idx, t0, node.op, node.qargs, node.cargs) + + def _pad_until_block_end(self, block_duration, block_idx): + # Add delays until the end of circuit. + for bit in self._dag.qubits: + if block_duration - self._idle_after[bit] > 0: + node = self._dag.output_map[bit] + prev_node = next(self._dag.predecessors(node)) + self._pad( + block_idx=block_idx, + qubit=bit, + t_start=self._idle_after[bit], + t_end=block_duration, + next_node=node, + prev_node=prev_node, + ) + + def _apply_scheduled_op( + self, + block_idx: int, + t_start: int, + oper: Instruction, + qubits: Union[Qubit, List[Qubit]], + clbits: Optional[Union[Clbit, List[Clbit]]] = None, + ): + """Add new operation to DAG with scheduled information. + + This is identical to apply_operation_back + updating the node_start_time propety. + + Args: + block_idx: Execution block index for this node. + t_start: Start time of new node. + oper: New operation that is added to the DAG circuit. + qubits: The list of qubits that the operation acts on. + clbits: The list of clbits that the operation acts on. + """ + if isinstance(qubits, Qubit): + qubits = [qubits] + if isinstance(clbits, Clbit): + clbits = [clbits] + + new_node = self._dag.apply_operation_back(oper, qargs=qubits, cargs=clbits) + self.property_set["node_start_time"][new_node] = (block_idx, t_start) diff --git a/qiskit_ibm_provider/dynamic_circuits/pad_delay.py b/qiskit_ibm_provider/dynamic_circuits/pad_delay.py index d816200d6..ca081ff4d 100644 --- a/qiskit_ibm_provider/dynamic_circuits/pad_delay.py +++ b/qiskit_ibm_provider/dynamic_circuits/pad_delay.py @@ -62,7 +62,6 @@ def __init__(self, fill_very_end: bool = True): def _pad( self, block_idx: int, - dag: DAGCircuit, qubit: Qubit, t_start: int, t_end: int, @@ -73,4 +72,4 @@ def _pad( return time_interval = t_end - t_start - self._apply_scheduled_op(dag, block_idx, t_start, Delay(time_interval, dag.unit), qubit) + self._apply_scheduled_op(block_idx, t_start, Delay(time_interval, self._dag.unit), qubit) diff --git a/qiskit_ibm_provider/dynamic_circuits/schedule.py b/qiskit_ibm_provider/dynamic_circuits/schedule.py index babc338d1..6be556020 100644 --- a/qiskit_ibm_provider/dynamic_circuits/schedule.py +++ b/qiskit_ibm_provider/dynamic_circuits/schedule.py @@ -12,6 +12,8 @@ """Scheduler for dynamic circuit backends.""" +import itertools + import qiskit from qiskit.circuit import Measure from qiskit.transpiler.exceptions import TranspilerError @@ -51,6 +53,7 @@ def __init__(self, durations: qiskit.transpiler.instruction_durations.Instructio self._node_start_time = None self._idle_after = None + self._current_block_measures = None self._bit_indices = None super().__init__(durations) @@ -70,6 +73,8 @@ def run(self, dag): for node in dag.topological_op_nodes(): self._visit_node(node) + for node, start_time in self._node_start_time.items(): + print(repr(node), start_time) self.property_set["node_start_time"] = self._node_start_time def _init_run(self, dag): @@ -85,6 +90,7 @@ def _init_run(self, dag): self._node_start_time = dict() self._idle_after = {q: (0, 0) for q in dag.qubits + dag.clbits} + self._current_block_measures = set() self._bit_indices = {q: index for index, q in enumerate(dag.qubits)} def _get_node_duration(self, node): @@ -143,7 +149,7 @@ def _visit_conditional_node(self, node): t1c = t0c + self._conditional_latency for bit in node.op.condition_bits: # Lock clbit until state is read - self._idle_after[bit] = (0, t1c) + self._idle_after[bit] = (self._current_block_idx, t1c) # It starts after register read access t0 = max(t0q, t1c) @@ -164,15 +170,31 @@ def _visit_measure(self, node): """ op_duration = self._get_node_duration(node) - # measure instruction handling is bit tricky due to clbit_write_latency - t0q = max(self._idle_after[q][1] for q in node.qargs) - t0 = t0q - t1 = t0 + op_duration - for clbit in node.cargs: - self._idle_after[clbit] = (0, t1) + current_block_measure_qargs = self._current_block_measure_qargs() + measure_qargs = set(node.qargs) + + t0q = max(self._idle_after[q][1] for q in measure_qargs) + + # If the measurement qubits overlap, we need to start a new scheduling block. + if current_block_measure_qargs & measure_qargs: + self._begin_new_circuit_block() + t0q = 0 + # Otherwise we need to increment all measurements to start at the same time within the block. + else: + t0q = max(itertools.chain([t0q], (self._node_start_time[measure][1] for measure in self._current_block_measures))) + + # Insert this measure into the block + self._current_block_measures.add(node) + + # now update all measure qarg times. + + self._current_block_measures.add(node) + + for measure in self._current_block_measures: + t0 = t0q + t1 = t0 + self._get_node_duration(measure) + self._update_idles(measure, t0, t1) - self._update_idles(node, t0, t1) - #self._begin_new_circuit_block() def _visit_generic(self, node): """Visit a generic node such as a gate or barrier.""" @@ -187,6 +209,9 @@ def _update_idles(self, node, t0, t1): for bit in node.qargs: self._idle_after[bit] = (self._current_block_idx, t1) + for bit in node.cargs: + self._idle_after[bit] = (self._current_block_idx, t1) + self._node_start_time[node] = (self._current_block_idx, t0) def _begin_new_circuit_block(self): @@ -194,3 +219,7 @@ def _begin_new_circuit_block(self): """ self._current_block_idx += 1 + self._current_block_measures = set() + + def _current_block_measure_qargs(self): + return set(qarg for measure in self._current_block_measures for qarg in measure.qargs) diff --git a/test/dynamic_circuits/test_schedule.py b/test/dynamic_circuits/test_schedule.py index 8eab9ba5b..1692c865a 100644 --- a/test/dynamic_circuits/test_schedule.py +++ b/test/dynamic_circuits/test_schedule.py @@ -29,6 +29,7 @@ from qiskit_ibm_provider.dynamic_circuits.pad_delay import PadDelay from qiskit_ibm_provider.dynamic_circuits.schedule import DynamicCircuitScheduleAnalysis + @ddt class TestSchedulingAndPaddingPass(QiskitTestCase): """Tests the Scheduling passes""" @@ -111,6 +112,57 @@ def test_measure_after_measure(self): expected.barrier() self.assertEqual(expected, scheduled) + def test_measure_block_end(self): + """Tests that measures trigger the end of a scheduling block and that measurements are grouped by block. + + (input) + ┌───┐┌─┐ + q_0: ┤ X ├┤M├─── + └───┘└╥┘┌─┐ + q_1: ──────╫─┤M├ + ║ └╥┘ + c: 1/══════╩══╩═ + 0 0 + (scheduled) + ┌───┐ ┌─┐ ░ + q_0: ──────┤ X ├───────┤M├────░─ + ┌─────┴───┴──────┐└╥┘┌─┐ ░ + q_1: ┤ Delay(200[dt]) ├─╫─┤M├─░─ + └────────────────┘ ║ └╥┘ ░ + c: 1/═══════════════════╩══╩════ + 0 0 + + """ + qc = QuantumCircuit(3, 1) + qc.x(0) + qc.measure(0, 0) + qc.measure(1, 0) + qc.x(2) + qc.measure(1, 0) + qc.measure(2, 0) + + durations = InstructionDurations([("x", None, 200), ("measure", None, 1000)]) + pm = PassManager([DynamicCircuitScheduleAnalysis(durations), PadDelay()]) + scheduled = pm.run(qc) + + expected = QuantumCircuit(3, 1) + expected.x(0) + expected.delay(200, 1) + expected.x(2) + expected.delay(1000, 2) + expected.measure(0, 0) + expected.measure(1, 0) + expected.barrier() + expected.delay(1000, 0) + expected.measure(1, 0) + expected.measure(2, 0) + expected.barrier() + + + + import pdb;pdb.set_trace() + self.assertEqual(expected, scheduled) + def test_c_if_on_different_qubits(self): """Test if schedules circuits with `c_if`s on different qubits. (input) @@ -186,7 +238,9 @@ def test_shorter_measure_after_measure(self): qc.measure(0, 0) qc.measure(1, 0) - durations = InstructionDurations([("measure", [0], 1000), ("measure", [1], 700)]) + durations = InstructionDurations( + [("measure", [0], 1000), ("measure", [1], 700)] + ) pm = PassManager([DynamicCircuitScheduleAnalysis(durations), PadDelay()]) scheduled = pm.run(qc) @@ -197,6 +251,9 @@ def test_shorter_measure_after_measure(self): expected.delay(1000, 2) expected.barrier() + import pdb + pdb.set_trace() + self.assertEqual(expected, scheduled) def test_measure_after_c_if(self): @@ -240,7 +297,9 @@ def test_measure_after_c_if(self): expected.delay(1000, 2) expected.measure(0, 0) expected.barrier() - expected.x(1).c_if(0, 1) # Not yet correct as we should insert delays for idle qubits in conditional. + expected.x(1).c_if( + 0, 1 + ) # Not yet correct as we should insert delays for idle qubits in conditional. expected.barrier() expected.delay(1000, 0) expected.measure(2, 0) @@ -410,7 +469,9 @@ def test_active_reset_circuit(self, write_lat, cond_lat): actual_sched = PassManager( [ - SetIOLatency(clbit_write_latency=write_lat, conditional_latency=cond_lat), + SetIOLatency( + clbit_write_latency=write_lat, conditional_latency=cond_lat + ), DynamicCircuitScheduleAnalysis(durations), PadDelay(), ] diff --git a/test/dynamic_circuits/test_tests.py b/test/dynamic_circuits/test_tests.py deleted file mode 100644 index 91b42c978..000000000 --- a/test/dynamic_circuits/test_tests.py +++ /dev/null @@ -1,20 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2022. -# -# 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. - -from qiskit_ibm_provider import dynamic_circuits - -from ..ibm_test_case import IBMTestCase - -class TestBasicServerPaths(IBMTestCase): - - def test_tests(self): - self.assertEqual(dynamic_circuits.foo, 1) From 1744ae4e694c01554e8291c0b5935e4de956ab3f Mon Sep 17 00:00:00 2001 From: Thomas Alexander Date: Tue, 5 Jul 2022 11:55:44 -0300 Subject: [PATCH 11/70] Base padding is working. --- .../dynamic_circuits/base_padding.py | 72 ++++++++++++------- .../dynamic_circuits/schedule.py | 1 + 2 files changed, 47 insertions(+), 26 deletions(-) diff --git a/qiskit_ibm_provider/dynamic_circuits/base_padding.py b/qiskit_ibm_provider/dynamic_circuits/base_padding.py index 2eeeb8ff1..995c70694 100644 --- a/qiskit_ibm_provider/dynamic_circuits/base_padding.py +++ b/qiskit_ibm_provider/dynamic_circuits/base_padding.py @@ -15,6 +15,7 @@ from typing import List, Optional, Union from qiskit.circuit import Qubit, Clbit, Instruction +from qiskit.circuit.library import Barrier from qiskit.circuit.delay import Delay from qiskit.dagcircuit import DAGCircuit, DAGNode from qiskit.transpiler.basepasses import TransformationPass @@ -53,7 +54,7 @@ def __init__(self): self._node_start_time = None self._idle_after = None self._dag = None - self._circuit_duration = 0 + self._block_duration = 0 self._current_block_idx = 0 super().__init__() @@ -74,9 +75,37 @@ def run(self, dag: DAGCircuit): """ self._pre_runhook(dag) + + self._init_run(dag) + + # Compute fresh circuit duration from the node start time dictionary and op duration. + # Note that pre-scheduled duration may change within the alignment passes, i.e. + # if some instruction time t0 violating the hardware alignment constraint, + # the alignment pass may delay t0 and accordingly the circuit duration changes. + for node in dag.topological_op_nodes(): + if node in self._node_start_time: + if isinstance(node.op, Delay): + self._visit_delay(node) + else: + self._visit_generic(node) + + else: + raise TranspilerError( + f"Operation {repr(node)} is likely added after the circuit is scheduled. " + "Schedule the circuit again if you transformed it." + ) + + # terminate final block + self._terminate_block(self._block_duration, self._current_block_idx) + + return self._dag + + def _init_run(self, dag): + """Setup for initial run.""" self._node_start_time = self.property_set["node_start_time"].copy() self._idle_after = {bit: 0 for bit in dag.qubits} self._current_block_idx = 0 + self._block_duration = 0 # Prepare DAG to pad self._dag = DAGCircuit() @@ -97,28 +126,6 @@ def run(self, dag: DAGCircuit): self._dag.global_phase = dag.global_phase - # Compute fresh circuit duration from the node start time dictionary and op duration. - # Note that pre-scheduled duration may change within the alignment passes, i.e. - # if some instruction time t0 violating the hardware alignment constraint, - # the alignment pass may delay t0 and accordingly the circuit duration changes. - self._circuit_duration = 0 - for node in dag.topological_op_nodes(): - if node in self._node_start_time: - if isinstance(node.op, Delay): - self._visit_delay(node) - else: - self._visit_generic(node) - - else: - raise TranspilerError( - f"Operation {repr(node)} is likely added after the circuit is scheduled. " - "Schedule the circuit again if you transformed it." - ) - - self._dag.duration = self._circuit_duration - - return self._dag - def _pre_runhook(self, dag: DAGCircuit): """Extra routine inserted before running the padding pass. @@ -183,16 +190,16 @@ def _visit_generic(self, node): # Trigger the end of a block if block_idx > self._current_block_idx: - self._pad_until_block_end(self._circuit_duration, self._current_block_idx) + self._terminate_block(self._block_duration, self._current_block_idx) # Now set the current block index. self._current_block_idx = block_idx t1 = t0 + node.op.duration - self._circuit_duration = max(self._circuit_duration, t1) - for bit in node.qargs: + self._block_duration = max(self._block_duration, t1) + for bit in node.qargs: # Fill idle time with some sequence if t0 - self._idle_after[bit] > 0: # Find previous node on the wire, i.e. always the latest node on the wire @@ -210,6 +217,19 @@ def _visit_generic(self, node): self._apply_scheduled_op(block_idx, t0, node.op, node.qargs, node.cargs) + def _terminate_block(self, block_duration, block_idx): + """Terminate the end of a block scheduling region.""" + self._pad_until_block_end(block_duration, block_idx) + + # Terminate with a barrier to be clear timing is non-deterministic + # across the barrier. + self._dag.apply_operation_back(Barrier(self._dag.num_qubits()), self._dag.qubits, []) + + # Reset idles for the new block. + self._idle_after = {bit: 0 for bit in self._dag.qubits} + self._block_duration = 0 + + def _pad_until_block_end(self, block_duration, block_idx): # Add delays until the end of circuit. for bit in self._dag.qubits: diff --git a/qiskit_ibm_provider/dynamic_circuits/schedule.py b/qiskit_ibm_provider/dynamic_circuits/schedule.py index 6be556020..fa12dc43b 100644 --- a/qiskit_ibm_provider/dynamic_circuits/schedule.py +++ b/qiskit_ibm_provider/dynamic_circuits/schedule.py @@ -220,6 +220,7 @@ def _begin_new_circuit_block(self): """ self._current_block_idx += 1 self._current_block_measures = set() + self._idle_after = {q: (0, 0) for q in self._dag.qubits + self._dag.clbits} def _current_block_measure_qargs(self): return set(qarg for measure in self._current_block_measures for qarg in measure.qargs) From 4340be052cb1ffafc107c59bf2f78767021e5094 Mon Sep 17 00:00:00 2001 From: Thomas Alexander Date: Tue, 5 Jul 2022 11:57:01 -0300 Subject: [PATCH 12/70] Remove debug statements. --- test/dynamic_circuits/test_schedule.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/test/dynamic_circuits/test_schedule.py b/test/dynamic_circuits/test_schedule.py index 1692c865a..fe249abaa 100644 --- a/test/dynamic_circuits/test_schedule.py +++ b/test/dynamic_circuits/test_schedule.py @@ -158,9 +158,6 @@ def test_measure_block_end(self): expected.measure(2, 0) expected.barrier() - - - import pdb;pdb.set_trace() self.assertEqual(expected, scheduled) def test_c_if_on_different_qubits(self): @@ -251,9 +248,6 @@ def test_shorter_measure_after_measure(self): expected.delay(1000, 2) expected.barrier() - import pdb - pdb.set_trace() - self.assertEqual(expected, scheduled) def test_measure_after_c_if(self): From 9e2d5ae8477bb06115a92a7d9a79eea4751a484c Mon Sep 17 00:00:00 2001 From: Thomas Alexander Date: Tue, 5 Jul 2022 12:35:21 -0300 Subject: [PATCH 13/70] Update non conditional tests to pass. --- test/dynamic_circuits/test_schedule.py | 278 ++++--------------------- 1 file changed, 36 insertions(+), 242 deletions(-) diff --git a/test/dynamic_circuits/test_schedule.py b/test/dynamic_circuits/test_schedule.py index fe249abaa..a75f6274c 100644 --- a/test/dynamic_circuits/test_schedule.py +++ b/test/dynamic_circuits/test_schedule.py @@ -12,6 +12,7 @@ """Test the dynamic circuits scheduling analysis""" +import sched import unittest from ddt import ddt, data, unpack @@ -36,26 +37,7 @@ class TestSchedulingAndPaddingPass(QiskitTestCase): def test_classically_controlled_gate_after_measure(self): """Test if schedules circuits with c_if after measure with a common clbit. - See: https://github.com/Qiskit/qiskit-terra/issues/7654 - (input) - ┌─┐ - q_0: ┤M├─────────── - └╥┘ ┌───┐ - q_1: ─╫────┤ X ├─── - ║ └─╥─┘ - ║ ┌────╨────┐ - c: 1/═╩═╡ c_0 = T ╞ - 0 └─────────┘ - (scheduled) - ┌─┐┌────────────────┐ - q_0: ───────────────────┤M├┤ Delay(200[dt]) ├ - ┌─────────────────┐└╥┘└─────┬───┬──────┘ - q_1: ┤ Delay(1000[dt]) ├─╫───────┤ X ├─────── - └─────────────────┘ ║ └─╥─┘ - ║ ┌────╨────┐ - c: 1/════════════════════╩════╡ c_0=0x1 ╞════ - 0 └─────────┘ - """ + See: https://github.com/Qiskit/qiskit-terra/issues/7654""" qc = QuantumCircuit(2, 1) qc.measure(0, 0) qc.x(1).c_if(0, True) @@ -75,26 +57,7 @@ def test_classically_controlled_gate_after_measure(self): def test_measure_after_measure(self): """Test if schedules circuits with measure after measure with a common clbit. - Note: There is no delay to write into the same clbit with IBM backends. - - (input) - ┌───┐┌─┐ - q_0: ┤ X ├┤M├─── - └───┘└╥┘┌─┐ - q_1: ──────╫─┤M├ - ║ └╥┘ - c: 1/══════╩══╩═ - 0 0 - (scheduled) - ┌───┐ ┌─┐ ░ - q_0: ──────┤ X ├───────┤M├────░─ - ┌─────┴───┴──────┐└╥┘┌─┐ ░ - q_1: ┤ Delay(200[dt]) ├─╫─┤M├─░─ - └────────────────┘ ║ └╥┘ ░ - c: 1/═══════════════════╩══╩════ - 0 0 - - """ + Note: There is no delay to write into the same clbit with IBM backends.""" qc = QuantumCircuit(2, 1) qc.x(0) qc.measure(0, 0) @@ -113,26 +76,7 @@ def test_measure_after_measure(self): self.assertEqual(expected, scheduled) def test_measure_block_end(self): - """Tests that measures trigger the end of a scheduling block and that measurements are grouped by block. - - (input) - ┌───┐┌─┐ - q_0: ┤ X ├┤M├─── - └───┘└╥┘┌─┐ - q_1: ──────╫─┤M├ - ║ └╥┘ - c: 1/══════╩══╩═ - 0 0 - (scheduled) - ┌───┐ ┌─┐ ░ - q_0: ──────┤ X ├───────┤M├────░─ - ┌─────┴───┴──────┐└╥┘┌─┐ ░ - q_1: ┤ Delay(200[dt]) ├─╫─┤M├─░─ - └────────────────┘ ║ └╥┘ ░ - c: 1/═══════════════════╩══╩════ - 0 0 - - """ + """Tests that measures trigger the end of a scheduling block and that measurements are grouped by block.""" qc = QuantumCircuit(3, 1) qc.x(0) qc.measure(0, 0) @@ -161,30 +105,7 @@ def test_measure_block_end(self): self.assertEqual(expected, scheduled) def test_c_if_on_different_qubits(self): - """Test if schedules circuits with `c_if`s on different qubits. - (input) - ┌─┐ - q_0: ┤M├────────────────────── - └╥┘ ┌───┐ - q_1: ─╫────┤ X ├────────────── - ║ └─╥─┘ ┌───┐ - q_2: ─╫──────╫────────┤ X ├─── - ║ ║ └─╥─┘ - ║ ┌────╨────┐┌────╨────┐ - c: 1/═╩═╡ c_0 = T ╞╡ c_0 = T ╞ - 0 └─────────┘└─────────┘ - (scheduled) - ┌─┐┌────────────────┐ - q_0: ───────────────────┤M├┤ Delay(200[dt]) ├─────────── - ┌─────────────────┐└╥┘└─────┬───┬──────┘ - q_1: ┤ Delay(1000[dt]) ├─╫───────┤ X ├────────────────── - ├─────────────────┤ ║ └─╥─┘ ┌───┐ - q_2: ┤ Delay(1000[dt]) ├─╫─────────╫────────────┤ X ├─── - └─────────────────┘ ║ ║ └─╥─┘ - ║ ┌────╨────┐ ┌────╨────┐ - c: 1/════════════════════╩════╡ c_0=0x1 ╞════╡ c_0=0x1 ╞ - 0 └─────────┘ └─────────┘ - """ + """Test if schedules circuits with `c_if`s on different qubits.""" qc = QuantumCircuit(3, 1) qc.measure(0, 0) qc.x(1).c_if(0, True) @@ -208,29 +129,7 @@ def test_shorter_measure_after_measure(self): """Test if schedules circuits with shorter measure after measure with a common clbit. Note: For dynamic circuits support we currently group measurements to start at the same time which in turn trigger - the end of a block. - - (input) - ┌─┐ - q_0: ┤M├─── - └╥┘┌─┐ - q_1: ─╫─┤M├ - ║ └╥┘ - q_2: ─╫──╫─ - ║ ║ - c: 1/═╩══╩═ - 0 0 - (scheduled) - ┌─┐ ░ - q_0: ┤M├─────────────────────────────────────────░─ - └╥┘ ┌─┐┌────────────────┐ ░ - q_1: ─╫────────────────────┤M├┤ Delay(300[dt]) ├─░─ - ║ ┌─────────────────┐└╥┘└────────────────┘ ░ - q_2: ─╫─┤ Delay(1000[dt]) ├─╫────────────────────░─ - ║ └─────────────────┘ ║ ░ - c: 1/═╩═════════════════════╩══════════════════════ - 0 0 - """ + the end of a block.""" qc = QuantumCircuit(3, 1) qc.measure(0, 0) qc.measure(1, 0) @@ -255,27 +154,6 @@ def test_measure_after_c_if(self): Note: This test is not yet correct as we should schedule the conditional block qubits with delays as well. - - (input) - ┌─┐ - q_0: ┤M├────────────── - └╥┘ ┌───┐ - q_1: ─╫────┤ X ├────── - ║ └─╥─┘ ┌─┐ - q_2: ─╫──────╫─────┤M├ - ║ ┌────╨────┐└╥┘ - c: 1/═╩═╡ c_0 = T ╞═╩═ - 0 └─────────┘ 0 - (scheduled) - ┌─┐ ░ ░ ┌─────────────────┐ - q_0: ───────────────────┤M├─░─────────────░─┤ Delay(1000[dt]) ├ - ┌─────────────────┐└╥┘ ░ ┌───┐ ░ ├─────────────────┤ - q_1: ┤ Delay(1000[dt]) ├─╫──░────┤ X ├────░─┤ Delay(1000[dt]) ├ - ├─────────────────┤ ║ ░ └─╥─┘ ░ └───────┬─┬───────┘ - q_2: ┤ Delay(1000[dt]) ├─╫──░──────╫──────░─────────┤M├──────── - └─────────────────┘ ║ ░ ┌────╨────┐ ░ └╥┘ - c: 1/════════════════════╩════╡ c_0=0x1 ╞════════════╩═════════ - 0 └─────────┘ 0 """ qc = QuantumCircuit(3, 1) qc.measure(0, 0) @@ -302,24 +180,7 @@ def test_measure_after_c_if(self): self.assertEqual(expected, scheduled) def test_parallel_gate_different_length(self): - """Test circuit having two parallel instruction with different length. - (input) - ┌───┐┌─┐ - q_0: ┤ X ├┤M├─── - ├───┤└╥┘┌─┐ - q_1: ┤ X ├─╫─┤M├ - └───┘ ║ └╥┘ - c: 2/══════╩══╩═ - 0 1 - (expected) - ┌───┐┌─┐┌────────────────┐ - q_0: ┤ X ├┤M├┤ Delay(200[dt]) ├ - ├───┤└╥┘└──────┬─┬───────┘ - q_1: ┤ X ├─╫────────┤M├──────── - └───┘ ║ └╥┘ - c: 2/══════╩═════════╩═════════ - 0 1 - """ + """Test circuit having two parallel instruction with different length.""" qc = QuantumCircuit(2, 2) qc.x(0) qc.x(1) @@ -331,36 +192,20 @@ def test_parallel_gate_different_length(self): ) pm = PassManager([DynamicCircuitScheduleAnalysis(durations), PadDelay()]) - qc_sched = pm.run(qc) + scheduled = pm.run(qc) - sched_expected = QuantumCircuit(2, 2) - sched_expected.x(0) - sched_expected.x(1) - sched_expected.measure(0, 0) # immediately start after X gate - sched_expected.measure(1, 1) - sched_expected.delay(200, 0) + expected = QuantumCircuit(2, 2) + expected.x(0) + expected.x(1) + expected.delay(200, 0) + expected.measure(0, 0) # immediately start after X gate + expected.measure(1, 1) + expected.barrier() - self.assertEqual(qc_sched, sched_expected) + self.assertEqual(scheduled, expected) def test_parallel_gate_different_length_with_barrier(self): - """Test circuit having two parallel instruction with different length with barrier. - (input) - ┌───┐┌─┐ - q_0: ┤ X ├┤M├─── - ├───┤└╥┘┌─┐ - q_1: ┤ X ├─╫─┤M├ - └───┘ ║ └╥┘ - c: 2/══════╩══╩═ - 0 1 - (expected) - ┌───┐┌────────────────┐ ░ ┌─┐ - q_0: ┤ X ├┤ Delay(200[dt]) ├─░─┤M├─── - ├───┤└────────────────┘ ░ └╥┘┌─┐ - q_1: ┤ X ├───────────────────░──╫─┤M├ - └───┘ ░ ║ └╥┘ - c: 2/═══════════════════════════╩══╩═ - 0 1 - """ + """Test circuit having two parallel instruction with different length with barrier.""" qc = QuantumCircuit(2, 2) qc.x(0) qc.x(1) @@ -373,44 +218,24 @@ def test_parallel_gate_different_length_with_barrier(self): ) pm = PassManager([DynamicCircuitScheduleAnalysis(durations), PadDelay()]) - qc_sched = pm.run(qc) + scheduled = pm.run(qc) - sched_expected = QuantumCircuit(2, 2) - sched_expected.x(0) - sched_expected.delay(200, 0) - sched_expected.x(1) - sched_expected.barrier() - sched_expected.measure(0, 0) - sched_expected.measure(1, 1) + expected = QuantumCircuit(2, 2) + expected.x(0) + expected.delay(200, 0) + expected.x(1) + expected.barrier() + expected.measure(0, 0) + expected.measure(1, 1) + expected.barrier() - self.assertEqual(qc_sched, sched_expected) + self.assertEqual(scheduled, expected) def test_measure_after_c_if_on_edge_locking(self): """Test if schedules circuits with c_if after measure with a common clbit. The scheduler is configured to reproduce behavior of the 0.20.0, in which clbit lock is applied to the end-edge of measure instruction. - See https://github.com/Qiskit/qiskit-terra/pull/7655 - (input) - ┌─┐ - q_0: ┤M├────────────── - └╥┘ ┌───┐ - q_1: ─╫────┤ X ├────── - ║ └─╥─┘ ┌─┐ - q_2: ─╫──────╫─────┤M├ - ║ ┌────╨────┐└╥┘ - c: 1/═╩═╡ c_0 = T ╞═╩═ - 0 └─────────┘ 0 - (scheduled) - ┌─┐┌────────────────┐ - q_0: ───────────────────┤M├┤ Delay(200[dt]) ├───────────────────── - ┌─────────────────┐└╥┘└─────┬───┬──────┘ - q_1: ┤ Delay(1000[dt]) ├─╫───────┤ X ├──────────────────────────── - └─────────────────┘ ║ └─╥─┘ ┌─┐┌────────────────┐ - q_2: ────────────────────╫─────────╫─────────┤M├┤ Delay(200[dt]) ├ - ║ ┌────╨────┐ └╥┘└────────────────┘ - c: 1/════════════════════╩════╡ c_0=0x1 ╞═════╩═══════════════════ - 0 └─────────┘ 0 - """ + See https://github.com/Qiskit/qiskit-terra/pull/7655""" qc = QuantumCircuit(3, 1) qc.measure(0, 0) qc.x(1).c_if(0, 1) @@ -442,15 +267,7 @@ def test_active_reset_circuit(self, write_lat, cond_lat): """Test practical example of reset circuit. Because of the stimulus pulse overlap with the previous XGate on the q register, measure instruction is always triggered after XGate regardless of write latency. - Thus only conditional latency matters in the scheduling. - (input) - ┌─┐ ┌───┐ ┌─┐ ┌───┐ ┌─┐ ┌───┐ - q: ┤M├───┤ X ├───┤M├───┤ X ├───┤M├───┤ X ├─── - └╥┘ └─╥─┘ └╥┘ └─╥─┘ └╥┘ └─╥─┘ - ║ ┌────╨────┐ ║ ┌────╨────┐ ║ ┌────╨────┐ - c: 1/═╩═╡ c_0=0x1 ╞═╩═╡ c_0=0x1 ╞═╩═╡ c_0=0x1 ╞ - 0 └─────────┘ 0 └─────────┘ 0 └─────────┘ - """ + Thus only conditional latency matters in the scheduling.""" qc = QuantumCircuit(1, 1) qc.measure(0, 0) qc.x(0).c_if(0, 1) @@ -493,26 +310,7 @@ def test_dag_introduces_extra_dependency_between_conditionals(self): however it must be scheduled after the conditional x on q0 in scheduling. That is because circuit model used in the transpiler passes (DAGCircuit) interprets instructions acting on common clbits must be run in the order - given by the original circuit (QuantumCircuit). - (input) - ┌────────────────┐ ┌───┐ - q_0: ┤ Delay(100[dt]) ├───┤ X ├─── - └─────┬───┬──────┘ └─╥─┘ - q_1: ──────┤ X ├────────────╫───── - └─╥─┘ ║ - ┌────╨────┐ ┌────╨────┐ - c: 1/═══╡ c_0=0x1 ╞════╡ c_0=0x1 ╞ - └─────────┘ └─────────┘ - (scheduled) - ┌────────────────┐ ┌───┐ - q_0: ┤ Delay(100[dt]) ├───┤ X ├────────────── - ├────────────────┤ └─╥─┘ ┌───┐ - q_1: ┤ Delay(100[dt]) ├─────╫────────┤ X ├─── - └────────────────┘ ║ └─╥─┘ - ┌────╨────┐┌────╨────┐ - c: 1/══════════════════╡ c_0=0x1 ╞╡ c_0=0x1 ╞ - └─────────┘└─────────┘ - """ + given by the original circuit (QuantumCircuit).""" qc = QuantumCircuit(2, 1) qc.delay(100, 0) qc.x(0).c_if(0, True) @@ -553,6 +351,7 @@ def test_scheduling_with_calibration(self): expected.delay(160, 0) expected.cx(0, 1) expected.add_calibration("x", (0,), xsched) + expected.barrier() self.assertEqual(expected, scheduled) @@ -568,15 +367,7 @@ def test_padding_not_working_without_scheduling(self): def test_no_pad_very_end_of_circuit(self): """Test padding option that inserts no delay at the very end of circuit. - This circuit will be unchanged after scheduling/padding. - ┌────────────────┐┌─┐ - q_0: ┤ Delay(100[dt]) ├┤M├ - └─────┬───┬──────┘└╥┘ - q_1: ──────┤ X ├────────╫─ - └───┘ ║ - c: 1/═══════════════════╩═ - 0 - """ + This circuit will be unchanged after scheduling/padding.""" qc = QuantumCircuit(2, 1) qc.delay(100, 0) qc.x(1) @@ -591,7 +382,10 @@ def test_no_pad_very_end_of_circuit(self): ] ).run(qc) - self.assertEqual(scheduled, qc) + expected = qc.copy() + expected.barrier() + + self.assertEqual(expected, scheduled) if __name__ == "__main__": From 3cd385972cc9ddc8da2524cfaf7944ac253a4933 Mon Sep 17 00:00:00 2001 From: Thomas Alexander Date: Tue, 5 Jul 2022 13:09:07 -0300 Subject: [PATCH 14/70] c_if scheduling tests are working. --- .../dynamic_circuits/base_padding.py | 19 +++++++- .../dynamic_circuits/schedule.py | 12 +++-- test/dynamic_circuits/test_schedule.py | 44 ++++++++++++------- 3 files changed, 54 insertions(+), 21 deletions(-) diff --git a/qiskit_ibm_provider/dynamic_circuits/base_padding.py b/qiskit_ibm_provider/dynamic_circuits/base_padding.py index 995c70694..1d6ba9d64 100644 --- a/qiskit_ibm_provider/dynamic_circuits/base_padding.py +++ b/qiskit_ibm_provider/dynamic_circuits/base_padding.py @@ -56,6 +56,7 @@ def __init__(self): self._dag = None self._block_duration = 0 self._current_block_idx = 0 + self._conditional_block = False super().__init__() @@ -105,6 +106,7 @@ def _init_run(self, dag): self._node_start_time = self.property_set["node_start_time"].copy() self._idle_after = {bit: 0 for bit in dag.qubits} self._current_block_idx = 0 + self._conditional_block = False self._block_duration = 0 # Prepare DAG to pad @@ -188,10 +190,16 @@ def _visit_generic(self, node): """Visit a generic node to pad.""" block_idx, t0 = self._node_start_time[node] + # Trigger the end of a block if block_idx > self._current_block_idx: + self._terminate_block(self._block_duration, self._current_block_idx) + # This block will not be padded as it is conditional. + # See TODO below. + self._conditional_block = True if node.op.condition_bits else False + # Now set the current block index. self._current_block_idx = block_idx @@ -219,7 +227,15 @@ def _visit_generic(self, node): def _terminate_block(self, block_duration, block_idx): """Terminate the end of a block scheduling region.""" - self._pad_until_block_end(block_duration, block_idx) + # Update all other qubits as not idle so that delays are *not* + # inserted. This is because we need the delays to be inserted in + # the conditional circuit block. However, c_if currently only + # allows writing a single conditional gate. + # TODO: This should be reworked to instead apply a transformation + # pass to rewrite all ``c_if`` operations as ``if_else`` + # blocks that are in turn scheduled. + if not self._conditional_block: + self._pad_until_block_end(block_duration, block_idx) # Terminate with a barrier to be clear timing is non-deterministic # across the barrier. @@ -228,6 +244,7 @@ def _terminate_block(self, block_duration, block_idx): # Reset idles for the new block. self._idle_after = {bit: 0 for bit in self._dag.qubits} self._block_duration = 0 + self._conditional_block = False def _pad_until_block_end(self, block_duration, block_idx): diff --git a/qiskit_ibm_provider/dynamic_circuits/schedule.py b/qiskit_ibm_provider/dynamic_circuits/schedule.py index fa12dc43b..cfec5e04f 100644 --- a/qiskit_ibm_provider/dynamic_circuits/schedule.py +++ b/qiskit_ibm_provider/dynamic_circuits/schedule.py @@ -73,8 +73,6 @@ def run(self, dag): for node in dag.topological_op_nodes(): self._visit_node(node) - for node, start_time in self._node_start_time.items(): - print(repr(node), start_time) self.property_set["node_start_time"] = self._node_start_time def _init_run(self, dag): @@ -123,8 +121,11 @@ def _visit_conditional_node(self, node): TODO: Update for support of general control-flow, not just single conditional operations. """ + # Special processing required to resolve conditional scheduling dependencies if node.op.condition_bits: - # Special processing required to resolve conditional scheduling dependencies + # Trigger the start of a conditional block + self._begin_new_circuit_block() + op_duration = self._get_node_duration(node) t0q = max(self._idle_after[q][1] for q in node.qargs) @@ -150,11 +151,16 @@ def _visit_conditional_node(self, node): for bit in node.op.condition_bits: # Lock clbit until state is read self._idle_after[bit] = (self._current_block_idx, t1c) + # It starts after register read access t0 = max(t0q, t1c) t1 = t0 + op_duration self._update_idles(node, t0, t1) + + # Terminate the conditional block + self._begin_new_circuit_block() + else: # Fall through to generic case if not conditional self._visit_generic(node) diff --git a/test/dynamic_circuits/test_schedule.py b/test/dynamic_circuits/test_schedule.py index a75f6274c..cb486a237 100644 --- a/test/dynamic_circuits/test_schedule.py +++ b/test/dynamic_circuits/test_schedule.py @@ -47,10 +47,11 @@ def test_classically_controlled_gate_after_measure(self): scheduled = pm.run(qc) expected = QuantumCircuit(2, 1) + expected.delay(1000, 1) expected.measure(0, 0) - expected.delay(1000, 1) # x.c_if starts after measure + expected.barrier() expected.x(1).c_if(0, True) - expected.delay(200, 0) + expected.barrier() self.assertEqual(expected, scheduled) @@ -116,12 +117,14 @@ def test_c_if_on_different_qubits(self): scheduled = pm.run(qc) expected = QuantumCircuit(3, 1) - expected.measure(0, 0) expected.delay(1000, 1) expected.delay(1000, 2) + expected.measure(0, 0) + expected.barrier() expected.x(1).c_if(0, True) + expected.barrier() expected.x(2).c_if(0, True) - expected.delay(200, 0) + expected.barrier() self.assertEqual(expected, scheduled) @@ -176,6 +179,7 @@ def test_measure_after_c_if(self): expected.delay(1000, 0) expected.measure(2, 0) expected.delay(1000, 1) + expected.barrier() self.assertEqual(expected, scheduled) @@ -244,7 +248,7 @@ def test_measure_after_c_if_on_edge_locking(self): durations = InstructionDurations([("x", None, 200), ("measure", None, 1000)]) # lock at the end edge - actual_sched = PassManager( + scheduled = PassManager( [ SetIOLatency(clbit_write_latency=1000), DynamicCircuitScheduleAnalysis(durations), @@ -252,14 +256,19 @@ def test_measure_after_c_if_on_edge_locking(self): ] ).run(qc) - expected_sched = QuantumCircuit(3, 1) - expected_sched.measure(0, 0) - expected_sched.delay(1000, 1) - expected_sched.x(1).c_if(0, 1) - expected_sched.measure(2, 0) - expected_sched.delay(200, 0) - expected_sched.delay(200, 2) - self.assertEqual(expected_sched, actual_sched) + expected = QuantumCircuit(3, 1) + expected.delay(1000, 1) + expected.delay(1000, 2) + expected.measure(0, 0) + expected.barrier() + expected.x(1).c_if(0, 1) + expected.barrier() + expected.delay(1000, 0) + expected.delay(1000, 1) + expected.measure(2, 0) + expected.barrier() + + self.assertEqual(expected, scheduled) @data([100, 200], [500, 0], [1000, 200]) @unpack @@ -278,7 +287,7 @@ def test_active_reset_circuit(self, write_lat, cond_lat): durations = InstructionDurations([("x", None, 100), ("measure", None, 1000)]) - actual_sched = PassManager( + scheduled = PassManager( [ SetIOLatency( clbit_write_latency=write_lat, conditional_latency=cond_lat @@ -302,7 +311,7 @@ def test_active_reset_circuit(self, write_lat, cond_lat): expected.delay(cond_lat, 0) expected.x(0).c_if(0, 1) - self.assertEqual(expected, actual_sched) + self.assertEqual(expected, scheduled) def test_dag_introduces_extra_dependency_between_conditionals(self): """Test dependency between conditional operations in the scheduling. @@ -321,10 +330,11 @@ def test_dag_introduces_extra_dependency_between_conditionals(self): scheduled = pm.run(qc) expected = QuantumCircuit(2, 1) - expected.delay(100, 0) - expected.delay(100, 1) # due to extra dependency on clbits + expected.barrier() expected.x(0).c_if(0, True) + expected.barrier() expected.x(1).c_if(0, True) + expected.barrier() self.assertEqual(expected, scheduled) From b48951d13dbb1a6a4c951a03613ff089a5b6765e Mon Sep 17 00:00:00 2001 From: Thomas Alexander Date: Tue, 5 Jul 2022 13:15:17 -0300 Subject: [PATCH 15/70] All tests passing. --- qiskit_ibm_provider/dynamic_circuits/schedule.py | 6 +++--- test/dynamic_circuits/test_schedule.py | 12 ++++++------ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/qiskit_ibm_provider/dynamic_circuits/schedule.py b/qiskit_ibm_provider/dynamic_circuits/schedule.py index cfec5e04f..75a9f6545 100644 --- a/qiskit_ibm_provider/dynamic_circuits/schedule.py +++ b/qiskit_ibm_provider/dynamic_circuits/schedule.py @@ -144,10 +144,10 @@ def _visit_conditional_node(self, node): # |t0q # Q ▒▒▒▒▒▒▒▒▒▒▒ # C ▒▒▒░░░▒▒░░░ - # |t0q - conditional_latency + # |t0q # - t0c = max(t0q - self._conditional_latency, t0c) - t1c = t0c + self._conditional_latency + t0c = max(t0q, t0c) + t1c = t0c for bit in node.op.condition_bits: # Lock clbit until state is read self._idle_after[bit] = (self._current_block_idx, t1c) diff --git a/test/dynamic_circuits/test_schedule.py b/test/dynamic_circuits/test_schedule.py index cb486a237..b15ab4d93 100644 --- a/test/dynamic_circuits/test_schedule.py +++ b/test/dynamic_circuits/test_schedule.py @@ -299,17 +299,17 @@ def test_active_reset_circuit(self, write_lat, cond_lat): expected = QuantumCircuit(1, 1) expected.measure(0, 0) - if cond_lat > 0: - expected.delay(cond_lat, 0) + expected.barrier() expected.x(0).c_if(0, 1) + expected.barrier() expected.measure(0, 0) - if cond_lat > 0: - expected.delay(cond_lat, 0) + expected.barrier() expected.x(0).c_if(0, 1) + expected.barrier() expected.measure(0, 0) - if cond_lat > 0: - expected.delay(cond_lat, 0) + expected.barrier() expected.x(0).c_if(0, 1) + expected.barrier() self.assertEqual(expected, scheduled) From 3efa7da2ddbeb2b3764ad1b6767571c3837d3e1d Mon Sep 17 00:00:00 2001 From: Thomas Alexander Date: Tue, 5 Jul 2022 21:57:54 -0300 Subject: [PATCH 16/70] All dynamic circuits tests passing. --- .../dynamic_circuits/schedule.py | 26 ++++-- test/dynamic_circuits/test_schedule.py | 82 +++++++++++++++++++ 2 files changed, 103 insertions(+), 5 deletions(-) diff --git a/qiskit_ibm_provider/dynamic_circuits/schedule.py b/qiskit_ibm_provider/dynamic_circuits/schedule.py index 75a9f6545..0a47d7d6c 100644 --- a/qiskit_ibm_provider/dynamic_circuits/schedule.py +++ b/qiskit_ibm_provider/dynamic_circuits/schedule.py @@ -15,7 +15,7 @@ import itertools import qiskit -from qiskit.circuit import Measure +from qiskit.circuit import Measure, Reset from qiskit.transpiler.exceptions import TranspilerError from qiskit.transpiler.passes.scheduling.scheduling.base_scheduler import BaseScheduler @@ -108,6 +108,8 @@ def _visit_node(self, node): if isinstance(node.op, Measure): self._visit_measure(node) + elif isinstance(node.op, Reset): + self._visit_reset(node) else: self._visit_generic(node) @@ -171,11 +173,9 @@ def _visit_measure(self, node): Measurement currently triggers the end of a pulse block in IBM dynamic circuits hardware. This means that it is possible to schedule *up to* a measurement (and during its pulses) but the measurement will be followed by a period of indeterminism. - All measurements on disjoing qubits will be collected on the same qubits to be run simulataneously. + All measurements on disjoint qubits will be collected on the same qubits to be run simultaneously. """ - op_duration = self._get_node_duration(node) - current_block_measure_qargs = self._current_block_measure_qargs() measure_qargs = set(node.qargs) @@ -198,9 +198,25 @@ def _visit_measure(self, node): for measure in self._current_block_measures: t0 = t0q - t1 = t0 + self._get_node_duration(measure) + bit_indices = {bit: index for index, bit in enumerate(self._dag.qubits)} + measure_duration = self.durations.get( + Measure(), [bit_indices[qarg] for qarg in node.qargs], unit="dt" + ) + t1 = t0 + measure_duration self._update_idles(measure, t0, t1) + def _visit_reset(self, node): + """Visit a reset node. + + Reset currently triggers the end of a pulse block in IBM dynamic circuits hardware + as conditional reset is performed internally using a c_if. + This means that it is possible to schedule *up to* a reset (and during its measurement pulses) + but the reset will be followed by a period of conditional indeterminism. + All resets on disjoint qubits will be collected on the same qubits to be run simultaneously. + This means that from the perspective of scheduling resets have the same behaviour and duration + as a measurement. + """ + self._visit_measure(node) def _visit_generic(self, node): """Visit a generic node such as a gate or barrier.""" diff --git a/test/dynamic_circuits/test_schedule.py b/test/dynamic_circuits/test_schedule.py index b15ab4d93..f629c2a52 100644 --- a/test/dynamic_circuits/test_schedule.py +++ b/test/dynamic_circuits/test_schedule.py @@ -397,6 +397,88 @@ def test_no_pad_very_end_of_circuit(self): self.assertEqual(expected, scheduled) + def test_reset_terminates_block(self): + """Test if reset operations terminate the block scheduled + + Note: For dynamic circuits support we currently group resets to start at the same time which in turn trigger + the end of a block.""" + qc = QuantumCircuit(3, 1) + qc.x(0) + qc.reset(0) + qc.reset(1) + + durations = InstructionDurations( + [ + ("x", None, 200), + ( + "reset", + [0], + 1000, + ), # ignored as only the duration of the measurement is used for scheduling + ( + "reset", + [1], + 900, + ), # ignored as only the duration of the measurement is used for scheduling + ("measure", [0], 600), + ("measure", [1], 700), + ] + ) + pm = PassManager([DynamicCircuitScheduleAnalysis(durations), PadDelay()]) + scheduled = pm.run(qc) + + expected = QuantumCircuit(3, 1) + expected.x(0) + expected.delay(200, 1) + expected.delay(1200, 2) + expected.reset(0) + expected.reset(1) + expected.delay(100, 1) + expected.barrier() + + self.assertEqual(expected, scheduled) + + def test_reset_merged_with_measure(self): + """Test if reset operations terminate the block scheduled + + Note: For dynamic circuits support we currently group resets to start at the same time which in turn trigger + the end of a block.""" + qc = QuantumCircuit(3, 1) + qc.x(0) + qc.reset(0) + qc.reset(1) + + durations = InstructionDurations( + [ + ("x", None, 200), + ( + "reset", + [0], + 1000, + ), # ignored as only the duration of the measurement is used for scheduling + ( + "reset", + [1], + 900, + ), # ignored as only the duration of the measurement is used for scheduling + ("measure", [0], 600), + ("measure", [1], 700), + ] + ) + pm = PassManager([DynamicCircuitScheduleAnalysis(durations), PadDelay()]) + scheduled = pm.run(qc) + + expected = QuantumCircuit(3, 1) + expected.x(0) + expected.delay(200, 1) + expected.delay(1200, 2) + expected.reset(0) + expected.reset(1) + expected.delay(100, 1) + expected.barrier() + + self.assertEqual(expected, scheduled) + if __name__ == "__main__": unittest.main() From 96e02fb172b0248e8589c4721ece1da6158edc55 Mon Sep 17 00:00:00 2001 From: Thomas Alexander Date: Tue, 5 Jul 2022 22:03:01 -0300 Subject: [PATCH 17/70] Black formatting. --- qiskit_ibm_provider/dynamic_circuits/base_padding.py | 12 ++++-------- qiskit_ibm_provider/dynamic_circuits/pad_delay.py | 4 +++- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/qiskit_ibm_provider/dynamic_circuits/base_padding.py b/qiskit_ibm_provider/dynamic_circuits/base_padding.py index 1d6ba9d64..96145c16b 100644 --- a/qiskit_ibm_provider/dynamic_circuits/base_padding.py +++ b/qiskit_ibm_provider/dynamic_circuits/base_padding.py @@ -60,7 +60,6 @@ def __init__(self): super().__init__() - def run(self, dag: DAGCircuit): """Run the padding pass on ``dag``. @@ -76,7 +75,6 @@ def run(self, dag: DAGCircuit): """ self._pre_runhook(dag) - self._init_run(dag) # Compute fresh circuit duration from the node start time dictionary and op duration. @@ -127,7 +125,6 @@ def _init_run(self, dag): self._dag.calibrations = dag.calibrations self._dag.global_phase = dag.global_phase - def _pre_runhook(self, dag: DAGCircuit): """Extra routine inserted before running the padding pass. @@ -190,7 +187,6 @@ def _visit_generic(self, node): """Visit a generic node to pad.""" block_idx, t0 = self._node_start_time[node] - # Trigger the end of a block if block_idx > self._current_block_idx: @@ -203,7 +199,6 @@ def _visit_generic(self, node): # Now set the current block index. self._current_block_idx = block_idx - t1 = t0 + node.op.duration self._block_duration = max(self._block_duration, t1) @@ -239,16 +234,17 @@ def _terminate_block(self, block_duration, block_idx): # Terminate with a barrier to be clear timing is non-deterministic # across the barrier. - self._dag.apply_operation_back(Barrier(self._dag.num_qubits()), self._dag.qubits, []) + self._dag.apply_operation_back( + Barrier(self._dag.num_qubits()), self._dag.qubits, [] + ) # Reset idles for the new block. self._idle_after = {bit: 0 for bit in self._dag.qubits} self._block_duration = 0 self._conditional_block = False - def _pad_until_block_end(self, block_duration, block_idx): - # Add delays until the end of circuit. + # Add delays until the end of circuit. for bit in self._dag.qubits: if block_duration - self._idle_after[bit] > 0: node = self._dag.output_map[bit] diff --git a/qiskit_ibm_provider/dynamic_circuits/pad_delay.py b/qiskit_ibm_provider/dynamic_circuits/pad_delay.py index ca081ff4d..6250ec82d 100644 --- a/qiskit_ibm_provider/dynamic_circuits/pad_delay.py +++ b/qiskit_ibm_provider/dynamic_circuits/pad_delay.py @@ -72,4 +72,6 @@ def _pad( return time_interval = t_end - t_start - self._apply_scheduled_op(block_idx, t_start, Delay(time_interval, self._dag.unit), qubit) + self._apply_scheduled_op( + block_idx, t_start, Delay(time_interval, self._dag.unit), qubit + ) From 9dddf1ee5a8575e8373b8c45c19d67ae592f0a1e Mon Sep 17 00:00:00 2001 From: Thomas Alexander Date: Tue, 5 Jul 2022 22:42:31 -0300 Subject: [PATCH 18/70] Move the dynamic_circuits module to a generic transpiler module. --- .../dynamic_circuits/__init__.py | 51 ------------ qiskit_ibm_provider/transpiler/__init__.py | 22 ++++++ .../transpiler/passes/__init__.py | 30 +++++++ .../transpiler/passes/scheduling/__init__.py | 79 +++++++++++++++++++ .../passes/scheduling}/base_padding.py | 2 +- .../passes/scheduling}/pad_delay.py | 4 +- .../passes/scheduling/scheduler.py} | 0 ...amic-circuits-module-af6f530072c82f44.yaml | 7 -- ...c-circuits-scheduler-b1ae525c0b358acb.yaml | 10 +++ ...dd-transpiler-module-af6f530072c82f44.yaml | 13 +++ .../__init__.py | 0 test/transpiler/passes/__init__.py | 11 +++ test/transpiler/passes/scheduling/__init__.py | 11 +++ .../passes/scheduling}/test_schedule.py | 28 ++----- 14 files changed, 185 insertions(+), 83 deletions(-) delete mode 100644 qiskit_ibm_provider/dynamic_circuits/__init__.py create mode 100644 qiskit_ibm_provider/transpiler/__init__.py create mode 100644 qiskit_ibm_provider/transpiler/passes/__init__.py create mode 100644 qiskit_ibm_provider/transpiler/passes/scheduling/__init__.py rename qiskit_ibm_provider/{dynamic_circuits => transpiler/passes/scheduling}/base_padding.py (99%) rename qiskit_ibm_provider/{dynamic_circuits => transpiler/passes/scheduling}/pad_delay.py (94%) rename qiskit_ibm_provider/{dynamic_circuits/schedule.py => transpiler/passes/scheduling/scheduler.py} (100%) delete mode 100644 releasenotes/notes/add-dynamic-circuits-module-af6f530072c82f44.yaml create mode 100644 releasenotes/notes/add-dynamic-circuits-scheduler-b1ae525c0b358acb.yaml create mode 100644 releasenotes/notes/add-transpiler-module-af6f530072c82f44.yaml rename test/{dynamic_circuits => transpiler}/__init__.py (100%) create mode 100644 test/transpiler/passes/__init__.py create mode 100644 test/transpiler/passes/scheduling/__init__.py rename test/{dynamic_circuits => transpiler/passes/scheduling}/test_schedule.py (95%) diff --git a/qiskit_ibm_provider/dynamic_circuits/__init__.py b/qiskit_ibm_provider/dynamic_circuits/__init__.py deleted file mode 100644 index 09c169da8..000000000 --- a/qiskit_ibm_provider/dynamic_circuits/__init__.py +++ /dev/null @@ -1,51 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2022. -# -# 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. - -""" -============================================================== -Dynamic Circuits (:mod:`qiskit_ibm_provider.dynamic_circuits`) -============================================================== - -.. currentmodule:: qiskit_ibm_provider.dynamic_circuits - -A collection of tools for working with IBM Quantum's next-generation -backends that support advanced "dynamic circuit" capabilities. Ie., -circuits with support for classical control-flow/feedback based off -of measurement results. - -Example Usage on a Supporting Backend -===================================== - -.. jupyter-execute:: - :hide-code: - :hide-output: - - from qiskit_ibm_provider.test.ibm_provider_mock import mock_get_backend - mock_get_backend('FakePerth') - -.. jupyter-execute:: - - from qiskit_ibm_provider import IBMProvider - import qiskit_ibm_provider.jupyter - - provider = IBMProvider(hub='ibm-q') - backend = provider.get_backend('ibm_perth') - - - -Scheduling & Dynamical Decoupling -================================= -.. autosummary:: - :toctree: ../stubs/ - - -""" diff --git a/qiskit_ibm_provider/transpiler/__init__.py b/qiskit_ibm_provider/transpiler/__init__.py new file mode 100644 index 000000000..a2ae05a42 --- /dev/null +++ b/qiskit_ibm_provider/transpiler/__init__.py @@ -0,0 +1,22 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2022. +# +# 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. + +""" +==================================================================== +IBM Backend Transpiler Tools (:mod:`qiskit_ibm_provider.transpiler`) +==================================================================== + +A collection of transpiler tools for working with IBM Quantum's +next-generation backends that support advanced "dynamic circuit" +capabilities. Ie., circuits with support for classical +compute and control-flow/feedback based off of measurement results. +""" diff --git a/qiskit_ibm_provider/transpiler/passes/__init__.py b/qiskit_ibm_provider/transpiler/passes/__init__.py new file mode 100644 index 000000000..680a92d87 --- /dev/null +++ b/qiskit_ibm_provider/transpiler/passes/__init__.py @@ -0,0 +1,30 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2022. +# +# 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. + +""" +=================================================== +Transpiler Passes (:mod:`qiskit_ibm_provider.transpiler.passes`) +=================================================== + +.. currentmodule:: qiskit_ibm_provider.transpiler.passes + +.. autosummary:: + :toctree: ../stubs/ + + Dynamic circuit scheduling + + +""" + +# circuit scheduling +from .scheduling import DynamicCircuitScheduleAnalysis +from .scheduling import PadDelay diff --git a/qiskit_ibm_provider/transpiler/passes/scheduling/__init__.py b/qiskit_ibm_provider/transpiler/passes/scheduling/__init__.py new file mode 100644 index 000000000..a995525ba --- /dev/null +++ b/qiskit_ibm_provider/transpiler/passes/scheduling/__init__.py @@ -0,0 +1,79 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2022. +# +# 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. + +""" +============================================================== +Scheduling (:mod:`qiskit_ibm_provider.transpiler.passes.scheduling`) +============================================================== + +.. currentmodule:: qiskit_ibm_provider.transpiler.passes.scheduling + +A collection of scheduling passes for working with IBM Quantum's next-generation +backends that support advanced "dynamic circuit" capabilities. Ie., +circuits with support for classical control-flow/feedback based off +of measurement results. + + +Below we demonstrate how to schedule and pad a teleportation circuit with delays +for a dynamic circuit backend's execution model + + +.. jupyter-execute:: + + from qiskit.transpiler.instruction_durations import InstructionDurations + + from qiskit_ibm_provider.transpiler.scheduling import DynamicCircuitScheduleAnalysis, PadDelay + from qiskit_ibm_provider.test.ibm_provider_mock import mock_get_backend + + + backend = mock_get_backend('FakePerth') + + durations = InstructionDurations.from_backend(backend) + pm = PassManager([DynamicCircuitScheduleAnalysis(durations), PadDelay()]) + + qr = QuantumRegister(3) + crz = ClassicalRegister(1, name="crz") + crx = ClassicalRegister(1, name="crx") + result = ClassicalRegister(1, name="result") + + teleport = QuantumCircuit(qr, crz, crx, result, name="Teleport") + + teleport.h(qr[1]) + teleport.cx(qr[1], qr[2]) + teleport.cx(qr[0], qr[1]) + teleport.h(qr[0]) + teleport.measure(qr[0], crz) + teleport.measure(qr[1], crx) + teleport.z(qr[2]).c_if(crz, 1) + teleport.x(qr[2]).c_if(crx, 1) + teleport.measure(qr[2], result) + + scheduled_teleport = pm.run(teleport) + + teleport.draw(output="mpl") + + +Scheduling & Dynamical Decoupling +================================= +.. autosummary:: + :toctree: ../stubs/ + + BasePadding + DynamicCircuitScheduleAnalysis + PadDelay + + + +""" + +from .pad_delay import PadDelay +from .scheduler import DynamicCircuitScheduleAnalysis diff --git a/qiskit_ibm_provider/dynamic_circuits/base_padding.py b/qiskit_ibm_provider/transpiler/passes/scheduling/base_padding.py similarity index 99% rename from qiskit_ibm_provider/dynamic_circuits/base_padding.py rename to qiskit_ibm_provider/transpiler/passes/scheduling/base_padding.py index 96145c16b..494c68187 100644 --- a/qiskit_ibm_provider/dynamic_circuits/base_padding.py +++ b/qiskit_ibm_provider/transpiler/passes/scheduling/base_padding.py @@ -10,7 +10,7 @@ # copyright notice, and modified files need to carry a notice indicating # that they have been altered from the originals. -"""Padding pass to fill timeslots for the dynamic circuits backends.""" +"""Padding pass to fill timeslots for IBM (dynamic circuit) backends.""" from typing import List, Optional, Union diff --git a/qiskit_ibm_provider/dynamic_circuits/pad_delay.py b/qiskit_ibm_provider/transpiler/passes/scheduling/pad_delay.py similarity index 94% rename from qiskit_ibm_provider/dynamic_circuits/pad_delay.py rename to qiskit_ibm_provider/transpiler/passes/scheduling/pad_delay.py index 6250ec82d..a7e3824e5 100644 --- a/qiskit_ibm_provider/dynamic_circuits/pad_delay.py +++ b/qiskit_ibm_provider/transpiler/passes/scheduling/pad_delay.py @@ -14,9 +14,9 @@ from qiskit.circuit import Qubit from qiskit.circuit.delay import Delay -from qiskit.dagcircuit import DAGCircuit, DAGNode, DAGOutNode +from qiskit.dagcircuit import DAGNode, DAGOutNode -from qiskit_ibm_provider.dynamic_circuits.base_padding import BasePadding +from .base_padding import BasePadding class PadDelay(BasePadding): diff --git a/qiskit_ibm_provider/dynamic_circuits/schedule.py b/qiskit_ibm_provider/transpiler/passes/scheduling/scheduler.py similarity index 100% rename from qiskit_ibm_provider/dynamic_circuits/schedule.py rename to qiskit_ibm_provider/transpiler/passes/scheduling/scheduler.py diff --git a/releasenotes/notes/add-dynamic-circuits-module-af6f530072c82f44.yaml b/releasenotes/notes/add-dynamic-circuits-module-af6f530072c82f44.yaml deleted file mode 100644 index de65fd8ef..000000000 --- a/releasenotes/notes/add-dynamic-circuits-module-af6f530072c82f44.yaml +++ /dev/null @@ -1,7 +0,0 @@ ---- -features: - - | - A new dynamic circuits module has been added to :mod:`qiskit_ibm_provider.dynamic_circuits`. - It will contain all specialized Qiskit routines for running applications on IBM's next-generation - quantum devices that support dynamic capabilities such as control-flow(feedforward) and classical - compute. diff --git a/releasenotes/notes/add-dynamic-circuits-scheduler-b1ae525c0b358acb.yaml b/releasenotes/notes/add-dynamic-circuits-scheduler-b1ae525c0b358acb.yaml new file mode 100644 index 000000000..54ffb0bd9 --- /dev/null +++ b/releasenotes/notes/add-dynamic-circuits-scheduler-b1ae525c0b358acb.yaml @@ -0,0 +1,10 @@ +--- +features: + - | + A scheduling analysis pass, :class:`~qiskit_ibm_provider.transpiler.passes.scheduling.DynamicCircuitScheduleAnalysis` + has been added for Qiskit dynamic circuit (OpenQASM 3) backends. This is capable of handling + scheduling for deterministic regions of a quantum circuit and may combined with a padding pass such as + :class:`~qiskit_ibm_provider.transpiler.passes.scheduling.PadDelay` to pad schedulable sections of a + circuit with delays. + + For an example see the :mod:`~qiskit_ibm_provider.transpiler.passes.scheduling` module's documentation. diff --git a/releasenotes/notes/add-transpiler-module-af6f530072c82f44.yaml b/releasenotes/notes/add-transpiler-module-af6f530072c82f44.yaml new file mode 100644 index 000000000..5b544a897 --- /dev/null +++ b/releasenotes/notes/add-transpiler-module-af6f530072c82f44.yaml @@ -0,0 +1,13 @@ +--- +prelude: > + A :mod:`~qiskit_ibm_provider.transpiler` module has been added. + It will contain routines that are specific to IBM hardware backends and + which consequently can not be placed directly within Qiskit Terra. +features: + - | + The module :mod:`~qiskit_ibm_provider.transpiler`` has been added. + + Primarily, it will contain all specialized Qiskit routines for running + applications on IBM's next-generation quantum devices that support dynamic + capabilities such as control-flow(feedforward) and classical + compute. diff --git a/test/dynamic_circuits/__init__.py b/test/transpiler/__init__.py similarity index 100% rename from test/dynamic_circuits/__init__.py rename to test/transpiler/__init__.py diff --git a/test/transpiler/passes/__init__.py b/test/transpiler/passes/__init__.py new file mode 100644 index 000000000..fdb172d36 --- /dev/null +++ b/test/transpiler/passes/__init__.py @@ -0,0 +1,11 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2022. +# +# 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. diff --git a/test/transpiler/passes/scheduling/__init__.py b/test/transpiler/passes/scheduling/__init__.py new file mode 100644 index 000000000..fdb172d36 --- /dev/null +++ b/test/transpiler/passes/scheduling/__init__.py @@ -0,0 +1,11 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2022. +# +# 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. diff --git a/test/dynamic_circuits/test_schedule.py b/test/transpiler/passes/scheduling/test_schedule.py similarity index 95% rename from test/dynamic_circuits/test_schedule.py rename to test/transpiler/passes/scheduling/test_schedule.py index f629c2a52..58846626f 100644 --- a/test/dynamic_circuits/test_schedule.py +++ b/test/transpiler/passes/scheduling/test_schedule.py @@ -12,26 +12,17 @@ """Test the dynamic circuits scheduling analysis""" -import sched -import unittest - -from ddt import ddt, data, unpack from qiskit import QuantumCircuit from qiskit.pulse import Schedule, Play, Constant, DriveChannel from qiskit.test import QiskitTestCase from qiskit.transpiler.instruction_durations import InstructionDurations -from qiskit.transpiler.passes import ( - PadDelay, - SetIOLatency, -) from qiskit.transpiler.passmanager import PassManager from qiskit.transpiler.exceptions import TranspilerError -from qiskit_ibm_provider.dynamic_circuits.pad_delay import PadDelay -from qiskit_ibm_provider.dynamic_circuits.schedule import DynamicCircuitScheduleAnalysis +from qiskit_ibm_provider.transpiler.passes.scheduling.pad_delay import PadDelay +from qiskit_ibm_provider.transpiler.passes.scheduling.scheduler import DynamicCircuitScheduleAnalysis -@ddt class TestSchedulingAndPaddingPass(QiskitTestCase): """Tests the Scheduling passes""" @@ -250,7 +241,6 @@ def test_measure_after_c_if_on_edge_locking(self): # lock at the end edge scheduled = PassManager( [ - SetIOLatency(clbit_write_latency=1000), DynamicCircuitScheduleAnalysis(durations), PadDelay(), ] @@ -270,10 +260,9 @@ def test_measure_after_c_if_on_edge_locking(self): self.assertEqual(expected, scheduled) - @data([100, 200], [500, 0], [1000, 200]) - @unpack - def test_active_reset_circuit(self, write_lat, cond_lat): + def test_active_reset_circuit(self): """Test practical example of reset circuit. + Because of the stimulus pulse overlap with the previous XGate on the q register, measure instruction is always triggered after XGate regardless of write latency. Thus only conditional latency matters in the scheduling.""" @@ -289,9 +278,6 @@ def test_active_reset_circuit(self, write_lat, cond_lat): scheduled = PassManager( [ - SetIOLatency( - clbit_write_latency=write_lat, conditional_latency=cond_lat - ), DynamicCircuitScheduleAnalysis(durations), PadDelay(), ] @@ -315,6 +301,7 @@ def test_active_reset_circuit(self, write_lat, cond_lat): def test_dag_introduces_extra_dependency_between_conditionals(self): """Test dependency between conditional operations in the scheduling. + In the below example circuit, the conditional x on q1 could start at time 0, however it must be scheduled after the conditional x on q0 in scheduling. That is because circuit model used in the transpiler passes (DAGCircuit) @@ -377,6 +364,7 @@ def test_padding_not_working_without_scheduling(self): def test_no_pad_very_end_of_circuit(self): """Test padding option that inserts no delay at the very end of circuit. + This circuit will be unchanged after scheduling/padding.""" qc = QuantumCircuit(2, 1) qc.delay(100, 0) @@ -478,7 +466,3 @@ def test_reset_merged_with_measure(self): expected.barrier() self.assertEqual(expected, scheduled) - - -if __name__ == "__main__": - unittest.main() From 2442d1e3e1179a5c0e8266d58a6acf5f54f7ca25 Mon Sep 17 00:00:00 2001 From: Thomas Alexander Date: Tue, 5 Jul 2022 23:13:58 -0300 Subject: [PATCH 19/70] Blackify --- .../transpiler/passes/scheduling/scheduler.py | 26 +++++++++++++------ .../passes/scheduling/test_schedule.py | 4 ++- 2 files changed, 21 insertions(+), 9 deletions(-) diff --git a/qiskit_ibm_provider/transpiler/passes/scheduling/scheduler.py b/qiskit_ibm_provider/transpiler/passes/scheduling/scheduler.py index 0a47d7d6c..a593b3015 100644 --- a/qiskit_ibm_provider/transpiler/passes/scheduling/scheduler.py +++ b/qiskit_ibm_provider/transpiler/passes/scheduling/scheduler.py @@ -40,7 +40,9 @@ class DynamicCircuitScheduleAnalysis(BaseScheduler): """ - def __init__(self, durations: qiskit.transpiler.instruction_durations.InstructionDurations): + def __init__( + self, durations: qiskit.transpiler.instruction_durations.InstructionDurations + ): """Scheduler for dynamic circuit backends. Args: @@ -187,7 +189,15 @@ def _visit_measure(self, node): t0q = 0 # Otherwise we need to increment all measurements to start at the same time within the block. else: - t0q = max(itertools.chain([t0q], (self._node_start_time[measure][1] for measure in self._current_block_measures))) + t0q = max( + itertools.chain( + [t0q], + ( + self._node_start_time[measure][1] + for measure in self._current_block_measures + ), + ) + ) # Insert this measure into the block self._current_block_measures.add(node) @@ -200,8 +210,8 @@ def _visit_measure(self, node): t0 = t0q bit_indices = {bit: index for index, bit in enumerate(self._dag.qubits)} measure_duration = self.durations.get( - Measure(), [bit_indices[qarg] for qarg in node.qargs], unit="dt" - ) + Measure(), [bit_indices[qarg] for qarg in node.qargs], unit="dt" + ) t1 = t0 + measure_duration self._update_idles(measure, t0, t1) @@ -237,12 +247,12 @@ def _update_idles(self, node, t0, t1): self._node_start_time[node] = (self._current_block_idx, t0) def _begin_new_circuit_block(self): - """Create a new timed circuit block completing the previous block. - - """ + """Create a new timed circuit block completing the previous block.""" self._current_block_idx += 1 self._current_block_measures = set() self._idle_after = {q: (0, 0) for q in self._dag.qubits + self._dag.clbits} def _current_block_measure_qargs(self): - return set(qarg for measure in self._current_block_measures for qarg in measure.qargs) + return set( + qarg for measure in self._current_block_measures for qarg in measure.qargs + ) diff --git a/test/transpiler/passes/scheduling/test_schedule.py b/test/transpiler/passes/scheduling/test_schedule.py index 58846626f..64f71d4a3 100644 --- a/test/transpiler/passes/scheduling/test_schedule.py +++ b/test/transpiler/passes/scheduling/test_schedule.py @@ -20,7 +20,9 @@ from qiskit.transpiler.exceptions import TranspilerError from qiskit_ibm_provider.transpiler.passes.scheduling.pad_delay import PadDelay -from qiskit_ibm_provider.transpiler.passes.scheduling.scheduler import DynamicCircuitScheduleAnalysis +from qiskit_ibm_provider.transpiler.passes.scheduling.scheduler import ( + DynamicCircuitScheduleAnalysis, +) class TestSchedulingAndPaddingPass(QiskitTestCase): From 7e144e962c27051247243f7d1f6bf36a0fe48f77 Mon Sep 17 00:00:00 2001 From: Thomas Alexander Date: Tue, 5 Jul 2022 23:33:45 -0300 Subject: [PATCH 20/70] Linting. --- .../passes/scheduling/base_padding.py | 6 +-- .../transpiler/passes/scheduling/scheduler.py | 54 +++++++++---------- .../passes/scheduling/test_schedule.py | 23 ++++---- 3 files changed, 42 insertions(+), 41 deletions(-) diff --git a/qiskit_ibm_provider/transpiler/passes/scheduling/base_padding.py b/qiskit_ibm_provider/transpiler/passes/scheduling/base_padding.py index 494c68187..160b2ceb7 100644 --- a/qiskit_ibm_provider/transpiler/passes/scheduling/base_padding.py +++ b/qiskit_ibm_provider/transpiler/passes/scheduling/base_padding.py @@ -185,7 +185,7 @@ def _visit_delay(self, node): def _visit_generic(self, node): """Visit a generic node to pad.""" - block_idx, t0 = self._node_start_time[node] + block_idx, t0 = self._node_start_time[node] # pylint: disable=invalid-name # Trigger the end of a block if block_idx > self._current_block_idx: @@ -194,12 +194,12 @@ def _visit_generic(self, node): # This block will not be padded as it is conditional. # See TODO below. - self._conditional_block = True if node.op.condition_bits else False + self._conditional_block = bool(node.op.condition_bits) # Now set the current block index. self._current_block_idx = block_idx - t1 = t0 + node.op.duration + t1 = t0 + node.op.duration # pylint: disable=invalid-name self._block_duration = max(self._block_duration, t1) for bit in node.qargs: diff --git a/qiskit_ibm_provider/transpiler/passes/scheduling/scheduler.py b/qiskit_ibm_provider/transpiler/passes/scheduling/scheduler.py index a593b3015..3844fe8db 100644 --- a/qiskit_ibm_provider/transpiler/passes/scheduling/scheduler.py +++ b/qiskit_ibm_provider/transpiler/passes/scheduling/scheduler.py @@ -24,19 +24,23 @@ class DynamicCircuitScheduleAnalysis(BaseScheduler): """Dynamic circuits scheduling analysis pass. This is a scheduler designed to work for the unique scheduling constraints of the dynamic circuits - backends due to the limitations imposed by hardware. This is expected to evolve overtime as the + backends due to the limitations imposed by hardware. This is expected to evolve over time as the dynamic circuit backends also change. - In its current form this is slow to Qiskit's ASAP scheduler in which instructions start asas early as possible. + In its current form this is slow to Qiskit's ASAP scheduler in which instructions + start as early as possible. The primary differences are that: - * Measurements currently trigger the end of a "quantum block". The period between the end of the block and the next is *indeterministic* - ie., we do not know when the next block will begin (as we could be evaluating a classical function of indeterministic length) and - therefore the next block starts at a *relative* t=0. + * Measurements currently trigger the end of a "quantum block". The period between the end + of the block and the next is *nondeterministic* + ie., we do not know when the next block will begin (as we could be evaluating a classical + function of nondeterministic length) and therefore the + next block starts at a *relative* t=0. * It is possible to apply gates during a measurement. - * Measurements on disjoint qubits happen simulataneously and are part of the same block. Measurements that are not lexigraphically - neighbors in the generated QASM3 will happen in separate blocks. + * Measurements on disjoint qubits happen simultaneously and are part of the same block. + Measurements that are not lexicographically neighbors in the generated QASM3 will + happen in separate blocks. """ @@ -64,8 +68,6 @@ def run(self, dag): """Run the ASAPSchedule pass on `dag`. Args: dag (DAGCircuit): DAG to schedule. - Returns: - DAGCircuit: A scheduled DAG. Raises: TranspilerError: if the circuit is not mapped on physical qubits. TranspilerError: if conditional bit is added to non-supported instruction. @@ -85,15 +87,12 @@ def _init_run(self, dag): if len(dag.qregs) != 1 or dag.qregs.get("q", None) is None: raise TranspilerError("ASAP schedule runs on physical circuits only") - self._conditional_latency = self.property_set.get("conditional_latency", 0) - self._clbit_write_latency = self.property_set.get("clbit_write_latency", 0) - - self._node_start_time = dict() + self._node_start_time = {} self._idle_after = {q: (0, 0) for q in dag.qubits + dag.clbits} self._current_block_measures = set() self._bit_indices = {q: index for index, q in enumerate(dag.qubits)} - def _get_node_duration(self, node): + def _get_duration(self, node): return super()._get_node_duration(node, self._bit_indices, self._dag) def _visit_node(self, node): @@ -130,7 +129,7 @@ def _visit_conditional_node(self, node): # Trigger the start of a conditional block self._begin_new_circuit_block() - op_duration = self._get_node_duration(node) + op_duration = self._get_duration(node) t0q = max(self._idle_after[q][1] for q in node.qargs) # conditional is bit tricky due to conditional_latency @@ -157,9 +156,9 @@ def _visit_conditional_node(self, node): self._idle_after[bit] = (self._current_block_idx, t1c) # It starts after register read access - t0 = max(t0q, t1c) + t0 = max(t0q, t1c) # pylint: disable=invalid-name - t1 = t0 + op_duration + t1 = t0 + op_duration # pylint: disable=invalid-name self._update_idles(node, t0, t1) # Terminate the conditional block @@ -175,13 +174,12 @@ def _visit_measure(self, node): Measurement currently triggers the end of a pulse block in IBM dynamic circuits hardware. This means that it is possible to schedule *up to* a measurement (and during its pulses) but the measurement will be followed by a period of indeterminism. - All measurements on disjoint qubits will be collected on the same qubits to be run simultaneously. - - """ + All measurements on disjoint qubits will be collected on the same qubits + to be run simultaneously.""" current_block_measure_qargs = self._current_block_measure_qargs() measure_qargs = set(node.qargs) - t0q = max(self._idle_after[q][1] for q in measure_qargs) + t0q = max(self._idle_after[q][1] for q in measure_qargs) # pylint: disable=invalid-name # If the measurement qubits overlap, we need to start a new scheduling block. if current_block_measure_qargs & measure_qargs: @@ -189,7 +187,7 @@ def _visit_measure(self, node): t0q = 0 # Otherwise we need to increment all measurements to start at the same time within the block. else: - t0q = max( + t0q = max( # pylint: disable=invalid-name itertools.chain( [t0q], ( @@ -207,12 +205,12 @@ def _visit_measure(self, node): self._current_block_measures.add(node) for measure in self._current_block_measures: - t0 = t0q + t0 = t0q # pylint: disable=invalid-name bit_indices = {bit: index for index, bit in enumerate(self._dag.qubits)} measure_duration = self.durations.get( Measure(), [bit_indices[qarg] for qarg in node.qargs], unit="dt" ) - t1 = t0 + measure_duration + t1 = t0 + measure_duration # pylint: disable=invalid-name self._update_idles(measure, t0, t1) def _visit_reset(self, node): @@ -230,14 +228,14 @@ def _visit_reset(self, node): def _visit_generic(self, node): """Visit a generic node such as a gate or barrier.""" - op_duration = self._get_node_duration(node) + op_duration = self._get_duration(node) # It happens to be directives such as barrier - t0 = max(self._idle_after[bit][1] for bit in node.qargs + node.cargs) - t1 = t0 + op_duration + t0 = max(self._idle_after[bit][1] for bit in node.qargs + node.cargs) # pylint: disable=invalid-name + t1 = t0 + op_duration # pylint: disable=invalid-name self._update_idles(node, t0, t1) - def _update_idles(self, node, t0, t1): + def _update_idles(self, node, t0, t1): # pylint: disable=invalid-name for bit in node.qargs: self._idle_after[bit] = (self._current_block_idx, t1) diff --git a/test/transpiler/passes/scheduling/test_schedule.py b/test/transpiler/passes/scheduling/test_schedule.py index 64f71d4a3..1e2b61ce7 100644 --- a/test/transpiler/passes/scheduling/test_schedule.py +++ b/test/transpiler/passes/scheduling/test_schedule.py @@ -24,6 +24,7 @@ DynamicCircuitScheduleAnalysis, ) +# pylint: disable=invalid-name class TestSchedulingAndPaddingPass(QiskitTestCase): """Tests the Scheduling passes""" @@ -70,7 +71,8 @@ def test_measure_after_measure(self): self.assertEqual(expected, scheduled) def test_measure_block_end(self): - """Tests that measures trigger the end of a scheduling block and that measurements are grouped by block.""" + """Tests that measures trigger the end of a scheduling block and + that measurements are grouped by block.""" qc = QuantumCircuit(3, 1) qc.x(0) qc.measure(0, 0) @@ -122,10 +124,11 @@ def test_c_if_on_different_qubits(self): self.assertEqual(expected, scheduled) def test_shorter_measure_after_measure(self): - """Test if schedules circuits with shorter measure after measure with a common clbit. + """Test if schedules circuits with shorter measure after measure + with a common clbit. - Note: For dynamic circuits support we currently group measurements to start at the same time which in turn trigger - the end of a block.""" + Note: For dynamic circuits support we currently group measurements + to start at the same time which in turn trigger the end of a block.""" qc = QuantumCircuit(3, 1) qc.measure(0, 0) qc.measure(1, 0) @@ -388,10 +391,10 @@ def test_no_pad_very_end_of_circuit(self): self.assertEqual(expected, scheduled) def test_reset_terminates_block(self): - """Test if reset operations terminate the block scheduled + """Test if reset operations terminate the block scheduled. - Note: For dynamic circuits support we currently group resets to start at the same time which in turn trigger - the end of a block.""" + Note: For dynamic circuits support we currently group resets + to start at the same time which in turn trigger the end of a block.""" qc = QuantumCircuit(3, 1) qc.x(0) qc.reset(0) @@ -429,10 +432,10 @@ def test_reset_terminates_block(self): self.assertEqual(expected, scheduled) def test_reset_merged_with_measure(self): - """Test if reset operations terminate the block scheduled + """Test if reset operations terminate the block scheduled. - Note: For dynamic circuits support we currently group resets to start at the same time which in turn trigger - the end of a block.""" + Note: For dynamic circuits support we currently group resets to start + at the same time which in turn trigger the end of a block.""" qc = QuantumCircuit(3, 1) qc.x(0) qc.reset(0) From 42c8bc8db3f0399eab7a406ea472cd4ae4611d00 Mon Sep 17 00:00:00 2001 From: Thomas Alexander Date: Tue, 5 Jul 2022 23:33:58 -0300 Subject: [PATCH 21/70] Blackify. --- .../passes/scheduling/base_padding.py | 4 ++-- .../transpiler/passes/scheduling/scheduler.py | 22 +++++++++++-------- .../passes/scheduling/test_schedule.py | 1 + 3 files changed, 16 insertions(+), 11 deletions(-) diff --git a/qiskit_ibm_provider/transpiler/passes/scheduling/base_padding.py b/qiskit_ibm_provider/transpiler/passes/scheduling/base_padding.py index 160b2ceb7..ac80b7fcf 100644 --- a/qiskit_ibm_provider/transpiler/passes/scheduling/base_padding.py +++ b/qiskit_ibm_provider/transpiler/passes/scheduling/base_padding.py @@ -185,7 +185,7 @@ def _visit_delay(self, node): def _visit_generic(self, node): """Visit a generic node to pad.""" - block_idx, t0 = self._node_start_time[node] # pylint: disable=invalid-name + block_idx, t0 = self._node_start_time[node] # pylint: disable=invalid-name # Trigger the end of a block if block_idx > self._current_block_idx: @@ -199,7 +199,7 @@ def _visit_generic(self, node): # Now set the current block index. self._current_block_idx = block_idx - t1 = t0 + node.op.duration # pylint: disable=invalid-name + t1 = t0 + node.op.duration # pylint: disable=invalid-name self._block_duration = max(self._block_duration, t1) for bit in node.qargs: diff --git a/qiskit_ibm_provider/transpiler/passes/scheduling/scheduler.py b/qiskit_ibm_provider/transpiler/passes/scheduling/scheduler.py index 3844fe8db..145676d0c 100644 --- a/qiskit_ibm_provider/transpiler/passes/scheduling/scheduler.py +++ b/qiskit_ibm_provider/transpiler/passes/scheduling/scheduler.py @@ -156,9 +156,9 @@ def _visit_conditional_node(self, node): self._idle_after[bit] = (self._current_block_idx, t1c) # It starts after register read access - t0 = max(t0q, t1c) # pylint: disable=invalid-name + t0 = max(t0q, t1c) # pylint: disable=invalid-name - t1 = t0 + op_duration # pylint: disable=invalid-name + t1 = t0 + op_duration # pylint: disable=invalid-name self._update_idles(node, t0, t1) # Terminate the conditional block @@ -179,7 +179,9 @@ def _visit_measure(self, node): current_block_measure_qargs = self._current_block_measure_qargs() measure_qargs = set(node.qargs) - t0q = max(self._idle_after[q][1] for q in measure_qargs) # pylint: disable=invalid-name + t0q = max( + self._idle_after[q][1] for q in measure_qargs + ) # pylint: disable=invalid-name # If the measurement qubits overlap, we need to start a new scheduling block. if current_block_measure_qargs & measure_qargs: @@ -187,7 +189,7 @@ def _visit_measure(self, node): t0q = 0 # Otherwise we need to increment all measurements to start at the same time within the block. else: - t0q = max( # pylint: disable=invalid-name + t0q = max( # pylint: disable=invalid-name itertools.chain( [t0q], ( @@ -205,12 +207,12 @@ def _visit_measure(self, node): self._current_block_measures.add(node) for measure in self._current_block_measures: - t0 = t0q # pylint: disable=invalid-name + t0 = t0q # pylint: disable=invalid-name bit_indices = {bit: index for index, bit in enumerate(self._dag.qubits)} measure_duration = self.durations.get( Measure(), [bit_indices[qarg] for qarg in node.qargs], unit="dt" ) - t1 = t0 + measure_duration # pylint: disable=invalid-name + t1 = t0 + measure_duration # pylint: disable=invalid-name self._update_idles(measure, t0, t1) def _visit_reset(self, node): @@ -231,11 +233,13 @@ def _visit_generic(self, node): op_duration = self._get_duration(node) # It happens to be directives such as barrier - t0 = max(self._idle_after[bit][1] for bit in node.qargs + node.cargs) # pylint: disable=invalid-name - t1 = t0 + op_duration # pylint: disable=invalid-name + t0 = max( + self._idle_after[bit][1] for bit in node.qargs + node.cargs + ) # pylint: disable=invalid-name + t1 = t0 + op_duration # pylint: disable=invalid-name self._update_idles(node, t0, t1) - def _update_idles(self, node, t0, t1): # pylint: disable=invalid-name + def _update_idles(self, node, t0, t1): # pylint: disable=invalid-name for bit in node.qargs: self._idle_after[bit] = (self._current_block_idx, t1) diff --git a/test/transpiler/passes/scheduling/test_schedule.py b/test/transpiler/passes/scheduling/test_schedule.py index 1e2b61ce7..d8ea6096a 100644 --- a/test/transpiler/passes/scheduling/test_schedule.py +++ b/test/transpiler/passes/scheduling/test_schedule.py @@ -26,6 +26,7 @@ # pylint: disable=invalid-name + class TestSchedulingAndPaddingPass(QiskitTestCase): """Tests the Scheduling passes""" From 615f03c91865d91d1d6c354cabf988aafe2a9901 Mon Sep 17 00:00:00 2001 From: Thomas Alexander Date: Tue, 5 Jul 2022 23:36:03 -0300 Subject: [PATCH 22/70] Reach black and pylint fixed point. --- qiskit_ibm_provider/transpiler/passes/scheduling/scheduler.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/qiskit_ibm_provider/transpiler/passes/scheduling/scheduler.py b/qiskit_ibm_provider/transpiler/passes/scheduling/scheduler.py index 145676d0c..373ec639f 100644 --- a/qiskit_ibm_provider/transpiler/passes/scheduling/scheduler.py +++ b/qiskit_ibm_provider/transpiler/passes/scheduling/scheduler.py @@ -233,9 +233,9 @@ def _visit_generic(self, node): op_duration = self._get_duration(node) # It happens to be directives such as barrier - t0 = max( + t0 = max( # pylint: disable=invalid-name self._idle_after[bit][1] for bit in node.qargs + node.cargs - ) # pylint: disable=invalid-name + ) t1 = t0 + op_duration # pylint: disable=invalid-name self._update_idles(node, t0, t1) From 70ae09da80e69a77995ff4befc2c63ccd9765890 Mon Sep 17 00:00:00 2001 From: Thomas Alexander Date: Tue, 5 Jul 2022 23:52:27 -0300 Subject: [PATCH 23/70] Mypyify the code. --- .../passes/scheduling/base_padding.py | 24 ++++++------ .../transpiler/passes/scheduling/pad_delay.py | 2 +- .../transpiler/passes/scheduling/scheduler.py | 38 ++++++++++--------- 3 files changed, 34 insertions(+), 30 deletions(-) diff --git a/qiskit_ibm_provider/transpiler/passes/scheduling/base_padding.py b/qiskit_ibm_provider/transpiler/passes/scheduling/base_padding.py index ac80b7fcf..61de6f284 100644 --- a/qiskit_ibm_provider/transpiler/passes/scheduling/base_padding.py +++ b/qiskit_ibm_provider/transpiler/passes/scheduling/base_padding.py @@ -12,7 +12,7 @@ """Padding pass to fill timeslots for IBM (dynamic circuit) backends.""" -from typing import List, Optional, Union +from typing import Any, Dict, List, Optional, Union from qiskit.circuit import Qubit, Clbit, Instruction from qiskit.circuit.library import Barrier @@ -50,9 +50,9 @@ class BasePadding(TransformationPass): which may result in violation of hardware alignment constraints. """ - def __init__(self): + def __init__(self) -> None: self._node_start_time = None - self._idle_after = None + self._idle_after: Optional[Dict[Qubit, int]] = None self._dag = None self._block_duration = 0 self._current_block_idx = 0 @@ -60,7 +60,7 @@ def __init__(self): super().__init__() - def run(self, dag: DAGCircuit): + def run(self, dag: DAGCircuit) -> DAGCircuit: """Run the padding pass on ``dag``. Args: @@ -99,7 +99,7 @@ def run(self, dag: DAGCircuit): return self._dag - def _init_run(self, dag): + def _init_run(self, dag: DAGCircuit) -> None: """Setup for initial run.""" self._node_start_time = self.property_set["node_start_time"].copy() self._idle_after = {bit: 0 for bit in dag.qubits} @@ -125,7 +125,7 @@ def _init_run(self, dag): self._dag.calibrations = dag.calibrations self._dag.global_phase = dag.global_phase - def _pre_runhook(self, dag: DAGCircuit): + def _pre_runhook(self, dag: DAGCircuit) -> None: """Extra routine inserted before running the padding pass. Args: @@ -148,7 +148,7 @@ def _pad( t_end: int, next_node: DAGNode, prev_node: DAGNode, - ): + ) -> None: """Interleave instruction sequence in between two nodes. .. note:: @@ -176,14 +176,14 @@ def _pad( """ raise NotImplementedError - def _visit_delay(self, node): + def _visit_delay(self, node: DAGNode) -> None: """The padding class considers a delay instruction as idle time rather than instruction. Delay node is not added so that we can extract non-delay predecessors. """ pass - def _visit_generic(self, node): + def _visit_generic(self, node: DAGNode) -> None: """Visit a generic node to pad.""" block_idx, t0 = self._node_start_time[node] # pylint: disable=invalid-name @@ -220,7 +220,7 @@ def _visit_generic(self, node): self._apply_scheduled_op(block_idx, t0, node.op, node.qargs, node.cargs) - def _terminate_block(self, block_duration, block_idx): + def _terminate_block(self, block_duration: int, block_idx: int) -> None: """Terminate the end of a block scheduling region.""" # Update all other qubits as not idle so that delays are *not* # inserted. This is because we need the delays to be inserted in @@ -243,7 +243,7 @@ def _terminate_block(self, block_duration, block_idx): self._block_duration = 0 self._conditional_block = False - def _pad_until_block_end(self, block_duration, block_idx): + def _pad_until_block_end(self, block_duration: int, block_idx: int) -> None: # Add delays until the end of circuit. for bit in self._dag.qubits: if block_duration - self._idle_after[bit] > 0: @@ -265,7 +265,7 @@ def _apply_scheduled_op( oper: Instruction, qubits: Union[Qubit, List[Qubit]], clbits: Optional[Union[Clbit, List[Clbit]]] = None, - ): + ) -> None: """Add new operation to DAG with scheduled information. This is identical to apply_operation_back + updating the node_start_time propety. diff --git a/qiskit_ibm_provider/transpiler/passes/scheduling/pad_delay.py b/qiskit_ibm_provider/transpiler/passes/scheduling/pad_delay.py index a7e3824e5..f30cbed03 100644 --- a/qiskit_ibm_provider/transpiler/passes/scheduling/pad_delay.py +++ b/qiskit_ibm_provider/transpiler/passes/scheduling/pad_delay.py @@ -67,7 +67,7 @@ def _pad( t_end: int, next_node: DAGNode, prev_node: DAGNode, - ): + ) -> None: if not self.fill_very_end and isinstance(next_node, DAGOutNode): return diff --git a/qiskit_ibm_provider/transpiler/passes/scheduling/scheduler.py b/qiskit_ibm_provider/transpiler/passes/scheduling/scheduler.py index 373ec639f..bcdaf673e 100644 --- a/qiskit_ibm_provider/transpiler/passes/scheduling/scheduler.py +++ b/qiskit_ibm_provider/transpiler/passes/scheduling/scheduler.py @@ -12,10 +12,12 @@ """Scheduler for dynamic circuit backends.""" +from typing import Dict, Optional, Union, Set, Tuple import itertools import qiskit -from qiskit.circuit import Measure, Reset +from qiskit.circuit import Clbit, Measure, Qubit, Reset +from qiskit.dagcircuit import DAGCircuit, DAGNode from qiskit.transpiler.exceptions import TranspilerError from qiskit.transpiler.passes.scheduling.scheduling.base_scheduler import BaseScheduler @@ -46,7 +48,7 @@ class DynamicCircuitScheduleAnalysis(BaseScheduler): def __init__( self, durations: qiskit.transpiler.instruction_durations.InstructionDurations - ): + ) -> None: """Scheduler for dynamic circuit backends. Args: @@ -57,14 +59,14 @@ def __init__( self._current_block_idx = 0 - self._node_start_time = None - self._idle_after = None - self._current_block_measures = None - self._bit_indices = None + self._node_start_time: Optional[Dict[DAGNode, Tuple[int, int]]] = None + self._idle_after: Optional[Dict[Union[Qubit, Clbit], Tuple[int, int]]] = None + self._current_block_measures: Set[DAGNode] = set() + self._bit_indices: Optional[Dict[Qubit, int]] = None super().__init__(durations) - def run(self, dag): + def run(self, dag: DAGCircuit) -> None: """Run the ASAPSchedule pass on `dag`. Args: dag (DAGCircuit): DAG to schedule. @@ -79,7 +81,7 @@ def run(self, dag): self.property_set["node_start_time"] = self._node_start_time - def _init_run(self, dag): + def _init_run(self, dag: DAGCircuit) -> None: """Setup for initial run.""" self._dag = dag @@ -92,10 +94,10 @@ def _init_run(self, dag): self._current_block_measures = set() self._bit_indices = {q: index for index, q in enumerate(dag.qubits)} - def _get_duration(self, node): + def _get_duration(self, node: DAGNode) -> int: return super()._get_node_duration(node, self._bit_indices, self._dag) - def _visit_node(self, node): + def _visit_node(self, node: DAGNode) -> None: # compute t0, t1: instruction interval, note that # t0: start time of instruction # t1: end time of instruction @@ -114,7 +116,7 @@ def _visit_node(self, node): else: self._visit_generic(node) - def _visit_conditional_node(self, node): + def _visit_conditional_node(self, node: DAGNode) -> None: """Handling case of a conditional execution. Conditional execution durations are currently non-deterministic. as we do not know @@ -168,7 +170,7 @@ def _visit_conditional_node(self, node): # Fall through to generic case if not conditional self._visit_generic(node) - def _visit_measure(self, node): + def _visit_measure(self, node: DAGNode) -> None: """Visit a measurement node. Measurement currently triggers the end of a pulse block in IBM dynamic circuits hardware. @@ -215,7 +217,7 @@ def _visit_measure(self, node): t1 = t0 + measure_duration # pylint: disable=invalid-name self._update_idles(measure, t0, t1) - def _visit_reset(self, node): + def _visit_reset(self, node: DAGNode) -> None: """Visit a reset node. Reset currently triggers the end of a pulse block in IBM dynamic circuits hardware @@ -228,7 +230,7 @@ def _visit_reset(self, node): """ self._visit_measure(node) - def _visit_generic(self, node): + def _visit_generic(self, node: DAGNode) -> None: """Visit a generic node such as a gate or barrier.""" op_duration = self._get_duration(node) @@ -239,7 +241,9 @@ def _visit_generic(self, node): t1 = t0 + op_duration # pylint: disable=invalid-name self._update_idles(node, t0, t1) - def _update_idles(self, node, t0, t1): # pylint: disable=invalid-name + def _update_idles( + self, node: DAGNode, t0: int, t1: int # pylint: disable=invalid-name + ) -> None: for bit in node.qargs: self._idle_after[bit] = (self._current_block_idx, t1) @@ -248,13 +252,13 @@ def _update_idles(self, node, t0, t1): # pylint: disable=invalid-name self._node_start_time[node] = (self._current_block_idx, t0) - def _begin_new_circuit_block(self): + def _begin_new_circuit_block(self) -> None: """Create a new timed circuit block completing the previous block.""" self._current_block_idx += 1 self._current_block_measures = set() self._idle_after = {q: (0, 0) for q in self._dag.qubits + self._dag.clbits} - def _current_block_measure_qargs(self): + def _current_block_measure_qargs(self) -> Set[Qubit]: return set( qarg for measure in self._current_block_measures for qarg in measure.qargs ) From b4f8f9b0c81aaa207e4cfab783d0672a3965b6c4 Mon Sep 17 00:00:00 2001 From: Thomas Alexander Date: Fri, 17 Jun 2022 10:37:31 -0300 Subject: [PATCH 24/70] Add initial scaffolding for dynamic_circuits module. --- .../dynamic_circuits/__init__.py | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 qiskit_ibm_provider/dynamic_circuits/__init__.py diff --git a/qiskit_ibm_provider/dynamic_circuits/__init__.py b/qiskit_ibm_provider/dynamic_circuits/__init__.py new file mode 100644 index 000000000..09c169da8 --- /dev/null +++ b/qiskit_ibm_provider/dynamic_circuits/__init__.py @@ -0,0 +1,51 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2022. +# +# 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. + +""" +============================================================== +Dynamic Circuits (:mod:`qiskit_ibm_provider.dynamic_circuits`) +============================================================== + +.. currentmodule:: qiskit_ibm_provider.dynamic_circuits + +A collection of tools for working with IBM Quantum's next-generation +backends that support advanced "dynamic circuit" capabilities. Ie., +circuits with support for classical control-flow/feedback based off +of measurement results. + +Example Usage on a Supporting Backend +===================================== + +.. jupyter-execute:: + :hide-code: + :hide-output: + + from qiskit_ibm_provider.test.ibm_provider_mock import mock_get_backend + mock_get_backend('FakePerth') + +.. jupyter-execute:: + + from qiskit_ibm_provider import IBMProvider + import qiskit_ibm_provider.jupyter + + provider = IBMProvider(hub='ibm-q') + backend = provider.get_backend('ibm_perth') + + + +Scheduling & Dynamical Decoupling +================================= +.. autosummary:: + :toctree: ../stubs/ + + +""" From af86472ec5b901581cc1f2d9acbf745dbf59744d Mon Sep 17 00:00:00 2001 From: Thomas Alexander Date: Fri, 17 Jun 2022 10:43:47 -0300 Subject: [PATCH 25/70] Add a dummy test. --- .../dynamic_circuits/__init__.py | 2 ++ test/dynamic_circuits/__init__.py | 11 ++++++++++ test/dynamic_circuits/test_tests.py | 20 +++++++++++++++++++ 3 files changed, 33 insertions(+) create mode 100644 test/dynamic_circuits/__init__.py create mode 100644 test/dynamic_circuits/test_tests.py diff --git a/qiskit_ibm_provider/dynamic_circuits/__init__.py b/qiskit_ibm_provider/dynamic_circuits/__init__.py index 09c169da8..bf1d122b6 100644 --- a/qiskit_ibm_provider/dynamic_circuits/__init__.py +++ b/qiskit_ibm_provider/dynamic_circuits/__init__.py @@ -49,3 +49,5 @@ """ + +foo = 1 diff --git a/test/dynamic_circuits/__init__.py b/test/dynamic_circuits/__init__.py new file mode 100644 index 000000000..fdb172d36 --- /dev/null +++ b/test/dynamic_circuits/__init__.py @@ -0,0 +1,11 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2022. +# +# 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. diff --git a/test/dynamic_circuits/test_tests.py b/test/dynamic_circuits/test_tests.py new file mode 100644 index 000000000..91b42c978 --- /dev/null +++ b/test/dynamic_circuits/test_tests.py @@ -0,0 +1,20 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2022. +# +# 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. + +from qiskit_ibm_provider import dynamic_circuits + +from ..ibm_test_case import IBMTestCase + +class TestBasicServerPaths(IBMTestCase): + + def test_tests(self): + self.assertEqual(dynamic_circuits.foo, 1) From b42cf0c51be013b8a1ea5b910ac45d94992e15bd Mon Sep 17 00:00:00 2001 From: Thomas Alexander Date: Fri, 17 Jun 2022 11:36:28 -0300 Subject: [PATCH 26/70] Add reno. --- .../add-dynamic-circuits-module-af6f530072c82f44.yaml | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 releasenotes/notes/add-dynamic-circuits-module-af6f530072c82f44.yaml diff --git a/releasenotes/notes/add-dynamic-circuits-module-af6f530072c82f44.yaml b/releasenotes/notes/add-dynamic-circuits-module-af6f530072c82f44.yaml new file mode 100644 index 000000000..de65fd8ef --- /dev/null +++ b/releasenotes/notes/add-dynamic-circuits-module-af6f530072c82f44.yaml @@ -0,0 +1,7 @@ +--- +features: + - | + A new dynamic circuits module has been added to :mod:`qiskit_ibm_provider.dynamic_circuits`. + It will contain all specialized Qiskit routines for running applications on IBM's next-generation + quantum devices that support dynamic capabilities such as control-flow(feedforward) and classical + compute. From 0bef47b099ce46264a868ad3df1388cfcf5bf5dd Mon Sep 17 00:00:00 2001 From: Thomas Alexander Date: Fri, 17 Jun 2022 17:25:27 -0300 Subject: [PATCH 27/70] Add initial qiskit DynamicalDecoupling scheduling scaffolding and tests. --- .../dynamic_circuits/__init__.py | 2 - .../dynamic_circuits/schedule.py | 142 ++++ requirements-dev.txt | 1 + test/dynamic_circuits/test_schedule.py | 626 ++++++++++++++++++ 4 files changed, 769 insertions(+), 2 deletions(-) create mode 100644 qiskit_ibm_provider/dynamic_circuits/schedule.py create mode 100644 test/dynamic_circuits/test_schedule.py diff --git a/qiskit_ibm_provider/dynamic_circuits/__init__.py b/qiskit_ibm_provider/dynamic_circuits/__init__.py index bf1d122b6..09c169da8 100644 --- a/qiskit_ibm_provider/dynamic_circuits/__init__.py +++ b/qiskit_ibm_provider/dynamic_circuits/__init__.py @@ -49,5 +49,3 @@ """ - -foo = 1 diff --git a/qiskit_ibm_provider/dynamic_circuits/schedule.py b/qiskit_ibm_provider/dynamic_circuits/schedule.py new file mode 100644 index 000000000..6e652941d --- /dev/null +++ b/qiskit_ibm_provider/dynamic_circuits/schedule.py @@ -0,0 +1,142 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2022. +# +# 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. + +"""Scheduler for dynamic circuit backends.""" + +from qiskit.circuit import Measure +from qiskit.transpiler.exceptions import TranspilerError + +from qiskit.transpiler.passes.scheduling.scheduling.base_scheduler import BaseScheduler + + +class DynamicCircuitScheduleAnalysis(BaseScheduler): + """Dynamic circuits scheduling analysis pass. + + This is a scheduler designed to work for the unique scheduling constraints of the dynamic circuits + backends due to the limitations imposed by hardware. This is expected to evolve overtime as the + dynamic circuit backends also change. + + In its current form this is slow to Qiskit's ASAP scheduler in which instructions start asas early as possible. + + The primary differences are that: + + * Measurements currently trigger the end of a "quantum block". The period between the end of the block and the next is *indeterministic* + ie., we do not know when the next block will begin (as we could be evaluating a classical function of indeterministic length) and + therefore the next block starts at a *relative* t=0. + * It is possible to apply gates during a measurement. + * Measurements on disjoint qubits happen simulataneously and are part of the same block. Measurements that are not lexigraphically + neighbors in the generated QASM3 will happen in separate blocks. + + """ + + def run(self, dag): + """Run the ASAPSchedule pass on `dag`. + Args: + dag (DAGCircuit): DAG to schedule. + Returns: + DAGCircuit: A scheduled DAG. + Raises: + TranspilerError: if the circuit is not mapped on physical qubits. + TranspilerError: if conditional bit is added to non-supported instruction. + """ + if len(dag.qregs) != 1 or dag.qregs.get("q", None) is None: + raise TranspilerError("ASAP schedule runs on physical circuits only") + + conditional_latency = self.property_set.get("conditional_latency", 0) + clbit_write_latency = self.property_set.get("clbit_write_latency", 0) + + node_start_time = dict() + idle_after = {q: 0 for q in dag.qubits + dag.clbits} + bit_indices = {q: index for index, q in enumerate(dag.qubits)} + for node in dag.topological_op_nodes(): + op_duration = self._get_node_duration(node, bit_indices, dag) + + # compute t0, t1: instruction interval, note that + # t0: start time of instruction + # t1: end time of instruction + if isinstance(node.op, self.CONDITIONAL_SUPPORTED): + t0q = max(idle_after[q] for q in node.qargs) + if node.op.condition_bits: + # conditional is bit tricky due to conditional_latency + t0c = max(idle_after[bit] for bit in node.op.condition_bits) + if t0q > t0c: + # This is situation something like below + # + # |t0q + # Q ▒▒▒▒▒▒▒▒▒░░ + # C ▒▒▒░░░░░░░░ + # |t0c + # + # In this case, you can insert readout access before tq0 + # + # |t0q + # Q ▒▒▒▒▒▒▒▒▒▒▒ + # C ▒▒▒░░░▒▒░░░ + # |t0q - conditional_latency + # + t0c = max(t0q - conditional_latency, t0c) + t1c = t0c + conditional_latency + for bit in node.op.condition_bits: + # Lock clbit until state is read + idle_after[bit] = t1c + # It starts after register read access + t0 = max(t0q, t1c) + else: + t0 = t0q + t1 = t0 + op_duration + else: + if node.op.condition_bits: + raise TranspilerError( + f"Conditional instruction {node.op.name} is not supported in ASAP scheduler." + ) + + if isinstance(node.op, Measure): + # measure instruction handling is bit tricky due to clbit_write_latency + t0q = max(idle_after[q] for q in node.qargs) + t0c = max(idle_after[c] for c in node.cargs) + # Assume following case (t0c > t0q) + # + # |t0q + # Q ▒▒▒▒░░░░░░░░░░░░ + # C ▒▒▒▒▒▒▒▒░░░░░░░░ + # |t0c + # + # In this case, there is no actual clbit access until clbit_write_latency. + # The node t0 can be push backward by this amount. + # + # |t0q' = t0c - clbit_write_latency + # Q ▒▒▒▒░░▒▒▒▒▒▒▒▒▒▒ + # C ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ + # |t0c' = t0c + # + # rather than naively doing + # + # |t0q' = t0c + # Q ▒▒▒▒░░░░▒▒▒▒▒▒▒▒ + # C ▒▒▒▒▒▒▒▒░░░▒▒▒▒▒ + # |t0c' = t0c + clbit_write_latency + # + t0 = max(t0q, t0c - clbit_write_latency) + t1 = t0 + op_duration + for clbit in node.cargs: + idle_after[clbit] = t1 + else: + # It happens to be directives such as barrier + t0 = max(idle_after[bit] for bit in node.qargs + node.cargs) + t1 = t0 + op_duration + + for bit in node.qargs: + idle_after[bit] = t1 + + node_start_time[node] = t0 + + self.property_set["node_start_time"] = node_start_time diff --git a/requirements-dev.txt b/requirements-dev.txt index 83a255774..d417921ff 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -25,3 +25,4 @@ websockets>=8 black==22.3.0 coverage>=6.3 scikit-learn>=0.20.0 +ddt>=1.2.0,!=1.4.0,!=1.4.3 diff --git a/test/dynamic_circuits/test_schedule.py b/test/dynamic_circuits/test_schedule.py new file mode 100644 index 000000000..8e805e6d8 --- /dev/null +++ b/test/dynamic_circuits/test_schedule.py @@ -0,0 +1,626 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2022. +# +# 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 dynamic circuits scheduling analysis""" + +import unittest + +from ddt import ddt, data, unpack +from qiskit import QuantumCircuit +from qiskit.pulse import Schedule, Play, Constant, DriveChannel +from qiskit.test import QiskitTestCase +from qiskit.transpiler.instruction_durations import InstructionDurations +from qiskit.transpiler.passes import ( + PadDelay, + SetIOLatency, +) +from qiskit.transpiler.passmanager import PassManager +from qiskit.transpiler.exceptions import TranspilerError + +from qiskit_ibm_provider.dynamic_circuits.schedule import DynamicCircuitScheduleAnalysis + +@ddt +class TestSchedulingAndPaddingPass(QiskitTestCase): + """Tests the Scheduling passes""" + + def test_classically_controlled_gate_after_measure(self): + """Test if schedules circuits with c_if after measure with a common clbit. + See: https://github.com/Qiskit/qiskit-terra/issues/7654 + (input) + ┌─┐ + q_0: ┤M├─────────── + └╥┘ ┌───┐ + q_1: ─╫────┤ X ├─── + ║ └─╥─┘ + ║ ┌────╨────┐ + c: 1/═╩═╡ c_0 = T ╞ + 0 └─────────┘ + (scheduled) + ┌─┐┌────────────────┐ + q_0: ───────────────────┤M├┤ Delay(200[dt]) ├ + ┌─────────────────┐└╥┘└─────┬───┬──────┘ + q_1: ┤ Delay(1000[dt]) ├─╫───────┤ X ├─────── + └─────────────────┘ ║ └─╥─┘ + ║ ┌────╨────┐ + c: 1/════════════════════╩════╡ c_0=0x1 ╞════ + 0 └─────────┘ + """ + qc = QuantumCircuit(2, 1) + qc.measure(0, 0) + qc.x(1).c_if(0, True) + + durations = InstructionDurations([("x", None, 200), ("measure", None, 1000)]) + pm = PassManager([DynamicCircuitScheduleAnalysis(durations), PadDelay()]) + scheduled = pm.run(qc) + + expected = QuantumCircuit(2, 1) + expected.measure(0, 0) + expected.delay(1000, 1) # x.c_if starts after measure + expected.x(1).c_if(0, True) + expected.delay(200, 0) + + self.assertEqual(expected, scheduled) + + def test_measure_after_measure(self): + """Test if schedules circuits with measure after measure with a common clbit. + See: https://github.com/Qiskit/qiskit-terra/issues/7654 + (input) + ┌───┐┌─┐ + q_0: ┤ X ├┤M├─── + └───┘└╥┘┌─┐ + q_1: ──────╫─┤M├ + ║ └╥┘ + c: 1/══════╩══╩═ + 0 0 + (scheduled) + ┌───┐ ┌─┐┌─────────────────┐ + q_0: ───────┤ X ├───────┤M├┤ Delay(1000[dt]) ├ + ┌──────┴───┴──────┐└╥┘└───────┬─┬───────┘ + q_1: ┤ Delay(1200[dt]) ├─╫─────────┤M├──────── + └─────────────────┘ ║ └╥┘ + c: 1/════════════════════╩══════════╩═════════ + 0 0 + """ + qc = QuantumCircuit(2, 1) + qc.x(0) + qc.measure(0, 0) + qc.measure(1, 0) + + durations = InstructionDurations([("x", None, 200), ("measure", None, 1000)]) + pm = PassManager([DynamicCircuitScheduleAnalysis(durations), PadDelay()]) + scheduled = pm.run(qc) + + expected = QuantumCircuit(2, 1) + expected.x(0) + expected.measure(0, 0) + expected.delay(1200, 1) + expected.measure(1, 0) + expected.delay(1000, 0) + + self.assertEqual(expected, scheduled) + + def test_c_if_on_different_qubits(self): + """Test if schedules circuits with `c_if`s on different qubits. + (input) + ┌─┐ + q_0: ┤M├────────────────────── + └╥┘ ┌───┐ + q_1: ─╫────┤ X ├────────────── + ║ └─╥─┘ ┌───┐ + q_2: ─╫──────╫────────┤ X ├─── + ║ ║ └─╥─┘ + ║ ┌────╨────┐┌────╨────┐ + c: 1/═╩═╡ c_0 = T ╞╡ c_0 = T ╞ + 0 └─────────┘└─────────┘ + (scheduled) + ┌─┐┌────────────────┐ + q_0: ───────────────────┤M├┤ Delay(200[dt]) ├─────────── + ┌─────────────────┐└╥┘└─────┬───┬──────┘ + q_1: ┤ Delay(1000[dt]) ├─╫───────┤ X ├────────────────── + ├─────────────────┤ ║ └─╥─┘ ┌───┐ + q_2: ┤ Delay(1000[dt]) ├─╫─────────╫────────────┤ X ├─── + └─────────────────┘ ║ ║ └─╥─┘ + ║ ┌────╨────┐ ┌────╨────┐ + c: 1/════════════════════╩════╡ c_0=0x1 ╞════╡ c_0=0x1 ╞ + 0 └─────────┘ └─────────┘ + """ + qc = QuantumCircuit(3, 1) + qc.measure(0, 0) + qc.x(1).c_if(0, True) + qc.x(2).c_if(0, True) + + durations = InstructionDurations([("x", None, 200), ("measure", None, 1000)]) + pm = PassManager([DynamicCircuitScheduleAnalysis(durations), PadDelay()]) + scheduled = pm.run(qc) + + expected = QuantumCircuit(3, 1) + expected.measure(0, 0) + expected.delay(1000, 1) + expected.delay(1000, 2) + expected.x(1).c_if(0, True) + expected.x(2).c_if(0, True) + expected.delay(200, 0) + + self.assertEqual(expected, scheduled) + + def test_shorter_measure_after_measure(self): + """Test if schedules circuits with shorter measure after measure with a common clbit. + (input) + ┌─┐ + q_0: ┤M├─── + └╥┘┌─┐ + q_1: ─╫─┤M├ + ║ └╥┘ + c: 1/═╩══╩═ + 0 0 + (scheduled) + ┌─┐┌────────────────┐ + q_0: ───────────────────┤M├┤ Delay(700[dt]) ├ + ┌─────────────────┐└╥┘└──────┬─┬───────┘ + q_1: ┤ Delay(1000[dt]) ├─╫────────┤M├──────── + └─────────────────┘ ║ └╥┘ + c: 1/════════════════════╩═════════╩═════════ + 0 0 + """ + qc = QuantumCircuit(2, 1) + qc.measure(0, 0) + qc.measure(1, 0) + + durations = InstructionDurations([("measure", [0], 1000), ("measure", [1], 700)]) + pm = PassManager([DynamicCircuitScheduleAnalysis(durations), PadDelay()]) + scheduled = pm.run(qc) + + expected = QuantumCircuit(2, 1) + expected.measure(0, 0) + expected.delay(1000, 1) + expected.measure(1, 0) + expected.delay(700, 0) + + self.assertEqual(expected, scheduled) + + def test_measure_after_c_if(self): + """Test if schedules circuits with c_if after measure with a common clbit. + (input) + ┌─┐ + q_0: ┤M├────────────── + └╥┘ ┌───┐ + q_1: ─╫────┤ X ├────── + ║ └─╥─┘ ┌─┐ + q_2: ─╫──────╫─────┤M├ + ║ ┌────╨────┐└╥┘ + c: 1/═╩═╡ c_0 = T ╞═╩═ + 0 └─────────┘ 0 + (scheduled) + ┌─┐┌─────────────────┐ + q_0: ───────────────────┤M├┤ Delay(1000[dt]) ├────────────────── + ┌─────────────────┐└╥┘└──────┬───┬──────┘┌────────────────┐ + q_1: ┤ Delay(1000[dt]) ├─╫────────┤ X ├───────┤ Delay(800[dt]) ├ + ├─────────────────┤ ║ └─╥─┘ └──────┬─┬───────┘ + q_2: ┤ Delay(1000[dt]) ├─╫──────────╫────────────────┤M├──────── + └─────────────────┘ ║ ┌────╨────┐ └╥┘ + c: 1/════════════════════╩═════╡ c_0=0x1 ╞════════════╩═════════ + 0 └─────────┘ 0 + """ + qc = QuantumCircuit(3, 1) + qc.measure(0, 0) + qc.x(1).c_if(0, 1) + qc.measure(2, 0) + + durations = InstructionDurations([("x", None, 200), ("measure", None, 1000)]) + pm = PassManager([DynamicCircuitScheduleAnalysis(durations), PadDelay()]) + scheduled = pm.run(qc) + + expected = QuantumCircuit(3, 1) + expected.delay(1000, 1) + expected.delay(1000, 2) + expected.measure(0, 0) + expected.x(1).c_if(0, 1) + expected.measure(2, 0) + expected.delay(1000, 0) + expected.delay(800, 1) + + self.assertEqual(expected, scheduled) + + def test_parallel_gate_different_length(self): + """Test circuit having two parallel instruction with different length. + (input) + ┌───┐┌─┐ + q_0: ┤ X ├┤M├─── + ├───┤└╥┘┌─┐ + q_1: ┤ X ├─╫─┤M├ + └───┘ ║ └╥┘ + c: 2/══════╩══╩═ + 0 1 + (expected) + ┌───┐┌─┐┌────────────────┐ + q_0: ┤ X ├┤M├┤ Delay(200[dt]) ├ + ├───┤└╥┘└──────┬─┬───────┘ + q_1: ┤ X ├─╫────────┤M├──────── + └───┘ ║ └╥┘ + c: 2/══════╩═════════╩═════════ + 0 1 + """ + qc = QuantumCircuit(2, 2) + qc.x(0) + qc.x(1) + qc.measure(0, 0) + qc.measure(1, 1) + + durations = InstructionDurations( + [("x", [0], 200), ("x", [1], 400), ("measure", None, 1000)] + ) + + pm = PassManager([DynamicCircuitScheduleAnalysis(durations), PadDelay()]) + qc_sched = pm.run(qc) + + sched_expected = QuantumCircuit(2, 2) + sched_expected.x(0) + sched_expected.x(1) + sched_expected.measure(0, 0) # immediately start after X gate + sched_expected.measure(1, 1) + sched_expected.delay(200, 0) + + self.assertEqual(qc_sched, sched_expected) + + def test_parallel_gate_different_length_with_barrier(self): + """Test circuit having two parallel instruction with different length with barrier. + (input) + ┌───┐┌─┐ + q_0: ┤ X ├┤M├─── + ├───┤└╥┘┌─┐ + q_1: ┤ X ├─╫─┤M├ + └───┘ ║ └╥┘ + c: 2/══════╩══╩═ + 0 1 + (expected) + ┌───┐┌────────────────┐ ░ ┌─┐ + q_0: ┤ X ├┤ Delay(200[dt]) ├─░─┤M├─── + ├───┤└────────────────┘ ░ └╥┘┌─┐ + q_1: ┤ X ├───────────────────░──╫─┤M├ + └───┘ ░ ║ └╥┘ + c: 2/═══════════════════════════╩══╩═ + 0 1 + """ + qc = QuantumCircuit(2, 2) + qc.x(0) + qc.x(1) + qc.barrier() + qc.measure(0, 0) + qc.measure(1, 1) + + durations = InstructionDurations( + [("x", [0], 200), ("x", [1], 400), ("measure", None, 1000)] + ) + + pm = PassManager([DynamicCircuitScheduleAnalysis(durations), PadDelay()]) + qc_sched = pm.run(qc) + + sched_expected = QuantumCircuit(2, 2) + sched_expected.x(0) + sched_expected.delay(200, 0) + sched_expected.x(1) + sched_expected.barrier() + sched_expected.measure(0, 0) + sched_expected.measure(1, 1) + + self.assertEqual(qc_sched, sched_expected) + + def test_measure_after_c_if_on_edge_locking(self): + """Test if schedules circuits with c_if after measure with a common clbit. + The scheduler is configured to reproduce behavior of the 0.20.0, + in which clbit lock is applied to the end-edge of measure instruction. + See https://github.com/Qiskit/qiskit-terra/pull/7655 + (input) + ┌─┐ + q_0: ┤M├────────────── + └╥┘ ┌───┐ + q_1: ─╫────┤ X ├────── + ║ └─╥─┘ ┌─┐ + q_2: ─╫──────╫─────┤M├ + ║ ┌────╨────┐└╥┘ + c: 1/═╩═╡ c_0 = T ╞═╩═ + 0 └─────────┘ 0 + (scheduled) + ┌─┐┌────────────────┐ + q_0: ───────────────────┤M├┤ Delay(200[dt]) ├───────────────────── + ┌─────────────────┐└╥┘└─────┬───┬──────┘ + q_1: ┤ Delay(1000[dt]) ├─╫───────┤ X ├──────────────────────────── + └─────────────────┘ ║ └─╥─┘ ┌─┐┌────────────────┐ + q_2: ────────────────────╫─────────╫─────────┤M├┤ Delay(200[dt]) ├ + ║ ┌────╨────┐ └╥┘└────────────────┘ + c: 1/════════════════════╩════╡ c_0=0x1 ╞═════╩═══════════════════ + 0 └─────────┘ 0 + """ + qc = QuantumCircuit(3, 1) + qc.measure(0, 0) + qc.x(1).c_if(0, 1) + qc.measure(2, 0) + + durations = InstructionDurations([("x", None, 200), ("measure", None, 1000)]) + + # lock at the end edge + actual_sched = PassManager( + [ + SetIOLatency(clbit_write_latency=1000), + DynamicCircuitScheduleAnalysis(durations), + PadDelay(), + ] + ).run(qc) + + expected_sched = QuantumCircuit(3, 1) + expected_sched.measure(0, 0) + expected_sched.delay(1000, 1) + expected_sched.x(1).c_if(0, 1) + expected_sched.measure(2, 0) + expected_sched.delay(200, 0) + expected_sched.delay(200, 2) + self.assertEqual(expected_sched, actual_sched) + + @data([100, 200], [500, 0], [1000, 200]) + @unpack + def test_active_reset_circuit(self, write_lat, cond_lat): + """Test practical example of reset circuit. + Because of the stimulus pulse overlap with the previous XGate on the q register, + measure instruction is always triggered after XGate regardless of write latency. + Thus only conditional latency matters in the scheduling. + (input) + ┌─┐ ┌───┐ ┌─┐ ┌───┐ ┌─┐ ┌───┐ + q: ┤M├───┤ X ├───┤M├───┤ X ├───┤M├───┤ X ├─── + └╥┘ └─╥─┘ └╥┘ └─╥─┘ └╥┘ └─╥─┘ + ║ ┌────╨────┐ ║ ┌────╨────┐ ║ ┌────╨────┐ + c: 1/═╩═╡ c_0=0x1 ╞═╩═╡ c_0=0x1 ╞═╩═╡ c_0=0x1 ╞ + 0 └─────────┘ 0 └─────────┘ 0 └─────────┘ + """ + qc = QuantumCircuit(1, 1) + qc.measure(0, 0) + qc.x(0).c_if(0, 1) + qc.measure(0, 0) + qc.x(0).c_if(0, 1) + qc.measure(0, 0) + qc.x(0).c_if(0, 1) + + durations = InstructionDurations([("x", None, 100), ("measure", None, 1000)]) + + actual_sched = PassManager( + [ + SetIOLatency(clbit_write_latency=write_lat, conditional_latency=cond_lat), + DynamicCircuitScheduleAnalysis(durations), + PadDelay(), + ] + ).run(qc) + + expected = QuantumCircuit(1, 1) + expected.measure(0, 0) + if cond_lat > 0: + expected.delay(cond_lat, 0) + expected.x(0).c_if(0, 1) + expected.measure(0, 0) + if cond_lat > 0: + expected.delay(cond_lat, 0) + expected.x(0).c_if(0, 1) + expected.measure(0, 0) + if cond_lat > 0: + expected.delay(cond_lat, 0) + expected.x(0).c_if(0, 1) + + self.assertEqual(expected, actual_sched) + + def test_random_complicated_circuit(self): + """Test scheduling complicated circuit with control flow. + (input) + ┌────────────────┐ ┌───┐ ░ ┌───┐ » + q_0: ┤ Delay(100[dt]) ├───┤ X ├────░──────────────────┤ X ├───» + └────────────────┘ └─╥─┘ ░ ┌───┐ └─╥─┘ » + q_1: ───────────────────────╫──────░───────┤ X ├────────╫─────» + ║ ░ ┌─┐ └─╥─┘ ║ » + q_2: ───────────────────────╫──────░─┤M├─────╫──────────╫─────» + ┌────╨────┐ ░ └╥┘┌────╨────┐┌────╨────┐» + c: 1/══════════════════╡ c_0=0x1 ╞════╩═╡ c_0=0x0 ╞╡ c_0=0x0 ╞» + └─────────┘ 0 └─────────┘└─────────┘» + « ┌────────────────┐┌───┐ + «q_0: ┤ Delay(300[dt]) ├┤ X ├─────■───── + « └────────────────┘└───┘ ┌─┴─┐ + «q_1: ────────■─────────────────┤ X ├─── + « ┌─┴─┐ ┌─┐ └─╥─┘ + «q_2: ──────┤ X ├────────┤M├──────╫───── + « └───┘ └╥┘ ┌────╨────┐ + «c: 1/════════════════════╩══╡ c_0=0x0 ╞ + « 0 └─────────┘ + (scheduled) duration = 2800 dt + ┌────────────────┐ ┌───┐ ░ ┌─────────────────┐ » + q_0: ┤ Delay(200[dt]) ├───┤ X ├────░─┤ Delay(1400[dt]) ├───────────» + ├────────────────┤ └─╥─┘ ░ ├─────────────────┤ ┌───┐ » + q_1: ┤ Delay(300[dt]) ├─────╫──────░─┤ Delay(1200[dt]) ├───┤ X ├───» + ├────────────────┤ ║ ░ └───────┬─┬───────┘ └─╥─┘ » + q_2: ┤ Delay(300[dt]) ├─────╫──────░─────────┤M├─────────────╫─────» + └────────────────┘┌────╨────┐ ░ └╥┘ ┌────╨────┐» + c: 1/══════════════════╡ c_0=0x1 ╞════════════╩═════════╡ c_0=0x0 ╞» + └─────────┘ 0 └─────────┘» + « ┌───┐ ┌────────────────┐ ┌───┐ » + «q_0: ─────────────────────┤ X ├───┤ Delay(300[dt]) ├──────┤ X ├───────» + « └─╥─┘ └────────────────┘┌─────┴───┴──────┐» + «q_1: ───────────────────────╫─────────────■─────────┤ Delay(400[dt]) ├» + « ┌────────────────┐ ║ ┌─┴─┐ ├────────────────┤» + «q_2: ┤ Delay(300[dt]) ├─────╫───────────┤ X ├───────┤ Delay(300[dt]) ├» + « └────────────────┘┌────╨────┐ └───┘ └────────────────┘» + «c: 1/══════════════════╡ c_0=0x0 ╞════════════════════════════════════» + « └─────────┘ » + « ┌────────────────┐ + «q_0: ─────■─────┤ Delay(700[dt]) ├ + « ┌─┴─┐ ├────────────────┤ + «q_1: ───┤ X ├───┤ Delay(700[dt]) ├ + « └─╥─┘ └──────┬─┬───────┘ + «q_2: ─────╫────────────┤M├──────── + « ┌────╨────┐ └╥┘ + «c: 1/╡ c_0=0x0 ╞════════╩═════════ + « └─────────┘ 0 + """ + qc = QuantumCircuit(3, 1) + qc.delay(100, 0) + qc.x(0).c_if(0, 1) + qc.barrier() + qc.measure(2, 0) + qc.x(1).c_if(0, 0) + qc.x(0).c_if(0, 0) + qc.delay(300, 0) + qc.cx(1, 2) + qc.x(0) + qc.cx(0, 1).c_if(0, 0) + qc.measure(2, 0) + + durations = InstructionDurations( + [("x", None, 100), ("measure", None, 1000), ("cx", None, 200)] + ) + + actual_sched = PassManager( + [ + SetIOLatency(clbit_write_latency=100, conditional_latency=200), + DynamicCircuitScheduleAnalysis(durations), + PadDelay(), + ] + ).run(qc) + + expected_sched = QuantumCircuit(3, 1) + expected_sched.delay(200, 0) # due to conditional latency of 200dt + expected_sched.delay(300, 1) + expected_sched.delay(300, 2) + expected_sched.x(0).c_if(0, 1) + expected_sched.barrier() + expected_sched.delay(1400, 0) + expected_sched.delay(1200, 1) + expected_sched.measure(2, 0) + expected_sched.x(1).c_if(0, 0) + expected_sched.x(0).c_if(0, 0) + expected_sched.delay(300, 0) + expected_sched.x(0) + expected_sched.delay(300, 2) + expected_sched.cx(1, 2) + expected_sched.delay(400, 1) + expected_sched.cx(0, 1).c_if(0, 0) + expected_sched.delay(700, 0) # creg is released at t0 of cx(0,1).c_if(0,0) + expected_sched.delay( + 700, 1 + ) # no creg write until 100dt. thus measure can move left by 300dt. + expected_sched.delay(300, 2) + expected_sched.measure(2, 0) + self.assertEqual(expected_sched, actual_sched) + self.assertEqual(actual_sched.duration, 3100) + + def test_dag_introduces_extra_dependency_between_conditionals(self): + """Test dependency between conditional operations in the scheduling. + In the below example circuit, the conditional x on q1 could start at time 0, + however it must be scheduled after the conditional x on q0 in scheduling. + That is because circuit model used in the transpiler passes (DAGCircuit) + interprets instructions acting on common clbits must be run in the order + given by the original circuit (QuantumCircuit). + (input) + ┌────────────────┐ ┌───┐ + q_0: ┤ Delay(100[dt]) ├───┤ X ├─── + └─────┬───┬──────┘ └─╥─┘ + q_1: ──────┤ X ├────────────╫───── + └─╥─┘ ║ + ┌────╨────┐ ┌────╨────┐ + c: 1/═══╡ c_0=0x1 ╞════╡ c_0=0x1 ╞ + └─────────┘ └─────────┘ + (scheduled) + ┌────────────────┐ ┌───┐ + q_0: ┤ Delay(100[dt]) ├───┤ X ├────────────── + ├────────────────┤ └─╥─┘ ┌───┐ + q_1: ┤ Delay(100[dt]) ├─────╫────────┤ X ├─── + └────────────────┘ ║ └─╥─┘ + ┌────╨────┐┌────╨────┐ + c: 1/══════════════════╡ c_0=0x1 ╞╡ c_0=0x1 ╞ + └─────────┘└─────────┘ + """ + qc = QuantumCircuit(2, 1) + qc.delay(100, 0) + qc.x(0).c_if(0, True) + qc.x(1).c_if(0, True) + + durations = InstructionDurations([("x", None, 160)]) + pm = PassManager([DynamicCircuitScheduleAnalysis(durations), PadDelay()]) + scheduled = pm.run(qc) + + expected = QuantumCircuit(2, 1) + expected.delay(100, 0) + expected.delay(100, 1) # due to extra dependency on clbits + expected.x(0).c_if(0, True) + expected.x(1).c_if(0, True) + + self.assertEqual(expected, scheduled) + + def test_scheduling_with_calibration(self): + """Test if calibrated instruction can update node duration.""" + qc = QuantumCircuit(2) + qc.x(0) + qc.cx(0, 1) + qc.x(1) + qc.cx(0, 1) + + xsched = Schedule(Play(Constant(300, 0.1), DriveChannel(0))) + qc.add_calibration("x", (0,), xsched) + + durations = InstructionDurations([("x", None, 160), ("cx", None, 600)]) + pm = PassManager([DynamicCircuitScheduleAnalysis(durations), PadDelay()]) + scheduled = pm.run(qc) + + expected = QuantumCircuit(2) + expected.x(0) + expected.delay(300, 1) + expected.cx(0, 1) + expected.x(1) + expected.delay(160, 0) + expected.cx(0, 1) + expected.add_calibration("x", (0,), xsched) + + self.assertEqual(expected, scheduled) + + def test_padding_not_working_without_scheduling(self): + """Test padding fails when un-scheduled DAG is input.""" + qc = QuantumCircuit(1, 1) + qc.delay(100, 0) + qc.x(0) + qc.measure(0, 0) + + with self.assertRaises(TranspilerError): + PassManager(PadDelay()).run(qc) + + def test_no_pad_very_end_of_circuit(self): + """Test padding option that inserts no delay at the very end of circuit. + This circuit will be unchanged after scheduling/padding. + ┌────────────────┐┌─┐ + q_0: ┤ Delay(100[dt]) ├┤M├ + └─────┬───┬──────┘└╥┘ + q_1: ──────┤ X ├────────╫─ + └───┘ ║ + c: 1/═══════════════════╩═ + 0 + """ + qc = QuantumCircuit(2, 1) + qc.delay(100, 0) + qc.x(1) + qc.measure(0, 0) + + durations = InstructionDurations([("x", None, 160), ("measure", None, 1000)]) + + scheduled = PassManager( + [ + DynamicCircuitScheduleAnalysis(durations), + PadDelay(fill_very_end=False), + ] + ).run(qc) + + self.assertEqual(scheduled, qc) + + +if __name__ == "__main__": + unittest.main() From f97fa79a56d14fad2219b3bd32c6fc115cf892a7 Mon Sep 17 00:00:00 2001 From: Thomas Alexander Date: Sun, 19 Jun 2022 15:26:26 -0300 Subject: [PATCH 28/70] Refactor scheduler with visitor pattern. --- .../dynamic_circuits/schedule.py | 230 +++++++++++------- 1 file changed, 141 insertions(+), 89 deletions(-) diff --git a/qiskit_ibm_provider/dynamic_circuits/schedule.py b/qiskit_ibm_provider/dynamic_circuits/schedule.py index 6e652941d..38810644c 100644 --- a/qiskit_ibm_provider/dynamic_circuits/schedule.py +++ b/qiskit_ibm_provider/dynamic_circuits/schedule.py @@ -12,9 +12,9 @@ """Scheduler for dynamic circuit backends.""" +import qiskit from qiskit.circuit import Measure from qiskit.transpiler.exceptions import TranspilerError - from qiskit.transpiler.passes.scheduling.scheduling.base_scheduler import BaseScheduler @@ -38,6 +38,24 @@ class DynamicCircuitScheduleAnalysis(BaseScheduler): """ + def __init__(self, durations: qiskit.transpiler.instruction_durations.InstructionDurations): + """Scheduler for dynamic circuit backends. + + Args: + durations: Durations of instructions to be used in scheduling. + """ + + self._dag = None + + self._conditional_latency = 0 + self._clbit_write_latency = 0 + + self._node_start_time = None + self._idle_after = None + self._bit_indices = None + + super().__init__(durations) + def run(self, dag): """Run the ASAPSchedule pass on `dag`. Args: @@ -48,95 +66,129 @@ def run(self, dag): TranspilerError: if the circuit is not mapped on physical qubits. TranspilerError: if conditional bit is added to non-supported instruction. """ + self._init_run(dag) + + for node in dag.topological_op_nodes(): + self._visit_node(node) + + self.property_set["node_start_time"] = self._node_start_time + + def _init_run(self, dag): + """Setup for initial run.""" + + self._dag = dag + if len(dag.qregs) != 1 or dag.qregs.get("q", None) is None: raise TranspilerError("ASAP schedule runs on physical circuits only") - conditional_latency = self.property_set.get("conditional_latency", 0) - clbit_write_latency = self.property_set.get("clbit_write_latency", 0) - - node_start_time = dict() - idle_after = {q: 0 for q in dag.qubits + dag.clbits} - bit_indices = {q: index for index, q in enumerate(dag.qubits)} - for node in dag.topological_op_nodes(): - op_duration = self._get_node_duration(node, bit_indices, dag) - - # compute t0, t1: instruction interval, note that - # t0: start time of instruction - # t1: end time of instruction - if isinstance(node.op, self.CONDITIONAL_SUPPORTED): - t0q = max(idle_after[q] for q in node.qargs) - if node.op.condition_bits: - # conditional is bit tricky due to conditional_latency - t0c = max(idle_after[bit] for bit in node.op.condition_bits) - if t0q > t0c: - # This is situation something like below - # - # |t0q - # Q ▒▒▒▒▒▒▒▒▒░░ - # C ▒▒▒░░░░░░░░ - # |t0c - # - # In this case, you can insert readout access before tq0 - # - # |t0q - # Q ▒▒▒▒▒▒▒▒▒▒▒ - # C ▒▒▒░░░▒▒░░░ - # |t0q - conditional_latency - # - t0c = max(t0q - conditional_latency, t0c) - t1c = t0c + conditional_latency - for bit in node.op.condition_bits: - # Lock clbit until state is read - idle_after[bit] = t1c - # It starts after register read access - t0 = max(t0q, t1c) - else: - t0 = t0q - t1 = t0 + op_duration + self._conditional_latency = self.property_set.get("conditional_latency", 0) + self._clbit_write_latency = self.property_set.get("clbit_write_latency", 0) + + self._node_start_time = dict() + self._idle_after = {q: 0 for q in dag.qubits + dag.clbits} + self._bit_indices = {q: index for index, q in enumerate(dag.qubits)} + + def _get_node_duration(self, node): + return super()._get_node_duration(node, self._bit_indices, self._dag) + + def _visit_node(self, node): + # compute t0, t1: instruction interval, note that + # t0: start time of instruction + # t1: end time of instruction + if isinstance(node.op, self.CONDITIONAL_SUPPORTED): + t0, t1 = self._visit_conditional_node(node) + else: + if node.op.condition_bits: + raise TranspilerError( + f"Conditional instruction {node.op.name} is not supported in ASAP scheduler." + ) + + if isinstance(node.op, Measure): + t0, t1 = self._visit_measure(node) else: - if node.op.condition_bits: - raise TranspilerError( - f"Conditional instruction {node.op.name} is not supported in ASAP scheduler." - ) - - if isinstance(node.op, Measure): - # measure instruction handling is bit tricky due to clbit_write_latency - t0q = max(idle_after[q] for q in node.qargs) - t0c = max(idle_after[c] for c in node.cargs) - # Assume following case (t0c > t0q) - # - # |t0q - # Q ▒▒▒▒░░░░░░░░░░░░ - # C ▒▒▒▒▒▒▒▒░░░░░░░░ - # |t0c - # - # In this case, there is no actual clbit access until clbit_write_latency. - # The node t0 can be push backward by this amount. - # - # |t0q' = t0c - clbit_write_latency - # Q ▒▒▒▒░░▒▒▒▒▒▒▒▒▒▒ - # C ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ - # |t0c' = t0c - # - # rather than naively doing - # - # |t0q' = t0c - # Q ▒▒▒▒░░░░▒▒▒▒▒▒▒▒ - # C ▒▒▒▒▒▒▒▒░░░▒▒▒▒▒ - # |t0c' = t0c + clbit_write_latency - # - t0 = max(t0q, t0c - clbit_write_latency) - t1 = t0 + op_duration - for clbit in node.cargs: - idle_after[clbit] = t1 - else: - # It happens to be directives such as barrier - t0 = max(idle_after[bit] for bit in node.qargs + node.cargs) - t1 = t0 + op_duration - - for bit in node.qargs: - idle_after[bit] = t1 - - node_start_time[node] = t0 - - self.property_set["node_start_time"] = node_start_time + t0, t1 = self._visit_generic(node) + + for bit in node.qargs: + self._idle_after[bit] = t1 + + self._node_start_time[node] = t0 + + def _visit_conditional_node(self, node): + op_duration = self._get_node_duration(node) + + t0q = max(self._idle_after[q] for q in node.qargs) + if node.op.condition_bits: + # conditional is bit tricky due to conditional_latency + t0c = max(self._idle_after[bit] for bit in node.op.condition_bits) + if t0q > t0c: + # This is situation something like below + # + # |t0q + # Q ▒▒▒▒▒▒▒▒▒░░ + # C ▒▒▒░░░░░░░░ + # |t0c + # + # In this case, you can insert readout access before tq0 + # + # |t0q + # Q ▒▒▒▒▒▒▒▒▒▒▒ + # C ▒▒▒░░░▒▒░░░ + # |t0q - conditional_latency + # + t0c = max(t0q - self._conditional_latency, t0c) + t1c = t0c + self._conditional_latency + for bit in node.op.condition_bits: + # Lock clbit until state is read + self._idle_after[bit] = t1c + # It starts after register read access + t0 = max(t0q, t1c) + else: + t0 = t0q + t1 = t0 + op_duration + + t1 = t0 + op_duration + return t0, t1 + + def _visit_measure(self, node): + op_duration = self._get_node_duration(node) + + # measure instruction handling is bit tricky due to clbit_write_latency + t0q = max(self._idle_after[q] for q in node.qargs) + t0c = max(self._idle_after[c] for c in node.cargs) + # Assume following case (t0c > t0q) + # + # |t0q + # Q ▒▒▒▒░░░░░░░░░░░░ + # C ▒▒▒▒▒▒▒▒░░░░░░░░ + # |t0c + # + # In this case, there is no actual clbit access until clbit_write_latency. + # The node t0 can be push backward by this amount. + # + # |t0q' = t0c - clbit_write_latency + # Q ▒▒▒▒░░▒▒▒▒▒▒▒▒▒▒ + # C ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ + # |t0c' = t0c + # + # rather than naively doing + # + # |t0q' = t0c + # Q ▒▒▒▒░░░░▒▒▒▒▒▒▒▒ + # C ▒▒▒▒▒▒▒▒░░░▒▒▒▒▒ + # |t0c' = t0c + clbit_write_latency + # + t0 = max(t0q, t0c - self._clbit_write_latency) + t1 = t0 + op_duration + for clbit in node.cargs: + self._idle_after[clbit] = t1 + + return t0, t1 + + def _visit_generic(self, node): + """Visit a generic node such as a gate or barrier.""" + op_duration = self._get_node_duration(node) + + # It happens to be directives such as barrier + t0 = max(self._idle_after[bit] for bit in node.qargs + node.cargs) + t1 = t0 + op_duration + return t0, t1 From e9db08433bf8cc62160b5a23f9b4002dcc7dddfd Mon Sep 17 00:00:00 2001 From: Thomas Alexander Date: Sun, 19 Jun 2022 16:04:17 -0300 Subject: [PATCH 29/70] Refactor nodes to no longer return. Remove special handling for classical bits. --- .../dynamic_circuits/schedule.py | 78 +++++++++---------- 1 file changed, 37 insertions(+), 41 deletions(-) diff --git a/qiskit_ibm_provider/dynamic_circuits/schedule.py b/qiskit_ibm_provider/dynamic_circuits/schedule.py index 38810644c..039f882bb 100644 --- a/qiskit_ibm_provider/dynamic_circuits/schedule.py +++ b/qiskit_ibm_provider/dynamic_circuits/schedule.py @@ -96,7 +96,7 @@ def _visit_node(self, node): # t0: start time of instruction # t1: end time of instruction if isinstance(node.op, self.CONDITIONAL_SUPPORTED): - t0, t1 = self._visit_conditional_node(node) + self._visit_conditional_node(node) else: if node.op.condition_bits: raise TranspilerError( @@ -104,20 +104,25 @@ def _visit_node(self, node): ) if isinstance(node.op, Measure): - t0, t1 = self._visit_measure(node) + self._visit_measure(node) else: - t0, t1 = self._visit_generic(node) - - for bit in node.qargs: - self._idle_after[bit] = t1 - - self._node_start_time[node] = t0 + self._visit_generic(node) def _visit_conditional_node(self, node): - op_duration = self._get_node_duration(node) + """Handling case of a conditional execution. - t0q = max(self._idle_after[q] for q in node.qargs) + Conditional execution durations are currently non-deterministic. as we do not know + the time it will take to begin executing the block. We do however know when the + duration of the block contents execution (provided it does not also contain + conditional executions). + + TODO: Update for support of general control-flow, not just single conditional operations. + """ if node.op.condition_bits: + # Special processing required to resolve conditional scheduling dependencies + op_duration = self._get_node_duration(node) + + t0q = max(self._idle_after[q] for q in node.qargs) # conditional is bit tricky due to conditional_latency t0c = max(self._idle_after[bit] for bit in node.op.condition_bits) if t0q > t0c: @@ -142,47 +147,32 @@ def _visit_conditional_node(self, node): self._idle_after[bit] = t1c # It starts after register read access t0 = max(t0q, t1c) - else: - t0 = t0q - t1 = t0 + op_duration - t1 = t0 + op_duration - return t0, t1 + t1 = t0 + op_duration + self._update_idles(node, t0, t1) + else: + # Fall through to generic case if not conditional + self._visit_generic(node) def _visit_measure(self, node): + """Visit a measurement node. + + Measurement currently triggers the end of a pulse block in IBM dynamic circuits hardware. + This means that it is possible to schedule *up to* a measurement (and during its pulses) + but the measurement will be followed by a period of indeterminism. + All measurements on disjoing qubits will be collected on the same qubits to be run simulataneously. + + """ op_duration = self._get_node_duration(node) # measure instruction handling is bit tricky due to clbit_write_latency t0q = max(self._idle_after[q] for q in node.qargs) - t0c = max(self._idle_after[c] for c in node.cargs) - # Assume following case (t0c > t0q) - # - # |t0q - # Q ▒▒▒▒░░░░░░░░░░░░ - # C ▒▒▒▒▒▒▒▒░░░░░░░░ - # |t0c - # - # In this case, there is no actual clbit access until clbit_write_latency. - # The node t0 can be push backward by this amount. - # - # |t0q' = t0c - clbit_write_latency - # Q ▒▒▒▒░░▒▒▒▒▒▒▒▒▒▒ - # C ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ - # |t0c' = t0c - # - # rather than naively doing - # - # |t0q' = t0c - # Q ▒▒▒▒░░░░▒▒▒▒▒▒▒▒ - # C ▒▒▒▒▒▒▒▒░░░▒▒▒▒▒ - # |t0c' = t0c + clbit_write_latency - # - t0 = max(t0q, t0c - self._clbit_write_latency) + t0 = t0q t1 = t0 + op_duration for clbit in node.cargs: self._idle_after[clbit] = t1 - return t0, t1 + self._update_idles(node, t0, t1) def _visit_generic(self, node): """Visit a generic node such as a gate or barrier.""" @@ -191,4 +181,10 @@ def _visit_generic(self, node): # It happens to be directives such as barrier t0 = max(self._idle_after[bit] for bit in node.qargs + node.cargs) t1 = t0 + op_duration - return t0, t1 + self._update_idles(node, t0, t1) + + def _update_idles(self, node, t0, t1): + for bit in node.qargs: + self._idle_after[bit] = t1 + + self._node_start_time[node] = t0 From eab1fdb5961511439615a0a62023c650fb7c0771 Mon Sep 17 00:00:00 2001 From: Thomas Alexander Date: Mon, 27 Jun 2022 11:12:42 -0300 Subject: [PATCH 30/70] Add block based scheduling and updated tests for dynamic circuits type scheduling. --- .../dynamic_circuits/base_padding.py | 230 +++++++++++++++++ .../dynamic_circuits/pad_delay.py | 76 ++++++ .../dynamic_circuits/schedule.py | 28 ++- test/dynamic_circuits/test_schedule.py | 233 ++++++------------ 4 files changed, 398 insertions(+), 169 deletions(-) create mode 100644 qiskit_ibm_provider/dynamic_circuits/base_padding.py create mode 100644 qiskit_ibm_provider/dynamic_circuits/pad_delay.py diff --git a/qiskit_ibm_provider/dynamic_circuits/base_padding.py b/qiskit_ibm_provider/dynamic_circuits/base_padding.py new file mode 100644 index 000000000..736b5ab95 --- /dev/null +++ b/qiskit_ibm_provider/dynamic_circuits/base_padding.py @@ -0,0 +1,230 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2022. +# +# 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. + +"""Padding pass to fill timeslots for the dynamic circuits backends.""" + +from typing import List, Optional, Union + +from qiskit.circuit import Qubit, Clbit, Instruction +from qiskit.circuit.delay import Delay +from qiskit.dagcircuit import DAGCircuit, DAGNode +from qiskit.transpiler.basepasses import TransformationPass +from qiskit.transpiler.exceptions import TranspilerError + + +class BasePadding(TransformationPass): + """The base class of padding pass. + + This pass requires one of scheduling passes to be executed before itself. + Since there are multiple scheduling strategies, the selection of scheduling + pass is left in the hands of the pass manager designer. + Once a scheduling analysis pass is run, ``node_start_time`` is generated + in the :attr:`property_set`. This information is represented by a python dictionary of + the expected instruction execution times keyed on the node instances. + Entries in the dictionary are only created for non-delay nodes. + The padding pass expects all ``DAGOpNode`` in the circuit to be scheduled. + + This base class doesn't define any sequence to interleave, but it manages + the location where the sequence is inserted, and provides a set of information necessary + to construct the proper sequence. Thus, a subclass of this pass just needs to implement + :meth:`_pad` method, in which the subclass constructs a circuit block to insert. + This mechanism removes lots of boilerplate logic to manage whole DAG circuits. + + Note that padding pass subclasses should define interleaving sequences satisfying: + + - Interleaved sequence does not change start time of other nodes + - Interleaved sequence should have total duration of the provided ``time_interval``. + + Any manipulation violating these constraints may prevent this base pass from correctly + tracking the start time of each instruction, + which may result in violation of hardware alignment constraints. + """ + + def run(self, dag: DAGCircuit): + """Run the padding pass on ``dag``. + + Args: + dag: DAG to be checked. + + Returns: + DAGCircuit: DAG with idle time filled with instructions. + + Raises: + TranspilerError: When a particular node is not scheduled, likely some transform pass + is inserted before this node is called. + """ + self._pre_runhook(dag) + + node_start_time = self.property_set["node_start_time"].copy() + + new_dag = DAGCircuit() + for qreg in dag.qregs.values(): + new_dag.add_qreg(qreg) + for creg in dag.cregs.values(): + new_dag.add_creg(creg) + + # Update start time dictionary for the new_dag. + # This information may be used for further scheduling tasks, + # but this is immediately invalidated becasue node id is updated in the new_dag. + self.property_set["node_start_time"].clear() + + new_dag.name = dag.name + new_dag.metadata = dag.metadata + new_dag.unit = self.property_set["time_unit"] + new_dag.calibrations = dag.calibrations + new_dag.global_phase = dag.global_phase + + idle_after = {bit: 0 for bit in dag.qubits} + + # Compute fresh circuit duration from the node start time dictionary and op duration. + # Note that pre-scheduled duration may change within the alignment passes, i.e. + # if some instruction time t0 violating the hardware alignment constraint, + # the alignment pass may delay t0 and accordingly the circuit duration changes. + circuit_duration = 0 + for node in dag.topological_op_nodes(): + if node in node_start_time: + block_idx, t0 = node_start_time[node] + t1 = t0 + node.op.duration + circuit_duration = max(circuit_duration, t1) + + if isinstance(node.op, Delay): + # The padding class considers a delay instruction as idle time + # rather than instruction. Delay node is removed so that + # we can extract non-delay predecessors. + dag.remove_op_node(node) + continue + + for bit in node.qargs: + + # Fill idle time with some sequence + if t0 - idle_after[bit] > 0: + # Find previous node on the wire, i.e. always the latest node on the wire + prev_node = next(new_dag.predecessors(new_dag.output_map[bit])) + self._pad( + dag=new_dag, + block_idx=block_idx, + qubit=bit, + t_start=idle_after[bit], + t_end=t0, + next_node=node, + prev_node=prev_node, + ) + + idle_after[bit] = t1 + + self._apply_scheduled_op(new_dag, block_idx, t0, node.op, node.qargs, node.cargs) + else: + raise TranspilerError( + f"Operation {repr(node)} is likely added after the circuit is scheduled. " + "Schedule the circuit again if you transformed it." + ) + + # Add delays until the end of circuit. + for bit in new_dag.qubits: + if circuit_duration - idle_after[bit] > 0: + node = new_dag.output_map[bit] + prev_node = next(new_dag.predecessors(node)) + self._pad( + dag=new_dag, + block_idx=block_idx, + qubit=bit, + t_start=idle_after[bit], + t_end=circuit_duration, + next_node=node, + prev_node=prev_node, + ) + + new_dag.duration = circuit_duration + + return new_dag + + def _pre_runhook(self, dag: DAGCircuit): + """Extra routine inserted before running the padding pass. + + Args: + dag: DAG circuit on which the sequence is applied. + + Raises: + TranspilerError: If the whole circuit or instruction is not scheduled. + """ + if "node_start_time" not in self.property_set: + raise TranspilerError( + f"The input circuit {dag.name} is not scheduled. Call one of scheduling passes " + f"before running the {self.__class__.__name__} pass." + ) + + def _apply_scheduled_op( + self, + dag: DAGCircuit, + block_idx: int, + t_start: int, + oper: Instruction, + qubits: Union[Qubit, List[Qubit]], + clbits: Optional[Union[Clbit, List[Clbit]]] = None, + ): + """Add new operation to DAG with scheduled information. + + This is identical to apply_operation_back + updating the node_start_time propety. + + Args: + dag: DAG circuit on which the sequence is applied. + block_idx: Execution block index for this node. + t_start: Start time of new node. + oper: New operation that is added to the DAG circuit. + qubits: The list of qubits that the operation acts on. + clbits: The list of clbits that the operation acts on. + """ + if isinstance(qubits, Qubit): + qubits = [qubits] + if isinstance(clbits, Clbit): + clbits = [clbits] + + new_node = dag.apply_operation_back(oper, qargs=qubits, cargs=clbits) + self.property_set["node_start_time"][new_node] = (block_idx, t_start) + + def _pad( + self, + dag: DAGCircuit, + block_idx: int, + qubit: Qubit, + t_start: int, + t_end: int, + next_node: DAGNode, + prev_node: DAGNode, + ): + """Interleave instruction sequence in between two nodes. + + .. note:: + If a DAGOpNode is added here, it should update node_start_time property + in the property set so that the added node is also scheduled. + This is achieved by adding operation via :meth:`_apply_scheduled_op`. + + .. note:: + + This method doesn't check if the total duration of new DAGOpNode added here + is identical to the interval (``t_end - t_start``). + A developer of the pass must guarantee this is satisfied. + If the duration is greater than the interval, your circuit may be + compiled down to the target code with extra duration on the backend compiler, + which is then played normally without error. However, the outcome of your circuit + might be unexpected due to erroneous scheduling. + + Args: + dag: DAG circuit that sequence is applied. + block_idx: Execution block index for this node. + qubit: The wire that the sequence is applied on. + t_start: Absolute start time of this interval. + t_end: Absolute end time of this interval. + next_node: Node that follows the sequence. + prev_node: Node ahead of the sequence. + """ + raise NotImplementedError diff --git a/qiskit_ibm_provider/dynamic_circuits/pad_delay.py b/qiskit_ibm_provider/dynamic_circuits/pad_delay.py new file mode 100644 index 000000000..d816200d6 --- /dev/null +++ b/qiskit_ibm_provider/dynamic_circuits/pad_delay.py @@ -0,0 +1,76 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2022. +# +# 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. + +"""Padding pass to insert Delay into empty timeslots for dynamic circuit backends.""" + +from qiskit.circuit import Qubit +from qiskit.circuit.delay import Delay +from qiskit.dagcircuit import DAGCircuit, DAGNode, DAGOutNode + +from qiskit_ibm_provider.dynamic_circuits.base_padding import BasePadding + + +class PadDelay(BasePadding): + """Padding idle time with Delay instructions. + + Consecutive delays will be merged in the output of this pass. + + .. code-block::python + + durations = InstructionDurations([("x", None, 160), ("cx", None, 800)]) + + qc = QuantumCircuit(2) + qc.delay(100, 0) + qc.x(1) + qc.cx(0, 1) + + The ASAP-scheduled circuit output may become + + .. parsed-literal:: + + ┌────────────────┐ + q_0: ┤ Delay(160[dt]) ├──■── + └─────┬───┬──────┘┌─┴─┐ + q_1: ──────┤ X ├───────┤ X ├ + └───┘ └───┘ + + Note that the additional idle time of 60dt on the ``q_0`` wire coming from the duration difference + between ``Delay`` of 100dt (``q_0``) and ``XGate`` of 160 dt (``q_1``) is absorbed in + the delay instruction on the ``q_0`` wire, i.e. in total 160 dt. + + See :class:`BasePadding` pass for details. + """ + + def __init__(self, fill_very_end: bool = True): + """Create new padding delay pass. + + Args: + fill_very_end: Set ``True`` to fill the end of circuit with delay. + """ + super().__init__() + self.fill_very_end = fill_very_end + + def _pad( + self, + block_idx: int, + dag: DAGCircuit, + qubit: Qubit, + t_start: int, + t_end: int, + next_node: DAGNode, + prev_node: DAGNode, + ): + if not self.fill_very_end and isinstance(next_node, DAGOutNode): + return + + time_interval = t_end - t_start + self._apply_scheduled_op(dag, block_idx, t_start, Delay(time_interval, dag.unit), qubit) diff --git a/qiskit_ibm_provider/dynamic_circuits/schedule.py b/qiskit_ibm_provider/dynamic_circuits/schedule.py index 039f882bb..babc338d1 100644 --- a/qiskit_ibm_provider/dynamic_circuits/schedule.py +++ b/qiskit_ibm_provider/dynamic_circuits/schedule.py @@ -47,8 +47,7 @@ def __init__(self, durations: qiskit.transpiler.instruction_durations.Instructio self._dag = None - self._conditional_latency = 0 - self._clbit_write_latency = 0 + self._current_block_idx = 0 self._node_start_time = None self._idle_after = None @@ -85,7 +84,7 @@ def _init_run(self, dag): self._clbit_write_latency = self.property_set.get("clbit_write_latency", 0) self._node_start_time = dict() - self._idle_after = {q: 0 for q in dag.qubits + dag.clbits} + self._idle_after = {q: (0, 0) for q in dag.qubits + dag.clbits} self._bit_indices = {q: index for index, q in enumerate(dag.qubits)} def _get_node_duration(self, node): @@ -122,9 +121,9 @@ def _visit_conditional_node(self, node): # Special processing required to resolve conditional scheduling dependencies op_duration = self._get_node_duration(node) - t0q = max(self._idle_after[q] for q in node.qargs) + t0q = max(self._idle_after[q][1] for q in node.qargs) # conditional is bit tricky due to conditional_latency - t0c = max(self._idle_after[bit] for bit in node.op.condition_bits) + t0c = max(self._idle_after[bit][1] for bit in node.op.condition_bits) if t0q > t0c: # This is situation something like below # @@ -144,7 +143,7 @@ def _visit_conditional_node(self, node): t1c = t0c + self._conditional_latency for bit in node.op.condition_bits: # Lock clbit until state is read - self._idle_after[bit] = t1c + self._idle_after[bit] = (0, t1c) # It starts after register read access t0 = max(t0q, t1c) @@ -166,25 +165,32 @@ def _visit_measure(self, node): op_duration = self._get_node_duration(node) # measure instruction handling is bit tricky due to clbit_write_latency - t0q = max(self._idle_after[q] for q in node.qargs) + t0q = max(self._idle_after[q][1] for q in node.qargs) t0 = t0q t1 = t0 + op_duration for clbit in node.cargs: - self._idle_after[clbit] = t1 + self._idle_after[clbit] = (0, t1) self._update_idles(node, t0, t1) + #self._begin_new_circuit_block() def _visit_generic(self, node): """Visit a generic node such as a gate or barrier.""" op_duration = self._get_node_duration(node) # It happens to be directives such as barrier - t0 = max(self._idle_after[bit] for bit in node.qargs + node.cargs) + t0 = max(self._idle_after[bit][1] for bit in node.qargs + node.cargs) t1 = t0 + op_duration self._update_idles(node, t0, t1) def _update_idles(self, node, t0, t1): for bit in node.qargs: - self._idle_after[bit] = t1 + self._idle_after[bit] = (self._current_block_idx, t1) - self._node_start_time[node] = t0 + self._node_start_time[node] = (self._current_block_idx, t0) + + def _begin_new_circuit_block(self): + """Create a new timed circuit block completing the previous block. + + """ + self._current_block_idx += 1 diff --git a/test/dynamic_circuits/test_schedule.py b/test/dynamic_circuits/test_schedule.py index 8e805e6d8..8eab9ba5b 100644 --- a/test/dynamic_circuits/test_schedule.py +++ b/test/dynamic_circuits/test_schedule.py @@ -26,6 +26,7 @@ from qiskit.transpiler.passmanager import PassManager from qiskit.transpiler.exceptions import TranspilerError +from qiskit_ibm_provider.dynamic_circuits.pad_delay import PadDelay from qiskit_ibm_provider.dynamic_circuits.schedule import DynamicCircuitScheduleAnalysis @ddt @@ -72,23 +73,26 @@ def test_classically_controlled_gate_after_measure(self): def test_measure_after_measure(self): """Test if schedules circuits with measure after measure with a common clbit. - See: https://github.com/Qiskit/qiskit-terra/issues/7654 + + Note: There is no delay to write into the same clbit with IBM backends. + (input) - ┌───┐┌─┐ - q_0: ┤ X ├┤M├─── - └───┘└╥┘┌─┐ - q_1: ──────╫─┤M├ - ║ └╥┘ - c: 1/══════╩══╩═ - 0 0 + ┌───┐┌─┐ + q_0: ┤ X ├┤M├─── + └───┘└╥┘┌─┐ + q_1: ──────╫─┤M├ + ║ └╥┘ + c: 1/══════╩══╩═ + 0 0 (scheduled) - ┌───┐ ┌─┐┌─────────────────┐ - q_0: ───────┤ X ├───────┤M├┤ Delay(1000[dt]) ├ - ┌──────┴───┴──────┐└╥┘└───────┬─┬───────┘ - q_1: ┤ Delay(1200[dt]) ├─╫─────────┤M├──────── - └─────────────────┘ ║ └╥┘ - c: 1/════════════════════╩══════════╩═════════ - 0 0 + ┌───┐ ┌─┐ ░ + q_0: ──────┤ X ├───────┤M├────░─ + ┌─────┴───┴──────┐└╥┘┌─┐ ░ + q_1: ┤ Delay(200[dt]) ├─╫─┤M├─░─ + └────────────────┘ ║ └╥┘ ░ + c: 1/═══════════════════╩══╩════ + 0 0 + """ qc = QuantumCircuit(2, 1) qc.x(0) @@ -101,11 +105,10 @@ def test_measure_after_measure(self): expected = QuantumCircuit(2, 1) expected.x(0) + expected.delay(200, 1) expected.measure(0, 0) - expected.delay(1200, 1) expected.measure(1, 0) - expected.delay(1000, 0) - + expected.barrier() self.assertEqual(expected, scheduled) def test_c_if_on_different_qubits(self): @@ -154,24 +157,32 @@ def test_c_if_on_different_qubits(self): def test_shorter_measure_after_measure(self): """Test if schedules circuits with shorter measure after measure with a common clbit. + + Note: For dynamic circuits support we currently group measurements to start at the same time which in turn trigger + the end of a block. + (input) - ┌─┐ - q_0: ┤M├─── - └╥┘┌─┐ - q_1: ─╫─┤M├ - ║ └╥┘ - c: 1/═╩══╩═ - 0 0 + ┌─┐ + q_0: ┤M├─── + └╥┘┌─┐ + q_1: ─╫─┤M├ + ║ └╥┘ + q_2: ─╫──╫─ + ║ ║ + c: 1/═╩══╩═ + 0 0 (scheduled) - ┌─┐┌────────────────┐ - q_0: ───────────────────┤M├┤ Delay(700[dt]) ├ - ┌─────────────────┐└╥┘└──────┬─┬───────┘ - q_1: ┤ Delay(1000[dt]) ├─╫────────┤M├──────── - └─────────────────┘ ║ └╥┘ - c: 1/════════════════════╩═════════╩═════════ - 0 0 + ┌─┐ ░ + q_0: ┤M├─────────────────────────────────────────░─ + └╥┘ ┌─┐┌────────────────┐ ░ + q_1: ─╫────────────────────┤M├┤ Delay(300[dt]) ├─░─ + ║ ┌─────────────────┐└╥┘└────────────────┘ ░ + q_2: ─╫─┤ Delay(1000[dt]) ├─╫────────────────────░─ + ║ └─────────────────┘ ║ ░ + c: 1/═╩═════════════════════╩══════════════════════ + 0 0 """ - qc = QuantumCircuit(2, 1) + qc = QuantumCircuit(3, 1) qc.measure(0, 0) qc.measure(1, 0) @@ -179,36 +190,41 @@ def test_shorter_measure_after_measure(self): pm = PassManager([DynamicCircuitScheduleAnalysis(durations), PadDelay()]) scheduled = pm.run(qc) - expected = QuantumCircuit(2, 1) + expected = QuantumCircuit(3, 1) expected.measure(0, 0) - expected.delay(1000, 1) expected.measure(1, 0) - expected.delay(700, 0) + expected.delay(300, 1) + expected.delay(1000, 2) + expected.barrier() self.assertEqual(expected, scheduled) def test_measure_after_c_if(self): """Test if schedules circuits with c_if after measure with a common clbit. + + Note: This test is not yet correct as we should schedule the conditional block + qubits with delays as well. + (input) - ┌─┐ - q_0: ┤M├────────────── - └╥┘ ┌───┐ - q_1: ─╫────┤ X ├────── - ║ └─╥─┘ ┌─┐ - q_2: ─╫──────╫─────┤M├ - ║ ┌────╨────┐└╥┘ - c: 1/═╩═╡ c_0 = T ╞═╩═ - 0 └─────────┘ 0 + ┌─┐ + q_0: ┤M├────────────── + └╥┘ ┌───┐ + q_1: ─╫────┤ X ├────── + ║ └─╥─┘ ┌─┐ + q_2: ─╫──────╫─────┤M├ + ║ ┌────╨────┐└╥┘ + c: 1/═╩═╡ c_0 = T ╞═╩═ + 0 └─────────┘ 0 (scheduled) - ┌─┐┌─────────────────┐ - q_0: ───────────────────┤M├┤ Delay(1000[dt]) ├────────────────── - ┌─────────────────┐└╥┘└──────┬───┬──────┘┌────────────────┐ - q_1: ┤ Delay(1000[dt]) ├─╫────────┤ X ├───────┤ Delay(800[dt]) ├ - ├─────────────────┤ ║ └─╥─┘ └──────┬─┬───────┘ - q_2: ┤ Delay(1000[dt]) ├─╫──────────╫────────────────┤M├──────── - └─────────────────┘ ║ ┌────╨────┐ └╥┘ - c: 1/════════════════════╩═════╡ c_0=0x1 ╞════════════╩═════════ - 0 └─────────┘ 0 + ┌─┐ ░ ░ ┌─────────────────┐ + q_0: ───────────────────┤M├─░─────────────░─┤ Delay(1000[dt]) ├ + ┌─────────────────┐└╥┘ ░ ┌───┐ ░ ├─────────────────┤ + q_1: ┤ Delay(1000[dt]) ├─╫──░────┤ X ├────░─┤ Delay(1000[dt]) ├ + ├─────────────────┤ ║ ░ └─╥─┘ ░ └───────┬─┬───────┘ + q_2: ┤ Delay(1000[dt]) ├─╫──░──────╫──────░─────────┤M├──────── + └─────────────────┘ ║ ░ ┌────╨────┐ ░ └╥┘ + c: 1/════════════════════╩════╡ c_0=0x1 ╞════════════╩═════════ + 0 └─────────┘ 0 """ qc = QuantumCircuit(3, 1) qc.measure(0, 0) @@ -223,10 +239,12 @@ def test_measure_after_c_if(self): expected.delay(1000, 1) expected.delay(1000, 2) expected.measure(0, 0) - expected.x(1).c_if(0, 1) - expected.measure(2, 0) + expected.barrier() + expected.x(1).c_if(0, 1) # Not yet correct as we should insert delays for idle qubits in conditional. + expected.barrier() expected.delay(1000, 0) - expected.delay(800, 1) + expected.measure(2, 0) + expected.delay(1000, 1) self.assertEqual(expected, scheduled) @@ -414,107 +432,6 @@ def test_active_reset_circuit(self, write_lat, cond_lat): self.assertEqual(expected, actual_sched) - def test_random_complicated_circuit(self): - """Test scheduling complicated circuit with control flow. - (input) - ┌────────────────┐ ┌───┐ ░ ┌───┐ » - q_0: ┤ Delay(100[dt]) ├───┤ X ├────░──────────────────┤ X ├───» - └────────────────┘ └─╥─┘ ░ ┌───┐ └─╥─┘ » - q_1: ───────────────────────╫──────░───────┤ X ├────────╫─────» - ║ ░ ┌─┐ └─╥─┘ ║ » - q_2: ───────────────────────╫──────░─┤M├─────╫──────────╫─────» - ┌────╨────┐ ░ └╥┘┌────╨────┐┌────╨────┐» - c: 1/══════════════════╡ c_0=0x1 ╞════╩═╡ c_0=0x0 ╞╡ c_0=0x0 ╞» - └─────────┘ 0 └─────────┘└─────────┘» - « ┌────────────────┐┌───┐ - «q_0: ┤ Delay(300[dt]) ├┤ X ├─────■───── - « └────────────────┘└───┘ ┌─┴─┐ - «q_1: ────────■─────────────────┤ X ├─── - « ┌─┴─┐ ┌─┐ └─╥─┘ - «q_2: ──────┤ X ├────────┤M├──────╫───── - « └───┘ └╥┘ ┌────╨────┐ - «c: 1/════════════════════╩══╡ c_0=0x0 ╞ - « 0 └─────────┘ - (scheduled) duration = 2800 dt - ┌────────────────┐ ┌───┐ ░ ┌─────────────────┐ » - q_0: ┤ Delay(200[dt]) ├───┤ X ├────░─┤ Delay(1400[dt]) ├───────────» - ├────────────────┤ └─╥─┘ ░ ├─────────────────┤ ┌───┐ » - q_1: ┤ Delay(300[dt]) ├─────╫──────░─┤ Delay(1200[dt]) ├───┤ X ├───» - ├────────────────┤ ║ ░ └───────┬─┬───────┘ └─╥─┘ » - q_2: ┤ Delay(300[dt]) ├─────╫──────░─────────┤M├─────────────╫─────» - └────────────────┘┌────╨────┐ ░ └╥┘ ┌────╨────┐» - c: 1/══════════════════╡ c_0=0x1 ╞════════════╩═════════╡ c_0=0x0 ╞» - └─────────┘ 0 └─────────┘» - « ┌───┐ ┌────────────────┐ ┌───┐ » - «q_0: ─────────────────────┤ X ├───┤ Delay(300[dt]) ├──────┤ X ├───────» - « └─╥─┘ └────────────────┘┌─────┴───┴──────┐» - «q_1: ───────────────────────╫─────────────■─────────┤ Delay(400[dt]) ├» - « ┌────────────────┐ ║ ┌─┴─┐ ├────────────────┤» - «q_2: ┤ Delay(300[dt]) ├─────╫───────────┤ X ├───────┤ Delay(300[dt]) ├» - « └────────────────┘┌────╨────┐ └───┘ └────────────────┘» - «c: 1/══════════════════╡ c_0=0x0 ╞════════════════════════════════════» - « └─────────┘ » - « ┌────────────────┐ - «q_0: ─────■─────┤ Delay(700[dt]) ├ - « ┌─┴─┐ ├────────────────┤ - «q_1: ───┤ X ├───┤ Delay(700[dt]) ├ - « └─╥─┘ └──────┬─┬───────┘ - «q_2: ─────╫────────────┤M├──────── - « ┌────╨────┐ └╥┘ - «c: 1/╡ c_0=0x0 ╞════════╩═════════ - « └─────────┘ 0 - """ - qc = QuantumCircuit(3, 1) - qc.delay(100, 0) - qc.x(0).c_if(0, 1) - qc.barrier() - qc.measure(2, 0) - qc.x(1).c_if(0, 0) - qc.x(0).c_if(0, 0) - qc.delay(300, 0) - qc.cx(1, 2) - qc.x(0) - qc.cx(0, 1).c_if(0, 0) - qc.measure(2, 0) - - durations = InstructionDurations( - [("x", None, 100), ("measure", None, 1000), ("cx", None, 200)] - ) - - actual_sched = PassManager( - [ - SetIOLatency(clbit_write_latency=100, conditional_latency=200), - DynamicCircuitScheduleAnalysis(durations), - PadDelay(), - ] - ).run(qc) - - expected_sched = QuantumCircuit(3, 1) - expected_sched.delay(200, 0) # due to conditional latency of 200dt - expected_sched.delay(300, 1) - expected_sched.delay(300, 2) - expected_sched.x(0).c_if(0, 1) - expected_sched.barrier() - expected_sched.delay(1400, 0) - expected_sched.delay(1200, 1) - expected_sched.measure(2, 0) - expected_sched.x(1).c_if(0, 0) - expected_sched.x(0).c_if(0, 0) - expected_sched.delay(300, 0) - expected_sched.x(0) - expected_sched.delay(300, 2) - expected_sched.cx(1, 2) - expected_sched.delay(400, 1) - expected_sched.cx(0, 1).c_if(0, 0) - expected_sched.delay(700, 0) # creg is released at t0 of cx(0,1).c_if(0,0) - expected_sched.delay( - 700, 1 - ) # no creg write until 100dt. thus measure can move left by 300dt. - expected_sched.delay(300, 2) - expected_sched.measure(2, 0) - self.assertEqual(expected_sched, actual_sched) - self.assertEqual(actual_sched.duration, 3100) - def test_dag_introduces_extra_dependency_between_conditionals(self): """Test dependency between conditional operations in the scheduling. In the below example circuit, the conditional x on q1 could start at time 0, From dc4ea3476de839fb15a9e3468741bd151a3988a0 Mon Sep 17 00:00:00 2001 From: Thomas Alexander Date: Tue, 5 Jul 2022 09:50:39 -0300 Subject: [PATCH 31/70] Refactor padder class. --- .../dynamic_circuits/base_padding.py | 201 ++++++++++-------- .../dynamic_circuits/pad_delay.py | 3 +- .../dynamic_circuits/schedule.py | 47 +++- test/dynamic_circuits/test_schedule.py | 67 +++++- test/dynamic_circuits/test_tests.py | 20 -- 5 files changed, 215 insertions(+), 123 deletions(-) delete mode 100644 test/dynamic_circuits/test_tests.py diff --git a/qiskit_ibm_provider/dynamic_circuits/base_padding.py b/qiskit_ibm_provider/dynamic_circuits/base_padding.py index 736b5ab95..2eeeb8ff1 100644 --- a/qiskit_ibm_provider/dynamic_circuits/base_padding.py +++ b/qiskit_ibm_provider/dynamic_circuits/base_padding.py @@ -49,6 +49,16 @@ class BasePadding(TransformationPass): which may result in violation of hardware alignment constraints. """ + def __init__(self): + self._node_start_time = None + self._idle_after = None + self._dag = None + self._circuit_duration = 0 + self._current_block_idx = 0 + + super().__init__() + + def run(self, dag: DAGCircuit): """Run the padding pass on ``dag``. @@ -64,88 +74,50 @@ def run(self, dag: DAGCircuit): """ self._pre_runhook(dag) - node_start_time = self.property_set["node_start_time"].copy() + self._node_start_time = self.property_set["node_start_time"].copy() + self._idle_after = {bit: 0 for bit in dag.qubits} + self._current_block_idx = 0 - new_dag = DAGCircuit() + # Prepare DAG to pad + self._dag = DAGCircuit() for qreg in dag.qregs.values(): - new_dag.add_qreg(qreg) + self._dag.add_qreg(qreg) for creg in dag.cregs.values(): - new_dag.add_creg(creg) + self._dag.add_creg(creg) # Update start time dictionary for the new_dag. # This information may be used for further scheduling tasks, # but this is immediately invalidated becasue node id is updated in the new_dag. self.property_set["node_start_time"].clear() - new_dag.name = dag.name - new_dag.metadata = dag.metadata - new_dag.unit = self.property_set["time_unit"] - new_dag.calibrations = dag.calibrations - new_dag.global_phase = dag.global_phase + self._dag.name = dag.name + self._dag.metadata = dag.metadata + self._dag.unit = self.property_set["time_unit"] + self._dag.calibrations = dag.calibrations + self._dag.global_phase = dag.global_phase - idle_after = {bit: 0 for bit in dag.qubits} # Compute fresh circuit duration from the node start time dictionary and op duration. # Note that pre-scheduled duration may change within the alignment passes, i.e. # if some instruction time t0 violating the hardware alignment constraint, # the alignment pass may delay t0 and accordingly the circuit duration changes. - circuit_duration = 0 + self._circuit_duration = 0 for node in dag.topological_op_nodes(): - if node in node_start_time: - block_idx, t0 = node_start_time[node] - t1 = t0 + node.op.duration - circuit_duration = max(circuit_duration, t1) - + if node in self._node_start_time: if isinstance(node.op, Delay): - # The padding class considers a delay instruction as idle time - # rather than instruction. Delay node is removed so that - # we can extract non-delay predecessors. - dag.remove_op_node(node) - continue - - for bit in node.qargs: - - # Fill idle time with some sequence - if t0 - idle_after[bit] > 0: - # Find previous node on the wire, i.e. always the latest node on the wire - prev_node = next(new_dag.predecessors(new_dag.output_map[bit])) - self._pad( - dag=new_dag, - block_idx=block_idx, - qubit=bit, - t_start=idle_after[bit], - t_end=t0, - next_node=node, - prev_node=prev_node, - ) - - idle_after[bit] = t1 - - self._apply_scheduled_op(new_dag, block_idx, t0, node.op, node.qargs, node.cargs) + self._visit_delay(node) + else: + self._visit_generic(node) + else: raise TranspilerError( f"Operation {repr(node)} is likely added after the circuit is scheduled. " "Schedule the circuit again if you transformed it." ) - # Add delays until the end of circuit. - for bit in new_dag.qubits: - if circuit_duration - idle_after[bit] > 0: - node = new_dag.output_map[bit] - prev_node = next(new_dag.predecessors(node)) - self._pad( - dag=new_dag, - block_idx=block_idx, - qubit=bit, - t_start=idle_after[bit], - t_end=circuit_duration, - next_node=node, - prev_node=prev_node, - ) - - new_dag.duration = circuit_duration + self._dag.duration = self._circuit_duration - return new_dag + return self._dag def _pre_runhook(self, dag: DAGCircuit): """Extra routine inserted before running the padding pass. @@ -162,38 +134,8 @@ def _pre_runhook(self, dag: DAGCircuit): f"before running the {self.__class__.__name__} pass." ) - def _apply_scheduled_op( - self, - dag: DAGCircuit, - block_idx: int, - t_start: int, - oper: Instruction, - qubits: Union[Qubit, List[Qubit]], - clbits: Optional[Union[Clbit, List[Clbit]]] = None, - ): - """Add new operation to DAG with scheduled information. - - This is identical to apply_operation_back + updating the node_start_time propety. - - Args: - dag: DAG circuit on which the sequence is applied. - block_idx: Execution block index for this node. - t_start: Start time of new node. - oper: New operation that is added to the DAG circuit. - qubits: The list of qubits that the operation acts on. - clbits: The list of clbits that the operation acts on. - """ - if isinstance(qubits, Qubit): - qubits = [qubits] - if isinstance(clbits, Clbit): - clbits = [clbits] - - new_node = dag.apply_operation_back(oper, qargs=qubits, cargs=clbits) - self.property_set["node_start_time"][new_node] = (block_idx, t_start) - def _pad( self, - dag: DAGCircuit, block_idx: int, qubit: Qubit, t_start: int, @@ -219,7 +161,6 @@ def _pad( might be unexpected due to erroneous scheduling. Args: - dag: DAG circuit that sequence is applied. block_idx: Execution block index for this node. qubit: The wire that the sequence is applied on. t_start: Absolute start time of this interval. @@ -228,3 +169,85 @@ def _pad( prev_node: Node ahead of the sequence. """ raise NotImplementedError + + def _visit_delay(self, node): + """The padding class considers a delay instruction as idle time + rather than instruction. Delay node is not added so that + we can extract non-delay predecessors. + """ + pass + + def _visit_generic(self, node): + """Visit a generic node to pad.""" + block_idx, t0 = self._node_start_time[node] + + # Trigger the end of a block + if block_idx > self._current_block_idx: + self._pad_until_block_end(self._circuit_duration, self._current_block_idx) + + # Now set the current block index. + self._current_block_idx = block_idx + + + t1 = t0 + node.op.duration + self._circuit_duration = max(self._circuit_duration, t1) + for bit in node.qargs: + + # Fill idle time with some sequence + if t0 - self._idle_after[bit] > 0: + # Find previous node on the wire, i.e. always the latest node on the wire + prev_node = next(self._dag.predecessors(self._dag.output_map[bit])) + self._pad( + block_idx=block_idx, + qubit=bit, + t_start=self._idle_after[bit], + t_end=t0, + next_node=node, + prev_node=prev_node, + ) + + self._idle_after[bit] = t1 + + self._apply_scheduled_op(block_idx, t0, node.op, node.qargs, node.cargs) + + def _pad_until_block_end(self, block_duration, block_idx): + # Add delays until the end of circuit. + for bit in self._dag.qubits: + if block_duration - self._idle_after[bit] > 0: + node = self._dag.output_map[bit] + prev_node = next(self._dag.predecessors(node)) + self._pad( + block_idx=block_idx, + qubit=bit, + t_start=self._idle_after[bit], + t_end=block_duration, + next_node=node, + prev_node=prev_node, + ) + + def _apply_scheduled_op( + self, + block_idx: int, + t_start: int, + oper: Instruction, + qubits: Union[Qubit, List[Qubit]], + clbits: Optional[Union[Clbit, List[Clbit]]] = None, + ): + """Add new operation to DAG with scheduled information. + + This is identical to apply_operation_back + updating the node_start_time propety. + + Args: + block_idx: Execution block index for this node. + t_start: Start time of new node. + oper: New operation that is added to the DAG circuit. + qubits: The list of qubits that the operation acts on. + clbits: The list of clbits that the operation acts on. + """ + if isinstance(qubits, Qubit): + qubits = [qubits] + if isinstance(clbits, Clbit): + clbits = [clbits] + + new_node = self._dag.apply_operation_back(oper, qargs=qubits, cargs=clbits) + self.property_set["node_start_time"][new_node] = (block_idx, t_start) diff --git a/qiskit_ibm_provider/dynamic_circuits/pad_delay.py b/qiskit_ibm_provider/dynamic_circuits/pad_delay.py index d816200d6..ca081ff4d 100644 --- a/qiskit_ibm_provider/dynamic_circuits/pad_delay.py +++ b/qiskit_ibm_provider/dynamic_circuits/pad_delay.py @@ -62,7 +62,6 @@ def __init__(self, fill_very_end: bool = True): def _pad( self, block_idx: int, - dag: DAGCircuit, qubit: Qubit, t_start: int, t_end: int, @@ -73,4 +72,4 @@ def _pad( return time_interval = t_end - t_start - self._apply_scheduled_op(dag, block_idx, t_start, Delay(time_interval, dag.unit), qubit) + self._apply_scheduled_op(block_idx, t_start, Delay(time_interval, self._dag.unit), qubit) diff --git a/qiskit_ibm_provider/dynamic_circuits/schedule.py b/qiskit_ibm_provider/dynamic_circuits/schedule.py index babc338d1..6be556020 100644 --- a/qiskit_ibm_provider/dynamic_circuits/schedule.py +++ b/qiskit_ibm_provider/dynamic_circuits/schedule.py @@ -12,6 +12,8 @@ """Scheduler for dynamic circuit backends.""" +import itertools + import qiskit from qiskit.circuit import Measure from qiskit.transpiler.exceptions import TranspilerError @@ -51,6 +53,7 @@ def __init__(self, durations: qiskit.transpiler.instruction_durations.Instructio self._node_start_time = None self._idle_after = None + self._current_block_measures = None self._bit_indices = None super().__init__(durations) @@ -70,6 +73,8 @@ def run(self, dag): for node in dag.topological_op_nodes(): self._visit_node(node) + for node, start_time in self._node_start_time.items(): + print(repr(node), start_time) self.property_set["node_start_time"] = self._node_start_time def _init_run(self, dag): @@ -85,6 +90,7 @@ def _init_run(self, dag): self._node_start_time = dict() self._idle_after = {q: (0, 0) for q in dag.qubits + dag.clbits} + self._current_block_measures = set() self._bit_indices = {q: index for index, q in enumerate(dag.qubits)} def _get_node_duration(self, node): @@ -143,7 +149,7 @@ def _visit_conditional_node(self, node): t1c = t0c + self._conditional_latency for bit in node.op.condition_bits: # Lock clbit until state is read - self._idle_after[bit] = (0, t1c) + self._idle_after[bit] = (self._current_block_idx, t1c) # It starts after register read access t0 = max(t0q, t1c) @@ -164,15 +170,31 @@ def _visit_measure(self, node): """ op_duration = self._get_node_duration(node) - # measure instruction handling is bit tricky due to clbit_write_latency - t0q = max(self._idle_after[q][1] for q in node.qargs) - t0 = t0q - t1 = t0 + op_duration - for clbit in node.cargs: - self._idle_after[clbit] = (0, t1) + current_block_measure_qargs = self._current_block_measure_qargs() + measure_qargs = set(node.qargs) + + t0q = max(self._idle_after[q][1] for q in measure_qargs) + + # If the measurement qubits overlap, we need to start a new scheduling block. + if current_block_measure_qargs & measure_qargs: + self._begin_new_circuit_block() + t0q = 0 + # Otherwise we need to increment all measurements to start at the same time within the block. + else: + t0q = max(itertools.chain([t0q], (self._node_start_time[measure][1] for measure in self._current_block_measures))) + + # Insert this measure into the block + self._current_block_measures.add(node) + + # now update all measure qarg times. + + self._current_block_measures.add(node) + + for measure in self._current_block_measures: + t0 = t0q + t1 = t0 + self._get_node_duration(measure) + self._update_idles(measure, t0, t1) - self._update_idles(node, t0, t1) - #self._begin_new_circuit_block() def _visit_generic(self, node): """Visit a generic node such as a gate or barrier.""" @@ -187,6 +209,9 @@ def _update_idles(self, node, t0, t1): for bit in node.qargs: self._idle_after[bit] = (self._current_block_idx, t1) + for bit in node.cargs: + self._idle_after[bit] = (self._current_block_idx, t1) + self._node_start_time[node] = (self._current_block_idx, t0) def _begin_new_circuit_block(self): @@ -194,3 +219,7 @@ def _begin_new_circuit_block(self): """ self._current_block_idx += 1 + self._current_block_measures = set() + + def _current_block_measure_qargs(self): + return set(qarg for measure in self._current_block_measures for qarg in measure.qargs) diff --git a/test/dynamic_circuits/test_schedule.py b/test/dynamic_circuits/test_schedule.py index 8eab9ba5b..1692c865a 100644 --- a/test/dynamic_circuits/test_schedule.py +++ b/test/dynamic_circuits/test_schedule.py @@ -29,6 +29,7 @@ from qiskit_ibm_provider.dynamic_circuits.pad_delay import PadDelay from qiskit_ibm_provider.dynamic_circuits.schedule import DynamicCircuitScheduleAnalysis + @ddt class TestSchedulingAndPaddingPass(QiskitTestCase): """Tests the Scheduling passes""" @@ -111,6 +112,57 @@ def test_measure_after_measure(self): expected.barrier() self.assertEqual(expected, scheduled) + def test_measure_block_end(self): + """Tests that measures trigger the end of a scheduling block and that measurements are grouped by block. + + (input) + ┌───┐┌─┐ + q_0: ┤ X ├┤M├─── + └───┘└╥┘┌─┐ + q_1: ──────╫─┤M├ + ║ └╥┘ + c: 1/══════╩══╩═ + 0 0 + (scheduled) + ┌───┐ ┌─┐ ░ + q_0: ──────┤ X ├───────┤M├────░─ + ┌─────┴───┴──────┐└╥┘┌─┐ ░ + q_1: ┤ Delay(200[dt]) ├─╫─┤M├─░─ + └────────────────┘ ║ └╥┘ ░ + c: 1/═══════════════════╩══╩════ + 0 0 + + """ + qc = QuantumCircuit(3, 1) + qc.x(0) + qc.measure(0, 0) + qc.measure(1, 0) + qc.x(2) + qc.measure(1, 0) + qc.measure(2, 0) + + durations = InstructionDurations([("x", None, 200), ("measure", None, 1000)]) + pm = PassManager([DynamicCircuitScheduleAnalysis(durations), PadDelay()]) + scheduled = pm.run(qc) + + expected = QuantumCircuit(3, 1) + expected.x(0) + expected.delay(200, 1) + expected.x(2) + expected.delay(1000, 2) + expected.measure(0, 0) + expected.measure(1, 0) + expected.barrier() + expected.delay(1000, 0) + expected.measure(1, 0) + expected.measure(2, 0) + expected.barrier() + + + + import pdb;pdb.set_trace() + self.assertEqual(expected, scheduled) + def test_c_if_on_different_qubits(self): """Test if schedules circuits with `c_if`s on different qubits. (input) @@ -186,7 +238,9 @@ def test_shorter_measure_after_measure(self): qc.measure(0, 0) qc.measure(1, 0) - durations = InstructionDurations([("measure", [0], 1000), ("measure", [1], 700)]) + durations = InstructionDurations( + [("measure", [0], 1000), ("measure", [1], 700)] + ) pm = PassManager([DynamicCircuitScheduleAnalysis(durations), PadDelay()]) scheduled = pm.run(qc) @@ -197,6 +251,9 @@ def test_shorter_measure_after_measure(self): expected.delay(1000, 2) expected.barrier() + import pdb + pdb.set_trace() + self.assertEqual(expected, scheduled) def test_measure_after_c_if(self): @@ -240,7 +297,9 @@ def test_measure_after_c_if(self): expected.delay(1000, 2) expected.measure(0, 0) expected.barrier() - expected.x(1).c_if(0, 1) # Not yet correct as we should insert delays for idle qubits in conditional. + expected.x(1).c_if( + 0, 1 + ) # Not yet correct as we should insert delays for idle qubits in conditional. expected.barrier() expected.delay(1000, 0) expected.measure(2, 0) @@ -410,7 +469,9 @@ def test_active_reset_circuit(self, write_lat, cond_lat): actual_sched = PassManager( [ - SetIOLatency(clbit_write_latency=write_lat, conditional_latency=cond_lat), + SetIOLatency( + clbit_write_latency=write_lat, conditional_latency=cond_lat + ), DynamicCircuitScheduleAnalysis(durations), PadDelay(), ] diff --git a/test/dynamic_circuits/test_tests.py b/test/dynamic_circuits/test_tests.py deleted file mode 100644 index 91b42c978..000000000 --- a/test/dynamic_circuits/test_tests.py +++ /dev/null @@ -1,20 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2022. -# -# 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. - -from qiskit_ibm_provider import dynamic_circuits - -from ..ibm_test_case import IBMTestCase - -class TestBasicServerPaths(IBMTestCase): - - def test_tests(self): - self.assertEqual(dynamic_circuits.foo, 1) From 6e173a538defd23dafb133a6bcc421bcb4250333 Mon Sep 17 00:00:00 2001 From: Thomas Alexander Date: Tue, 5 Jul 2022 11:55:44 -0300 Subject: [PATCH 32/70] Base padding is working. --- .../dynamic_circuits/base_padding.py | 72 ++++++++++++------- .../dynamic_circuits/schedule.py | 1 + 2 files changed, 47 insertions(+), 26 deletions(-) diff --git a/qiskit_ibm_provider/dynamic_circuits/base_padding.py b/qiskit_ibm_provider/dynamic_circuits/base_padding.py index 2eeeb8ff1..995c70694 100644 --- a/qiskit_ibm_provider/dynamic_circuits/base_padding.py +++ b/qiskit_ibm_provider/dynamic_circuits/base_padding.py @@ -15,6 +15,7 @@ from typing import List, Optional, Union from qiskit.circuit import Qubit, Clbit, Instruction +from qiskit.circuit.library import Barrier from qiskit.circuit.delay import Delay from qiskit.dagcircuit import DAGCircuit, DAGNode from qiskit.transpiler.basepasses import TransformationPass @@ -53,7 +54,7 @@ def __init__(self): self._node_start_time = None self._idle_after = None self._dag = None - self._circuit_duration = 0 + self._block_duration = 0 self._current_block_idx = 0 super().__init__() @@ -74,9 +75,37 @@ def run(self, dag: DAGCircuit): """ self._pre_runhook(dag) + + self._init_run(dag) + + # Compute fresh circuit duration from the node start time dictionary and op duration. + # Note that pre-scheduled duration may change within the alignment passes, i.e. + # if some instruction time t0 violating the hardware alignment constraint, + # the alignment pass may delay t0 and accordingly the circuit duration changes. + for node in dag.topological_op_nodes(): + if node in self._node_start_time: + if isinstance(node.op, Delay): + self._visit_delay(node) + else: + self._visit_generic(node) + + else: + raise TranspilerError( + f"Operation {repr(node)} is likely added after the circuit is scheduled. " + "Schedule the circuit again if you transformed it." + ) + + # terminate final block + self._terminate_block(self._block_duration, self._current_block_idx) + + return self._dag + + def _init_run(self, dag): + """Setup for initial run.""" self._node_start_time = self.property_set["node_start_time"].copy() self._idle_after = {bit: 0 for bit in dag.qubits} self._current_block_idx = 0 + self._block_duration = 0 # Prepare DAG to pad self._dag = DAGCircuit() @@ -97,28 +126,6 @@ def run(self, dag: DAGCircuit): self._dag.global_phase = dag.global_phase - # Compute fresh circuit duration from the node start time dictionary and op duration. - # Note that pre-scheduled duration may change within the alignment passes, i.e. - # if some instruction time t0 violating the hardware alignment constraint, - # the alignment pass may delay t0 and accordingly the circuit duration changes. - self._circuit_duration = 0 - for node in dag.topological_op_nodes(): - if node in self._node_start_time: - if isinstance(node.op, Delay): - self._visit_delay(node) - else: - self._visit_generic(node) - - else: - raise TranspilerError( - f"Operation {repr(node)} is likely added after the circuit is scheduled. " - "Schedule the circuit again if you transformed it." - ) - - self._dag.duration = self._circuit_duration - - return self._dag - def _pre_runhook(self, dag: DAGCircuit): """Extra routine inserted before running the padding pass. @@ -183,16 +190,16 @@ def _visit_generic(self, node): # Trigger the end of a block if block_idx > self._current_block_idx: - self._pad_until_block_end(self._circuit_duration, self._current_block_idx) + self._terminate_block(self._block_duration, self._current_block_idx) # Now set the current block index. self._current_block_idx = block_idx t1 = t0 + node.op.duration - self._circuit_duration = max(self._circuit_duration, t1) - for bit in node.qargs: + self._block_duration = max(self._block_duration, t1) + for bit in node.qargs: # Fill idle time with some sequence if t0 - self._idle_after[bit] > 0: # Find previous node on the wire, i.e. always the latest node on the wire @@ -210,6 +217,19 @@ def _visit_generic(self, node): self._apply_scheduled_op(block_idx, t0, node.op, node.qargs, node.cargs) + def _terminate_block(self, block_duration, block_idx): + """Terminate the end of a block scheduling region.""" + self._pad_until_block_end(block_duration, block_idx) + + # Terminate with a barrier to be clear timing is non-deterministic + # across the barrier. + self._dag.apply_operation_back(Barrier(self._dag.num_qubits()), self._dag.qubits, []) + + # Reset idles for the new block. + self._idle_after = {bit: 0 for bit in self._dag.qubits} + self._block_duration = 0 + + def _pad_until_block_end(self, block_duration, block_idx): # Add delays until the end of circuit. for bit in self._dag.qubits: diff --git a/qiskit_ibm_provider/dynamic_circuits/schedule.py b/qiskit_ibm_provider/dynamic_circuits/schedule.py index 6be556020..fa12dc43b 100644 --- a/qiskit_ibm_provider/dynamic_circuits/schedule.py +++ b/qiskit_ibm_provider/dynamic_circuits/schedule.py @@ -220,6 +220,7 @@ def _begin_new_circuit_block(self): """ self._current_block_idx += 1 self._current_block_measures = set() + self._idle_after = {q: (0, 0) for q in self._dag.qubits + self._dag.clbits} def _current_block_measure_qargs(self): return set(qarg for measure in self._current_block_measures for qarg in measure.qargs) From f490053e8646018c7d9beba11c2ac231c6dfea19 Mon Sep 17 00:00:00 2001 From: Thomas Alexander Date: Tue, 5 Jul 2022 11:57:01 -0300 Subject: [PATCH 33/70] Remove debug statements. --- test/dynamic_circuits/test_schedule.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/test/dynamic_circuits/test_schedule.py b/test/dynamic_circuits/test_schedule.py index 1692c865a..fe249abaa 100644 --- a/test/dynamic_circuits/test_schedule.py +++ b/test/dynamic_circuits/test_schedule.py @@ -158,9 +158,6 @@ def test_measure_block_end(self): expected.measure(2, 0) expected.barrier() - - - import pdb;pdb.set_trace() self.assertEqual(expected, scheduled) def test_c_if_on_different_qubits(self): @@ -251,9 +248,6 @@ def test_shorter_measure_after_measure(self): expected.delay(1000, 2) expected.barrier() - import pdb - pdb.set_trace() - self.assertEqual(expected, scheduled) def test_measure_after_c_if(self): From ccbad76857183708eee200ca91ca761542bcb535 Mon Sep 17 00:00:00 2001 From: Thomas Alexander Date: Tue, 5 Jul 2022 12:35:21 -0300 Subject: [PATCH 34/70] Update non conditional tests to pass. --- test/dynamic_circuits/test_schedule.py | 278 ++++--------------------- 1 file changed, 36 insertions(+), 242 deletions(-) diff --git a/test/dynamic_circuits/test_schedule.py b/test/dynamic_circuits/test_schedule.py index fe249abaa..a75f6274c 100644 --- a/test/dynamic_circuits/test_schedule.py +++ b/test/dynamic_circuits/test_schedule.py @@ -12,6 +12,7 @@ """Test the dynamic circuits scheduling analysis""" +import sched import unittest from ddt import ddt, data, unpack @@ -36,26 +37,7 @@ class TestSchedulingAndPaddingPass(QiskitTestCase): def test_classically_controlled_gate_after_measure(self): """Test if schedules circuits with c_if after measure with a common clbit. - See: https://github.com/Qiskit/qiskit-terra/issues/7654 - (input) - ┌─┐ - q_0: ┤M├─────────── - └╥┘ ┌───┐ - q_1: ─╫────┤ X ├─── - ║ └─╥─┘ - ║ ┌────╨────┐ - c: 1/═╩═╡ c_0 = T ╞ - 0 └─────────┘ - (scheduled) - ┌─┐┌────────────────┐ - q_0: ───────────────────┤M├┤ Delay(200[dt]) ├ - ┌─────────────────┐└╥┘└─────┬───┬──────┘ - q_1: ┤ Delay(1000[dt]) ├─╫───────┤ X ├─────── - └─────────────────┘ ║ └─╥─┘ - ║ ┌────╨────┐ - c: 1/════════════════════╩════╡ c_0=0x1 ╞════ - 0 └─────────┘ - """ + See: https://github.com/Qiskit/qiskit-terra/issues/7654""" qc = QuantumCircuit(2, 1) qc.measure(0, 0) qc.x(1).c_if(0, True) @@ -75,26 +57,7 @@ def test_classically_controlled_gate_after_measure(self): def test_measure_after_measure(self): """Test if schedules circuits with measure after measure with a common clbit. - Note: There is no delay to write into the same clbit with IBM backends. - - (input) - ┌───┐┌─┐ - q_0: ┤ X ├┤M├─── - └───┘└╥┘┌─┐ - q_1: ──────╫─┤M├ - ║ └╥┘ - c: 1/══════╩══╩═ - 0 0 - (scheduled) - ┌───┐ ┌─┐ ░ - q_0: ──────┤ X ├───────┤M├────░─ - ┌─────┴───┴──────┐└╥┘┌─┐ ░ - q_1: ┤ Delay(200[dt]) ├─╫─┤M├─░─ - └────────────────┘ ║ └╥┘ ░ - c: 1/═══════════════════╩══╩════ - 0 0 - - """ + Note: There is no delay to write into the same clbit with IBM backends.""" qc = QuantumCircuit(2, 1) qc.x(0) qc.measure(0, 0) @@ -113,26 +76,7 @@ def test_measure_after_measure(self): self.assertEqual(expected, scheduled) def test_measure_block_end(self): - """Tests that measures trigger the end of a scheduling block and that measurements are grouped by block. - - (input) - ┌───┐┌─┐ - q_0: ┤ X ├┤M├─── - └───┘└╥┘┌─┐ - q_1: ──────╫─┤M├ - ║ └╥┘ - c: 1/══════╩══╩═ - 0 0 - (scheduled) - ┌───┐ ┌─┐ ░ - q_0: ──────┤ X ├───────┤M├────░─ - ┌─────┴───┴──────┐└╥┘┌─┐ ░ - q_1: ┤ Delay(200[dt]) ├─╫─┤M├─░─ - └────────────────┘ ║ └╥┘ ░ - c: 1/═══════════════════╩══╩════ - 0 0 - - """ + """Tests that measures trigger the end of a scheduling block and that measurements are grouped by block.""" qc = QuantumCircuit(3, 1) qc.x(0) qc.measure(0, 0) @@ -161,30 +105,7 @@ def test_measure_block_end(self): self.assertEqual(expected, scheduled) def test_c_if_on_different_qubits(self): - """Test if schedules circuits with `c_if`s on different qubits. - (input) - ┌─┐ - q_0: ┤M├────────────────────── - └╥┘ ┌───┐ - q_1: ─╫────┤ X ├────────────── - ║ └─╥─┘ ┌───┐ - q_2: ─╫──────╫────────┤ X ├─── - ║ ║ └─╥─┘ - ║ ┌────╨────┐┌────╨────┐ - c: 1/═╩═╡ c_0 = T ╞╡ c_0 = T ╞ - 0 └─────────┘└─────────┘ - (scheduled) - ┌─┐┌────────────────┐ - q_0: ───────────────────┤M├┤ Delay(200[dt]) ├─────────── - ┌─────────────────┐└╥┘└─────┬───┬──────┘ - q_1: ┤ Delay(1000[dt]) ├─╫───────┤ X ├────────────────── - ├─────────────────┤ ║ └─╥─┘ ┌───┐ - q_2: ┤ Delay(1000[dt]) ├─╫─────────╫────────────┤ X ├─── - └─────────────────┘ ║ ║ └─╥─┘ - ║ ┌────╨────┐ ┌────╨────┐ - c: 1/════════════════════╩════╡ c_0=0x1 ╞════╡ c_0=0x1 ╞ - 0 └─────────┘ └─────────┘ - """ + """Test if schedules circuits with `c_if`s on different qubits.""" qc = QuantumCircuit(3, 1) qc.measure(0, 0) qc.x(1).c_if(0, True) @@ -208,29 +129,7 @@ def test_shorter_measure_after_measure(self): """Test if schedules circuits with shorter measure after measure with a common clbit. Note: For dynamic circuits support we currently group measurements to start at the same time which in turn trigger - the end of a block. - - (input) - ┌─┐ - q_0: ┤M├─── - └╥┘┌─┐ - q_1: ─╫─┤M├ - ║ └╥┘ - q_2: ─╫──╫─ - ║ ║ - c: 1/═╩══╩═ - 0 0 - (scheduled) - ┌─┐ ░ - q_0: ┤M├─────────────────────────────────────────░─ - └╥┘ ┌─┐┌────────────────┐ ░ - q_1: ─╫────────────────────┤M├┤ Delay(300[dt]) ├─░─ - ║ ┌─────────────────┐└╥┘└────────────────┘ ░ - q_2: ─╫─┤ Delay(1000[dt]) ├─╫────────────────────░─ - ║ └─────────────────┘ ║ ░ - c: 1/═╩═════════════════════╩══════════════════════ - 0 0 - """ + the end of a block.""" qc = QuantumCircuit(3, 1) qc.measure(0, 0) qc.measure(1, 0) @@ -255,27 +154,6 @@ def test_measure_after_c_if(self): Note: This test is not yet correct as we should schedule the conditional block qubits with delays as well. - - (input) - ┌─┐ - q_0: ┤M├────────────── - └╥┘ ┌───┐ - q_1: ─╫────┤ X ├────── - ║ └─╥─┘ ┌─┐ - q_2: ─╫──────╫─────┤M├ - ║ ┌────╨────┐└╥┘ - c: 1/═╩═╡ c_0 = T ╞═╩═ - 0 └─────────┘ 0 - (scheduled) - ┌─┐ ░ ░ ┌─────────────────┐ - q_0: ───────────────────┤M├─░─────────────░─┤ Delay(1000[dt]) ├ - ┌─────────────────┐└╥┘ ░ ┌───┐ ░ ├─────────────────┤ - q_1: ┤ Delay(1000[dt]) ├─╫──░────┤ X ├────░─┤ Delay(1000[dt]) ├ - ├─────────────────┤ ║ ░ └─╥─┘ ░ └───────┬─┬───────┘ - q_2: ┤ Delay(1000[dt]) ├─╫──░──────╫──────░─────────┤M├──────── - └─────────────────┘ ║ ░ ┌────╨────┐ ░ └╥┘ - c: 1/════════════════════╩════╡ c_0=0x1 ╞════════════╩═════════ - 0 └─────────┘ 0 """ qc = QuantumCircuit(3, 1) qc.measure(0, 0) @@ -302,24 +180,7 @@ def test_measure_after_c_if(self): self.assertEqual(expected, scheduled) def test_parallel_gate_different_length(self): - """Test circuit having two parallel instruction with different length. - (input) - ┌───┐┌─┐ - q_0: ┤ X ├┤M├─── - ├───┤└╥┘┌─┐ - q_1: ┤ X ├─╫─┤M├ - └───┘ ║ └╥┘ - c: 2/══════╩══╩═ - 0 1 - (expected) - ┌───┐┌─┐┌────────────────┐ - q_0: ┤ X ├┤M├┤ Delay(200[dt]) ├ - ├───┤└╥┘└──────┬─┬───────┘ - q_1: ┤ X ├─╫────────┤M├──────── - └───┘ ║ └╥┘ - c: 2/══════╩═════════╩═════════ - 0 1 - """ + """Test circuit having two parallel instruction with different length.""" qc = QuantumCircuit(2, 2) qc.x(0) qc.x(1) @@ -331,36 +192,20 @@ def test_parallel_gate_different_length(self): ) pm = PassManager([DynamicCircuitScheduleAnalysis(durations), PadDelay()]) - qc_sched = pm.run(qc) + scheduled = pm.run(qc) - sched_expected = QuantumCircuit(2, 2) - sched_expected.x(0) - sched_expected.x(1) - sched_expected.measure(0, 0) # immediately start after X gate - sched_expected.measure(1, 1) - sched_expected.delay(200, 0) + expected = QuantumCircuit(2, 2) + expected.x(0) + expected.x(1) + expected.delay(200, 0) + expected.measure(0, 0) # immediately start after X gate + expected.measure(1, 1) + expected.barrier() - self.assertEqual(qc_sched, sched_expected) + self.assertEqual(scheduled, expected) def test_parallel_gate_different_length_with_barrier(self): - """Test circuit having two parallel instruction with different length with barrier. - (input) - ┌───┐┌─┐ - q_0: ┤ X ├┤M├─── - ├───┤└╥┘┌─┐ - q_1: ┤ X ├─╫─┤M├ - └───┘ ║ └╥┘ - c: 2/══════╩══╩═ - 0 1 - (expected) - ┌───┐┌────────────────┐ ░ ┌─┐ - q_0: ┤ X ├┤ Delay(200[dt]) ├─░─┤M├─── - ├───┤└────────────────┘ ░ └╥┘┌─┐ - q_1: ┤ X ├───────────────────░──╫─┤M├ - └───┘ ░ ║ └╥┘ - c: 2/═══════════════════════════╩══╩═ - 0 1 - """ + """Test circuit having two parallel instruction with different length with barrier.""" qc = QuantumCircuit(2, 2) qc.x(0) qc.x(1) @@ -373,44 +218,24 @@ def test_parallel_gate_different_length_with_barrier(self): ) pm = PassManager([DynamicCircuitScheduleAnalysis(durations), PadDelay()]) - qc_sched = pm.run(qc) + scheduled = pm.run(qc) - sched_expected = QuantumCircuit(2, 2) - sched_expected.x(0) - sched_expected.delay(200, 0) - sched_expected.x(1) - sched_expected.barrier() - sched_expected.measure(0, 0) - sched_expected.measure(1, 1) + expected = QuantumCircuit(2, 2) + expected.x(0) + expected.delay(200, 0) + expected.x(1) + expected.barrier() + expected.measure(0, 0) + expected.measure(1, 1) + expected.barrier() - self.assertEqual(qc_sched, sched_expected) + self.assertEqual(scheduled, expected) def test_measure_after_c_if_on_edge_locking(self): """Test if schedules circuits with c_if after measure with a common clbit. The scheduler is configured to reproduce behavior of the 0.20.0, in which clbit lock is applied to the end-edge of measure instruction. - See https://github.com/Qiskit/qiskit-terra/pull/7655 - (input) - ┌─┐ - q_0: ┤M├────────────── - └╥┘ ┌───┐ - q_1: ─╫────┤ X ├────── - ║ └─╥─┘ ┌─┐ - q_2: ─╫──────╫─────┤M├ - ║ ┌────╨────┐└╥┘ - c: 1/═╩═╡ c_0 = T ╞═╩═ - 0 └─────────┘ 0 - (scheduled) - ┌─┐┌────────────────┐ - q_0: ───────────────────┤M├┤ Delay(200[dt]) ├───────────────────── - ┌─────────────────┐└╥┘└─────┬───┬──────┘ - q_1: ┤ Delay(1000[dt]) ├─╫───────┤ X ├──────────────────────────── - └─────────────────┘ ║ └─╥─┘ ┌─┐┌────────────────┐ - q_2: ────────────────────╫─────────╫─────────┤M├┤ Delay(200[dt]) ├ - ║ ┌────╨────┐ └╥┘└────────────────┘ - c: 1/════════════════════╩════╡ c_0=0x1 ╞═════╩═══════════════════ - 0 └─────────┘ 0 - """ + See https://github.com/Qiskit/qiskit-terra/pull/7655""" qc = QuantumCircuit(3, 1) qc.measure(0, 0) qc.x(1).c_if(0, 1) @@ -442,15 +267,7 @@ def test_active_reset_circuit(self, write_lat, cond_lat): """Test practical example of reset circuit. Because of the stimulus pulse overlap with the previous XGate on the q register, measure instruction is always triggered after XGate regardless of write latency. - Thus only conditional latency matters in the scheduling. - (input) - ┌─┐ ┌───┐ ┌─┐ ┌───┐ ┌─┐ ┌───┐ - q: ┤M├───┤ X ├───┤M├───┤ X ├───┤M├───┤ X ├─── - └╥┘ └─╥─┘ └╥┘ └─╥─┘ └╥┘ └─╥─┘ - ║ ┌────╨────┐ ║ ┌────╨────┐ ║ ┌────╨────┐ - c: 1/═╩═╡ c_0=0x1 ╞═╩═╡ c_0=0x1 ╞═╩═╡ c_0=0x1 ╞ - 0 └─────────┘ 0 └─────────┘ 0 └─────────┘ - """ + Thus only conditional latency matters in the scheduling.""" qc = QuantumCircuit(1, 1) qc.measure(0, 0) qc.x(0).c_if(0, 1) @@ -493,26 +310,7 @@ def test_dag_introduces_extra_dependency_between_conditionals(self): however it must be scheduled after the conditional x on q0 in scheduling. That is because circuit model used in the transpiler passes (DAGCircuit) interprets instructions acting on common clbits must be run in the order - given by the original circuit (QuantumCircuit). - (input) - ┌────────────────┐ ┌───┐ - q_0: ┤ Delay(100[dt]) ├───┤ X ├─── - └─────┬───┬──────┘ └─╥─┘ - q_1: ──────┤ X ├────────────╫───── - └─╥─┘ ║ - ┌────╨────┐ ┌────╨────┐ - c: 1/═══╡ c_0=0x1 ╞════╡ c_0=0x1 ╞ - └─────────┘ └─────────┘ - (scheduled) - ┌────────────────┐ ┌───┐ - q_0: ┤ Delay(100[dt]) ├───┤ X ├────────────── - ├────────────────┤ └─╥─┘ ┌───┐ - q_1: ┤ Delay(100[dt]) ├─────╫────────┤ X ├─── - └────────────────┘ ║ └─╥─┘ - ┌────╨────┐┌────╨────┐ - c: 1/══════════════════╡ c_0=0x1 ╞╡ c_0=0x1 ╞ - └─────────┘└─────────┘ - """ + given by the original circuit (QuantumCircuit).""" qc = QuantumCircuit(2, 1) qc.delay(100, 0) qc.x(0).c_if(0, True) @@ -553,6 +351,7 @@ def test_scheduling_with_calibration(self): expected.delay(160, 0) expected.cx(0, 1) expected.add_calibration("x", (0,), xsched) + expected.barrier() self.assertEqual(expected, scheduled) @@ -568,15 +367,7 @@ def test_padding_not_working_without_scheduling(self): def test_no_pad_very_end_of_circuit(self): """Test padding option that inserts no delay at the very end of circuit. - This circuit will be unchanged after scheduling/padding. - ┌────────────────┐┌─┐ - q_0: ┤ Delay(100[dt]) ├┤M├ - └─────┬───┬──────┘└╥┘ - q_1: ──────┤ X ├────────╫─ - └───┘ ║ - c: 1/═══════════════════╩═ - 0 - """ + This circuit will be unchanged after scheduling/padding.""" qc = QuantumCircuit(2, 1) qc.delay(100, 0) qc.x(1) @@ -591,7 +382,10 @@ def test_no_pad_very_end_of_circuit(self): ] ).run(qc) - self.assertEqual(scheduled, qc) + expected = qc.copy() + expected.barrier() + + self.assertEqual(expected, scheduled) if __name__ == "__main__": From 25bba97da5aa2ef57fddd84a94d15eb97c0e90d7 Mon Sep 17 00:00:00 2001 From: Thomas Alexander Date: Tue, 5 Jul 2022 13:09:07 -0300 Subject: [PATCH 35/70] c_if scheduling tests are working. --- .../dynamic_circuits/base_padding.py | 19 +++++++- .../dynamic_circuits/schedule.py | 12 +++-- test/dynamic_circuits/test_schedule.py | 44 ++++++++++++------- 3 files changed, 54 insertions(+), 21 deletions(-) diff --git a/qiskit_ibm_provider/dynamic_circuits/base_padding.py b/qiskit_ibm_provider/dynamic_circuits/base_padding.py index 995c70694..1d6ba9d64 100644 --- a/qiskit_ibm_provider/dynamic_circuits/base_padding.py +++ b/qiskit_ibm_provider/dynamic_circuits/base_padding.py @@ -56,6 +56,7 @@ def __init__(self): self._dag = None self._block_duration = 0 self._current_block_idx = 0 + self._conditional_block = False super().__init__() @@ -105,6 +106,7 @@ def _init_run(self, dag): self._node_start_time = self.property_set["node_start_time"].copy() self._idle_after = {bit: 0 for bit in dag.qubits} self._current_block_idx = 0 + self._conditional_block = False self._block_duration = 0 # Prepare DAG to pad @@ -188,10 +190,16 @@ def _visit_generic(self, node): """Visit a generic node to pad.""" block_idx, t0 = self._node_start_time[node] + # Trigger the end of a block if block_idx > self._current_block_idx: + self._terminate_block(self._block_duration, self._current_block_idx) + # This block will not be padded as it is conditional. + # See TODO below. + self._conditional_block = True if node.op.condition_bits else False + # Now set the current block index. self._current_block_idx = block_idx @@ -219,7 +227,15 @@ def _visit_generic(self, node): def _terminate_block(self, block_duration, block_idx): """Terminate the end of a block scheduling region.""" - self._pad_until_block_end(block_duration, block_idx) + # Update all other qubits as not idle so that delays are *not* + # inserted. This is because we need the delays to be inserted in + # the conditional circuit block. However, c_if currently only + # allows writing a single conditional gate. + # TODO: This should be reworked to instead apply a transformation + # pass to rewrite all ``c_if`` operations as ``if_else`` + # blocks that are in turn scheduled. + if not self._conditional_block: + self._pad_until_block_end(block_duration, block_idx) # Terminate with a barrier to be clear timing is non-deterministic # across the barrier. @@ -228,6 +244,7 @@ def _terminate_block(self, block_duration, block_idx): # Reset idles for the new block. self._idle_after = {bit: 0 for bit in self._dag.qubits} self._block_duration = 0 + self._conditional_block = False def _pad_until_block_end(self, block_duration, block_idx): diff --git a/qiskit_ibm_provider/dynamic_circuits/schedule.py b/qiskit_ibm_provider/dynamic_circuits/schedule.py index fa12dc43b..cfec5e04f 100644 --- a/qiskit_ibm_provider/dynamic_circuits/schedule.py +++ b/qiskit_ibm_provider/dynamic_circuits/schedule.py @@ -73,8 +73,6 @@ def run(self, dag): for node in dag.topological_op_nodes(): self._visit_node(node) - for node, start_time in self._node_start_time.items(): - print(repr(node), start_time) self.property_set["node_start_time"] = self._node_start_time def _init_run(self, dag): @@ -123,8 +121,11 @@ def _visit_conditional_node(self, node): TODO: Update for support of general control-flow, not just single conditional operations. """ + # Special processing required to resolve conditional scheduling dependencies if node.op.condition_bits: - # Special processing required to resolve conditional scheduling dependencies + # Trigger the start of a conditional block + self._begin_new_circuit_block() + op_duration = self._get_node_duration(node) t0q = max(self._idle_after[q][1] for q in node.qargs) @@ -150,11 +151,16 @@ def _visit_conditional_node(self, node): for bit in node.op.condition_bits: # Lock clbit until state is read self._idle_after[bit] = (self._current_block_idx, t1c) + # It starts after register read access t0 = max(t0q, t1c) t1 = t0 + op_duration self._update_idles(node, t0, t1) + + # Terminate the conditional block + self._begin_new_circuit_block() + else: # Fall through to generic case if not conditional self._visit_generic(node) diff --git a/test/dynamic_circuits/test_schedule.py b/test/dynamic_circuits/test_schedule.py index a75f6274c..cb486a237 100644 --- a/test/dynamic_circuits/test_schedule.py +++ b/test/dynamic_circuits/test_schedule.py @@ -47,10 +47,11 @@ def test_classically_controlled_gate_after_measure(self): scheduled = pm.run(qc) expected = QuantumCircuit(2, 1) + expected.delay(1000, 1) expected.measure(0, 0) - expected.delay(1000, 1) # x.c_if starts after measure + expected.barrier() expected.x(1).c_if(0, True) - expected.delay(200, 0) + expected.barrier() self.assertEqual(expected, scheduled) @@ -116,12 +117,14 @@ def test_c_if_on_different_qubits(self): scheduled = pm.run(qc) expected = QuantumCircuit(3, 1) - expected.measure(0, 0) expected.delay(1000, 1) expected.delay(1000, 2) + expected.measure(0, 0) + expected.barrier() expected.x(1).c_if(0, True) + expected.barrier() expected.x(2).c_if(0, True) - expected.delay(200, 0) + expected.barrier() self.assertEqual(expected, scheduled) @@ -176,6 +179,7 @@ def test_measure_after_c_if(self): expected.delay(1000, 0) expected.measure(2, 0) expected.delay(1000, 1) + expected.barrier() self.assertEqual(expected, scheduled) @@ -244,7 +248,7 @@ def test_measure_after_c_if_on_edge_locking(self): durations = InstructionDurations([("x", None, 200), ("measure", None, 1000)]) # lock at the end edge - actual_sched = PassManager( + scheduled = PassManager( [ SetIOLatency(clbit_write_latency=1000), DynamicCircuitScheduleAnalysis(durations), @@ -252,14 +256,19 @@ def test_measure_after_c_if_on_edge_locking(self): ] ).run(qc) - expected_sched = QuantumCircuit(3, 1) - expected_sched.measure(0, 0) - expected_sched.delay(1000, 1) - expected_sched.x(1).c_if(0, 1) - expected_sched.measure(2, 0) - expected_sched.delay(200, 0) - expected_sched.delay(200, 2) - self.assertEqual(expected_sched, actual_sched) + expected = QuantumCircuit(3, 1) + expected.delay(1000, 1) + expected.delay(1000, 2) + expected.measure(0, 0) + expected.barrier() + expected.x(1).c_if(0, 1) + expected.barrier() + expected.delay(1000, 0) + expected.delay(1000, 1) + expected.measure(2, 0) + expected.barrier() + + self.assertEqual(expected, scheduled) @data([100, 200], [500, 0], [1000, 200]) @unpack @@ -278,7 +287,7 @@ def test_active_reset_circuit(self, write_lat, cond_lat): durations = InstructionDurations([("x", None, 100), ("measure", None, 1000)]) - actual_sched = PassManager( + scheduled = PassManager( [ SetIOLatency( clbit_write_latency=write_lat, conditional_latency=cond_lat @@ -302,7 +311,7 @@ def test_active_reset_circuit(self, write_lat, cond_lat): expected.delay(cond_lat, 0) expected.x(0).c_if(0, 1) - self.assertEqual(expected, actual_sched) + self.assertEqual(expected, scheduled) def test_dag_introduces_extra_dependency_between_conditionals(self): """Test dependency between conditional operations in the scheduling. @@ -321,10 +330,11 @@ def test_dag_introduces_extra_dependency_between_conditionals(self): scheduled = pm.run(qc) expected = QuantumCircuit(2, 1) - expected.delay(100, 0) - expected.delay(100, 1) # due to extra dependency on clbits + expected.barrier() expected.x(0).c_if(0, True) + expected.barrier() expected.x(1).c_if(0, True) + expected.barrier() self.assertEqual(expected, scheduled) From 65760f3a0c6ad3063ad5fd87eab6deb6c24cab58 Mon Sep 17 00:00:00 2001 From: Thomas Alexander Date: Tue, 5 Jul 2022 13:15:17 -0300 Subject: [PATCH 36/70] All tests passing. --- qiskit_ibm_provider/dynamic_circuits/schedule.py | 6 +++--- test/dynamic_circuits/test_schedule.py | 12 ++++++------ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/qiskit_ibm_provider/dynamic_circuits/schedule.py b/qiskit_ibm_provider/dynamic_circuits/schedule.py index cfec5e04f..75a9f6545 100644 --- a/qiskit_ibm_provider/dynamic_circuits/schedule.py +++ b/qiskit_ibm_provider/dynamic_circuits/schedule.py @@ -144,10 +144,10 @@ def _visit_conditional_node(self, node): # |t0q # Q ▒▒▒▒▒▒▒▒▒▒▒ # C ▒▒▒░░░▒▒░░░ - # |t0q - conditional_latency + # |t0q # - t0c = max(t0q - self._conditional_latency, t0c) - t1c = t0c + self._conditional_latency + t0c = max(t0q, t0c) + t1c = t0c for bit in node.op.condition_bits: # Lock clbit until state is read self._idle_after[bit] = (self._current_block_idx, t1c) diff --git a/test/dynamic_circuits/test_schedule.py b/test/dynamic_circuits/test_schedule.py index cb486a237..b15ab4d93 100644 --- a/test/dynamic_circuits/test_schedule.py +++ b/test/dynamic_circuits/test_schedule.py @@ -299,17 +299,17 @@ def test_active_reset_circuit(self, write_lat, cond_lat): expected = QuantumCircuit(1, 1) expected.measure(0, 0) - if cond_lat > 0: - expected.delay(cond_lat, 0) + expected.barrier() expected.x(0).c_if(0, 1) + expected.barrier() expected.measure(0, 0) - if cond_lat > 0: - expected.delay(cond_lat, 0) + expected.barrier() expected.x(0).c_if(0, 1) + expected.barrier() expected.measure(0, 0) - if cond_lat > 0: - expected.delay(cond_lat, 0) + expected.barrier() expected.x(0).c_if(0, 1) + expected.barrier() self.assertEqual(expected, scheduled) From d7cbdd5a0d49e5244d3aa398ad1c6b782d22cd88 Mon Sep 17 00:00:00 2001 From: Thomas Alexander Date: Tue, 5 Jul 2022 21:57:54 -0300 Subject: [PATCH 37/70] All dynamic circuits tests passing. --- .../dynamic_circuits/schedule.py | 26 ++++-- test/dynamic_circuits/test_schedule.py | 82 +++++++++++++++++++ 2 files changed, 103 insertions(+), 5 deletions(-) diff --git a/qiskit_ibm_provider/dynamic_circuits/schedule.py b/qiskit_ibm_provider/dynamic_circuits/schedule.py index 75a9f6545..0a47d7d6c 100644 --- a/qiskit_ibm_provider/dynamic_circuits/schedule.py +++ b/qiskit_ibm_provider/dynamic_circuits/schedule.py @@ -15,7 +15,7 @@ import itertools import qiskit -from qiskit.circuit import Measure +from qiskit.circuit import Measure, Reset from qiskit.transpiler.exceptions import TranspilerError from qiskit.transpiler.passes.scheduling.scheduling.base_scheduler import BaseScheduler @@ -108,6 +108,8 @@ def _visit_node(self, node): if isinstance(node.op, Measure): self._visit_measure(node) + elif isinstance(node.op, Reset): + self._visit_reset(node) else: self._visit_generic(node) @@ -171,11 +173,9 @@ def _visit_measure(self, node): Measurement currently triggers the end of a pulse block in IBM dynamic circuits hardware. This means that it is possible to schedule *up to* a measurement (and during its pulses) but the measurement will be followed by a period of indeterminism. - All measurements on disjoing qubits will be collected on the same qubits to be run simulataneously. + All measurements on disjoint qubits will be collected on the same qubits to be run simultaneously. """ - op_duration = self._get_node_duration(node) - current_block_measure_qargs = self._current_block_measure_qargs() measure_qargs = set(node.qargs) @@ -198,9 +198,25 @@ def _visit_measure(self, node): for measure in self._current_block_measures: t0 = t0q - t1 = t0 + self._get_node_duration(measure) + bit_indices = {bit: index for index, bit in enumerate(self._dag.qubits)} + measure_duration = self.durations.get( + Measure(), [bit_indices[qarg] for qarg in node.qargs], unit="dt" + ) + t1 = t0 + measure_duration self._update_idles(measure, t0, t1) + def _visit_reset(self, node): + """Visit a reset node. + + Reset currently triggers the end of a pulse block in IBM dynamic circuits hardware + as conditional reset is performed internally using a c_if. + This means that it is possible to schedule *up to* a reset (and during its measurement pulses) + but the reset will be followed by a period of conditional indeterminism. + All resets on disjoint qubits will be collected on the same qubits to be run simultaneously. + This means that from the perspective of scheduling resets have the same behaviour and duration + as a measurement. + """ + self._visit_measure(node) def _visit_generic(self, node): """Visit a generic node such as a gate or barrier.""" diff --git a/test/dynamic_circuits/test_schedule.py b/test/dynamic_circuits/test_schedule.py index b15ab4d93..f629c2a52 100644 --- a/test/dynamic_circuits/test_schedule.py +++ b/test/dynamic_circuits/test_schedule.py @@ -397,6 +397,88 @@ def test_no_pad_very_end_of_circuit(self): self.assertEqual(expected, scheduled) + def test_reset_terminates_block(self): + """Test if reset operations terminate the block scheduled + + Note: For dynamic circuits support we currently group resets to start at the same time which in turn trigger + the end of a block.""" + qc = QuantumCircuit(3, 1) + qc.x(0) + qc.reset(0) + qc.reset(1) + + durations = InstructionDurations( + [ + ("x", None, 200), + ( + "reset", + [0], + 1000, + ), # ignored as only the duration of the measurement is used for scheduling + ( + "reset", + [1], + 900, + ), # ignored as only the duration of the measurement is used for scheduling + ("measure", [0], 600), + ("measure", [1], 700), + ] + ) + pm = PassManager([DynamicCircuitScheduleAnalysis(durations), PadDelay()]) + scheduled = pm.run(qc) + + expected = QuantumCircuit(3, 1) + expected.x(0) + expected.delay(200, 1) + expected.delay(1200, 2) + expected.reset(0) + expected.reset(1) + expected.delay(100, 1) + expected.barrier() + + self.assertEqual(expected, scheduled) + + def test_reset_merged_with_measure(self): + """Test if reset operations terminate the block scheduled + + Note: For dynamic circuits support we currently group resets to start at the same time which in turn trigger + the end of a block.""" + qc = QuantumCircuit(3, 1) + qc.x(0) + qc.reset(0) + qc.reset(1) + + durations = InstructionDurations( + [ + ("x", None, 200), + ( + "reset", + [0], + 1000, + ), # ignored as only the duration of the measurement is used for scheduling + ( + "reset", + [1], + 900, + ), # ignored as only the duration of the measurement is used for scheduling + ("measure", [0], 600), + ("measure", [1], 700), + ] + ) + pm = PassManager([DynamicCircuitScheduleAnalysis(durations), PadDelay()]) + scheduled = pm.run(qc) + + expected = QuantumCircuit(3, 1) + expected.x(0) + expected.delay(200, 1) + expected.delay(1200, 2) + expected.reset(0) + expected.reset(1) + expected.delay(100, 1) + expected.barrier() + + self.assertEqual(expected, scheduled) + if __name__ == "__main__": unittest.main() From 2be8de8385d4f9200f5fb960c6634b2a8f165d89 Mon Sep 17 00:00:00 2001 From: Thomas Alexander Date: Tue, 5 Jul 2022 22:03:01 -0300 Subject: [PATCH 38/70] Black formatting. --- qiskit_ibm_provider/dynamic_circuits/base_padding.py | 12 ++++-------- qiskit_ibm_provider/dynamic_circuits/pad_delay.py | 4 +++- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/qiskit_ibm_provider/dynamic_circuits/base_padding.py b/qiskit_ibm_provider/dynamic_circuits/base_padding.py index 1d6ba9d64..96145c16b 100644 --- a/qiskit_ibm_provider/dynamic_circuits/base_padding.py +++ b/qiskit_ibm_provider/dynamic_circuits/base_padding.py @@ -60,7 +60,6 @@ def __init__(self): super().__init__() - def run(self, dag: DAGCircuit): """Run the padding pass on ``dag``. @@ -76,7 +75,6 @@ def run(self, dag: DAGCircuit): """ self._pre_runhook(dag) - self._init_run(dag) # Compute fresh circuit duration from the node start time dictionary and op duration. @@ -127,7 +125,6 @@ def _init_run(self, dag): self._dag.calibrations = dag.calibrations self._dag.global_phase = dag.global_phase - def _pre_runhook(self, dag: DAGCircuit): """Extra routine inserted before running the padding pass. @@ -190,7 +187,6 @@ def _visit_generic(self, node): """Visit a generic node to pad.""" block_idx, t0 = self._node_start_time[node] - # Trigger the end of a block if block_idx > self._current_block_idx: @@ -203,7 +199,6 @@ def _visit_generic(self, node): # Now set the current block index. self._current_block_idx = block_idx - t1 = t0 + node.op.duration self._block_duration = max(self._block_duration, t1) @@ -239,16 +234,17 @@ def _terminate_block(self, block_duration, block_idx): # Terminate with a barrier to be clear timing is non-deterministic # across the barrier. - self._dag.apply_operation_back(Barrier(self._dag.num_qubits()), self._dag.qubits, []) + self._dag.apply_operation_back( + Barrier(self._dag.num_qubits()), self._dag.qubits, [] + ) # Reset idles for the new block. self._idle_after = {bit: 0 for bit in self._dag.qubits} self._block_duration = 0 self._conditional_block = False - def _pad_until_block_end(self, block_duration, block_idx): - # Add delays until the end of circuit. + # Add delays until the end of circuit. for bit in self._dag.qubits: if block_duration - self._idle_after[bit] > 0: node = self._dag.output_map[bit] diff --git a/qiskit_ibm_provider/dynamic_circuits/pad_delay.py b/qiskit_ibm_provider/dynamic_circuits/pad_delay.py index ca081ff4d..6250ec82d 100644 --- a/qiskit_ibm_provider/dynamic_circuits/pad_delay.py +++ b/qiskit_ibm_provider/dynamic_circuits/pad_delay.py @@ -72,4 +72,6 @@ def _pad( return time_interval = t_end - t_start - self._apply_scheduled_op(block_idx, t_start, Delay(time_interval, self._dag.unit), qubit) + self._apply_scheduled_op( + block_idx, t_start, Delay(time_interval, self._dag.unit), qubit + ) From eb33397ef029dbee8b8536cdebe7fe6e04bf7bc9 Mon Sep 17 00:00:00 2001 From: Thomas Alexander Date: Tue, 5 Jul 2022 22:42:31 -0300 Subject: [PATCH 39/70] Move the dynamic_circuits module to a generic transpiler module. --- .../dynamic_circuits/__init__.py | 51 ------------ qiskit_ibm_provider/transpiler/__init__.py | 22 ++++++ .../transpiler/passes/__init__.py | 30 +++++++ .../transpiler/passes/scheduling/__init__.py | 79 +++++++++++++++++++ .../passes/scheduling}/base_padding.py | 2 +- .../passes/scheduling}/pad_delay.py | 4 +- .../passes/scheduling/scheduler.py} | 0 ...amic-circuits-module-af6f530072c82f44.yaml | 7 -- ...c-circuits-scheduler-b1ae525c0b358acb.yaml | 10 +++ ...dd-transpiler-module-af6f530072c82f44.yaml | 13 +++ .../__init__.py | 0 test/transpiler/passes/__init__.py | 11 +++ test/transpiler/passes/scheduling/__init__.py | 11 +++ .../passes/scheduling}/test_schedule.py | 28 ++----- 14 files changed, 185 insertions(+), 83 deletions(-) delete mode 100644 qiskit_ibm_provider/dynamic_circuits/__init__.py create mode 100644 qiskit_ibm_provider/transpiler/__init__.py create mode 100644 qiskit_ibm_provider/transpiler/passes/__init__.py create mode 100644 qiskit_ibm_provider/transpiler/passes/scheduling/__init__.py rename qiskit_ibm_provider/{dynamic_circuits => transpiler/passes/scheduling}/base_padding.py (99%) rename qiskit_ibm_provider/{dynamic_circuits => transpiler/passes/scheduling}/pad_delay.py (94%) rename qiskit_ibm_provider/{dynamic_circuits/schedule.py => transpiler/passes/scheduling/scheduler.py} (100%) delete mode 100644 releasenotes/notes/add-dynamic-circuits-module-af6f530072c82f44.yaml create mode 100644 releasenotes/notes/add-dynamic-circuits-scheduler-b1ae525c0b358acb.yaml create mode 100644 releasenotes/notes/add-transpiler-module-af6f530072c82f44.yaml rename test/{dynamic_circuits => transpiler}/__init__.py (100%) create mode 100644 test/transpiler/passes/__init__.py create mode 100644 test/transpiler/passes/scheduling/__init__.py rename test/{dynamic_circuits => transpiler/passes/scheduling}/test_schedule.py (95%) diff --git a/qiskit_ibm_provider/dynamic_circuits/__init__.py b/qiskit_ibm_provider/dynamic_circuits/__init__.py deleted file mode 100644 index 09c169da8..000000000 --- a/qiskit_ibm_provider/dynamic_circuits/__init__.py +++ /dev/null @@ -1,51 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2022. -# -# 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. - -""" -============================================================== -Dynamic Circuits (:mod:`qiskit_ibm_provider.dynamic_circuits`) -============================================================== - -.. currentmodule:: qiskit_ibm_provider.dynamic_circuits - -A collection of tools for working with IBM Quantum's next-generation -backends that support advanced "dynamic circuit" capabilities. Ie., -circuits with support for classical control-flow/feedback based off -of measurement results. - -Example Usage on a Supporting Backend -===================================== - -.. jupyter-execute:: - :hide-code: - :hide-output: - - from qiskit_ibm_provider.test.ibm_provider_mock import mock_get_backend - mock_get_backend('FakePerth') - -.. jupyter-execute:: - - from qiskit_ibm_provider import IBMProvider - import qiskit_ibm_provider.jupyter - - provider = IBMProvider(hub='ibm-q') - backend = provider.get_backend('ibm_perth') - - - -Scheduling & Dynamical Decoupling -================================= -.. autosummary:: - :toctree: ../stubs/ - - -""" diff --git a/qiskit_ibm_provider/transpiler/__init__.py b/qiskit_ibm_provider/transpiler/__init__.py new file mode 100644 index 000000000..a2ae05a42 --- /dev/null +++ b/qiskit_ibm_provider/transpiler/__init__.py @@ -0,0 +1,22 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2022. +# +# 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. + +""" +==================================================================== +IBM Backend Transpiler Tools (:mod:`qiskit_ibm_provider.transpiler`) +==================================================================== + +A collection of transpiler tools for working with IBM Quantum's +next-generation backends that support advanced "dynamic circuit" +capabilities. Ie., circuits with support for classical +compute and control-flow/feedback based off of measurement results. +""" diff --git a/qiskit_ibm_provider/transpiler/passes/__init__.py b/qiskit_ibm_provider/transpiler/passes/__init__.py new file mode 100644 index 000000000..680a92d87 --- /dev/null +++ b/qiskit_ibm_provider/transpiler/passes/__init__.py @@ -0,0 +1,30 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2022. +# +# 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. + +""" +=================================================== +Transpiler Passes (:mod:`qiskit_ibm_provider.transpiler.passes`) +=================================================== + +.. currentmodule:: qiskit_ibm_provider.transpiler.passes + +.. autosummary:: + :toctree: ../stubs/ + + Dynamic circuit scheduling + + +""" + +# circuit scheduling +from .scheduling import DynamicCircuitScheduleAnalysis +from .scheduling import PadDelay diff --git a/qiskit_ibm_provider/transpiler/passes/scheduling/__init__.py b/qiskit_ibm_provider/transpiler/passes/scheduling/__init__.py new file mode 100644 index 000000000..a995525ba --- /dev/null +++ b/qiskit_ibm_provider/transpiler/passes/scheduling/__init__.py @@ -0,0 +1,79 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2022. +# +# 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. + +""" +============================================================== +Scheduling (:mod:`qiskit_ibm_provider.transpiler.passes.scheduling`) +============================================================== + +.. currentmodule:: qiskit_ibm_provider.transpiler.passes.scheduling + +A collection of scheduling passes for working with IBM Quantum's next-generation +backends that support advanced "dynamic circuit" capabilities. Ie., +circuits with support for classical control-flow/feedback based off +of measurement results. + + +Below we demonstrate how to schedule and pad a teleportation circuit with delays +for a dynamic circuit backend's execution model + + +.. jupyter-execute:: + + from qiskit.transpiler.instruction_durations import InstructionDurations + + from qiskit_ibm_provider.transpiler.scheduling import DynamicCircuitScheduleAnalysis, PadDelay + from qiskit_ibm_provider.test.ibm_provider_mock import mock_get_backend + + + backend = mock_get_backend('FakePerth') + + durations = InstructionDurations.from_backend(backend) + pm = PassManager([DynamicCircuitScheduleAnalysis(durations), PadDelay()]) + + qr = QuantumRegister(3) + crz = ClassicalRegister(1, name="crz") + crx = ClassicalRegister(1, name="crx") + result = ClassicalRegister(1, name="result") + + teleport = QuantumCircuit(qr, crz, crx, result, name="Teleport") + + teleport.h(qr[1]) + teleport.cx(qr[1], qr[2]) + teleport.cx(qr[0], qr[1]) + teleport.h(qr[0]) + teleport.measure(qr[0], crz) + teleport.measure(qr[1], crx) + teleport.z(qr[2]).c_if(crz, 1) + teleport.x(qr[2]).c_if(crx, 1) + teleport.measure(qr[2], result) + + scheduled_teleport = pm.run(teleport) + + teleport.draw(output="mpl") + + +Scheduling & Dynamical Decoupling +================================= +.. autosummary:: + :toctree: ../stubs/ + + BasePadding + DynamicCircuitScheduleAnalysis + PadDelay + + + +""" + +from .pad_delay import PadDelay +from .scheduler import DynamicCircuitScheduleAnalysis diff --git a/qiskit_ibm_provider/dynamic_circuits/base_padding.py b/qiskit_ibm_provider/transpiler/passes/scheduling/base_padding.py similarity index 99% rename from qiskit_ibm_provider/dynamic_circuits/base_padding.py rename to qiskit_ibm_provider/transpiler/passes/scheduling/base_padding.py index 96145c16b..494c68187 100644 --- a/qiskit_ibm_provider/dynamic_circuits/base_padding.py +++ b/qiskit_ibm_provider/transpiler/passes/scheduling/base_padding.py @@ -10,7 +10,7 @@ # copyright notice, and modified files need to carry a notice indicating # that they have been altered from the originals. -"""Padding pass to fill timeslots for the dynamic circuits backends.""" +"""Padding pass to fill timeslots for IBM (dynamic circuit) backends.""" from typing import List, Optional, Union diff --git a/qiskit_ibm_provider/dynamic_circuits/pad_delay.py b/qiskit_ibm_provider/transpiler/passes/scheduling/pad_delay.py similarity index 94% rename from qiskit_ibm_provider/dynamic_circuits/pad_delay.py rename to qiskit_ibm_provider/transpiler/passes/scheduling/pad_delay.py index 6250ec82d..a7e3824e5 100644 --- a/qiskit_ibm_provider/dynamic_circuits/pad_delay.py +++ b/qiskit_ibm_provider/transpiler/passes/scheduling/pad_delay.py @@ -14,9 +14,9 @@ from qiskit.circuit import Qubit from qiskit.circuit.delay import Delay -from qiskit.dagcircuit import DAGCircuit, DAGNode, DAGOutNode +from qiskit.dagcircuit import DAGNode, DAGOutNode -from qiskit_ibm_provider.dynamic_circuits.base_padding import BasePadding +from .base_padding import BasePadding class PadDelay(BasePadding): diff --git a/qiskit_ibm_provider/dynamic_circuits/schedule.py b/qiskit_ibm_provider/transpiler/passes/scheduling/scheduler.py similarity index 100% rename from qiskit_ibm_provider/dynamic_circuits/schedule.py rename to qiskit_ibm_provider/transpiler/passes/scheduling/scheduler.py diff --git a/releasenotes/notes/add-dynamic-circuits-module-af6f530072c82f44.yaml b/releasenotes/notes/add-dynamic-circuits-module-af6f530072c82f44.yaml deleted file mode 100644 index de65fd8ef..000000000 --- a/releasenotes/notes/add-dynamic-circuits-module-af6f530072c82f44.yaml +++ /dev/null @@ -1,7 +0,0 @@ ---- -features: - - | - A new dynamic circuits module has been added to :mod:`qiskit_ibm_provider.dynamic_circuits`. - It will contain all specialized Qiskit routines for running applications on IBM's next-generation - quantum devices that support dynamic capabilities such as control-flow(feedforward) and classical - compute. diff --git a/releasenotes/notes/add-dynamic-circuits-scheduler-b1ae525c0b358acb.yaml b/releasenotes/notes/add-dynamic-circuits-scheduler-b1ae525c0b358acb.yaml new file mode 100644 index 000000000..54ffb0bd9 --- /dev/null +++ b/releasenotes/notes/add-dynamic-circuits-scheduler-b1ae525c0b358acb.yaml @@ -0,0 +1,10 @@ +--- +features: + - | + A scheduling analysis pass, :class:`~qiskit_ibm_provider.transpiler.passes.scheduling.DynamicCircuitScheduleAnalysis` + has been added for Qiskit dynamic circuit (OpenQASM 3) backends. This is capable of handling + scheduling for deterministic regions of a quantum circuit and may combined with a padding pass such as + :class:`~qiskit_ibm_provider.transpiler.passes.scheduling.PadDelay` to pad schedulable sections of a + circuit with delays. + + For an example see the :mod:`~qiskit_ibm_provider.transpiler.passes.scheduling` module's documentation. diff --git a/releasenotes/notes/add-transpiler-module-af6f530072c82f44.yaml b/releasenotes/notes/add-transpiler-module-af6f530072c82f44.yaml new file mode 100644 index 000000000..5b544a897 --- /dev/null +++ b/releasenotes/notes/add-transpiler-module-af6f530072c82f44.yaml @@ -0,0 +1,13 @@ +--- +prelude: > + A :mod:`~qiskit_ibm_provider.transpiler` module has been added. + It will contain routines that are specific to IBM hardware backends and + which consequently can not be placed directly within Qiskit Terra. +features: + - | + The module :mod:`~qiskit_ibm_provider.transpiler`` has been added. + + Primarily, it will contain all specialized Qiskit routines for running + applications on IBM's next-generation quantum devices that support dynamic + capabilities such as control-flow(feedforward) and classical + compute. diff --git a/test/dynamic_circuits/__init__.py b/test/transpiler/__init__.py similarity index 100% rename from test/dynamic_circuits/__init__.py rename to test/transpiler/__init__.py diff --git a/test/transpiler/passes/__init__.py b/test/transpiler/passes/__init__.py new file mode 100644 index 000000000..fdb172d36 --- /dev/null +++ b/test/transpiler/passes/__init__.py @@ -0,0 +1,11 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2022. +# +# 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. diff --git a/test/transpiler/passes/scheduling/__init__.py b/test/transpiler/passes/scheduling/__init__.py new file mode 100644 index 000000000..fdb172d36 --- /dev/null +++ b/test/transpiler/passes/scheduling/__init__.py @@ -0,0 +1,11 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2022. +# +# 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. diff --git a/test/dynamic_circuits/test_schedule.py b/test/transpiler/passes/scheduling/test_schedule.py similarity index 95% rename from test/dynamic_circuits/test_schedule.py rename to test/transpiler/passes/scheduling/test_schedule.py index f629c2a52..58846626f 100644 --- a/test/dynamic_circuits/test_schedule.py +++ b/test/transpiler/passes/scheduling/test_schedule.py @@ -12,26 +12,17 @@ """Test the dynamic circuits scheduling analysis""" -import sched -import unittest - -from ddt import ddt, data, unpack from qiskit import QuantumCircuit from qiskit.pulse import Schedule, Play, Constant, DriveChannel from qiskit.test import QiskitTestCase from qiskit.transpiler.instruction_durations import InstructionDurations -from qiskit.transpiler.passes import ( - PadDelay, - SetIOLatency, -) from qiskit.transpiler.passmanager import PassManager from qiskit.transpiler.exceptions import TranspilerError -from qiskit_ibm_provider.dynamic_circuits.pad_delay import PadDelay -from qiskit_ibm_provider.dynamic_circuits.schedule import DynamicCircuitScheduleAnalysis +from qiskit_ibm_provider.transpiler.passes.scheduling.pad_delay import PadDelay +from qiskit_ibm_provider.transpiler.passes.scheduling.scheduler import DynamicCircuitScheduleAnalysis -@ddt class TestSchedulingAndPaddingPass(QiskitTestCase): """Tests the Scheduling passes""" @@ -250,7 +241,6 @@ def test_measure_after_c_if_on_edge_locking(self): # lock at the end edge scheduled = PassManager( [ - SetIOLatency(clbit_write_latency=1000), DynamicCircuitScheduleAnalysis(durations), PadDelay(), ] @@ -270,10 +260,9 @@ def test_measure_after_c_if_on_edge_locking(self): self.assertEqual(expected, scheduled) - @data([100, 200], [500, 0], [1000, 200]) - @unpack - def test_active_reset_circuit(self, write_lat, cond_lat): + def test_active_reset_circuit(self): """Test practical example of reset circuit. + Because of the stimulus pulse overlap with the previous XGate on the q register, measure instruction is always triggered after XGate regardless of write latency. Thus only conditional latency matters in the scheduling.""" @@ -289,9 +278,6 @@ def test_active_reset_circuit(self, write_lat, cond_lat): scheduled = PassManager( [ - SetIOLatency( - clbit_write_latency=write_lat, conditional_latency=cond_lat - ), DynamicCircuitScheduleAnalysis(durations), PadDelay(), ] @@ -315,6 +301,7 @@ def test_active_reset_circuit(self, write_lat, cond_lat): def test_dag_introduces_extra_dependency_between_conditionals(self): """Test dependency between conditional operations in the scheduling. + In the below example circuit, the conditional x on q1 could start at time 0, however it must be scheduled after the conditional x on q0 in scheduling. That is because circuit model used in the transpiler passes (DAGCircuit) @@ -377,6 +364,7 @@ def test_padding_not_working_without_scheduling(self): def test_no_pad_very_end_of_circuit(self): """Test padding option that inserts no delay at the very end of circuit. + This circuit will be unchanged after scheduling/padding.""" qc = QuantumCircuit(2, 1) qc.delay(100, 0) @@ -478,7 +466,3 @@ def test_reset_merged_with_measure(self): expected.barrier() self.assertEqual(expected, scheduled) - - -if __name__ == "__main__": - unittest.main() From 8deed389800887c25d3c346758e3296f798e56e7 Mon Sep 17 00:00:00 2001 From: Thomas Alexander Date: Tue, 5 Jul 2022 23:13:58 -0300 Subject: [PATCH 40/70] Blackify --- .../transpiler/passes/scheduling/scheduler.py | 26 +++++++++++++------ .../passes/scheduling/test_schedule.py | 4 ++- 2 files changed, 21 insertions(+), 9 deletions(-) diff --git a/qiskit_ibm_provider/transpiler/passes/scheduling/scheduler.py b/qiskit_ibm_provider/transpiler/passes/scheduling/scheduler.py index 0a47d7d6c..a593b3015 100644 --- a/qiskit_ibm_provider/transpiler/passes/scheduling/scheduler.py +++ b/qiskit_ibm_provider/transpiler/passes/scheduling/scheduler.py @@ -40,7 +40,9 @@ class DynamicCircuitScheduleAnalysis(BaseScheduler): """ - def __init__(self, durations: qiskit.transpiler.instruction_durations.InstructionDurations): + def __init__( + self, durations: qiskit.transpiler.instruction_durations.InstructionDurations + ): """Scheduler for dynamic circuit backends. Args: @@ -187,7 +189,15 @@ def _visit_measure(self, node): t0q = 0 # Otherwise we need to increment all measurements to start at the same time within the block. else: - t0q = max(itertools.chain([t0q], (self._node_start_time[measure][1] for measure in self._current_block_measures))) + t0q = max( + itertools.chain( + [t0q], + ( + self._node_start_time[measure][1] + for measure in self._current_block_measures + ), + ) + ) # Insert this measure into the block self._current_block_measures.add(node) @@ -200,8 +210,8 @@ def _visit_measure(self, node): t0 = t0q bit_indices = {bit: index for index, bit in enumerate(self._dag.qubits)} measure_duration = self.durations.get( - Measure(), [bit_indices[qarg] for qarg in node.qargs], unit="dt" - ) + Measure(), [bit_indices[qarg] for qarg in node.qargs], unit="dt" + ) t1 = t0 + measure_duration self._update_idles(measure, t0, t1) @@ -237,12 +247,12 @@ def _update_idles(self, node, t0, t1): self._node_start_time[node] = (self._current_block_idx, t0) def _begin_new_circuit_block(self): - """Create a new timed circuit block completing the previous block. - - """ + """Create a new timed circuit block completing the previous block.""" self._current_block_idx += 1 self._current_block_measures = set() self._idle_after = {q: (0, 0) for q in self._dag.qubits + self._dag.clbits} def _current_block_measure_qargs(self): - return set(qarg for measure in self._current_block_measures for qarg in measure.qargs) + return set( + qarg for measure in self._current_block_measures for qarg in measure.qargs + ) diff --git a/test/transpiler/passes/scheduling/test_schedule.py b/test/transpiler/passes/scheduling/test_schedule.py index 58846626f..64f71d4a3 100644 --- a/test/transpiler/passes/scheduling/test_schedule.py +++ b/test/transpiler/passes/scheduling/test_schedule.py @@ -20,7 +20,9 @@ from qiskit.transpiler.exceptions import TranspilerError from qiskit_ibm_provider.transpiler.passes.scheduling.pad_delay import PadDelay -from qiskit_ibm_provider.transpiler.passes.scheduling.scheduler import DynamicCircuitScheduleAnalysis +from qiskit_ibm_provider.transpiler.passes.scheduling.scheduler import ( + DynamicCircuitScheduleAnalysis, +) class TestSchedulingAndPaddingPass(QiskitTestCase): From bf0807bb364499a4c482e74d2650a1b55704364e Mon Sep 17 00:00:00 2001 From: Thomas Alexander Date: Tue, 5 Jul 2022 23:33:45 -0300 Subject: [PATCH 41/70] Linting. --- .../passes/scheduling/base_padding.py | 6 +-- .../transpiler/passes/scheduling/scheduler.py | 54 +++++++++---------- .../passes/scheduling/test_schedule.py | 23 ++++---- 3 files changed, 42 insertions(+), 41 deletions(-) diff --git a/qiskit_ibm_provider/transpiler/passes/scheduling/base_padding.py b/qiskit_ibm_provider/transpiler/passes/scheduling/base_padding.py index 494c68187..160b2ceb7 100644 --- a/qiskit_ibm_provider/transpiler/passes/scheduling/base_padding.py +++ b/qiskit_ibm_provider/transpiler/passes/scheduling/base_padding.py @@ -185,7 +185,7 @@ def _visit_delay(self, node): def _visit_generic(self, node): """Visit a generic node to pad.""" - block_idx, t0 = self._node_start_time[node] + block_idx, t0 = self._node_start_time[node] # pylint: disable=invalid-name # Trigger the end of a block if block_idx > self._current_block_idx: @@ -194,12 +194,12 @@ def _visit_generic(self, node): # This block will not be padded as it is conditional. # See TODO below. - self._conditional_block = True if node.op.condition_bits else False + self._conditional_block = bool(node.op.condition_bits) # Now set the current block index. self._current_block_idx = block_idx - t1 = t0 + node.op.duration + t1 = t0 + node.op.duration # pylint: disable=invalid-name self._block_duration = max(self._block_duration, t1) for bit in node.qargs: diff --git a/qiskit_ibm_provider/transpiler/passes/scheduling/scheduler.py b/qiskit_ibm_provider/transpiler/passes/scheduling/scheduler.py index a593b3015..3844fe8db 100644 --- a/qiskit_ibm_provider/transpiler/passes/scheduling/scheduler.py +++ b/qiskit_ibm_provider/transpiler/passes/scheduling/scheduler.py @@ -24,19 +24,23 @@ class DynamicCircuitScheduleAnalysis(BaseScheduler): """Dynamic circuits scheduling analysis pass. This is a scheduler designed to work for the unique scheduling constraints of the dynamic circuits - backends due to the limitations imposed by hardware. This is expected to evolve overtime as the + backends due to the limitations imposed by hardware. This is expected to evolve over time as the dynamic circuit backends also change. - In its current form this is slow to Qiskit's ASAP scheduler in which instructions start asas early as possible. + In its current form this is slow to Qiskit's ASAP scheduler in which instructions + start as early as possible. The primary differences are that: - * Measurements currently trigger the end of a "quantum block". The period between the end of the block and the next is *indeterministic* - ie., we do not know when the next block will begin (as we could be evaluating a classical function of indeterministic length) and - therefore the next block starts at a *relative* t=0. + * Measurements currently trigger the end of a "quantum block". The period between the end + of the block and the next is *nondeterministic* + ie., we do not know when the next block will begin (as we could be evaluating a classical + function of nondeterministic length) and therefore the + next block starts at a *relative* t=0. * It is possible to apply gates during a measurement. - * Measurements on disjoint qubits happen simulataneously and are part of the same block. Measurements that are not lexigraphically - neighbors in the generated QASM3 will happen in separate blocks. + * Measurements on disjoint qubits happen simultaneously and are part of the same block. + Measurements that are not lexicographically neighbors in the generated QASM3 will + happen in separate blocks. """ @@ -64,8 +68,6 @@ def run(self, dag): """Run the ASAPSchedule pass on `dag`. Args: dag (DAGCircuit): DAG to schedule. - Returns: - DAGCircuit: A scheduled DAG. Raises: TranspilerError: if the circuit is not mapped on physical qubits. TranspilerError: if conditional bit is added to non-supported instruction. @@ -85,15 +87,12 @@ def _init_run(self, dag): if len(dag.qregs) != 1 or dag.qregs.get("q", None) is None: raise TranspilerError("ASAP schedule runs on physical circuits only") - self._conditional_latency = self.property_set.get("conditional_latency", 0) - self._clbit_write_latency = self.property_set.get("clbit_write_latency", 0) - - self._node_start_time = dict() + self._node_start_time = {} self._idle_after = {q: (0, 0) for q in dag.qubits + dag.clbits} self._current_block_measures = set() self._bit_indices = {q: index for index, q in enumerate(dag.qubits)} - def _get_node_duration(self, node): + def _get_duration(self, node): return super()._get_node_duration(node, self._bit_indices, self._dag) def _visit_node(self, node): @@ -130,7 +129,7 @@ def _visit_conditional_node(self, node): # Trigger the start of a conditional block self._begin_new_circuit_block() - op_duration = self._get_node_duration(node) + op_duration = self._get_duration(node) t0q = max(self._idle_after[q][1] for q in node.qargs) # conditional is bit tricky due to conditional_latency @@ -157,9 +156,9 @@ def _visit_conditional_node(self, node): self._idle_after[bit] = (self._current_block_idx, t1c) # It starts after register read access - t0 = max(t0q, t1c) + t0 = max(t0q, t1c) # pylint: disable=invalid-name - t1 = t0 + op_duration + t1 = t0 + op_duration # pylint: disable=invalid-name self._update_idles(node, t0, t1) # Terminate the conditional block @@ -175,13 +174,12 @@ def _visit_measure(self, node): Measurement currently triggers the end of a pulse block in IBM dynamic circuits hardware. This means that it is possible to schedule *up to* a measurement (and during its pulses) but the measurement will be followed by a period of indeterminism. - All measurements on disjoint qubits will be collected on the same qubits to be run simultaneously. - - """ + All measurements on disjoint qubits will be collected on the same qubits + to be run simultaneously.""" current_block_measure_qargs = self._current_block_measure_qargs() measure_qargs = set(node.qargs) - t0q = max(self._idle_after[q][1] for q in measure_qargs) + t0q = max(self._idle_after[q][1] for q in measure_qargs) # pylint: disable=invalid-name # If the measurement qubits overlap, we need to start a new scheduling block. if current_block_measure_qargs & measure_qargs: @@ -189,7 +187,7 @@ def _visit_measure(self, node): t0q = 0 # Otherwise we need to increment all measurements to start at the same time within the block. else: - t0q = max( + t0q = max( # pylint: disable=invalid-name itertools.chain( [t0q], ( @@ -207,12 +205,12 @@ def _visit_measure(self, node): self._current_block_measures.add(node) for measure in self._current_block_measures: - t0 = t0q + t0 = t0q # pylint: disable=invalid-name bit_indices = {bit: index for index, bit in enumerate(self._dag.qubits)} measure_duration = self.durations.get( Measure(), [bit_indices[qarg] for qarg in node.qargs], unit="dt" ) - t1 = t0 + measure_duration + t1 = t0 + measure_duration # pylint: disable=invalid-name self._update_idles(measure, t0, t1) def _visit_reset(self, node): @@ -230,14 +228,14 @@ def _visit_reset(self, node): def _visit_generic(self, node): """Visit a generic node such as a gate or barrier.""" - op_duration = self._get_node_duration(node) + op_duration = self._get_duration(node) # It happens to be directives such as barrier - t0 = max(self._idle_after[bit][1] for bit in node.qargs + node.cargs) - t1 = t0 + op_duration + t0 = max(self._idle_after[bit][1] for bit in node.qargs + node.cargs) # pylint: disable=invalid-name + t1 = t0 + op_duration # pylint: disable=invalid-name self._update_idles(node, t0, t1) - def _update_idles(self, node, t0, t1): + def _update_idles(self, node, t0, t1): # pylint: disable=invalid-name for bit in node.qargs: self._idle_after[bit] = (self._current_block_idx, t1) diff --git a/test/transpiler/passes/scheduling/test_schedule.py b/test/transpiler/passes/scheduling/test_schedule.py index 64f71d4a3..1e2b61ce7 100644 --- a/test/transpiler/passes/scheduling/test_schedule.py +++ b/test/transpiler/passes/scheduling/test_schedule.py @@ -24,6 +24,7 @@ DynamicCircuitScheduleAnalysis, ) +# pylint: disable=invalid-name class TestSchedulingAndPaddingPass(QiskitTestCase): """Tests the Scheduling passes""" @@ -70,7 +71,8 @@ def test_measure_after_measure(self): self.assertEqual(expected, scheduled) def test_measure_block_end(self): - """Tests that measures trigger the end of a scheduling block and that measurements are grouped by block.""" + """Tests that measures trigger the end of a scheduling block and + that measurements are grouped by block.""" qc = QuantumCircuit(3, 1) qc.x(0) qc.measure(0, 0) @@ -122,10 +124,11 @@ def test_c_if_on_different_qubits(self): self.assertEqual(expected, scheduled) def test_shorter_measure_after_measure(self): - """Test if schedules circuits with shorter measure after measure with a common clbit. + """Test if schedules circuits with shorter measure after measure + with a common clbit. - Note: For dynamic circuits support we currently group measurements to start at the same time which in turn trigger - the end of a block.""" + Note: For dynamic circuits support we currently group measurements + to start at the same time which in turn trigger the end of a block.""" qc = QuantumCircuit(3, 1) qc.measure(0, 0) qc.measure(1, 0) @@ -388,10 +391,10 @@ def test_no_pad_very_end_of_circuit(self): self.assertEqual(expected, scheduled) def test_reset_terminates_block(self): - """Test if reset operations terminate the block scheduled + """Test if reset operations terminate the block scheduled. - Note: For dynamic circuits support we currently group resets to start at the same time which in turn trigger - the end of a block.""" + Note: For dynamic circuits support we currently group resets + to start at the same time which in turn trigger the end of a block.""" qc = QuantumCircuit(3, 1) qc.x(0) qc.reset(0) @@ -429,10 +432,10 @@ def test_reset_terminates_block(self): self.assertEqual(expected, scheduled) def test_reset_merged_with_measure(self): - """Test if reset operations terminate the block scheduled + """Test if reset operations terminate the block scheduled. - Note: For dynamic circuits support we currently group resets to start at the same time which in turn trigger - the end of a block.""" + Note: For dynamic circuits support we currently group resets to start + at the same time which in turn trigger the end of a block.""" qc = QuantumCircuit(3, 1) qc.x(0) qc.reset(0) From 8b4a602d659dad3d01f5b6e7566029842ab05d3e Mon Sep 17 00:00:00 2001 From: Thomas Alexander Date: Tue, 5 Jul 2022 23:33:58 -0300 Subject: [PATCH 42/70] Blackify. --- .../passes/scheduling/base_padding.py | 4 ++-- .../transpiler/passes/scheduling/scheduler.py | 22 +++++++++++-------- .../passes/scheduling/test_schedule.py | 1 + 3 files changed, 16 insertions(+), 11 deletions(-) diff --git a/qiskit_ibm_provider/transpiler/passes/scheduling/base_padding.py b/qiskit_ibm_provider/transpiler/passes/scheduling/base_padding.py index 160b2ceb7..ac80b7fcf 100644 --- a/qiskit_ibm_provider/transpiler/passes/scheduling/base_padding.py +++ b/qiskit_ibm_provider/transpiler/passes/scheduling/base_padding.py @@ -185,7 +185,7 @@ def _visit_delay(self, node): def _visit_generic(self, node): """Visit a generic node to pad.""" - block_idx, t0 = self._node_start_time[node] # pylint: disable=invalid-name + block_idx, t0 = self._node_start_time[node] # pylint: disable=invalid-name # Trigger the end of a block if block_idx > self._current_block_idx: @@ -199,7 +199,7 @@ def _visit_generic(self, node): # Now set the current block index. self._current_block_idx = block_idx - t1 = t0 + node.op.duration # pylint: disable=invalid-name + t1 = t0 + node.op.duration # pylint: disable=invalid-name self._block_duration = max(self._block_duration, t1) for bit in node.qargs: diff --git a/qiskit_ibm_provider/transpiler/passes/scheduling/scheduler.py b/qiskit_ibm_provider/transpiler/passes/scheduling/scheduler.py index 3844fe8db..145676d0c 100644 --- a/qiskit_ibm_provider/transpiler/passes/scheduling/scheduler.py +++ b/qiskit_ibm_provider/transpiler/passes/scheduling/scheduler.py @@ -156,9 +156,9 @@ def _visit_conditional_node(self, node): self._idle_after[bit] = (self._current_block_idx, t1c) # It starts after register read access - t0 = max(t0q, t1c) # pylint: disable=invalid-name + t0 = max(t0q, t1c) # pylint: disable=invalid-name - t1 = t0 + op_duration # pylint: disable=invalid-name + t1 = t0 + op_duration # pylint: disable=invalid-name self._update_idles(node, t0, t1) # Terminate the conditional block @@ -179,7 +179,9 @@ def _visit_measure(self, node): current_block_measure_qargs = self._current_block_measure_qargs() measure_qargs = set(node.qargs) - t0q = max(self._idle_after[q][1] for q in measure_qargs) # pylint: disable=invalid-name + t0q = max( + self._idle_after[q][1] for q in measure_qargs + ) # pylint: disable=invalid-name # If the measurement qubits overlap, we need to start a new scheduling block. if current_block_measure_qargs & measure_qargs: @@ -187,7 +189,7 @@ def _visit_measure(self, node): t0q = 0 # Otherwise we need to increment all measurements to start at the same time within the block. else: - t0q = max( # pylint: disable=invalid-name + t0q = max( # pylint: disable=invalid-name itertools.chain( [t0q], ( @@ -205,12 +207,12 @@ def _visit_measure(self, node): self._current_block_measures.add(node) for measure in self._current_block_measures: - t0 = t0q # pylint: disable=invalid-name + t0 = t0q # pylint: disable=invalid-name bit_indices = {bit: index for index, bit in enumerate(self._dag.qubits)} measure_duration = self.durations.get( Measure(), [bit_indices[qarg] for qarg in node.qargs], unit="dt" ) - t1 = t0 + measure_duration # pylint: disable=invalid-name + t1 = t0 + measure_duration # pylint: disable=invalid-name self._update_idles(measure, t0, t1) def _visit_reset(self, node): @@ -231,11 +233,13 @@ def _visit_generic(self, node): op_duration = self._get_duration(node) # It happens to be directives such as barrier - t0 = max(self._idle_after[bit][1] for bit in node.qargs + node.cargs) # pylint: disable=invalid-name - t1 = t0 + op_duration # pylint: disable=invalid-name + t0 = max( + self._idle_after[bit][1] for bit in node.qargs + node.cargs + ) # pylint: disable=invalid-name + t1 = t0 + op_duration # pylint: disable=invalid-name self._update_idles(node, t0, t1) - def _update_idles(self, node, t0, t1): # pylint: disable=invalid-name + def _update_idles(self, node, t0, t1): # pylint: disable=invalid-name for bit in node.qargs: self._idle_after[bit] = (self._current_block_idx, t1) diff --git a/test/transpiler/passes/scheduling/test_schedule.py b/test/transpiler/passes/scheduling/test_schedule.py index 1e2b61ce7..d8ea6096a 100644 --- a/test/transpiler/passes/scheduling/test_schedule.py +++ b/test/transpiler/passes/scheduling/test_schedule.py @@ -26,6 +26,7 @@ # pylint: disable=invalid-name + class TestSchedulingAndPaddingPass(QiskitTestCase): """Tests the Scheduling passes""" From 5ac8693d6299a5e3de1231b1db7b82df391e8926 Mon Sep 17 00:00:00 2001 From: Thomas Alexander Date: Tue, 5 Jul 2022 23:36:03 -0300 Subject: [PATCH 43/70] Reach black and pylint fixed point. --- qiskit_ibm_provider/transpiler/passes/scheduling/scheduler.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/qiskit_ibm_provider/transpiler/passes/scheduling/scheduler.py b/qiskit_ibm_provider/transpiler/passes/scheduling/scheduler.py index 145676d0c..373ec639f 100644 --- a/qiskit_ibm_provider/transpiler/passes/scheduling/scheduler.py +++ b/qiskit_ibm_provider/transpiler/passes/scheduling/scheduler.py @@ -233,9 +233,9 @@ def _visit_generic(self, node): op_duration = self._get_duration(node) # It happens to be directives such as barrier - t0 = max( + t0 = max( # pylint: disable=invalid-name self._idle_after[bit][1] for bit in node.qargs + node.cargs - ) # pylint: disable=invalid-name + ) t1 = t0 + op_duration # pylint: disable=invalid-name self._update_idles(node, t0, t1) From 8f49cb1ed2cbd0fe22c613ad49969f1318cd5131 Mon Sep 17 00:00:00 2001 From: Thomas Alexander Date: Tue, 5 Jul 2022 23:52:27 -0300 Subject: [PATCH 44/70] Mypyify the code. --- .../passes/scheduling/base_padding.py | 24 ++++++------ .../transpiler/passes/scheduling/pad_delay.py | 2 +- .../transpiler/passes/scheduling/scheduler.py | 38 ++++++++++--------- 3 files changed, 34 insertions(+), 30 deletions(-) diff --git a/qiskit_ibm_provider/transpiler/passes/scheduling/base_padding.py b/qiskit_ibm_provider/transpiler/passes/scheduling/base_padding.py index ac80b7fcf..61de6f284 100644 --- a/qiskit_ibm_provider/transpiler/passes/scheduling/base_padding.py +++ b/qiskit_ibm_provider/transpiler/passes/scheduling/base_padding.py @@ -12,7 +12,7 @@ """Padding pass to fill timeslots for IBM (dynamic circuit) backends.""" -from typing import List, Optional, Union +from typing import Any, Dict, List, Optional, Union from qiskit.circuit import Qubit, Clbit, Instruction from qiskit.circuit.library import Barrier @@ -50,9 +50,9 @@ class BasePadding(TransformationPass): which may result in violation of hardware alignment constraints. """ - def __init__(self): + def __init__(self) -> None: self._node_start_time = None - self._idle_after = None + self._idle_after: Optional[Dict[Qubit, int]] = None self._dag = None self._block_duration = 0 self._current_block_idx = 0 @@ -60,7 +60,7 @@ def __init__(self): super().__init__() - def run(self, dag: DAGCircuit): + def run(self, dag: DAGCircuit) -> DAGCircuit: """Run the padding pass on ``dag``. Args: @@ -99,7 +99,7 @@ def run(self, dag: DAGCircuit): return self._dag - def _init_run(self, dag): + def _init_run(self, dag: DAGCircuit) -> None: """Setup for initial run.""" self._node_start_time = self.property_set["node_start_time"].copy() self._idle_after = {bit: 0 for bit in dag.qubits} @@ -125,7 +125,7 @@ def _init_run(self, dag): self._dag.calibrations = dag.calibrations self._dag.global_phase = dag.global_phase - def _pre_runhook(self, dag: DAGCircuit): + def _pre_runhook(self, dag: DAGCircuit) -> None: """Extra routine inserted before running the padding pass. Args: @@ -148,7 +148,7 @@ def _pad( t_end: int, next_node: DAGNode, prev_node: DAGNode, - ): + ) -> None: """Interleave instruction sequence in between two nodes. .. note:: @@ -176,14 +176,14 @@ def _pad( """ raise NotImplementedError - def _visit_delay(self, node): + def _visit_delay(self, node: DAGNode) -> None: """The padding class considers a delay instruction as idle time rather than instruction. Delay node is not added so that we can extract non-delay predecessors. """ pass - def _visit_generic(self, node): + def _visit_generic(self, node: DAGNode) -> None: """Visit a generic node to pad.""" block_idx, t0 = self._node_start_time[node] # pylint: disable=invalid-name @@ -220,7 +220,7 @@ def _visit_generic(self, node): self._apply_scheduled_op(block_idx, t0, node.op, node.qargs, node.cargs) - def _terminate_block(self, block_duration, block_idx): + def _terminate_block(self, block_duration: int, block_idx: int) -> None: """Terminate the end of a block scheduling region.""" # Update all other qubits as not idle so that delays are *not* # inserted. This is because we need the delays to be inserted in @@ -243,7 +243,7 @@ def _terminate_block(self, block_duration, block_idx): self._block_duration = 0 self._conditional_block = False - def _pad_until_block_end(self, block_duration, block_idx): + def _pad_until_block_end(self, block_duration: int, block_idx: int) -> None: # Add delays until the end of circuit. for bit in self._dag.qubits: if block_duration - self._idle_after[bit] > 0: @@ -265,7 +265,7 @@ def _apply_scheduled_op( oper: Instruction, qubits: Union[Qubit, List[Qubit]], clbits: Optional[Union[Clbit, List[Clbit]]] = None, - ): + ) -> None: """Add new operation to DAG with scheduled information. This is identical to apply_operation_back + updating the node_start_time propety. diff --git a/qiskit_ibm_provider/transpiler/passes/scheduling/pad_delay.py b/qiskit_ibm_provider/transpiler/passes/scheduling/pad_delay.py index a7e3824e5..f30cbed03 100644 --- a/qiskit_ibm_provider/transpiler/passes/scheduling/pad_delay.py +++ b/qiskit_ibm_provider/transpiler/passes/scheduling/pad_delay.py @@ -67,7 +67,7 @@ def _pad( t_end: int, next_node: DAGNode, prev_node: DAGNode, - ): + ) -> None: if not self.fill_very_end and isinstance(next_node, DAGOutNode): return diff --git a/qiskit_ibm_provider/transpiler/passes/scheduling/scheduler.py b/qiskit_ibm_provider/transpiler/passes/scheduling/scheduler.py index 373ec639f..bcdaf673e 100644 --- a/qiskit_ibm_provider/transpiler/passes/scheduling/scheduler.py +++ b/qiskit_ibm_provider/transpiler/passes/scheduling/scheduler.py @@ -12,10 +12,12 @@ """Scheduler for dynamic circuit backends.""" +from typing import Dict, Optional, Union, Set, Tuple import itertools import qiskit -from qiskit.circuit import Measure, Reset +from qiskit.circuit import Clbit, Measure, Qubit, Reset +from qiskit.dagcircuit import DAGCircuit, DAGNode from qiskit.transpiler.exceptions import TranspilerError from qiskit.transpiler.passes.scheduling.scheduling.base_scheduler import BaseScheduler @@ -46,7 +48,7 @@ class DynamicCircuitScheduleAnalysis(BaseScheduler): def __init__( self, durations: qiskit.transpiler.instruction_durations.InstructionDurations - ): + ) -> None: """Scheduler for dynamic circuit backends. Args: @@ -57,14 +59,14 @@ def __init__( self._current_block_idx = 0 - self._node_start_time = None - self._idle_after = None - self._current_block_measures = None - self._bit_indices = None + self._node_start_time: Optional[Dict[DAGNode, Tuple[int, int]]] = None + self._idle_after: Optional[Dict[Union[Qubit, Clbit], Tuple[int, int]]] = None + self._current_block_measures: Set[DAGNode] = set() + self._bit_indices: Optional[Dict[Qubit, int]] = None super().__init__(durations) - def run(self, dag): + def run(self, dag: DAGCircuit) -> None: """Run the ASAPSchedule pass on `dag`. Args: dag (DAGCircuit): DAG to schedule. @@ -79,7 +81,7 @@ def run(self, dag): self.property_set["node_start_time"] = self._node_start_time - def _init_run(self, dag): + def _init_run(self, dag: DAGCircuit) -> None: """Setup for initial run.""" self._dag = dag @@ -92,10 +94,10 @@ def _init_run(self, dag): self._current_block_measures = set() self._bit_indices = {q: index for index, q in enumerate(dag.qubits)} - def _get_duration(self, node): + def _get_duration(self, node: DAGNode) -> int: return super()._get_node_duration(node, self._bit_indices, self._dag) - def _visit_node(self, node): + def _visit_node(self, node: DAGNode) -> None: # compute t0, t1: instruction interval, note that # t0: start time of instruction # t1: end time of instruction @@ -114,7 +116,7 @@ def _visit_node(self, node): else: self._visit_generic(node) - def _visit_conditional_node(self, node): + def _visit_conditional_node(self, node: DAGNode) -> None: """Handling case of a conditional execution. Conditional execution durations are currently non-deterministic. as we do not know @@ -168,7 +170,7 @@ def _visit_conditional_node(self, node): # Fall through to generic case if not conditional self._visit_generic(node) - def _visit_measure(self, node): + def _visit_measure(self, node: DAGNode) -> None: """Visit a measurement node. Measurement currently triggers the end of a pulse block in IBM dynamic circuits hardware. @@ -215,7 +217,7 @@ def _visit_measure(self, node): t1 = t0 + measure_duration # pylint: disable=invalid-name self._update_idles(measure, t0, t1) - def _visit_reset(self, node): + def _visit_reset(self, node: DAGNode) -> None: """Visit a reset node. Reset currently triggers the end of a pulse block in IBM dynamic circuits hardware @@ -228,7 +230,7 @@ def _visit_reset(self, node): """ self._visit_measure(node) - def _visit_generic(self, node): + def _visit_generic(self, node: DAGNode) -> None: """Visit a generic node such as a gate or barrier.""" op_duration = self._get_duration(node) @@ -239,7 +241,9 @@ def _visit_generic(self, node): t1 = t0 + op_duration # pylint: disable=invalid-name self._update_idles(node, t0, t1) - def _update_idles(self, node, t0, t1): # pylint: disable=invalid-name + def _update_idles( + self, node: DAGNode, t0: int, t1: int # pylint: disable=invalid-name + ) -> None: for bit in node.qargs: self._idle_after[bit] = (self._current_block_idx, t1) @@ -248,13 +252,13 @@ def _update_idles(self, node, t0, t1): # pylint: disable=invalid-name self._node_start_time[node] = (self._current_block_idx, t0) - def _begin_new_circuit_block(self): + def _begin_new_circuit_block(self) -> None: """Create a new timed circuit block completing the previous block.""" self._current_block_idx += 1 self._current_block_measures = set() self._idle_after = {q: (0, 0) for q in self._dag.qubits + self._dag.clbits} - def _current_block_measure_qargs(self): + def _current_block_measure_qargs(self) -> Set[Qubit]: return set( qarg for measure in self._current_block_measures for qarg in measure.qargs ) From 209cc8e834989d6a941960ad9b56c5ec1cede1bf Mon Sep 17 00:00:00 2001 From: Thomas Alexander Date: Wed, 6 Jul 2022 09:08:02 -0300 Subject: [PATCH 45/70] Rename BasePadding to BlockBasePadder. --- .../transpiler/passes/scheduling/__init__.py | 2 +- .../scheduling/{base_padding.py => block_base_padder.py} | 2 +- .../transpiler/passes/scheduling/pad_delay.py | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) rename qiskit_ibm_provider/transpiler/passes/scheduling/{base_padding.py => block_base_padder.py} (99%) diff --git a/qiskit_ibm_provider/transpiler/passes/scheduling/__init__.py b/qiskit_ibm_provider/transpiler/passes/scheduling/__init__.py index a995525ba..f41c0079c 100644 --- a/qiskit_ibm_provider/transpiler/passes/scheduling/__init__.py +++ b/qiskit_ibm_provider/transpiler/passes/scheduling/__init__.py @@ -67,7 +67,7 @@ .. autosummary:: :toctree: ../stubs/ - BasePadding + BlockBasePadder DynamicCircuitScheduleAnalysis PadDelay diff --git a/qiskit_ibm_provider/transpiler/passes/scheduling/base_padding.py b/qiskit_ibm_provider/transpiler/passes/scheduling/block_base_padder.py similarity index 99% rename from qiskit_ibm_provider/transpiler/passes/scheduling/base_padding.py rename to qiskit_ibm_provider/transpiler/passes/scheduling/block_base_padder.py index 61de6f284..1053650da 100644 --- a/qiskit_ibm_provider/transpiler/passes/scheduling/base_padding.py +++ b/qiskit_ibm_provider/transpiler/passes/scheduling/block_base_padder.py @@ -22,7 +22,7 @@ from qiskit.transpiler.exceptions import TranspilerError -class BasePadding(TransformationPass): +class BlockBasePadder(TransformationPass): """The base class of padding pass. This pass requires one of scheduling passes to be executed before itself. diff --git a/qiskit_ibm_provider/transpiler/passes/scheduling/pad_delay.py b/qiskit_ibm_provider/transpiler/passes/scheduling/pad_delay.py index f30cbed03..014b90f0d 100644 --- a/qiskit_ibm_provider/transpiler/passes/scheduling/pad_delay.py +++ b/qiskit_ibm_provider/transpiler/passes/scheduling/pad_delay.py @@ -16,10 +16,10 @@ from qiskit.circuit.delay import Delay from qiskit.dagcircuit import DAGNode, DAGOutNode -from .base_padding import BasePadding +from .block_base_padder import BlockBasePadder -class PadDelay(BasePadding): +class PadDelay(BlockBasePadder): """Padding idle time with Delay instructions. Consecutive delays will be merged in the output of this pass. @@ -47,7 +47,7 @@ class PadDelay(BasePadding): between ``Delay`` of 100dt (``q_0``) and ``XGate`` of 160 dt (``q_1``) is absorbed in the delay instruction on the ``q_0`` wire, i.e. in total 160 dt. - See :class:`BasePadding` pass for details. + See :class:`BlockBasePadder` pass for details. """ def __init__(self, fill_very_end: bool = True): From 89e7fba9702d6d786cebc2193ac5bddd9e4823e3 Mon Sep 17 00:00:00 2001 From: Thomas Alexander Date: Wed, 6 Jul 2022 09:31:12 -0300 Subject: [PATCH 46/70] Fix broken tests by Qiskit upgrade. --- .../transpiler/passes/scheduling/block_base_padder.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/qiskit_ibm_provider/transpiler/passes/scheduling/block_base_padder.py b/qiskit_ibm_provider/transpiler/passes/scheduling/block_base_padder.py index 1053650da..5f4140819 100644 --- a/qiskit_ibm_provider/transpiler/passes/scheduling/block_base_padder.py +++ b/qiskit_ibm_provider/transpiler/passes/scheduling/block_base_padder.py @@ -116,7 +116,7 @@ def _init_run(self, dag: DAGCircuit) -> None: # Update start time dictionary for the new_dag. # This information may be used for further scheduling tasks, - # but this is immediately invalidated becasue node id is updated in the new_dag. + # but this is immediately invalidated because most node ids are updated in the new_dag. self.property_set["node_start_time"].clear() self._dag.name = dag.name @@ -234,9 +234,7 @@ def _terminate_block(self, block_duration: int, block_idx: int) -> None: # Terminate with a barrier to be clear timing is non-deterministic # across the barrier. - self._dag.apply_operation_back( - Barrier(self._dag.num_qubits()), self._dag.qubits, [] - ) + self._apply_scheduled_op(block_idx, block_duration, Barrier(self._dag.num_qubits()), self._dag.qubits, []) # Reset idles for the new block. self._idle_after = {bit: 0 for bit in self._dag.qubits} From ee823a528e476710a3edde8c3229b38a73387173 Mon Sep 17 00:00:00 2001 From: Thomas Alexander Date: Wed, 6 Jul 2022 09:35:33 -0300 Subject: [PATCH 47/70] Move tests into unit. --- test/{ => unit}/transpiler/__init__.py | 0 test/{ => unit}/transpiler/passes/__init__.py | 0 test/{ => unit}/transpiler/passes/scheduling/__init__.py | 0 test/{ => unit}/transpiler/passes/scheduling/test_schedule.py | 0 4 files changed, 0 insertions(+), 0 deletions(-) rename test/{ => unit}/transpiler/__init__.py (100%) rename test/{ => unit}/transpiler/passes/__init__.py (100%) rename test/{ => unit}/transpiler/passes/scheduling/__init__.py (100%) rename test/{ => unit}/transpiler/passes/scheduling/test_schedule.py (100%) diff --git a/test/transpiler/__init__.py b/test/unit/transpiler/__init__.py similarity index 100% rename from test/transpiler/__init__.py rename to test/unit/transpiler/__init__.py diff --git a/test/transpiler/passes/__init__.py b/test/unit/transpiler/passes/__init__.py similarity index 100% rename from test/transpiler/passes/__init__.py rename to test/unit/transpiler/passes/__init__.py diff --git a/test/transpiler/passes/scheduling/__init__.py b/test/unit/transpiler/passes/scheduling/__init__.py similarity index 100% rename from test/transpiler/passes/scheduling/__init__.py rename to test/unit/transpiler/passes/scheduling/__init__.py diff --git a/test/transpiler/passes/scheduling/test_schedule.py b/test/unit/transpiler/passes/scheduling/test_schedule.py similarity index 100% rename from test/transpiler/passes/scheduling/test_schedule.py rename to test/unit/transpiler/passes/scheduling/test_schedule.py From 4c33ca717db2d49760e49f122814ed15db20f721 Mon Sep 17 00:00:00 2001 From: Thomas Alexander Date: Wed, 6 Jul 2022 09:48:56 -0300 Subject: [PATCH 48/70] linting. --- docs/apidocs/ibm-provider.rst | 1 + docs/apidocs/ibm_transpiler.rst | 6 ++++++ qiskit_ibm_provider/transpiler/__init__.py | 9 +++++++++ .../transpiler/passes/scheduling/block_base_padder.py | 8 +++++++- test/integration/test_ibm_qasm_simulator.py | 4 ++-- 5 files changed, 25 insertions(+), 3 deletions(-) create mode 100644 docs/apidocs/ibm_transpiler.rst diff --git a/docs/apidocs/ibm-provider.rst b/docs/apidocs/ibm-provider.rst index a98f08d10..7b962792b 100644 --- a/docs/apidocs/ibm-provider.rst +++ b/docs/apidocs/ibm-provider.rst @@ -10,3 +10,4 @@ qiskit-ibm-provider API Reference ibm_visualization ibm_jupyter ibm_utils + ibm_transpiler diff --git a/docs/apidocs/ibm_transpiler.rst b/docs/apidocs/ibm_transpiler.rst new file mode 100644 index 000000000..db99eb170 --- /dev/null +++ b/docs/apidocs/ibm_transpiler.rst @@ -0,0 +1,6 @@ +.. _qiskit_ibm_provider-utils: + +.. automodule:: qiskit_ibm_provider.transpiler + :no-members: + :no-inherited-members: + :no-special-members: diff --git a/qiskit_ibm_provider/transpiler/__init__.py b/qiskit_ibm_provider/transpiler/__init__.py index a2ae05a42..e43a109f1 100644 --- a/qiskit_ibm_provider/transpiler/__init__.py +++ b/qiskit_ibm_provider/transpiler/__init__.py @@ -19,4 +19,13 @@ next-generation backends that support advanced "dynamic circuit" capabilities. Ie., circuits with support for classical compute and control-flow/feedback based off of measurement results. + +Transpiler Passes +================== + +.. autosummary:: + :toctree: ../stubs/ + + Scheduling + """ diff --git a/qiskit_ibm_provider/transpiler/passes/scheduling/block_base_padder.py b/qiskit_ibm_provider/transpiler/passes/scheduling/block_base_padder.py index 5f4140819..a60e3f140 100644 --- a/qiskit_ibm_provider/transpiler/passes/scheduling/block_base_padder.py +++ b/qiskit_ibm_provider/transpiler/passes/scheduling/block_base_padder.py @@ -234,7 +234,13 @@ def _terminate_block(self, block_duration: int, block_idx: int) -> None: # Terminate with a barrier to be clear timing is non-deterministic # across the barrier. - self._apply_scheduled_op(block_idx, block_duration, Barrier(self._dag.num_qubits()), self._dag.qubits, []) + self._apply_scheduled_op( + block_idx, + block_duration, + Barrier(self._dag.num_qubits()), + self._dag.qubits, + [], + ) # Reset idles for the new block. self._idle_after = {bit: 0 for bit in self._dag.qubits} diff --git a/test/integration/test_ibm_qasm_simulator.py b/test/integration/test_ibm_qasm_simulator.py index 469a9cc07..be4b29fcf 100644 --- a/test/integration/test_ibm_qasm_simulator.py +++ b/test/integration/test_ibm_qasm_simulator.py @@ -17,9 +17,9 @@ from qiskit import ClassicalRegister, QuantumCircuit, QuantumRegister from qiskit.compiler import transpile -from qiskit.providers.aer.noise import ( +from qiskit.providers.aer.noise import ( # pylint: disable=import-error,no-name-in-module NoiseModel, -) # pylint: disable=import-error,no-name-in-module +) from qiskit.test.reference_circuits import ReferenceCircuits from qiskit_ibm_provider import IBMBackend From aea54737633ef282f7f5c4aa758908552fba1725 Mon Sep 17 00:00:00 2001 From: Thomas Alexander Date: Wed, 6 Jul 2022 10:36:45 -0300 Subject: [PATCH 49/70] Update documentation. --- qiskit_ibm_provider/transpiler/__init__.py | 2 +- .../transpiler/passes/__init__.py | 6 +++--- .../transpiler/passes/scheduling/__init__.py | 18 ++++++++++++------ 3 files changed, 16 insertions(+), 10 deletions(-) diff --git a/qiskit_ibm_provider/transpiler/__init__.py b/qiskit_ibm_provider/transpiler/__init__.py index e43a109f1..d6e62daa4 100644 --- a/qiskit_ibm_provider/transpiler/__init__.py +++ b/qiskit_ibm_provider/transpiler/__init__.py @@ -26,6 +26,6 @@ .. autosummary:: :toctree: ../stubs/ - Scheduling + passes """ diff --git a/qiskit_ibm_provider/transpiler/passes/__init__.py b/qiskit_ibm_provider/transpiler/passes/__init__.py index 680a92d87..0bae4b142 100644 --- a/qiskit_ibm_provider/transpiler/passes/__init__.py +++ b/qiskit_ibm_provider/transpiler/passes/__init__.py @@ -11,16 +11,16 @@ # that they have been altered from the originals. """ -=================================================== +================================================================ Transpiler Passes (:mod:`qiskit_ibm_provider.transpiler.passes`) -=================================================== +================================================================ .. currentmodule:: qiskit_ibm_provider.transpiler.passes .. autosummary:: :toctree: ../stubs/ - Dynamic circuit scheduling + scheduling """ diff --git a/qiskit_ibm_provider/transpiler/passes/scheduling/__init__.py b/qiskit_ibm_provider/transpiler/passes/scheduling/__init__.py index f41c0079c..cae47ead2 100644 --- a/qiskit_ibm_provider/transpiler/passes/scheduling/__init__.py +++ b/qiskit_ibm_provider/transpiler/passes/scheduling/__init__.py @@ -11,9 +11,9 @@ # that they have been altered from the originals. """ -============================================================== +==================================================================== Scheduling (:mod:`qiskit_ibm_provider.transpiler.passes.scheduling`) -============================================================== +==================================================================== .. currentmodule:: qiskit_ibm_provider.transpiler.passes.scheduling @@ -29,13 +29,16 @@ .. jupyter-execute:: + from qiskit import transpile + from qiskit.circuit import ClassicalRegister, QuantumCircuit, QuantumRegister from qiskit.transpiler.instruction_durations import InstructionDurations + from qiskit.transpiler.passmanager import PassManager - from qiskit_ibm_provider.transpiler.scheduling import DynamicCircuitScheduleAnalysis, PadDelay - from qiskit_ibm_provider.test.ibm_provider_mock import mock_get_backend + from qiskit_ibm_provider.transpiler.passes.scheduling import DynamicCircuitScheduleAnalysis, PadDelay + from qiskit.providers.fake_provider.backends.jakarta.fake_jakarta import FakeJakarta - backend = mock_get_backend('FakePerth') + backend = FakeJakarta() durations = InstructionDurations.from_backend(backend) pm = PassManager([DynamicCircuitScheduleAnalysis(durations), PadDelay()]) @@ -57,9 +60,11 @@ teleport.x(qr[2]).c_if(crx, 1) teleport.measure(qr[2], result) + teleport = transpile(teleport, backend) + scheduled_teleport = pm.run(teleport) - teleport.draw(output="mpl") + scheduled_teleport.draw(output="mpl") Scheduling & Dynamical Decoupling @@ -75,5 +80,6 @@ """ +from .block_base_padder import BlockBasePadder from .pad_delay import PadDelay from .scheduler import DynamicCircuitScheduleAnalysis From ea37d66c0a3b9f065e79330846be0ee727721983 Mon Sep 17 00:00:00 2001 From: Thomas Alexander Date: Wed, 6 Jul 2022 10:58:21 -0300 Subject: [PATCH 50/70] Fix requirements dev for matplotlib drawer./ --- requirements-dev.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index d417921ff..0febbfdad 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -7,7 +7,7 @@ sphinx-rtd-theme>=0.4.0 sphinx-tabs>=1.1.11 sphinx-automodapi sphinx-autodoc-typehints -matplotlib>=2.1 +matplotlib>=3.3 jupyter plotly>=4.4 ipyvuetify>=1.1 @@ -26,3 +26,4 @@ black==22.3.0 coverage>=6.3 scikit-learn>=0.20.0 ddt>=1.2.0,!=1.4.0,!=1.4.3 +pylatexenc>=1.4 From 69cbab4228289d15ff7c3523eda836df556b850b Mon Sep 17 00:00:00 2001 From: Thomas Alexander Date: Wed, 6 Jul 2022 15:36:59 -0300 Subject: [PATCH 51/70] Chain qargs and cargs. --- .../transpiler/passes/scheduling/scheduler.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/qiskit_ibm_provider/transpiler/passes/scheduling/scheduler.py b/qiskit_ibm_provider/transpiler/passes/scheduling/scheduler.py index bcdaf673e..147757ab7 100644 --- a/qiskit_ibm_provider/transpiler/passes/scheduling/scheduler.py +++ b/qiskit_ibm_provider/transpiler/passes/scheduling/scheduler.py @@ -244,10 +244,7 @@ def _visit_generic(self, node: DAGNode) -> None: def _update_idles( self, node: DAGNode, t0: int, t1: int # pylint: disable=invalid-name ) -> None: - for bit in node.qargs: - self._idle_after[bit] = (self._current_block_idx, t1) - - for bit in node.cargs: + for bit in itertools.chain(node.qargs, node.cargs): self._idle_after[bit] = (self._current_block_idx, t1) self._node_start_time[node] = (self._current_block_idx, t0) From b1943a62d8c6085fb52884d5704e634eeec15e65 Mon Sep 17 00:00:00 2001 From: Thomas Alexander Date: Wed, 6 Jul 2022 15:38:19 -0300 Subject: [PATCH 52/70] Update pulse block comment. --- qiskit_ibm_provider/transpiler/passes/scheduling/scheduler.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/qiskit_ibm_provider/transpiler/passes/scheduling/scheduler.py b/qiskit_ibm_provider/transpiler/passes/scheduling/scheduler.py index 147757ab7..83f7fa0d1 100644 --- a/qiskit_ibm_provider/transpiler/passes/scheduling/scheduler.py +++ b/qiskit_ibm_provider/transpiler/passes/scheduling/scheduler.py @@ -173,7 +173,8 @@ def _visit_conditional_node(self, node: DAGNode) -> None: def _visit_measure(self, node: DAGNode) -> None: """Visit a measurement node. - Measurement currently triggers the end of a pulse block in IBM dynamic circuits hardware. + Measurement currently triggers the end of a deterministically scheduled block + of instructions in IBM dynamic circuits hardware. This means that it is possible to schedule *up to* a measurement (and during its pulses) but the measurement will be followed by a period of indeterminism. All measurements on disjoint qubits will be collected on the same qubits From 2b971b3fff8cb468dfed6c376ef0b40cf8d01f11 Mon Sep 17 00:00:00 2001 From: Thomas Alexander Date: Wed, 6 Jul 2022 15:42:41 -0300 Subject: [PATCH 53/70] Update measurement comment. --- .../transpiler/passes/scheduling/scheduler.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/qiskit_ibm_provider/transpiler/passes/scheduling/scheduler.py b/qiskit_ibm_provider/transpiler/passes/scheduling/scheduler.py index 83f7fa0d1..54edb80cb 100644 --- a/qiskit_ibm_provider/transpiler/passes/scheduling/scheduler.py +++ b/qiskit_ibm_provider/transpiler/passes/scheduling/scheduler.py @@ -177,8 +177,12 @@ def _visit_measure(self, node: DAGNode) -> None: of instructions in IBM dynamic circuits hardware. This means that it is possible to schedule *up to* a measurement (and during its pulses) but the measurement will be followed by a period of indeterminism. - All measurements on disjoint qubits will be collected on the same qubits - to be run simultaneously.""" + All measurements on disjoint qubits that lexicographically follow another + measurement will be collected and performed in parallel. A measurement on a qubit + intersecting with the set of qubits to be measured in parallel will trigger the + end of a scheduling block with said measurement occurring in a following block + which begins another grouping sequence. This behavior will change in future + backend software updates.""" current_block_measure_qargs = self._current_block_measure_qargs() measure_qargs = set(node.qargs) From 44a1905937a0c0e5ce51c7b4ec2099d3ce1f77af Mon Sep 17 00:00:00 2001 From: Thomas Alexander Date: Wed, 6 Jul 2022 15:48:13 -0300 Subject: [PATCH 54/70] Add t0 comment. --- .../transpiler/passes/scheduling/block_base_padder.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/qiskit_ibm_provider/transpiler/passes/scheduling/block_base_padder.py b/qiskit_ibm_provider/transpiler/passes/scheduling/block_base_padder.py index a60e3f140..2e4ff6267 100644 --- a/qiskit_ibm_provider/transpiler/passes/scheduling/block_base_padder.py +++ b/qiskit_ibm_provider/transpiler/passes/scheduling/block_base_padder.py @@ -185,6 +185,8 @@ def _visit_delay(self, node: DAGNode) -> None: def _visit_generic(self, node: DAGNode) -> None: """Visit a generic node to pad.""" + # Note: t0 is the relative time with respect to the current block specified + # by block_idx. block_idx, t0 = self._node_start_time[node] # pylint: disable=invalid-name # Trigger the end of a block From 2d995df6ce8268dc0bfd6526a4fe5749e89fdc33 Mon Sep 17 00:00:00 2001 From: Thomas Alexander Date: Wed, 6 Jul 2022 16:02:29 -0300 Subject: [PATCH 55/70] Move scheduler unit tests. --- .../passes/scheduling/{test_schedule.py => test_scheduler.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename test/unit/transpiler/passes/scheduling/{test_schedule.py => test_scheduler.py} (100%) diff --git a/test/unit/transpiler/passes/scheduling/test_schedule.py b/test/unit/transpiler/passes/scheduling/test_scheduler.py similarity index 100% rename from test/unit/transpiler/passes/scheduling/test_schedule.py rename to test/unit/transpiler/passes/scheduling/test_scheduler.py From 931fe00df47e63b49a0a2da2edf8fe6d334298d5 Mon Sep 17 00:00:00 2001 From: Thomas Alexander Date: Wed, 6 Jul 2022 22:51:28 -0300 Subject: [PATCH 56/70] Make scheduler idempotent. --- .../passes/scheduling/block_base_padder.py | 44 +++++++++++++------ .../passes/scheduling/test_scheduler.py | 28 ++++++++++++ 2 files changed, 59 insertions(+), 13 deletions(-) diff --git a/qiskit_ibm_provider/transpiler/passes/scheduling/block_base_padder.py b/qiskit_ibm_provider/transpiler/passes/scheduling/block_base_padder.py index 2e4ff6267..b4dac3c15 100644 --- a/qiskit_ibm_provider/transpiler/passes/scheduling/block_base_padder.py +++ b/qiskit_ibm_provider/transpiler/passes/scheduling/block_base_padder.py @@ -54,6 +54,7 @@ def __init__(self) -> None: self._node_start_time = None self._idle_after: Optional[Dict[Qubit, int]] = None self._dag = None + self._prev_node: Optional[DAGNode] = None self._block_duration = 0 self._current_block_idx = 0 self._conditional_block = False @@ -94,8 +95,10 @@ def run(self, dag: DAGCircuit) -> DAGCircuit: "Schedule the circuit again if you transformed it." ) + self._prev_node = node + # terminate final block - self._terminate_block(self._block_duration, self._current_block_idx) + self._terminate_block(self._block_duration, self._current_block_idx, None) return self._dag @@ -125,6 +128,8 @@ def _init_run(self, dag: DAGCircuit) -> None: self._dag.calibrations = dag.calibrations self._dag.global_phase = dag.global_phase + self._prev_node = None + def _pre_runhook(self, dag: DAGCircuit) -> None: """Extra routine inserted before running the padding pass. @@ -191,8 +196,7 @@ def _visit_generic(self, node: DAGNode) -> None: # Trigger the end of a block if block_idx > self._current_block_idx: - - self._terminate_block(self._block_duration, self._current_block_idx) + self._terminate_block(self._block_duration, self._current_block_idx, node) # This block will not be padded as it is conditional. # See TODO below. @@ -222,7 +226,9 @@ def _visit_generic(self, node: DAGNode) -> None: self._apply_scheduled_op(block_idx, t0, node.op, node.qargs, node.cargs) - def _terminate_block(self, block_duration: int, block_idx: int) -> None: + def _terminate_block( + self, block_duration: int, block_idx: int, node: Optional[DAGNode] + ) -> None: """Terminate the end of a block scheduling region.""" # Update all other qubits as not idle so that delays are *not* # inserted. This is because we need the delays to be inserted in @@ -234,15 +240,27 @@ def _terminate_block(self, block_duration: int, block_idx: int) -> None: if not self._conditional_block: self._pad_until_block_end(block_duration, block_idx) - # Terminate with a barrier to be clear timing is non-deterministic - # across the barrier. - self._apply_scheduled_op( - block_idx, - block_duration, - Barrier(self._dag.num_qubits()), - self._dag.qubits, - [], - ) + def _is_terminating_barrier(node: Optional[DAGNode]) -> bool: + return ( + node + and isinstance(node.op, Barrier) + and len(node.qargs) == self._dag.num_qubits() + ) + + # Only add a barrier to the end if a viable barrier is not already present on all qubits + is_terminating_barrier = _is_terminating_barrier( + self._prev_node + ) or _is_terminating_barrier(node) + if not is_terminating_barrier: + # Terminate with a barrier to be clear timing is non-deterministic + # across the barrier. + self._apply_scheduled_op( + block_idx, + block_duration, + Barrier(self._dag.num_qubits()), + self._dag.qubits, + [], + ) # Reset idles for the new block. self._idle_after = {bit: 0 for bit in self._dag.qubits} diff --git a/test/unit/transpiler/passes/scheduling/test_scheduler.py b/test/unit/transpiler/passes/scheduling/test_scheduler.py index d8ea6096a..4338f0556 100644 --- a/test/unit/transpiler/passes/scheduling/test_scheduler.py +++ b/test/unit/transpiler/passes/scheduling/test_scheduler.py @@ -472,3 +472,31 @@ def test_reset_merged_with_measure(self): expected.barrier() self.assertEqual(expected, scheduled) + + def test_scheduling_is_idempotent(self): + """Test that padding can be applied back to back without changing the circuit.""" + qc = QuantumCircuit(1, 1) + qc.measure(0, 0) + qc.x(0).c_if(0, 1) + qc.measure(0, 0) + qc.x(0).c_if(0, 1) + qc.measure(0, 0) + qc.x(0).c_if(0, 1) + + durations = InstructionDurations([("x", None, 100), ("measure", None, 1000)]) + + scheduled0 = PassManager( + [ + DynamicCircuitScheduleAnalysis(durations), + PadDelay(), + ] + ).run(qc) + + scheduled1 = PassManager( + [ + DynamicCircuitScheduleAnalysis(durations), + PadDelay(), + ] + ).run(scheduled0) + + self.assertEqual(scheduled0, scheduled1) From 783d6081e3d9df282383d016dfe3b8c0e45b99b7 Mon Sep 17 00:00:00 2001 From: Thomas Alexander Date: Wed, 6 Jul 2022 22:59:25 -0300 Subject: [PATCH 57/70] Operations on qubits already measured trigger block termination. --- .../transpiler/passes/scheduling/scheduler.py | 13 +++++++---- .../passes/scheduling/test_scheduler.py | 22 +++++++++++++++++++ 2 files changed, 31 insertions(+), 4 deletions(-) diff --git a/qiskit_ibm_provider/transpiler/passes/scheduling/scheduler.py b/qiskit_ibm_provider/transpiler/passes/scheduling/scheduler.py index 54edb80cb..2c08ad2bf 100644 --- a/qiskit_ibm_provider/transpiler/passes/scheduling/scheduler.py +++ b/qiskit_ibm_provider/transpiler/passes/scheduling/scheduler.py @@ -239,10 +239,15 @@ def _visit_generic(self, node: DAGNode) -> None: """Visit a generic node such as a gate or barrier.""" op_duration = self._get_duration(node) - # It happens to be directives such as barrier - t0 = max( # pylint: disable=invalid-name - self._idle_after[bit][1] for bit in node.qargs + node.cargs - ) + # If the measurement qubits overlap, we need to start a new scheduling block. + if self._current_block_measure_qargs() & set(node.qargs): + self._begin_new_circuit_block() + t0 = 0 # pylint: disable=invalid-name + else: + t0 = max( # pylint: disable=invalid-name + self._idle_after[bit][1] for bit in node.qargs + node.cargs + ) + t1 = t0 + op_duration # pylint: disable=invalid-name self._update_idles(node, t0, t1) diff --git a/test/unit/transpiler/passes/scheduling/test_scheduler.py b/test/unit/transpiler/passes/scheduling/test_scheduler.py index 4338f0556..ce35e73bd 100644 --- a/test/unit/transpiler/passes/scheduling/test_scheduler.py +++ b/test/unit/transpiler/passes/scheduling/test_scheduler.py @@ -500,3 +500,25 @@ def test_scheduling_is_idempotent(self): ).run(scheduled0) self.assertEqual(scheduled0, scheduled1) + + def test_gate_on_measured_qubit(self): + """Test that a gate on a previously measured qubit triggers the end of the block""" + qc = QuantumCircuit(2, 1) + qc.measure(0, 0) + qc.x(0) + qc.x(1) + + durations = InstructionDurations([("x", None, 200), ("measure", None, 1000)]) + pm = PassManager([DynamicCircuitScheduleAnalysis(durations), PadDelay()]) + scheduled = pm.run(qc) + + expected = QuantumCircuit(2, 1) + expected.x(1) + expected.delay(800, 1) + expected.measure(0, 0) + expected.barrier() + expected.x(0) + expected.delay(200, 1) + expected.barrier() + + self.assertEqual(expected, scheduled) From b7772497dd9059e83290bb4dacd21f594d2909cb Mon Sep 17 00:00:00 2001 From: Thomas Alexander Date: Thu, 7 Jul 2022 00:08:11 -0300 Subject: [PATCH 58/70] Black. --- qiskit_ibm_provider/transpiler/passes/scheduling/scheduler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiskit_ibm_provider/transpiler/passes/scheduling/scheduler.py b/qiskit_ibm_provider/transpiler/passes/scheduling/scheduler.py index 2c08ad2bf..5b40862bf 100644 --- a/qiskit_ibm_provider/transpiler/passes/scheduling/scheduler.py +++ b/qiskit_ibm_provider/transpiler/passes/scheduling/scheduler.py @@ -242,7 +242,7 @@ def _visit_generic(self, node: DAGNode) -> None: # If the measurement qubits overlap, we need to start a new scheduling block. if self._current_block_measure_qargs() & set(node.qargs): self._begin_new_circuit_block() - t0 = 0 # pylint: disable=invalid-name + t0 = 0 # pylint: disable=invalid-name else: t0 = max( # pylint: disable=invalid-name self._idle_after[bit][1] for bit in node.qargs + node.cargs From 81596b178282e523a15b30d4fd06b3f12d53957b Mon Sep 17 00:00:00 2001 From: Thomas Alexander Date: Wed, 10 Aug 2022 23:19:27 -0300 Subject: [PATCH 59/70] Update comment on _visit_measure qargs. --- qiskit_ibm_provider/transpiler/passes/scheduling/scheduler.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/qiskit_ibm_provider/transpiler/passes/scheduling/scheduler.py b/qiskit_ibm_provider/transpiler/passes/scheduling/scheduler.py index 5b40862bf..4ace93b0c 100644 --- a/qiskit_ibm_provider/transpiler/passes/scheduling/scheduler.py +++ b/qiskit_ibm_provider/transpiler/passes/scheduling/scheduler.py @@ -184,6 +184,8 @@ def _visit_measure(self, node: DAGNode) -> None: which begins another grouping sequence. This behavior will change in future backend software updates.""" current_block_measure_qargs = self._current_block_measure_qargs() + # We handle a set of qubits here as _visit_reset currently calls + # this method and a reset may have multiple qubits. measure_qargs = set(node.qargs) t0q = max( From d50b28b58efe1ff022550dcf08393380790d5715 Mon Sep 17 00:00:00 2001 From: Thomas Alexander Date: Wed, 10 Aug 2022 23:22:50 -0300 Subject: [PATCH 60/70] Update comment typo. --- qiskit_ibm_provider/transpiler/passes/scheduling/scheduler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiskit_ibm_provider/transpiler/passes/scheduling/scheduler.py b/qiskit_ibm_provider/transpiler/passes/scheduling/scheduler.py index 4ace93b0c..76c9998c5 100644 --- a/qiskit_ibm_provider/transpiler/passes/scheduling/scheduler.py +++ b/qiskit_ibm_provider/transpiler/passes/scheduling/scheduler.py @@ -29,7 +29,7 @@ class DynamicCircuitScheduleAnalysis(BaseScheduler): backends due to the limitations imposed by hardware. This is expected to evolve over time as the dynamic circuit backends also change. - In its current form this is slow to Qiskit's ASAP scheduler in which instructions + In its current form this is similar to Qiskit's ASAP scheduler in which instructions start as early as possible. The primary differences are that: From 97e5bcd380addb98ef4d6482467d665205b08721 Mon Sep 17 00:00:00 2001 From: Thomas Alexander Date: Wed, 10 Aug 2022 23:25:03 -0300 Subject: [PATCH 61/70] Update comment on neighbor ordering. --- qiskit_ibm_provider/transpiler/passes/scheduling/scheduler.py | 1 + 1 file changed, 1 insertion(+) diff --git a/qiskit_ibm_provider/transpiler/passes/scheduling/scheduler.py b/qiskit_ibm_provider/transpiler/passes/scheduling/scheduler.py index 76c9998c5..f24248d3b 100644 --- a/qiskit_ibm_provider/transpiler/passes/scheduling/scheduler.py +++ b/qiskit_ibm_provider/transpiler/passes/scheduling/scheduler.py @@ -42,6 +42,7 @@ class DynamicCircuitScheduleAnalysis(BaseScheduler): * It is possible to apply gates during a measurement. * Measurements on disjoint qubits happen simultaneously and are part of the same block. Measurements that are not lexicographically neighbors in the generated QASM3 will + such as ``measure $0; x $1; measure $2;`` happen in separate blocks. """ From b5d9e3103922d00d8924635e7d80835bf807f7bd Mon Sep 17 00:00:00 2001 From: Thomas Alexander Date: Wed, 10 Aug 2022 23:31:59 -0300 Subject: [PATCH 62/70] Add comments. --- .../transpiler/passes/scheduling/scheduler.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/qiskit_ibm_provider/transpiler/passes/scheduling/scheduler.py b/qiskit_ibm_provider/transpiler/passes/scheduling/scheduler.py index f24248d3b..0fcd3140a 100644 --- a/qiskit_ibm_provider/transpiler/passes/scheduling/scheduler.py +++ b/qiskit_ibm_provider/transpiler/passes/scheduling/scheduler.py @@ -121,7 +121,7 @@ def _visit_conditional_node(self, node: DAGNode) -> None: """Handling case of a conditional execution. Conditional execution durations are currently non-deterministic. as we do not know - the time it will take to begin executing the block. We do however know when the + the time it will take to begin executing the block. We do however know the duration of the block contents execution (provided it does not also contain conditional executions). @@ -150,9 +150,9 @@ def _visit_conditional_node(self, node: DAGNode) -> None: # |t0q # Q ▒▒▒▒▒▒▒▒▒▒▒ # C ▒▒▒░░░▒▒░░░ - # |t0q + # |t0c # - t0c = max(t0q, t0c) + t0c = t0q t1c = t0c for bit in node.op.condition_bits: # Lock clbit until state is read From be11a0b2743d555db680b8d041e3b753c9afc659 Mon Sep 17 00:00:00 2001 From: Thomas Alexander Date: Wed, 6 Jul 2022 12:19:37 -0300 Subject: [PATCH 63/70] Update docs. --- qiskit_ibm_provider/transpiler/passes/__init__.py | 2 ++ qiskit_ibm_provider/transpiler/passes/scheduling/__init__.py | 2 -- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/qiskit_ibm_provider/transpiler/passes/__init__.py b/qiskit_ibm_provider/transpiler/passes/__init__.py index 0bae4b142..651f22dd5 100644 --- a/qiskit_ibm_provider/transpiler/passes/__init__.py +++ b/qiskit_ibm_provider/transpiler/passes/__init__.py @@ -17,6 +17,8 @@ .. currentmodule:: qiskit_ibm_provider.transpiler.passes +A collection of transpiler passes for IBM backends. + .. autosummary:: :toctree: ../stubs/ diff --git a/qiskit_ibm_provider/transpiler/passes/scheduling/__init__.py b/qiskit_ibm_provider/transpiler/passes/scheduling/__init__.py index cae47ead2..cdbd52fdf 100644 --- a/qiskit_ibm_provider/transpiler/passes/scheduling/__init__.py +++ b/qiskit_ibm_provider/transpiler/passes/scheduling/__init__.py @@ -15,8 +15,6 @@ Scheduling (:mod:`qiskit_ibm_provider.transpiler.passes.scheduling`) ==================================================================== -.. currentmodule:: qiskit_ibm_provider.transpiler.passes.scheduling - A collection of scheduling passes for working with IBM Quantum's next-generation backends that support advanced "dynamic circuit" capabilities. Ie., circuits with support for classical control-flow/feedback based off From e2f7f2cfa0d1da3603bbf23c6dc7efb8727ba335 Mon Sep 17 00:00:00 2001 From: Thomas Alexander Date: Wed, 6 Jul 2022 16:43:34 -0300 Subject: [PATCH 64/70] Ported dynamical decoupling pass and all tests to the dynamic circuits DD support. Tests are executing but failing currently. --- .../passes/scheduling/block_base_padder.py | 7 +- .../passes/scheduling/dynamical_decoupling.py | 354 ++++++++++++ .../scheduling/test_dynamical_decoupling.py | 529 ++++++++++++++++++ 3 files changed, 888 insertions(+), 2 deletions(-) create mode 100644 qiskit_ibm_provider/transpiler/passes/scheduling/dynamical_decoupling.py create mode 100644 test/unit/transpiler/passes/scheduling/test_dynamical_decoupling.py diff --git a/qiskit_ibm_provider/transpiler/passes/scheduling/block_base_padder.py b/qiskit_ibm_provider/transpiler/passes/scheduling/block_base_padder.py index b4dac3c15..6a96ebcfe 100644 --- a/qiskit_ibm_provider/transpiler/passes/scheduling/block_base_padder.py +++ b/qiskit_ibm_provider/transpiler/passes/scheduling/block_base_padder.py @@ -206,6 +206,7 @@ def _visit_generic(self, node: DAGNode) -> None: self._current_block_idx = block_idx t1 = t0 + node.op.duration # pylint: disable=invalid-name + self._block_duration = max(self._block_duration, t1) for bit in node.qargs: @@ -254,13 +255,14 @@ def _is_terminating_barrier(node: Optional[DAGNode]) -> bool: if not is_terminating_barrier: # Terminate with a barrier to be clear timing is non-deterministic # across the barrier. - self._apply_scheduled_op( + barrier_node = self._apply_scheduled_op( block_idx, block_duration, Barrier(self._dag.num_qubits()), self._dag.qubits, [], ) + barrier_node.op.duration = 0 # Reset idles for the new block. self._idle_after = {bit: 0 for bit in self._dag.qubits} @@ -289,7 +291,7 @@ def _apply_scheduled_op( oper: Instruction, qubits: Union[Qubit, List[Qubit]], clbits: Optional[Union[Clbit, List[Clbit]]] = None, - ) -> None: + ) -> DAGNode: """Add new operation to DAG with scheduled information. This is identical to apply_operation_back + updating the node_start_time propety. @@ -308,3 +310,4 @@ def _apply_scheduled_op( new_node = self._dag.apply_operation_back(oper, qargs=qubits, cargs=clbits) self.property_set["node_start_time"][new_node] = (block_idx, t_start) + return new_node diff --git a/qiskit_ibm_provider/transpiler/passes/scheduling/dynamical_decoupling.py b/qiskit_ibm_provider/transpiler/passes/scheduling/dynamical_decoupling.py new file mode 100644 index 000000000..888b2760f --- /dev/null +++ b/qiskit_ibm_provider/transpiler/passes/scheduling/dynamical_decoupling.py @@ -0,0 +1,354 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2022. +# +# 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. + +"""Dynamical decoupling insertion pass for IBM (dynamic circuit) backends.""" + +from typing import List, Optional + +import numpy as np +from qiskit.circuit import Qubit, Gate +from qiskit.circuit.delay import Delay +from qiskit.circuit.library.standard_gates import IGate, UGate, U3Gate +from qiskit.circuit.reset import Reset +from qiskit.dagcircuit import DAGCircuit, DAGNode, DAGInNode, DAGOpNode +from qiskit.quantum_info.operators.predicates import matrix_equal +from qiskit.quantum_info.synthesis import OneQubitEulerDecomposer +from qiskit.transpiler.exceptions import TranspilerError +from qiskit.transpiler.instruction_durations import InstructionDurations +from qiskit.transpiler.passes.optimization import Optimize1qGates + +from .block_base_padder import BlockBasePadder + + +class PadDynamicalDecoupling(BlockBasePadder): + """Dynamical decoupling insertion pass for IBM dynamic circuit backends. + + This pass works on a scheduled, physical circuit. It scans the circuit for + idle periods of time (i.e. those containing delay instructions) and inserts + a DD sequence of gates in those spots. These gates amount to the identity, + so do not alter the logical action of the circuit, but have the effect of + mitigating decoherence in those idle periods. + As a special case, the pass allows a length-1 sequence (e.g. [XGate()]). + In this case the DD insertion happens only when the gate inverse can be + absorbed into a neighboring gate in the circuit (so we would still be + replacing Delay with something that is equivalent to the identity). + This can be used, for instance, as a Hahn echo. + This pass ensures that the inserted sequence preserves the circuit exactly + (including global phase). + .. jupyter-execute:: + import numpy as np + from qiskit.circuit import QuantumCircuit + from qiskit.circuit.library import XGate + from qiskit.transpiler import PassManager, InstructionDurations + from qiskit.visualization import timeline_drawer + + from qiskit_ibm_provider.transpiler.passes.scheduling import DynamicCircuitScheduleAnalysis + from qiskit_ibm_provider.transpiler.passes.scheduling import PadDynamicalDecoupling + + circ = QuantumCircuit(4) + circ.h(0) + circ.cx(0, 1) + circ.cx(1, 2) + circ.cx(2, 3) + circ.measure_all() + durations = InstructionDurations( + [("h", 0, 50), ("cx", [0, 1], 700), ("reset", None, 10), + ("cx", [1, 2], 200), ("cx", [2, 3], 300), + ("x", None, 50), ("measure", None, 1000)] + ) + .. jupyter-execute:: + # balanced X-X sequence on all qubits + dd_sequence = [XGate(), XGate()] + pm = PassManager([DynamicCircuitScheduleAnalysis(durations), + PadDynamicalDecoupling(durations, dd_sequence)]) + circ_dd = pm.run(circ) + timeline_drawer(circ_dd) + .. jupyter-execute:: + # Uhrig sequence on qubit 0 + n = 8 + dd_sequence = [XGate()] * n + def uhrig_pulse_location(k): + return np.sin(np.pi * (k + 1) / (2 * n + 2)) ** 2 + spacing = [] + for k in range(n): + spacing.append(uhrig_pulse_location(k) - sum(spacing)) + spacing.append(1 - sum(spacing)) + pm = PassManager( + [ + ALAPScheduleAnalysis(durations), + PadDynamicalDecoupling(durations, dd_sequence, qubits=[0], spacing=spacing), + ] + ) + circ_dd = pm.run(circ) + timeline_drawer(circ_dd) + .. note:: + You need to call + :class:`~qiskit_ibm_provider.transpiler.passes.scheduling.DynamicCircuitScheduleAnalysis` + before running dynamical decoupling to guarantee your circuit satisfies acquisition + alignment constraints for dynamic circuit backends. + """ + + def __init__( + self, + durations: InstructionDurations, + dd_sequence: List[Gate], + qubits: Optional[List[int]] = None, + spacing: Optional[List[float]] = None, + skip_reset_qubits: bool = True, + pulse_alignment: int = 1, + extra_slack_distribution: str = "middle", + ): + """Dynamical decoupling initializer. + Args: + durations: Durations of instructions to be used in scheduling. + dd_sequence: Sequence of gates to apply in idle spots. + qubits: Physical qubits on which to apply DD. + If None, all qubits will undergo DD (when possible). + spacing: A list of spacings between the DD gates. + The available slack will be divided according to this. + The list length must be one more than the length of dd_sequence, + and the elements must sum to 1. If None, a balanced spacing + will be used [d/2, d, d, ..., d, d, d/2]. + skip_reset_qubits: If True, does not insert DD on idle periods that + immediately follow initialized/reset qubits + (as qubits in the ground state are less susceptible to decoherence). + pulse_alignment: The hardware constraints for gate timing allocation. + This is usually provided from ``backend.configuration().timing_constraints``. + If provided, the delay length, i.e. ``spacing``, is implicitly adjusted to + satisfy this constraint. + extra_slack_distribution: The option to control the behavior of DD sequence generation. + The duration of the DD sequence should be identical to an idle time in the + scheduled quantum circuit, however, the delay in between gates comprising the sequence + should be integer number in units of dt, and it might be further truncated + when ``pulse_alignment`` is specified. This sometimes results in the duration of + the created sequence being shorter than the idle time + that you want to fill with the sequence, i.e. `extra slack`. + This option takes following values. + - "middle": Put the extra slack to the interval at the middle of the sequence. + - "edges": Divide the extra slack as evenly as possible into + intervals at beginning and end of the sequence. + Raises: + TranspilerError: When invalid DD sequence is specified. + TranspilerError: When pulse gate with the duration which is + non-multiple of the alignment constraint value is found. + """ + super().__init__() + self._durations = durations + self._dd_sequence = dd_sequence + self._qubits = qubits + self._skip_reset_qubits = skip_reset_qubits + self._alignment = pulse_alignment + self._spacing = spacing + self._extra_slack_distribution = extra_slack_distribution + + self._dd_sequence_lengths = dict() + self._sequence_phase = 0 + + def _pre_runhook(self, dag: DAGCircuit): + super()._pre_runhook(dag) + + num_pulses = len(self._dd_sequence) + + # Check if physical circuit is given + if len(dag.qregs) != 1 or dag.qregs.get("q", None) is None: + raise TranspilerError("DD runs on physical circuits only.") + + # Set default spacing otherwise validate user input + if self._spacing is None: + mid = 1 / num_pulses + end = mid / 2 + self._spacing = [end] + [mid] * (num_pulses - 1) + [end] + else: + if sum(self._spacing) != 1 or any(a < 0 for a in self._spacing): + raise TranspilerError( + "The spacings must be given in terms of fractions " + "of the slack period and sum to 1." + ) + + # Check if DD sequence is identity + if num_pulses != 1: + if num_pulses % 2 != 0: + raise TranspilerError("DD sequence must contain an even number of gates (or 1).") + noop = np.eye(2) + for gate in self._dd_sequence: + noop = noop.dot(gate.to_matrix()) + if not matrix_equal(noop, IGate().to_matrix(), ignore_phase=True): + raise TranspilerError("The DD sequence does not make an identity operation.") + self._sequence_phase = np.angle(noop[0][0]) + + # Precompute qubit-wise DD sequence length for performance + for qubit in dag.qubits: + physical_index = dag.qubits.index(qubit) + if self._qubits and physical_index not in self._qubits: + continue + + sequence_lengths = [] + for gate in self._dd_sequence: + try: + # Check calibration. + gate_length = dag.calibrations[gate.name][(physical_index, gate.params)] + if gate_length % self._alignment != 0: + # This is necessary to implement lightweight scheduling logic for this pass. + # Usually the pulse alignment constraint and pulse data chunk size take + # the same value, however, we can intentionally violate this pattern + # at the gate level. For example, we can create a schedule consisting of + # a pi-pulse of 32 dt followed by a post buffer, i.e. delay, of 4 dt + # on the device with 16 dt constraint. Note that the pi-pulse length + # is multiple of 16 dt but the gate length of 36 is not multiple of it. + # Such pulse gate should be excluded. + raise TranspilerError( + f"Pulse gate {gate.name} with length non-multiple of {self._alignment} " + f"is not acceptable in {self.__class__.__name__} pass." + ) + except KeyError: + gate_length = self._durations.get(gate, physical_index) + sequence_lengths.append(gate_length) + # Update gate duration. This is necessary for current timeline drawer, i.e. scheduled. + gate.duration = gate_length + self._dd_sequence_lengths[qubit] = sequence_lengths + + def _pad( + self, + block_idx: int, + qubit: Qubit, + t_start: int, + t_end: int, + next_node: DAGNode, + prev_node: DAGNode, + ): + # This routine takes care of the pulse alignment constraint for the DD sequence. + # Note that the alignment constraint acts on the t0 of the DAGOpNode. + # Now this constrained scheduling problem is simplified to the problem of + # finding a delay amount which is a multiple of the constraint value by assuming + # that the duration of every DAGOpNode is also a multiple of the constraint value. + # + # For example, given the constraint value of 16 and XY4 with 160 dt gates. + # Here we assume current interval is 992 dt. + # + # relative spacing := [0.125, 0.25, 0.25, 0.25, 0.125] + # slack = 992 dt - 4 x 160 dt = 352 dt + # + # unconstrained sequence: 44dt-X1-88dt-Y2-88dt-X3-88dt-Y4-44dt + # constrained sequence : 32dt-X1-80dt-Y2-80dt-X3-80dt-Y4-32dt + extra slack 48 dt + # + # Now we evenly split extra slack into start and end of the sequence. + # The distributed slack should be multiple of 16. + # Start = +16, End += 32 + # + # final sequence : 48dt-X1-80dt-Y2-80dt-X3-80dt-Y4-64dt / in total 992 dt + # + # Now we verify t0 of every node starts from multiple of 16 dt. + # + # X1: 48 dt (3 x 16 dt) + # Y2: 48 dt + 160 dt + 80 dt = 288 dt (18 x 16 dt) + # Y3: 288 dt + 160 dt + 80 dt = 528 dt (33 x 16 dt) + # Y4: 368 dt + 160 dt + 80 dt = 768 dt (48 x 16 dt) + # + # As you can see, constraints on t0 are all satified without explicit scheduling. + time_interval = t_end - t_start + + if self._qubits and self._dag.qubits.index(qubit) not in self._qubits: + # Target physical qubit is not the target of this DD sequence. + self._apply_scheduled_op(block_idx, t_start, Delay(time_interval, self._dag.unit), qubit) + return + + if self._skip_reset_qubits and ( + isinstance(prev_node, DAGInNode) or isinstance(prev_node.op, Reset) + ): + # Previous node is the start edge or reset, i.e. qubit is ground state. + self._apply_scheduled_op(block_idx, t_start, Delay(time_interval, self._dag.unit), qubit) + return + + slack = time_interval - np.sum(self._dd_sequence_lengths[qubit]) + sequence_gphase = self._sequence_phase + + if slack <= 0: + # Interval too short. + self._apply_scheduled_op(block_idx, t_start, Delay(time_interval, self._dag.unit), qubit) + return + + if len(self._dd_sequence) == 1: + # Special case of using a single gate for DD + u_inv = self._dd_sequence[0].inverse().to_matrix() + theta, phi, lam, phase = OneQubitEulerDecomposer().angles_and_phase(u_inv) + if isinstance(next_node, DAGOpNode) and isinstance(next_node.op, (UGate, U3Gate)): + # Absorb the inverse into the successor (from left in circuit) + theta_r, phi_r, lam_r = next_node.op.params + next_node.op.params = Optimize1qGates.compose_u3( + theta_r, phi_r, lam_r, theta, phi, lam + ) + sequence_gphase += phase + elif isinstance(prev_node, DAGOpNode) and isinstance(prev_node.op, (UGate, U3Gate)): + # Absorb the inverse into the predecessor (from right in circuit) + theta_l, phi_l, lam_l = prev_node.op.params + prev_node.op.params = Optimize1qGates.compose_u3( + theta, phi, lam, theta_l, phi_l, lam_l + ) + sequence_gphase += phase + else: + # Don't do anything if there's no single-qubit gate to absorb the inverse + self._apply_scheduled_op(block_idx, t_start, Delay(time_interval, self._dag.unit), qubit) + return + + def _constrained_length(values): + return self._alignment * np.floor(values / self._alignment) + + # (1) Compute DD intervals satisfying the constraint + taus = _constrained_length(slack * np.asarray(self._spacing)) + extra_slack = slack - np.sum(taus) + + # (2) Distribute extra slack + if self._extra_slack_distribution == "middle": + mid_ind = int((len(taus) - 1) / 2) + to_middle = _constrained_length(extra_slack) + taus[mid_ind] += to_middle + if extra_slack - to_middle: + # If to_middle is not a multiple value of the pulse alignment, + # it is truncated to the nearlest multiple value and + # the rest of slack is added to the end. + taus[-1] += extra_slack - to_middle + elif self._extra_slack_distribution == "edges": + to_begin_edge = _constrained_length(extra_slack / 2) + taus[0] += to_begin_edge + taus[-1] += extra_slack - to_begin_edge + else: + raise TranspilerError( + f"Option extra_slack_distribution = {self._extra_slack_distribution} is invalid." + ) + + # (3) Construct DD sequence with delays + num_elements = max(len(self._dd_sequence), len(taus)) + idle_after = t_start + for dd_ind in range(num_elements): + if dd_ind < len(taus): + tau = taus[dd_ind] + if tau > 0: + self._apply_scheduled_op(block_idx, idle_after, Delay(tau, self._dag.unit), qubit) + idle_after += tau + if dd_ind < len(self._dd_sequence): + gate = self._dd_sequence[dd_ind] + gate_length = self._dd_sequence_lengths[qubit][dd_ind] + self._apply_scheduled_op(block_idx, idle_after, gate, qubit) + idle_after += gate_length + + self._dag.global_phase = self._mod_2pi(self._dag.global_phase + sequence_gphase) + + @staticmethod + def _mod_2pi(angle: float, atol: float = 0): + """Wrap angle into interval [-π,π). If within atol of the endpoint, clamp to -π""" + wrapped = (angle + np.pi) % (2 * np.pi) - np.pi + if abs(wrapped - np.pi) < atol: + wrapped = -np.pi + return wrapped + + + diff --git a/test/unit/transpiler/passes/scheduling/test_dynamical_decoupling.py b/test/unit/transpiler/passes/scheduling/test_dynamical_decoupling.py new file mode 100644 index 000000000..d2dc98a5f --- /dev/null +++ b/test/unit/transpiler/passes/scheduling/test_dynamical_decoupling.py @@ -0,0 +1,529 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 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 dynamical decoupling insertion pass.""" + +import unittest +import numpy as np +from numpy import pi + +import qiskit.pulse as pulse +from ddt import ddt, data +from qiskit.circuit import QuantumCircuit, Delay +from qiskit.circuit.library import XGate, YGate, RXGate, UGate +from qiskit.quantum_info import Operator +from qiskit.test import QiskitTestCase +from qiskit.transpiler.instruction_durations import InstructionDurations +from qiskit.transpiler.passmanager import PassManager +from qiskit.transpiler.exceptions import TranspilerError + +from qiskit_ibm_provider.transpiler.passes.scheduling.dynamical_decoupling import PadDynamicalDecoupling +from qiskit_ibm_provider.transpiler.passes.scheduling.scheduler import ( + DynamicCircuitScheduleAnalysis, +) + +@ddt +class TestPadDynamicalDecoupling(QiskitTestCase): + """Tests PadDynamicalDecoupling pass.""" + + def setUp(self): + """Circuits to test DD on. + ┌───┐ + q_0: ┤ H ├──■──────────── + └───┘┌─┴─┐ + q_1: ─────┤ X ├──■─────── + └───┘┌─┴─┐ + q_2: ──────────┤ X ├──■── + └───┘┌─┴─┐ + q_3: ───────────────┤ X ├ + └───┘ + ┌──────────┐ + q_0: ──■──┤ U(π,0,π) ├──────────■── + ┌─┴─┐└──────────┘ ┌─┴─┐ + q_1: ┤ X ├─────■───────────■──┤ X ├ + └───┘ ┌─┴─┐ ┌─┐┌─┴─┐└───┘ + q_2: ────────┤ X ├────┤M├┤ X ├───── + └───┘ └╥┘└───┘ + c: 1/══════════════════╩═══════════ + 0 + """ + super().setUp() + + self.ghz4 = QuantumCircuit(4) + self.ghz4.h(0) + self.ghz4.cx(0, 1) + self.ghz4.cx(1, 2) + self.ghz4.cx(2, 3) + + self.midmeas = QuantumCircuit(3, 1) + self.midmeas.cx(0, 1) + self.midmeas.cx(1, 2) + self.midmeas.u(pi, 0, pi, 0) + self.midmeas.measure(2, 0) + self.midmeas.cx(1, 2) + self.midmeas.cx(0, 1) + + self.durations = InstructionDurations( + [ + ("h", 0, 50), + ("cx", [0, 1], 700), + ("cx", [1, 2], 200), + ("cx", [2, 3], 300), + ("x", None, 50), + ("y", None, 50), + ("u", None, 100), + ("rx", None, 100), + ("measure", None, 1000), + ("reset", None, 1500), + ] + ) + + def test_insert_dd_ghz(self): + """Test DD gates are inserted in correct spots.""" + dd_sequence = [XGate(), XGate()] + pm = PassManager( + [ + DynamicCircuitScheduleAnalysis(self.durations), + PadDynamicalDecoupling(self.durations, dd_sequence), + ] + ) + + ghz4_dd = pm.run(self.ghz4) + + expected = self.ghz4.copy() + expected = expected.compose(Delay(50), [1], front=True) + expected = expected.compose(Delay(750), [2], front=True) + expected = expected.compose(Delay(950), [3], front=True) + + expected = expected.compose(Delay(100), [0]) + expected = expected.compose(XGate(), [0]) + expected = expected.compose(Delay(200), [0]) + expected = expected.compose(XGate(), [0]) + expected = expected.compose(Delay(100), [0]) + + expected = expected.compose(Delay(50), [1]) + expected = expected.compose(XGate(), [1]) + expected = expected.compose(Delay(100), [1]) + expected = expected.compose(XGate(), [1]) + expected = expected.compose(Delay(50), [1]) + + self.assertEqual(ghz4_dd, expected) + + def test_insert_dd_ghz_one_qubit(self): + """Test DD gates are inserted on only one qubit.""" + dd_sequence = [XGate(), XGate()] + pm = PassManager( + [ + DynamicCircuitScheduleAnalysis(self.durations), + PadDynamicalDecoupling(self.durations, dd_sequence, qubits=[0]), + ] + ) + + ghz4_dd = pm.run(self.ghz4.measure_all(inplace=False)) + + expected = self.ghz4.copy() + expected = expected.compose(Delay(50), [1], front=True) + expected = expected.compose(Delay(750), [2], front=True) + expected = expected.compose(Delay(950), [3], front=True) + + expected = expected.compose(Delay(100), [0]) + expected = expected.compose(XGate(), [0]) + expected = expected.compose(Delay(200), [0]) + expected = expected.compose(XGate(), [0]) + expected = expected.compose(Delay(100), [0]) + + expected = expected.compose(Delay(300), [1]) + + expected.measure_all() + + self.assertEqual(ghz4_dd, expected) + + def test_insert_dd_ghz_everywhere(self): + """Test DD gates even on initial idle spots.""" + dd_sequence = [YGate(), YGate()] + pm = PassManager( + [ + DynamicCircuitScheduleAnalysis(self.durations), + PadDynamicalDecoupling(self.durations, dd_sequence, skip_reset_qubits=False), + ] + ) + + ghz4_dd = pm.run(self.ghz4) + + expected = self.ghz4.copy() + expected = expected.compose(Delay(50), [1], front=True) + + expected = expected.compose(Delay(162), [2], front=True) + expected = expected.compose(YGate(), [2], front=True) + expected = expected.compose(Delay(326), [2], front=True) + expected = expected.compose(YGate(), [2], front=True) + expected = expected.compose(Delay(162), [2], front=True) + + expected = expected.compose(Delay(212), [3], front=True) + expected = expected.compose(YGate(), [3], front=True) + expected = expected.compose(Delay(426), [3], front=True) + expected = expected.compose(YGate(), [3], front=True) + expected = expected.compose(Delay(212), [3], front=True) + + expected = expected.compose(Delay(100), [0]) + expected = expected.compose(YGate(), [0]) + expected = expected.compose(Delay(200), [0]) + expected = expected.compose(YGate(), [0]) + expected = expected.compose(Delay(100), [0]) + + expected = expected.compose(Delay(50), [1]) + expected = expected.compose(YGate(), [1]) + expected = expected.compose(Delay(100), [1]) + expected = expected.compose(YGate(), [1]) + expected = expected.compose(Delay(50), [1]) + + self.assertEqual(ghz4_dd, expected) + + def test_insert_dd_ghz_xy4(self): + """Test XY4 sequence of DD gates.""" + dd_sequence = [XGate(), YGate(), XGate(), YGate()] + pm = PassManager( + [ + DynamicCircuitScheduleAnalysis(self.durations), + PadDynamicalDecoupling(self.durations, dd_sequence), + ] + ) + + ghz4_dd = pm.run(self.ghz4) + + expected = self.ghz4.copy() + expected = expected.compose(Delay(50), [1], front=True) + expected = expected.compose(Delay(750), [2], front=True) + expected = expected.compose(Delay(950), [3], front=True) + + expected = expected.compose(Delay(37), [0]) + expected = expected.compose(XGate(), [0]) + expected = expected.compose(Delay(75), [0]) + expected = expected.compose(YGate(), [0]) + expected = expected.compose(Delay(76), [0]) + expected = expected.compose(XGate(), [0]) + expected = expected.compose(Delay(75), [0]) + expected = expected.compose(YGate(), [0]) + expected = expected.compose(Delay(37), [0]) + + expected = expected.compose(Delay(12), [1]) + expected = expected.compose(XGate(), [1]) + expected = expected.compose(Delay(25), [1]) + expected = expected.compose(YGate(), [1]) + expected = expected.compose(Delay(26), [1]) + expected = expected.compose(XGate(), [1]) + expected = expected.compose(Delay(25), [1]) + expected = expected.compose(YGate(), [1]) + expected = expected.compose(Delay(12), [1]) + + self.assertEqual(ghz4_dd, expected) + + def test_insert_midmeas_hahn_alap(self): + """Test a single X gate as Hahn echo can absorb in the downstream circuit. + global phase: 3π/2.""" + dd_sequence = [XGate()] + pm = PassManager( + [ + DynamicCircuitScheduleAnalysis(self.durations), + PadDynamicalDecoupling(self.durations, dd_sequence), + ] + ) + + midmeas_dd = pm.run(self.midmeas) + + combined_u = UGate(0, pi / 2, -pi / 2) + + expected = QuantumCircuit(3, 1) + expected.cx(0, 1) + expected.delay(625, 0) + expected.x(0) + expected.delay(625, 0) + expected.compose(combined_u, [0], inplace=True) + expected.delay(700, 2) + expected.cx(1, 2) + expected.delay(1000, 1) + expected.measure(2, 0) + expected.cx(1, 2) + expected.cx(0, 1) + expected.delay(700, 2) + expected.global_phase = 4.71238898038469 + + self.assertEqual(midmeas_dd, expected) + # check the absorption into U was done correctly + self.assertEqual(Operator(combined_u), Operator(XGate()) & Operator(XGate())) + + def test_insert_midmeas_hahn_asap(self): + """Test a single X gate as Hahn echo can absorb in the upstream circuit.""" + dd_sequence = [RXGate(pi / 4)] + pm = PassManager( + [ + DynamicCircuitScheduleAnalysis(self.durations), + PadDynamicalDecoupling(self.durations, dd_sequence), + ] + ) + + midmeas_dd = pm.run(self.midmeas) + + combined_u = UGate(3 * pi / 4, -pi / 2, pi / 2) + + expected = QuantumCircuit(3, 1) + expected.cx(0, 1) + expected.compose(combined_u, [0], inplace=True) + expected.delay(600, 0) + expected.rx(pi / 4, 0) + expected.delay(600, 0) + expected.delay(700, 2) + expected.cx(1, 2) + expected.delay(1000, 1) + expected.measure(2, 0) + expected.cx(1, 2) + expected.cx(0, 1) + expected.delay(700, 2) + + self.assertEqual(midmeas_dd, expected) + # check the absorption into U was done correctly + self.assertTrue( + Operator(XGate()).equiv( + Operator(UGate(3 * pi / 4, -pi / 2, pi / 2)) & Operator(RXGate(pi / 4)) + ) + ) + + def test_insert_ghz_uhrig(self): + """Test custom spacing (following Uhrig DD [1]). + [1] Uhrig, G. "Keeping a quantum bit alive by optimized π-pulse sequences." + Physical Review Letters 98.10 (2007): 100504.""" + n = 8 + dd_sequence = [XGate()] * n + + # uhrig specifies the location of the k'th pulse + def uhrig(k): + return np.sin(np.pi * (k + 1) / (2 * n + 2)) ** 2 + + # convert that to spacing between pulses (whatever finite duration pulses have) + spacing = [] + for k in range(n): + spacing.append(uhrig(k) - sum(spacing)) + spacing.append(1 - sum(spacing)) + + pm = PassManager( + [ + DynamicCircuitScheduleAnalysis(self.durations), + PadDynamicalDecoupling(self.durations, dd_sequence, qubits=[0], spacing=spacing), + ] + ) + + ghz4_dd = pm.run(self.ghz4) + + expected = self.ghz4.copy() + expected = expected.compose(Delay(50), [1], front=True) + expected = expected.compose(Delay(750), [2], front=True) + expected = expected.compose(Delay(950), [3], front=True) + + expected = expected.compose(Delay(3), [0]) + expected = expected.compose(XGate(), [0]) + expected = expected.compose(Delay(8), [0]) + expected = expected.compose(XGate(), [0]) + expected = expected.compose(Delay(13), [0]) + expected = expected.compose(XGate(), [0]) + expected = expected.compose(Delay(16), [0]) + expected = expected.compose(XGate(), [0]) + expected = expected.compose(Delay(20), [0]) + expected = expected.compose(XGate(), [0]) + expected = expected.compose(Delay(16), [0]) + expected = expected.compose(XGate(), [0]) + expected = expected.compose(Delay(13), [0]) + expected = expected.compose(XGate(), [0]) + expected = expected.compose(Delay(8), [0]) + expected = expected.compose(XGate(), [0]) + expected = expected.compose(Delay(3), [0]) + + expected = expected.compose(Delay(300), [1]) + + self.assertEqual(ghz4_dd, expected) + + def test_asymmetric_xy4_in_t2(self): + """Test insertion of XY4 sequence with unbalanced spacing.""" + dd_sequence = [XGate(), YGate()] * 2 + spacing = [0] + [1 / 4] * 4 + pm = PassManager( + [ + DynamicCircuitScheduleAnalysis(self.durations), + PadDynamicalDecoupling(self.durations, dd_sequence, spacing=spacing), + ] + ) + + t2 = QuantumCircuit(1) + t2.h(0) + t2.delay(2000, 0) + t2.h(0) + + expected = QuantumCircuit(1) + expected.h(0) + expected.x(0) + expected.delay(450, 0) + expected.y(0) + expected.delay(450, 0) + expected.x(0) + expected.delay(450, 0) + expected.y(0) + expected.delay(450, 0) + expected.h(0) + expected.global_phase = pi + + t2_dd = pm.run(t2) + + self.assertEqual(t2_dd, expected) + # check global phase is correct + self.assertEqual(Operator(t2), Operator(expected)) + + def test_dd_after_reset(self): + """Test skip_reset_qubits option works.""" + dd_sequence = [XGate(), XGate()] + spacing = [0.1, 0.9] + pm = PassManager( + [ + DynamicCircuitScheduleAnalysis(self.durations), + PadDynamicalDecoupling( + self.durations, dd_sequence, spacing=spacing, skip_reset_qubits=True + ), + ] + ) + + t2 = QuantumCircuit(1) + t2.reset(0) + t2.delay(1000) + t2.h(0) + t2.delay(2000, 0) + t2.h(0) + + expected = QuantumCircuit(1) + expected.reset(0) + expected.delay(1000) + expected.h(0) + expected.delay(190, 0) + expected.x(0) + expected.delay(1710, 0) + expected.x(0) + expected.h(0) + + t2_dd = pm.run(t2) + + self.assertEqual(t2_dd, expected) + + def test_insert_dd_bad_sequence(self): + """Test DD raises when non-identity sequence is inserted.""" + dd_sequence = [XGate(), YGate()] + pm = PassManager( + [ + DynamicCircuitScheduleAnalysis(self.durations), + PadDynamicalDecoupling(self.durations, dd_sequence), + ] + ) + + with self.assertRaises(TranspilerError): + pm.run(self.ghz4) + + @data(0.5, 1.5) + def test_dd_with_calibrations_with_parameters(self, param_value): + """Check that calibrations in a circuit with parameters work fine.""" + + circ = QuantumCircuit(2) + circ.x(0) + circ.cx(0, 1) + circ.rx(param_value, 1) + + rx_duration = int(param_value * 1000) + + with pulse.build() as rx: + pulse.play(pulse.Gaussian(rx_duration, 0.1, rx_duration // 4), pulse.DriveChannel(1)) + + circ.add_calibration("rx", (1,), rx, params=[param_value]) + + durations = InstructionDurations([("x", None, 100), ("cx", None, 300)]) + + dd_sequence = [XGate(), XGate()] + pm = PassManager( + [DynamicCircuitScheduleAnalysis(durations), PadDynamicalDecoupling(durations, dd_sequence)] + ) + + self.assertEqual(pm.run(circ).duration, rx_duration + 100 + 300) + + def test_insert_dd_ghz_xy4_with_alignment(self): + """Test DD with pulse alignment constraints.""" + dd_sequence = [XGate(), YGate(), XGate(), YGate()] + pm = PassManager( + [ + DynamicCircuitScheduleAnalysis(self.durations), + PadDynamicalDecoupling( + self.durations, + dd_sequence, + pulse_alignment=10, + extra_slack_distribution="edges", + ), + ] + ) + + ghz4_dd = pm.run(self.ghz4) + + expected = self.ghz4.copy() + expected = expected.compose(Delay(50), [1], front=True) + expected = expected.compose(Delay(750), [2], front=True) + expected = expected.compose(Delay(950), [3], front=True) + + expected = expected.compose(Delay(40), [0]) + expected = expected.compose(XGate(), [0]) + expected = expected.compose(Delay(70), [0]) + expected = expected.compose(YGate(), [0]) + expected = expected.compose(Delay(70), [0]) + expected = expected.compose(XGate(), [0]) + expected = expected.compose(Delay(70), [0]) + expected = expected.compose(YGate(), [0]) + expected = expected.compose(Delay(50), [0]) + + expected = expected.compose(Delay(20), [1]) + expected = expected.compose(XGate(), [1]) + expected = expected.compose(Delay(20), [1]) + expected = expected.compose(YGate(), [1]) + expected = expected.compose(Delay(20), [1]) + expected = expected.compose(XGate(), [1]) + expected = expected.compose(Delay(20), [1]) + expected = expected.compose(YGate(), [1]) + expected = expected.compose(Delay(20), [1]) + + self.assertEqual(ghz4_dd, expected) + + def test_dd_can_sequentially_called(self): + """Test if sequentially called DD pass can output the same circuit. + This test verifies: + - if global phase is properly propagated from the previous padding node. + - if node_start_time property is properly updated for new dag circuit. + """ + dd_sequence = [XGate(), YGate(), XGate(), YGate()] + + pm1 = PassManager( + [ + DynamicCircuitScheduleAnalysis(self.durations), + PadDynamicalDecoupling(self.durations, dd_sequence, qubits=[0]), + PadDynamicalDecoupling(self.durations, dd_sequence, qubits=[1]), + ] + ) + circ1 = pm1.run(self.ghz4) + + pm2 = PassManager( + [ + DynamicCircuitScheduleAnalysis(self.durations), + PadDynamicalDecoupling(self.durations, dd_sequence, qubits=[0, 1]), + ] + ) + circ2 = pm2.run(self.ghz4) + + self.assertEqual(circ1, circ2) From 9c4d4761a771214b98c41d768b87784f79024a83 Mon Sep 17 00:00:00 2001 From: Thomas Alexander Date: Wed, 6 Jul 2022 16:59:42 -0300 Subject: [PATCH 65/70] Tests passing outside of scheduling fixes required. --- .../scheduling/test_dynamical_decoupling.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/test/unit/transpiler/passes/scheduling/test_dynamical_decoupling.py b/test/unit/transpiler/passes/scheduling/test_dynamical_decoupling.py index d2dc98a5f..a4069af26 100644 --- a/test/unit/transpiler/passes/scheduling/test_dynamical_decoupling.py +++ b/test/unit/transpiler/passes/scheduling/test_dynamical_decoupling.py @@ -115,6 +115,7 @@ def test_insert_dd_ghz(self): expected = expected.compose(Delay(100), [1]) expected = expected.compose(XGate(), [1]) expected = expected.compose(Delay(50), [1]) + expected.barrier() self.assertEqual(ghz4_dd, expected) @@ -144,6 +145,7 @@ def test_insert_dd_ghz_one_qubit(self): expected = expected.compose(Delay(300), [1]) expected.measure_all() + expected.barrier() self.assertEqual(ghz4_dd, expected) @@ -185,6 +187,7 @@ def test_insert_dd_ghz_everywhere(self): expected = expected.compose(Delay(100), [1]) expected = expected.compose(YGate(), [1]) expected = expected.compose(Delay(50), [1]) + expected.barrier() self.assertEqual(ghz4_dd, expected) @@ -224,6 +227,7 @@ def test_insert_dd_ghz_xy4(self): expected = expected.compose(Delay(25), [1]) expected = expected.compose(YGate(), [1]) expected = expected.compose(Delay(12), [1]) + expected.barrier() self.assertEqual(ghz4_dd, expected) @@ -252,6 +256,7 @@ def test_insert_midmeas_hahn_alap(self): expected.cx(1, 2) expected.delay(1000, 1) expected.measure(2, 0) + expected.barrier() expected.cx(1, 2) expected.cx(0, 1) expected.delay(700, 2) @@ -285,6 +290,7 @@ def test_insert_midmeas_hahn_asap(self): expected.cx(1, 2) expected.delay(1000, 1) expected.measure(2, 0) + expected.barrier() expected.cx(1, 2) expected.cx(0, 1) expected.delay(700, 2) @@ -347,6 +353,7 @@ def uhrig(k): expected = expected.compose(Delay(3), [0]) expected = expected.compose(Delay(300), [1]) + expected.barrier() self.assertEqual(ghz4_dd, expected) @@ -377,6 +384,7 @@ def test_asymmetric_xy4_in_t2(self): expected.y(0) expected.delay(450, 0) expected.h(0) + expected.barrier() expected.global_phase = pi t2_dd = pm.run(t2) @@ -454,8 +462,11 @@ def test_dd_with_calibrations_with_parameters(self, param_value): pm = PassManager( [DynamicCircuitScheduleAnalysis(durations), PadDynamicalDecoupling(durations, dd_sequence)] ) - - self.assertEqual(pm.run(circ).duration, rx_duration + 100 + 300) + pm.run(circ) + node_start_times = pm.property_set["node_start_time"] + for node, times in node_start_times.items(): + if isinstance(node.op, RXGate): + self.assertEqual(node.op.duration, rx_duration) def test_insert_dd_ghz_xy4_with_alignment(self): """Test DD with pulse alignment constraints.""" @@ -498,6 +509,7 @@ def test_insert_dd_ghz_xy4_with_alignment(self): expected = expected.compose(Delay(20), [1]) expected = expected.compose(YGate(), [1]) expected = expected.compose(Delay(20), [1]) + expected.barrier() self.assertEqual(ghz4_dd, expected) From 8e7b4c43989777e800818d18b28d71af7a3995ba Mon Sep 17 00:00:00 2001 From: Thomas Alexander Date: Wed, 6 Jul 2022 23:25:01 -0300 Subject: [PATCH 66/70] Dynamical decoupling tests passing. --- .../passes/scheduling/dynamical_decoupling.py | 55 ++++++++----- .../scheduling/test_dynamical_decoupling.py | 80 ++++++++----------- 2 files changed, 69 insertions(+), 66 deletions(-) diff --git a/qiskit_ibm_provider/transpiler/passes/scheduling/dynamical_decoupling.py b/qiskit_ibm_provider/transpiler/passes/scheduling/dynamical_decoupling.py index 888b2760f..f591fb880 100644 --- a/qiskit_ibm_provider/transpiler/passes/scheduling/dynamical_decoupling.py +++ b/qiskit_ibm_provider/transpiler/passes/scheduling/dynamical_decoupling.py @@ -12,7 +12,7 @@ """Dynamical decoupling insertion pass for IBM (dynamic circuit) backends.""" -from typing import List, Optional +from typing import Dict, List, Optional import numpy as np from qiskit.circuit import Qubit, Gate @@ -150,10 +150,10 @@ def __init__( self._spacing = spacing self._extra_slack_distribution = extra_slack_distribution - self._dd_sequence_lengths = dict() + self._dd_sequence_lengths: Dict[Qubit, list] = {} self._sequence_phase = 0 - def _pre_runhook(self, dag: DAGCircuit): + def _pre_runhook(self, dag: DAGCircuit) -> None: super()._pre_runhook(dag) num_pulses = len(self._dd_sequence) @@ -177,12 +177,16 @@ def _pre_runhook(self, dag: DAGCircuit): # Check if DD sequence is identity if num_pulses != 1: if num_pulses % 2 != 0: - raise TranspilerError("DD sequence must contain an even number of gates (or 1).") + raise TranspilerError( + "DD sequence must contain an even number of gates (or 1)." + ) noop = np.eye(2) for gate in self._dd_sequence: noop = noop.dot(gate.to_matrix()) if not matrix_equal(noop, IGate().to_matrix(), ignore_phase=True): - raise TranspilerError("The DD sequence does not make an identity operation.") + raise TranspilerError( + "The DD sequence does not make an identity operation." + ) self._sequence_phase = np.angle(noop[0][0]) # Precompute qubit-wise DD sequence length for performance @@ -195,7 +199,9 @@ def _pre_runhook(self, dag: DAGCircuit): for gate in self._dd_sequence: try: # Check calibration. - gate_length = dag.calibrations[gate.name][(physical_index, gate.params)] + gate_length = dag.calibrations[gate.name][ + (physical_index, gate.params) + ] if gate_length % self._alignment != 0: # This is necessary to implement lightweight scheduling logic for this pass. # Usually the pulse alignment constraint and pulse data chunk size take @@ -224,7 +230,7 @@ def _pad( t_end: int, next_node: DAGNode, prev_node: DAGNode, - ): + ) -> None: # This routine takes care of the pulse alignment constraint for the DD sequence. # Note that the alignment constraint acts on the t0 of the DAGOpNode. # Now this constrained scheduling problem is simplified to the problem of @@ -258,14 +264,18 @@ def _pad( if self._qubits and self._dag.qubits.index(qubit) not in self._qubits: # Target physical qubit is not the target of this DD sequence. - self._apply_scheduled_op(block_idx, t_start, Delay(time_interval, self._dag.unit), qubit) + self._apply_scheduled_op( + block_idx, t_start, Delay(time_interval, self._dag.unit), qubit + ) return if self._skip_reset_qubits and ( isinstance(prev_node, DAGInNode) or isinstance(prev_node.op, Reset) ): # Previous node is the start edge or reset, i.e. qubit is ground state. - self._apply_scheduled_op(block_idx, t_start, Delay(time_interval, self._dag.unit), qubit) + self._apply_scheduled_op( + block_idx, t_start, Delay(time_interval, self._dag.unit), qubit + ) return slack = time_interval - np.sum(self._dd_sequence_lengths[qubit]) @@ -273,21 +283,27 @@ def _pad( if slack <= 0: # Interval too short. - self._apply_scheduled_op(block_idx, t_start, Delay(time_interval, self._dag.unit), qubit) + self._apply_scheduled_op( + block_idx, t_start, Delay(time_interval, self._dag.unit), qubit + ) return if len(self._dd_sequence) == 1: # Special case of using a single gate for DD u_inv = self._dd_sequence[0].inverse().to_matrix() theta, phi, lam, phase = OneQubitEulerDecomposer().angles_and_phase(u_inv) - if isinstance(next_node, DAGOpNode) and isinstance(next_node.op, (UGate, U3Gate)): + if isinstance(next_node, DAGOpNode) and isinstance( + next_node.op, (UGate, U3Gate) + ): # Absorb the inverse into the successor (from left in circuit) theta_r, phi_r, lam_r = next_node.op.params next_node.op.params = Optimize1qGates.compose_u3( theta_r, phi_r, lam_r, theta, phi, lam ) sequence_gphase += phase - elif isinstance(prev_node, DAGOpNode) and isinstance(prev_node.op, (UGate, U3Gate)): + elif isinstance(prev_node, DAGOpNode) and isinstance( + prev_node.op, (UGate, U3Gate) + ): # Absorb the inverse into the predecessor (from right in circuit) theta_l, phi_l, lam_l = prev_node.op.params prev_node.op.params = Optimize1qGates.compose_u3( @@ -296,10 +312,12 @@ def _pad( sequence_gphase += phase else: # Don't do anything if there's no single-qubit gate to absorb the inverse - self._apply_scheduled_op(block_idx, t_start, Delay(time_interval, self._dag.unit), qubit) + self._apply_scheduled_op( + block_idx, t_start, Delay(time_interval, self._dag.unit), qubit + ) return - def _constrained_length(values): + def _constrained_length(values: np.array) -> np.array: return self._alignment * np.floor(values / self._alignment) # (1) Compute DD intervals satisfying the constraint @@ -332,7 +350,9 @@ def _constrained_length(values): if dd_ind < len(taus): tau = taus[dd_ind] if tau > 0: - self._apply_scheduled_op(block_idx, idle_after, Delay(tau, self._dag.unit), qubit) + self._apply_scheduled_op( + block_idx, idle_after, Delay(tau, self._dag.unit), qubit + ) idle_after += tau if dd_ind < len(self._dd_sequence): gate = self._dd_sequence[dd_ind] @@ -343,12 +363,9 @@ def _constrained_length(values): self._dag.global_phase = self._mod_2pi(self._dag.global_phase + sequence_gphase) @staticmethod - def _mod_2pi(angle: float, atol: float = 0): + def _mod_2pi(angle: float, atol: float = 0) -> float: """Wrap angle into interval [-π,π). If within atol of the endpoint, clamp to -π""" wrapped = (angle + np.pi) % (2 * np.pi) - np.pi if abs(wrapped - np.pi) < atol: wrapped = -np.pi return wrapped - - - diff --git a/test/unit/transpiler/passes/scheduling/test_dynamical_decoupling.py b/test/unit/transpiler/passes/scheduling/test_dynamical_decoupling.py index a4069af26..b67d5af59 100644 --- a/test/unit/transpiler/passes/scheduling/test_dynamical_decoupling.py +++ b/test/unit/transpiler/passes/scheduling/test_dynamical_decoupling.py @@ -12,12 +12,11 @@ """Test dynamical decoupling insertion pass.""" -import unittest import numpy as np from numpy import pi -import qiskit.pulse as pulse from ddt import ddt, data +from qiskit import pulse from qiskit.circuit import QuantumCircuit, Delay from qiskit.circuit.library import XGate, YGate, RXGate, UGate from qiskit.quantum_info import Operator @@ -26,11 +25,16 @@ from qiskit.transpiler.passmanager import PassManager from qiskit.transpiler.exceptions import TranspilerError -from qiskit_ibm_provider.transpiler.passes.scheduling.dynamical_decoupling import PadDynamicalDecoupling +from qiskit_ibm_provider.transpiler.passes.scheduling.dynamical_decoupling import ( + PadDynamicalDecoupling, +) from qiskit_ibm_provider.transpiler.passes.scheduling.scheduler import ( DynamicCircuitScheduleAnalysis, ) +# pylint: disable=invalid-name + + @ddt class TestPadDynamicalDecoupling(QiskitTestCase): """Tests PadDynamicalDecoupling pass.""" @@ -155,7 +159,9 @@ def test_insert_dd_ghz_everywhere(self): pm = PassManager( [ DynamicCircuitScheduleAnalysis(self.durations), - PadDynamicalDecoupling(self.durations, dd_sequence, skip_reset_qubits=False), + PadDynamicalDecoupling( + self.durations, dd_sequence, skip_reset_qubits=False + ), ] ) @@ -231,42 +237,7 @@ def test_insert_dd_ghz_xy4(self): self.assertEqual(ghz4_dd, expected) - def test_insert_midmeas_hahn_alap(self): - """Test a single X gate as Hahn echo can absorb in the downstream circuit. - global phase: 3π/2.""" - dd_sequence = [XGate()] - pm = PassManager( - [ - DynamicCircuitScheduleAnalysis(self.durations), - PadDynamicalDecoupling(self.durations, dd_sequence), - ] - ) - - midmeas_dd = pm.run(self.midmeas) - - combined_u = UGate(0, pi / 2, -pi / 2) - - expected = QuantumCircuit(3, 1) - expected.cx(0, 1) - expected.delay(625, 0) - expected.x(0) - expected.delay(625, 0) - expected.compose(combined_u, [0], inplace=True) - expected.delay(700, 2) - expected.cx(1, 2) - expected.delay(1000, 1) - expected.measure(2, 0) - expected.barrier() - expected.cx(1, 2) - expected.cx(0, 1) - expected.delay(700, 2) - expected.global_phase = 4.71238898038469 - - self.assertEqual(midmeas_dd, expected) - # check the absorption into U was done correctly - self.assertEqual(Operator(combined_u), Operator(XGate()) & Operator(XGate())) - - def test_insert_midmeas_hahn_asap(self): + def test_insert_midmeas_hahn(self): """Test a single X gate as Hahn echo can absorb in the upstream circuit.""" dd_sequence = [RXGate(pi / 4)] pm = PassManager( @@ -283,17 +254,19 @@ def test_insert_midmeas_hahn_asap(self): expected = QuantumCircuit(3, 1) expected.cx(0, 1) expected.compose(combined_u, [0], inplace=True) - expected.delay(600, 0) + expected.delay(500, 0) expected.rx(pi / 4, 0) - expected.delay(600, 0) + expected.delay(500, 0) expected.delay(700, 2) expected.cx(1, 2) expected.delay(1000, 1) expected.measure(2, 0) expected.barrier() + expected.delay(200, 0) expected.cx(1, 2) expected.cx(0, 1) expected.delay(700, 2) + expected.barrier() self.assertEqual(midmeas_dd, expected) # check the absorption into U was done correctly @@ -323,7 +296,9 @@ def uhrig(k): pm = PassManager( [ DynamicCircuitScheduleAnalysis(self.durations), - PadDynamicalDecoupling(self.durations, dd_sequence, qubits=[0], spacing=spacing), + PadDynamicalDecoupling( + self.durations, dd_sequence, qubits=[0], spacing=spacing + ), ] ) @@ -415,13 +390,18 @@ def test_dd_after_reset(self): expected = QuantumCircuit(1) expected.reset(0) - expected.delay(1000) + expected.barrier() + expected.delay(90) + expected.x(0) + expected.delay(810) + expected.x(0) expected.h(0) expected.delay(190, 0) expected.x(0) expected.delay(1710, 0) expected.x(0) expected.h(0) + expected.barrier() t2_dd = pm.run(t2) @@ -452,7 +432,10 @@ def test_dd_with_calibrations_with_parameters(self, param_value): rx_duration = int(param_value * 1000) with pulse.build() as rx: - pulse.play(pulse.Gaussian(rx_duration, 0.1, rx_duration // 4), pulse.DriveChannel(1)) + pulse.play( + pulse.Gaussian(rx_duration, 0.1, rx_duration // 4), + pulse.DriveChannel(1), + ) circ.add_calibration("rx", (1,), rx, params=[param_value]) @@ -460,11 +443,14 @@ def test_dd_with_calibrations_with_parameters(self, param_value): dd_sequence = [XGate(), XGate()] pm = PassManager( - [DynamicCircuitScheduleAnalysis(durations), PadDynamicalDecoupling(durations, dd_sequence)] + [ + DynamicCircuitScheduleAnalysis(durations), + PadDynamicalDecoupling(durations, dd_sequence), + ] ) pm.run(circ) node_start_times = pm.property_set["node_start_time"] - for node, times in node_start_times.items(): + for node, _ in node_start_times.items(): if isinstance(node.op, RXGate): self.assertEqual(node.op.duration, rx_duration) From 2d15470e62baedb25d3273595ee37245ff3b93e4 Mon Sep 17 00:00:00 2001 From: Thomas Alexander Date: Wed, 6 Jul 2022 23:45:11 -0300 Subject: [PATCH 67/70] Add dynamical decoupling c_if test. --- .../scheduling/test_dynamical_decoupling.py | 83 ++++++++++++++----- 1 file changed, 63 insertions(+), 20 deletions(-) diff --git a/test/unit/transpiler/passes/scheduling/test_dynamical_decoupling.py b/test/unit/transpiler/passes/scheduling/test_dynamical_decoupling.py index b67d5af59..6daa5319a 100644 --- a/test/unit/transpiler/passes/scheduling/test_dynamical_decoupling.py +++ b/test/unit/transpiler/passes/scheduling/test_dynamical_decoupling.py @@ -40,26 +40,7 @@ class TestPadDynamicalDecoupling(QiskitTestCase): """Tests PadDynamicalDecoupling pass.""" def setUp(self): - """Circuits to test DD on. - ┌───┐ - q_0: ┤ H ├──■──────────── - └───┘┌─┴─┐ - q_1: ─────┤ X ├──■─────── - └───┘┌─┴─┐ - q_2: ──────────┤ X ├──■── - └───┘┌─┴─┐ - q_3: ───────────────┤ X ├ - └───┘ - ┌──────────┐ - q_0: ──■──┤ U(π,0,π) ├──────────■── - ┌─┴─┐└──────────┘ ┌─┴─┐ - q_1: ┤ X ├─────■───────────■──┤ X ├ - └───┘ ┌─┴─┐ ┌─┐┌─┴─┐└───┘ - q_2: ────────┤ X ├────┤M├┤ X ├───── - └───┘ └╥┘└───┘ - c: 1/══════════════════╩═══════════ - 0 - """ + """Circuits to test dynamical decoupling on.""" super().setUp() self.ghz4 = QuantumCircuit(4) @@ -525,3 +506,65 @@ def test_dd_can_sequentially_called(self): circ2 = pm2.run(self.ghz4) self.assertEqual(circ1, circ2) + + def test_dd_c_if(self): + """Test DD with c_if circuit.""" + + dd_sequence = [XGate(), XGate()] + pm = PassManager( + [ + DynamicCircuitScheduleAnalysis(self.durations), + PadDynamicalDecoupling(self.durations, dd_sequence), + ] + ) + + qc = QuantumCircuit(3, 1) + qc.measure(0, 0) + qc.x(2) + qc.delay(1000, 1) + qc.x(1).c_if(0, True) + qc.delay(800, 1) + qc.x(2).c_if(0, True) + qc.delay(1000, 2) + qc.x(0) + qc.x(2) + + qc_dd = pm.run(qc) + + expected = QuantumCircuit(3, 1) + expected.x(2) + expected.delay(212, 2) + expected.x(2) + expected.delay(426, 2) + expected.x(2) + expected.delay(212, 2) + expected.delay(1000, 1) + expected.measure(0, 0) + expected.barrier() + expected.x(0) + expected.delay(50, 1) + expected.delay(50, 2) + expected.barrier() + expected.x(1).c_if(0, True) + expected.barrier() + expected.x(2).c_if(0, True) + expected.barrier() + expected.delay(237, 0) + expected.x(0) + expected.delay(476, 0) + expected.x(0) + expected.delay(237, 0) + expected.delay(237, 1) + expected.x(1) + expected.delay(476, 1) + expected.x(1) + expected.delay(237, 1) + expected.delay(225, 2) + expected.x(2) + expected.delay(450, 2) + expected.x(2) + expected.delay(225, 2) + expected.x(2) + expected.barrier() + + self.assertEqual(expected, qc_dd) From f9be27464f22fa92c829ffa4965888b38d7166e7 Mon Sep 17 00:00:00 2001 From: Thomas Alexander Date: Thu, 7 Jul 2022 00:07:06 -0300 Subject: [PATCH 68/70] Add working documentation. --- .../transpiler/passes/__init__.py | 1 + .../transpiler/passes/scheduling/__init__.py | 31 +++++++++++++++++-- .../passes/scheduling/dynamical_decoupling.py | 21 ++++++++++--- 3 files changed, 46 insertions(+), 7 deletions(-) diff --git a/qiskit_ibm_provider/transpiler/passes/__init__.py b/qiskit_ibm_provider/transpiler/passes/__init__.py index 651f22dd5..8da268d58 100644 --- a/qiskit_ibm_provider/transpiler/passes/__init__.py +++ b/qiskit_ibm_provider/transpiler/passes/__init__.py @@ -29,4 +29,5 @@ # circuit scheduling from .scheduling import DynamicCircuitScheduleAnalysis +from .scheduling import PadDynamicalDecoupling from .scheduling import PadDelay diff --git a/qiskit_ibm_provider/transpiler/passes/scheduling/__init__.py b/qiskit_ibm_provider/transpiler/passes/scheduling/__init__.py index cdbd52fdf..4661059e2 100644 --- a/qiskit_ibm_provider/transpiler/passes/scheduling/__init__.py +++ b/qiskit_ibm_provider/transpiler/passes/scheduling/__init__.py @@ -15,6 +15,8 @@ Scheduling (:mod:`qiskit_ibm_provider.transpiler.passes.scheduling`) ==================================================================== +.. currentmodule:: qiskit_ibm_provider.transpiler.passes.scheduling + A collection of scheduling passes for working with IBM Quantum's next-generation backends that support advanced "dynamic circuit" capabilities. Ie., circuits with support for classical control-flow/feedback based off @@ -22,8 +24,7 @@ Below we demonstrate how to schedule and pad a teleportation circuit with delays -for a dynamic circuit backend's execution model - +for a dynamic circuit backend's execution model: .. jupyter-execute:: @@ -65,6 +66,30 @@ scheduled_teleport.draw(output="mpl") +Instead of padding with delays we may also insert a dynamical decoupling sequence +using the :class:`PadDynamicalDecoupling` pass as shown below: + +.. jupyter-execute:: + + from qiskit.circuit.library import XGate + + from qiskit_ibm_provider.transpiler.passes.scheduling import PadDynamicalDecoupling + + + dd_sequence = [XGate(), XGate()] + + pm = PassManager( + [ + DynamicCircuitScheduleAnalysis(durations), + PadDynamicalDecoupling(durations, dd_sequence), + ] + ) + + dd_teleport = pm.run(teleport) + + dd_teleport.draw(output="mpl") + + Scheduling & Dynamical Decoupling ================================= .. autosummary:: @@ -73,11 +98,13 @@ BlockBasePadder DynamicCircuitScheduleAnalysis PadDelay + PadDynamicalDecoupling """ from .block_base_padder import BlockBasePadder +from .dynamical_decoupling import PadDynamicalDecoupling from .pad_delay import PadDelay from .scheduler import DynamicCircuitScheduleAnalysis diff --git a/qiskit_ibm_provider/transpiler/passes/scheduling/dynamical_decoupling.py b/qiskit_ibm_provider/transpiler/passes/scheduling/dynamical_decoupling.py index f591fb880..c7ca05ca0 100644 --- a/qiskit_ibm_provider/transpiler/passes/scheduling/dynamical_decoupling.py +++ b/qiskit_ibm_provider/transpiler/passes/scheduling/dynamical_decoupling.py @@ -44,7 +44,9 @@ class PadDynamicalDecoupling(BlockBasePadder): This can be used, for instance, as a Hahn echo. This pass ensures that the inserted sequence preserves the circuit exactly (including global phase). + .. jupyter-execute:: + import numpy as np from qiskit.circuit import QuantumCircuit from qiskit.circuit.library import XGate @@ -65,14 +67,18 @@ class PadDynamicalDecoupling(BlockBasePadder): ("cx", [1, 2], 200), ("cx", [2, 3], 300), ("x", None, 50), ("measure", None, 1000)] ) + .. jupyter-execute:: + # balanced X-X sequence on all qubits dd_sequence = [XGate(), XGate()] pm = PassManager([DynamicCircuitScheduleAnalysis(durations), PadDynamicalDecoupling(durations, dd_sequence)]) circ_dd = pm.run(circ) - timeline_drawer(circ_dd) + circ_dd.draw() + .. jupyter-execute:: + # Uhrig sequence on qubit 0 n = 8 dd_sequence = [XGate()] * n @@ -84,13 +90,15 @@ def uhrig_pulse_location(k): spacing.append(1 - sum(spacing)) pm = PassManager( [ - ALAPScheduleAnalysis(durations), + DynamicCircuitScheduleAnalysis(durations), PadDynamicalDecoupling(durations, dd_sequence, qubits=[0], spacing=spacing), ] ) circ_dd = pm.run(circ) - timeline_drawer(circ_dd) + circ_dd.draw() + .. note:: + You need to call :class:`~qiskit_ibm_provider.transpiler.passes.scheduling.DynamicCircuitScheduleAnalysis` before running dynamical decoupling to guarantee your circuit satisfies acquisition @@ -108,6 +116,7 @@ def __init__( extra_slack_distribution: str = "middle", ): """Dynamical decoupling initializer. + Args: durations: Durations of instructions to be used in scheduling. dd_sequence: Sequence of gates to apply in idle spots. @@ -133,14 +142,16 @@ def __init__( the created sequence being shorter than the idle time that you want to fill with the sequence, i.e. `extra slack`. This option takes following values. - - "middle": Put the extra slack to the interval at the middle of the sequence. - - "edges": Divide the extra slack as evenly as possible into + + * "middle": Put the extra slack to the interval at the middle of the sequence. + * "edges": Divide the extra slack as evenly as possible into intervals at beginning and end of the sequence. Raises: TranspilerError: When invalid DD sequence is specified. TranspilerError: When pulse gate with the duration which is non-multiple of the alignment constraint value is found. """ + super().__init__() self._durations = durations self._dd_sequence = dd_sequence From 8f1ac4fb9f3db7de9d5b934a1a74f7ca22969be9 Mon Sep 17 00:00:00 2001 From: Thomas Alexander Date: Thu, 7 Jul 2022 00:12:32 -0300 Subject: [PATCH 69/70] Reno added. --- .../add-dynamical-decoupling-77b0591e95e96da8.yaml | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 releasenotes/notes/add-dynamical-decoupling-77b0591e95e96da8.yaml diff --git a/releasenotes/notes/add-dynamical-decoupling-77b0591e95e96da8.yaml b/releasenotes/notes/add-dynamical-decoupling-77b0591e95e96da8.yaml new file mode 100644 index 000000000..ccda01252 --- /dev/null +++ b/releasenotes/notes/add-dynamical-decoupling-77b0591e95e96da8.yaml @@ -0,0 +1,11 @@ +--- +features: + - | + A dynamical decoupling pass has been added for IBM Quantum dynamic circuit backends + :class:`~qiskit_ibm_provider.transpiler.passes.scheduling.PadDynamicalDecoupling` + to pad schedulable sections of a circuit with dynamical decoupling sequences. + It relies on having run the + :class:`~qiskit_ibm_provider.transpiler.passes.scheduling.DynamicCircuitScheduleAnalysis` + analysis prior to the padding sequence. + + For an example see the :mod:`~qiskit_ibm_provider.transpiler.passes.scheduling` module's documentation. From 065b71dbf654d573a63bc8cdc26da539ffc4f4c2 Mon Sep 17 00:00:00 2001 From: Thomas Alexander Date: Wed, 10 Aug 2022 23:48:49 -0300 Subject: [PATCH 70/70] Add quick comment. --- .../transpiler/passes/scheduling/dynamical_decoupling.py | 1 + 1 file changed, 1 insertion(+) diff --git a/qiskit_ibm_provider/transpiler/passes/scheduling/dynamical_decoupling.py b/qiskit_ibm_provider/transpiler/passes/scheduling/dynamical_decoupling.py index c7ca05ca0..9eb843a6a 100644 --- a/qiskit_ibm_provider/transpiler/passes/scheduling/dynamical_decoupling.py +++ b/qiskit_ibm_provider/transpiler/passes/scheduling/dynamical_decoupling.py @@ -191,6 +191,7 @@ def _pre_runhook(self, dag: DAGCircuit) -> None: raise TranspilerError( "DD sequence must contain an even number of gates (or 1)." ) + # TODO: this check should use the quantum info package in Qiskit. noop = np.eye(2) for gate in self._dd_sequence: noop = noop.dot(gate.to_matrix())