From 8095ace7ea90d144bd73cdc7745ecfc2740cc2b0 Mon Sep 17 00:00:00 2001 From: Kevin Hartman Date: Tue, 4 Feb 2025 10:23:50 -0500 Subject: [PATCH 01/48] Make sure that 'tox -erust' fails on bad RC. (#13753) * Make sure that 'tox -erust' fails on bad RC. Previously, a non-zero exit code from the subprocess would not fail the tox run like we'd expect. * Use script to run cargo test, simplify subprocess call. * Use PYTHONPATH instead of PYTHONUSERBASE. This fixes an issue where the site-packages added to sys.path could be wrong (e.g. if using Framework-based Python on macOS). This was happening because the venv would install qiskit using one pathing scheme while the Python interpreter baked into the test executable would use (potentially) another when generating the path from PYTHONUSERBASE. See https://docs.python.org/3/library/sysconfig.html#installation-paths for details. To resolve this, we just set the path to the venv's site-packages via PYTHONPATH. * Add CWD to search path when running run_cargo_test.py * Update docs. --- .azure/test-linux.yml | 13 +++---------- CONTRIBUTING.md | 18 +++-------------- tools/run_cargo_test.py | 43 +++++++++++++++++++++++++++++++++++++++++ tox.ini | 8 +------- 4 files changed, 50 insertions(+), 32 deletions(-) create mode 100755 tools/run_cargo_test.py diff --git a/.azure/test-linux.yml b/.azure/test-linux.yml index 2be97d1d30ad..374c3efc2051 100644 --- a/.azure/test-linux.yml +++ b/.azure/test-linux.yml @@ -96,20 +96,13 @@ jobs: sudo apt-get install -y graphviz displayName: 'Install optional non-Python dependencies' - # Note that we explicitly use the virtual env with Qiskit installed to run the Rust - # tests since some of them still depend on Qiskit's Python API via PyO3. - ${{ if eq(parameters.testRust, true) }}: - # We need to avoid linking our crates into full Python extension libraries during Rust-only - # testing because Rust/PyO3 can't handle finding a static CPython interpreter. + # Note that we use the virtual env with Qiskit installed to run the Rust + # tests since some of them still depend on Qiskit's Python API via PyO3. - bash: | source test-job/bin/activate python tools/report_numpy_state.py - PYTHONUSERBASE="$VIRTUAL_ENV" cargo test --no-default-features - env: - # On Linux we link against `libpython` dynamically, but it isn't written into the rpath - # of the test executable (I'm not 100% sure why ---Jake). It's easiest just to forcibly - # include the correct place in the `dlopen` search path. - LD_LIBRARY_PATH: '$(usePython.pythonLocation)/lib:$LD_LIBRARY_PATH' + python tools/run_cargo_test.py displayName: "Run Rust tests" - bash: | diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 02300493183e..b2fa38177412 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -619,25 +619,13 @@ Then, run the following commands: ```bash python setup.py build_rust --inplace -PYTHONUSERBASE="$VIRTUAL_ENV" cargo test --no-default-features +tools/run_cargo_test.py ``` -> [!IMPORTANT] -> On Linux, you may need to first set your `LD_LIBRARY_PATH` env var to include the -> path to your Python installation's shared lib, e.g.: -> ```bash -> export LD_LIBRARY_PATH="$(python -c 'import sysconfig; print(sysconfig.get_config_var("LIBDIR"))'):$LD_LIBRARY_PATH" -> ``` - The first command builds Qiskit in editable mode, which ensures that Rust tests that interact with Qiskit's Python code actually -use the latest Python code from your working directory. - -The second command actually invokes the tests via Cargo. The `PYTHONUSERBASE` -environment variable tells the embedded Python interpreter to look for packages -in your active virtual environment. The `--no-default-features` -flag is used to compile an isolated test runner without building a linked CPython -extension module (which would otherwise cause linker failures). +use the latest Python code from your working directory. The second command invokes +the tests via Cargo. #### Calling Python from Rust tests By default, our Cargo project configuration allows Rust tests to interact with the diff --git a/tools/run_cargo_test.py b/tools/run_cargo_test.py new file mode 100755 index 000000000000..0866b6952639 --- /dev/null +++ b/tools/run_cargo_test.py @@ -0,0 +1,43 @@ +#!/usr/bin/env python3 + +# This code is part of Qiskit. +# +# (C) Copyright IBM 2025. +# +# 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. + +""" +Utility script to invoke cargo test within the current Python environment. + +Notably, this sets up the environment variables necessary for Qiskit to be +found by PyO3 / our Rust test executable. +""" + +import os +import subprocess +import site +import sysconfig + +# This allows the Python interpreter baked into our test executable to find the +# Qiskit installed in the active environment. +os.environ["PYTHONPATH"] = os.pathsep.join([os.getcwd()] + site.getsitepackages()) + +# Uncomment to debug PyO3's build / link against Python. +# os.environ["PYO3_PRINT_CONFIG"] = "1" + +# On Linux, the test executable's RPATH doesn't contain libpython, so we add it +# to the dlopen search path here. +os.environ["LD_LIBRARY_PATH"] = os.pathsep.join( + filter(None, [sysconfig.get_config_var("LIBDIR"), os.getenv("LD_LIBRARY_PATH")]) +) + +# The '--no-default-features' flag is used here to disable PyO3's +# 'extension-module' when running the tests (which would otherwise cause link +# errors). +subprocess.run(["cargo", "test", "--no-default-features"], check=True) diff --git a/tox.ini b/tox.ini index 2809057a2ed6..c7e852c6ee0a 100644 --- a/tox.ini +++ b/tox.ini @@ -34,14 +34,8 @@ commands = [testenv:rust] basepython = python3 package_env = .pkg-rust -setenv = - PYTHONUSERBASE={envdir} allowlist_externals = cargo -commands = - python -c '\ - import os, subprocess, sysconfig;\ - os.environ["LD_LIBRARY_PATH"] = os.pathsep.join(filter(None, [sysconfig.get_config_var("LIBDIR"), os.getenv("LD_LIBRARY_PATH")]));\ - subprocess.run(["cargo", "test", "--no-default-features"])' +commands = python tools/run_cargo_test.py # This is a special package_env used by the 'rust' env above # to force Qiskit's Rust code to build in debug mode. We do this From 69bb4391ba7290fbb96c23efe493487233ceeda1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elena=20Pe=C3=B1a=20Tapia?= <57907331+ElePT@users.noreply.github.com> Date: Wed, 5 Feb 2025 10:26:42 +0100 Subject: [PATCH 02/48] Remove `assemble` and `assembler` module (#13748) * Remove assembler module and assemble function (deprecated) * Make lint happy * Add reno and missing files * Apply suggestions from Eli's code review Co-authored-by: Eli Arbel <46826214+eliarbel@users.noreply.github.com> --------- Co-authored-by: Eli Arbel <46826214+eliarbel@users.noreply.github.com> --- docs/apidoc/assembler.rst | 6 - docs/apidoc/index.rst | 1 - qiskit/__init__.py | 3 +- qiskit/assembler/__init__.py | 42 - qiskit/assembler/assemble_circuits.py | 453 ---- qiskit/assembler/assemble_schedules.py | 369 --- qiskit/assembler/disassemble.py | 312 --- qiskit/assembler/run_config.py | 77 - qiskit/circuit/controlflow/builder.py | 3 - qiskit/circuit/instruction.py | 37 - qiskit/compiler/__init__.py | 2 - qiskit/compiler/assembler.py | 681 ------ .../remove-assemble-2d5d9cea4ca504f5.yaml | 18 + ...tion-basic-simulator-cac1c2783a5a4e25.yaml | 2 +- test/benchmarks/transpiler_benchmarks.py | 9 +- test/python/circuit/test_diagonal_gate.py | 10 +- test/python/circuit/test_hamiltonian_gate.py | 5 +- test/python/circuit/test_initializer.py | 8 - test/python/circuit/test_parameters.py | 7 +- test/python/circuit/test_scheduled_circuit.py | 27 +- test/python/circuit/test_unitary.py | 49 - test/python/compiler/test_assembler.py | 2108 ----------------- test/python/compiler/test_compiler.py | 50 +- test/python/compiler/test_disassembler.py | 599 ----- test/python/providers/test_fake_backends.py | 10 - test/python/qobj/test_qobj.py | 23 - test/python/qobj/test_qobj_identifiers.py | 47 - 27 files changed, 35 insertions(+), 4923 deletions(-) delete mode 100644 docs/apidoc/assembler.rst delete mode 100644 qiskit/assembler/__init__.py delete mode 100644 qiskit/assembler/assemble_circuits.py delete mode 100644 qiskit/assembler/assemble_schedules.py delete mode 100644 qiskit/assembler/disassemble.py delete mode 100644 qiskit/assembler/run_config.py delete mode 100644 qiskit/compiler/assembler.py create mode 100644 releasenotes/notes/remove-assemble-2d5d9cea4ca504f5.yaml delete mode 100644 test/python/compiler/test_assembler.py delete mode 100644 test/python/compiler/test_disassembler.py delete mode 100644 test/python/qobj/test_qobj_identifiers.py diff --git a/docs/apidoc/assembler.rst b/docs/apidoc/assembler.rst deleted file mode 100644 index 932b67e450af..000000000000 --- a/docs/apidoc/assembler.rst +++ /dev/null @@ -1,6 +0,0 @@ -.. _qiskit-assembler: - -.. automodule:: qiskit.assembler - :no-members: - :no-inherited-members: - :no-special-members: diff --git a/docs/apidoc/index.rst b/docs/apidoc/index.rst index 8581d56ace77..d0ec14a416e4 100644 --- a/docs/apidoc/index.rst +++ b/docs/apidoc/index.rst @@ -83,7 +83,6 @@ Other: .. toctree:: :maxdepth: 1 - assembler compiler exceptions qobj diff --git a/qiskit/__init__.py b/qiskit/__init__.py index af543bea80c7..c121c99112f8 100644 --- a/qiskit/__init__.py +++ b/qiskit/__init__.py @@ -127,7 +127,7 @@ _config = _user_config.get_config() -from qiskit.compiler import transpile, assemble, schedule, sequence +from qiskit.compiler import transpile, schedule, sequence from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager from .version import __version__ @@ -138,7 +138,6 @@ "QiskitError", "QuantumCircuit", "QuantumRegister", - "assemble", "schedule", "sequence", "transpile", diff --git a/qiskit/assembler/__init__.py b/qiskit/assembler/__init__.py deleted file mode 100644 index 45798084ea62..000000000000 --- a/qiskit/assembler/__init__.py +++ /dev/null @@ -1,42 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2017, 2019. -# -# 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. - -""" -======================================================== -Circuit and Schedule Assembler (:mod:`qiskit.assembler`) -======================================================== - -.. currentmodule:: qiskit.assembler - -Functions -========= - - -.. autofunction:: assemble_circuits - -.. autofunction:: assemble_schedules - -.. autofunction:: disassemble - -Classes -======= - -.. autosummary:: - :toctree: ../stubs/ - - RunConfig -""" - -from qiskit.assembler.assemble_circuits import assemble_circuits -from qiskit.assembler.assemble_schedules import assemble_schedules -from qiskit.assembler.disassemble import disassemble -from qiskit.assembler.run_config import RunConfig diff --git a/qiskit/assembler/assemble_circuits.py b/qiskit/assembler/assemble_circuits.py deleted file mode 100644 index 0656bae0778e..000000000000 --- a/qiskit/assembler/assemble_circuits.py +++ /dev/null @@ -1,453 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2017, 2019. -# -# 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. - -"""Assemble function for converting a list of circuits into a qobj.""" -import copy -import warnings -from collections import defaultdict -from typing import Dict, List, Optional, Tuple - -from qiskit.assembler.run_config import RunConfig -from qiskit.assembler.assemble_schedules import _assemble_instructions as _assemble_schedule -from qiskit.circuit import QuantumCircuit -from qiskit.circuit.classicalregister import Clbit -from qiskit.exceptions import QiskitError -from qiskit.qobj import ( - QasmQobj, - QobjExperimentHeader, - QasmQobjInstruction, - QasmQobjExperimentConfig, - QasmQobjExperiment, - QasmQobjConfig, - QasmExperimentCalibrations, - GateCalibration, - PulseQobjInstruction, - PulseLibraryItem, - converters, - QobjHeader, -) -from qiskit.utils.parallel import parallel_map -from qiskit.utils import deprecate_func - - -PulseLibrary = Dict[str, List[complex]] - - -def _assemble_circuit( - circuit: QuantumCircuit, run_config: RunConfig -) -> Tuple[QasmQobjExperiment, Optional[PulseLibrary]]: - """Assemble one circuit. - - Args: - circuit: circuit to assemble - run_config: configuration of the runtime environment - - Returns: - One experiment for the QasmQobj, and pulse library for pulse gates (which could be None) - - Raises: - QiskitError: when the circuit has unit other than 'dt'. - """ - if circuit.unit != "dt": - raise QiskitError( - f"Unable to assemble circuit with unit '{circuit.unit}', which must be 'dt'." - ) - - # header data - num_qubits = 0 - memory_slots = 0 - qubit_labels = [] - clbit_labels = [] - - qreg_sizes = [] - creg_sizes = [] - for qreg in circuit.qregs: - qreg_sizes.append([qreg.name, qreg.size]) - for j in range(qreg.size): - qubit_labels.append([qreg.name, j]) - num_qubits += qreg.size - for creg in circuit.cregs: - creg_sizes.append([creg.name, creg.size]) - for j in range(creg.size): - clbit_labels.append([creg.name, j]) - memory_slots += creg.size - - qubit_indices = {qubit: idx for idx, qubit in enumerate(circuit.qubits)} - clbit_indices = {clbit: idx for idx, clbit in enumerate(circuit.clbits)} - - # TODO: why do we need creq_sizes and qreg_sizes in header - # TODO: we need to rethink memory_slots as they are tied to classical bit - metadata = circuit.metadata - if metadata is None: - metadata = {} - with warnings.catch_warnings(): - # The class QobjExperimentHeader is deprecated - warnings.filterwarnings("ignore", category=DeprecationWarning, module="qiskit") - header = QobjExperimentHeader( - qubit_labels=qubit_labels, - n_qubits=num_qubits, - qreg_sizes=qreg_sizes, - clbit_labels=clbit_labels, - memory_slots=memory_slots, - creg_sizes=creg_sizes, - name=circuit.name, - global_phase=float(circuit.global_phase), - metadata=metadata, - ) - - # TODO: why do we need n_qubits and memory_slots in both the header and the config - with warnings.catch_warnings(): - # The class QasmQobjExperimentConfig is deprecated - warnings.filterwarnings("ignore", category=DeprecationWarning, module="qiskit") - config = QasmQobjExperimentConfig(n_qubits=num_qubits, memory_slots=memory_slots) - calibrations, pulse_library = _assemble_pulse_gates(circuit, run_config) - if calibrations: - config.calibrations = calibrations - - # Convert conditionals from OpenQASM-2-style (creg ?= int) to qobj-style - # (register_bit ?= 1), by assuming device has unlimited register slots - # (supported only for simulators). Map all measures to a register matching - # their clbit_index, create a new register slot for every conditional gate - # and add a bfunc to map the creg=val mask onto the gating register bit. - - is_conditional_experiment = any( - getattr(instruction.operation, "condition", None) for instruction in circuit.data - ) - max_conditional_idx = 0 - - instructions = [] - for op_context in circuit.data: - instruction = op_context.operation._assemble() - - # Add register attributes to the instruction - qargs = op_context.qubits - cargs = op_context.clbits - if qargs: - instruction.qubits = [qubit_indices[qubit] for qubit in qargs] - if cargs: - instruction.memory = [clbit_indices[clbit] for clbit in cargs] - # If the experiment has conditional instructions, assume every - # measurement result may be needed for a conditional gate. - if instruction.name == "measure" and is_conditional_experiment: - instruction.register = [clbit_indices[clbit] for clbit in cargs] - - # To convert to a qobj-style conditional, insert a bfunc prior - # to the conditional instruction to map the creg ?= val condition - # onto a gating register bit. - if hasattr(instruction, "_condition"): - ctrl_reg, ctrl_val = instruction._condition - mask = 0 - val = 0 - if isinstance(ctrl_reg, Clbit): - mask = 1 << clbit_indices[ctrl_reg] - val = (ctrl_val & 1) << clbit_indices[ctrl_reg] - else: - for clbit in clbit_indices: - if clbit in ctrl_reg: - mask |= 1 << clbit_indices[clbit] - val |= ((ctrl_val >> list(ctrl_reg).index(clbit)) & 1) << clbit_indices[ - clbit - ] - - conditional_reg_idx = memory_slots + max_conditional_idx - with warnings.catch_warnings(): - # The class QasmQobjInstruction is deprecated - warnings.filterwarnings("ignore", category=DeprecationWarning, module="qiskit") - conversion_bfunc = QasmQobjInstruction( - name="bfunc", - mask="0x%X" % mask, # pylint: disable=consider-using-f-string - relation="==", - val="0x%X" % val, # pylint: disable=consider-using-f-string - register=conditional_reg_idx, - ) - instructions.append(conversion_bfunc) - instruction.conditional = conditional_reg_idx - max_conditional_idx += 1 - # Delete condition attribute now that we have replaced it with - # the conditional and bfunc - del instruction._condition - - instructions.append(instruction) - with warnings.catch_warnings(): - # The class QasmQobjExperiment is deprecated - warnings.filterwarnings("ignore", category=DeprecationWarning, module="qiskit") - return ( - QasmQobjExperiment(instructions=instructions, header=header, config=config), - pulse_library, - ) - - -def _assemble_pulse_gates( - circuit: QuantumCircuit, run_config: RunConfig -) -> Tuple[Optional[QasmExperimentCalibrations], Optional[PulseLibrary]]: - """Assemble and return the circuit calibrations and associated pulse library, if there are any. - The calibrations themselves may reference the pulse library which is returned as a dict. - - Args: - circuit: circuit which may have pulse calibrations - run_config: configuration of the runtime environment - - Returns: - The calibrations and pulse library, if there are any - """ - if not circuit.calibrations: - return None, None - if not hasattr(run_config, "parametric_pulses"): - run_config.parametric_pulses = [] - calibrations = [] - pulse_library = {} - for gate, cals in circuit.calibrations.items(): - for (qubits, params), schedule in cals.items(): - qobj_instructions, _ = _assemble_schedule( - schedule, - converters.InstructionToQobjConverter(PulseQobjInstruction), - run_config, - pulse_library, - ) - calibrations.append( - GateCalibration(str(gate), list(qubits), list(params), qobj_instructions) - ) - return QasmExperimentCalibrations(gates=calibrations), pulse_library - - -def _extract_common_calibrations( - experiments: List[QasmQobjExperiment], -) -> Tuple[List[QasmQobjExperiment], Optional[QasmExperimentCalibrations]]: - """Given a list of ``QasmQobjExperiment``s, each of which may have calibrations in their - ``config``, collect common calibrations into a global ``QasmExperimentCalibrations`` - and delete them from their local experiments. - - Args: - experiments: The list of OpenQASM experiments that are being assembled into one qobj - - Returns: - The input experiments with modified calibrations, and common calibrations, if there - are any - """ - - def index_calibrations() -> Dict[int, List[Tuple[int, GateCalibration]]]: - """Map each calibration to all experiments that contain it.""" - exp_indices = defaultdict(list) - for exp_idx, exp in enumerate(experiments): - for gate_cal in exp.config.calibrations.gates: - # They must be keyed on the hash or identical cals will be indexed separately - exp_indices[hash(gate_cal)].append((exp_idx, gate_cal)) - return exp_indices - - def collect_common_calibrations() -> List[GateCalibration]: - """If a gate calibration appears in all experiments, collect it.""" - common_calibrations = [] - for _, exps_w_cal in exp_indices.items(): - if len(exps_w_cal) == len(experiments): - _, gate_cal = exps_w_cal[0] - common_calibrations.append(gate_cal) - return common_calibrations - - def remove_common_gate_calibrations(exps: List[QasmQobjExperiment]) -> None: - """For calibrations that appear in all experiments, remove them from the individual - experiment's ``config.calibrations``.""" - for _, exps_w_cal in exp_indices.items(): - if len(exps_w_cal) == len(exps): - for exp_idx, gate_cal in exps_w_cal: - exps[exp_idx].config.calibrations.gates.remove(gate_cal) - - if not (experiments and all(hasattr(exp.config, "calibrations") for exp in experiments)): - # No common calibrations - return experiments, None - - exp_indices = index_calibrations() - common_calibrations = collect_common_calibrations() - remove_common_gate_calibrations(experiments) - - # Remove the ``calibrations`` attribute if it's now empty - for exp in experiments: - if not exp.config.calibrations.gates: - del exp.config.calibrations - - return experiments, QasmExperimentCalibrations(gates=common_calibrations) - - -def _configure_experiment_los( - experiments: List[QasmQobjExperiment], - lo_converter: converters.LoConfigConverter, - run_config: RunConfig, -): - # get per experiment los - freq_configs = [lo_converter(lo_dict) for lo_dict in getattr(run_config, "schedule_los", [])] - - if len(experiments) > 1 and len(freq_configs) not in [0, 1, len(experiments)]: - raise QiskitError( - "Invalid 'schedule_los' setting specified. If specified, it should be " - "either have a single entry to apply the same LOs for each experiment or " - "have length equal to the number of experiments." - ) - - if len(freq_configs) > 1: - if len(experiments) > 1: - for idx, expt in enumerate(experiments): - freq_config = freq_configs[idx] - expt.config.qubit_lo_freq = freq_config.qubit_lo_freq - expt.config.meas_lo_freq = freq_config.meas_lo_freq - elif len(experiments) == 1: - expt = experiments[0] - experiments = [] - for freq_config in freq_configs: - expt_config = copy.deepcopy(expt.config) - expt_config.qubit_lo_freq = freq_config.qubit_lo_freq - expt_config.meas_lo_freq = freq_config.meas_lo_freq - experiments.append( - QasmQobjExperiment( - header=expt.header, instructions=expt.instructions, config=expt_config - ) - ) - - return experiments - - -def _assemble_circuits( - circuits: List[QuantumCircuit], run_config: RunConfig, qobj_id: int, qobj_header: QobjHeader -) -> QasmQobj: - with warnings.catch_warnings(): - # Still constructs Qobj, that is deprecated. The message is hard to trace to a module, - # because concurrency is hard. - warnings.filterwarnings("ignore", category=DeprecationWarning) - experiments_and_pulse_libs = parallel_map(_assemble_circuit, circuits, [run_config]) - experiments = [] - pulse_library = {} - for exp, lib in experiments_and_pulse_libs: - experiments.append(exp) - if lib: - pulse_library.update(lib) - - # extract common calibrations - experiments, calibrations = _extract_common_calibrations(experiments) - - # configure LO freqs per circuit - with warnings.catch_warnings(): - warnings.filterwarnings("ignore", category=DeprecationWarning, module="qiskit") - lo_converter = converters.LoConfigConverter( - QasmQobjExperimentConfig, **run_config.to_dict() - ) - experiments = _configure_experiment_los(experiments, lo_converter, run_config) - - with warnings.catch_warnings(): - warnings.filterwarnings("ignore", category=DeprecationWarning, module="qiskit") - qobj_config = QasmQobjConfig() - if run_config: - qobj_config_dict = run_config.to_dict() - - # remove LO ranges, not needed in qobj - qobj_config_dict.pop("qubit_lo_range", None) - qobj_config_dict.pop("meas_lo_range", None) - - # convert LO frequencies to GHz, if they exist - if "qubit_lo_freq" in qobj_config_dict: - qobj_config_dict["qubit_lo_freq"] = [ - freq / 1e9 for freq in qobj_config_dict["qubit_lo_freq"] - ] - if "meas_lo_freq" in qobj_config_dict: - qobj_config_dict["meas_lo_freq"] = [ - freq / 1e9 for freq in qobj_config_dict["meas_lo_freq"] - ] - - # override default los if single ``schedule_los`` entry set - schedule_los = qobj_config_dict.pop("schedule_los", []) - if len(schedule_los) == 1: - lo_dict = schedule_los[0] - q_los = lo_converter.get_qubit_los(lo_dict) - # Hz -> GHz - if q_los: - qobj_config_dict["qubit_lo_freq"] = [freq / 1e9 for freq in q_los] - m_los = lo_converter.get_meas_los(lo_dict) - if m_los: - qobj_config_dict["meas_lo_freq"] = [freq / 1e9 for freq in m_los] - - with warnings.catch_warnings(): - # The class QasmQobjConfig is deprecated - warnings.filterwarnings("ignore", category=DeprecationWarning, module="qiskit") - qobj_config = QasmQobjConfig(**qobj_config_dict) - - qubit_sizes = [] - memory_slot_sizes = [] - for circ in circuits: - num_qubits = 0 - memory_slots = 0 - for qreg in circ.qregs: - num_qubits += qreg.size - for creg in circ.cregs: - memory_slots += creg.size - qubit_sizes.append(num_qubits) - memory_slot_sizes.append(memory_slots) - qobj_config.memory_slots = max(memory_slot_sizes) - qobj_config.n_qubits = max(qubit_sizes) - - if pulse_library: - qobj_config.pulse_library = [ - PulseLibraryItem(name=name, samples=samples) for name, samples in pulse_library.items() - ] - - if calibrations and calibrations.gates: - qobj_config.calibrations = calibrations - with warnings.catch_warnings(): - # The class QasmQobj is deprecated - warnings.filterwarnings("ignore", category=DeprecationWarning, module="qiskit") - return QasmQobj( - qobj_id=qobj_id, config=qobj_config, experiments=experiments, header=qobj_header - ) - - -@deprecate_func( - since="1.2", - removal_timeline="in the 2.0 release", - additional_msg="The `Qobj` class and related functionality are part of the deprecated `BackendV1` " - "workflow, and no longer necessary for `BackendV2`. If a user workflow requires " - "`Qobj` it likely relies on deprecated functionality and should be updated to " - "use `BackendV2`.", -) -def assemble_circuits( - circuits: List[QuantumCircuit], run_config: RunConfig, qobj_id: int, qobj_header: QobjHeader -) -> QasmQobj: - """Assembles a list of circuits into a qobj that can be run on the backend. - - Args: - circuits: circuit(s) to assemble - run_config: configuration of the runtime environment - qobj_id: identifier for the generated qobj - qobj_header: header to pass to the results - - Returns: - The qobj to be run on the backends - - Examples: - - .. plot:: - :include-source: - :nofigs: - - from qiskit.circuit import QuantumRegister, ClassicalRegister, QuantumCircuit - from qiskit.assembler import assemble_circuits - from qiskit.assembler.run_config import RunConfig - # Build a circuit to convert into a Qobj - q = QuantumRegister(2) - c = ClassicalRegister(2) - qc = QuantumCircuit(q, c) - qc.h(q[0]) - qc.cx(q[0], q[1]) - qc.measure(q, c) - # Assemble a Qobj from the input circuit - qobj = assemble_circuits(circuits=[qc], - qobj_id="custom-id", - qobj_header=[], - run_config=RunConfig(shots=2000, memory=True, init_qubits=True)) - """ - # assemble the circuit experiments - return _assemble_circuits(circuits, run_config, qobj_id, qobj_header) diff --git a/qiskit/assembler/assemble_schedules.py b/qiskit/assembler/assemble_schedules.py deleted file mode 100644 index 5a9fae4c79b6..000000000000 --- a/qiskit/assembler/assemble_schedules.py +++ /dev/null @@ -1,369 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2017, 2019. -# -# 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. - - -"""Assemble function for converting a list of circuits into a qobj.""" -import hashlib -from collections import defaultdict -from typing import Any, Dict, List, Tuple, Union - -from qiskit import qobj, pulse -from qiskit.assembler.run_config import RunConfig -from qiskit.exceptions import QiskitError -from qiskit.pulse import instructions, transforms, library, schedule, channels -from qiskit.qobj import utils as qobj_utils, converters -from qiskit.qobj.converters.pulse_instruction import ParametricPulseShapes -from qiskit.utils.deprecate_pulse import deprecate_pulse_dependency - - -@deprecate_pulse_dependency -def assemble_schedules( - schedules: List[ - Union[ - schedule.ScheduleBlock, - schedule.ScheduleComponent, - Tuple[int, schedule.ScheduleComponent], - ] - ], - qobj_id: int, - qobj_header: qobj.QobjHeader, - run_config: RunConfig, -) -> qobj.PulseQobj: - """Assembles a list of schedules into a qobj that can be run on the backend. - - Args: - schedules: Schedules to assemble. - qobj_id: Identifier for the generated qobj. - qobj_header: Header to pass to the results. - run_config: Configuration of the runtime environment. - - Returns: - The Qobj to be run on the backends. - - Raises: - QiskitError: when frequency settings are not supplied. - - Examples: - - .. plot:: - :include-source: - :nofigs: - - from qiskit import pulse - from qiskit.assembler import assemble_schedules - from qiskit.assembler.run_config import RunConfig - # Construct a Qobj header for the output Qobj - header = {"backend_name": "FakeOpenPulse2Q", "backend_version": "0.0.0"} - # Build a configuration object for the output Qobj - config = RunConfig(shots=1024, - memory=False, - meas_level=1, - meas_return='avg', - memory_slot_size=100, - parametric_pulses=[], - init_qubits=True, - qubit_lo_freq=[4900000000.0, 5000000000.0], - meas_lo_freq=[6500000000.0, 6600000000.0], - schedule_los=[]) - # Build a Pulse schedule to assemble into a Qobj - schedule = pulse.Schedule() - schedule += pulse.Play(pulse.Waveform([0.1] * 16, name="test0"), - pulse.DriveChannel(0), - name="test1") - schedule += pulse.Play(pulse.Waveform([0.1] * 16, name="test1"), - pulse.DriveChannel(0), - name="test2") - schedule += pulse.Play(pulse.Waveform([0.5] * 16, name="test0"), - pulse.DriveChannel(0), - name="test1") - # Assemble a Qobj from the schedule. - pulseQobj = assemble_schedules(schedules=[schedule], - qobj_id="custom-id", - qobj_header=header, - run_config=config) - """ - if not hasattr(run_config, "qubit_lo_freq"): - raise QiskitError("qubit_lo_freq must be supplied.") - if not hasattr(run_config, "meas_lo_freq"): - raise QiskitError("meas_lo_freq must be supplied.") - - lo_converter = converters.LoConfigConverter( - qobj.PulseQobjExperimentConfig, **run_config.to_dict() - ) - experiments, experiment_config = _assemble_experiments(schedules, lo_converter, run_config) - qobj_config = _assemble_config(lo_converter, experiment_config, run_config) - - return qobj.PulseQobj( - experiments=experiments, qobj_id=qobj_id, header=qobj_header, config=qobj_config - ) - - -def _assemble_experiments( - schedules: List[Union[schedule.ScheduleComponent, Tuple[int, schedule.ScheduleComponent]]], - lo_converter: converters.LoConfigConverter, - run_config: RunConfig, -) -> Tuple[List[qobj.PulseQobjExperiment], Dict[str, Any]]: - """Assembles a list of schedules into PulseQobjExperiments, and returns related metadata that - will be assembled into the Qobj configuration. - - Args: - schedules: Schedules to assemble. - lo_converter: The configured frequency converter and validator. - run_config: Configuration of the runtime environment. - - Returns: - The list of assembled experiments, and the dictionary of related experiment config. - - Raises: - QiskitError: when frequency settings are not compatible with the experiments. - """ - freq_configs = [lo_converter(lo_dict) for lo_dict in getattr(run_config, "schedule_los", [])] - - if len(schedules) > 1 and len(freq_configs) not in [0, 1, len(schedules)]: - raise QiskitError( - "Invalid 'schedule_los' setting specified. If specified, it should be " - "either have a single entry to apply the same LOs for each schedule or " - "have length equal to the number of schedules." - ) - - instruction_converter = getattr( - run_config, "instruction_converter", converters.InstructionToQobjConverter - ) - instruction_converter = instruction_converter(qobj.PulseQobjInstruction, **run_config.to_dict()) - - formatted_schedules = [transforms.target_qobj_transform(sched) for sched in schedules] - compressed_schedules = transforms.compress_pulses(formatted_schedules) - - user_pulselib = {} - experiments = [] - for idx, sched in enumerate(compressed_schedules): - qobj_instructions, max_memory_slot = _assemble_instructions( - sched, instruction_converter, run_config, user_pulselib - ) - - metadata = sched.metadata - if metadata is None: - metadata = {} - # TODO: add other experimental header items (see circuit assembler) - qobj_experiment_header = qobj.QobjExperimentHeader( - memory_slots=max_memory_slot + 1, # Memory slots are 0 indexed - name=sched.name or f"Experiment-{idx}", - metadata=metadata, - ) - - experiment = qobj.PulseQobjExperiment( - header=qobj_experiment_header, instructions=qobj_instructions - ) - if freq_configs: - # This handles the cases where one frequency setting applies to all experiments and - # where each experiment has a different frequency - freq_idx = idx if len(freq_configs) != 1 else 0 - experiment.config = freq_configs[freq_idx] - - experiments.append(experiment) - - # Frequency sweep - if freq_configs and len(experiments) == 1: - experiment = experiments[0] - experiments = [] - for freq_config in freq_configs: - experiments.append( - qobj.PulseQobjExperiment( - header=experiment.header, - instructions=experiment.instructions, - config=freq_config, - ) - ) - - # Top level Qobj configuration - experiment_config = { - "pulse_library": [ - qobj.PulseLibraryItem(name=name, samples=samples) - for name, samples in user_pulselib.items() - ], - "memory_slots": max(exp.header.memory_slots for exp in experiments), - } - - return experiments, experiment_config - - -def _assemble_instructions( - sched: Union[pulse.Schedule, pulse.ScheduleBlock], - instruction_converter: converters.InstructionToQobjConverter, - run_config: RunConfig, - user_pulselib: Dict[str, List[complex]], -) -> Tuple[List[qobj.PulseQobjInstruction], int]: - """Assembles the instructions in a schedule into a list of PulseQobjInstructions and returns - related metadata that will be assembled into the Qobj configuration. Lookup table for - pulses defined in all experiments are registered in ``user_pulselib``. This object should be - mutable python dictionary so that items are properly updated after each instruction assemble. - The dictionary is not returned to avoid redundancy. - - Args: - sched: Schedule to assemble. - instruction_converter: A converter instance which can convert PulseInstructions to - PulseQobjInstructions. - run_config: Configuration of the runtime environment. - user_pulselib: User pulse library from previous schedule. - - Returns: - A list of converted instructions, the user pulse library dictionary (from pulse name to - pulse samples), and the maximum number of readout memory slots used by this Schedule. - """ - sched = transforms.target_qobj_transform(sched) - - max_memory_slot = 0 - qobj_instructions = [] - - acquire_instruction_map = defaultdict(list) - for time, instruction in sched.instructions: - - if isinstance(instruction, instructions.Play): - if isinstance(instruction.pulse, library.SymbolicPulse): - is_backend_supported = True - try: - pulse_shape = ParametricPulseShapes.from_instance(instruction.pulse).name - if pulse_shape not in run_config.parametric_pulses: - is_backend_supported = False - except ValueError: - # Custom pulse class, or bare SymbolicPulse object. - is_backend_supported = False - - if not is_backend_supported: - instruction = instructions.Play( - instruction.pulse.get_waveform(), instruction.channel, name=instruction.name - ) - - if isinstance(instruction.pulse, library.Waveform): - name = hashlib.sha256(instruction.pulse.samples).hexdigest() - instruction = instructions.Play( - library.Waveform(name=name, samples=instruction.pulse.samples), - channel=instruction.channel, - name=name, - ) - user_pulselib[name] = instruction.pulse.samples - - # ignore explicit delay instrs on acq channels as they are invalid on IBMQ backends; - # timing of other instrs will still be shifted appropriately - if isinstance(instruction, instructions.Delay) and isinstance( - instruction.channel, channels.AcquireChannel - ): - continue - - if isinstance(instruction, instructions.Acquire): - if instruction.mem_slot: - max_memory_slot = max(max_memory_slot, instruction.mem_slot.index) - # Acquires have a single AcquireChannel per inst, but we have to bundle them - # together into the Qobj as one instruction with many channels - acquire_instruction_map[(time, instruction.duration)].append(instruction) - continue - - qobj_instructions.append(instruction_converter(time, instruction)) - - if acquire_instruction_map: - if hasattr(run_config, "meas_map"): - _validate_meas_map(acquire_instruction_map, run_config.meas_map) - for (time, _), instruction_bundle in acquire_instruction_map.items(): - qobj_instructions.append( - instruction_converter(time, instruction_bundle), - ) - - return qobj_instructions, max_memory_slot - - -def _validate_meas_map( - instruction_map: Dict[Tuple[int, instructions.Acquire], List[instructions.Acquire]], - meas_map: List[List[int]], -) -> None: - """Validate all qubits tied in ``meas_map`` are to be acquired. - - Args: - instruction_map: A dictionary grouping Acquire instructions according to their start time - and duration. - meas_map: List of groups of qubits that must be acquired together. - - Raises: - QiskitError: If the instructions do not satisfy the measurement map. - """ - sorted_inst_map = sorted(instruction_map.items(), key=lambda item: item[0]) - meas_map_sets = [set(m) for m in meas_map] - - # error if there is time overlap between qubits in the same meas_map - for idx, inst in enumerate(sorted_inst_map[:-1]): - inst_end_time = inst[0][0] + inst[0][1] - next_inst = sorted_inst_map[idx + 1] - next_inst_time = next_inst[0][0] - if next_inst_time < inst_end_time: - inst_qubits = {inst.channel.index for inst in inst[1]} - next_inst_qubits = {inst.channel.index for inst in next_inst[1]} - for meas_set in meas_map_sets: - common_instr_qubits = inst_qubits.intersection(meas_set) - common_next = next_inst_qubits.intersection(meas_set) - if common_instr_qubits and common_next: - raise QiskitError( - f"Qubits {common_instr_qubits} and {common_next} are in the same measurement " - f"grouping: {meas_map}. " - "They must either be acquired at the same time, or disjointly" - f". Instead, they were acquired at times: {inst[0][0]}-{inst_end_time} and " - f"{next_inst_time}-{next_inst_time + next_inst[0][1]}" - ) - - -def _assemble_config( - lo_converter: converters.LoConfigConverter, - experiment_config: Dict[str, Any], - run_config: RunConfig, -) -> qobj.PulseQobjConfig: - """Assembles the QobjConfiguration from experimental config and runtime config. - - Args: - lo_converter: The configured frequency converter and validator. - experiment_config: Schedules to assemble. - run_config: Configuration of the runtime environment. - - Returns: - The assembled PulseQobjConfig. - """ - qobj_config = run_config.to_dict() - qobj_config.update(experiment_config) - - # Run config not needed in qobj config - qobj_config.pop("meas_map", None) - qobj_config.pop("qubit_lo_range", None) - qobj_config.pop("meas_lo_range", None) - - # convert enums to serialized values - meas_return = qobj_config.get("meas_return", "avg") - if isinstance(meas_return, qobj_utils.MeasReturnType): - qobj_config["meas_return"] = meas_return.value - - meas_level = qobj_config.get("meas_level", 2) - if isinstance(meas_level, qobj_utils.MeasLevel): - qobj_config["meas_level"] = meas_level.value - - # convert LO frequencies to GHz - qobj_config["qubit_lo_freq"] = [freq / 1e9 for freq in qobj_config["qubit_lo_freq"]] - qobj_config["meas_lo_freq"] = [freq / 1e9 for freq in qobj_config["meas_lo_freq"]] - - # override defaults if single entry for ``schedule_los`` - schedule_los = qobj_config.pop("schedule_los", []) - if len(schedule_los) == 1: - lo_dict = schedule_los[0] - q_los = lo_converter.get_qubit_los(lo_dict) - # Hz -> GHz - if q_los: - qobj_config["qubit_lo_freq"] = [freq / 1e9 for freq in q_los] - m_los = lo_converter.get_meas_los(lo_dict) - if m_los: - qobj_config["meas_lo_freq"] = [freq / 1e9 for freq in m_los] - - return qobj.PulseQobjConfig(**qobj_config) diff --git a/qiskit/assembler/disassemble.py b/qiskit/assembler/disassemble.py deleted file mode 100644 index 44107eddbafb..000000000000 --- a/qiskit/assembler/disassemble.py +++ /dev/null @@ -1,312 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2017, 2019. -# -# 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. - -"""Disassemble function for a qobj into a list of circuits and its config""" -from typing import Any, Dict, List, NewType, Tuple, Union -import collections -import math - -from qiskit import pulse -from qiskit.circuit.classicalregister import ClassicalRegister -from qiskit.circuit.instruction import Instruction -from qiskit.circuit.quantumcircuit import QuantumCircuit -from qiskit.circuit.quantumregister import QuantumRegister - -from qiskit.qobj import PulseQobjInstruction -from qiskit.qobj.converters import QobjToInstructionConverter -from qiskit.utils import deprecate_func - -# A ``CircuitModule`` is a representation of a circuit execution on the backend. -# It is currently a list of quantum circuits to execute, a run Qobj dictionary -# and a header dictionary. -CircuitModule = NewType( - "CircuitModule", Tuple[List[QuantumCircuit], Dict[str, Any], Dict[str, Any]] -) - -# A ``PulseModule`` is a representation of a pulse execution on the backend. -# It is currently a list of pulse schedules to execute, a run Qobj dictionary -# and a header dictionary. -PulseModule = NewType("PulseModule", Tuple[List[pulse.Schedule], Dict[str, Any], Dict[str, Any]]) - - -@deprecate_func( - since="1.2", - removal_timeline="in the 2.0 release", - additional_msg="The `Qobj` class and related functionality are part of the deprecated " - "`BackendV1` workflow, and no longer necessary for `BackendV2`. If a user " - "workflow requires `Qobj` it likely relies on deprecated functionality and " - "should be updated to use `BackendV2`.", -) -def disassemble(qobj) -> Union[CircuitModule, PulseModule]: - """Disassemble a qobj and return the circuits or pulse schedules, run_config, and user header. - - .. note:: - - ``disassemble(assemble(qc))`` is not guaranteed to produce an exactly equal circuit to the - input, due to limitations in the :obj:`.QasmQobj` format that need to be maintained for - backend system compatibility. This is most likely to be the case when using newer features - of :obj:`.QuantumCircuit`. In most cases, the output should be equivalent, if not quite - equal. - - Args: - qobj (Qobj): The input qobj object to disassemble - - Returns: - Union[CircuitModule, PulseModule]: The disassembled program which consists of: - - * programs: A list of quantum circuits or pulse schedules - * run_config: The dict of the run config - * user_qobj_header: The dict of any user headers in the qobj - - Examples: - - .. plot:: - :include-source: - :nofigs: - - from qiskit.circuit import QuantumRegister, ClassicalRegister, QuantumCircuit - from qiskit.compiler.assembler import assemble - from qiskit.assembler.disassemble import disassemble - # Create a circuit to assemble into a qobj - q = QuantumRegister(2) - c = ClassicalRegister(2) - qc = QuantumCircuit(q, c) - qc.h(q[0]) - qc.cx(q[0], q[1]) - qc.measure(q, c) - # Assemble the circuit into a Qobj - qobj = assemble(qc, shots=2000, memory=True) - # Disassemble the qobj back into a circuit - circuits, run_config_out, headers = disassemble(qobj) - """ - if qobj.type == "PULSE": - return _disassemble_pulse_schedule(qobj) - else: - return _disassemble_circuit(qobj) - - -def _disassemble_circuit(qobj) -> CircuitModule: - run_config = qobj.config.to_dict() - - # convert lo freq back to Hz - qubit_lo_freq = run_config.get("qubit_lo_freq", []) - if qubit_lo_freq: - run_config["qubit_lo_freq"] = [freq * 1e9 for freq in qubit_lo_freq] - - meas_lo_freq = run_config.get("meas_lo_freq", []) - if meas_lo_freq: - run_config["meas_lo_freq"] = [freq * 1e9 for freq in meas_lo_freq] - - user_qobj_header = qobj.header.to_dict() - return CircuitModule((_experiments_to_circuits(qobj), run_config, user_qobj_header)) - - -def _qobj_to_circuit_cals(qobj, pulse_lib): - """Return circuit calibrations dictionary from qobj/exp config calibrations.""" - qobj_cals = qobj.config.calibrations.to_dict()["gates"] - converter = QobjToInstructionConverter(pulse_lib) - - qc_cals = {} - for gate in qobj_cals: - config = (tuple(gate["qubits"]), tuple(gate["params"])) - cal = { - config: pulse.Schedule( - name=f"{gate['name']} {str(gate['params'])} {str(gate['qubits'])}" - ) - } - for instruction in gate["instructions"]: - qobj_instruction = PulseQobjInstruction.from_dict(instruction) - schedule = converter(qobj_instruction) - cal[config] = cal[config].insert(schedule.ch_start_time(), schedule) - if gate["name"] in qc_cals: - qc_cals[gate["name"]].update(cal) - else: - qc_cals[gate["name"]] = cal - - return qc_cals - - -def _experiments_to_circuits(qobj): - """Return a list of QuantumCircuit object(s) from a qobj. - - Args: - qobj (Qobj): The Qobj object to convert to QuantumCircuits - - Returns: - list: A list of QuantumCircuit objects from the qobj - """ - if not qobj.experiments: - return None - - circuits = [] - for exp in qobj.experiments: - quantum_registers = [QuantumRegister(i[1], name=i[0]) for i in exp.header.qreg_sizes] - classical_registers = [ClassicalRegister(i[1], name=i[0]) for i in exp.header.creg_sizes] - circuit = QuantumCircuit(*quantum_registers, *classical_registers, name=exp.header.name) - qreg_dict = collections.OrderedDict() - creg_dict = collections.OrderedDict() - for reg in quantum_registers: - qreg_dict[reg.name] = reg - for reg in classical_registers: - creg_dict[reg.name] = reg - conditional = {} - for i in exp.instructions: - name = i.name - qubits = [] - params = getattr(i, "params", []) - try: - for qubit in i.qubits: - qubit_label = exp.header.qubit_labels[qubit] - qubits.append(qreg_dict[qubit_label[0]][qubit_label[1]]) - except Exception: # pylint: disable=broad-except - pass - clbits = [] - try: - for clbit in i.memory: - clbit_label = exp.header.clbit_labels[clbit] - clbits.append(creg_dict[clbit_label[0]][clbit_label[1]]) - except Exception: # pylint: disable=broad-except - pass - if hasattr(circuit, name): - instr_method = getattr(circuit, name) - if i.name == "initialize": - _inst = instr_method(params, qubits) - elif i.name in ["mcx", "mcu1", "mcp"]: - _inst = instr_method(*params, qubits[:-1], qubits[-1], *clbits) - else: - _inst = instr_method(*params, *qubits, *clbits) - elif name == "bfunc": - conditional["value"] = int(i.val, 16) - full_bit_size = sum(creg_dict[x].size for x in creg_dict) - mask_map = {} - raw_map = {} - raw = [] - - for creg in creg_dict: - size = creg_dict[creg].size - reg_raw = [1] * size - if not raw: - raw = reg_raw - else: - for pos, val in enumerate(raw): - if val == 1: - raw[pos] = 0 - raw = reg_raw + raw - mask = [0] * (full_bit_size - len(raw)) + raw - raw_map[creg] = mask - mask_map[int("".join(str(x) for x in mask), 2)] = creg - if bin(int(i.mask, 16)).count("1") == 1: - # The condition is on a single bit. This might be a single-bit condition, or it - # might be a register of length one. The case that it's a single-bit condition - # in a register of length one is ambiguous, and we choose to return a condition - # on the register. This may not match the input circuit exactly, but is at - # least equivalent. - cbit = int(math.log2(int(i.mask, 16))) - for reg in creg_dict.values(): - size = reg.size - if cbit >= size: - cbit -= size - else: - conditional["register"] = reg if reg.size == 1 else reg[cbit] - break - mask_str = bin(int(i.mask, 16))[2:].zfill(full_bit_size) - mask = [int(item) for item in list(mask_str)] - else: - creg = mask_map[int(i.mask, 16)] - conditional["register"] = creg_dict[creg] - mask = raw_map[creg] - val = int(i.val, 16) - for j in reversed(mask): - if j == 0: - val = val >> 1 - else: - conditional["value"] = val - break - else: - _inst = temp_opaque_instruction = Instruction( - name=name, num_qubits=len(qubits), num_clbits=len(clbits), params=params - ) - circuit.append(temp_opaque_instruction, qubits, clbits) - if conditional and name != "bfunc": - _inst.c_if(conditional["register"], conditional["value"]) - conditional = {} - pulse_lib = qobj.config.pulse_library if hasattr(qobj.config, "pulse_library") else [] - # The dict update method did not work here; could investigate in the future - if hasattr(qobj.config, "calibrations"): - circuit.calibrations = { - **circuit.calibrations, - **_qobj_to_circuit_cals(qobj, pulse_lib), - } - if hasattr(exp.config, "calibrations"): - circuit.calibrations = {**circuit.calibrations, **_qobj_to_circuit_cals(exp, pulse_lib)} - circuits.append(circuit) - return circuits - - -def _disassemble_pulse_schedule(qobj) -> PulseModule: - run_config = qobj.config.to_dict() - run_config.pop("pulse_library") - - qubit_lo_freq = run_config.get("qubit_lo_freq") - if qubit_lo_freq: - run_config["qubit_lo_freq"] = [freq * 1e9 for freq in qubit_lo_freq] - - meas_lo_freq = run_config.get("meas_lo_freq") - if meas_lo_freq: - run_config["meas_lo_freq"] = [freq * 1e9 for freq in meas_lo_freq] - - user_qobj_header = qobj.header.to_dict() - - # extract schedule lo settings - schedule_los = [] - for program in qobj.experiments: - program_los = {} - if hasattr(program, "config"): - if hasattr(program.config, "qubit_lo_freq"): - for i, lo in enumerate(program.config.qubit_lo_freq): - program_los[pulse.DriveChannel(i)] = lo * 1e9 - - if hasattr(program.config, "meas_lo_freq"): - for i, lo in enumerate(program.config.meas_lo_freq): - program_los[pulse.MeasureChannel(i)] = lo * 1e9 - - schedule_los.append(program_los) - - if any(schedule_los): - run_config["schedule_los"] = schedule_los - - return PulseModule((_experiments_to_schedules(qobj), run_config, user_qobj_header)) - - -def _experiments_to_schedules(qobj) -> List[pulse.Schedule]: - """Return a list of :class:`qiskit.pulse.Schedule` object(s) from a qobj. - - Args: - qobj (Qobj): The Qobj object to convert to pulse schedules. - - Returns: - A list of :class:`qiskit.pulse.Schedule` objects from the qobj - - Raises: - pulse.PulseError: If a parameterized instruction is supplied. - """ - converter = QobjToInstructionConverter(qobj.config.pulse_library) - - schedules = [] - for program in qobj.experiments: - insts = [] - for inst in program.instructions: - insts.append(converter(inst)) - - schedule = pulse.Schedule(*insts) - schedules.append(schedule) - return schedules diff --git a/qiskit/assembler/run_config.py b/qiskit/assembler/run_config.py deleted file mode 100644 index b83f07ec08e1..000000000000 --- a/qiskit/assembler/run_config.py +++ /dev/null @@ -1,77 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2017, 2019. -# -# 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. - -"""Models for RunConfig and its related components.""" - -from types import SimpleNamespace - - -class RunConfig(SimpleNamespace): - """Class for Run Configuration. - - Attributes: - shots (int): the number of shots - seed_simulator (int): the seed to use in the simulator - memory (bool): whether to request memory from backend (per-shot - readouts) - parameter_binds (list[dict]): List of parameter bindings - """ - - def __init__( - self, - shots=None, - seed_simulator=None, - memory=None, - parameter_binds=None, - **kwargs, - ): - """Initialize a RunConfig object - - Args: - shots (int): the number of shots - seed_simulator (int): the seed to use in the simulator - memory (bool): whether to request memory from backend - (per-shot readouts) - parameter_binds (list[dict]): List of parameter bindings - **kwargs: optional fields - """ - if shots is not None: - self.shots = shots - if seed_simulator is not None: - self.seed_simulator = seed_simulator - if memory is not None: - self.memory = memory - if parameter_binds is not None: - self.parameter_binds = parameter_binds - self.__dict__.update(kwargs) - - @classmethod - def from_dict(cls, data): - """Create a new RunConfig object from a dictionary. - - Args: - data (dict): A dictionary representing the RunConfig to create. - It will be in the same format as output by - :meth:`to_dict`. - - Returns: - RunConfig: The RunConfig from the input dictionary. - """ - return cls(**data) - - def to_dict(self): - """Return a dictionary format representation of the RunConfig - - Returns: - dict: The dictionary form of the RunConfig. - """ - return self.__dict__ diff --git a/qiskit/circuit/controlflow/builder.py b/qiskit/circuit/controlflow/builder.py index adde79808a84..be056e3c8677 100644 --- a/qiskit/circuit/controlflow/builder.py +++ b/qiskit/circuit/controlflow/builder.py @@ -297,9 +297,6 @@ def _copy_mutable_properties(self, instruction: Instruction) -> Instruction: # Provide some better error messages, just in case something goes wrong during development and # the placeholder type leaks out to somewhere visible. - def assemble(self): - raise CircuitError("Cannot assemble a placeholder instruction.") - def repeat(self, n): raise CircuitError("Cannot repeat a placeholder instruction.") diff --git a/qiskit/circuit/instruction.py b/qiskit/circuit/instruction.py index e1df54c2cb99..cc51f2459b55 100644 --- a/qiskit/circuit/instruction.py +++ b/qiskit/circuit/instruction.py @@ -43,7 +43,6 @@ from qiskit.circuit.exceptions import CircuitError from qiskit.circuit.classicalregister import ClassicalRegister, Clbit -from qiskit.qobj.qasm_qobj import QasmQobjInstruction from qiskit.circuit.parameter import ParameterExpression from qiskit.circuit.operation import Operation @@ -379,42 +378,6 @@ def unit(self, value): """Set the time unit of duration.""" self._unit = value - @deprecate_func( - since="1.2", - removal_timeline="in the 2.0 release", - additional_msg="The `Qobj` class and related functionality are part of the deprecated " - "`BackendV1` workflow, and no longer necessary for `BackendV2`. If a user " - "workflow requires `Qobj` it likely relies on deprecated functionality and " - "should be updated to use `BackendV2`.", - ) - def assemble(self): - """Assemble a QasmQobjInstruction""" - return self._assemble() - - def _assemble(self): - with warnings.catch_warnings(): - # The class QasmQobjInstruction is deprecated - warnings.filterwarnings("ignore", category=DeprecationWarning, module="qiskit") - instruction = QasmQobjInstruction(name=self.name) - # Evaluate parameters - if self.params: - params = [x.evalf(x) if hasattr(x, "evalf") else x for x in self.params] - instruction.params = params - # Add placeholder for qarg and carg params - if self.num_qubits: - instruction.qubits = list(range(self.num_qubits)) - if self.num_clbits: - instruction.memory = list(range(self.num_clbits)) - # Add label if defined - if self.label: - instruction.label = self.label - # Add condition parameters for assembler. This is needed to convert - # to a qobj conditional instruction at assemble time and after - # conversion will be deleted by the assembler. - if self._condition: - instruction._condition = self._condition - return instruction - @property def label(self) -> str: """Return instruction label""" diff --git a/qiskit/compiler/__init__.py b/qiskit/compiler/__init__.py index 4974fe68a4cd..cd3ed166a4dd 100644 --- a/qiskit/compiler/__init__.py +++ b/qiskit/compiler/__init__.py @@ -20,14 +20,12 @@ Circuit and Pulse Compilation Functions ======================================= -.. autofunction:: assemble .. autofunction:: schedule .. autofunction:: transpile .. autofunction:: sequence """ -from .assembler import assemble from .transpiler import transpile from .scheduler import schedule from .sequencer import sequence diff --git a/qiskit/compiler/assembler.py b/qiskit/compiler/assembler.py deleted file mode 100644 index c42ec0663d90..000000000000 --- a/qiskit/compiler/assembler.py +++ /dev/null @@ -1,681 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2017, 2019. -# -# 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. - -"""Assemble function for converting a list of circuits into a qobj""" -import copy -import logging -import uuid -import warnings -from time import time -from typing import Dict, List, Optional, Union - -import numpy as np - -from qiskit.assembler import assemble_schedules -from qiskit.assembler.run_config import RunConfig -from qiskit.circuit import Parameter, QuantumCircuit, Qubit -from qiskit.exceptions import QiskitError -from qiskit.providers.backend import Backend -from qiskit.pulse import Instruction, LoConfig, Schedule, ScheduleBlock -from qiskit.pulse.channels import PulseChannel -from qiskit.qobj import QasmQobj, PulseQobj, QobjHeader -from qiskit.qobj.utils import MeasLevel, MeasReturnType -from qiskit.utils import deprecate_func -from qiskit.assembler.assemble_circuits import _assemble_circuits - -logger = logging.getLogger(__name__) - - -def _log_assembly_time(start_time, end_time): - log_msg = f"Total Assembly Time - {((end_time - start_time) * 1000):.5f} (ms)" - logger.info(log_msg) - - -# TODO: parallelize over the experiments (serialize each separately, then add global header/config) -@deprecate_func( - since="1.2", - removal_timeline="in the 2.0 release", - additional_msg="The `Qobj` class and related functionality are part of the deprecated " - "`BackendV1` workflow, and no longer necessary for `BackendV2`. If a user " - "workflow requires `Qobj` it likely relies on deprecated functionality and " - "should be updated to use `BackendV2`.", -) -def assemble( - experiments: Union[ - QuantumCircuit, - List[QuantumCircuit], - Schedule, - List[Schedule], - ScheduleBlock, - List[ScheduleBlock], - ], - backend: Optional[Backend] = None, - qobj_id: Optional[str] = None, - qobj_header: Optional[Union[QobjHeader, Dict]] = None, - shots: Optional[int] = None, - memory: Optional[bool] = False, - seed_simulator: Optional[int] = None, - qubit_lo_freq: Optional[List[float]] = None, - meas_lo_freq: Optional[List[float]] = None, - qubit_lo_range: Optional[List[float]] = None, - meas_lo_range: Optional[List[float]] = None, - schedule_los: Optional[ - Union[ - List[Union[Dict[PulseChannel, float], LoConfig]], - Union[Dict[PulseChannel, float], LoConfig], - ] - ] = None, - meas_level: Union[int, MeasLevel] = MeasLevel.CLASSIFIED, - meas_return: Union[str, MeasReturnType] = MeasReturnType.AVERAGE, - meas_map: Optional[List[List[Qubit]]] = None, - memory_slot_size: int = 100, - rep_time: Optional[int] = None, - rep_delay: Optional[float] = None, - parameter_binds: Optional[List[Dict[Parameter, float]]] = None, - parametric_pulses: Optional[List[str]] = None, - init_qubits: bool = True, - **run_config: Dict, -) -> Union[QasmQobj, PulseQobj]: - """Assemble a list of circuits or pulse schedules into a ``Qobj``. - - This function serializes the payloads, which could be either circuits or schedules, - to create ``Qobj`` "experiments". It further annotates the experiment payload with - header and configurations. - - NOTE: ``Backend.options`` is not used within assemble. The required values - (previously given by backend.set_options) should be manually extracted - from options and supplied directly when calling. - - Args: - experiments: Circuit(s) or pulse schedule(s) to execute - backend: If set, some runtime options are automatically grabbed from - ``backend.configuration()`` and ``backend.defaults()``. - If any other option is explicitly set (e.g., ``rep_time``), it - will override the backend's. - If any other options is set in the run_config, it will - also override the backend's. - qobj_id: String identifier to annotate the ``Qobj`` - qobj_header: User input that will be inserted in ``Qobj`` header, and will also be - copied to the corresponding Result header. Headers do not affect the run. - shots: Number of repetitions of each circuit, for sampling. Default: 1024 - or ``max_shots`` from the backend configuration, whichever is smaller - memory: If ``True``, per-shot measurement bitstrings are returned as well - (provided the backend supports it). For OpenPulse jobs, only - measurement level 2 supports this option. - seed_simulator: Random seed to control sampling, for when backend is a simulator - qubit_lo_freq: List of job level qubit drive LO frequencies in Hz. Overridden by - ``schedule_los`` if specified. Must have length ``n_qubits.`` - meas_lo_freq: List of measurement LO frequencies in Hz. Overridden by ``schedule_los`` if - specified. Must have length ``n_qubits.`` - qubit_lo_range: List of job level drive LO ranges each of form ``[range_min, range_max]`` - in Hz. Used to validate ``qubit_lo_freq``. Must have length ``n_qubits.`` - meas_lo_range: List of job level measurement LO ranges each of form - ``[range_min, range_max]`` in Hz. Used to validate ``meas_lo_freq``. Must have length - ``n_qubits.`` - schedule_los: Experiment level (ie circuit or schedule) LO frequency configurations for - qubit drive and measurement channels. These values override the job level values from - ``default_qubit_los`` and ``default_meas_los``. Frequencies are in Hz. Settable for - OpenQASM 2 and pulse jobs. - meas_level: Set the appropriate level of the measurement output for pulse experiments. - meas_return: Level of measurement data for the backend to return. - - For ``meas_level`` 0 and 1: - * ``single`` returns information from every shot. - * ``avg`` returns average measurement output (averaged over number of shots). - meas_map: List of lists, containing qubits that must be measured together. - memory_slot_size: Size of each memory slot if the output is Level 0. - rep_time (int): Time per program execution in seconds. Must be from the list provided - by the backend (``backend.configuration().rep_times``). Defaults to the first entry. - rep_delay (float): Delay between programs in seconds. Only supported on certain - backends (if ``backend.configuration().dynamic_reprate_enabled=True``). If supported, - ``rep_delay`` will be used instead of ``rep_time`` and must be from the range supplied - by the backend (``backend.configuration().rep_delay_range``). Default is given by - ``backend.configuration().default_rep_delay``. - parameter_binds: List of Parameter bindings over which the set of experiments will be - executed. Each list element (bind) should be of the form - {Parameter1: value1, Parameter2: value2, ...}. All binds will be - executed across all experiments; e.g., if parameter_binds is a - length-n list, and there are m experiments, a total of m x n - experiments will be run (one for each experiment/bind pair). - parametric_pulses: A list of pulse shapes which are supported internally on the backend. - Example:: - - ['gaussian', 'constant'] - init_qubits: Whether to reset the qubits to the ground state for each shot. - Default: ``True``. - **run_config: Extra arguments used to configure the run (e.g., for Aer configurable - backends). Refer to the backend documentation for details on these - arguments. - - Returns: - A ``Qobj`` that can be run on a backend. Depending on the type of input, - this will be either a ``QasmQobj`` or a ``PulseQobj``. - - Raises: - QiskitError: if the input cannot be interpreted as either circuits or schedules - """ - return _assemble( - experiments, - backend, - qobj_id, - qobj_header, - shots, - memory, - seed_simulator, - qubit_lo_freq, - meas_lo_freq, - qubit_lo_range, - meas_lo_range, - schedule_los, - meas_level, - meas_return, - meas_map, - memory_slot_size, - rep_time, - rep_delay, - parameter_binds, - parametric_pulses, - init_qubits, - **run_config, - ) - - -# Note for future: this method is used in `BasicSimulator` and may need to be kept past the -# `assemble` removal deadline (2.0). If it is kept (potentially in a different location), -# we will need an alternative for the backend.configuration() access that currently takes -# place in L566 (`parse_circuit_args`) and L351 (`parse_common_args`) -# because backend.configuration() is also set for removal in 2.0. -# The ultimate goal will be to move away from relying on any kind of `assemble` implementation -# because of how tightly coupled it is to these legacy data structures. But as a transition step, -# given that we would only have to support the subcase of `BasicSimulator`, we could probably just -# inline the relevant config values that are already hardcoded in the basic simulator configuration -# generator. -def _assemble( - experiments: Union[ - QuantumCircuit, - List[QuantumCircuit], - Schedule, - List[Schedule], - ScheduleBlock, - List[ScheduleBlock], - ], - backend: Optional[Backend] = None, - qobj_id: Optional[str] = None, - qobj_header: Optional[Union[QobjHeader, Dict]] = None, - shots: Optional[int] = None, - memory: Optional[bool] = False, - seed_simulator: Optional[int] = None, - qubit_lo_freq: Optional[List[float]] = None, - meas_lo_freq: Optional[List[float]] = None, - qubit_lo_range: Optional[List[float]] = None, - meas_lo_range: Optional[List[float]] = None, - schedule_los: Optional[ - Union[ - List[Union[Dict[PulseChannel, float], LoConfig]], - Union[Dict[PulseChannel, float], LoConfig], - ] - ] = None, - meas_level: Union[int, MeasLevel] = MeasLevel.CLASSIFIED, - meas_return: Union[str, MeasReturnType] = MeasReturnType.AVERAGE, - meas_map: Optional[List[List[Qubit]]] = None, - memory_slot_size: int = 100, - rep_time: Optional[int] = None, - rep_delay: Optional[float] = None, - parameter_binds: Optional[List[Dict[Parameter, float]]] = None, - parametric_pulses: Optional[List[str]] = None, - init_qubits: bool = True, - **run_config: Dict, -) -> Union[QasmQobj, PulseQobj]: - start_time = time() - experiments = experiments if isinstance(experiments, list) else [experiments] - pulse_qobj = any(isinstance(exp, (ScheduleBlock, Schedule, Instruction)) for exp in experiments) - with warnings.catch_warnings(): - # The Qobj class is deprecated, the backend.configuration() method is too - warnings.filterwarnings("ignore", category=DeprecationWarning, module="qiskit") - qobj_id, qobj_header, run_config_common_dict = _parse_common_args( - backend, - qobj_id, - qobj_header, - shots, - memory, - seed_simulator, - init_qubits, - rep_delay, - qubit_lo_freq, - meas_lo_freq, - qubit_lo_range, - meas_lo_range, - schedule_los, - pulse_qobj=pulse_qobj, - **run_config, - ) - - # assemble either circuits or schedules - if all(isinstance(exp, QuantumCircuit) for exp in experiments): - run_config = _parse_circuit_args( - parameter_binds, - backend, - meas_level, - meas_return, - parametric_pulses, - **run_config_common_dict, - ) - - # If circuits are parameterized, bind parameters and remove from run_config - bound_experiments, run_config = _expand_parameters( - circuits=experiments, run_config=run_config - ) - end_time = time() - _log_assembly_time(start_time, end_time) - return _assemble_circuits( - circuits=bound_experiments, - qobj_id=qobj_id, - qobj_header=qobj_header, - run_config=run_config, - ) - - elif all(isinstance(exp, (ScheduleBlock, Schedule, Instruction)) for exp in experiments): - run_config = _parse_pulse_args( - backend, - meas_level, - meas_return, - meas_map, - memory_slot_size, - rep_time, - parametric_pulses, - **run_config_common_dict, - ) - - end_time = time() - _log_assembly_time(start_time, end_time) - return assemble_schedules( - schedules=experiments, qobj_id=qobj_id, qobj_header=qobj_header, run_config=run_config - ) - - else: - raise QiskitError("bad input to assemble() function; must be either circuits or schedules") - - -# TODO: rework to return a list of RunConfigs (one for each experiments), and a global one -def _parse_common_args( - backend, - qobj_id, - qobj_header, - shots, - memory, - seed_simulator, - init_qubits, - rep_delay, - qubit_lo_freq, - meas_lo_freq, - qubit_lo_range, - meas_lo_range, - schedule_los, - pulse_qobj=False, - **run_config, -): - """Resolve the various types of args allowed to the assemble() function through - duck typing, overriding args, etc. Refer to the assemble() docstring for details on - what types of inputs are allowed. - - Here the args are resolved by converting them to standard instances, and prioritizing - them in case a run option is passed through multiple args (explicitly setting an arg - has more priority than the arg set by backend) - - Returns: - RunConfig: a run config, which is a standardized object that configures the qobj - and determines the runtime environment. - - Raises: - QiskitError: - - If the memory arg is True and the backend does not support memory. - - If ``shots`` exceeds ``max_shots`` for the configured backend. - - If ``shots`` are not int type. - - If any of qubit or meas lo's, or associated ranges do not have length equal to - ``n_qubits``. - - If qubit or meas lo's do not fit into prescribed ranges. - """ - # grab relevant info from backend if it exists - backend_config = None - backend_defaults = None - n_qubits = None - if backend: - backend_config = backend.configuration() - n_qubits = backend_config.n_qubits - # check for memory flag applied to backend that does not support memory - if memory and not backend_config.memory: - raise QiskitError(f"memory not supported by backend {backend_config.backend_name}") - - # try to set defaults for pulse, other leave as None - pulse_param_set = ( - qubit_lo_freq is not None - or meas_lo_freq is not None - or qubit_lo_range is not None - or meas_lo_range is not None - or schedule_los is not None - ) - if pulse_qobj or (backend_config.open_pulse and pulse_param_set): - try: - backend_defaults = backend.defaults() - except AttributeError: - pass - - # an identifier for the Qobj - qobj_id = qobj_id or str(uuid.uuid4()) - - # The header that goes at the top of the Qobj (and later Result) - # we process it as dict, then write entries that are not None to a QobjHeader object - qobj_header = qobj_header or {} - if isinstance(qobj_header, QobjHeader): - qobj_header = qobj_header.to_dict() - backend_name = getattr(backend_config, "backend_name", None) - backend_version = getattr(backend_config, "backend_version", None) - qobj_header = { - "backend_name": backend_name, - "backend_version": backend_version, - **qobj_header, - } - qobj_header = QobjHeader(**{k: v for k, v in qobj_header.items() if v is not None}) - - max_shots = getattr(backend_config, "max_shots", None) - if shots is None: - if max_shots: - shots = min(1024, max_shots) - else: - shots = 1024 - elif not isinstance(shots, (int, np.integer)): - raise QiskitError("Argument 'shots' should be of type 'int'") - elif max_shots and max_shots < shots: - raise QiskitError( - f"Number of shots specified: {max_shots} exceeds max_shots property of the " - f"backend: {max_shots}." - ) - - dynamic_reprate_enabled = getattr(backend_config, "dynamic_reprate_enabled", False) - if dynamic_reprate_enabled: - default_rep_delay = getattr(backend_config, "default_rep_delay", None) - rep_delay_range = getattr(backend_config, "rep_delay_range", None) - rep_delay = _parse_rep_delay(rep_delay, default_rep_delay, rep_delay_range) - else: - if rep_delay is not None: - rep_delay = None - warnings.warn( - "Dynamic rep rates not supported on this backend, cannot use rep_delay.", - RuntimeWarning, - ) - - qubit_lo_freq = qubit_lo_freq or getattr(backend_defaults, "qubit_freq_est", None) - meas_lo_freq = meas_lo_freq or getattr(backend_defaults, "meas_freq_est", None) - - qubit_lo_range = qubit_lo_range or getattr(backend_config, "qubit_lo_range", None) - meas_lo_range = meas_lo_range or getattr(backend_config, "meas_lo_range", None) - - # check that LO frequencies are in the perscribed range - _check_lo_freqs(qubit_lo_freq, qubit_lo_range, "qubit") - _check_lo_freqs(meas_lo_freq, meas_lo_range, "meas") - - # configure experiment level LO frequencies - schedule_los = schedule_los or [] - if isinstance(schedule_los, (LoConfig, dict)): - schedule_los = [schedule_los] - - # Convert to LoConfig if LO configuration supplied as dictionary - schedule_los = [ - lo_config if isinstance(lo_config, LoConfig) else LoConfig(lo_config) - for lo_config in schedule_los - ] - - # create run configuration and populate - run_config_dict = { - "shots": shots, - "memory": memory, - "seed_simulator": seed_simulator, - "init_qubits": init_qubits, - "rep_delay": rep_delay, - "qubit_lo_freq": qubit_lo_freq, - "meas_lo_freq": meas_lo_freq, - "qubit_lo_range": qubit_lo_range, - "meas_lo_range": meas_lo_range, - "schedule_los": schedule_los, - "n_qubits": n_qubits, - **run_config, - } - - return qobj_id, qobj_header, run_config_dict - - -def _check_lo_freqs( - lo_freq: Union[List[float], None], - lo_range: Union[List[float], None], - lo_type: str, -): - """Check that LO frequencies are within the perscribed LO range. - - NOTE: Only checks if frequency/range lists have equal length. And does not check that the lists - have length ``n_qubits``. This is because some backends, like simulator backends, do not - require these constraints. For real hardware, these parameters will be validated on the backend. - - Args: - lo_freq: List of LO frequencies. - lo_range: Nested list of LO frequency ranges. Inner list is of the form - ``[lo_min, lo_max]``. - lo_type: The type of LO value--"qubit" or "meas". - - Raises: - QiskitError: - - If each element of the LO range is not a 2d list. - - If the LO frequency is not in the LO range for a given qubit. - """ - if lo_freq and lo_range and len(lo_freq) == len(lo_range): - for i, freq in enumerate(lo_freq): - freq_range = lo_range[i] - if not (isinstance(freq_range, list) and len(freq_range) == 2): - raise QiskitError(f"Each element of {lo_type} LO range must be a 2d list.") - if freq < freq_range[0] or freq > freq_range[1]: - raise QiskitError( - f"Qubit {i} {lo_type} LO frequency is {freq}. " - f"The range is [{freq_range[0]}, {freq_range[1]}]." - ) - - -def _parse_pulse_args( - backend, - meas_level, - meas_return, - meas_map, - memory_slot_size, - rep_time, - parametric_pulses, - **run_config, -): - """Build a pulse RunConfig replacing unset arguments with defaults derived from the `backend`. - See `assemble` for more information on the required arguments. - - Returns: - RunConfig: a run config, which is a standardized object that configures the qobj - and determines the runtime environment. - Raises: - QiskitError: If the given meas_level is not allowed for the given `backend`. - """ - # grab relevant info from backend if it exists - backend_config = None - if backend: - backend_config = backend.configuration() - - if meas_level not in getattr(backend_config, "meas_levels", [MeasLevel.CLASSIFIED]): - raise QiskitError( - f"meas_level = {meas_level} not supported for backend " - f"{backend_config.backend_name}, only {backend_config.meas_levels} is supported" - ) - - meas_map = meas_map or getattr(backend_config, "meas_map", None) - dynamic_reprate_enabled = getattr(backend_config, "dynamic_reprate_enabled", False) - - rep_time = rep_time or getattr(backend_config, "rep_times", None) - if rep_time: - if dynamic_reprate_enabled: - warnings.warn( - "Dynamic rep rates are supported on this backend. 'rep_delay' will be " - "used instead of 'rep_time'.", - RuntimeWarning, - ) - if isinstance(rep_time, list): - rep_time = rep_time[0] - rep_time = int(rep_time * 1e6) # convert sec to μs - if parametric_pulses is None: - parametric_pulses = getattr(backend_config, "parametric_pulses", []) - - # create run configuration and populate - run_config_dict = { - "meas_level": meas_level, - "meas_return": meas_return, - "meas_map": meas_map, - "memory_slot_size": memory_slot_size, - "rep_time": rep_time, - "parametric_pulses": parametric_pulses, - **run_config, - } - run_config = RunConfig(**{k: v for k, v in run_config_dict.items() if v is not None}) - - return run_config - - -def _parse_circuit_args( - parameter_binds, backend, meas_level, meas_return, parametric_pulses, **run_config -): - """Build a circuit RunConfig replacing unset arguments with defaults derived from the `backend`. - See `assemble` for more information on the required arguments. - - Returns: - RunConfig: a run config, which is a standardized object that configures the qobj - and determines the runtime environment. - """ - parameter_binds = parameter_binds or [] - # create run configuration and populate - run_config_dict = {"parameter_binds": parameter_binds, **run_config} - if parametric_pulses is None: - if backend: - with warnings.catch_warnings(): - # TODO (2.0): See comment on L192 regarding backend.configuration removal - warnings.filterwarnings("ignore", category=DeprecationWarning, module="qiskit") - run_config_dict["parametric_pulses"] = getattr( - backend.configuration(), "parametric_pulses", [] - ) - else: - run_config_dict["parametric_pulses"] = [] - else: - run_config_dict["parametric_pulses"] = parametric_pulses - if meas_level: - run_config_dict["meas_level"] = meas_level - # only enable `meas_return` if `meas_level` isn't classified - if meas_level != MeasLevel.CLASSIFIED: - run_config_dict["meas_return"] = meas_return - - run_config = RunConfig(**{k: v for k, v in run_config_dict.items() if v is not None}) - - return run_config - - -def _parse_rep_delay( - rep_delay: float, default_rep_delay: float, rep_delay_range: List[float] -) -> float: - """Parse and set ``rep_delay`` parameter in runtime config. - - Args: - rep_delay: Initial rep delay. - default_rep_delay: Backend default rep delay. - rep_delay_range: Backend list defining allowable range of rep delays. - - Raises: - QiskitError: If rep_delay is not in the backend rep_delay_range. - Returns: - float: Modified rep delay after parsing. - """ - if rep_delay is None: - rep_delay = default_rep_delay - - if rep_delay is not None: - # check that rep_delay is in rep_delay_range - if rep_delay_range is not None and isinstance(rep_delay_range, list): - if len(rep_delay_range) != 2: - raise QiskitError( - f"Backend rep_delay_range {rep_delay_range} must be a list with two entries." - ) - if not rep_delay_range[0] <= rep_delay <= rep_delay_range[1]: - raise QiskitError( - f"Supplied rep delay {rep_delay} not in the supported " - f"backend range {rep_delay_range}" - ) - rep_delay = rep_delay * 1e6 # convert sec to μs - - return rep_delay - - -def _expand_parameters(circuits, run_config): - """Verifies that there is a single common set of parameters shared between - all circuits and all parameter binds in the run_config. Returns an expanded - list of circuits (if parameterized) with all parameters bound, and a copy of - the run_config with parameter_binds cleared. - - If neither the circuits nor the run_config specify parameters, the two are - returned unmodified. - - Raises: - QiskitError: if run_config parameters are not compatible with circuit parameters - - Returns: - Tuple(List[QuantumCircuit], RunConfig): - - List of input circuits expanded and with parameters bound - - RunConfig with parameter_binds removed - """ - parameter_binds = run_config.parameter_binds - - if parameter_binds and any(parameter_binds) or any(circuit.parameters for circuit in circuits): - - # Unroll params here in order to handle ParamVects - all_bind_parameters = [ - QuantumCircuit()._unroll_param_dict(bind).keys() for bind in parameter_binds - ] - - all_circuit_parameters = [circuit.parameters for circuit in circuits] - - # Collect set of all unique parameters across all circuits and binds - unique_parameters = { - param - for param_list in all_bind_parameters + all_circuit_parameters - for param in param_list - } - - # Check that all parameters are common to all circuits and binds - if ( - not all_bind_parameters - or not all_circuit_parameters - or any(unique_parameters != bind_params for bind_params in all_bind_parameters) - or any(unique_parameters != parameters for parameters in all_circuit_parameters) - ): - raise QiskitError( - ( - "Mismatch between run_config.parameter_binds and all circuit parameters. " - + "Parameter binds: {} " - + "Circuit parameters: {}" - ).format(all_bind_parameters, all_circuit_parameters) - ) - - circuits = [ - circuit.assign_parameters(binds) for circuit in circuits for binds in parameter_binds - ] - - # All parameters have been expanded and bound, so remove from run_config - run_config = copy.deepcopy(run_config) - run_config.parameter_binds = [] - - return circuits, run_config diff --git a/releasenotes/notes/remove-assemble-2d5d9cea4ca504f5.yaml b/releasenotes/notes/remove-assemble-2d5d9cea4ca504f5.yaml new file mode 100644 index 000000000000..be80688e8e97 --- /dev/null +++ b/releasenotes/notes/remove-assemble-2d5d9cea4ca504f5.yaml @@ -0,0 +1,18 @@ +--- +upgrade_misc: + - | + The ``assemble`` function and related capabilities (contained in the ``assembler`` module) + have been removed from the codebase following their deprecation in Qiskit 1.2. + ``assemble`` was used to generate ``Qobj`` in the context of the + deprecated ``BackendV1`` workflow. The conversion is no longer necessary as the + transpilation and primitives pipeline handles quantum circuits directly, rendering the + ``Qobj`` obsolete. + + The removal includes the following public API components: + + * ``qiskit.compiler.assemble`` function + * ``qiskit.assembler.assemble_circuits`` function + * ``qiskit.assembler.assemble_schedules`` function + * ``qiskit.assembler.disassemble`` function + * ``qiskit.assembler.RunConfig`` class + * ``qiskit.circuit.Instruction.assemble`` method diff --git a/releasenotes/notes/remove-backend-configuration-basic-simulator-cac1c2783a5a4e25.yaml b/releasenotes/notes/remove-backend-configuration-basic-simulator-cac1c2783a5a4e25.yaml index 38ce4cfe77d3..cf2a5cb26136 100644 --- a/releasenotes/notes/remove-backend-configuration-basic-simulator-cac1c2783a5a4e25.yaml +++ b/releasenotes/notes/remove-backend-configuration-basic-simulator-cac1c2783a5a4e25.yaml @@ -3,7 +3,7 @@ upgrade_providers: - | The ``configuration`` method of :class:`.BasicSimulator` has been removed following its deprecation in Qiskit 1.3. This method returned a ``BackendConfiguration`` instance, and this class was - part of the deprecated :class:`.BackendV1` workflow. The individual configuration elements + part of the deprecated ``BackendV1`` workflow. The individual configuration elements can be retrieved directly from the backend or from the contained :class:`.Target` instance ``(backend.target)``. - | diff --git a/test/benchmarks/transpiler_benchmarks.py b/test/benchmarks/transpiler_benchmarks.py index a2ba39ab306d..9aecb803e8b4 100644 --- a/test/benchmarks/transpiler_benchmarks.py +++ b/test/benchmarks/transpiler_benchmarks.py @@ -103,28 +103,25 @@ def setup(self): self.basis = ["id", "rz", "sx", "x", "cx", "reset"] def time_single_gate_compile(self): - circ = qiskit.compiler.transpile( + qiskit.compiler.transpile( self.single_gate_circuit, coupling_map=self.coupling_map, basis_gates=self.basis, seed_transpiler=20220125, ) - qiskit.compiler.assemble(circ) def time_cx_compile(self): - circ = qiskit.compiler.transpile( + qiskit.compiler.transpile( self.cx_circuit, coupling_map=self.coupling_map, basis_gates=self.basis, seed_transpiler=20220125, ) - qiskit.compiler.assemble(circ) def time_compile_from_large_qasm(self): - circ = qiskit.compiler.transpile( + qiskit.compiler.transpile( self.large_qasm, coupling_map=self.coupling_map, basis_gates=self.basis, seed_transpiler=20220125, ) - qiskit.compiler.assemble(circ) diff --git a/test/python/circuit/test_diagonal_gate.py b/test/python/circuit/test_diagonal_gate.py index bb9259999576..95c605e263e4 100644 --- a/test/python/circuit/test_diagonal_gate.py +++ b/test/python/circuit/test_diagonal_gate.py @@ -16,7 +16,7 @@ import unittest import numpy as np -from qiskit import QuantumCircuit, assemble +from qiskit import QuantumCircuit from qiskit import QiskitError from qiskit.compiler import transpile from qiskit.circuit.library.generalized_gates import DiagonalGate @@ -75,14 +75,6 @@ def test_npcomplex_params_conversion(self): all(isinstance(p, complex) and not isinstance(p, np.number) for p in params) ) - with self.assertWarns(DeprecationWarning): - # REMOVE this assertion (not the full test) once ASSEMBLE is removed. - qobj = assemble(qc) - params = qobj.experiments[0].instructions[0].params - self.assertTrue( - all(isinstance(p, complex) and not isinstance(p, np.number) for p in params) - ) - def test_repeat(self): """Test the repeat() method.""" for phases in [ diff --git a/test/python/circuit/test_hamiltonian_gate.py b/test/python/circuit/test_hamiltonian_gate.py index e65e2f07ba5e..fa8525561b43 100644 --- a/test/python/circuit/test_hamiltonian_gate.py +++ b/test/python/circuit/test_hamiltonian_gate.py @@ -16,7 +16,6 @@ import numpy as np from numpy.testing import assert_allclose -import qiskit from qiskit.circuit.library import HamiltonianGate from qiskit import QuantumRegister, ClassicalRegister, QuantumCircuit from qiskit.circuit import Parameter @@ -148,9 +147,7 @@ def test_qobj_with_hamiltonian(self): qc.append(uni, [qr[0], qr[1], qr[3]]) qc.cx(qr[3], qr[2]) qc = qc.assign_parameters({theta: np.pi / 2}) - with self.assertWarns(DeprecationWarning): - qobj = qiskit.compiler.assemble(qc) - instr = qobj.experiments[0].instructions[1] + instr = qc.data[1] self.assertEqual(instr.name, "hamiltonian") # Also test label self.assertEqual(instr.label, "XIZ") diff --git a/test/python/circuit/test_initializer.py b/test/python/circuit/test_initializer.py index c0a8e568e647..76d0a0641cb2 100644 --- a/test/python/circuit/test_initializer.py +++ b/test/python/circuit/test_initializer.py @@ -24,7 +24,6 @@ QuantumRegister, ClassicalRegister, transpile, - assemble, ) from qiskit.providers.basic_provider import BasicSimulator from qiskit.quantum_info import state_fidelity, Statevector, Operator @@ -531,13 +530,6 @@ def test_init(self): all(isinstance(p, complex) and not isinstance(p, np.number) for p in params) ) - with self.assertWarns(DeprecationWarning): - qobj = assemble(qc) - params = qobj.experiments[0].instructions[0].params - self.assertTrue( - all(isinstance(p, complex) and not isinstance(p, np.number) for p in params) - ) - if __name__ == "__main__": unittest.main() diff --git a/test/python/circuit/test_parameters.py b/test/python/circuit/test_parameters.py index 6f85f7899a5d..bcd0316aee36 100644 --- a/test/python/circuit/test_parameters.py +++ b/test/python/circuit/test_parameters.py @@ -28,7 +28,7 @@ from qiskit.circuit import Gate, Instruction, Parameter, ParameterExpression, ParameterVector from qiskit.circuit.parametertable import ParameterView from qiskit.circuit.exceptions import CircuitError -from qiskit.compiler import assemble, transpile +from qiskit.compiler import transpile from qiskit import pulse from qiskit.quantum_info import Operator from qiskit.providers.fake_provider import Fake5QV1, GenericBackendV2 @@ -896,10 +896,9 @@ def test_circuit_generation(self): theta_list = numpy.linspace(0, numpy.pi, 20) for theta_i in theta_list: circs.append(qc_aer.assign_parameters({theta: theta_i})) - with self.assertWarns(DeprecationWarning): - qobj = assemble(circs) + for index, theta_i in enumerate(theta_list): - res = float(qobj.experiments[index].instructions[0].params[0]) + res = float(circs[index].data[0].params[0]) self.assertTrue(math.isclose(res, theta_i), f"{res} != {theta_i}") def test_circuit_composition(self): diff --git a/test/python/circuit/test_scheduled_circuit.py b/test/python/circuit/test_scheduled_circuit.py index 63a521c3faec..45238dc79e74 100644 --- a/test/python/circuit/test_scheduled_circuit.py +++ b/test/python/circuit/test_scheduled_circuit.py @@ -16,7 +16,7 @@ from ddt import ddt, data from qiskit import QuantumCircuit, QiskitError -from qiskit import transpile, assemble +from qiskit import transpile from qiskit.circuit import Parameter from qiskit.circuit.duration import convert_durations_to_dt from qiskit.providers.fake_provider import Fake27QPulseV1, GenericBackendV2 @@ -130,8 +130,6 @@ def test_schedule_circuit_in_sec_when_no_one_tells_dt(self): self.assertEqual(sc.data[4].operation.name, "delay") self.assertAlmostEqual(sc.data[4].operation.duration, 1.0e-4 + 1.0e-7) self.assertEqual(sc.data[4].operation.unit, "s") - with self.assertRaises(DeprecationWarning): - assemble(sc, self.backend_without_dt) def test_cannot_schedule_circuit_with_mixed_SI_and_dt_when_no_one_tells_dt(self): """dt is unknown but delays and gate times have a mix of SI and dt""" @@ -536,7 +534,7 @@ def test_can_transpile_circuits_after_assigning_parameters(self): self.assertEqual(circ.duration, None) # not scheduled self.assertEqual(circ.data[1].operation.duration, 450) # converted in dt - def test_can_transpile_and_assemble_circuits_with_assigning_parameters_inbetween(self): + def test_can_transpile_circuits_with_assigning_parameters_inbetween(self): idle_dur = Parameter("t") qc = QuantumCircuit(1, 1) qc.x(0) @@ -549,10 +547,8 @@ def test_can_transpile_and_assemble_circuits_with_assigning_parameters_inbetween ): circ = transpile(qc, self.backend_with_dt) circ = circ.assign_parameters({idle_dur: 0.1}) - with self.assertWarns(DeprecationWarning): - qobj = assemble(circ, self.backend_with_dt) - self.assertEqual(qobj.experiments[0].instructions[1].name, "delay") - self.assertEqual(qobj.experiments[0].instructions[1].params[0], 450) + self.assertEqual(circ.data[1].name, "delay") + self.assertEqual(circ.data[1].params[0], 450) def test_can_transpile_circuits_with_unbounded_parameters(self): idle_dur = Parameter("t") @@ -573,21 +569,6 @@ def test_can_transpile_circuits_with_unbounded_parameters(self): circ.data[1].operation.duration, idle_dur * 1e-6 / self.dt ) # still parameterized - def test_fail_to_assemble_circuits_with_unbounded_parameters(self): - idle_dur = Parameter("t") - qc = QuantumCircuit(1, 1) - qc.x(0) - qc.delay(idle_dur, 0, "us") - qc.measure(0, 0) - with self.assertWarnsRegex( - DeprecationWarning, - expected_regex="The `transpile` function will " - "stop supporting inputs of type `BackendV1`", - ): - qc = transpile(qc, self.backend_with_dt) - with self.assertRaises(DeprecationWarning): - assemble(qc, self.backend_with_dt) - @data("asap", "alap") def test_can_schedule_circuits_with_bounded_parameters(self, scheduling_method): idle_dur = Parameter("t") diff --git a/test/python/circuit/test_unitary.py b/test/python/circuit/test_unitary.py index 6ab4a93c3f7d..66e6499ebb9d 100644 --- a/test/python/circuit/test_unitary.py +++ b/test/python/circuit/test_unitary.py @@ -12,11 +12,9 @@ """UnitaryGate tests""" -import json import numpy from numpy.testing import assert_allclose -import qiskit from qiskit.circuit.library import UnitaryGate, CXGate from qiskit import QuantumRegister, ClassicalRegister, QuantumCircuit from qiskit.transpiler import PassManager @@ -162,53 +160,6 @@ def test_1q_unitary_int_qargs(self): qc_target.append(UnitaryGate(sigmaz), [[0, 1]]) self.assertEqual(qc, qc_target) - def test_qobj_with_unitary_matrix(self): - """test qobj output with unitary matrix""" - qr = QuantumRegister(4) - qc = QuantumCircuit(qr) - sigmax = numpy.array([[0, 1], [1, 0]]) - sigmay = numpy.array([[0, -1j], [1j, 0]]) - matrix = numpy.kron(sigmay, numpy.kron(sigmax, sigmay)) - qc.rx(numpy.pi / 4, qr[0]) - uni = UnitaryGate(matrix) - qc.append(uni, [qr[0], qr[1], qr[3]]) - qc.cx(qr[3], qr[2]) - with self.assertWarns(DeprecationWarning): - qobj = qiskit.compiler.assemble(qc) - instr = qobj.experiments[0].instructions[1] - self.assertEqual(instr.name, "unitary") - assert_allclose(numpy.array(instr.params[0]).astype(numpy.complex64), matrix) - # check conversion to dict - qobj_dict = qobj.to_dict() - - class NumpyEncoder(json.JSONEncoder): - """Class for encoding json str with complex and numpy arrays.""" - - def default(self, obj): # pylint:disable=arguments-renamed - if isinstance(obj, numpy.ndarray): - return obj.tolist() - if isinstance(obj, complex): - return (obj.real, obj.imag) - return json.JSONEncoder.default(self, obj) - - # check json serialization - self.assertTrue(isinstance(json.dumps(qobj_dict, cls=NumpyEncoder), str)) - - def test_labeled_unitary(self): - """test qobj output with unitary matrix""" - qr = QuantumRegister(4) - qc = QuantumCircuit(qr) - sigmax = numpy.array([[0, 1], [1, 0]]) - sigmay = numpy.array([[0, -1j], [1j, 0]]) - matrix = numpy.kron(sigmax, sigmay) - uni = UnitaryGate(matrix, label="xy") - qc.append(uni, [qr[0], qr[1]]) - with self.assertWarns(DeprecationWarning): - qobj = qiskit.compiler.assemble(qc) - instr = qobj.experiments[0].instructions[0] - self.assertEqual(instr.name, "unitary") - self.assertEqual(instr.label, "xy") - def test_qasm_unitary_only_one_def(self): """test that a custom unitary can be converted to qasm and the definition is only written once""" diff --git a/test/python/compiler/test_assembler.py b/test/python/compiler/test_assembler.py deleted file mode 100644 index 6a954d871cc9..000000000000 --- a/test/python/compiler/test_assembler.py +++ /dev/null @@ -1,2108 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2017, 2019. -# -# 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. - -"""Assembler Test.""" - -import unittest -import io -from logging import StreamHandler, getLogger -import sys -import copy -import numpy as np - -from qiskit import pulse -from qiskit.circuit import Instruction, Gate, Parameter, ParameterVector -from qiskit.circuit import QuantumRegister, ClassicalRegister, QuantumCircuit -from qiskit.compiler.assembler import assemble -from qiskit.exceptions import QiskitError -from qiskit.pulse import Schedule, Acquire, Play -from qiskit.pulse.channels import MemorySlot, AcquireChannel, DriveChannel, MeasureChannel -from qiskit.pulse.configuration import Kernel, Discriminator -from qiskit.qobj import QasmQobj, PulseQobj -from qiskit.qobj.utils import MeasLevel, MeasReturnType -from qiskit.pulse.macros import measure -from qiskit.providers.fake_provider import ( - FakeOpenPulse2Q, - FakeOpenPulse3Q, - Fake5QV1, - Fake27QPulseV1, -) -from test import QiskitTestCase # pylint: disable=wrong-import-order - - -class RxGate(Gate): - """Used to test custom gate assembly. - - Useful for testing pulse gates with parameters, as well. - Note: Parallel maps (e.g., in assemble_circuits) pickle their input, - so circuit features have to be defined top level. - """ - - def __init__(self, theta): - super().__init__("rxtheta", 1, [theta]) - - -class TestCircuitAssembler(QiskitTestCase): - """Tests for assembling circuits to qobj.""" - - def setUp(self): - super().setUp() - qr = QuantumRegister(2, name="q") - cr = ClassicalRegister(2, name="c") - self.circ = QuantumCircuit(qr, cr, name="circ") - self.circ.h(qr[0]) - self.circ.cx(qr[0], qr[1]) - self.circ.measure(qr, cr) - - with self.assertWarns(DeprecationWarning): - self.backend = Fake5QV1() - self.backend_config = self.backend.configuration() - self.num_qubits = self.backend_config.n_qubits - - # lo test values - self.default_qubit_lo_freq = [5e9 for _ in range(self.num_qubits)] - self.default_meas_lo_freq = [6.7e9 for _ in range(self.num_qubits)] - with self.assertWarns(DeprecationWarning): - self.user_lo_config_dict = { - pulse.DriveChannel(0): 5.55e9, - pulse.MeasureChannel(0): 6.64e9, - pulse.DriveChannel(3): 4.91e9, - pulse.MeasureChannel(4): 6.1e9, - } - self.user_lo_config = pulse.LoConfig(self.user_lo_config_dict) - - def test_assemble_single_circuit(self): - """Test assembling a single circuit.""" - with self.assertWarns(DeprecationWarning): - qobj = assemble(self.circ, shots=2000, memory=True) - - self.assertIsInstance(qobj, QasmQobj) - self.assertEqual(qobj.config.shots, 2000) - self.assertEqual(qobj.config.memory, True) - self.assertEqual(len(qobj.experiments), 1) - self.assertEqual(qobj.experiments[0].instructions[1].name, "cx") - - def test_assemble_multiple_circuits(self): - """Test assembling multiple circuits, all should have the same config.""" - qr0 = QuantumRegister(2, name="q0") - qc0 = ClassicalRegister(2, name="c0") - circ0 = QuantumCircuit(qr0, qc0, name="circ0") - circ0.h(qr0[0]) - circ0.cx(qr0[0], qr0[1]) - circ0.measure(qr0, qc0) - - qr1 = QuantumRegister(3, name="q1") - qc1 = ClassicalRegister(3, name="c1") - circ1 = QuantumCircuit(qr1, qc1, name="circ0") - circ1.h(qr1[0]) - circ1.cx(qr1[0], qr1[1]) - circ1.cx(qr1[0], qr1[2]) - circ1.measure(qr1, qc1) - - with self.assertWarns(DeprecationWarning): - qobj = assemble([circ0, circ1], shots=100, memory=False, seed_simulator=6) - - self.assertIsInstance(qobj, QasmQobj) - self.assertEqual(qobj.config.seed_simulator, 6) - self.assertEqual(len(qobj.experiments), 2) - self.assertEqual(qobj.experiments[1].config.n_qubits, 3) - self.assertEqual(len(qobj.experiments), 2) - self.assertEqual(len(qobj.experiments[1].instructions), 6) - - def test_assemble_no_run_config(self): - """Test assembling with no run_config, relying on default.""" - with self.assertWarns(DeprecationWarning): - qobj = assemble(self.circ) - - self.assertIsInstance(qobj, QasmQobj) - self.assertEqual(qobj.config.shots, 1024) - - def test_shots_greater_than_max_shots(self): - """Test assembling with shots greater than max shots""" - with self.assertWarns(DeprecationWarning): - self.assertRaises(QiskitError, assemble, self.backend, shots=1024000) - - def test_shots_not_of_type_int(self): - """Test assembling with shots having type other than int""" - with self.assertWarns(DeprecationWarning): - self.assertRaises(QiskitError, assemble, self.backend, shots="1024") - - def test_shots_of_type_numpy_int64(self): - """Test assembling with shots having type numpy.int64""" - with self.assertWarns(DeprecationWarning): - qobj = assemble(self.circ, shots=np.int64(2048)) - self.assertEqual(qobj.config.shots, 2048) - - def test_default_shots_greater_than_max_shots(self): - """Test assembling with default shots greater than max shots""" - self.backend_config.max_shots = 5 - - with self.assertWarns(DeprecationWarning): - qobj = assemble(self.circ, self.backend) - - self.assertIsInstance(qobj, QasmQobj) - self.assertEqual(qobj.config.shots, 5) - - def test_assemble_initialize(self): - """Test assembling a circuit with an initialize.""" - q = QuantumRegister(2, name="q") - circ = QuantumCircuit(q, name="circ") - circ.initialize([1 / np.sqrt(2), 0, 0, 1 / np.sqrt(2)], q[:]) - - with self.assertWarns(DeprecationWarning): - qobj = assemble(circ) - - self.assertIsInstance(qobj, QasmQobj) - self.assertEqual(qobj.experiments[0].instructions[0].name, "initialize") - np.testing.assert_almost_equal( - qobj.experiments[0].instructions[0].params, [0.7071067811865, 0, 0, 0.707106781186] - ) - - def test_assemble_meas_level_meas_return(self): - """Test assembling a circuit schedule with `meas_level`.""" - with self.assertWarns(DeprecationWarning): - qobj = assemble(self.circ, meas_level=1, meas_return="single") - - self.assertIsInstance(qobj, QasmQobj) - self.assertEqual(qobj.config.meas_level, 1) - self.assertEqual(qobj.config.meas_return, "single") - - # no meas_level set - with self.assertWarns(DeprecationWarning): - qobj = assemble(self.circ) - - self.assertIsInstance(qobj, QasmQobj) - self.assertEqual(qobj.config.meas_level, 2) - self.assertEqual(hasattr(qobj.config, "meas_return"), False) - - def test_assemble_backend_rep_delays(self): - """Check that rep_delay is properly set from backend values.""" - rep_delay_range = [2.5e-3, 4.5e-3] # sec - default_rep_delay = 3.0e-3 - setattr(self.backend_config, "rep_delay_range", rep_delay_range) - setattr(self.backend_config, "default_rep_delay", default_rep_delay) - - # dynamic rep rates off - setattr(self.backend_config, "dynamic_reprate_enabled", False) - with self.assertWarns(DeprecationWarning): - qobj = assemble(self.circ, self.backend) - self.assertEqual(hasattr(qobj.config, "rep_delay"), False) - - # dynamic rep rates on - setattr(self.backend_config, "dynamic_reprate_enabled", True) - with self.assertWarns(DeprecationWarning): - qobj = assemble(self.circ, self.backend) - self.assertEqual(qobj.config.rep_delay, default_rep_delay * 1e6) - - def test_assemble_user_rep_time_delay(self): - """Check that user runtime config rep_delay works.""" - # set custom rep_delay in runtime config - rep_delay = 2.2e-6 - rep_delay_range = [0, 3e-6] # sec - setattr(self.backend_config, "rep_delay_range", rep_delay_range) - - # dynamic rep rates off (no default so shouldn't be in qobj config) - setattr(self.backend_config, "dynamic_reprate_enabled", False) - with self.assertWarns(DeprecationWarning): - qobj = assemble(self.circ, self.backend, rep_delay=rep_delay) - self.assertEqual(hasattr(qobj.config, "rep_delay"), False) - - # turn on dynamic rep rates, rep_delay should be set - setattr(self.backend_config, "dynamic_reprate_enabled", True) - with self.assertWarns(DeprecationWarning): - qobj = assemble(self.circ, self.backend, rep_delay=rep_delay) - self.assertEqual(qobj.config.rep_delay, 2.2) - - # test ``rep_delay=0`` - with self.assertWarns(DeprecationWarning): - qobj = assemble(self.circ, self.backend, rep_delay=0) - self.assertEqual(qobj.config.rep_delay, 0) - - # use ``rep_delay`` outside of ``rep_delay_range``` - rep_delay_large = 5.0e-6 - with self.assertWarns(DeprecationWarning): - with self.assertRaises(QiskitError): - assemble(self.circ, self.backend, rep_delay=rep_delay_large) - - def test_assemble_opaque_inst(self): - """Test opaque instruction is assembled as-is""" - opaque_inst = Instruction(name="my_inst", num_qubits=4, num_clbits=2, params=[0.5, 0.4]) - q = QuantumRegister(6, name="q") - c = ClassicalRegister(4, name="c") - circ = QuantumCircuit(q, c, name="circ") - circ.append(opaque_inst, [q[0], q[2], q[5], q[3]], [c[3], c[0]]) - - with self.assertWarns(DeprecationWarning): - qobj = assemble(circ) - - self.assertIsInstance(qobj, QasmQobj) - self.assertEqual(len(qobj.experiments[0].instructions), 1) - self.assertEqual(qobj.experiments[0].instructions[0].name, "my_inst") - self.assertEqual(qobj.experiments[0].instructions[0].qubits, [0, 2, 5, 3]) - self.assertEqual(qobj.experiments[0].instructions[0].memory, [3, 0]) - self.assertEqual(qobj.experiments[0].instructions[0].params, [0.5, 0.4]) - - def test_assemble_unroll_parametervector(self): - """Verify that assemble unrolls parametervectors ref #5467""" - pv1 = ParameterVector("pv1", 3) - pv2 = ParameterVector("pv2", 3) - qc = QuantumCircuit(2, 2) - for i in range(3): - qc.rx(pv1[i], 0) - qc.ry(pv2[i], 1) - qc.barrier() - qc.measure([0, 1], [0, 1]) - - qc.assign_parameters({pv1: [0.1, 0.2, 0.3], pv2: [0.4, 0.5, 0.6]}) - - with self.assertWarns(DeprecationWarning): - qobj = assemble(qc, parameter_binds=[{pv1: [0.1, 0.2, 0.3], pv2: [0.4, 0.5, 0.6]}]) - - self.assertIsInstance(qobj, QasmQobj) - self.assertEqual(qobj.experiments[0].instructions[0].params[0], 0.100000000000000) - self.assertEqual(qobj.experiments[0].instructions[1].params[0], 0.400000000000000) - self.assertEqual(qobj.experiments[0].instructions[2].params[0], 0.200000000000000) - self.assertEqual(qobj.experiments[0].instructions[3].params[0], 0.500000000000000) - self.assertEqual(qobj.experiments[0].instructions[4].params[0], 0.300000000000000) - self.assertEqual(qobj.experiments[0].instructions[5].params[0], 0.600000000000000) - - def test_measure_to_registers_when_conditionals(self): - """Verify assemble_circuits maps all measure ops on to a register slot - for a circuit containing conditionals.""" - qr = QuantumRegister(2) - cr1 = ClassicalRegister(1) - cr2 = ClassicalRegister(2) - qc = QuantumCircuit(qr, cr1, cr2) - - qc.measure(qr[0], cr1) # Measure not required for a later conditional - qc.measure(qr[1], cr2[1]) # Measure required for a later conditional - with self.assertWarns(DeprecationWarning): - qc.h(qr[1]).c_if(cr2, 3) - - with self.assertWarns(DeprecationWarning): - qobj = assemble(qc) - - first_measure, second_measure = ( - op for op in qobj.experiments[0].instructions if op.name == "measure" - ) - - self.assertTrue(hasattr(first_measure, "register")) - self.assertEqual(first_measure.register, first_measure.memory) - self.assertTrue(hasattr(second_measure, "register")) - self.assertEqual(second_measure.register, second_measure.memory) - - def test_convert_to_bfunc_plus_conditional(self): - """Verify assemble_circuits converts conditionals from QASM to Qobj.""" - qr = QuantumRegister(1) - cr = ClassicalRegister(1) - qc = QuantumCircuit(qr, cr) - - with self.assertWarns(DeprecationWarning): - qc.h(qr[0]).c_if(cr, 1) - - with self.assertWarns(DeprecationWarning): - qobj = assemble(qc) - - bfunc_op, h_op = qobj.experiments[0].instructions - - self.assertEqual(bfunc_op.name, "bfunc") - self.assertEqual(bfunc_op.mask, "0x1") - self.assertEqual(bfunc_op.val, "0x1") - self.assertEqual(bfunc_op.relation, "==") - - self.assertTrue(hasattr(h_op, "conditional")) - self.assertEqual(bfunc_op.register, h_op.conditional) - - def test_convert_to_bfunc_plus_conditional_onebit(self): - """Verify assemble_circuits converts single bit conditionals from QASM to Qobj.""" - qr = QuantumRegister(1) - cr = ClassicalRegister(3) - qc = QuantumCircuit(qr, cr) - - with self.assertWarns(DeprecationWarning): - qc.h(qr[0]).c_if(cr[2], 1) - - with self.assertWarns(DeprecationWarning): - qobj = assemble(qc) - - inst_set = qobj.experiments[0].instructions - [bfunc_op, h_op] = inst_set - - self.assertEqual(len(inst_set), 2) - self.assertEqual(bfunc_op.name, "bfunc") - self.assertEqual(bfunc_op.mask, "0x4") - self.assertEqual(bfunc_op.val, "0x4") - self.assertEqual(bfunc_op.relation, "==") - - self.assertTrue(hasattr(h_op, "conditional")) - self.assertEqual(bfunc_op.register, h_op.conditional) - - def test_resize_value_to_register(self): - """Verify assemble_circuits converts the value provided on the classical - creg to its mapped location on the device register.""" - qr = QuantumRegister(1) - cr1 = ClassicalRegister(2) - cr2 = ClassicalRegister(2) - cr3 = ClassicalRegister(1) - qc = QuantumCircuit(qr, cr1, cr2, cr3) - - with self.assertWarns(DeprecationWarning): - qc.h(qr[0]).c_if(cr2, 2) - - with self.assertWarns(DeprecationWarning): - qobj = assemble(qc) - - bfunc_op, h_op = qobj.experiments[0].instructions - - self.assertEqual(bfunc_op.name, "bfunc") - self.assertEqual(bfunc_op.mask, "0xC") - self.assertEqual(bfunc_op.val, "0x8") - self.assertEqual(bfunc_op.relation, "==") - - self.assertTrue(hasattr(h_op, "conditional")) - self.assertEqual(bfunc_op.register, h_op.conditional) - - def test_assemble_circuits_raises_for_bind_circuit_mismatch(self): - """Verify assemble_circuits raises error for parameterized circuits without matching - binds.""" - qr = QuantumRegister(2) - x = Parameter("x") - y = Parameter("y") - - full_bound_circ = QuantumCircuit(qr) - full_param_circ = QuantumCircuit(qr) - partial_param_circ = QuantumCircuit(qr) - - partial_param_circ.p(x, qr[0]) - - full_param_circ.p(x, qr[0]) - full_param_circ.p(y, qr[1]) - - partial_bind_args = {"parameter_binds": [{x: 1}, {x: 0}]} - full_bind_args = {"parameter_binds": [{x: 1, y: 1}, {x: 0, y: 0}]} - inconsistent_bind_args = {"parameter_binds": [{x: 1}, {x: 0, y: 0}]} - - with self.assertWarns(DeprecationWarning): - # Raise when parameters passed for non-parametric circuit - self.assertRaises(QiskitError, assemble, full_bound_circ, **partial_bind_args) - - # Raise when no parameters passed for parametric circuit - self.assertRaises(QiskitError, assemble, partial_param_circ) - self.assertRaises(QiskitError, assemble, full_param_circ) - - # Raise when circuit has more parameters than run_config - self.assertRaises(QiskitError, assemble, full_param_circ, **partial_bind_args) - - # Raise when not all circuits have all parameters - self.assertRaises( - QiskitError, assemble, [full_param_circ, partial_param_circ], **full_bind_args - ) - - # Raise when not all binds have all circuit params - self.assertRaises(QiskitError, assemble, full_param_circ, **inconsistent_bind_args) - - def test_assemble_circuits_rases_for_bind_mismatch_over_expressions(self): - """Verify assemble_circuits raises for invalid binds for circuit including - ParameterExpressions. - """ - qr = QuantumRegister(1) - x = Parameter("x") - y = Parameter("y") - - expr_circ = QuantumCircuit(qr) - - expr_circ.p(x + y, qr[0]) - - partial_bind_args = {"parameter_binds": [{x: 1}, {x: 0}]} - - with self.assertWarns(DeprecationWarning): - # Raise when no parameters passed for parametric circuit - self.assertRaises(QiskitError, assemble, expr_circ) - - with self.assertWarns(DeprecationWarning): - # Raise when circuit has more parameters than run_config - self.assertRaises(QiskitError, assemble, expr_circ, **partial_bind_args) - - def test_assemble_circuits_binds_parameters(self): - """Verify assemble_circuits applies parameter bindings and output circuits are bound.""" - qr = QuantumRegister(1) - qc1 = QuantumCircuit(qr) - qc2 = QuantumCircuit(qr) - qc3 = QuantumCircuit(qr) - - x = Parameter("x") - y = Parameter("y") - sum_ = x + y - product_ = x * y - - qc1.u(x, y, 0, qr[0]) - - qc2.rz(x, qr[0]) - qc2.rz(y, qr[0]) - - qc3.u(sum_, product_, 0, qr[0]) - - bind_args = {"parameter_binds": [{x: 0, y: 0}, {x: 1, y: 0}, {x: 1, y: 1}]} - - with self.assertWarns(DeprecationWarning): - qobj = assemble([qc1, qc2, qc3], **bind_args) - - self.assertEqual(len(qobj.experiments), 9) - self.assertEqual( - [len(expt.instructions) for expt in qobj.experiments], [1, 1, 1, 2, 2, 2, 1, 1, 1] - ) - - def _qobj_inst_params(expt_no, inst_no): - expt = qobj.experiments[expt_no] - inst = expt.instructions[inst_no] - return [float(p) for p in inst.params] - - self.assertEqual(_qobj_inst_params(0, 0), [0, 0, 0]) - self.assertEqual(_qobj_inst_params(1, 0), [1, 0, 0]) - self.assertEqual(_qobj_inst_params(2, 0), [1, 1, 0]) - - self.assertEqual(_qobj_inst_params(3, 0), [0]) - self.assertEqual(_qobj_inst_params(3, 1), [0]) - self.assertEqual(_qobj_inst_params(4, 0), [1]) - self.assertEqual(_qobj_inst_params(4, 1), [0]) - self.assertEqual(_qobj_inst_params(5, 0), [1]) - self.assertEqual(_qobj_inst_params(5, 1), [1]) - - self.assertEqual(_qobj_inst_params(6, 0), [0, 0, 0]) - self.assertEqual(_qobj_inst_params(7, 0), [1, 0, 0]) - self.assertEqual(_qobj_inst_params(8, 0), [2, 1, 0]) - - def test_init_qubits_default(self): - """Check that the init_qubits=None assemble option is passed on to the qobj.""" - with self.assertWarns(DeprecationWarning): - qobj = assemble(self.circ) - self.assertEqual(qobj.config.init_qubits, True) - - def test_init_qubits_true(self): - """Check that the init_qubits=True assemble option is passed on to the qobj.""" - with self.assertWarns(DeprecationWarning): - qobj = assemble(self.circ, init_qubits=True) - self.assertEqual(qobj.config.init_qubits, True) - - def test_init_qubits_false(self): - """Check that the init_qubits=False assemble option is passed on to the qobj.""" - with self.assertWarns(DeprecationWarning): - qobj = assemble(self.circ, init_qubits=False) - self.assertEqual(qobj.config.init_qubits, False) - - def test_circuit_with_global_phase(self): - """Test that global phase for a circuit is handled correctly.""" - circ = QuantumCircuit(2) - circ.h(0) - circ.cx(0, 1) - circ.measure_all() - circ.global_phase = 0.3 * np.pi - with self.assertWarns(DeprecationWarning): - qobj = assemble([circ, self.circ]) - self.assertEqual(getattr(qobj.experiments[1].header, "global_phase"), 0) - self.assertEqual(getattr(qobj.experiments[0].header, "global_phase"), 0.3 * np.pi) - - def test_circuit_global_phase_gate_definitions(self): - """Test circuit with global phase on gate definitions.""" - - class TestGate(Gate): - """dummy gate""" - - def __init__(self): - super().__init__("test_gate", 1, []) - - def _define(self): - circ_def = QuantumCircuit(1) - circ_def.x(0) - circ_def.global_phase = np.pi - self._definition = circ_def - - gate = TestGate() - circ = QuantumCircuit(1) - circ.append(gate, [0]) - with self.assertWarns(DeprecationWarning): - qobj = assemble([circ]) - self.assertEqual(getattr(qobj.experiments[0].header, "global_phase"), 0) - circ.global_phase = np.pi / 2 - with self.assertWarns(DeprecationWarning): - qobj = assemble([circ]) - self.assertEqual(getattr(qobj.experiments[0].header, "global_phase"), np.pi / 2) - - def test_pulse_gates_single_circ(self): - """Test that we can add calibrations to circuits.""" - theta = Parameter("theta") - circ = QuantumCircuit(2) - circ.h(0) - circ.append(RxGate(3.14), [0]) - circ.append(RxGate(theta), [1]) - circ = circ.assign_parameters({theta: 3.14}) - - with self.assertWarns(DeprecationWarning): - with pulse.build() as custom_h_schedule: - pulse.play(pulse.library.Drag(50, 0.15, 4, 2), pulse.DriveChannel(0)) - - with pulse.build() as x180: - pulse.play(pulse.library.Gaussian(50, 0.2, 5), pulse.DriveChannel(1)) - - circ.add_calibration("h", [0], custom_h_schedule) - circ.add_calibration(RxGate(3.14), [0], x180) - circ.add_calibration(RxGate(3.14), [1], x180) - - qobj = assemble(circ, FakeOpenPulse2Q()) - # Only one circuit, so everything is stored at the job level - cals = qobj.config.calibrations - lib = qobj.config.pulse_library - self.assertFalse(hasattr(qobj.experiments[0].config, "calibrations")) - self.assertEqual([gate.name == "rxtheta" for gate in cals.gates].count(True), 2) - self.assertEqual([gate.name == "h" for gate in cals.gates].count(True), 1) - self.assertEqual(len(lib), 2) - self.assertTrue(all(len(item.samples) == 50 for item in lib)) - - def test_custom_pulse_gates_single_circ(self): - """Test that we can add calibrations to circuits of pulses which are not in - qiskit.qobj.converters.pulse_instruction.ParametricPulseShapes""" - circ = QuantumCircuit(2) - circ.h(0) - - with self.assertWarns(DeprecationWarning): - with pulse.build() as custom_h_schedule: - pulse.play(pulse.library.Triangle(50, 0.1, 0.2), pulse.DriveChannel(0)) - - circ.add_calibration("h", [0], custom_h_schedule) - - qobj = assemble(circ, FakeOpenPulse2Q()) - lib = qobj.config.pulse_library - self.assertEqual(len(lib), 1) - with self.assertWarns(DeprecationWarning): - np.testing.assert_almost_equal( - lib[0].samples, pulse.library.Triangle(50, 0.1, 0.2).get_waveform().samples - ) - - def test_pulse_gates_with_parameteric_pulses(self): - """Test that pulse gates are assembled efficiently for backends that enable - parametric pulses. - """ - with self.assertWarns(DeprecationWarning): - with pulse.build() as custom_h_schedule: - pulse.play(pulse.library.Drag(50, 0.15, 4, 2), pulse.DriveChannel(0)) - - circ = QuantumCircuit(2) - circ.h(0) - - with self.assertWarns(DeprecationWarning): - circ.add_calibration("h", [0], custom_h_schedule) - backend = FakeOpenPulse2Q() - backend.configuration().parametric_pulses = ["drag"] - with self.assertWarns(DeprecationWarning): - qobj = assemble(circ, backend) - self.assertFalse(hasattr(qobj.config, "pulse_library")) - self.assertTrue(hasattr(qobj.config, "calibrations")) - - def test_pulse_gates_multiple_circuits(self): - """Test one circuit with cals and another without.""" - with self.assertWarns(DeprecationWarning): - with pulse.build() as dummy_sched: - pulse.play(pulse.library.Drag(50, 0.15, 4, 2), pulse.DriveChannel(0)) - - circ = QuantumCircuit(2) - circ.h(0) - circ.append(RxGate(3.14), [1]) - with self.assertWarns(DeprecationWarning): - circ.add_calibration("h", [0], dummy_sched) - circ.add_calibration(RxGate(3.14), [1], dummy_sched) - - circ2 = QuantumCircuit(2) - circ2.h(0) - - with self.assertWarns(DeprecationWarning): - qobj = assemble([circ, circ2], FakeOpenPulse2Q()) - self.assertEqual(len(qobj.config.pulse_library), 1) - self.assertEqual(len(qobj.experiments[0].config.calibrations.gates), 2) - self.assertFalse(hasattr(qobj.config, "calibrations")) - self.assertFalse(hasattr(qobj.experiments[1].config, "calibrations")) - - def test_pulse_gates_common_cals(self): - """Test that common calibrations are added at the top level.""" - with self.assertWarns(DeprecationWarning): - with pulse.build() as dummy_sched: - pulse.play(pulse.library.Drag(50, 0.15, 4, 2), pulse.DriveChannel(0)) - - circ = QuantumCircuit(2) - circ.h(0) - circ.append(RxGate(3.14), [1]) - with self.assertWarns(DeprecationWarning): - circ.add_calibration("h", [0], dummy_sched) - circ.add_calibration(RxGate(3.14), [1], dummy_sched) - - circ2 = QuantumCircuit(2) - circ2.h(0) - with self.assertWarns(DeprecationWarning): - circ2.add_calibration(RxGate(3.14), [1], dummy_sched) - - with self.assertWarns(DeprecationWarning): - qobj = assemble([circ, circ2], FakeOpenPulse2Q()) - # Identical pulses are only added once - self.assertEqual(len(qobj.config.pulse_library), 1) - # Identical calibrations are only added once - self.assertEqual(qobj.config.calibrations.gates[0].name, "rxtheta") - self.assertEqual(qobj.config.calibrations.gates[0].params, [3.14]) - self.assertEqual(qobj.config.calibrations.gates[0].qubits, [1]) - self.assertEqual(len(qobj.experiments[0].config.calibrations.gates), 1) - self.assertFalse(hasattr(qobj.experiments[1].config, "calibrations")) - - def test_assemble_adds_circuit_metadata_to_experiment_header(self): - """Verify that any circuit metadata is added to the experiment header.""" - circ = QuantumCircuit(2, metadata={"experiment_type": "gst", "execution_number": "1234"}) - with self.assertWarns(DeprecationWarning): - qobj = assemble(circ, shots=100, memory=False, seed_simulator=6) - self.assertEqual( - qobj.experiments[0].header.metadata, - {"experiment_type": "gst", "execution_number": "1234"}, - ) - - def test_pulse_gates_delay_only(self): - """Test that a single delay gate is translated to an instruction.""" - circ = QuantumCircuit(2) - circ.append(Gate("test", 1, []), [0]) - with self.assertWarns(DeprecationWarning): - test_sched = pulse.Delay(64, DriveChannel(0)) + pulse.Delay(160, DriveChannel(0)) - circ.add_calibration("test", [0], test_sched) - qobj = assemble(circ, FakeOpenPulse2Q()) - self.assertEqual(len(qobj.config.calibrations.gates[0].instructions), 2) - self.assertEqual( - qobj.config.calibrations.gates[0].instructions[1].to_dict(), - {"name": "delay", "t0": 64, "ch": "d0", "duration": 160}, - ) - - def test_job_qubit_meas_los_no_range(self): - """Test that adding job qubit/meas lo freq lists are assembled into the qobj.config, w/ out - any lo range.""" - with self.assertWarns(DeprecationWarning): - qobj = assemble( - self.circ, - backend=self.backend, - qubit_lo_freq=self.default_qubit_lo_freq, - meas_lo_freq=self.default_meas_lo_freq, - ) - - # convert to ghz - qubit_lo_freq_ghz = [freq / 1e9 for freq in self.default_qubit_lo_freq] - meas_lo_freq_ghz = [freq / 1e9 for freq in self.default_meas_lo_freq] - self.assertEqual(qobj.config.qubit_lo_freq, qubit_lo_freq_ghz) - self.assertEqual(qobj.config.meas_lo_freq, meas_lo_freq_ghz) - - def test_job_lo_errors(self): - """Test that job lo's are checked against the lo ranges and that errors are thrown if either - quantity has an incorrect length or type.""" - qubit_lo_range = [[freq - 5e6, freq + 5e6] for freq in self.default_qubit_lo_freq] - meas_lo_range = [[freq - 5e6, freq + 5e6] for freq in self.default_meas_lo_freq] - - # lo range not a nested list - with self.assertWarns(DeprecationWarning): - with self.assertRaises(QiskitError): - assemble( - self.circ, - backend=self.backend, - qubit_lo_freq=self.default_qubit_lo_freq, - meas_lo_freq=self.default_meas_lo_freq, - qubit_lo_range=[4.995e9 for i in range(self.num_qubits)], - meas_lo_range=meas_lo_range, - ) - - # qubit lo range inner list not 2d - with self.assertWarns(DeprecationWarning): - with self.assertRaises(QiskitError): - assemble( - self.circ, - backend=self.backend, - qubit_lo_freq=self.default_qubit_lo_freq, - meas_lo_freq=self.default_meas_lo_freq, - qubit_lo_range=qubit_lo_range, - meas_lo_range=[[6.695e9] for i in range(self.num_qubits)], - ) - - # meas lo range inner list not 2d - with self.assertWarns(DeprecationWarning): - with self.assertRaises(QiskitError): - assemble( - self.circ, - backend=self.backend, - qubit_lo_freq=self.default_qubit_lo_freq, - meas_lo_freq=self.default_meas_lo_freq, - qubit_lo_range=qubit_lo_range, - meas_lo_range=[[6.695e9] for i in range(self.num_qubits)], - ) - - # qubit lo out of range - with self.assertWarns(DeprecationWarning): - with self.assertRaises(QiskitError): - assemble( - self.circ, - backend=self.backend, - qubit_lo_freq=self.default_qubit_lo_freq, - meas_lo_freq=self.default_meas_lo_freq, - qubit_lo_range=[[5.005e9, 5.010e9] for i in range(self.num_qubits)], - meas_lo_range=meas_lo_range, - ) - - # meas lo out of range - with self.assertWarns(DeprecationWarning): - with self.assertRaises(QiskitError): - assemble( - self.circ, - backend=self.backend, - qubit_lo_freq=self.default_qubit_lo_freq, - meas_lo_freq=self.default_meas_lo_freq, - qubit_lo_range=qubit_lo_range, - meas_lo_range=[[6.705e9, 6.710e9] for i in range(self.num_qubits)], - ) - - def test_job_qubit_meas_los_w_range(self): - """Test that adding job qubit/meas lo freq lists are assembled into the qobj.config, w/ lo - ranges input. Verify that lo ranges do not enter into the config.""" - qubit_lo_range = [[freq - 5e6, freq + 5e6] for freq in self.default_qubit_lo_freq] - meas_lo_range = [[freq - 5e6, freq + 5e6] for freq in self.default_meas_lo_freq] - with self.assertWarns(DeprecationWarning): - qobj = assemble( - self.circ, - backend=self.backend, - qubit_lo_freq=self.default_qubit_lo_freq, - meas_lo_freq=self.default_meas_lo_freq, - qubit_lo_range=qubit_lo_range, - meas_lo_range=meas_lo_range, - ) - - # convert to ghz - qubit_lo_freq_ghz = [freq / 1e9 for freq in self.default_qubit_lo_freq] - meas_lo_freq_ghz = [freq / 1e9 for freq in self.default_meas_lo_freq] - self.assertEqual(qobj.config.qubit_lo_freq, qubit_lo_freq_ghz) - self.assertEqual(qobj.config.meas_lo_freq, meas_lo_freq_ghz) - self.assertNotIn("qubit_lo_range", qobj.config.to_dict()) - self.assertNotIn("meas_lo_range", qobj.config.to_dict()) - - def test_assemble_single_circ_single_lo_config(self): - """Test assembling a single circuit, with a single experiment level lo config.""" - with self.assertWarns(DeprecationWarning): - qobj = assemble( - self.circ, - self.backend, - qubit_lo_freq=self.default_qubit_lo_freq, - meas_lo_freq=self.default_meas_lo_freq, - schedule_los=self.user_lo_config, - ) - - self.assertListEqual(qobj.config.qubit_lo_freq, [5.55, 5, 5, 4.91, 5]) - self.assertListEqual(qobj.config.meas_lo_freq, [6.64, 6.7, 6.7, 6.7, 6.1]) - self.assertEqual(len(qobj.experiments), 1) - - def test_assemble_single_circ_single_lo_config_dict(self): - """Test assembling a single circuit, with a single experiment level lo config supplied as - dictionary.""" - with self.assertWarns(DeprecationWarning): - qobj = assemble( - self.circ, - self.backend, - qubit_lo_freq=self.default_qubit_lo_freq, - meas_lo_freq=self.default_meas_lo_freq, - schedule_los=self.user_lo_config_dict, - ) - - self.assertListEqual(qobj.config.qubit_lo_freq, [5.55, 5, 5, 4.91, 5]) - self.assertListEqual(qobj.config.meas_lo_freq, [6.64, 6.7, 6.7, 6.7, 6.1]) - self.assertEqual(len(qobj.experiments), 1) - - def test_assemble_single_circ_multi_lo_config(self): - """Test assembling a single circuit, with multiple experiment level lo configs (frequency - sweep). - """ - with self.assertWarns(DeprecationWarning): - user_lo_config_dict2 = { - pulse.DriveChannel(1): 5.55e9, - pulse.MeasureChannel(1): 6.64e9, - pulse.DriveChannel(4): 4.91e9, - pulse.MeasureChannel(3): 6.1e9, - } - user_lo_config2 = pulse.LoConfig(user_lo_config_dict2) - with self.assertWarns(DeprecationWarning): - qobj = assemble( - self.circ, - self.backend, - qubit_lo_freq=self.default_qubit_lo_freq, - meas_lo_freq=self.default_meas_lo_freq, - schedule_los=[self.user_lo_config, user_lo_config2], - ) - - qubit_lo_freq_ghz = [freq / 1e9 for freq in self.default_qubit_lo_freq] - meas_lo_freq_ghz = [freq / 1e9 for freq in self.default_meas_lo_freq] - self.assertListEqual(qobj.config.qubit_lo_freq, qubit_lo_freq_ghz) - self.assertListEqual(qobj.config.meas_lo_freq, meas_lo_freq_ghz) - self.assertEqual(len(qobj.experiments), 2) - - # experiment 0 los - self.assertEqual(qobj.experiments[0].config.qubit_lo_freq, [5.55, 5, 5, 4.91, 5]) - self.assertEqual(qobj.experiments[0].config.meas_lo_freq, [6.64, 6.7, 6.7, 6.7, 6.1]) - # experiment 1 los - self.assertEqual(qobj.experiments[1].config.qubit_lo_freq, [5, 5.55, 5, 5, 4.91]) - self.assertEqual(qobj.experiments[1].config.meas_lo_freq, [6.7, 6.64, 6.7, 6.1, 6.7]) - - def test_assemble_multi_circ_multi_lo_config(self): - """Test assembling circuits, with the same number of experiment level lo configs (n:n - setup).""" - with self.assertWarns(DeprecationWarning): - user_lo_config_dict2 = { - pulse.DriveChannel(1): 5.55e9, - pulse.MeasureChannel(1): 6.64e9, - pulse.DriveChannel(4): 4.91e9, - pulse.MeasureChannel(3): 6.1e9, - } - user_lo_config2 = pulse.LoConfig(user_lo_config_dict2) - with self.assertWarns(DeprecationWarning): - qobj = assemble( - [self.circ, self.circ], - self.backend, - qubit_lo_freq=self.default_qubit_lo_freq, - meas_lo_freq=self.default_meas_lo_freq, - schedule_los=[self.user_lo_config, user_lo_config2], - ) - - qubit_lo_freq_ghz = [freq / 1e9 for freq in self.default_qubit_lo_freq] - meas_lo_freq_ghz = [freq / 1e9 for freq in self.default_meas_lo_freq] - self.assertListEqual(qobj.config.qubit_lo_freq, qubit_lo_freq_ghz) - self.assertListEqual(qobj.config.meas_lo_freq, meas_lo_freq_ghz) - self.assertEqual(len(qobj.experiments), 2) - - # experiment 0 los - self.assertEqual(qobj.experiments[0].config.qubit_lo_freq, [5.55, 5, 5, 4.91, 5]) - self.assertEqual(qobj.experiments[0].config.meas_lo_freq, [6.64, 6.7, 6.7, 6.7, 6.1]) - # experiment 1 los - self.assertEqual(qobj.experiments[1].config.qubit_lo_freq, [5, 5.55, 5, 5, 4.91]) - self.assertEqual(qobj.experiments[1].config.meas_lo_freq, [6.7, 6.64, 6.7, 6.1, 6.7]) - - def test_assemble_multi_circ_single_lo_config(self): - """Test assembling multiple circuits, with a single experiment level lo config (should - override job level).""" - with self.assertWarns(DeprecationWarning): - qobj = assemble( - [self.circ, self.circ], - self.backend, - qubit_lo_freq=self.default_qubit_lo_freq, - meas_lo_freq=self.default_meas_lo_freq, - schedule_los=self.user_lo_config, - ) - - self.assertListEqual(qobj.config.qubit_lo_freq, [5.55, 5, 5, 4.91, 5]) - self.assertListEqual(qobj.config.meas_lo_freq, [6.64, 6.7, 6.7, 6.7, 6.1]) - self.assertEqual(len(qobj.experiments), 2) - - def test_assemble_multi_circ_wrong_number_of_multi_lo_configs(self): - """Test assembling circuits, with a different number of experiment level lo configs (n:m - setup). - """ - with self.assertWarns(DeprecationWarning): - with self.assertRaises(QiskitError): - assemble( - [self.circ, self.circ, self.circ], - self.backend, - qubit_lo_freq=self.default_qubit_lo_freq, - meas_lo_freq=self.default_meas_lo_freq, - schedule_los=[self.user_lo_config, self.user_lo_config], - ) - - def test_assemble_circ_lo_config_errors(self): - """Test that lo config errors are raised properly if experiment level los are provided and - some are missing or if default values are not provided. Also check that experiment level lo - range is validated.""" - # no defaults, but have drive/meas experiment level los for each qubit (no error) - with self.assertWarns(DeprecationWarning): - full_lo_config_dict = { - pulse.DriveChannel(0): 4.85e9, - pulse.DriveChannel(1): 4.9e9, - pulse.DriveChannel(2): 4.95e9, - pulse.DriveChannel(3): 5e9, - pulse.DriveChannel(4): 5.05e9, - pulse.MeasureChannel(0): 6.8e9, - pulse.MeasureChannel(1): 6.85e9, - pulse.MeasureChannel(2): 6.9e9, - pulse.MeasureChannel(3): 6.95e9, - pulse.MeasureChannel(4): 7e9, - } - - with self.assertWarns(DeprecationWarning): - qobj = assemble(self.circ, self.backend, schedule_los=full_lo_config_dict) - - self.assertListEqual(qobj.config.qubit_lo_freq, [4.85, 4.9, 4.95, 5, 5.05]) - self.assertListEqual(qobj.config.meas_lo_freq, [6.8, 6.85, 6.9, 6.95, 7]) - self.assertEqual(len(qobj.experiments), 1) - - # no defaults and missing experiment level drive lo raises - missing_drive_lo_config_dict = copy.deepcopy(full_lo_config_dict) - with self.assertWarns(DeprecationWarning): - missing_drive_lo_config_dict.pop(pulse.DriveChannel(0)) - with self.assertRaises(QiskitError): - qobj = assemble(self.circ, self.backend, schedule_los=missing_drive_lo_config_dict) - - # no defaults and missing experiment level meas lo raises - missing_meas_lo_config_dict = copy.deepcopy(full_lo_config_dict) - with self.assertWarns(DeprecationWarning): - missing_meas_lo_config_dict.pop(pulse.MeasureChannel(0)) - with self.assertRaises(QiskitError): - qobj = assemble(self.circ, self.backend, schedule_los=missing_meas_lo_config_dict) - - # verify lo ranges are checked at experiment level - lo_values = list(full_lo_config_dict.values()) - qubit_lo_range = [[freq - 5e6, freq + 5e6] for freq in lo_values[:5]] - meas_lo_range = [[freq - 5e6, freq + 5e6] for freq in lo_values[5:]] - - # out of range drive lo - with self.assertWarns(DeprecationWarning): - full_lo_config_dict[pulse.DriveChannel(0)] -= 5.5e6 - with self.assertRaises(QiskitError): - qobj = assemble( - self.circ, - self.backend, - qubit_lo_range=qubit_lo_range, - schedule_los=full_lo_config_dict, - ) - full_lo_config_dict[pulse.DriveChannel(0)] += 5.5e6 # reset drive value - - with self.assertWarns(DeprecationWarning): - # out of range meas lo - full_lo_config_dict[pulse.MeasureChannel(0)] += 5.5e6 - with self.assertRaises(QiskitError): - qobj = assemble( - self.circ, - self.backend, - meas_lo_range=meas_lo_range, - schedule_los=full_lo_config_dict, - ) - - -class TestPulseAssembler(QiskitTestCase): - """Tests for assembling schedules to qobj.""" - - def setUp(self): - super().setUp() - with self.assertWarns(DeprecationWarning): - self.backend = FakeOpenPulse2Q() - self.backend_config = self.backend.configuration() - - with self.assertWarns(DeprecationWarning): - test_pulse = pulse.Waveform( - samples=np.array([0.02739068, 0.05, 0.05, 0.05, 0.02739068], dtype=np.complex128), - name="pulse0", - ) - - self.schedule = pulse.Schedule(name="fake_experiment") - self.schedule = self.schedule.insert(0, Play(test_pulse, self.backend_config.drive(0))) - for i in range(self.backend_config.n_qubits): - self.schedule = self.schedule.insert( - 5, Acquire(5, self.backend_config.acquire(i), MemorySlot(i)) - ) - - self.user_lo_config_dict = {self.backend_config.drive(0): 4.91e9} - self.user_lo_config = pulse.LoConfig(self.user_lo_config_dict) - - self.default_qubit_lo_freq = [4.9e9, 5.0e9] - self.default_meas_lo_freq = [6.5e9, 6.6e9] - - self.config = {"meas_level": 1, "memory_slot_size": 100, "meas_return": "avg"} - - self.header = {"backend_name": "FakeOpenPulse2Q", "backend_version": "0.0.0"} - - def test_assemble_adds_schedule_metadata_to_experiment_header(self): - """Verify that any circuit metadata is added to the experiment header.""" - self.schedule.metadata = {"experiment_type": "gst", "execution_number": "1234"} - with self.assertWarns(DeprecationWarning): - qobj = assemble( - self.schedule, - shots=100, - qubit_lo_freq=self.default_qubit_lo_freq, - meas_lo_freq=self.default_meas_lo_freq, - schedule_los=[], - ) - self.assertEqual( - qobj.experiments[0].header.metadata, - {"experiment_type": "gst", "execution_number": "1234"}, - ) - - def test_assemble_sample_pulse(self): - """Test that the pulse lib and qobj instruction can be paired up.""" - with self.assertWarns(DeprecationWarning): - schedule = pulse.Schedule() - schedule += pulse.Play( - pulse.Waveform([0.1] * 16, name="test0"), pulse.DriveChannel(0), name="test1" - ) - schedule += pulse.Play( - pulse.Waveform([0.1] * 16, name="test1"), pulse.DriveChannel(0), name="test2" - ) - schedule += pulse.Play( - pulse.Waveform([0.5] * 16, name="test0"), pulse.DriveChannel(0), name="test1" - ) - - with self.assertWarns(DeprecationWarning): - qobj = assemble( - schedule, - qobj_header=self.header, - qubit_lo_freq=self.default_qubit_lo_freq, - meas_lo_freq=self.default_meas_lo_freq, - schedule_los=[], - **self.config, - ) - - test_dict = qobj.to_dict() - experiment = test_dict["experiments"][0] - inst0_name = experiment["instructions"][0]["name"] - inst1_name = experiment["instructions"][1]["name"] - inst2_name = experiment["instructions"][2]["name"] - pulses = {} - for item in test_dict["config"]["pulse_library"]: - pulses[item["name"]] = item["samples"] - self.assertTrue(all(name in pulses for name in [inst0_name, inst1_name, inst2_name])) - # Their pulses are the same - self.assertEqual(inst0_name, inst1_name) - self.assertTrue(np.allclose(pulses[inst0_name], [0.1] * 16)) - self.assertTrue(np.allclose(pulses[inst2_name], [0.5] * 16)) - - def test_assemble_single_schedule_without_lo_config(self): - """Test assembling a single schedule, no lo config.""" - with self.assertWarns(DeprecationWarning): - qobj = assemble( - self.schedule, - qobj_header=self.header, - qubit_lo_freq=self.default_qubit_lo_freq, - meas_lo_freq=self.default_meas_lo_freq, - schedule_los=[], - **self.config, - ) - - test_dict = qobj.to_dict() - self.assertListEqual(test_dict["config"]["qubit_lo_freq"], [4.9, 5.0]) - self.assertEqual(len(test_dict["experiments"]), 1) - self.assertEqual(len(test_dict["experiments"][0]["instructions"]), 2) - - def test_assemble_multi_schedules_without_lo_config(self): - """Test assembling schedules, no lo config.""" - with self.assertWarns(DeprecationWarning): - qobj = assemble( - [self.schedule, self.schedule], - qobj_header=self.header, - qubit_lo_freq=self.default_qubit_lo_freq, - meas_lo_freq=self.default_meas_lo_freq, - **self.config, - ) - - test_dict = qobj.to_dict() - self.assertListEqual(test_dict["config"]["qubit_lo_freq"], [4.9, 5.0]) - self.assertEqual(len(test_dict["experiments"]), 2) - self.assertEqual(len(test_dict["experiments"][0]["instructions"]), 2) - - def test_assemble_single_schedule_with_lo_config(self): - """Test assembling a single schedule, with a single lo config.""" - with self.assertWarns(DeprecationWarning): - qobj = assemble( - self.schedule, - qobj_header=self.header, - qubit_lo_freq=self.default_qubit_lo_freq, - meas_lo_freq=self.default_meas_lo_freq, - schedule_los=self.user_lo_config, - **self.config, - ) - - test_dict = qobj.to_dict() - self.assertListEqual(test_dict["config"]["qubit_lo_freq"], [4.91, 5.0]) - self.assertEqual(len(test_dict["experiments"]), 1) - self.assertEqual(len(test_dict["experiments"][0]["instructions"]), 2) - - def test_assemble_single_schedule_with_lo_config_dict(self): - """Test assembling a single schedule, with a single lo config supplied as dictionary.""" - with self.assertWarns(DeprecationWarning): - qobj = assemble( - self.schedule, - qobj_header=self.header, - qubit_lo_freq=self.default_qubit_lo_freq, - meas_lo_freq=self.default_meas_lo_freq, - schedule_los=self.user_lo_config_dict, - **self.config, - ) - - test_dict = qobj.to_dict() - self.assertListEqual(test_dict["config"]["qubit_lo_freq"], [4.91, 5.0]) - self.assertEqual(len(test_dict["experiments"]), 1) - self.assertEqual(len(test_dict["experiments"][0]["instructions"]), 2) - - def test_assemble_single_schedule_with_multi_lo_configs(self): - """Test assembling a single schedule, with multiple lo configs (frequency sweep).""" - with self.assertWarns(DeprecationWarning): - qobj = assemble( - self.schedule, - qobj_header=self.header, - qubit_lo_freq=self.default_qubit_lo_freq, - meas_lo_freq=self.default_meas_lo_freq, - schedule_los=[self.user_lo_config, self.user_lo_config], - **self.config, - ) - test_dict = qobj.to_dict() - - self.assertListEqual(test_dict["config"]["qubit_lo_freq"], [4.9, 5.0]) - self.assertEqual(len(test_dict["experiments"]), 2) - self.assertEqual(len(test_dict["experiments"][0]["instructions"]), 2) - self.assertDictEqual(test_dict["experiments"][0]["config"], {"qubit_lo_freq": [4.91, 5.0]}) - - def test_assemble_multi_schedules_with_multi_lo_configs(self): - """Test assembling schedules, with the same number of lo configs (n:n setup).""" - with self.assertWarns(DeprecationWarning): - qobj = assemble( - [self.schedule, self.schedule], - qobj_header=self.header, - qubit_lo_freq=self.default_qubit_lo_freq, - meas_lo_freq=self.default_meas_lo_freq, - schedule_los=[self.user_lo_config, self.user_lo_config], - **self.config, - ) - - test_dict = qobj.to_dict() - self.assertListEqual(test_dict["config"]["qubit_lo_freq"], [4.9, 5.0]) - self.assertEqual(len(test_dict["experiments"]), 2) - self.assertEqual(len(test_dict["experiments"][0]["instructions"]), 2) - self.assertDictEqual(test_dict["experiments"][0]["config"], {"qubit_lo_freq": [4.91, 5.0]}) - - def test_assemble_multi_schedules_with_wrong_number_of_multi_lo_configs(self): - """Test assembling schedules, with a different number of lo configs (n:m setup).""" - with self.assertRaises(QiskitError): - with self.assertWarns(DeprecationWarning): - assemble( - [self.schedule, self.schedule, self.schedule], - qobj_header=self.header, - qubit_lo_freq=self.default_qubit_lo_freq, - meas_lo_freq=self.default_meas_lo_freq, - schedule_los=[self.user_lo_config, self.user_lo_config], - **self.config, - ) - - def test_assemble_meas_map(self): - """Test assembling a single schedule, no lo config.""" - with self.assertWarns(DeprecationWarning): - schedule = Schedule(name="fake_experiment") - schedule = schedule.insert(5, Acquire(5, AcquireChannel(0), MemorySlot(0))) - schedule = schedule.insert(5, Acquire(5, AcquireChannel(1), MemorySlot(1))) - - with self.assertWarns(DeprecationWarning): - qobj = assemble( - schedule, - qubit_lo_freq=self.default_qubit_lo_freq, - meas_lo_freq=self.default_meas_lo_freq, - meas_map=[[0], [1]], - ) - self.assertIsInstance(qobj, PulseQobj) - - with self.assertWarns(DeprecationWarning): - qobj = assemble( - schedule, - qubit_lo_freq=self.default_qubit_lo_freq, - meas_lo_freq=self.default_meas_lo_freq, - meas_map=[[0, 1, 2]], - ) - self.assertIsInstance(qobj, PulseQobj) - - def test_assemble_memory_slots(self): - """Test assembling a schedule and inferring number of memoryslots.""" - n_memoryslots = 10 - - # single acquisition - with self.assertWarns(DeprecationWarning): - schedule = Acquire( - 5, self.backend_config.acquire(0), mem_slot=pulse.MemorySlot(n_memoryslots - 1) - ) - - with self.assertWarns(DeprecationWarning): - qobj = assemble( - schedule, - qubit_lo_freq=self.default_qubit_lo_freq, - meas_lo_freq=self.default_meas_lo_freq, - meas_map=[[0], [1]], - ) - - self.assertEqual(qobj.config.memory_slots, n_memoryslots) - # this should be in experimental header as well - self.assertEqual(qobj.experiments[0].header.memory_slots, n_memoryslots) - - # multiple acquisition - with self.assertWarns(DeprecationWarning): - schedule = Acquire( - 5, self.backend_config.acquire(0), mem_slot=pulse.MemorySlot(n_memoryslots - 1) - ) - schedule = schedule.insert( - 10, - Acquire( - 5, self.backend_config.acquire(0), mem_slot=pulse.MemorySlot(n_memoryslots - 1) - ), - ) - - with self.assertWarns(DeprecationWarning): - qobj = assemble( - schedule, - qubit_lo_freq=self.default_qubit_lo_freq, - meas_lo_freq=self.default_meas_lo_freq, - meas_map=[[0], [1]], - ) - - self.assertEqual(qobj.config.memory_slots, n_memoryslots) - # this should be in experimental header as well - self.assertEqual(qobj.experiments[0].header.memory_slots, n_memoryslots) - - def test_assemble_memory_slots_for_schedules(self): - """Test assembling schedules with different memory slots.""" - n_memoryslots = [10, 5, 7] - - schedules = [] - with self.assertWarns(DeprecationWarning): - for n_memoryslot in n_memoryslots: - schedule = Acquire( - 5, self.backend_config.acquire(0), mem_slot=pulse.MemorySlot(n_memoryslot - 1) - ) - schedules.append(schedule) - - with self.assertWarns(DeprecationWarning): - qobj = assemble( - schedules, - qubit_lo_freq=self.default_qubit_lo_freq, - meas_lo_freq=self.default_meas_lo_freq, - meas_map=[[0], [1]], - ) - - self.assertEqual(qobj.config.memory_slots, max(n_memoryslots)) - self.assertEqual(qobj.experiments[0].header.memory_slots, n_memoryslots[0]) - self.assertEqual(qobj.experiments[1].header.memory_slots, n_memoryslots[1]) - self.assertEqual(qobj.experiments[2].header.memory_slots, n_memoryslots[2]) - - def test_pulse_name_conflicts(self): - """Test that pulse name conflicts can be resolved.""" - with self.assertWarns(DeprecationWarning): - name_conflict_pulse = pulse.Waveform( - samples=np.array([0.02, 0.05, 0.05, 0.05, 0.02], dtype=np.complex128), name="pulse0" - ) - - self.schedule = self.schedule.insert( - 1, Play(name_conflict_pulse, self.backend_config.drive(1)) - ) - - with self.assertWarns(DeprecationWarning): - qobj = assemble( - self.schedule, - qobj_header=self.header, - qubit_lo_freq=self.default_qubit_lo_freq, - meas_lo_freq=self.default_meas_lo_freq, - schedule_los=[], - **self.config, - ) - - self.assertNotEqual(qobj.config.pulse_library[0].name, qobj.config.pulse_library[1].name) - - def test_pulse_name_conflicts_in_other_schedule(self): - """Test two pulses with the same name in different schedule can be resolved.""" - with self.assertWarns(DeprecationWarning): - backend = Fake27QPulseV1() - defaults = backend.defaults() - - schedules = [] - with self.assertWarns(DeprecationWarning): - ch_d0 = pulse.DriveChannel(0) - for amp in (0.1, 0.2): - sched = Schedule() - sched += Play( - pulse.Gaussian(duration=100, amp=amp, sigma=30, name="my_pulse"), ch_d0 - ) - sched += measure(qubits=[0], backend=backend) << 100 - schedules.append(sched) - - with self.assertWarns(DeprecationWarning): - qobj = assemble( - schedules, - qubit_lo_freq=defaults.qubit_freq_est, - meas_lo_freq=defaults.meas_freq_est, - ) - - # two user pulses and one measurement pulse should be contained - self.assertEqual(len(qobj.config.pulse_library), 3) - - def test_assemble_with_delay(self): - """Test that delay instruction is not ignored in assembly.""" - with self.assertWarns(DeprecationWarning): - delay_schedule = pulse.Delay(10, self.backend_config.drive(0)) - delay_schedule += self.schedule - - with self.assertWarns(DeprecationWarning): - delay_qobj = assemble(delay_schedule, self.backend) - - self.assertEqual(delay_qobj.experiments[0].instructions[0].name, "delay") - self.assertEqual(delay_qobj.experiments[0].instructions[0].duration, 10) - self.assertEqual(delay_qobj.experiments[0].instructions[0].t0, 0) - - def test_delay_removed_on_acq_ch(self): - """Test that delay instructions on acquire channels are skipped on assembly with times - shifted properly. - """ - with self.assertWarns(DeprecationWarning): - delay0 = pulse.Delay(5, self.backend_config.acquire(0)) - delay1 = pulse.Delay(7, self.backend_config.acquire(1)) - - sched0 = delay0 - with self.assertWarns(DeprecationWarning): - sched0 += self.schedule # includes ``Acquire`` instr - sched0 += delay1 - - with self.assertWarns(DeprecationWarning): - sched1 = self.schedule # includes ``Acquire`` instr - sched1 += delay0 - sched1 += delay1 - - sched2 = delay0 - sched2 += delay1 - sched2 += self.schedule # includes ``Acquire`` instr - - with self.assertWarns(DeprecationWarning): - delay_qobj = assemble([sched0, sched1, sched2], self.backend) - - # check that no delay instrs occur on acquire channels - is_acq_delay = False - for exp in delay_qobj.experiments: - for instr in exp.instructions: - if instr.name == "delay" and "a" in instr.ch: - is_acq_delay = True - - self.assertFalse(is_acq_delay) - - # check that acquire instr are shifted from ``t0=5`` as needed - self.assertEqual(delay_qobj.experiments[0].instructions[1].t0, 10) - self.assertEqual(delay_qobj.experiments[0].instructions[1].name, "acquire") - self.assertEqual(delay_qobj.experiments[1].instructions[1].t0, 5) - self.assertEqual(delay_qobj.experiments[1].instructions[1].name, "acquire") - self.assertEqual(delay_qobj.experiments[2].instructions[1].t0, 12) - self.assertEqual(delay_qobj.experiments[2].instructions[1].name, "acquire") - - def test_assemble_schedule_enum(self): - """Test assembling a schedule with enum input values to assemble.""" - with self.assertWarns(DeprecationWarning): - qobj = assemble( - self.schedule, - qobj_header=self.header, - qubit_lo_freq=self.default_qubit_lo_freq, - meas_lo_freq=self.default_meas_lo_freq, - schedule_los=[], - meas_level=MeasLevel.CLASSIFIED, - meas_return=MeasReturnType.AVERAGE, - ) - - test_dict = qobj.to_dict() - self.assertEqual(test_dict["config"]["meas_return"], "avg") - self.assertEqual(test_dict["config"]["meas_level"], 2) - - def test_assemble_parametric(self): - """Test that parametric pulses can be assembled properly into a PulseQobj.""" - amp = [0.5, 0.6, 1, 0.2] - angle = [np.pi / 2, 0.6, 0, 0] - with self.assertWarns(DeprecationWarning): - sched = pulse.Schedule(name="test_parametric") - sched += Play( - pulse.Gaussian(duration=25, sigma=4, amp=amp[0], angle=angle[0]), DriveChannel(0) - ) - sched += Play( - pulse.Drag(duration=25, amp=amp[1], angle=angle[1], sigma=7.8, beta=4), - DriveChannel(1), - ) - sched += Play(pulse.Constant(duration=25, amp=amp[2], angle=angle[2]), DriveChannel(2)) - sched += ( - Play( - pulse.GaussianSquare( - duration=150, amp=amp[3], angle=angle[3], sigma=8, width=140 - ), - MeasureChannel(0), - ) - << sched.duration - ) - - with self.assertWarns(DeprecationWarning): - backend = FakeOpenPulse3Q() - backend.configuration().parametric_pulses = [ - "gaussian", - "drag", - "gaussian_square", - "constant", - ] - with self.assertWarns(DeprecationWarning): - qobj = assemble(sched, backend) - - self.assertEqual(qobj.config.pulse_library, []) - qobj_insts = qobj.experiments[0].instructions - self.assertTrue(all(inst.name == "parametric_pulse" for inst in qobj_insts)) - self.assertEqual(qobj_insts[0].pulse_shape, "gaussian") - self.assertEqual(qobj_insts[1].pulse_shape, "drag") - self.assertEqual(qobj_insts[2].pulse_shape, "constant") - self.assertEqual(qobj_insts[3].pulse_shape, "gaussian_square") - self.assertDictEqual( - qobj_insts[0].parameters, - {"duration": 25, "sigma": 4, "amp": amp[0] * np.exp(1j * angle[0])}, - ) - self.assertDictEqual( - qobj_insts[1].parameters, - {"duration": 25, "sigma": 7.8, "amp": amp[1] * np.exp(1j * angle[1]), "beta": 4}, - ) - self.assertDictEqual( - qobj_insts[2].parameters, {"duration": 25, "amp": amp[2] * np.exp(1j * angle[2])} - ) - self.assertDictEqual( - qobj_insts[3].parameters, - {"duration": 150, "sigma": 8, "amp": amp[3] * np.exp(1j * angle[3]), "width": 140}, - ) - self.assertEqual( - qobj.to_dict()["experiments"][0]["instructions"][0]["parameters"]["amp"], - amp[0] * np.exp(1j * angle[0]), - ) - - def test_assemble_parametric_unsupported(self): - """Test that parametric pulses are translated to Waveform if they're not supported - by the backend during assemble time. - """ - with self.assertWarns(DeprecationWarning): - sched = pulse.Schedule(name="test_parametric_to_sample_pulse") - sched += Play( - pulse.Drag(duration=25, amp=0.5, angle=-0.3, sigma=7.8, beta=4), DriveChannel(1) - ) - sched += Play(pulse.Constant(duration=25, amp=1), DriveChannel(2)) - - with self.assertWarns(DeprecationWarning): - backend = FakeOpenPulse3Q() - backend.configuration().parametric_pulses = ["something_extra"] - - with self.assertWarns(DeprecationWarning): - qobj = assemble(sched, backend) - - self.assertNotEqual(qobj.config.pulse_library, []) - qobj_insts = qobj.experiments[0].instructions - self.assertFalse(hasattr(qobj_insts[0], "pulse_shape")) - - def test_assemble_parametric_pulse_kwarg_with_backend_setting(self): - """Test that parametric pulses respect the kwarg over backend""" - with self.assertWarns(DeprecationWarning): - backend = Fake27QPulseV1() - - qc = QuantumCircuit(1, 1) - qc.x(0) - qc.measure(0, 0) - with self.assertWarns(DeprecationWarning): - with pulse.build(backend, name="x") as x_q0: - pulse.play(pulse.Gaussian(duration=128, amp=0.1, sigma=16), pulse.drive_channel(0)) - - qc.add_calibration("x", (0,), x_q0) - - qobj = assemble(qc, backend, parametric_pulses=["gaussian"]) - self.assertEqual(qobj.config.parametric_pulses, ["gaussian"]) - - def test_assemble_parametric_pulse_kwarg_empty_list_with_backend_setting(self): - """Test that parametric pulses respect the kwarg as empty list over backend""" - with self.assertWarns(DeprecationWarning): - backend = Fake27QPulseV1() - - qc = QuantumCircuit(1, 1) - qc.x(0) - qc.measure(0, 0) - with self.assertWarns(DeprecationWarning): - with pulse.build(backend, name="x") as x_q0: - pulse.play(pulse.Gaussian(duration=128, amp=0.1, sigma=16), pulse.drive_channel(0)) - - with self.assertWarns(DeprecationWarning): - qc.add_calibration("x", (0,), x_q0) - qobj = assemble(qc, backend, parametric_pulses=[]) - self.assertEqual(qobj.config.parametric_pulses, []) - - def test_init_qubits_default(self): - """Check that the init_qubits=None assemble option is passed on to the qobj.""" - with self.assertWarns(DeprecationWarning): - qobj = assemble(self.schedule, self.backend) - self.assertEqual(qobj.config.init_qubits, True) - - def test_init_qubits_true(self): - """Check that the init_qubits=True assemble option is passed on to the qobj.""" - with self.assertWarns(DeprecationWarning): - qobj = assemble(self.schedule, self.backend, init_qubits=True) - self.assertEqual(qobj.config.init_qubits, True) - - def test_init_qubits_false(self): - """Check that the init_qubits=False assemble option is passed on to the qobj.""" - with self.assertWarns(DeprecationWarning): - qobj = assemble(self.schedule, self.backend, init_qubits=False) - self.assertEqual(qobj.config.init_qubits, False) - - def test_assemble_backend_rep_times_delays(self): - """Check that rep_time and rep_delay are properly set from backend values.""" - # use first entry from allowed backend values - rep_times = [2.0, 3.0, 4.0] # sec - rep_delay_range = [2.5e-3, 4.5e-3] - default_rep_delay = 3.0e-3 - self.backend_config.rep_times = rep_times - setattr(self.backend_config, "rep_delay_range", rep_delay_range) - setattr(self.backend_config, "default_rep_delay", default_rep_delay) - - # dynamic rep rates off - with self.assertWarns(DeprecationWarning): - qobj = assemble(self.schedule, self.backend) - self.assertEqual(qobj.config.rep_time, int(rep_times[0] * 1e6)) - self.assertEqual(hasattr(qobj.config, "rep_delay"), False) - - # dynamic rep rates on - setattr(self.backend_config, "dynamic_reprate_enabled", True) - # RuntimeWarning bc ``rep_time`` is specified`` when dynamic rep rates not enabled - with self.assertWarns(DeprecationWarning): - qobj = assemble(self.schedule, self.backend) - self.assertEqual(qobj.config.rep_time, int(rep_times[0] * 1e6)) - self.assertEqual(qobj.config.rep_delay, default_rep_delay * 1e6) - - def test_assemble_user_rep_time_delay(self): - """Check that user runtime config rep_time and rep_delay work.""" - # set custom rep_time and rep_delay in runtime config - rep_time = 200.0e-6 - rep_delay = 2.5e-6 - self.config["rep_time"] = rep_time - self.config["rep_delay"] = rep_delay - - # dynamic rep rates off - # RuntimeWarning bc using ``rep_delay`` when dynamic rep rates off - with self.assertWarns(DeprecationWarning): - qobj = assemble(self.schedule, self.backend, **self.config) - self.assertEqual(qobj.config.rep_time, int(rep_time * 1e6)) - self.assertEqual(hasattr(qobj.config, "rep_delay"), False) - - # now remove rep_delay and enable dynamic rep rates - # RuntimeWarning bc using ``rep_time`` when dynamic rep rates are enabled - del self.config["rep_delay"] - setattr(self.backend_config, "dynamic_reprate_enabled", True) - with self.assertWarns(DeprecationWarning): - qobj = assemble(self.schedule, self.backend, **self.config) - self.assertEqual(qobj.config.rep_time, int(rep_time * 1e6)) - self.assertEqual(hasattr(qobj.config, "rep_delay"), False) - - # use ``default_rep_delay`` - # ``rep_time`` comes from allowed backend rep_times - rep_times = [0.5, 1.0, 1.5] # sec - self.backend_config.rep_times = rep_times - setattr(self.backend_config, "rep_delay_range", [0, 3.0e-6]) - setattr(self.backend_config, "default_rep_delay", 2.2e-6) - del self.config["rep_time"] - with self.assertWarns(DeprecationWarning): - qobj = assemble(self.schedule, self.backend, **self.config) - self.assertEqual(qobj.config.rep_time, int(rep_times[0] * 1e6)) - self.assertEqual(qobj.config.rep_delay, 2.2) - - # use qobj ``default_rep_delay`` - self.config["rep_delay"] = 1.5e-6 - with self.assertWarns(DeprecationWarning): - qobj = assemble(self.schedule, self.backend, **self.config) - self.assertEqual(qobj.config.rep_time, int(rep_times[0] * 1e6)) - self.assertEqual(qobj.config.rep_delay, 1.5) - - # use ``rep_delay`` outside of ``rep_delay_range - self.config["rep_delay"] = 5.0e-6 - with self.assertRaises(DeprecationWarning): - assemble(self.schedule, self.backend, **self.config) - - def test_assemble_with_individual_discriminators(self): - """Test that assembly works with individual discriminators.""" - disc_one = Discriminator("disc_one", test_params=True) - disc_two = Discriminator("disc_two", test_params=False) - - with self.assertWarns(DeprecationWarning): - schedule = Schedule() - schedule = schedule.append( - Acquire(5, AcquireChannel(0), MemorySlot(0), discriminator=disc_one), - ) - schedule = schedule.append( - Acquire(5, AcquireChannel(1), MemorySlot(1), discriminator=disc_two), - ) - - with self.assertWarns(DeprecationWarning): - qobj = assemble( - schedule, - qubit_lo_freq=self.default_qubit_lo_freq, - meas_lo_freq=self.default_meas_lo_freq, - meas_map=[[0, 1]], - ) - - qobj_discriminators = qobj.experiments[0].instructions[0].discriminators - self.assertEqual(len(qobj_discriminators), 2) - self.assertEqual(qobj_discriminators[0].name, "disc_one") - self.assertEqual(qobj_discriminators[0].params["test_params"], True) - self.assertEqual(qobj_discriminators[1].name, "disc_two") - self.assertEqual(qobj_discriminators[1].params["test_params"], False) - - def test_assemble_with_single_discriminators(self): - """Test that assembly works with both a single discriminator.""" - disc_one = Discriminator("disc_one", test_params=True) - - with self.assertWarns(DeprecationWarning): - schedule = Schedule() - schedule = schedule.append( - Acquire(5, AcquireChannel(0), MemorySlot(0), discriminator=disc_one), - ) - schedule = schedule.append( - Acquire(5, AcquireChannel(1), MemorySlot(1)), - ) - - with self.assertWarns(DeprecationWarning): - qobj = assemble( - schedule, - qubit_lo_freq=self.default_qubit_lo_freq, - meas_lo_freq=self.default_meas_lo_freq, - meas_map=[[0, 1]], - ) - - qobj_discriminators = qobj.experiments[0].instructions[0].discriminators - self.assertEqual(len(qobj_discriminators), 1) - self.assertEqual(qobj_discriminators[0].name, "disc_one") - self.assertEqual(qobj_discriminators[0].params["test_params"], True) - - def test_assemble_with_unequal_discriminators(self): - """Test that assembly works with incorrect number of discriminators for - number of qubits.""" - disc_one = Discriminator("disc_one", test_params=True) - disc_two = Discriminator("disc_two", test_params=False) - - with self.assertWarns(DeprecationWarning): - schedule = Schedule() - schedule += Acquire(5, AcquireChannel(0), MemorySlot(0), discriminator=disc_one) - schedule += Acquire(5, AcquireChannel(1), MemorySlot(1), discriminator=disc_two) - schedule += Acquire(5, AcquireChannel(2), MemorySlot(2)) - - with self.assertRaises(QiskitError), self.assertWarns(DeprecationWarning): - assemble( - schedule, - qubit_lo_freq=self.default_qubit_lo_freq, - meas_lo_freq=self.default_meas_lo_freq, - meas_map=[[0, 1, 2]], - ) - - def test_assemble_with_individual_kernels(self): - """Test that assembly works with individual kernels.""" - disc_one = Kernel("disc_one", test_params=True) - disc_two = Kernel("disc_two", test_params=False) - - with self.assertWarns(DeprecationWarning): - schedule = Schedule() - schedule = schedule.append( - Acquire(5, AcquireChannel(0), MemorySlot(0), kernel=disc_one), - ) - schedule = schedule.append( - Acquire(5, AcquireChannel(1), MemorySlot(1), kernel=disc_two), - ) - - with self.assertWarns(DeprecationWarning): - qobj = assemble( - schedule, - qubit_lo_freq=self.default_qubit_lo_freq, - meas_lo_freq=self.default_meas_lo_freq, - meas_map=[[0, 1]], - ) - - qobj_kernels = qobj.experiments[0].instructions[0].kernels - self.assertEqual(len(qobj_kernels), 2) - self.assertEqual(qobj_kernels[0].name, "disc_one") - self.assertEqual(qobj_kernels[0].params["test_params"], True) - self.assertEqual(qobj_kernels[1].name, "disc_two") - self.assertEqual(qobj_kernels[1].params["test_params"], False) - - def test_assemble_with_single_kernels(self): - """Test that assembly works with both a single kernel.""" - disc_one = Kernel("disc_one", test_params=True) - - with self.assertWarns(DeprecationWarning): - schedule = Schedule() - schedule = schedule.append( - Acquire(5, AcquireChannel(0), MemorySlot(0), kernel=disc_one), - ) - schedule = schedule.append( - Acquire(5, AcquireChannel(1), MemorySlot(1)), - ) - - with self.assertWarns(DeprecationWarning): - qobj = assemble( - schedule, - qubit_lo_freq=self.default_qubit_lo_freq, - meas_lo_freq=self.default_meas_lo_freq, - meas_map=[[0, 1]], - ) - - qobj_kernels = qobj.experiments[0].instructions[0].kernels - self.assertEqual(len(qobj_kernels), 1) - self.assertEqual(qobj_kernels[0].name, "disc_one") - self.assertEqual(qobj_kernels[0].params["test_params"], True) - - def test_assemble_with_unequal_kernels(self): - """Test that assembly works with incorrect number of discriminators for - number of qubits.""" - disc_one = Kernel("disc_one", test_params=True) - disc_two = Kernel("disc_two", test_params=False) - - with self.assertWarns(DeprecationWarning): - schedule = Schedule() - schedule += Acquire(5, AcquireChannel(0), MemorySlot(0), kernel=disc_one) - schedule += Acquire(5, AcquireChannel(1), MemorySlot(1), kernel=disc_two) - schedule += Acquire(5, AcquireChannel(2), MemorySlot(2)) - - with self.assertRaises(QiskitError), self.assertWarns(DeprecationWarning): - assemble( - schedule, - qubit_lo_freq=self.default_qubit_lo_freq, - meas_lo_freq=self.default_meas_lo_freq, - meas_map=[[0, 1, 2]], - ) - - def test_assemble_single_instruction(self): - """Test assembling schedules, no lo config.""" - with self.assertWarns(DeprecationWarning): - inst = pulse.Play(pulse.Constant(100, 1.0), pulse.DriveChannel(0)) - self.assertIsInstance(assemble(inst, self.backend), PulseQobj) - - def test_assemble_overlapping_time(self): - """Test that assembly errors when qubits are measured in overlapping time.""" - with self.assertWarns(DeprecationWarning): - schedule = Schedule() - schedule = schedule.append( - Acquire(5, AcquireChannel(0), MemorySlot(0)), - ) - schedule = schedule.append( - Acquire(5, AcquireChannel(1), MemorySlot(1)) << 1, - ) - with self.assertRaises(QiskitError): - with self.assertWarns(DeprecationWarning): - assemble( - schedule, - qubit_lo_freq=self.default_qubit_lo_freq, - meas_lo_freq=self.default_meas_lo_freq, - meas_map=[[0, 1]], - ) - - def test_assemble_meas_map_vs_insts(self): - """Test that assembly errors when the qubits are measured in overlapping time - and qubits are not in the first meas_map list.""" - with self.assertWarns(DeprecationWarning): - schedule = Schedule() - schedule += Acquire(5, AcquireChannel(0), MemorySlot(0)) - schedule += Acquire(5, AcquireChannel(1), MemorySlot(1)) - schedule += Acquire(5, AcquireChannel(2), MemorySlot(2)) << 2 - schedule += Acquire(5, AcquireChannel(3), MemorySlot(3)) << 2 - - with self.assertRaises(QiskitError): - with self.assertWarns(DeprecationWarning): - assemble( - schedule, - qubit_lo_freq=self.default_qubit_lo_freq, - meas_lo_freq=self.default_meas_lo_freq, - meas_map=[[0], [1, 2], [3]], - ) - - def test_assemble_non_overlapping_time_single_meas_map(self): - """Test that assembly works when qubits are measured in non-overlapping - time within the same measurement map list.""" - with self.assertWarns(DeprecationWarning): - schedule = Schedule() - schedule = schedule.append( - Acquire(5, AcquireChannel(0), MemorySlot(0)), - ) - schedule = schedule.append( - Acquire(5, AcquireChannel(1), MemorySlot(1)) << 5, - ) - with self.assertWarns(DeprecationWarning): - qobj = assemble( - schedule, - qubit_lo_freq=self.default_qubit_lo_freq, - meas_lo_freq=self.default_meas_lo_freq, - meas_map=[[0, 1]], - ) - self.assertIsInstance(qobj, PulseQobj) - - def test_assemble_disjoint_time(self): - """Test that assembly works when qubits are in disjoint meas map sets.""" - with self.assertWarns(DeprecationWarning): - schedule = Schedule() - schedule = schedule.append( - Acquire(5, AcquireChannel(0), MemorySlot(0)), - ) - schedule = schedule.append( - Acquire(5, AcquireChannel(1), MemorySlot(1)) << 1, - ) - with self.assertWarns(DeprecationWarning): - qobj = assemble( - schedule, - qubit_lo_freq=self.default_qubit_lo_freq, - meas_lo_freq=self.default_meas_lo_freq, - meas_map=[[0, 2], [1, 3]], - ) - self.assertIsInstance(qobj, PulseQobj) - - def test_assemble_valid_qubits(self): - """Test that assembly works when qubits that are in the measurement map - is measured.""" - with self.assertWarns(DeprecationWarning): - schedule = Schedule() - schedule = schedule.append( - Acquire(5, AcquireChannel(1), MemorySlot(1)), - ) - schedule = schedule.append( - Acquire(5, AcquireChannel(2), MemorySlot(2)), - ) - schedule = schedule.append( - Acquire(5, AcquireChannel(3), MemorySlot(3)), - ) - with self.assertWarns(DeprecationWarning): - qobj = assemble( - schedule, - qubit_lo_freq=self.default_qubit_lo_freq, - meas_lo_freq=self.default_meas_lo_freq, - meas_map=[[0, 1, 2], [3]], - ) - self.assertIsInstance(qobj, PulseQobj) - - -class TestPulseAssemblerMissingKwargs(QiskitTestCase): - """Verify that errors are raised in case backend is not provided and kwargs are missing.""" - - def setUp(self): - super().setUp() - with self.assertWarns(DeprecationWarning): - self.schedule = pulse.Schedule(name="fake_experiment") - self.backend = FakeOpenPulse2Q() - - self.config = self.backend.configuration() - self.defaults = self.backend.defaults() - self.qubit_lo_freq = list(self.defaults.qubit_freq_est) - self.meas_lo_freq = list(self.defaults.meas_freq_est) - self.qubit_lo_range = self.config.qubit_lo_range - self.meas_lo_range = self.config.meas_lo_range - with self.assertWarns(DeprecationWarning): - self.schedule_los = { - pulse.DriveChannel(0): self.qubit_lo_freq[0], - pulse.DriveChannel(1): self.qubit_lo_freq[1], - pulse.MeasureChannel(0): self.meas_lo_freq[0], - pulse.MeasureChannel(1): self.meas_lo_freq[1], - } - self.meas_map = self.config.meas_map - self.memory_slots = self.config.n_qubits - - # default rep_time and rep_delay - self.rep_time = self.config.rep_times[0] - self.rep_delay = None - - def test_defaults(self): - """Test defaults work.""" - with self.assertWarns(DeprecationWarning): - qobj = assemble( - self.schedule, - qubit_lo_freq=self.qubit_lo_freq, - meas_lo_freq=self.meas_lo_freq, - qubit_lo_range=self.qubit_lo_range, - meas_lo_range=self.meas_lo_range, - schedule_los=self.schedule_los, - meas_map=self.meas_map, - memory_slots=self.memory_slots, - rep_time=self.rep_time, - rep_delay=self.rep_delay, - ) - self.assertIsInstance(qobj, PulseQobj) - - def test_missing_qubit_lo_freq(self): - """Test error raised if qubit_lo_freq missing.""" - - with self.assertRaises(QiskitError), self.assertWarns(DeprecationWarning): - - assemble( - self.schedule, - qubit_lo_freq=None, - meas_lo_freq=self.meas_lo_freq, - qubit_lo_range=self.qubit_lo_range, - meas_lo_range=self.meas_lo_range, - meas_map=self.meas_map, - memory_slots=self.memory_slots, - rep_time=self.rep_time, - rep_delay=self.rep_delay, - ) - - def test_missing_meas_lo_freq(self): - """Test error raised if meas_lo_freq missing.""" - - with self.assertRaises(QiskitError), self.assertWarns(DeprecationWarning): - assemble( - self.schedule, - qubit_lo_freq=self.qubit_lo_freq, - meas_lo_freq=None, - qubit_lo_range=self.qubit_lo_range, - meas_lo_range=self.meas_lo_range, - meas_map=self.meas_map, - memory_slots=self.memory_slots, - rep_time=self.rep_time, - rep_delay=self.rep_delay, - ) - - def test_missing_memory_slots(self): - """Test error is not raised if memory_slots are missing.""" - with self.assertWarns(DeprecationWarning): - qobj = assemble( - self.schedule, - qubit_lo_freq=self.qubit_lo_freq, - meas_lo_freq=self.meas_lo_freq, - qubit_lo_range=self.qubit_lo_range, - meas_lo_range=self.meas_lo_range, - schedule_los=self.schedule_los, - meas_map=self.meas_map, - memory_slots=None, - rep_time=self.rep_time, - rep_delay=self.rep_delay, - ) - self.assertIsInstance(qobj, PulseQobj) - - def test_missing_rep_time_and_delay(self): - """Test qobj is valid if rep_time and rep_delay are missing.""" - with self.assertWarns(DeprecationWarning): - qobj = assemble( - self.schedule, - qubit_lo_freq=self.qubit_lo_freq, - meas_lo_freq=self.meas_lo_freq, - qubit_lo_range=self.qubit_lo_range, - meas_lo_range=self.meas_lo_range, - schedule_los=self.schedule_los, - meas_map=self.meas_map, - memory_slots=None, - rep_time=None, - rep_delay=None, - ) - self.assertEqual(hasattr(qobj, "rep_time"), False) - self.assertEqual(hasattr(qobj, "rep_delay"), False) - - def test_missing_meas_map(self): - """Test that assembly still works if meas_map is missing.""" - with self.assertWarns(DeprecationWarning): - qobj = assemble( - self.schedule, - qubit_lo_freq=self.qubit_lo_freq, - meas_lo_freq=self.meas_lo_freq, - qubit_lo_range=self.qubit_lo_range, - meas_lo_range=self.meas_lo_range, - schedule_los=self.schedule_los, - meas_map=None, - memory_slots=self.memory_slots, - rep_time=self.rep_time, - rep_delay=self.rep_delay, - ) - self.assertIsInstance(qobj, PulseQobj) - - def test_missing_lo_ranges(self): - """Test that assembly still works if lo_ranges are missing.""" - with self.assertWarns(DeprecationWarning): - qobj = assemble( - self.schedule, - qubit_lo_freq=self.qubit_lo_freq, - meas_lo_freq=self.meas_lo_freq, - qubit_lo_range=None, - meas_lo_range=None, - schedule_los=self.schedule_los, - meas_map=self.meas_map, - memory_slots=self.memory_slots, - rep_time=self.rep_time, - rep_delay=self.rep_delay, - ) - self.assertIsInstance(qobj, PulseQobj) - - def test_unsupported_meas_level(self): - """Test that assembly raises an error if meas_level is not supported""" - with self.assertWarns(DeprecationWarning): - backend = FakeOpenPulse2Q() - backend.configuration().meas_levels = [1, 2] - with self.assertRaises(QiskitError), self.assertWarns(DeprecationWarning): - assemble( - self.schedule, - backend, - qubit_lo_freq=self.qubit_lo_freq, - meas_lo_freq=self.meas_lo_freq, - qubit_lo_range=self.qubit_lo_range, - meas_lo_range=self.meas_lo_range, - schedule_los=self.schedule_los, - meas_level=0, - meas_map=self.meas_map, - memory_slots=self.memory_slots, - rep_time=self.rep_time, - rep_delay=self.rep_delay, - ) - - def test_single_and_deprecated_acquire_styles(self): - """Test that acquires are identically combined with Acquires that take a single channel.""" - with self.assertWarns(DeprecationWarning): - backend = FakeOpenPulse2Q() - new_style_schedule = Schedule() - acq_dur = 1200 - with self.assertWarns(DeprecationWarning): - for i in range(2): - new_style_schedule += Acquire(acq_dur, AcquireChannel(i), MemorySlot(i)) - - deprecated_style_schedule = Schedule() - for i in range(2): - deprecated_style_schedule += Acquire(1200, AcquireChannel(i), MemorySlot(i)) - - # The Qobj IDs will be different - with self.assertWarns(DeprecationWarning): - n_qobj = assemble(new_style_schedule, backend) - n_qobj.qobj_id = None - n_qobj.experiments[0].header.name = None - with self.assertWarns(DeprecationWarning): - d_qobj = assemble(deprecated_style_schedule, backend) - d_qobj.qobj_id = None - d_qobj.experiments[0].header.name = None - self.assertEqual(n_qobj, d_qobj) - - assembled_acquire = n_qobj.experiments[0].instructions[0] - self.assertEqual(assembled_acquire.qubits, [0, 1]) - self.assertEqual(assembled_acquire.memory_slot, [0, 1]) - - -class StreamHandlerRaiseException(StreamHandler): - """Handler class that will raise an exception on formatting errors.""" - - def handleError(self, record): - raise sys.exc_info() - - -class TestLogAssembler(QiskitTestCase): - """Testing the log_assembly option.""" - - def setUp(self): - super().setUp() - logger = getLogger() - self.addCleanup(logger.setLevel, logger.level) - logger.setLevel("DEBUG") - self.output = io.StringIO() - logger.addHandler(StreamHandlerRaiseException(self.output)) - self.circuit = QuantumCircuit(QuantumRegister(1)) - - def assertAssembleLog(self, log_msg): - """Runs assemble and checks for logs containing specified message""" - with self.assertWarns(DeprecationWarning): - assemble(self.circuit, shots=2000, memory=True) - self.output.seek(0) - # Filter unrelated log lines - output_lines = self.output.readlines() - assembly_log_lines = [x for x in output_lines if log_msg in x] - self.assertTrue(len(assembly_log_lines) == 1) - - def test_assembly_log_time(self): - """Check Total Assembly Time is logged""" - self.assertAssembleLog("Total Assembly Time") - - -if __name__ == "__main__": - unittest.main(verbosity=2) diff --git a/test/python/compiler/test_compiler.py b/test/python/compiler/test_compiler.py index 32d33ba3e56c..4b16e2ee45ab 100644 --- a/test/python/compiler/test_compiler.py +++ b/test/python/compiler/test_compiler.py @@ -18,10 +18,8 @@ from qiskit import QuantumRegister, ClassicalRegister, QuantumCircuit from qiskit.transpiler import PassManager from qiskit.circuit.library import U1Gate, U2Gate -from qiskit.compiler import transpile, assemble -from qiskit.providers.fake_provider import Fake20QV1, Fake5QV1 +from qiskit.compiler import transpile from qiskit.providers.basic_provider import BasicSimulator -from qiskit.qobj import QasmQobj from qiskit.qasm2 import dumps from test import QiskitTestCase # pylint: disable=wrong-import-order @@ -186,21 +184,6 @@ def test_example_swap_bits(self): ).result() self.assertEqual(result.get_counts(qc), {"010000": 1024}) - def test_parallel_compile(self): - """Trigger parallel routines in compile.""" - qr = QuantumRegister(16) - cr = ClassicalRegister(2) - qc = QuantumCircuit(qr, cr) - qc.h(qr[0]) - for k in range(1, 15): - qc.cx(qr[0], qr[k]) - qc.measure(qr[5], cr[0]) - qlist = [qc for k in range(10)] - with self.assertWarns(DeprecationWarning): - backend = Fake20QV1() - qobj = assemble(transpile(qlist, backend=backend)) - self.assertEqual(len(qobj.experiments), 10) - def test_no_conflict_backend_passmanager(self): """See: https://github.com/Qiskit/qiskit-terra/issues/5037""" backend = BasicSimulator() @@ -241,13 +224,11 @@ def test_compile_single_qubit(self): circuit2 = transpile( circuit, backend=None, coupling_map=cmap, basis_gates=["u2"], initial_layout=layout ) - with self.assertWarns(DeprecationWarning): - qobj = assemble(circuit2) - compiled_instruction = qobj.experiments[0].instructions[0] + compiled_instruction = circuit2.data[0] self.assertEqual(compiled_instruction.name, "u2") - self.assertEqual(compiled_instruction.qubits, [12]) + self.assertEqual(circuit2.find_bit(compiled_instruction.qubits[0]).index, 12) self.assertEqual(compiled_instruction.params, [0, 3.141592653589793]) def test_compile_pass_manager(self): @@ -495,31 +476,6 @@ def test_random_parameter_circuit(self): threshold = 0.04 * shots self.assertDictAlmostEqual(counts, target, threshold) - def test_yzy_zyz_cases(self): - """yzy_to_zyz works in previously failed cases. - - See: https://github.com/Qiskit/qiskit-terra/issues/607 - """ - with self.assertWarns(DeprecationWarning): - backend = Fake5QV1() - qr = QuantumRegister(2) - circ1 = QuantumCircuit(qr) - circ1.cx(qr[0], qr[1]) - circ1.rz(0.7, qr[1]) - circ1.rx(1.570796, qr[1]) - with self.assertWarns(DeprecationWarning): - qobj1 = assemble(transpile(circ1, backend)) - self.assertIsInstance(qobj1, QasmQobj) - - circ2 = QuantumCircuit(qr) - circ2.y(qr[0]) - circ2.h(qr[0]) - circ2.s(qr[0]) - circ2.h(qr[0]) - with self.assertWarns(DeprecationWarning): - qobj2 = assemble(transpile(circ2, backend)) - self.assertIsInstance(qobj2, QasmQobj) - if __name__ == "__main__": unittest.main(verbosity=2) diff --git a/test/python/compiler/test_disassembler.py b/test/python/compiler/test_disassembler.py deleted file mode 100644 index 4556550422b2..000000000000 --- a/test/python/compiler/test_disassembler.py +++ /dev/null @@ -1,599 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2017, 2019. -# -# 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. - -"""Assembler Test. -FULLY REMOVE ONCE Qobj, assemble AND disassemble ARE REMOVED. -""" - -import unittest - -import numpy as np -from numpy.testing import assert_allclose - -from qiskit import pulse -from qiskit.compiler.assembler import assemble -from qiskit.assembler.disassemble import disassemble -from qiskit.assembler.run_config import RunConfig -from qiskit.circuit import QuantumRegister, ClassicalRegister, QuantumCircuit -from qiskit.circuit import Gate, Instruction, Parameter - -from qiskit.circuit.library import RXGate, Isometry -from qiskit.pulse.transforms import target_qobj_transform -from qiskit.providers.fake_provider import FakeOpenPulse2Q -import qiskit.quantum_info as qi -from test import QiskitTestCase # pylint: disable=wrong-import-order - - -def _parametric_to_waveforms(schedule): - instructions = list(schedule.instructions) - for i, time_instruction_tuple in enumerate(schedule.instructions): - time, instruction = time_instruction_tuple - if not isinstance(instruction.pulse, pulse.library.Waveform): - new_inst = pulse.Play(instruction.pulse.get_waveform(), instruction.channel) - instructions[i] = (time, new_inst) - return tuple(instructions) - - -class TestQuantumCircuitDisassembler(QiskitTestCase): - """Tests for disassembling circuits to qobj.""" - - def test_disassemble_single_circuit(self): - """Test disassembling a single circuit.""" - qr = QuantumRegister(2, name="q") - cr = ClassicalRegister(2, name="c") - circ = QuantumCircuit(qr, cr, name="circ") - circ.h(qr[0]) - circ.cx(qr[0], qr[1]) - circ.measure(qr, cr) - - qubit_lo_freq = [5e9, 5e9] - meas_lo_freq = [6.7e9, 6.7e9] - with self.assertWarns(DeprecationWarning): - qobj = assemble( - circ, - shots=2000, - memory=True, - qubit_lo_freq=qubit_lo_freq, - meas_lo_freq=meas_lo_freq, - ) - circuits, run_config_out, headers = disassemble(qobj) - run_config_out = RunConfig(**run_config_out) - self.assertEqual(run_config_out.n_qubits, 2) - self.assertEqual(run_config_out.memory_slots, 2) - self.assertEqual(run_config_out.shots, 2000) - self.assertEqual(run_config_out.memory, True) - self.assertEqual(run_config_out.qubit_lo_freq, qubit_lo_freq) - self.assertEqual(run_config_out.meas_lo_freq, meas_lo_freq) - self.assertEqual(len(circuits), 1) - self.assertEqual(circuits[0], circ) - self.assertEqual({}, headers) - - def test_disassemble_multiple_circuits(self): - """Test disassembling multiple circuits, all should have the same config.""" - qr0 = QuantumRegister(2, name="q0") - qc0 = ClassicalRegister(2, name="c0") - circ0 = QuantumCircuit(qr0, qc0, name="circ0") - circ0.h(qr0[0]) - circ0.cx(qr0[0], qr0[1]) - circ0.measure(qr0, qc0) - - qr1 = QuantumRegister(3, name="q1") - qc1 = ClassicalRegister(3, name="c1") - circ1 = QuantumCircuit(qr1, qc1, name="circ0") - circ1.h(qr1[0]) - circ1.cx(qr1[0], qr1[1]) - circ1.cx(qr1[0], qr1[2]) - circ1.measure(qr1, qc1) - - with self.assertWarns(DeprecationWarning): - qobj = assemble([circ0, circ1], shots=100, memory=False, seed=6) - circuits, run_config_out, headers = disassemble(qobj) - run_config_out = RunConfig(**run_config_out) - self.assertEqual(run_config_out.n_qubits, 3) - self.assertEqual(run_config_out.memory_slots, 3) - self.assertEqual(run_config_out.shots, 100) - self.assertEqual(run_config_out.memory, False) - self.assertEqual(run_config_out.seed, 6) - self.assertEqual(len(circuits), 2) - for circuit in circuits: - self.assertIn(circuit, [circ0, circ1]) - self.assertEqual({}, headers) - - def test_disassemble_no_run_config(self): - """Test disassembling with no run_config, relying on default.""" - qr = QuantumRegister(2, name="q") - qc = ClassicalRegister(2, name="c") - circ = QuantumCircuit(qr, qc, name="circ") - circ.h(qr[0]) - circ.cx(qr[0], qr[1]) - circ.measure(qr, qc) - - with self.assertWarns(DeprecationWarning): - qobj = assemble(circ) - circuits, run_config_out, headers = disassemble(qobj) - run_config_out = RunConfig(**run_config_out) - self.assertEqual(run_config_out.n_qubits, 2) - self.assertEqual(run_config_out.memory_slots, 2) - self.assertEqual(len(circuits), 1) - self.assertEqual(circuits[0], circ) - self.assertEqual({}, headers) - - def test_disassemble_initialize(self): - """Test disassembling a circuit with an initialize.""" - q = QuantumRegister(2, name="q") - circ = QuantumCircuit(q, name="circ") - circ.initialize([1 / np.sqrt(2), 0, 0, 1 / np.sqrt(2)], q[:]) - - with self.assertWarns(DeprecationWarning): - qobj = assemble(circ) - circuits, run_config_out, header = disassemble(qobj) - run_config_out = RunConfig(**run_config_out) - self.assertEqual(run_config_out.n_qubits, 2) - self.assertEqual(run_config_out.memory_slots, 0) - self.assertEqual(len(circuits), 1) - self.assertEqual(circuits[0], circ) - self.assertEqual({}, header) - - def test_disassemble_isometry(self): - """Test disassembling a circuit with an isometry.""" - q = QuantumRegister(2, name="q") - circ = QuantumCircuit(q, name="circ") - circ.append(Isometry(qi.random_unitary(4).data, 0, 0), circ.qubits) - with self.assertWarns(DeprecationWarning): - qobj = assemble(circ) - circuits, run_config_out, header = disassemble(qobj) - run_config_out = RunConfig(**run_config_out) - self.assertEqual(run_config_out.n_qubits, 2) - self.assertEqual(run_config_out.memory_slots, 0) - self.assertEqual(len(circuits), 1) - # params array - assert_allclose(circuits[0]._data[0].operation.params[0], circ._data[0].operation.params[0]) - # all other data - self.assertEqual( - circuits[0]._data[0].operation.params[1:], circ._data[0].operation.params[1:] - ) - self.assertEqual(circuits[0]._data[0].qubits, circ._data[0].qubits) - self.assertEqual(circuits[0]._data[0].clbits, circ._data[0].clbits) - self.assertEqual(circuits[0]._data[1:], circ._data[1:]) - self.assertEqual({}, header) - - def test_opaque_instruction(self): - """Test the disassembler handles opaque instructions correctly.""" - opaque_inst = Instruction(name="my_inst", num_qubits=4, num_clbits=2, params=[0.5, 0.4]) - q = QuantumRegister(6, name="q") - c = ClassicalRegister(4, name="c") - circ = QuantumCircuit(q, c, name="circ") - circ.append(opaque_inst, [q[0], q[2], q[5], q[3]], [c[3], c[0]]) - with self.assertWarns(DeprecationWarning): - qobj = assemble(circ) - circuits, run_config_out, header = disassemble(qobj) - run_config_out = RunConfig(**run_config_out) - self.assertEqual(run_config_out.n_qubits, 6) - self.assertEqual(run_config_out.memory_slots, 4) - self.assertEqual(len(circuits), 1) - self.assertEqual(circuits[0], circ) - self.assertEqual({}, header) - - def test_circuit_with_conditionals(self): - """Verify disassemble sets conditionals correctly.""" - qr = QuantumRegister(2) - cr1 = ClassicalRegister(1) - cr2 = ClassicalRegister(2) - qc = QuantumCircuit(qr, cr1, cr2) - qc.measure(qr[0], cr1) # Measure not required for a later conditional - qc.measure(qr[1], cr2[1]) # Measure required for a later conditional - with self.assertWarns(DeprecationWarning): - qc.h(qr[1]).c_if(cr2, 3) - with self.assertWarns(DeprecationWarning): - qobj = assemble(qc) - circuits, run_config_out, header = disassemble(qobj) - run_config_out = RunConfig(**run_config_out) - self.assertEqual(run_config_out.n_qubits, 2) - self.assertEqual(run_config_out.memory_slots, 3) - self.assertEqual(len(circuits), 1) - self.assertEqual(circuits[0], qc) - self.assertEqual({}, header) - - def test_circuit_with_simple_conditional(self): - """Verify disassemble handles a simple conditional on the only bits.""" - qr = QuantumRegister(1) - cr = ClassicalRegister(1) - qc = QuantumCircuit(qr, cr) - with self.assertWarns(DeprecationWarning): - qc.h(qr[0]).c_if(cr, 1) - with self.assertWarns(DeprecationWarning): - qobj = assemble(qc) - circuits, run_config_out, header = disassemble(qobj) - run_config_out = RunConfig(**run_config_out) - self.assertEqual(run_config_out.n_qubits, 1) - self.assertEqual(run_config_out.memory_slots, 1) - self.assertEqual(len(circuits), 1) - self.assertEqual(circuits[0], qc) - self.assertEqual({}, header) - - def test_circuit_with_single_bit_conditions(self): - """Verify disassemble handles a simple conditional on a single bit of a register.""" - # This circuit would fail to perfectly round-trip if 'cr' below had only one bit in it. - # This is because the format of QasmQobj is insufficient to disambiguate single-bit - # conditions from conditions on registers with only one bit. Since single-bit conditions are - # mostly a hack for the QasmQobj format at all, `disassemble` always prefers to return the - # register if it can. It would also fail if registers overlap. - qr = QuantumRegister(1) - cr = ClassicalRegister(2) - qc = QuantumCircuit(qr, cr) - with self.assertWarns(DeprecationWarning): - qc.h(qr[0]).c_if(cr[0], 1) - - with self.assertWarns(DeprecationWarning): - qobj = assemble(qc) - circuits, run_config_out, header = disassemble(qobj) - run_config_out = RunConfig(**run_config_out) - self.assertEqual(run_config_out.n_qubits, len(qr)) - self.assertEqual(run_config_out.memory_slots, len(cr)) - self.assertEqual(len(circuits), 1) - self.assertEqual(circuits[0], qc) - self.assertEqual({}, header) - - def test_circuit_with_mcx(self): - """Verify disassemble handles mcx gate - #6271.""" - qr = QuantumRegister(5) - cr = ClassicalRegister(5) - qc = QuantumCircuit(qr, cr) - qc.mcx([0, 1, 2], 4) - with self.assertWarns(DeprecationWarning): - qobj = assemble(qc) - circuits, run_config_out, header = disassemble(qobj) - run_config_out = RunConfig(**run_config_out) - self.assertEqual(run_config_out.n_qubits, 5) - self.assertEqual(run_config_out.memory_slots, 5) - self.assertEqual(len(circuits), 1) - self.assertEqual(circuits[0], qc) - self.assertEqual({}, header) - - def test_multiple_conditionals_multiple_registers(self): - """Verify disassemble handles multiple conditionals and registers.""" - qr = QuantumRegister(3) - cr1 = ClassicalRegister(3) - cr2 = ClassicalRegister(5) - cr3 = ClassicalRegister(6) - cr4 = ClassicalRegister(1) - - qc = QuantumCircuit(qr, cr1, cr2, cr3, cr4) - qc.x(qr[1]) - qc.h(qr) - with self.assertWarns(DeprecationWarning): - qc.cx(qr[1], qr[0]).c_if(cr3, 14) - with self.assertWarns(DeprecationWarning): - qc.ccx(qr[0], qr[2], qr[1]).c_if(cr4, 1) - with self.assertWarns(DeprecationWarning): - qc.h(qr).c_if(cr1, 3) - with self.assertWarns(DeprecationWarning): - qobj = assemble(qc) - circuits, run_config_out, header = disassemble(qobj) - run_config_out = RunConfig(**run_config_out) - self.assertEqual(run_config_out.n_qubits, 3) - self.assertEqual(run_config_out.memory_slots, 15) - self.assertEqual(len(circuits), 1) - self.assertEqual(circuits[0], qc) - self.assertEqual({}, header) - - def test_circuit_with_bit_conditional_1(self): - """Verify disassemble handles conditional on a single bit.""" - qr = QuantumRegister(2) - cr = ClassicalRegister(2) - qc = QuantumCircuit(qr, cr) - with self.assertWarns(DeprecationWarning): - qc.h(qr[0]).c_if(cr[1], True) - with self.assertWarns(DeprecationWarning): - qobj = assemble(qc) - circuits, run_config_out, header = disassemble(qobj) - run_config_out = RunConfig(**run_config_out) - self.assertEqual(run_config_out.n_qubits, 2) - self.assertEqual(run_config_out.memory_slots, 2) - self.assertEqual(len(circuits), 1) - self.assertEqual(circuits[0], qc) - self.assertEqual({}, header) - - def test_circuit_with_bit_conditional_2(self): - """Verify disassemble handles multiple single bit conditionals.""" - qr = QuantumRegister(2) - cr = ClassicalRegister(2) - cr1 = ClassicalRegister(2) - qc = QuantumCircuit(qr, cr, cr1) - with self.assertWarns(DeprecationWarning): - qc.h(qr[0]).c_if(cr1[1], False) - with self.assertWarns(DeprecationWarning): - qc.h(qr[1]).c_if(cr[0], True) - with self.assertWarns(DeprecationWarning): - qc.cx(qr[0], qr[1]).c_if(cr1[0], False) - with self.assertWarns(DeprecationWarning): - qobj = assemble(qc) - circuits, run_config_out, header = disassemble(qobj) - run_config_out = RunConfig(**run_config_out) - self.assertEqual(run_config_out.n_qubits, 2) - self.assertEqual(run_config_out.memory_slots, 4) - self.assertEqual(len(circuits), 1) - self.assertEqual(circuits[0], qc) - self.assertEqual({}, header) - - def assertCircuitCalibrationsEqual(self, in_circuits, out_circuits): - """Verify circuit calibrations are equivalent pre-assembly and post-disassembly""" - self.assertEqual(len(in_circuits), len(out_circuits)) - for in_qc, out_qc in zip(in_circuits, out_circuits): - with self.assertWarns(DeprecationWarning): - in_cals = in_qc.calibrations - out_cals = out_qc.calibrations - self.assertEqual(in_cals.keys(), out_cals.keys()) - for gate_name in in_cals: - self.assertEqual(in_cals[gate_name].keys(), out_cals[gate_name].keys()) - for gate_params, in_sched in in_cals[gate_name].items(): - out_sched = out_cals[gate_name][gate_params] - self.assertEqual(*map(_parametric_to_waveforms, (in_sched, out_sched))) - - def test_single_circuit_calibrations(self): - """Test that disassembler parses single circuit QOBJ calibrations (from QOBJ-level).""" - theta = Parameter("theta") - qc = QuantumCircuit(2) - qc.h(0) - qc.rx(np.pi, 0) - qc.rx(theta, 1) - qc = qc.assign_parameters({theta: np.pi}) - - with self.assertWarns(DeprecationWarning): - with pulse.build() as h_sched: - pulse.play(pulse.library.Drag(1, 0.15, 4, 2), pulse.DriveChannel(0)) - - with pulse.build() as x180: - pulse.play(pulse.library.Gaussian(1, 0.2, 5), pulse.DriveChannel(0)) - - qc.add_calibration("h", [0], h_sched) - qc.add_calibration(RXGate(np.pi), [0], x180) - - with self.assertWarns(DeprecationWarning): - qobj = assemble(qc, FakeOpenPulse2Q()) - output_circuits, _, _ = disassemble(qobj) - - self.assertCircuitCalibrationsEqual([qc], output_circuits) - - def test_parametric_pulse_circuit_calibrations(self): - """Test that disassembler parses parametric pulses back to pulse gates.""" - with self.assertWarns(DeprecationWarning): - with pulse.build() as h_sched: - pulse.play(pulse.library.Drag(50, 0.15, 4, 2), pulse.DriveChannel(0)) - - qc = QuantumCircuit(2) - qc.h(0) - - with self.assertWarns(DeprecationWarning): - qc.add_calibration("h", [0], h_sched) - backend = FakeOpenPulse2Q() - backend.configuration().parametric_pulses = ["drag"] - - qobj = assemble(qc, backend) - output_circuits, _, _ = disassemble(qobj) - out_qc = output_circuits[0] - - with self.assertWarns(DeprecationWarning): - self.assertCircuitCalibrationsEqual([qc], output_circuits) - self.assertTrue( - all( - qc_sched.instructions == out_qc_sched.instructions - for (_, qc_gate), (_, out_qc_gate) in zip( - qc.calibrations.items(), out_qc.calibrations.items() - ) - for qc_sched, out_qc_sched in zip(qc_gate.values(), out_qc_gate.values()) - ), - ) - - def test_multi_circuit_uncommon_calibrations(self): - """Test that disassembler parses uncommon calibrations (stored at QOBJ experiment-level).""" - with self.assertWarns(DeprecationWarning): - with pulse.build() as sched: - pulse.play(pulse.library.Drag(50, 0.15, 4, 2), pulse.DriveChannel(0)) - - qc_0 = QuantumCircuit(2) - qc_0.h(0) - qc_0.append(RXGate(np.pi), [1]) - with self.assertWarns(DeprecationWarning): - qc_0.add_calibration("h", [0], sched) - qc_0.add_calibration(RXGate(np.pi), [1], sched) - - qc_1 = QuantumCircuit(2) - qc_1.h(0) - - circuits = [qc_0, qc_1] - with self.assertWarns(DeprecationWarning): - qobj = assemble(circuits, FakeOpenPulse2Q()) - output_circuits, _, _ = disassemble(qobj) - - self.assertCircuitCalibrationsEqual(circuits, output_circuits) - - def test_multi_circuit_common_calibrations(self): - """Test that disassembler parses common calibrations (stored at QOBJ-level).""" - with self.assertWarns(DeprecationWarning): - with pulse.build() as sched: - pulse.play(pulse.library.Drag(1, 0.15, 4, 2), pulse.DriveChannel(0)) - - qc_0 = QuantumCircuit(2) - qc_0.h(0) - qc_0.append(RXGate(np.pi), [1]) - with self.assertWarns(DeprecationWarning): - qc_0.add_calibration("h", [0], sched) - qc_0.add_calibration(RXGate(np.pi), [1], sched) - - qc_1 = QuantumCircuit(2) - qc_1.h(0) - with self.assertWarns(DeprecationWarning): - qc_1.add_calibration(RXGate(np.pi), [1], sched) - - circuits = [qc_0, qc_1] - with self.assertWarns(DeprecationWarning): - qobj = assemble(circuits, FakeOpenPulse2Q()) - output_circuits, _, _ = disassemble(qobj) - - self.assertCircuitCalibrationsEqual(circuits, output_circuits) - - def test_single_circuit_delay_calibrations(self): - """Test that disassembler parses delay instruction back to delay gate.""" - qc = QuantumCircuit(2) - qc.append(Gate("test", 1, []), [0]) - with self.assertWarns(DeprecationWarning): - test_sched = pulse.Delay(64, pulse.DriveChannel(0)) + pulse.Delay( - 160, pulse.DriveChannel(0) - ) - - qc.add_calibration("test", [0], test_sched) - - qobj = assemble(qc, FakeOpenPulse2Q()) - output_circuits, _, _ = disassemble(qobj) - - self.assertEqual(len(qc.calibrations), len(output_circuits[0].calibrations)) - self.assertEqual(qc.calibrations.keys(), output_circuits[0].calibrations.keys()) - self.assertTrue( - all( - qc_cal.keys() == out_qc_cal.keys() - for qc_cal, out_qc_cal in zip( - qc.calibrations.values(), output_circuits[0].calibrations.values() - ) - ) - ) - self.assertEqual( - qc.calibrations["test"][((0,), ())], - output_circuits[0].calibrations["test"][((0,), ())], - ) - - -class TestPulseScheduleDisassembler(QiskitTestCase): - """Tests for disassembling pulse schedules to qobj.""" - - def setUp(self): - super().setUp() - with self.assertWarns(DeprecationWarning): - self.backend = FakeOpenPulse2Q() - self.backend_config = self.backend.configuration() - self.backend_config.parametric_pulses = ["constant", "gaussian", "gaussian_square", "drag"] - - def test_disassemble_single_schedule(self): - """Test disassembling a single schedule.""" - with self.assertWarns(DeprecationWarning): - d0 = pulse.DriveChannel(0) - d1 = pulse.DriveChannel(1) - with pulse.build(self.backend) as sched: - with pulse.align_right(): - pulse.play(pulse.library.Constant(10, 1.0), d0) - pulse.set_phase(1.0, d0) - pulse.shift_phase(3.11, d0) - pulse.set_frequency(1e9, d0) - pulse.shift_frequency(1e7, d0) - pulse.delay(20, d0) - pulse.delay(10, d1) - pulse.play(pulse.library.Constant(8, 0.1), d1) - pulse.measure_all() - - with self.assertWarns(DeprecationWarning): - qobj = assemble(sched, backend=self.backend, shots=2000) - scheds, run_config_out, _ = disassemble(qobj) - run_config_out = RunConfig(**run_config_out) - self.assertEqual(run_config_out.memory_slots, 2) - self.assertEqual(run_config_out.shots, 2000) - self.assertEqual(run_config_out.memory, False) - self.assertEqual(run_config_out.meas_level, 2) - self.assertEqual(run_config_out.meas_lo_freq, self.backend.defaults().meas_freq_est) - self.assertEqual(run_config_out.qubit_lo_freq, self.backend.defaults().qubit_freq_est) - self.assertEqual(run_config_out.rep_time, 99) - self.assertEqual(len(scheds), 1) - with self.assertWarns(DeprecationWarning): - self.assertEqual(scheds[0], target_qobj_transform(sched)) - - def test_disassemble_multiple_schedules(self): - """Test disassembling multiple schedules, all should have the same config.""" - with self.assertWarns(DeprecationWarning): - d0 = pulse.DriveChannel(0) - d1 = pulse.DriveChannel(1) - with pulse.build(self.backend) as sched0: - with pulse.align_right(): - pulse.play(pulse.library.Constant(10, 1.0), d0) - pulse.set_phase(1.0, d0) - pulse.shift_phase(3.11, d0) - pulse.set_frequency(1e9, d0) - pulse.shift_frequency(1e7, d0) - pulse.delay(20, d0) - pulse.delay(10, d1) - pulse.play(pulse.library.Constant(8, 0.1), d1) - pulse.measure_all() - - with pulse.build(self.backend) as sched1: - with pulse.align_right(): - pulse.play(pulse.library.Constant(8, 0.1), d0) - pulse.play(pulse.library.Waveform([0.0, 1.0]), d1) - pulse.set_phase(1.1, d0) - pulse.shift_phase(3.5, d0) - pulse.set_frequency(2e9, d0) - pulse.shift_frequency(3e7, d1) - pulse.delay(20, d1) - pulse.delay(10, d0) - pulse.play(pulse.library.Constant(8, 0.4), d1) - pulse.measure_all() - - with self.assertWarns(DeprecationWarning): - qobj = assemble([sched0, sched1], backend=self.backend, shots=2000) - scheds, run_config_out, _ = disassemble(qobj) - run_config_out = RunConfig(**run_config_out) - self.assertEqual(run_config_out.memory_slots, 2) - self.assertEqual(run_config_out.shots, 2000) - self.assertEqual(run_config_out.memory, False) - self.assertEqual(len(scheds), 2) - with self.assertWarns(DeprecationWarning): - self.assertEqual(scheds[0], target_qobj_transform(sched0)) - self.assertEqual(scheds[1], target_qobj_transform(sched1)) - - def test_disassemble_parametric_pulses(self): - """Test disassembling multiple schedules all should have the same config.""" - with self.assertWarns(DeprecationWarning): - d0 = pulse.DriveChannel(0) - with pulse.build(self.backend) as sched: - with pulse.align_right(): - pulse.play(pulse.library.Constant(10, 1.0), d0) - pulse.play(pulse.library.Gaussian(10, 1.0, 2.0), d0) - pulse.play(pulse.library.GaussianSquare(10, 1.0, 2.0, 3), d0) - pulse.play(pulse.library.Drag(10, 1.0, 2.0, 0.1), d0) - - with self.assertWarns(DeprecationWarning): - qobj = assemble(sched, backend=self.backend, shots=2000) - scheds, _, _ = disassemble(qobj) - self.assertEqual(scheds[0], target_qobj_transform(sched)) - - def test_disassemble_schedule_los(self): - """Test disassembling schedule los.""" - with self.assertWarns(DeprecationWarning): - d0 = pulse.DriveChannel(0) - m0 = pulse.MeasureChannel(0) - d1 = pulse.DriveChannel(1) - m1 = pulse.MeasureChannel(1) - - sched0 = pulse.Schedule() - sched1 = pulse.Schedule() - - schedule_los = [ - {d0: 4.5e9, d1: 5e9, m0: 6e9, m1: 7e9}, - {d0: 5e9, d1: 4.5e9, m0: 7e9, m1: 6e9}, - ] - with self.assertWarns(DeprecationWarning): - qobj = assemble([sched0, sched1], backend=self.backend, schedule_los=schedule_los) - _, run_config_out, _ = disassemble(qobj) - run_config_out = RunConfig(**run_config_out) - - self.assertEqual(run_config_out.schedule_los, schedule_los) - - -if __name__ == "__main__": - unittest.main(verbosity=2) diff --git a/test/python/providers/test_fake_backends.py b/test/python/providers/test_fake_backends.py index c03c31e55c65..2a272b91daef 100644 --- a/test/python/providers/test_fake_backends.py +++ b/test/python/providers/test_fake_backends.py @@ -23,9 +23,7 @@ from ddt import ddt, data from qiskit.circuit import QuantumCircuit -from qiskit.compiler import assemble from qiskit.compiler import transpile -from qiskit.exceptions import QiskitError from qiskit.providers.fake_provider import ( Fake5QV1, Fake20QV1, @@ -144,14 +142,6 @@ def test_circuit_on_fake_backend(self, backend, optimization_level): max_count = max(counts.items(), key=operator.itemgetter(1))[0] self.assertEqual(max_count, "11") - def test_qobj_failure(self): - backend = BACKENDS[-1] - with self.assertWarns(DeprecationWarning): - tqc = transpile(self.circuit, backend) - qobj = assemble(tqc, backend) - with self.assertRaises(QiskitError): - backend.run(qobj) - @data(*BACKENDS) def test_to_dict_properties(self, backend): with warnings.catch_warnings(): diff --git a/test/python/qobj/test_qobj.py b/test/python/qobj/test_qobj.py index ae59d6de00a5..21950b0a1486 100644 --- a/test/python/qobj/test_qobj.py +++ b/test/python/qobj/test_qobj.py @@ -15,8 +15,6 @@ import copy -from qiskit import QuantumRegister, ClassicalRegister, QuantumCircuit -from qiskit.compiler import assemble from qiskit.qobj import ( QasmQobj, PulseQobj, @@ -197,27 +195,6 @@ def test_snapshot_instruction_from_dict(self): qobj = QasmQobj.from_dict(qobj_dict) self.assertEqual(expected_qobj, qobj) - def test_change_qobj_after_compile(self): - """Test modifying Qobj parameters after compile.""" - qr = QuantumRegister(3) - cr = ClassicalRegister(3) - qc1 = QuantumCircuit(qr, cr) - qc2 = QuantumCircuit(qr, cr) - qc1.h(qr[0]) - qc1.cx(qr[0], qr[1]) - qc1.cx(qr[0], qr[2]) - qc2.h(qr) - qc1.measure(qr, cr) - qc2.measure(qr, cr) - circuits = [qc1, qc2] - with self.assertWarns(DeprecationWarning): - qobj1 = assemble(circuits, shots=1024, seed=88) - qobj1.experiments[0].config.shots = 50 - qobj1.experiments[1].config.shots = 1 - self.assertTrue(qobj1.experiments[0].config.shots == 50) - self.assertTrue(qobj1.experiments[1].config.shots == 1) - self.assertTrue(qobj1.config.shots == 1024) - def test_gate_calibrations_to_dict(self): """Test gate calibrations to dict.""" with self.assertWarns(DeprecationWarning): diff --git a/test/python/qobj/test_qobj_identifiers.py b/test/python/qobj/test_qobj_identifiers.py deleted file mode 100644 index 84eb58a1d782..000000000000 --- a/test/python/qobj/test_qobj_identifiers.py +++ /dev/null @@ -1,47 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2017, 2023. -# -# 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. - -# pylint: disable=missing-function-docstring - -"""Non-string identifiers for circuit and record identifiers test""" - -import unittest - -from qiskit.circuit import ClassicalRegister, QuantumCircuit, QuantumRegister -from qiskit.compiler import assemble -from test import QiskitTestCase # pylint: disable=wrong-import-order - - -class TestQobjIdentifiers(QiskitTestCase): - """Check the Qobj create names properly""" - - def setUp(self): - super().setUp() - qr = QuantumRegister(2, name="qr2") - cr = ClassicalRegister(2, name=None) - qc = QuantumCircuit(qr, cr, name="qc10") - qc.h(qr[0]) - qc.measure(qr[0], cr[0]) - self.qr_name = qr.name - self.cr_name = cr.name - self.circuits = [qc] - - def test_qobj_identifiers(self): - with self.assertWarns(DeprecationWarning): - qobj = assemble(self.circuits) - exp = qobj.experiments[0] - self.assertIn(self.qr_name, (x[0] for x in exp.header.qubit_labels)) - self.assertIn(self.cr_name, (x[0] for x in exp.header.clbit_labels)) - - -if __name__ == "__main__": - unittest.main(verbosity=2) From 1ef0d7957cde5d6ad7df640632b73371c118b265 Mon Sep 17 00:00:00 2001 From: atharva-satpute <55058959+atharva-satpute@users.noreply.github.com> Date: Wed, 5 Feb 2025 19:20:51 +0530 Subject: [PATCH 03/48] Fix barrier label position when bits are reversed (#13780) * qa: Fix barrier label position when bits are reversed * Consider qubit with min index as top qubit * Fix lint issues * Fix typo --------- Co-authored-by: Jake Lishman --- qiskit/visualization/circuit/text.py | 5 ++-- ...osition-reverse-bits-41819043ebb3d701.yaml | 8 ++++++ .../visualization/test_circuit_text_drawer.py | 27 +++++++++++++++++++ 3 files changed, 38 insertions(+), 2 deletions(-) create mode 100644 releasenotes/notes/barrier-label-position-reverse-bits-41819043ebb3d701.yaml diff --git a/qiskit/visualization/circuit/text.py b/qiskit/visualization/circuit/text.py index e9b7aa819e9b..d51a3efa6828 100644 --- a/qiskit/visualization/circuit/text.py +++ b/qiskit/visualization/circuit/text.py @@ -1153,9 +1153,10 @@ def add_connected_gate(node, gates, layer, current_cons, gate_wire_map): if not self.plotbarriers: return layer, current_cons, current_cons_cond, connection_label - for i, qubit in enumerate(node.qargs): + top_qubit = min(node.qargs, key=lambda q: self._wire_map.get(q, float("inf"))) + for qubit in node.qargs: if qubit in self.qubits: - label = op.label if i == 0 else "" + label = op.label if qubit == top_qubit else "" layer.set_qubit(qubit, Barrier(label)) elif isinstance(op, SwapGate): diff --git a/releasenotes/notes/barrier-label-position-reverse-bits-41819043ebb3d701.yaml b/releasenotes/notes/barrier-label-position-reverse-bits-41819043ebb3d701.yaml new file mode 100644 index 000000000000..8837e0342892 --- /dev/null +++ b/releasenotes/notes/barrier-label-position-reverse-bits-41819043ebb3d701.yaml @@ -0,0 +1,8 @@ +--- +fixes: + - | + Fixed a bug where the barrier labels were incorrectly positioned when + using the ``reverse_bits = True`` parameter in the :meth:`.QuantumCircuit.draw` + method. The bug caused the labels on barrier operations to be misaligned, + leading to potential confusion in circuit visualizations. + Fixed `#13609 `__. diff --git a/test/python/visualization/test_circuit_text_drawer.py b/test/python/visualization/test_circuit_text_drawer.py index 666255c44f23..6ae4adb42b8b 100644 --- a/test/python/visualization/test_circuit_text_drawer.py +++ b/test/python/visualization/test_circuit_text_drawer.py @@ -1240,6 +1240,33 @@ def test_text_barrier_label(self): circuit.barrier(label="End Y/X") self.assertEqual(str(circuit_drawer(circuit, output="text", initial_state=True)), expected) + def test_text_barrier_label_reversed_bits(self): + """Show barrier label with reversed bits""" + expected = "\n".join( + [ + " ░ ┌───┐ End Y/X ", + "q_2: |0>──────░─┤ X ├────░────", + " ┌───┐ ░ ├───┤ ░ ", + "q_1: |0>┤ Y ├─░─┤ Y ├────░────", + " ├───┤ ░ └───┘ ░ ", + "q_0: |0>┤ X ├─░───────────────", + " └───┘ ░ ", + ] + ) + + qr = QuantumRegister(3, "q") + circuit = QuantumCircuit(qr) + circuit.x(0) + circuit.y(1) + circuit.barrier() + circuit.y(1) + circuit.x(2) + circuit.barrier([1, 2], label="End Y/X") + self.assertEqual( + str(circuit_drawer(circuit, output="text", initial_state=True, reverse_bits=True)), + expected, + ) + def test_text_overlap_cx(self): """Overlapping CX gates are drawn not overlapping""" expected = "\n".join( From 62748435b3815809b649b74bc1cfc38ae5363aa2 Mon Sep 17 00:00:00 2001 From: Julien Gacon Date: Wed, 5 Feb 2025 15:02:06 +0100 Subject: [PATCH 04/48] Support `SparseObservable` to `SparsePauliOp` conversions (#13758) * SparseObservable -> SparsePauliOp and SparseObservable.to_sparse_list, which can also be used for the PauliEvolutionGate compat * reno * allow observable alphabet as well * Jake's review comments - to_paulis -> SparseObservable - test more edge cases - don't guarantee any ordering * update reno * fix ruff ... which was somehow not captured by my pre commit hook * round 2 - use &'static over Vec - reverse bit/index order - rename as_paulis - direct construciton w/o CoherenceError * fix tests and improve docs * Update documentation --------- Co-authored-by: Jake Lishman --- crates/accelerate/src/sparse_observable.rs | 193 +++++++++++++++++- .../operators/symplectic/sparse_pauli_op.py | 22 ++ ...e-obs-to-sparse-list-5b7c17c5d7cffd72.yaml | 25 +++ .../symplectic/test_sparse_pauli_op.py | 68 +++++- .../quantum_info/test_sparse_observable.py | 107 ++++++++++ 5 files changed, 407 insertions(+), 8 deletions(-) create mode 100644 releasenotes/notes/sparse-obs-to-sparse-list-5b7c17c5d7cffd72.yaml diff --git a/crates/accelerate/src/sparse_observable.rs b/crates/accelerate/src/sparse_observable.rs index efe3996cf179..6f25b8a22cfa 100644 --- a/crates/accelerate/src/sparse_observable.rs +++ b/crates/accelerate/src/sparse_observable.rs @@ -11,6 +11,7 @@ // that they have been altered from the originals. use hashbrown::HashSet; +use itertools::Itertools; use ndarray::Array2; use num_complex::Complex64; use num_traits::Zero; @@ -23,7 +24,7 @@ use pyo3::{ intern, prelude::*, sync::GILOnceCell, - types::{IntoPyDict, PyList, PyTuple, PyType}, + types::{IntoPyDict, PyList, PyString, PyTuple, PyType}, IntoPyObjectExt, PyErr, }; use std::{ @@ -184,6 +185,24 @@ impl BitTerm { pub fn has_z_component(&self) -> bool { ((*self as u8) & (Self::Z as u8)) != 0 } + + pub fn is_projector(&self) -> bool { + !matches!(self, BitTerm::X | BitTerm::Y | BitTerm::Z) + } +} + +fn bit_term_as_pauli(bit: &BitTerm) -> &'static [(bool, Option)] { + match bit { + BitTerm::X => &[(true, Some(BitTerm::X))], + BitTerm::Y => &[(true, Some(BitTerm::Y))], + BitTerm::Z => &[(true, Some(BitTerm::Z))], + BitTerm::Plus => &[(true, None), (true, Some(BitTerm::X))], + BitTerm::Minus => &[(true, None), (false, Some(BitTerm::X))], + BitTerm::Left => &[(true, None), (true, Some(BitTerm::Y))], + BitTerm::Right => &[(true, None), (false, Some(BitTerm::Y))], + BitTerm::Zero => &[(true, None), (true, Some(BitTerm::Z))], + BitTerm::One => &[(true, None), (false, Some(BitTerm::Z))], + } } /// The error type for a failed conversion into `BitTerm`. @@ -641,6 +660,58 @@ impl SparseObservable { } } + /// Expand all projectors into Pauli representation. + /// + /// # Warning + /// + /// This representation is highly inefficient for projectors. For example, a term with + /// :math:`n` projectors :math:`|+\rangle\langle +|` will use :math:`2^n` Pauli terms. + pub fn as_paulis(&self) -> Self { + let mut paulis: Vec = Vec::new(); // maybe get capacity here + let mut indices: Vec = Vec::new(); + let mut coeffs: Vec = Vec::new(); + let mut boundaries: Vec = vec![0]; + + for view in self.iter() { + let num_projectors = view + .bit_terms + .iter() + .filter(|&bit| bit.is_projector()) + .count(); + let div = 2_f64.powi(num_projectors as i32); + + let combinations = view + .bit_terms + .iter() + .map(bit_term_as_pauli) + .multi_cartesian_product(); + + for combination in combinations { + let mut positive = true; + + for (index, (sign, bit)) in combination.iter().enumerate() { + positive &= sign; + if let Some(bit) = bit { + paulis.push(*bit); + indices.push(view.indices[index]); + } + } + boundaries.push(paulis.len()); + + let coeff = if positive { view.coeff } else { -view.coeff }; + coeffs.push(coeff / div) + } + } + + Self { + num_qubits: self.num_qubits, + coeffs, + bit_terms: paulis, + indices, + boundaries, + } + } + /// Add the term implied by a dense string label onto this observable. pub fn add_dense_label>( &mut self, @@ -1741,10 +1812,11 @@ impl PySparseTerm { /// /// .. note:: /// -/// The canonical form produced by :meth:`simplify` will still not universally detect all -/// observables that are equivalent due to the over-complete basis alphabet; it is not -/// computationally feasible to do this at scale. For example, on observable built from ``+`` -/// and ``-`` components will not canonicalize to a single ``X`` term. +/// The canonical form produced by :meth:`simplify` alone will not universally detect all +/// observables that are equivalent due to the over-complete basis alphabet. To obtain a +/// unique expression, you can first represent the observable using Pauli terms only by +/// calling :meth:`as_paulis`, followed by :meth:`simplify`. Note that the projector +/// expansion (e.g. ``+`` into ``I`` and ``X``) is not computationally feasible at scale. /// /// Indexing /// -------- @@ -1824,6 +1896,29 @@ impl PySparseTerm { /// :meth:`identity` The identity operator on a given number of qubits. /// ============================ ================================================================ /// +/// Conversions +/// =========== +/// +/// An existing :class:`SparseObservable` can be converted into other :mod:`~qiskit.quantum_info` +/// operators or generic formats. Beware that other objects may not be able to represent the same +/// observable as efficiently as :class:`SparseObservable`, including potentially needed +/// exponentially more memory. +/// +/// .. table:: Conversion methods to other observable forms. +/// +/// =========================== ================================================================= +/// Method Summary +/// =========================== ================================================================= +/// :meth:`as_paulis` Create a new :class:`SparseObservable`, expanding in terms +/// of Pauli operators only. +/// +/// :meth:`to_sparse_list` Express the observable in a sparse list format with elements +/// ``(bit_terms, indices, coeff)``. +/// =========================== ================================================================= +/// +/// In addition, :meth:`.SparsePauliOp.from_sparse_observable` is available for conversion from this +/// class to :class:`.SparsePauliOp`. Beware that this method suffers from the same +/// exponential-memory usage concerns as :meth:`as_paulis`. /// /// Mathematical manipulation /// ========================= @@ -1925,8 +2020,8 @@ impl PySparseObservable { let inner = borrowed.inner.read().map_err(|_| InnerReadError)?; return Ok(inner.clone().into()); } - // The type of `vec` is inferred from the subsequent calls to `Self::py_from_list` or - // `Self::py_from_sparse_list` to be either the two-tuple or the three-tuple form during the + // The type of `vec` is inferred from the subsequent calls to `Self::from_list` or + // `Self::from_sparse_list` to be either the two-tuple or the three-tuple form during the // `extract`. The empty list will pass either, but it means the same to both functions. if let Ok(vec) = data.extract() { return Self::from_list(vec, num_qubits); @@ -2289,6 +2384,10 @@ impl PySparseObservable { /// ... for label, coeff in zip(labels, coeffs) /// ... ]) /// >>> assert from_list == from_sparse_list + /// + /// See also: + /// :meth:`to_sparse_list` + /// The reverse of this method. #[staticmethod] #[pyo3(signature = (iter, /, num_qubits))] fn from_sparse_list( @@ -2337,6 +2436,86 @@ impl PySparseObservable { Ok(inner.into()) } + /// Express the observable in Pauli terms only, by writing each projector as sum of Pauli terms. + /// + /// Note that there is no guarantee of the order the resulting Pauli terms. Use + /// :meth:`SparseObservable.simplify` in addition to obtain a canonical representation. + /// + /// .. warning:: + /// + /// Beware that this will use at least :math:`2^n` terms if there are :math:`n` + /// single-qubit projectors present, which can lead to an exponential number of terms. + /// + /// Returns: + /// The same observable, but expressed in Pauli terms only. + /// + /// Examples: + /// + /// Rewrite an observable in terms of projectors into Pauli operators:: + /// + /// >>> obs = SparseObservable("+") + /// >>> obs.as_paulis() + /// + /// >>> direct = SparseObservable.from_list([("I", 0.5), ("Z", 0.5)]) + /// >>> assert direct.simplify() == obs.as_paulis().simplify() + /// + /// For small operators, this can be used with :meth:`simplify` as a unique canonical form:: + /// + /// >>> left = SparseObservable.from_list([("+", 0.5), ("-", 0.5)]) + /// >>> right = SparseObservable.from_list([("r", 0.5), ("l", 0.5)]) + /// >>> assert left.as_paulis().simplify() == right.as_paulis().simplify() + /// + /// See also: + /// :meth:`.SparsePauliOp.from_sparse_observable` + /// A constructor of :class:`.SparsePauliOp` that can convert a + /// :class:`SparseObservable` in the :class:`.SparsePauliOp` dense Pauli representation. + fn as_paulis(&self) -> PyResult { + let inner = self.inner.read().map_err(|_| InnerReadError)?; + Ok(inner.as_paulis().into()) + } + + /// Express the observable in terms of a sparse list format. + /// + /// This can be seen as counter-operation of :meth:`.SparseObservable.from_sparse_list`, however + /// the order of terms is not guaranteed to be the same at after a roundtrip to a sparse + /// list and back. + /// + /// Examples: + /// + /// >>> obs = SparseObservable.from_list([("IIXIZ", 2j), ("IIZIX", 2j)]) + /// >>> reconstructed = SparseObservable.from_sparse_list(obs.to_sparse_list(), obs.num_qubits) + /// + /// See also: + /// :meth:`from_sparse_list` + /// The constructor that can interpret these lists. + #[pyo3(signature = ())] + fn to_sparse_list(&self, py: Python) -> PyResult> { + let inner = self.inner.read().map_err(|_| InnerReadError)?; + + // turn a SparseView into a Python tuple of (bit terms, indices, coeff) + let to_py_tuple = |view: SparseTermView| { + let mut pauli_string = String::with_capacity(view.bit_terms.len()); + + // we reverse the order of bits and indices so the Pauli string comes out in + // "reading order", consistent with how one would write the label in + // SparseObservable.from_list or .from_label + for bit in view.bit_terms.iter().rev() { + pauli_string.push_str(bit.py_label()); + } + let py_string = PyString::new(py, &pauli_string).unbind(); + let py_indices = PyList::new(py, view.indices.iter().rev())?.unbind(); + let py_coeff = view.coeff.into_py_any(py)?; + + PyTuple::new(py, vec![py_string.as_any(), py_indices.as_any(), &py_coeff]) + }; + + let out = PyList::empty(py); + for view in inner.iter() { + out.append(to_py_tuple(view)?)?; + } + Ok(out.unbind()) + } + /// Construct a :class:`.SparseObservable` from a :class:`.SparsePauliOp` instance. /// /// This will be a largely direct translation of the :class:`.SparsePauliOp`; in particular, diff --git a/qiskit/quantum_info/operators/symplectic/sparse_pauli_op.py b/qiskit/quantum_info/operators/symplectic/sparse_pauli_op.py index 50b80ec89051..43420473232f 100644 --- a/qiskit/quantum_info/operators/symplectic/sparse_pauli_op.py +++ b/qiskit/quantum_info/operators/symplectic/sparse_pauli_op.py @@ -30,6 +30,7 @@ to_matrix_sparse, unordered_unique, ) +from qiskit._accelerate.sparse_observable import SparseObservable from qiskit.circuit.parameter import Parameter from qiskit.circuit.parameterexpression import ParameterExpression from qiskit.circuit.parametertable import ParameterView @@ -929,6 +930,27 @@ def from_sparse_list( paulis = PauliList(labels) return SparsePauliOp(paulis, coeffs, copy=False) + @staticmethod + def from_sparse_observable(obs: SparseObservable) -> SparsePauliOp: + r"""Initialize from a :class:`.SparseObservable`. + + .. warning:: + + A :class:`.SparseObservable` can efficiently represent eigenstate projectors + (such as :math:`|0\langle\rangle 0|`), but a :class:`.SparsePauliOp` **cannot**. + If the input ``obs`` has :math:`n` single-qubit projectors, the resulting + :class:`.SparsePauliOp` will use :math:`2^n` terms, which is an exponentially + expensive representation that can quickly run out of memory. + + Args: + obs: The :class:`.SparseObservable` to convert. + + Returns: + A :class:`.SparsePauliOp` version of the observable. + """ + as_sparse_list = obs.as_paulis().to_sparse_list() + return SparsePauliOp.from_sparse_list(as_sparse_list, obs.num_qubits) + def to_list(self, array: bool = False): """Convert to a list Pauli string labels and coefficients. diff --git a/releasenotes/notes/sparse-obs-to-sparse-list-5b7c17c5d7cffd72.yaml b/releasenotes/notes/sparse-obs-to-sparse-list-5b7c17c5d7cffd72.yaml new file mode 100644 index 000000000000..f781d9976721 --- /dev/null +++ b/releasenotes/notes/sparse-obs-to-sparse-list-5b7c17c5d7cffd72.yaml @@ -0,0 +1,25 @@ +--- +features_quantum_info: + - | + Added :meth:`.SparseObservable.to_sparse_list` to obtain a sparse list representation + of a :class:`.SparseObservable`. For example:: + + from qiskit.quantum_info import SparseObservable + + obs = SparseObservable.from_list([("+II", 1), ("-II", 1)]) + print(obs.to_sparse_list()) # [("+", [2], 1), ("-", [2], 1)] + + - | + Added :meth:`.SparseObservable.as_paulis` to express a sparse observable in terms of Paulis + only by expanding all projectors. For example:: + + from qiskit.quantum_info import SparseObservable + + obs = SparseObservable("+-") + obs_paulis = obs.as_paulis() # 1/4 ( II + XI - IX - XX ) + + - | + Support construction of a :class:`.SparsePauliOp` from a :class:`.SparseObservable` + via the new method :class:`.SparsePauliOp.from_sparse_observable`. It is important + to remember that :class:`.SparseObservable`\ s can efficiently represent projectors, + which require an exponential number of terms in the :class:`.SparsePauliOp`. diff --git a/test/python/quantum_info/operators/symplectic/test_sparse_pauli_op.py b/test/python/quantum_info/operators/symplectic/test_sparse_pauli_op.py index 3602492030c4..0704353ab333 100644 --- a/test/python/quantum_info/operators/symplectic/test_sparse_pauli_op.py +++ b/test/python/quantum_info/operators/symplectic/test_sparse_pauli_op.py @@ -28,7 +28,13 @@ from qiskit.compiler.transpiler import transpile from qiskit.primitives import BackendEstimator from qiskit.providers.fake_provider import GenericBackendV2 -from qiskit.quantum_info.operators import Operator, Pauli, PauliList, SparsePauliOp +from qiskit.quantum_info import SparseObservable +from qiskit.quantum_info.operators import ( + Operator, + Pauli, + PauliList, + SparsePauliOp, +) from qiskit.utils import optionals @@ -361,6 +367,66 @@ def test_to_list_parameters(self): target = list(zip(labels, coeffs)) self.assertEqual(op.to_list(), target) + def test_from_sparse_observable(self): + """Test from a SparseObservable.""" + with self.subTest("zero(0)"): + obs = SparseObservable.zero(0) + expected = SparsePauliOp([""], coeffs=[0]) + self.assertEqual(expected, SparsePauliOp.from_sparse_observable(obs)) + + with self.subTest("identity(0)"): + obs = SparseObservable.identity(0) + expected = SparsePauliOp([""], coeffs=[1]) + self.assertEqual(expected, SparsePauliOp.from_sparse_observable(obs)) + + with self.subTest("zero(10)"): + obs = SparseObservable.zero(10) + expected = SparsePauliOp(["I" * 10], coeffs=[0]) + self.assertEqual(expected, SparsePauliOp.from_sparse_observable(obs)) + + with self.subTest("identity(10)"): + obs = SparseObservable.identity(10) + expected = SparsePauliOp(["I" * 10], coeffs=[1]) + self.assertEqual(expected, SparsePauliOp.from_sparse_observable(obs)) + + with self.subTest("XrZ"): + obs = SparseObservable("XrZ") + spo = SparsePauliOp.from_sparse_observable(obs) + expected = SparsePauliOp(["XIZ", "XYZ"], coeffs=[0.5, -0.5]) + + # we don't guarantee the order of Paulis, so check equality by comparing + # the matrix representation and that all Pauli strings are present + self.assertEqual(Operator(expected), Operator(spo)) + self.assertTrue(set(spo.paulis.to_labels()) == set(expected.paulis.to_labels())) + + def test_sparse_observable_roundtrip(self): + """Test SPO -> OBS -> SPO.""" + with self.subTest(msg="empty"): + op = SparsePauliOp([""], coeffs=[1]) + obs = SparseObservable.from_sparse_pauli_op(op) + roundtrip = SparsePauliOp.from_sparse_observable(obs) + self.assertEqual(op, roundtrip) + + with self.subTest(msg="zero"): + op = SparsePauliOp(["I"], coeffs=[0]) + obs = SparseObservable.from_sparse_pauli_op(op) + roundtrip = SparsePauliOp.from_sparse_observable(obs) + self.assertEqual(op, roundtrip) + + with self.subTest(msg="identity"): + op = SparsePauliOp(["I" * 25]) + obs = SparseObservable.from_sparse_pauli_op(op) + roundtrip = SparsePauliOp.from_sparse_observable(obs) + self.assertEqual(op, roundtrip) + + with self.subTest(msg="ising like"): + op = SparsePauliOp(["ZZI", "IZZ", "IIX", "IXI", "YII"]) + obs = SparseObservable.from_sparse_pauli_op(op) + roundtrip = SparsePauliOp.from_sparse_observable(obs) + + self.assertEqual(Operator(op), Operator(roundtrip)) + self.assertTrue(set(op.paulis.to_labels()) == set(roundtrip.paulis.to_labels())) + class TestSparsePauliOpIteration(QiskitTestCase): """Tests for SparsePauliOp iterators class.""" diff --git a/test/python/quantum_info/test_sparse_observable.py b/test/python/quantum_info/test_sparse_observable.py index 38c7d859bd53..3c4d1c999360 100644 --- a/test/python/quantum_info/test_sparse_observable.py +++ b/test/python/quantum_info/test_sparse_observable.py @@ -2008,3 +2008,110 @@ def test_term_pauli_base(self): Pauli("YYYII"), ] self.assertEqual([term.pauli_base() for term in obs], expected) + + def test_to_sparse_list(self): + """Test converting to a sparse list.""" + with self.subTest(msg="zero"): + obs = SparseObservable.zero(100) + expected = [] + self.assertEqual(expected, obs.to_sparse_list()) + + with self.subTest(msg="identity"): + obs = SparseObservable.identity(100) + expected = [("", [], 1)] + self.assertEqual(expected, obs.to_sparse_list()) + + with self.subTest(msg="IXYZ"): + obs = SparseObservable("IXYZ") + expected = [("ZYX", [0, 1, 2], 1)] + self.assertEqual( + canonicalize_sparse_list(expected), canonicalize_sparse_list(obs.to_sparse_list()) + ) + + with self.subTest(msg="multiple"): + obs = SparseObservable.from_list([("lrI0", 0.5), ("YYIZ", -1j)]) + expected = [("lr0", [3, 2, 0], 0.5), ("ZYY", [0, 2, 3], -1j)] + self.assertEqual( + canonicalize_sparse_list(expected), canonicalize_sparse_list(obs.to_sparse_list()) + ) + + def test_as_paulis(self): + """Test converting to Paulis.""" + # test on zero operator + with self.subTest(msg="zero"): + obs = SparseObservable.zero(10) + obs_paulis = obs.as_paulis() + self.assertEqual(obs, obs_paulis) + + # test on identity operator + with self.subTest(msg="identity"): + obs = SparseObservable.identity(10) + obs_paulis = obs.as_paulis() + self.assertEqual(obs, obs_paulis) + + # test it does nothing on Paulis + with self.subTest(msg="paulis"): + obs = SparseObservable.from_list([("IIX", 1), ("ZZY", -1)]) + obs_paulis = obs.as_paulis() + self.assertEqual(obs, obs_paulis) + + # test explicitly on written-out projector + with self.subTest(msg="lrI0"): + obs = SparseObservable("lrI0") + obs_paulis = obs.as_paulis() + expected = SparseObservable.from_sparse_list( + [ + ("", [], 1 / 8), + ("Y", [2], -1 / 8), + ("YY", [3, 2], -1 / 8), + ("Z", [0], 1 / 8), + ("YZ", [2, 0], -1 / 8), + ("YYZ", [3, 2, 0], -1 / 8), + ("Y", [3], 1 / 8), + ("YZ", [3, 0], 1 / 8), + ], + 4, + ) + self.assertEqual(expected.simplify(), obs_paulis.simplify()) + + # test multiple terms + with self.subTest(msg="+X + lY - ZI"): + obs = SparseObservable.from_list([("+X", 1), ("rY", 1), ("ZI", -1)]) + obs_paulis = obs.as_paulis() + + expected = SparseObservable.from_list( + [("IX", 0.5), ("XX", 0.5), ("IY", 0.5), ("YY", -0.5), ("ZI", -1)] + ) + + self.assertEqual(expected.simplify(), obs_paulis.simplify()) + + def test_sparse_list_roundtrip(self): + """Test dumping into a sparse list and constructing from one.""" + obs = SparseObservable.from_list( + [ + ("IIXIZ", 2j), + ("IIZIX", 2j), + ("++III", -1.5), + ("--III", -1.5), + ("IrIlI", 0.5), + ("IIrIl", 0.5), + ] + ) + + reconstructed = SparseObservable.from_sparse_list(obs.to_sparse_list(), obs.num_qubits) + self.assertEqual(obs.simplify(), reconstructed.simplify()) + + +def canonicalize_term(pauli, indices, coeff): + # canonicalize a sparse list term by sorting by indices (which is unique as + # indices cannot be repeated) + idcs = np.argsort(indices) + sorted_paulis = "".join(pauli[i] for i in idcs) + return (sorted_paulis, np.asarray(indices)[idcs].tolist(), complex(coeff)) + + +def canonicalize_sparse_list(sparse_list): + # sort a sparse list representation by canonicalizing the terms and then applying + # Python's built-in sort + canonicalized_terms = [canonicalize_term(*term) for term in sparse_list] + return sorted(canonicalized_terms) From 73409fb4ac9d659aa514ed2e39cd4b591d88db05 Mon Sep 17 00:00:00 2001 From: Kevin Hartman Date: Wed, 5 Feb 2025 12:19:30 -0500 Subject: [PATCH 05/48] Port standard instructions to Rust. (#13486) * Extend PackedOperation for standard instructions. * WIP * Compiling. * Add missing module registration. * Implement Operation::directive * Add StandardInstructionType enum. * Expose only StandardInstructionType to Python. This way, we can also use this enum to tag the Python classes. * Fix unit access. * Remove no longer needed enum variant parens. * Remove unused stuff. * Fix up packed_instruction docs, add From. * Fix up imports. * Improve comments in circuit_instruction. * Fix typo. * More doc fixes. * Reorganize PackedInstruction mem layout to support 32 bit archs. * Update layout description, cleanup. * Revert "Update layout description, cleanup." This reverts commit b6d8f929accc6786a460124aee01ac82f5b268c8. * Revert "Reorganize PackedInstruction mem layout to support 32 bit archs." This reverts commit 6049498babe5cd7033c8f7fbc3c85361d15d4587. * Use bitfield-struct crate for PackedOperation. Trying out a neat crate for Rust bitfields. The caveats are: * Custom enums used in the bitfield must specify const funcs for bit conversion, and bytemuck's cast functions aren't const. * The bitfield crate implements Clone for you, but does not seem to have a way to disable this. We can't rely on their clone, since for pointer types we need to allocate a new Box. To get around this, PackedOperation is a wrapper around an internal bitfield struct (rather than being a bitfield struct itself). (Note: I'm not yet happy with this. Specifically, I think the abstraction may be cleaner if OpBitField is defined entirely in terms of raw native types and any reinterpretation / transmutation is done from PackedOperation. Consider this a first pass.) * Pivot. * Compiling. * Remove unused bytemuck stuff. * Run format. * Improve static assertion error messages. * Clean up. * Implement delay unit for ImmediateValue. * Finish implementing PointerBits for 32 bit. I can't easily test this on my own machine, but I think it'll work. * More cleanup and documentation updates. * Ensure subclasses of Measure are not treated as standard. * Drop 32-bit support. * Make StandardInstruction::Barrier hold a u32. * Remove static assertion in favor of comment. * Add notes on bitfield macro behavior to structs. * Make immediate a union. This is really what it should be here, since selecting one of its members is inherently unsafe and something the caller should be responsible for doing correctly. * Rewrite to localize bit packing by kind. * Rename 'standard_gate label. * Add backup handling for PyInstruction compare. This shouldn't happen, but if it does, we can handle it gracefully like we do for StandardGate and PyGate. * Use StandardInstruction in BarrierBeforeFinalMeasurements. * Rename OperationRef::StandardGate. * Replace Barrier uses. * Remove _Pointer suffixes. * Merge fixes. * Avoid coercing int to float in Delay duration. * Update Cargo.lock. * Fix lint. * Make impl_packable_pointer operation_type an expr. * Move PackedOperation Drop to root mod. Also uses codegen for From> explicitly for all T, rather than a blanket impl on the private PackablePointer trait to ensure proper docs are generated. * Remove StandardGate pad. * Add #[inline] in a few places. Probably not necessary in most of these cases. --- Cargo.lock | 12 + Cargo.toml | 1 + .../src/barrier_before_final_measurement.rs | 53 +- .../src/circuit_library/multi_local.rs | 23 +- .../src/circuit_library/pauli_evolution.rs | 35 +- .../src/circuit_library/pauli_feature_map.rs | 35 +- crates/accelerate/src/commutation_checker.rs | 33 +- crates/accelerate/src/gate_direction.rs | 6 +- .../accelerate/src/remove_identity_equiv.rs | 2 +- .../src/synthesis/multi_controlled/mcmt.rs | 2 +- .../accelerate/src/target_transpiler/mod.rs | 5 +- crates/accelerate/src/twirling.rs | 10 +- crates/accelerate/src/two_qubit_decompose.rs | 4 +- crates/accelerate/src/unitary_synthesis.rs | 4 +- crates/circuit/Cargo.toml | 1 + crates/circuit/src/circuit_instruction.rs | 69 ++- crates/circuit/src/dag_circuit.rs | 61 +- crates/circuit/src/imports.rs | 2 + crates/circuit/src/lib.rs | 1 + crates/circuit/src/operations.rs | 259 ++++++++- crates/circuit/src/packed_instruction.rs | 538 ++++++++++++------ qiskit/circuit/barrier.py | 4 +- qiskit/circuit/delay.py | 3 + qiskit/circuit/measure.py | 9 + qiskit/circuit/reset.py | 3 + 25 files changed, 818 insertions(+), 357 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b4157bc1ce61..717596ccfdbc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -77,6 +77,17 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" +[[package]] +name = "bitfield-struct" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d7a33e7b9505a52e33ed0ad66db6434f18cda0b1c72665fabf14e85cdd39e43" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.96", +] + [[package]] name = "bitflags" version = "1.3.2" @@ -1210,6 +1221,7 @@ version = "2.0.0" dependencies = [ "ahash 0.8.11", "approx 0.5.1", + "bitfield-struct", "bytemuck", "hashbrown 0.14.5", "indexmap", diff --git a/Cargo.toml b/Cargo.toml index f8ce108e4b32..50276f437ec5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,6 +15,7 @@ license = "Apache-2.0" # Each crate can add on specific features freely as it inherits. [workspace.dependencies] bytemuck = "1.21" +bitfield-struct = "0.9.3" indexmap.version = "2.7.1" hashbrown.version = "0.14.5" num-bigint = "0.4" diff --git a/crates/accelerate/src/barrier_before_final_measurement.rs b/crates/accelerate/src/barrier_before_final_measurement.rs index 63fb250f8ed9..1ae258e34042 100644 --- a/crates/accelerate/src/barrier_before_final_measurement.rs +++ b/crates/accelerate/src/barrier_before_final_measurement.rs @@ -16,8 +16,7 @@ use rustworkx_core::petgraph::stable_graph::NodeIndex; use qiskit_circuit::circuit_instruction::ExtraInstructionAttributes; use qiskit_circuit::dag_circuit::{DAGCircuit, NodeType}; -use qiskit_circuit::imports::BARRIER; -use qiskit_circuit::operations::{Operation, PyInstruction}; +use qiskit_circuit::operations::{Operation, StandardInstruction}; use qiskit_circuit::packed_instruction::{PackedInstruction, PackedOperation}; use qiskit_circuit::Qubit; @@ -65,45 +64,19 @@ pub fn barrier_before_final_measurements( res }) .collect(); - let new_barrier = BARRIER - .get_bound(py) - .call1((dag.num_qubits(), label.as_deref()))?; - - let new_barrier_py_inst = PyInstruction { - qubits: dag.num_qubits() as u32, - clbits: 0, - params: 0, - op_name: "barrier".to_string(), - control_flow: false, - #[cfg(feature = "cache_pygates")] - instruction: new_barrier.clone().unbind(), - #[cfg(not(feature = "cache_pygates"))] - instruction: new_barrier.unbind(), - }; let qargs: Vec = (0..dag.num_qubits() as u32).map(Qubit).collect(); - #[cfg(feature = "cache_pygates")] - { - dag.apply_operation_back( - py, - PackedOperation::from_instruction(Box::new(new_barrier_py_inst)), - qargs.as_slice(), - &[], - None, - ExtraInstructionAttributes::new(label, None, None, None), - Some(new_barrier.unbind()), - )?; - } - #[cfg(not(feature = "cache_pygates"))] - { - dag.apply_operation_back( - py, - PackedOperation::from_instruction(Box::new(new_barrier_py_inst)), - qargs.as_slice(), - &[], - None, - ExtraInstructionAttributes::new(label, None, None, None), - )?; - } + dag.apply_operation_back( + py, + PackedOperation::from_standard_instruction(StandardInstruction::Barrier( + dag.num_qubits() as u32 + )), + qargs.as_slice(), + &[], + None, + ExtraInstructionAttributes::new(label, None, None, None), + #[cfg(feature = "cache_pygates")] + None, + )?; for inst in final_packed_ops { dag.push_back(py, inst)?; } diff --git a/crates/accelerate/src/circuit_library/multi_local.rs b/crates/accelerate/src/circuit_library/multi_local.rs index dd75ebbcc5e8..ca48919d616a 100644 --- a/crates/accelerate/src/circuit_library/multi_local.rs +++ b/crates/accelerate/src/circuit_library/multi_local.rs @@ -19,8 +19,8 @@ use qiskit_circuit::packed_instruction::PackedOperation; use smallvec::{smallvec, SmallVec}; use qiskit_circuit::circuit_data::CircuitData; -use qiskit_circuit::operations::{Param, PyInstruction}; -use qiskit_circuit::{imports, Clbit, Qubit}; +use qiskit_circuit::operations::{Param, StandardInstruction}; +use qiskit_circuit::{Clbit, Qubit}; use itertools::izip; @@ -193,7 +193,7 @@ pub fn n_local( // This struct can be used to yield barrier if insert_barriers is true, otherwise // it returns an empty iterator. For conveniently injecting barriers in-between operations. - let maybe_barrier = MaybeBarrier::new(py, num_qubits, insert_barriers)?; + let maybe_barrier = MaybeBarrier::new(num_qubits, insert_barriers)?; let packed_insts = (0..reps).flat_map(|layer| { rotation_layer( @@ -280,23 +280,14 @@ struct MaybeBarrier { } impl MaybeBarrier { - fn new(py: Python, num_qubits: u32, insert_barriers: bool) -> PyResult { + fn new(num_qubits: u32, insert_barriers: bool) -> PyResult { if !insert_barriers { Ok(Self { barrier: None }) } else { - let barrier_cls = imports::BARRIER.get_bound(py); - let py_barrier = barrier_cls.call1((num_qubits,))?; - let py_inst = PyInstruction { - qubits: num_qubits, - clbits: 0, - params: 0, - op_name: "barrier".to_string(), - control_flow: false, - instruction: py_barrier.into(), - }; - let inst = ( - py_inst.into(), + PackedOperation::from_standard_instruction(StandardInstruction::Barrier( + num_qubits, + )), smallvec![], (0..num_qubits).map(Qubit).collect(), vec![] as Vec, diff --git a/crates/accelerate/src/circuit_library/pauli_evolution.rs b/crates/accelerate/src/circuit_library/pauli_evolution.rs index b5aa71716341..cbdfe15e5f8c 100644 --- a/crates/accelerate/src/circuit_library/pauli_evolution.rs +++ b/crates/accelerate/src/circuit_library/pauli_evolution.rs @@ -13,9 +13,10 @@ use pyo3::prelude::*; use pyo3::types::{PyList, PyString, PyTuple}; use qiskit_circuit::circuit_data::CircuitData; -use qiskit_circuit::operations::{multiply_param, radd_param, Param, PyInstruction, StandardGate}; +use qiskit_circuit::operations; +use qiskit_circuit::operations::{multiply_param, radd_param, Param, StandardGate}; use qiskit_circuit::packed_instruction::PackedOperation; -use qiskit_circuit::{imports, Clbit, Qubit}; +use qiskit_circuit::{Clbit, Qubit}; use smallvec::{smallvec, SmallVec}; // custom types for a more readable code @@ -248,7 +249,14 @@ pub fn py_pauli_evolution( indices.push(tuple.get_item(1)?.extract::>()?) } - let barrier = get_barrier(py, num_qubits as u32); + let barrier = ( + PackedOperation::from_standard_instruction(operations::StandardInstruction::Barrier( + num_qubits as u32, + )), + smallvec![], + (0..num_qubits as u32).map(Qubit).collect(), + vec![], + ); let evos = paulis.iter().enumerate().zip(indices).zip(times).flat_map( |(((i, pauli), qubits), time)| { @@ -328,24 +336,3 @@ fn cx_fountain( ) })) } - -fn get_barrier(py: Python, num_qubits: u32) -> Instruction { - let barrier_cls = imports::BARRIER.get_bound(py); - let barrier = barrier_cls - .call1((num_qubits,)) - .expect("Could not create Barrier Python-side"); - let barrier_inst = PyInstruction { - qubits: num_qubits, - clbits: 0, - params: 0, - op_name: "barrier".to_string(), - control_flow: false, - instruction: barrier.into(), - }; - ( - barrier_inst.into(), - smallvec![], - (0..num_qubits).map(Qubit).collect(), - vec![], - ) -} diff --git a/crates/accelerate/src/circuit_library/pauli_feature_map.rs b/crates/accelerate/src/circuit_library/pauli_feature_map.rs index 66bea22a7231..497d2eb2ee0f 100644 --- a/crates/accelerate/src/circuit_library/pauli_feature_map.rs +++ b/crates/accelerate/src/circuit_library/pauli_feature_map.rs @@ -14,9 +14,9 @@ use pyo3::prelude::*; use pyo3::types::PySequence; use pyo3::types::PyString; use qiskit_circuit::circuit_data::CircuitData; -use qiskit_circuit::imports; -use qiskit_circuit::operations::PyInstruction; -use qiskit_circuit::operations::{add_param, multiply_param, multiply_params, Param, StandardGate}; +use qiskit_circuit::operations::{ + add_param, multiply_param, multiply_params, Param, StandardGate, StandardInstruction, +}; use qiskit_circuit::packed_instruction::PackedOperation; use qiskit_circuit::{Clbit, Qubit}; use smallvec::{smallvec, SmallVec}; @@ -78,7 +78,14 @@ pub fn pauli_feature_map( // construct a Barrier object Python side to (possibly) add to the circuit let packed_barrier = if insert_barriers { - Some(_get_barrier(py, feature_dimension)?) + Some(( + PackedOperation::from_standard_instruction(StandardInstruction::Barrier( + feature_dimension, + )), + smallvec![], + (0..feature_dimension).map(Qubit).collect(), + vec![] as Vec, + )) } else { None }; @@ -244,23 +251,3 @@ fn _get_paulis( }, ) } - -/// Get a barrier object from Python space. -fn _get_barrier(py: Python, feature_dimension: u32) -> PyResult { - let barrier_cls = imports::BARRIER.get_bound(py); - let barrier = barrier_cls.call1((feature_dimension,))?; - let barrier_inst = PyInstruction { - qubits: feature_dimension, - clbits: 0, - params: 0, - op_name: "barrier".to_string(), - control_flow: false, - instruction: barrier.into(), - }; - Ok(( - barrier_inst.into(), - smallvec![], - (0..feature_dimension).map(Qubit).collect(), - vec![] as Vec, - )) -} diff --git a/crates/accelerate/src/commutation_checker.rs b/crates/accelerate/src/commutation_checker.rs index 89826578615f..6484c562c2dd 100644 --- a/crates/accelerate/src/commutation_checker.rs +++ b/crates/accelerate/src/commutation_checker.rs @@ -56,30 +56,45 @@ static SUPPORTED_OP: Lazy> = Lazy::new(|| { // E.g. RX is generated by X and 2-pi periodic, while CRX is generated by CX and 4-pi periodic. static SUPPORTED_ROTATIONS: Lazy)>> = Lazy::new(|| { HashMap::from([ - ("rx", (2, Some(OperationRef::Standard(StandardGate::XGate)))), - ("ry", (2, Some(OperationRef::Standard(StandardGate::YGate)))), - ("rz", (2, Some(OperationRef::Standard(StandardGate::ZGate)))), - ("p", (2, Some(OperationRef::Standard(StandardGate::ZGate)))), - ("u1", (2, Some(OperationRef::Standard(StandardGate::ZGate)))), + ( + "rx", + (2, Some(OperationRef::StandardGate(StandardGate::XGate))), + ), + ( + "ry", + (2, Some(OperationRef::StandardGate(StandardGate::YGate))), + ), + ( + "rz", + (2, Some(OperationRef::StandardGate(StandardGate::ZGate))), + ), + ( + "p", + (2, Some(OperationRef::StandardGate(StandardGate::ZGate))), + ), + ( + "u1", + (2, Some(OperationRef::StandardGate(StandardGate::ZGate))), + ), ("rxx", (2, None)), // None means the gate is in the commutation dictionary ("ryy", (2, None)), ("rzx", (2, None)), ("rzz", (2, None)), ( "crx", - (4, Some(OperationRef::Standard(StandardGate::CXGate))), + (4, Some(OperationRef::StandardGate(StandardGate::CXGate))), ), ( "cry", - (4, Some(OperationRef::Standard(StandardGate::CYGate))), + (4, Some(OperationRef::StandardGate(StandardGate::CYGate))), ), ( "crz", - (4, Some(OperationRef::Standard(StandardGate::CZGate))), + (4, Some(OperationRef::StandardGate(StandardGate::CZGate))), ), ( "cp", - (2, Some(OperationRef::Standard(StandardGate::CZGate))), + (2, Some(OperationRef::StandardGate(StandardGate::CZGate))), ), ]) }); diff --git a/crates/accelerate/src/gate_direction.rs b/crates/accelerate/src/gate_direction.rs index f62f049f5a84..711779a3b47a 100755 --- a/crates/accelerate/src/gate_direction.rs +++ b/crates/accelerate/src/gate_direction.rs @@ -208,7 +208,7 @@ fn py_fix_direction_target( ]; // Take this path so Target can check for exact match of the parameterized gate's angle - if let OperationRef::Standard(std_gate) = inst.op.view() { + if let OperationRef::StandardGate(std_gate) = inst.op.view() { match std_gate { StandardGate::RXXGate | StandardGate::RYYGate @@ -309,7 +309,7 @@ where // If the op has a pre-defined replacement - replace if the other direction is supported otherwise error // If no pre-defined replacement for the op - if the other direction is supported error saying no pre-defined rule otherwise error saying op is not supported - if let OperationRef::Standard(std_gate) = packed_inst.op.view() { + if let OperationRef::StandardGate(std_gate) = packed_inst.op.view() { match std_gate { StandardGate::CXGate | StandardGate::ECRGate @@ -463,7 +463,7 @@ fn apply_operation_back( ) -> PyResult<()> { dag.apply_operation_back( py, - PackedOperation::from_standard(gate), + PackedOperation::from_standard_gate(gate), qargs, &[], param, diff --git a/crates/accelerate/src/remove_identity_equiv.rs b/crates/accelerate/src/remove_identity_equiv.rs index f6dbc5ea20a3..d94250f3c399 100644 --- a/crates/accelerate/src/remove_identity_equiv.rs +++ b/crates/accelerate/src/remove_identity_equiv.rs @@ -76,7 +76,7 @@ fn remove_identity_equiv( for (op_node, inst) in dag.op_nodes(false) { match inst.op.view() { - OperationRef::Standard(gate) => { + OperationRef::StandardGate(gate) => { let (dim, trace) = match gate { StandardGate::RXGate | StandardGate::RYGate | StandardGate::RZGate => { if let Param::Float(theta) = inst.params_view()[0] { diff --git a/crates/accelerate/src/synthesis/multi_controlled/mcmt.rs b/crates/accelerate/src/synthesis/multi_controlled/mcmt.rs index 42f7a72542f6..f2091113dde7 100644 --- a/crates/accelerate/src/synthesis/multi_controlled/mcmt.rs +++ b/crates/accelerate/src/synthesis/multi_controlled/mcmt.rs @@ -118,7 +118,7 @@ pub fn mcmt_v_chain( .filter(|index| control_state & (1 << index) == 0) .map(|index| { Ok(( - PackedOperation::from_standard(StandardGate::XGate), + PackedOperation::from_standard_gate(StandardGate::XGate), smallvec![] as SmallVec<[Param; 3]>, vec![Qubit::new(index)], vec![] as Vec, diff --git a/crates/accelerate/src/target_transpiler/mod.rs b/crates/accelerate/src/target_transpiler/mod.rs index 42c21e6f8668..0a14a3dec72f 100644 --- a/crates/accelerate/src/target_transpiler/mod.rs +++ b/crates/accelerate/src/target_transpiler/mod.rs @@ -774,7 +774,10 @@ impl Target { }; let out_inst = match inst { TargetOperation::Normal(op) => match op.operation.view() { - OperationRef::Standard(standard) => standard + OperationRef::StandardGate(standard) => standard + .create_py_op(py, Some(&op.params), &ExtraInstructionAttributes::default())? + .into_any(), + OperationRef::StandardInstruction(standard) => standard .create_py_op(py, Some(&op.params), &ExtraInstructionAttributes::default())? .into_any(), OperationRef::Gate(gate) => gate.gate.clone_ref(py), diff --git a/crates/accelerate/src/twirling.rs b/crates/accelerate/src/twirling.rs index 881e4340f9ea..480b3924ea76 100644 --- a/crates/accelerate/src/twirling.rs +++ b/crates/accelerate/src/twirling.rs @@ -204,7 +204,7 @@ fn twirl_gate( out_circ.push( py, PackedInstruction { - op: PackedOperation::from_standard(twirl[0]), + op: PackedOperation::from_standard_gate(twirl[0]), qubits: bit_zero, clbits: circ.cargs_interner().get_default(), params: None, @@ -216,7 +216,7 @@ fn twirl_gate( out_circ.push( py, PackedInstruction { - op: PackedOperation::from_standard(twirl[1]), + op: PackedOperation::from_standard_gate(twirl[1]), qubits: bit_one, clbits: circ.cargs_interner().get_default(), params: None, @@ -230,7 +230,7 @@ fn twirl_gate( out_circ.push( py, PackedInstruction { - op: PackedOperation::from_standard(twirl[2]), + op: PackedOperation::from_standard_gate(twirl[2]), qubits: bit_zero, clbits: circ.cargs_interner().get_default(), params: None, @@ -242,7 +242,7 @@ fn twirl_gate( out_circ.push( py, PackedInstruction { - op: PackedOperation::from_standard(twirl[3]), + op: PackedOperation::from_standard_gate(twirl[3]), qubits: bit_one, clbits: circ.cargs_interner().get_default(), params: None, @@ -278,7 +278,7 @@ fn generate_twirled_circuit( } } match inst.op.view() { - OperationRef::Standard(gate) => match gate { + OperationRef::StandardGate(gate) => match gate { StandardGate::CXGate => { if twirling_mask & CX_MASK != 0 { twirl_gate(py, circ, rng, &mut out_circ, TWIRLING_SETS[0], inst)?; diff --git a/crates/accelerate/src/two_qubit_decompose.rs b/crates/accelerate/src/two_qubit_decompose.rs index f691e8333684..de20430d25bf 100644 --- a/crates/accelerate/src/two_qubit_decompose.rs +++ b/crates/accelerate/src/two_qubit_decompose.rs @@ -2245,7 +2245,7 @@ impl TwoQubitBasisDecomposer { .into_iter() .map(|(gate, params, qubits)| match gate { Some(gate) => Ok(( - PackedOperation::from_standard(gate), + PackedOperation::from_standard_gate(gate), params.into_iter().map(Param::Float).collect(), qubits.into_iter().map(|x| Qubit(x.into())).collect(), Vec::new(), @@ -2877,7 +2877,7 @@ impl TwoQubitControlledUDecomposer { .into_iter() .map(|(gate, params, qubits)| match gate { Some(gate) => Ok(( - PackedOperation::from_standard(gate), + PackedOperation::from_standard_gate(gate), params.into_iter().map(Param::Float).collect(), qubits.into_iter().map(|x| Qubit(x.into())).collect(), Vec::new(), diff --git a/crates/accelerate/src/unitary_synthesis.rs b/crates/accelerate/src/unitary_synthesis.rs index b7b15c4c88a1..2721c58c9237 100644 --- a/crates/accelerate/src/unitary_synthesis.rs +++ b/crates/accelerate/src/unitary_synthesis.rs @@ -143,7 +143,7 @@ fn apply_synth_sequence( None => Some(Box::new(sequence.decomp_gate.params.clone())), }; let instruction = PackedInstruction { - op: PackedOperation::from_standard(gate_node), + op: PackedOperation::from_standard_gate(gate_node), qubits: out_dag.qargs_interner.insert(&mapped_qargs), clbits: out_dag.cargs_interner.get_default(), params: new_params, @@ -595,7 +595,7 @@ fn get_2q_decomposers_from_target( Ok(op) => { match op.operation.view() { OperationRef::Gate(_) => (), - OperationRef::Standard(_) => (), + OperationRef::StandardGate(_) => (), _ => continue, } // Filter out non-2q-gate candidates diff --git a/crates/circuit/Cargo.toml b/crates/circuit/Cargo.toml index 373295408be1..477997f8004c 100644 --- a/crates/circuit/Cargo.toml +++ b/crates/circuit/Cargo.toml @@ -17,6 +17,7 @@ rayon.workspace = true ahash.workspace = true rustworkx-core.workspace = true bytemuck.workspace = true +bitfield-struct.workspace = true num-complex.workspace = true ndarray.workspace = true numpy.workspace = true diff --git a/crates/circuit/src/circuit_instruction.rs b/crates/circuit/src/circuit_instruction.rs index 3ded8da806a5..5d401de37826 100644 --- a/crates/circuit/src/circuit_instruction.rs +++ b/crates/circuit/src/circuit_instruction.rs @@ -29,6 +29,7 @@ use crate::imports::{ }; use crate::operations::{ Operation, OperationRef, Param, PyGate, PyInstruction, PyOperation, StandardGate, + StandardInstruction, StandardInstructionType, }; use crate::packed_instruction::PackedOperation; @@ -331,7 +332,10 @@ impl CircuitInstruction { } let out = match self.operation.view() { - OperationRef::Standard(standard) => standard + OperationRef::StandardGate(standard) => standard + .create_py_op(py, Some(&self.params), &self.extra_attrs)? + .into_any(), + OperationRef::StandardInstruction(instruction) => instruction .create_py_op(py, Some(&self.params), &self.extra_attrs)? .into_any(), OperationRef::Gate(gate) => gate.gate.clone_ref(py), @@ -395,7 +399,7 @@ impl CircuitInstruction { /// :class:`.ControlledGate`? pub fn is_controlled_gate(&self, py: Python) -> PyResult { match self.operation.view() { - OperationRef::Standard(standard) => Ok(standard.num_ctrl_qubits() != 0), + OperationRef::StandardGate(standard) => Ok(standard.num_ctrl_qubits() != 0), OperationRef::Gate(gate) => gate .gate .bind(py) @@ -645,6 +649,18 @@ impl<'py> FromPyObject<'py> for OperationFromPython { .transpose() .map(|params| params.unwrap_or_default()) }; + let extract_params_no_coerce = || { + ob.getattr(intern!(py, "params")) + .ok() + .map(|params| { + params + .try_iter()? + .map(|p| Param::extract_no_coerce(&p?)) + .collect() + }) + .transpose() + .map(|params| params.unwrap_or_default()) + }; let extract_extra = || -> PyResult<_> { let unit = { // We accept Python-space `None` or `"dt"` as both meaning the default `"dt"`. @@ -670,13 +686,15 @@ impl<'py> FromPyObject<'py> for OperationFromPython { )) }; - 'standard: { + 'standard_gate: { + // Our Python standard gates have a `_standard_gate` field at the class level so we can + // quickly identify them here without an `isinstance` check. let Some(standard) = ob_type .getattr(intern!(py, "_standard_gate")) .and_then(|standard| standard.extract::()) .ok() else { - break 'standard; + break 'standard_gate; }; // If the instruction is a controlled gate with a not-all-ones control state, it doesn't @@ -697,14 +715,53 @@ impl<'py> FromPyObject<'py> for OperationFromPython { .getattr(intern!(py, "label"))? .is_none()) { - break 'standard; + break 'standard_gate; } return Ok(OperationFromPython { - operation: PackedOperation::from_standard(standard), + operation: PackedOperation::from_standard_gate(standard), params: extract_params()?, extra_attrs: extract_extra()?, }); } + 'standard_instr: { + // Our Python standard instructions have a `_standard_instruction_type` field at the + // class level so we can quickly identify them here without an `isinstance` check. + // Once we know the type, we query the object for any type-specific fields we need to + // read (e.g. a Barrier's number of qubits) to build the Rust representation. + let Some(standard_type) = ob_type + .getattr(intern!(py, "_standard_instruction_type")) + .and_then(|standard| standard.extract::()) + .ok() + else { + break 'standard_instr; + }; + let standard = match standard_type { + StandardInstructionType::Barrier => { + let num_qubits = ob.getattr(intern!(py, "num_qubits"))?.extract()?; + StandardInstruction::Barrier(num_qubits) + } + StandardInstructionType::Delay => { + let unit = ob.getattr(intern!(py, "unit"))?.extract()?; + return Ok(OperationFromPython { + operation: PackedOperation::from_standard_instruction( + StandardInstruction::Delay(unit), + ), + // If the delay's duration is a Python int, we preserve it rather than + // coercing it to a float (e.g. when unit is 'dt'). + params: extract_params_no_coerce()?, + extra_attrs: extract_extra()?, + }); + } + StandardInstructionType::Measure => StandardInstruction::Measure, + StandardInstructionType::Reset => StandardInstruction::Reset, + }; + return Ok(OperationFromPython { + operation: PackedOperation::from_standard_instruction(standard), + params: extract_params()?, + extra_attrs: extract_extra()?, + }); + } + if ob_type.is_subclass(GATE.get_bound(py))? { let params = extract_params()?; let gate = Box::new(PyGate { diff --git a/crates/circuit/src/dag_circuit.rs b/crates/circuit/src/dag_circuit.rs index d891dfe2af12..41de1e1d559b 100644 --- a/crates/circuit/src/dag_circuit.rs +++ b/crates/circuit/src/dag_circuit.rs @@ -2575,7 +2575,8 @@ def _format(operand): }; match [inst1.op.view(), inst2.op.view()] { - [OperationRef::Standard(_op1), OperationRef::Standard(_op2)] => { + [OperationRef::StandardGate(_), OperationRef::StandardGate(_)] + | [OperationRef::StandardInstruction(_), OperationRef::StandardInstruction(_)] => { Ok(inst1.py_op_eq(py, inst2)? && check_args() && check_conditions()? @@ -2620,13 +2621,14 @@ def _format(operand): [OperationRef::Operation(_op1), OperationRef::Operation(_op2)] => { Ok(inst1.py_op_eq(py, inst2)? && check_args()) } - // Handle the case we end up with a pygate for a standardgate - // this typically only happens if it's a ControlledGate in python + // Handle the edge case where we end up with a Python object and a standard + // gate/instruction. + // This typically only happens if we have a ControlledGate in Python // and we have mutable state set. - [OperationRef::Standard(_op1), OperationRef::Gate(_op2)] => { - Ok(inst1.py_op_eq(py, inst2)? && check_args() && check_conditions()?) - } - [OperationRef::Gate(_op1), OperationRef::Standard(_op2)] => { + [OperationRef::StandardGate(_), OperationRef::Gate(_)] + | [OperationRef::Gate(_), OperationRef::StandardGate(_)] + | [OperationRef::StandardInstruction(_), OperationRef::Instruction(_)] + | [OperationRef::Instruction(_), OperationRef::StandardInstruction(_)] => { Ok(inst1.py_op_eq(py, inst2)? && check_args() && check_conditions()?) } _ => Ok(false), @@ -3292,7 +3294,8 @@ def _format(operand): OperationRef::Operation(py_op) => { py_op.operation.setattr(py, "condition", new_condition)?; } - OperationRef::Standard(_) => {} + OperationRef::StandardGate(_) + | OperationRef::StandardInstruction(_) => {} } } } @@ -3741,7 +3744,7 @@ def _format(operand): .node_references() .filter_map(|(node, weight)| match weight { NodeType::Operation(ref packed) => match packed.op.view() { - OperationRef::Gate(_) | OperationRef::Standard(_) => { + OperationRef::Gate(_) | OperationRef::StandardGate(_) => { Some(self.unpack_into(py, node, weight)) } _ => None, @@ -4925,7 +4928,7 @@ impl DAGCircuit { let node = &self.dag[node_index]; match node { NodeType::Operation(inst) => match inst.op.view() { - OperationRef::Standard(gate) => Ok(Some( + OperationRef::StandardGate(gate) => Ok(Some( gate.num_qubits() <= 2 && inst.condition().is_none() && !inst.is_parameterized(), @@ -6312,7 +6315,9 @@ impl DAGCircuit { }; #[cfg(feature = "cache_pygates")] let py_op = match new_op.operation.view() { - OperationRef::Standard(_) => OnceLock::new(), + OperationRef::StandardGate(_) | OperationRef::StandardInstruction(_) => { + OnceLock::new() + } OperationRef::Gate(gate) => OnceLock::from(gate.gate.clone_ref(py)), OperationRef::Instruction(instruction) => { OnceLock::from(instruction.instruction.clone_ref(py)) @@ -7086,10 +7091,10 @@ fn emit_pulse_dependency_deprecation(py: Python, msg: &str) { #[cfg(all(test, not(miri)))] mod test { - use crate::circuit_instruction::OperationFromPython; + use crate::circuit_instruction::ExtraInstructionAttributes; use crate::dag_circuit::{DAGCircuit, Wire}; - use crate::imports::{CLASSICAL_REGISTER, MEASURE, QUANTUM_REGISTER}; - use crate::operations::StandardGate; + use crate::imports::{CLASSICAL_REGISTER, QUANTUM_REGISTER}; + use crate::operations::{StandardGate, StandardInstruction}; use crate::packed_instruction::{PackedInstruction, PackedOperation}; use crate::{Clbit, Qubit}; use ahash::HashSet; @@ -7109,7 +7114,7 @@ mod test { macro_rules! cx_gate { ($dag:expr, $q0:expr, $q1:expr) => { PackedInstruction { - op: PackedOperation::from_standard(StandardGate::CXGate), + op: PackedOperation::from_standard_gate(StandardGate::CXGate), qubits: $dag .qargs_interner .insert_owned(vec![Qubit($q0), Qubit($q1)]), @@ -7124,21 +7129,17 @@ mod test { macro_rules! measure { ($dag:expr, $qarg:expr, $carg:expr) => {{ - Python::with_gil(|py| { - let py_op = MEASURE.get_bound(py).call0().unwrap(); - let op_from_py: OperationFromPython = py_op.extract().unwrap(); - let qubits = $dag.qargs_interner.insert_owned(vec![Qubit($qarg)]); - let clbits = $dag.cargs_interner.insert_owned(vec![Clbit($qarg)]); - PackedInstruction { - op: op_from_py.operation, - qubits, - clbits, - params: Some(Box::new(op_from_py.params)), - extra_attrs: op_from_py.extra_attrs, - #[cfg(feature = "cache_pygates")] - py_op: Default::default(), - } - }) + let qubits = $dag.qargs_interner.insert_owned(vec![Qubit($qarg)]); + let clbits = $dag.cargs_interner.insert_owned(vec![Clbit($qarg)]); + PackedInstruction { + op: PackedOperation::from_standard_instruction(StandardInstruction::Measure), + qubits, + clbits, + params: None, + extra_attrs: ExtraInstructionAttributes::new(None, None, None, None), + #[cfg(feature = "cache_pygates")] + py_op: Default::default(), + } }}; } diff --git a/crates/circuit/src/imports.rs b/crates/circuit/src/imports.rs index abe0d15baf03..a591dfb1569e 100644 --- a/crates/circuit/src/imports.rs +++ b/crates/circuit/src/imports.rs @@ -112,7 +112,9 @@ pub static FOR_LOOP_OP_CHECK: ImportOnceCell = ImportOnceCell::new("qiskit.dagcircuit.dagnode", "_for_loop_eq"); pub static UUID: ImportOnceCell = ImportOnceCell::new("uuid", "UUID"); pub static BARRIER: ImportOnceCell = ImportOnceCell::new("qiskit.circuit", "Barrier"); +pub static DELAY: ImportOnceCell = ImportOnceCell::new("qiskit.circuit", "Delay"); pub static MEASURE: ImportOnceCell = ImportOnceCell::new("qiskit.circuit", "Measure"); +pub static RESET: ImportOnceCell = ImportOnceCell::new("qiskit.circuit", "Reset"); pub static UNITARY_GATE: ImportOnceCell = ImportOnceCell::new( "qiskit.circuit.library.generalized_gates.unitary", "UnitaryGate", diff --git a/crates/circuit/src/lib.rs b/crates/circuit/src/lib.rs index ba120acecefb..5138438dec33 100644 --- a/crates/circuit/src/lib.rs +++ b/crates/circuit/src/lib.rs @@ -165,6 +165,7 @@ pub fn circuit(m: &Bound) -> PyResult<()> { m.add_class::()?; m.add_class::()?; m.add_class::()?; + m.add_class::()?; Ok(()) } diff --git a/crates/circuit/src/operations.rs b/crates/circuit/src/operations.rs index 61fea09c20b0..a3a917815150 100644 --- a/crates/circuit/src/operations.rs +++ b/crates/circuit/src/operations.rs @@ -12,11 +12,11 @@ use approx::relative_eq; use std::f64::consts::PI; -use std::vec; +use std::{fmt, vec}; use crate::circuit_data::CircuitData; use crate::circuit_instruction::ExtraInstructionAttributes; -use crate::imports::get_std_gate_class; +use crate::imports::{get_std_gate_class, BARRIER, DELAY, MEASURE, RESET}; use crate::imports::{PARAMETER_EXPRESSION, QUANTUM_CIRCUIT}; use crate::{gate_matrix, impl_intopyobject_for_copy_pyclass, Qubit}; @@ -27,9 +27,10 @@ use smallvec::{smallvec, SmallVec}; use numpy::IntoPyArray; use numpy::PyArray2; use numpy::PyReadonlyArray2; +use pyo3::exceptions::PyValueError; use pyo3::prelude::*; use pyo3::types::{IntoPyDict, PyFloat, PyIterator, PyList, PyTuple}; -use pyo3::{intern, Python}; +use pyo3::{intern, IntoPyObjectExt, Python}; #[derive(Clone, Debug, IntoPyObject, IntoPyObjectRef)] pub enum Param { @@ -158,7 +159,8 @@ pub trait Operation { /// This is the main way that we interact immutably with general circuit operations from Rust space. #[derive(Debug)] pub enum OperationRef<'a> { - Standard(StandardGate), + StandardGate(StandardGate), + StandardInstruction(StandardInstruction), Gate(&'a PyGate), Instruction(&'a PyInstruction), Operation(&'a PyOperation), @@ -168,7 +170,8 @@ impl Operation for OperationRef<'_> { #[inline] fn name(&self) -> &str { match self { - Self::Standard(standard) => standard.name(), + Self::StandardGate(standard) => standard.name(), + Self::StandardInstruction(instruction) => instruction.name(), Self::Gate(gate) => gate.name(), Self::Instruction(instruction) => instruction.name(), Self::Operation(operation) => operation.name(), @@ -177,7 +180,8 @@ impl Operation for OperationRef<'_> { #[inline] fn num_qubits(&self) -> u32 { match self { - Self::Standard(standard) => standard.num_qubits(), + Self::StandardGate(standard) => standard.num_qubits(), + Self::StandardInstruction(instruction) => instruction.num_qubits(), Self::Gate(gate) => gate.num_qubits(), Self::Instruction(instruction) => instruction.num_qubits(), Self::Operation(operation) => operation.num_qubits(), @@ -186,7 +190,8 @@ impl Operation for OperationRef<'_> { #[inline] fn num_clbits(&self) -> u32 { match self { - Self::Standard(standard) => standard.num_clbits(), + Self::StandardGate(standard) => standard.num_clbits(), + Self::StandardInstruction(instruction) => instruction.num_clbits(), Self::Gate(gate) => gate.num_clbits(), Self::Instruction(instruction) => instruction.num_clbits(), Self::Operation(operation) => operation.num_clbits(), @@ -195,7 +200,8 @@ impl Operation for OperationRef<'_> { #[inline] fn num_params(&self) -> u32 { match self { - Self::Standard(standard) => standard.num_params(), + Self::StandardGate(standard) => standard.num_params(), + Self::StandardInstruction(instruction) => instruction.num_params(), Self::Gate(gate) => gate.num_params(), Self::Instruction(instruction) => instruction.num_params(), Self::Operation(operation) => operation.num_params(), @@ -204,7 +210,8 @@ impl Operation for OperationRef<'_> { #[inline] fn control_flow(&self) -> bool { match self { - Self::Standard(standard) => standard.control_flow(), + Self::StandardGate(standard) => standard.control_flow(), + Self::StandardInstruction(instruction) => instruction.control_flow(), Self::Gate(gate) => gate.control_flow(), Self::Instruction(instruction) => instruction.control_flow(), Self::Operation(operation) => operation.control_flow(), @@ -213,7 +220,8 @@ impl Operation for OperationRef<'_> { #[inline] fn blocks(&self) -> Vec { match self { - OperationRef::Standard(standard) => standard.blocks(), + OperationRef::StandardGate(standard) => standard.blocks(), + OperationRef::StandardInstruction(instruction) => instruction.blocks(), OperationRef::Gate(gate) => gate.blocks(), OperationRef::Instruction(instruction) => instruction.blocks(), OperationRef::Operation(operation) => operation.blocks(), @@ -222,7 +230,8 @@ impl Operation for OperationRef<'_> { #[inline] fn matrix(&self, params: &[Param]) -> Option> { match self { - Self::Standard(standard) => standard.matrix(params), + Self::StandardGate(standard) => standard.matrix(params), + Self::StandardInstruction(instruction) => instruction.matrix(params), Self::Gate(gate) => gate.matrix(params), Self::Instruction(instruction) => instruction.matrix(params), Self::Operation(operation) => operation.matrix(params), @@ -231,7 +240,8 @@ impl Operation for OperationRef<'_> { #[inline] fn definition(&self, params: &[Param]) -> Option { match self { - Self::Standard(standard) => standard.definition(params), + Self::StandardGate(standard) => standard.definition(params), + Self::StandardInstruction(instruction) => instruction.definition(params), Self::Gate(gate) => gate.definition(params), Self::Instruction(instruction) => instruction.definition(params), Self::Operation(operation) => operation.definition(params), @@ -240,7 +250,8 @@ impl Operation for OperationRef<'_> { #[inline] fn standard_gate(&self) -> Option { match self { - Self::Standard(standard) => standard.standard_gate(), + Self::StandardGate(standard) => standard.standard_gate(), + Self::StandardInstruction(instruction) => instruction.standard_gate(), Self::Gate(gate) => gate.standard_gate(), Self::Instruction(instruction) => instruction.standard_gate(), Self::Operation(operation) => operation.standard_gate(), @@ -249,7 +260,8 @@ impl Operation for OperationRef<'_> { #[inline] fn directive(&self) -> bool { match self { - Self::Standard(standard) => standard.directive(), + Self::StandardGate(standard) => standard.directive(), + Self::StandardInstruction(instruction) => instruction.directive(), Self::Gate(gate) => gate.directive(), Self::Instruction(instruction) => instruction.directive(), Self::Operation(operation) => operation.directive(), @@ -257,6 +269,221 @@ impl Operation for OperationRef<'_> { } } +#[derive(Clone, Debug, Copy, Eq, PartialEq, Hash)] +#[repr(u8)] +pub enum DelayUnit { + NS, + PS, + US, + MS, + S, + DT, +} + +unsafe impl ::bytemuck::CheckedBitPattern for DelayUnit { + type Bits = u8; + + fn is_valid_bit_pattern(bits: &Self::Bits) -> bool { + *bits < 6 + } +} +unsafe impl ::bytemuck::NoUninit for DelayUnit {} + +impl fmt::Display for DelayUnit { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "{}", + match self { + DelayUnit::NS => "ns", + DelayUnit::PS => "ps", + DelayUnit::US => "us", + DelayUnit::MS => "ms", + DelayUnit::S => "s", + DelayUnit::DT => "dt", + } + ) + } +} + +impl<'py> FromPyObject<'py> for DelayUnit { + fn extract_bound(b: &Bound<'py, PyAny>) -> Result { + let str: String = b.extract()?; + Ok(match str.as_str() { + "ns" => DelayUnit::NS, + "ps" => DelayUnit::PS, + "us" => DelayUnit::US, + "ms" => DelayUnit::MS, + "s" => DelayUnit::S, + "dt" => DelayUnit::DT, + unknown_unit => { + return Err(PyValueError::new_err(format!( + "Unit '{}' is invalid.", + unknown_unit + ))); + } + }) + } +} + +/// An internal type used to further discriminate the payload of a `PackedOperation` when its +/// discriminant is `PackedOperationType::StandardInstruction`. +/// +/// This is also used to tag standard instructions via the `_standard_instruction_type` class +/// attribute in the corresponding Python class. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[pyclass(module = "qiskit._accelerate.circuit", eq, eq_int)] +#[repr(u8)] +pub(crate) enum StandardInstructionType { + Barrier = 0, + Delay = 1, + Measure = 2, + Reset = 3, +} + +unsafe impl ::bytemuck::CheckedBitPattern for StandardInstructionType { + type Bits = u8; + + fn is_valid_bit_pattern(bits: &Self::Bits) -> bool { + *bits < 4 + } +} +unsafe impl ::bytemuck::NoUninit for StandardInstructionType {} + +#[derive(Clone, Debug, Copy, Eq, PartialEq, Hash)] +pub enum StandardInstruction { + Barrier(u32), + Delay(DelayUnit), + Measure, + Reset, +} + +// This must be kept up-to-date with `StandardInstruction` when adding or removing +// gates from the enum +// +// Remove this when std::mem::variant_count() is stabilized (see +// https://github.com/rust-lang/rust/issues/73662 ) +pub const STANDARD_INSTRUCTION_SIZE: usize = 4; + +impl Operation for StandardInstruction { + fn name(&self) -> &str { + match self { + StandardInstruction::Barrier(_) => "barrier", + StandardInstruction::Delay(_) => "delay", + StandardInstruction::Measure => "measure", + StandardInstruction::Reset => "reset", + } + } + + fn num_qubits(&self) -> u32 { + match self { + StandardInstruction::Barrier(num_qubits) => *num_qubits, + StandardInstruction::Delay(_) => 1, + StandardInstruction::Measure => 1, + StandardInstruction::Reset => 1, + } + } + + fn num_clbits(&self) -> u32 { + match self { + StandardInstruction::Barrier(_) => 0, + StandardInstruction::Delay(_) => 0, + StandardInstruction::Measure => 1, + StandardInstruction::Reset => 0, + } + } + + fn num_params(&self) -> u32 { + 0 + } + + fn control_flow(&self) -> bool { + false + } + + fn blocks(&self) -> Vec { + vec![] + } + + fn matrix(&self, _params: &[Param]) -> Option> { + None + } + + fn definition(&self, _params: &[Param]) -> Option { + None + } + + fn standard_gate(&self) -> Option { + None + } + + fn directive(&self) -> bool { + match self { + StandardInstruction::Barrier(_) => true, + StandardInstruction::Delay(_) => false, + StandardInstruction::Measure => false, + StandardInstruction::Reset => false, + } + } +} + +impl StandardInstruction { + pub fn create_py_op( + &self, + py: Python, + params: Option<&[Param]>, + extra_attrs: &ExtraInstructionAttributes, + ) -> PyResult> { + let (label, unit, duration, condition) = ( + extra_attrs.label(), + extra_attrs.unit(), + extra_attrs.duration(), + extra_attrs.condition(), + ); + let kwargs = label + .map(|label| [("label", label.into_py_any(py)?)].into_py_dict(py)) + .transpose()?; + let mut out = match self { + StandardInstruction::Barrier(num_qubits) => BARRIER + .get_bound(py) + .call1((num_qubits.into_py_any(py)?, label.into_py_any(py)?))?, + StandardInstruction::Delay(unit) => { + let duration = ¶ms.unwrap()[0]; + DELAY + .get_bound(py) + .call1((duration.into_py_any(py)?, unit.to_string()))? + } + StandardInstruction::Measure => MEASURE.get_bound(py).call((), kwargs.as_ref())?, + StandardInstruction::Reset => RESET.get_bound(py).call((), kwargs.as_ref())?, + }; + + if label.is_some() || unit.is_some() || duration.is_some() || condition.is_some() { + let mut mutable = false; + if let Some(condition) = condition { + if !mutable { + out = out.call_method0("to_mutable")?; + mutable = true; + } + out.setattr("condition", condition)?; + } + if let Some(duration) = duration { + if !mutable { + out = out.call_method0("to_mutable")?; + mutable = true; + } + out.setattr("_duration", duration)?; + } + if let Some(unit) = unit { + if !mutable { + out = out.call_method0("to_mutable")?; + } + out.setattr("_unit", unit)?; + } + } + Ok(out.unbind()) + } +} + #[derive(Clone, Debug, Copy, Eq, PartialEq, Hash)] #[repr(u8)] #[pyclass(module = "qiskit._accelerate.circuit", eq, eq_int)] @@ -313,6 +540,8 @@ pub enum StandardGate { C3XGate = 49, C3SXGate = 50, RC3XGate = 51, + // Remember to update StandardGate::is_valid_bit_pattern below + // if you add or remove this enum's variants! } impl_intopyobject_for_copy_pyclass!(StandardGate); @@ -320,7 +549,7 @@ unsafe impl ::bytemuck::CheckedBitPattern for StandardGate { type Bits = u8; fn is_valid_bit_pattern(bits: &Self::Bits) -> bool { - *bits < 53 + *bits < 52 } } unsafe impl ::bytemuck::NoUninit for StandardGate {} diff --git a/crates/circuit/src/packed_instruction.rs b/crates/circuit/src/packed_instruction.rs index 4da706280336..76b1896d3a27 100644 --- a/crates/circuit/src/packed_instruction.rs +++ b/crates/circuit/src/packed_instruction.rs @@ -10,7 +10,6 @@ // copyright notice, and modified files need to carry a notice indicating // that they have been altered from the originals. -use std::ptr::NonNull; #[cfg(feature = "cache_pygates")] use std::sync::OnceLock; @@ -24,10 +23,11 @@ use smallvec::SmallVec; use crate::circuit_data::CircuitData; use crate::circuit_instruction::ExtraInstructionAttributes; -use crate::imports::{get_std_gate_class, DEEPCOPY}; +use crate::imports::{get_std_gate_class, BARRIER, DEEPCOPY, DELAY, MEASURE, RESET}; use crate::interner::Interned; use crate::operations::{ Operation, OperationRef, Param, PyGate, PyInstruction, PyOperation, StandardGate, + StandardInstruction, }; use crate::{Clbit, Qubit}; @@ -39,15 +39,17 @@ enum PackedOperationType { // will make it appear as a standard gate, which will never allow accidental dangling-pointer // dereferencing. StandardGate = 0, - Gate = 1, - Instruction = 2, - Operation = 3, + StandardInstruction = 1, + PyGate = 2, + PyInstruction = 3, + PyOperation = 4, } + unsafe impl ::bytemuck::CheckedBitPattern for PackedOperationType { type Bits = u8; fn is_valid_bit_pattern(bits: &Self::Bits) -> bool { - *bits < 4 + *bits < 5 } } unsafe impl ::bytemuck::NoUninit for PackedOperationType {} @@ -58,53 +60,67 @@ unsafe impl ::bytemuck::NoUninit for PackedOperationType {} /// /// ```rust /// enum Operation { -/// Standard(StandardGate), +/// StandardGate(StandardGate), +/// StandardInstruction(StandardInstruction), /// Gate(Box), /// Instruction(Box), /// Operation(Box), /// } /// ``` /// -/// including all ownership semantics, except it bit-packs the enumeration into a single pointer. -/// This works because `PyGate` (and friends) have an alignment of 8, so pointers to them always -/// have the low three bits set to 0, and `StandardGate` has a width much smaller than a pointer. -/// This lets us store the enum discriminant in the low data bits, and then type-pun a suitable -/// bitmask on the contained value back into proper data. +/// including all ownership semantics, except it bit-packs the enumeration into a `u64`. +/// +/// The lowest three bits of this `u64` is always the discriminant and identifies which of the +/// above variants the field contains (and thus the layout required to decode it). +/// This works even for pointer variants (like `PyGate`) on 64-bit systems, which would normally +/// span the entire `u64`, since pointers on these systems have a natural alignment of 8 (and thus +/// their lowest three bits are always 0). This lets us store the discriminant within the address +/// and mask it out before reinterpreting it as a pointer. /// -/// Explicitly, this is logical memory layout of `PackedOperation` on a 64-bit system, written out -/// as a binary integer. `x` marks padding bits with undefined values, `S` is the bits that make up -/// a `StandardGate`, and `P` is bits that make up part of a pointer. +/// The layouts for each variant are described as follows, written out as a 64-bit binary integer. +/// `x` marks padding bits with undefined values. /// /// ```text -/// Standard gate: -/// 0b_xxxxxxxx_xxxxxxxx_xxxxxxxx_xxxxxxxx_xxxxxxxx_xxxxxxxx_xxxxxxSS_SSSSSS00 -/// |-------||| -/// | | -/// Standard gate, stored inline as a u8. --+ +-- Discriminant. +/// StandardGate: +/// 0b_xxxxxxxx_xxxxxxxx_xxxxxxxx_xxxxxxxx_xxxxxxxx_xxxxxxxx_SSSSSSSS_xxxxx000 +/// |------| |-| +/// | | +/// Standard gate, stored inline as a u8. --+ +-- Discriminant. +/// +/// StandardInstruction: +/// 0b_DDDDDDDD_DDDDDDDD_DDDDDDDD_DDDDDDDD_xxxxxxxx_xxxxxxxx_SSSSSSSS_xxxxx001 +/// |---------------------------------| |------| |-| +/// | | | +/// +-- An optional 32 bit immediate value. | | +/// Standard instruction type, stored inline as a u8. --+ +-- Discriminant. +/// +/// Optional immediate value: +/// Depending on the variant of the standard instruction type, a 32 bit +/// inline value may be present. Currently, this is used to store the +/// number of qubits in a Barrier and the unit of a Delay. /// -/// Python object: -/// 0b_PPPPPPPP_PPPPPPPP_PPPPPPPP_PPPPPPPP_PPPPPPPP_PPPPPPPP_PPPPPPPP_PPPPP10 -/// |------------------------------------------------------------------||| +/// Gate, Instruction, Operation: +/// 0b_PPPPPPPP_PPPPPPPP_PPPPPPPP_PPPPPPPP_PPPPPPPP_PPPPPPPP_PPPPPPPP_PPPP011 +/// |-----------------------------------------------------------------||-| /// | | /// The high 62 bits of the pointer. Because of alignment, the low 3 | Discriminant of the -/// bits of the full 64 bits are guaranteed to be zero (so one marked +-- enumeration. This -/// `P` is always zero here), so we can retrieve the "full" pointer by is 0b10, which means -/// taking the whole `usize` and zeroing the low 3 bits, letting us that this points to -/// store the discriminant in there at other times. a `PyInstruction`. +/// bits of the full 64 bits are guaranteed to be zero so we can +-- enumeration. This +/// retrieve the "full" pointer by taking the whole `u64` and zeroing is 0b011, which means +/// the low 3 bits, letting us store the discriminant in there at other that this points to +/// times. a `PyInstruction`. /// ``` /// -/// There is currently one spare bit that could be used for additional metadata, if required. -/// /// # Construction /// /// From Rust space, build this type using one of the `from_*` methods, depending on which -/// implementer of `Operation` you have. `StandardGate` has an implementation of `Into` for this. +/// implementer of `Operation` you have. `StandardGate` and `StandardInstruction` have +/// implementations of `Into` for this. /// /// From Python space, use the supplied `FromPyObject`. /// /// # Safety /// -/// `PackedOperation` asserts ownership over its contained pointer (if not a `StandardGate`). This +/// `PackedOperation` asserts ownership over its contained pointer (if it contains one). This /// has the following requirements: /// /// * The pointer must be managed by a `Box` using the global allocator. @@ -112,149 +128,308 @@ unsafe impl ::bytemuck::NoUninit for PackedOperationType {} /// * `PackedOperation` must take care to forward implementations of `Clone` and `Drop` to the /// contained pointer. #[derive(Debug)] -#[repr(transparent)] -pub struct PackedOperation(usize); +pub struct PackedOperation(u64); -impl PackedOperation { - /// The bits representing the `PackedOperationType` discriminant. This can be used to mask out - /// the discriminant, and defines the rest of the bit shifting. - const DISCRIMINANT_MASK: usize = 0b11; - /// The number of bits used to store the discriminant metadata. - const DISCRIMINANT_BITS: u32 = Self::DISCRIMINANT_MASK.count_ones(); - /// A bitmask that masks out only the standard gate information. This should always have the - /// same effect as `POINTER_MASK` because the high bits should be 0 for a `StandardGate`, but - /// this is defensive against us adding further metadata on `StandardGate` later. After - /// masking, the resulting integer still needs shifting downwards to retrieve the standard gate. - const STANDARD_GATE_MASK: usize = (u8::MAX as usize) << Self::DISCRIMINANT_BITS; - /// A bitmask that retrieves the stored pointer directly. The discriminant is stored in the - /// low pointer bits that are guaranteed to be 0 by alignment, so no shifting is required. - const POINTER_MASK: usize = usize::MAX ^ Self::DISCRIMINANT_MASK; - - /// Extract the discriminant of the operation. - #[inline] - fn discriminant(&self) -> PackedOperationType { - ::bytemuck::checked::cast((self.0 & Self::DISCRIMINANT_MASK) as u8) +/// A private module to encapsulate the encoding of [StandardGate]. +mod standard_gate { + use crate::operations::StandardGate; + use crate::packed_instruction::{PackedOperation, PackedOperationType}; + use bitfield_struct::bitfield; + + /// The packed layout of a standard gate, as a bitfield. + /// + /// NOTE: this _looks_ like a named struct, but the `bitfield` attribute macro + /// turns it into a transparent wrapper around a `u64`. + #[bitfield(u64)] + struct StandardGateBits { + #[bits(3)] + discriminant: u8, + #[bits(8)] + standard_gate: u8, + #[bits(53)] + _pad1: u64, + } + + impl From for PackedOperation { + fn from(value: StandardGate) -> Self { + Self( + StandardGateBits::new() + .with_discriminant(bytemuck::cast(PackedOperationType::StandardGate)) + .with_standard_gate(bytemuck::cast(value)) + .into_bits(), + ) + } } - /// Get the contained pointer to the `PyGate`/`PyInstruction`/`PyOperation` that this object - /// contains. + impl TryFrom<&PackedOperation> for StandardGate { + type Error = &'static str; + + fn try_from(value: &PackedOperation) -> Result { + match value.discriminant() { + PackedOperationType::StandardGate => { + let bits = StandardGateBits::from(value.0); + Ok(bytemuck::checked::cast(bits.standard_gate())) + } + _ => Err("not a standard gate!"), + } + } + } +} + +/// A private module to encapsulate the encoding of [StandardInstruction]. +mod standard_instruction { + use crate::operations::{StandardInstruction, StandardInstructionType}; + use crate::packed_instruction::{PackedOperation, PackedOperationType}; + use bitfield_struct::bitfield; + + /// The packed layout of a standard instruction, as a bitfield. /// - /// **Panics** if the object represents a standard gate; see `try_pointer`. - #[inline] - fn pointer(&self) -> NonNull<()> { - self.try_pointer() - .expect("the caller is responsible for knowing the correct type") + /// NOTE: this _looks_ like a named struct, but the `bitfield` attribute macro + /// turns it into a transparent wrapper around a `u64`. + #[bitfield(u64)] + struct StandardInstructionBits { + #[bits(3)] + discriminant: u8, + #[bits(5)] + _pad0: u8, + #[bits(8)] + standard_instruction: u8, + #[bits(16)] + _pad1: u32, + #[bits(32)] + payload: u32, + } + + impl From for PackedOperation { + fn from(value: StandardInstruction) -> Self { + let packed = StandardInstructionBits::new() + .with_discriminant(bytemuck::cast(PackedOperationType::StandardInstruction)); + Self( + match value { + StandardInstruction::Barrier(bits) => packed + .with_standard_instruction(bytemuck::cast(StandardInstructionType::Barrier)) + .with_payload(bits), + StandardInstruction::Delay(unit) => packed + .with_standard_instruction(bytemuck::cast(StandardInstructionType::Delay)) + .with_payload(unit as u32), + StandardInstruction::Measure => packed.with_standard_instruction( + bytemuck::cast(StandardInstructionType::Measure), + ), + StandardInstruction::Reset => packed + .with_standard_instruction(bytemuck::cast(StandardInstructionType::Reset)), + } + .into_bits(), + ) + } } - /// Get the contained pointer to the `PyGate`/`PyInstruction`/`PyOperation` that this object - /// contains. + impl TryFrom<&PackedOperation> for StandardInstruction { + type Error = &'static str; + + fn try_from(value: &PackedOperation) -> Result { + match value.discriminant() { + PackedOperationType::StandardInstruction => { + let bits = StandardInstructionBits::from_bits(value.0); + let ty: StandardInstructionType = + bytemuck::checked::cast(bits.standard_instruction()); + Ok(match ty { + StandardInstructionType::Barrier => { + StandardInstruction::Barrier(bits.payload()) + } + StandardInstructionType::Delay => StandardInstruction::Delay( + bytemuck::checked::cast(bits.payload() as u8), + ), + StandardInstructionType::Measure => StandardInstruction::Measure, + StandardInstructionType::Reset => StandardInstruction::Reset, + }) + } + _ => Err("not a standard instruction!"), + } + } + } +} + +/// A private module to encapsulate the encoding of pointer types. +mod pointer { + use crate::operations::{PyGate, PyInstruction, PyOperation}; + use crate::packed_instruction::{PackedOperation, PackedOperationType}; + use std::ptr::NonNull; + + const POINTER_MASK: u64 = !PackedOperation::DISCRIMINANT_MASK; + + /// Used to associate a supported pointer type (e.g. PyGate) with a [PackedOperationType] and + /// a drop implementation. /// - /// Returns `None` if the object represents a standard gate. + /// Note: this is public only within this file for use by [PackedOperation]'s [Drop] impl. + pub trait PackablePointer: Sized { + const OPERATION_TYPE: PackedOperationType; + + /// Drops `op` as this pointer type. + fn drop_packed(op: &mut PackedOperation) { + // This should only ever be called from PackedOperation's Drop impl after the + // operation's type has already been validated, but this is defensive just + // to 100% ensure that our `Drop` implementation doesn't panic. + let Some(pointer) = try_pointer::(op) else { + return; + }; + + // SAFETY: `PackedOperation` asserts ownership over its contents, and the contained + // pointer can only be null if we were already dropped. We set our discriminant to mark + // ourselves as plain old data immediately just as a defensive measure. + let boxed = unsafe { Box::from_raw(pointer.as_ptr()) }; + op.0 = PackedOperationType::StandardGate as u64; + ::std::mem::drop(boxed); + } + } + #[inline] - pub fn try_pointer(&self) -> Option> { - match self.discriminant() { - PackedOperationType::StandardGate => None, - PackedOperationType::Gate - | PackedOperationType::Instruction - | PackedOperationType::Operation => { - let ptr = (self.0 & Self::POINTER_MASK) as *mut (); - // SAFETY: `PackedOperation` can only be constructed from a pointer via `Box`, which - // is always non-null (except in the case that we're partway through a `Drop`). - Some(unsafe { NonNull::new_unchecked(ptr) }) - } + fn try_pointer(value: &PackedOperation) -> Option> { + if value.discriminant() == T::OPERATION_TYPE { + let ptr = (value.0 & POINTER_MASK) as *mut (); + // SAFETY: `PackedOperation` can only be constructed from a pointer via `Box`, which + // is always non-null (except in the case that we're partway through a `Drop`). + Some(unsafe { NonNull::new_unchecked(ptr) }.cast::()) + } else { + None } } + macro_rules! impl_packable_pointer { + ($type:ty, $operation_type:expr) => { + impl PackablePointer for $type { + const OPERATION_TYPE: PackedOperationType = $operation_type; + } + + impl From<$type> for PackedOperation { + #[inline] + fn from(value: $type) -> Self { + Box::new(value).into() + } + } + + // Supports reference conversion (e.g. &PackedOperation => &PyGate). + impl<'a> TryFrom<&'a PackedOperation> for &'a $type { + type Error = &'static str; + + fn try_from(value: &'a PackedOperation) -> Result { + try_pointer(value) + .map(|ptr| unsafe { ptr.as_ref() }) + .ok_or(concat!("not a(n) ", stringify!($type), " pointer!")) + } + } + + impl From> for PackedOperation { + fn from(value: Box<$type>) -> Self { + let discriminant = $operation_type as u64; + let ptr = NonNull::from(Box::leak(value)).cast::<()>(); + let addr = ptr.as_ptr() as u64; + assert!((addr & PackedOperation::DISCRIMINANT_MASK == 0)); + Self(discriminant | addr) + } + } + }; + } + + impl_packable_pointer!(PyGate, PackedOperationType::PyGate); + impl_packable_pointer!(PyInstruction, PackedOperationType::PyInstruction); + impl_packable_pointer!(PyOperation, PackedOperationType::PyOperation); +} + +impl PackedOperation { + const DISCRIMINANT_MASK: u64 = 0b111; + + #[inline] + fn discriminant(&self) -> PackedOperationType { + bytemuck::checked::cast((self.0 & Self::DISCRIMINANT_MASK) as u8) + } + /// Get the contained `StandardGate`. /// /// **Panics** if this `PackedOperation` doesn't contain a `StandardGate`; see /// `try_standard_gate`. #[inline] pub fn standard_gate(&self) -> StandardGate { - self.try_standard_gate() + self.try_into() .expect("the caller is responsible for knowing the correct type") } /// Get the contained `StandardGate`, if any. #[inline] pub fn try_standard_gate(&self) -> Option { - match self.discriminant() { - PackedOperationType::StandardGate => ::bytemuck::checked::try_cast( - ((self.0 & Self::STANDARD_GATE_MASK) >> Self::DISCRIMINANT_BITS) as u8, - ) - .ok(), - _ => None, - } + self.try_into().ok() + } + + /// Get the contained `StandardInstruction`. + /// + /// **Panics** if this `PackedOperation` doesn't contain a `StandardInstruction`; see + /// `try_standard_instruction`. + #[inline] + pub fn standard_instruction(&self) -> StandardInstruction { + self.try_into() + .expect("the caller is responsible for knowing the correct type") + } + + /// Get the contained `StandardInstruction`, if any. + #[inline] + pub fn try_standard_instruction(&self) -> Option { + self.try_into().ok() } /// Get a safe view onto the packed data within, without assuming ownership. #[inline] pub fn view(&self) -> OperationRef { match self.discriminant() { - PackedOperationType::StandardGate => OperationRef::Standard(self.standard_gate()), - PackedOperationType::Gate => { - let ptr = self.pointer().cast::(); - OperationRef::Gate(unsafe { ptr.as_ref() }) + PackedOperationType::StandardGate => OperationRef::StandardGate(self.standard_gate()), + PackedOperationType::StandardInstruction => { + OperationRef::StandardInstruction(self.standard_instruction()) } - PackedOperationType::Instruction => { - let ptr = self.pointer().cast::(); - OperationRef::Instruction(unsafe { ptr.as_ref() }) - } - PackedOperationType::Operation => { - let ptr = self.pointer().cast::(); - OperationRef::Operation(unsafe { ptr.as_ref() }) + PackedOperationType::PyGate => OperationRef::Gate(self.try_into().unwrap()), + PackedOperationType::PyInstruction => { + OperationRef::Instruction(self.try_into().unwrap()) } + PackedOperationType::PyOperation => OperationRef::Operation(self.try_into().unwrap()), } } /// Create a `PackedOperation` from a `StandardGate`. #[inline] - pub fn from_standard(standard: StandardGate) -> Self { - Self((standard as usize) << Self::DISCRIMINANT_BITS) + pub fn from_standard_gate(standard: StandardGate) -> Self { + standard.into() } - /// Create a `PackedOperation` given a raw pointer to the inner type. - /// - /// **Panics** if the given `discriminant` does not correspond to a pointer type. - /// - /// SAFETY: the inner pointer must have come from an owning `Box` in the global allocator, whose - /// type matches that indicated by the discriminant. The returned `PackedOperation` takes - /// ownership of the pointed-to data. + /// Create a `PackedOperation` from a `StandardInstruction`. #[inline] - unsafe fn from_py_wrapper(discriminant: PackedOperationType, value: NonNull<()>) -> Self { - if discriminant == PackedOperationType::StandardGate { - panic!("given standard-gate discriminant during pointer-type construction") - } - let addr = value.as_ptr() as usize; - assert_eq!(addr & Self::DISCRIMINANT_MASK, 0); - Self(addr | (discriminant as usize)) + pub fn from_standard_instruction(instruction: StandardInstruction) -> Self { + instruction.into() } /// Construct a new `PackedOperation` from an owned heap-allocated `PyGate`. + #[inline] pub fn from_gate(gate: Box) -> Self { - let ptr = NonNull::from(Box::leak(gate)).cast::<()>(); - // SAFETY: the `ptr` comes directly from a owning `Box` of the correct type. - unsafe { Self::from_py_wrapper(PackedOperationType::Gate, ptr) } + gate.into() } /// Construct a new `PackedOperation` from an owned heap-allocated `PyInstruction`. + #[inline] pub fn from_instruction(instruction: Box) -> Self { - let ptr = NonNull::from(Box::leak(instruction)).cast::<()>(); - // SAFETY: the `ptr` comes directly from a owning `Box` of the correct type. - unsafe { Self::from_py_wrapper(PackedOperationType::Instruction, ptr) } + instruction.into() } /// Construct a new `PackedOperation` from an owned heap-allocated `PyOperation`. + #[inline] pub fn from_operation(operation: Box) -> Self { - let ptr = NonNull::from(Box::leak(operation)).cast::<()>(); - // SAFETY: the `ptr` comes directly from a owning `Box` of the correct type. - unsafe { Self::from_py_wrapper(PackedOperationType::Operation, ptr) } + operation.into() } /// Check equality of the operation, including Python-space checks, if appropriate. pub fn py_eq(&self, py: Python, other: &PackedOperation) -> PyResult { match (self.view(), other.view()) { - (OperationRef::Standard(left), OperationRef::Standard(right)) => Ok(left == right), + (OperationRef::StandardGate(left), OperationRef::StandardGate(right)) => { + Ok(left == right) + } + (OperationRef::StandardInstruction(left), OperationRef::StandardInstruction(right)) => { + Ok(left == right) + } (OperationRef::Gate(left), OperationRef::Gate(right)) => { left.gate.bind(py).eq(&right.gate) } @@ -276,7 +451,10 @@ impl PackedOperation { ) -> PyResult { let deepcopy = DEEPCOPY.get_bound(py); match self.view() { - OperationRef::Standard(standard) => Ok(standard.into()), + OperationRef::StandardGate(standard) => Ok(standard.into()), + OperationRef::StandardInstruction(instruction) => { + Ok(Self::from_standard_instruction(instruction)) + } OperationRef::Gate(gate) => Ok(PyGate { gate: deepcopy.call1((&gate.gate, memo))?.unbind(), qubits: gate.qubits, @@ -310,7 +488,10 @@ impl PackedOperation { pub fn py_copy(&self, py: Python) -> PyResult { let copy_attr = intern!(py, "copy"); match self.view() { - OperationRef::Standard(standard) => Ok(standard.into()), + OperationRef::StandardGate(standard) => Ok(standard.into()), + OperationRef::StandardInstruction(instruction) => { + Ok(Self::from_standard_instruction(instruction)) + } OperationRef::Gate(gate) => Ok(Box::new(PyGate { gate: gate.gate.call_method0(py, copy_attr)?, qubits: gate.qubits, @@ -345,12 +526,32 @@ impl PackedOperation { pub fn py_op_is_instance(&self, py_type: &Bound) -> PyResult { let py = py_type.py(); let py_op = match self.view() { - OperationRef::Standard(standard) => { + OperationRef::StandardGate(standard) => { return get_std_gate_class(py, standard)? .bind(py) .downcast::()? .is_subclass(py_type) } + OperationRef::StandardInstruction(standard) => { + return match standard { + StandardInstruction::Barrier(_) => BARRIER + .get_bound(py) + .downcast::()? + .is_subclass(py_type), + StandardInstruction::Delay(_) => DELAY + .get_bound(py) + .downcast::()? + .is_subclass(py_type), + StandardInstruction::Measure => MEASURE + .get_bound(py) + .downcast::()? + .is_subclass(py_type), + StandardInstruction::Reset => RESET + .get_bound(py) + .downcast::()? + .is_subclass(py_type), + } + } OperationRef::Gate(gate) => gate.gate.bind(py), OperationRef::Instruction(instruction) => instruction.instruction.bind(py), OperationRef::Operation(operation) => operation.operation.bind(py), @@ -363,7 +564,8 @@ impl Operation for PackedOperation { fn name(&self) -> &str { let view = self.view(); let name = match view { - OperationRef::Standard(ref standard) => standard.name(), + OperationRef::StandardGate(ref standard) => standard.name(), + OperationRef::StandardInstruction(ref instruction) => instruction.name(), OperationRef::Gate(gate) => gate.name(), OperationRef::Instruction(instruction) => instruction.name(), OperationRef::Operation(operation) => operation.name(), @@ -416,38 +618,13 @@ impl Operation for PackedOperation { } } -impl From for PackedOperation { - #[inline] - fn from(value: StandardGate) -> Self { - Self::from_standard(value) - } -} - -macro_rules! impl_packed_operation_from_py { - ($type:ty, $constructor:path) => { - impl From<$type> for PackedOperation { - #[inline] - fn from(value: $type) -> Self { - $constructor(Box::new(value)) - } - } - - impl From> for PackedOperation { - #[inline] - fn from(value: Box<$type>) -> Self { - $constructor(value) - } - } - }; -} -impl_packed_operation_from_py!(PyGate, PackedOperation::from_gate); -impl_packed_operation_from_py!(PyInstruction, PackedOperation::from_instruction); -impl_packed_operation_from_py!(PyOperation, PackedOperation::from_operation); - impl Clone for PackedOperation { fn clone(&self) -> Self { match self.view() { - OperationRef::Standard(standard) => Self::from_standard(standard), + OperationRef::StandardGate(standard) => Self::from_standard_gate(standard), + OperationRef::StandardInstruction(instruction) => { + Self::from_standard_instruction(instruction) + } OperationRef::Gate(gate) => Self::from_gate(Box::new(gate.to_owned())), OperationRef::Instruction(instruction) => { Self::from_instruction(Box::new(instruction.to_owned())) @@ -458,27 +635,15 @@ impl Clone for PackedOperation { } } } + impl Drop for PackedOperation { fn drop(&mut self) { - fn drop_pointer_as(slf: &mut PackedOperation) { - // This should only ever be called when the pointer is valid, but this is defensive just - // to 100% ensure that our `Drop` implementation doesn't panic. - let Some(pointer) = slf.try_pointer() else { - return; - }; - // SAFETY: `PackedOperation` asserts ownership over its contents, and the contained - // pointer can only be null if we were already dropped. We set our discriminant to mark - // ourselves as plain old data immediately just as a defensive measure. - let boxed = unsafe { Box::from_raw(pointer.cast::().as_ptr()) }; - slf.0 = PackedOperationType::StandardGate as usize; - ::std::mem::drop(boxed); - } - + use crate::packed_instruction::pointer::PackablePointer; match self.discriminant() { - PackedOperationType::StandardGate => (), - PackedOperationType::Gate => drop_pointer_as::(self), - PackedOperationType::Instruction => drop_pointer_as::(self), - PackedOperationType::Operation => drop_pointer_as::(self), + PackedOperationType::StandardGate | PackedOperationType::StandardInstruction => (), + PackedOperationType::PyGate => PyGate::drop_packed(self), + PackedOperationType::PyInstruction => PyInstruction::drop_packed(self), + PackedOperationType::PyOperation => PyOperation::drop_packed(self), } } } @@ -562,15 +727,20 @@ impl PackedInstruction { /// Build a reference to the Python-space operation object (the `Gate`, etc) packed into this /// instruction. This may construct the reference if the `PackedInstruction` is a standard - /// gate with no already stored operation. + /// gate or instruction with no already stored operation. /// - /// A standard-gate operation object returned by this function is disconnected from the - /// containing circuit; updates to its parameters, label, duration, unit and condition will not - /// be propagated back. + /// A standard-gate or standard-instruction operation object returned by this function is + /// disconnected from the containing circuit; updates to its parameters, label, duration, unit + /// and condition will not be propagated back. pub fn unpack_py_op(&self, py: Python) -> PyResult> { let unpack = || -> PyResult> { match self.op.view() { - OperationRef::Standard(standard) => standard.create_py_op( + OperationRef::StandardGate(standard) => standard.create_py_op( + py, + self.params.as_deref().map(SmallVec::as_slice), + &self.extra_attrs, + ), + OperationRef::StandardInstruction(instruction) => instruction.create_py_op( py, self.params.as_deref().map(SmallVec::as_slice), &self.extra_attrs, @@ -605,7 +775,12 @@ impl PackedInstruction { /// Check equality of the operation, including Python-space checks, if appropriate. pub fn py_op_eq(&self, py: Python, other: &Self) -> PyResult { match (self.op.view(), other.op.view()) { - (OperationRef::Standard(left), OperationRef::Standard(right)) => Ok(left == right), + (OperationRef::StandardGate(left), OperationRef::StandardGate(right)) => { + Ok(left == right) + } + (OperationRef::StandardInstruction(left), OperationRef::StandardInstruction(right)) => { + Ok(left == right) + } (OperationRef::Gate(left), OperationRef::Gate(right)) => { left.gate.bind(py).eq(&right.gate) } @@ -618,12 +793,21 @@ impl PackedInstruction { // Handle the case we end up with a pygate for a standard gate // this typically only happens if it's a ControlledGate in python // and we have mutable state set. - (OperationRef::Standard(_left), OperationRef::Gate(right)) => { + (OperationRef::StandardGate(_left), OperationRef::Gate(right)) => { self.unpack_py_op(py)?.bind(py).eq(&right.gate) } - (OperationRef::Gate(left), OperationRef::Standard(_right)) => { + (OperationRef::Gate(left), OperationRef::StandardGate(_right)) => { other.unpack_py_op(py)?.bind(py).eq(&left.gate) } + // Handle the case we end up with a pyinstruction for a standard instruction + (OperationRef::StandardInstruction(_left), OperationRef::Instruction(right)) => { + println!("RHS is just instruction..."); + self.unpack_py_op(py)?.bind(py).eq(&right.instruction) + } + (OperationRef::Instruction(left), OperationRef::StandardInstruction(_right)) => { + println!("LHS is just instruction..."); + other.unpack_py_op(py)?.bind(py).eq(&left.instruction) + } _ => Ok(false), } } diff --git a/qiskit/circuit/barrier.py b/qiskit/circuit/barrier.py index d04769b68d39..b8046a0c6869 100644 --- a/qiskit/circuit/barrier.py +++ b/qiskit/circuit/barrier.py @@ -19,8 +19,9 @@ from __future__ import annotations from qiskit.exceptions import QiskitError +from qiskit.circuit.instruction import Instruction from qiskit.utils import deprecate_func -from .instruction import Instruction +from qiskit._accelerate.circuit import StandardInstructionType class Barrier(Instruction): @@ -31,6 +32,7 @@ class Barrier(Instruction): """ _directive = True + _standard_instruction_type = StandardInstructionType.Barrier def __init__(self, num_qubits: int, label: str | None = None): """ diff --git a/qiskit/circuit/delay.py b/qiskit/circuit/delay.py index 2f0613acb932..97a7f935d0f8 100644 --- a/qiskit/circuit/delay.py +++ b/qiskit/circuit/delay.py @@ -20,12 +20,15 @@ from qiskit.circuit import _utils from qiskit.circuit.parameterexpression import ParameterExpression from qiskit.utils import deprecate_func +from qiskit._accelerate.circuit import StandardInstructionType @_utils.with_gate_array(np.eye(2, dtype=complex)) class Delay(Instruction): """Do nothing and just delay/wait/idle for a specified duration.""" + _standard_instruction_type = StandardInstructionType.Delay + def __init__(self, duration, unit="dt"): """ Args: diff --git a/qiskit/circuit/measure.py b/qiskit/circuit/measure.py index ab329903db4a..14795c285a23 100644 --- a/qiskit/circuit/measure.py +++ b/qiskit/circuit/measure.py @@ -16,11 +16,14 @@ from qiskit.circuit.singleton import SingletonInstruction, stdlib_singleton_key from qiskit.circuit.exceptions import CircuitError +from qiskit._accelerate.circuit import StandardInstructionType class Measure(SingletonInstruction): """Quantum measurement in the computational basis.""" + _standard_instruction_type = StandardInstructionType.Measure + def __init__(self, label=None, *, duration=None, unit="dt"): """ Args: @@ -28,6 +31,12 @@ def __init__(self, label=None, *, duration=None, unit="dt"): """ super().__init__("measure", 1, 1, [], label=label, duration=duration, unit=unit) + def __init_subclass__(cls, **kwargs): + super().__init_subclass__(**kwargs) + # Subclasses of Measure are not "standard", so we set this to None to + # prevent the Rust code from treating them as such. + cls._standard_instruction_type = None + _singleton_lookup_key = stdlib_singleton_key() def broadcast_arguments(self, qargs, cargs): diff --git a/qiskit/circuit/reset.py b/qiskit/circuit/reset.py index 3649a882655e..80c4e43775f4 100644 --- a/qiskit/circuit/reset.py +++ b/qiskit/circuit/reset.py @@ -15,11 +15,14 @@ """ from qiskit.circuit.singleton import SingletonInstruction, stdlib_singleton_key +from qiskit._accelerate.circuit import StandardInstructionType class Reset(SingletonInstruction): r"""Incoherently reset a qubit to the :math:`\lvert0\rangle` state.""" + _standard_instruction_type = StandardInstructionType.Reset + def __init__(self, label=None, *, duration=None, unit="dt"): """ Args: From 4d2265f62a56d23e95bcc273705b048e3ed95acc Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Wed, 5 Feb 2025 18:50:42 +0000 Subject: [PATCH 06/48] Increase documentation of built-in transpiler plugins (#13620) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Increase documentation of built-in transpiler plugins This overhauls how the `qiskit.transpiler` documentation talks about the transpiler plugins. All of the built-in plugins now have a decent amount of overview documentation, and the requirements and expectations for each stage of the preset pipelines is explained in more detail. This form of the documentation makes the distinction between "a compilation routine in general" and "Qiskit's specific choice of default pipeline" clearer, to avoid confusion for advanced users. Much of the guide-level explanations of the different preset pipeline stages moved to https://docs.quantum.ibm.com some time ago, so this PR removes those, in favour of focussing on the actual API, and inserts links to learn more about the principles elsewhere. The guide-level explanation of scheduling is left in-place for now, because the content on the other parts of the IBM documentation isn't as complete for that. * Fix cross references in release notes * Include Elena's suggestions Co-authored-by: Elena Peña Tapia <57907331+ElePT@users.noreply.github.com> * Apply copy-editing suggestions Co-authored-by: abbycross * Add manual copy-editing suggestions * Reflow text * Copy edit Co-authored-by: Rebecca Dimock <66339736+beckykd@users.noreply.github.com> * Add missed copy edit Co-authored-by: Rebecca Dimock <66339736+beckykd@users.noreply.github.com> * Reflow line breaks * Apply Elena's suggestions Co-authored-by: Elena Peña Tapia <57907331+ElePT@users.noreply.github.com> * Apply Elena's suggestions that needed line wraps * Correct layout description * Mark stochastic plugin as deprecated --------- Co-authored-by: Elena Peña Tapia <57907331+ElePT@users.noreply.github.com> Co-authored-by: abbycross Co-authored-by: Rebecca Dimock <66339736+beckykd@users.noreply.github.com> --- qiskit/transpiler/__init__.py | 1322 ++++++++++------- qiskit/transpiler/layout.py | 12 +- .../passes/scheduling/scheduling/alap.py | 2 +- .../passes/scheduling/scheduling/asap.py | 2 +- .../preset_passmanagers/__init__.py | 32 +- .../transpiler/preset_passmanagers/plugin.py | 73 +- ...add-layout-attribute-c84e56c08ca93ada.yaml | 2 +- .../1.1/add-elide-swaps-b0a4c373c9af1efd.yaml | 2 +- 8 files changed, 839 insertions(+), 608 deletions(-) diff --git a/qiskit/transpiler/__init__.py b/qiskit/transpiler/__init__.py index 4b1446915081..696df5151cc3 100644 --- a/qiskit/transpiler/__init__.py +++ b/qiskit/transpiler/__init__.py @@ -36,76 +36,156 @@ :alt: The transpilation process takes the input circuit, applies the transpilation \ passes, then produces the output circuit. -.. raw:: html - -
- -Qiskit has four pre-built transpilation pipelines available here: -:mod:`qiskit.transpiler.preset_passmanagers`. Unless the reader is familiar with -quantum circuit optimization methods and their usage, it is best to use one of -these ready-made routines. By default the preset pass managers are composed -of six stages: - -#. ``init`` - This stage runs any initial passes that are required before we start embedding the - circuit to the backend. This typically involves unrolling custom instructions and converting - the circuit to all 1 and 2 qubit gates. -#. ``layout`` - This stage applies a layout, mapping the virtual qubits in the circuit to the - physical qubits on a backend. See :ref:`layout_stage` for more details. -#. ``routing`` - This stage runs after a layout has been applied and will inject - gates (i.e. swaps) into the original circuit to make it compatible - with the backend's connectivity. See :ref:`routing_stage` for more details. -#. ``translation`` - This stage translates the gates in the circuit to the target backend's basis set. - See :ref:`translation_stage` for more details. -#. ``optimization`` - This stage runs the main optimization loop repeatedly - until a condition (such as fixed depth) is reached. See :ref:`optimization_stage` for more details. -#. ``scheduling`` - This stage is for any hardware-aware scheduling passes. See - :ref:`scheduling_stage` for more details. - -When using :func:`~.transpile`, the implementation of each stage can be modified with the ``*_method`` -arguments (e.g. ``layout_method``). These can be set to one of the built-in methods and -can also refer to available external plugins. See -:mod:`qiskit.transpiler.preset_passmanagers.plugin` for details on this plugin interface. - -.. _working_with_preset_pass_managers: - -Working with Preset Pass Managers -================================= - -Qiskit includes functions to build preset :class:`~.PassManager` objects. -These preset passmanagers are used by the :func:`~.transpile` function -for each optimization level. There are 4 optimization levels ranging from 0 to 3, where higher -optimization levels take more time and computational effort but may yield a -more optimal circuit. -Optimization level 0 is intended for device characterization experiments and, as such, only -maps the input circuit to the constraints of the target backend, without -performing any optimizations. Optimization level 3 spends the most effort to optimize the circuit. -However, as many of the optimization techniques in the transpiler are heuristic based, spending more -computational effort does not always result in an improvement in the quality of the output -circuit. +Qiskit uses the graph-based :class:`.DAGCircuit` intermediate representation (IR) of a circuit +throughout the transpiler stack, rather than the tree-based :class:`.QuantumCircuit`. A transpiler +pipeline is a :class:`.PassManager` object, whose :meth:`.PassManager.run` method takes in a +:class:`.QuantumCircuit` and converts it to a :class:`.DAGCircuit`, then subjects the IR to a +sequence of *passes*, finally returning a :class:`.QuantumCircuit` back. A pass is either an +:class:`.AnalysisPass`, which calculates and stores properties about the circuit in the +stateful :class:`.PropertySet`, or a :class:`.TransformationPass`, which modifies the IR +to achieve a particular singular goal. You can think of a pipeline as being split into +"stages", where each stage is responsible for one high-level transformation. + +Qiskit exposes a default transpilation pipeline builder using the function +:func:`.generate_preset_pass_manager`. This returns a properly configured pipeline for complete +transpilation, at a chosen ``optimization_level`` (between 0 and 3, inclusive). Unless you are +looking for something highly specialized, this is almost certainly the entry point you want. A +sample transpilation looks like:: + + from qiskit.circuit import QuantumCircuit + from qiskit.transpiler import generate_preset_pass_manager + from qiskit_ibm_runtime import QiskitRuntimeService + + # Any abstract circuit you want: + abstract = QuantumCircuit(2) + abstract.h(0) + abstract.cx(0, 1) + + # Any method you like to retrieve the backend you want to run on: + backend = QiskitRuntimeService().backend("some-backend") + + # Create the pass manager for the transpilation ... + pm = generate_preset_pass_manager(backend=backend) + # ... and use it (as many times as you like). + physical = pm.run(abstract) + +For most use cases, this is all you need. +All of Qiskit's transpiler infrastructure is highly extensible and configurable, however. +The rest of this page details how to harness the low-level capabilities of the transpiler stack. + +.. _transpiler-preset: + +Preset pass managers +==================== -If you'd like to work directly with a -preset pass manager you can use the :func:`~.generate_preset_pass_manager` -function to easily generate one. For example: +The function :func:`.generate_preset_pass_manager` creates the "preset pass managers". +These are all instances of :class:`.PassManager`, so are used by passing a :class:`.QuantumCircuit` +to the :meth:`.PassManager.run` method. More specifically, the preset pass managers are instances +of :class:`.StagedPassManager`, which allows greater configuration of the individual stages of a +transpilation, including pre- and post-stage hooks. + +A preset pass manager has up to six named stages. These are summarized, in order of execution, +below, with more in-depth information in the following subsections. + +``init`` + Abstract-circuit optimizations, and reduction of multi-qubit operations to one- and two-qubit + operations. See :ref:`transpiler-preset-stage-init` for more details. + +``layout`` + Choose an initial mapping of virtual qubits to physical qubits, including expansion of the + circuit to contain explicit ancillas. This stage sometimes subsumes ``routing``. See + :ref:`transpiler-preset-stage-layout` for more details. + +``routing`` + Insert gates into the circuit to ensure it matches the connectivity constraints of the + :class:`.Target`. The inserted gates need not match the target ISA yet, so are often just + ``swap`` instructions. This stage is sometimes omitted, when the ``layout`` stage handles its + job. See :ref:`transpiler-preset-stage-routing` for more details. + +``translation`` + Convert all gates in the circuit to ones matching the ISA of the :class:`Target`. See + :ref:`transpiler-preset-stage-translation` for more details. + +``optimization`` + Low-level, hardware-aware optimizations. Unlike the abstract optimizations of the ``init`` + stage, this stage acts on a physical circuit. See :ref:`transpiler-preset-stage-optimization` + for more details. + +``scheduling`` + Insert :class:`~.circuit.Delay` instructions to make the wall-clock timing of a circuit + explicit. This may also include hardware-aware online error reduction techniques such as + dynamical decoupling, which are dependent on knowing wall-clock timings. See + :ref:`transpiler-preset-stage-scheduling` for more details. + +The preset transpiler pipelines can also be configured at a high level by setting an +``optimization_level``. This is an integer from 0 to 3, inclusive, indicating the relative effort to +exert in attempting to optimize the circuit for the hardware. Level 0 disables all unnecessary +optimizations; only transformations needed to make the circuit runnable are used. On +the other end, level 3 enables a full range of optimization techniques, some of which can be very +expensive in compilation time. Similar to classical compilers, optimization level 3 is not always +guaranteed to produce the best results. Qiskit defaults to optimization level 2, as a trade-off +between compilation time and the expected amount of optimization. + +The optimization level affects which implementations are used for a given stage by default, though +this can be overridden by passing explicit ``_method=""`` arguments to +:func:`.generate_preset_pass_manager`. + +.. note:: + + The preset pass managers almost always include stochastic, heuristic-based passes. If you need + to ensure reproducibility of a compilation, pass a known integer to the ``seed_transpiler`` + argument to the generator functions. + + This stochasticity arises because many of the problems the transpiler must solve are known to be + non-polynomial in complexity, but transpilation must complete in a workable amount of time. + +Choosing preset stage implementations +------------------------------------- + +Qiskit includes several implementations of the above stages, and more can be installed as +separate "plugins". To control which implementation of a stage is used, pass its name to the +``_method`` keyword argument of the two functions, such as +``translation_method="translator"``. To read more about implementing such external plugins for a +stage, see :mod:`qiskit.transpiler.preset_passmanagers.plugin`. + +For example, to generate a preset pass manager at optimization level 1 that explicitly uses the +``trivial`` method for layout with the ``sabre`` method for routing, we would do: .. plot:: - :include-source: - :nofigs: + :include-source: + :nofigs: - from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager + from qiskit.transpiler import generate_preset_pass_manager from qiskit.providers.fake_provider import GenericBackendV2 + # Whatever backend you like: backend = GenericBackendV2(num_qubits=5) - pass_manager = generate_preset_pass_manager(3, backend) - -which will generate a :class:`~.StagedPassManager` object for optimization level 3 -targeting the :class:`~.GenericBackendV2` backend (equivalent to what is used internally -by :func:`~.transpile` with ``backend=GenericBackendV2(5)`` and ``optimization_level=3``). -You can use this just like you would any other :class:`~.PassManager`. However, -because it is a :class:`~.StagedPassManager` it also makes it easy to compose and/or -replace stages of the pipeline. For example, if you wanted to run a custom scheduling -stage using dynamical decoupling (via the :class:`~.PadDynamicalDecoupling` pass) and -also add initial logical optimization prior to routing, you would do something like -(building off the previous example): + + pass_manager = generate_preset_pass_manager( + optimization_level=1, + backend=backend, + layout_method="trivial", + routing_method="sabre", + ) + +.. note:: + + The built-in set of available plugins for each stage is part of Qiskit's public API, and subject + to all the stability guarantees. This includes the high-level logical effects of that method + (for example, ``routing_method="sabre"`` will always use a Sabre-derived algorithm). The exact + internal construction of the :class:`.PassManager` representing the stage is not, however; the + order of passes might change between minor versions, or new passes might be introduced. + + For any stage that has one, the method named ``"default"`` is the most subject to change. + Qiskit typically only makes complete algorithmic changes in the default method across a + major-version boundary, but it might rebalance heuristics and add new passes to default + methods between minor versions. + +Since the output of :func:`.generate_preset_pass_manager` is a :class:`.StagedPassManager`, you can +also modify the pass manager after its creation to provide an entirely custom stage implementation. +For example, if you wanted to run a custom scheduling stage using dynamical decoupling (using the +:class:`~.PadDynamicalDecoupling` pass) and also add initial logical optimization prior to routing, +you would do something like the following (building off the previous example): .. plot:: :include-source: @@ -113,7 +193,7 @@ import numpy as np from qiskit.providers.fake_provider import GenericBackendV2 - from qiskit.circuit.library import HGate, PhaseGate, RXGate, TdgGate, TGate, XGate, CXGate + from qiskit.circuit import library as lib from qiskit.transpiler import PassManager, generate_preset_pass_manager from qiskit.transpiler.passes import ( ALAPScheduleAnalysis, @@ -122,7 +202,7 @@ ) backend = GenericBackendV2(num_qubits=5) - dd_sequence = [XGate(), XGate()] + dd_sequence = [lib.XGate(), lib.XGate()] scheduling_pm = PassManager( [ ALAPScheduleAnalysis(target=backend.target), @@ -130,22 +210,15 @@ ] ) inverse_gate_list = [ - HGate(), - (RXGate(np.pi / 4), RXGate(-np.pi / 4)), - (PhaseGate(np.pi / 4), PhaseGate(-np.pi / 4)), - (TGate(), TdgGate()), + lib.CXGate(), + lib.HGate(), + (lib.RXGate(np.pi / 4), lib.RXGate(-np.pi / 4)), + (lib.PhaseGate(np.pi / 4), lib.PhaseGate(-np.pi / 4)), + (lib.TGate(), lib.TdgGate()), ] - logical_opt = PassManager( - [ - InverseCancellation([CXGate()]), - InverseCancellation(inverse_gate_list), - ] - ) - - pass_manager = generate_preset_pass_manager( - optimization_level=0 - ) + logical_opt = PassManager([InverseCancellation(inverse_gate_list)]) + pass_manager = generate_preset_pass_manager(optimization_level=0) # Add pre-layout stage to run extra logical optimization pass_manager.pre_layout = logical_opt # Set scheduling stage to custom pass manager @@ -155,7 +228,593 @@ the ``logical_opt`` pass manager will be called before the ``layout`` stage, and the ``scheduling_pm`` pass manager will be used for the ``scheduling`` stage instead of the default. -Custom Pass Managers +If you are constructing custom stages for the preset pass managers, you may find some of the +low-level helper functions in :mod:`qiskit.transpiler.preset_passmanagers` useful. + +.. _transpiler-preset-stage-init: + +Initialization stage +-------------------- + +.. seealso:: + `Init stage explanation `__ + Higher-level user-facing explanation of the init stage in the IBM Quantum guide. + +The ``init`` stage is responsible for high-level, logical optimizations on abstract circuits, and +for lowering multi-qubit (3+) operations down to a series of one- and two-qubit operations. As this is +the first stage run, its input is a fully abstract circuit. The ``init`` stage must be able to +handle custom user-defined gates, and all the high-level abstract circuit-description objects, such +as :class:`.AnnotatedOperation`. + +The output of the ``init`` stage is an abstract circuit that contains only one- and two-qubit +operations. + +When writing :ref:`stage plugins `, the entry point for ``init`` is +``qiskit.transpiler.init``. The built-in plugins are: + +.. list-table:: + :header-rows: 1 + + * - Method + - Summary + + * - :ref:`default ` + - Built-in unrolling of multi-qubit operations and abstract optimizations. + + +.. _transpiler-preset-stage-init-default: + +Built-in ``default`` plugin +........................... + +At optimization level 0, no abstract optimization is done. The default plugin simply "unrolls" +operations with more than three qubits by accessing their hierarchical +:class:`~.circuit.Instruction.definition` fields. + +At optimization levels 1 and above, the default plugin also does simple cancellation of adjacent +inverse gates, such as two back-to-back ``cx`` gates. + +At optimization levels 2 and 3, the default plugin enables a much wider range of abstract +optimizations. This includes: + +* "Virtual permutation elision" (see :class:`.ElidePermutations`), where explicit + permutation-inducing operations are removed and instead effected as remapping of virtual qubits. +* Analysis of the commutation structure of the IR to find pairs of gates that can be canceled out. +* Numerical splitting of two-qubit operations that can be expressed as a series of separable + one-qubit operations. +* Removal of imperceivable operations, such as tiny-angle Pauli rotations and diagonal operations + immediately preceding measurements. + +.. _transpiler-preset-stage-layout: + +Layout stage +------------ + +.. seealso:: + `Layout stage explanation `__ + Higher-level user-facing explanation of the layout stage in the IBM Quantum guide. + +The layout stage is responsible for making an initial mapping between the virtual qubits of the +input circuit, and the hardware qubits of the target. This includes expanding the input circuit +with explicit ancillas so it has as many qubits as the target has, and rewriting all operations in +terms of hardware qubits. You may also see this problem called the "placement" problem in other +toolkits or literature. + +The layout stage must set the properties ``layout`` and ``original_qubit_indices`` in the pipeline's +:class:`.PropertySet`. + +.. note:: + + All built-in plugins for the layout stage will give priority to an explicit layout selected + using the ``initial_layout`` argument to :func:`.generate_preset_pass_manager` or + :func:`.transpile`. + +At any given point in a circuit, we can identify a mapping between currently active "virtual" qubits +of the input circuit to hardware qubits of the backend. A hardware qubit can only ever represent a +single virtual qubit at a given point, but the mapping might vary over the course of the circuit. +In principle, some virtual qubits might not be mapped at all points in the circuit +execution, if the lifetime of a virtual qubit state can be shortened, though Qiskit's built-in +pipelines do not use this currently. + +.. image:: /source_images/mapping.png + :alt: Illustration of how virtual qubits from an input circuit could be mapped to hardware + qubits on a backend device's connectivity map. + +The layout stage is not responsible for ensuring that the connectivity of the target +is respected all the way through the circuit, nor that all operations are valid for direct execution +on the target; these are the responsibilities of the :ref:`routing +` and :ref:`translation ` +stages, respectively. + +The choice of initial layout is one of the most important factors that affects the quality of the +output circuit. The layout stage is often the most computationally expensive stage in the default +pipelines; the default plugin for layout even tries several different algorithms (described in more +detail in :ref:`transpiler-preset-stage-layout-default`). + +The ideal situation for the layout stage is to find a "perfect" layout, where all operations +respect the connectivity constraints of the :class:`.Target` such that the routing stage +is not required. This is typically not possible for arbitrary input circuits, but when it is, the +:class:`.VF2Layout` pass can be used to find a valid initial layout. If multiple perfect layouts +are found, a scoring heuristic based on estimated error rates is used to decide which one to use. + +In all built-in plugins, passing the :func:`.generate_preset_pass_manager` argument +``initial_layout`` causes the given layout to be used verbatim, skipping the individual "choosing" +logic. All built-in plugins also handle embedding the circuit into the full width of the device, +including assigning ancillas. + +If you write your own layout plugin, you might find :func:`.generate_embed_passmanager` useful for +automating the "embedding" stage of the layout application. + +When writing :ref:`stage plugins `, the entry point for ``layout`` +is ``qiskit.transpiler.layout``. The built-in plugins are: + +.. list-table:: + :header-rows: 1 + + * - Method + - Summary + + * - :ref:`default ` + - At the highest optimization levels, attempts to find a perfect layout, then tries a + Sabre-based layout-and-routing combined pass. + + * - :ref:`dense ` + - Finds the densest subgraph (in terms of qubit link degrees) of the backend to use as the + initial qubits. + + * - :ref:`trivial ` + - Maps virtual qubit 0 to physical qubit 0, and so on. + + * - :ref:`sabre ` + - Uses `Qiskit's enhanced Sabre layout algorithm `_. + +At all optimization levels, the default layout method is ``default``, though the structure of this +stage changes dramatically based on the level. + +.. _transpiler-preset-stage-layout-default: + +Built-in ``default`` plugin +........................... + +An amalgamation of several different layout techniques. + +At optimization level 0, the trivial layout is chosen. + +At optimization levels above 0, there is a two-step process: + +#. First, use :class:`.VF2Layout` to attempt to find a "perfect" layout. The maximum number of + calls to the isomorphism evaluator increases with the optimization level. For huge, complex + targets, we are not guaranteed to find perfect layouts even if they exist, but the chance + increases with the optimization level. + +#. If no perfect layout can be found, use :class:`.SabreLayout` to choose an initial layout, with + the numbers of initial layout trials, swap-map trials, and forwards–backwards iterations + increasing with the optimization level. + +In addition, optimization level 1 also tries the trivial layout before the VF2-based version, +for historical backwards compatibility. + + +.. _transpiler-preset-stage-layout-dense: + +Built-in ``dense`` plugin +......................... + +Uses the :class:`.DenseLayout` pass to choose the layout. This pass finds the densest connected +subgraph of the complete target connectivity graph, where "densest" means that hardware qubits with +the greatest number of available connections are preferred. The virtual-to-hardware mapping is +completed by assigning the highest-degree virtual qubits to the highest-degree hardware qubits. + +This is a relatively cheap heuristic for choosing an initial layout, but typically has far worse +output quality than Sabre-based methods. The :ref:`default layout plugin +` uses the initial mapping selected by :class:`.DenseLayout` +as one of its initial layouts to seed the Sabre algorithm. + +.. _transpiler-preset-stage-layout-trivial: + +Built-in ``trivial`` plugin +........................... + +Uses the :class:`.TrivialLayout` pass to choose the layout. This is the simplest assignment, where +each virtual qubit is assigned to the hardware qubit with the same index, so virtual qubit 0 is +mapped to hardware qubit 0, and so on. + +This method is most useful for hardware-characterization experiments, where the incoming "abstract" +circuit is already full-width on the device, its operations correspond to physical operations, and +the transpiler is just being invoked to formalize the creation of a physical +:class:`.QuantumCircuit`. + + +.. _transpiler-preset-stage-layout-sabre: + +Built-in ``sabre`` plugin +......................... + +Uses the :class:`.SabreLayout` to choose an initial layout, using Qiskit's modified :ref:`Sabre +routing algorithm ` as the subroutine to swap-map the +candidate circuit both forwards and backwards. + +Summarily, the layout component of `the original Sabre algorithm `_ +chooses an initial layout arbitrarily, then tries to "improve" it by running routing on the circuit, +reversing the circuit, and running routing on the reversed circuit with the previous "final" +virtual-to-hardware assignment as the initial state. The configured optimization level decides how +many iterations of this to-and-fro to do, and how many different random initial layouts to try. + +The principal difference to the :ref:`default stage ` at +optimization levels other than 0 is that this plugin *only* runs the Sabre-based algorithm. It +does not attempt to find a perfect layout, nor attempt the trivial layout. + + + +.. _transpiler-preset-stage-routing: + +Routing stage +------------- + +.. seealso:: + `Routing stage explanation `__ + Higher-level user-facing explanation of the routing stage in the IBM Quantum guide. + +The routing stage ensures that the virtual connectivity graph of the circuit is compatible with the +hardware connectivity graph of the target. In simpler terms, the routing stage makes sure that all +two-qubit gates in the circuit are mapped to hardware qubits that have a defined two-qubit operation +in the target ISA. You may also see this problem referred to as the "mapping" or "swap-mapping" +problem in other toolkits or literature. + +Routing algorithms typically do this by inserting ``swap`` gates into the circuit, and modifying the +virtual-to-hardware mapping of qubits over the course of the circuit execution. + +The routing stage does not need to ensure that all the gates in the circuit are valid for the target +ISA. For example, a routing plugin can leave literal ``swap`` gates in the circuit, even if the +:class:`.Target` does not contain :class:`.SwapGate`. However, there must be at least one two-qubit +gate defined in the :class:`.Target` for any pair of hardware qubits that has a gate applied in the +circuit. + +The routing stage must set the ``final_layout`` and ``virtual_permutation_layout`` properties in +the :class:`.PropertySet` if routing has taken place. + +All of Qiskit's built-in routing stages will additionally run the :class:`.VF2PostLayout` pass after +routing. This might reassign the initial layout, if lower-error qubits can be found. This +pass is very similar to the :class:`.VF2Layout` class that :ref:`the default layout plugin +` uses, except in :class:`.VF2PostLayout` we can guarantee +that there is at least one isomorphic induced subgraph of the target topology that matches the +circuit topology. + +.. note:: + + Qiskit's built-in routing plugins all generally assume that all pairs of qubits with a + defined two-qubit link have a *universal* set of gates defined for those two qubits. Hardware + does not necessarily need to respect this (for example, if the only defined two-qubit gate is + ``swap``, then entangling operations like ``cx`` cannot be realized), but Qiskit does not yet + consider this possibility. + +.. note:: + + Finding the minimal number of swaps to insert is known to be a non-polynomial problem. This + means it is prohibitively expensive to attempt, so many of Qiskit's built-in algorithms are + stochastic, and you may see large variations between different compilations. If you need + reproducibility, be sure to set the ``seed_transpiler`` argument of + :func:`.generate_preset_pass_manager` or :func:`.transpile`. + +When writing :ref:`stage plugins `, the entry point for ``routing`` +is ``qiskit.transpiler.routing``. The built-in plugins are: + +.. list-table:: + :header-rows: 1 + + * - Method + - Summary + + * - :ref:`sabre ` + - Default. Uses `Qiskit's modified Sabre routing algorithm `_ to + swap map. + + * - :ref:`none ` + - Disable routing. Raises an error if routing is required. + + * - :ref:`basic ` + - Greedy swap insertion to route a single operation at a time. + + * - :ref:`stochastic ` + - Consider operations layer-by-layer, using a stochastic algorithm to find swap networks that + implement a suitable permutation to make the layer executable. + + * - :ref:`lookahead ` + - Breadth-first search with heuristic pruning to find swaps that make gates executable. + +.. _transpiler-preset-stage-routing-none: + +Built-in ``none`` plugin +........................ + +A dummy plugin used to disable routing entirely. This can occasionally be useful for +hardware-configuration experiments, or in certain special cases of partial compilation. + +.. _transpiler-preset-stage-routing-basic: + +Built-in ``basic`` plugin +......................... + +Uses the :class:`.BasisSwap` greedy swap-insertion algorithm. This is conceptually very simple; for +each operation in topological order, insert the shortest-path swaps needed to make the connection +executable on the device. + +The optimization level only affects the amount of work the :class:`.VF2PostLayout` step does to +attempt to improve the initial layout after routing. + +This method typically has poor output quality. + +.. _transpiler-preset-stage-routing-stochastic: + +Built-in ``stochastic`` plugin +.............................. + +.. deprecated:: 1.3 + Use :ref:`transpiler-preset-stage-routing-sabre` instead. + +Uses the :class:`.StochasticSwap` algorithm to route. In short, this stratifies the circuit into +layers, then uses a stochastic algorithm to find a permutation that will allow the layer to execute, +and a series of swaps that will implement that permutation in a hardware-valid way. + +The optimization level affects the number of stochastic trials used for each layer, and the amount +of work spent in :class:`.VF2PostLayout` to optimize the initial layout. + +This was Qiskit's primary routing algorithm for several years, until approximately 2021. Now, it +is reliably beaten in runtime and output quality by :ref:`Qiskit's custom Sabre-based routing +algorithm `. + +.. _transpiler-preset-stage-routing-lookahead: + +Built-in ``lookahead`` plugin +............................. + +Uses the :class:`.LookaheadSwap` algorithm to route. This is essentially a breadth-first search +at producing a swap network, where the tree being explored is pruned down to a small number of +candidate swaps at each depth. + +This algorithm is similar to the ``basic`` heuristic of :ref:`the "sabre" plugin +`, except it considers the following effects of each swap to +a small depth as well. + +The optimization level affects the search depth, the amount of per-depth pruning, and amount of work +done by :class:`.VF2PostLayout` to post-optimize the initial layout. + +In practice, :ref:`the "sabre" plugin ` runs several orders +of magnitude faster, and produces better output. + +.. _transpiler-preset-stage-routing-sabre: + +Built-in ``sabre`` plugin +......................... + +Uses the :class:`.SabreSwap` algorithm to route. This uses `Qiskit's enhanced version +`_ of `the original Sabre routing algorithm `_. + +This routing algorithm runs with threaded parallelism to consider several different possibilities +for routing, choosing the one that minimizes the number of inserted swaps. + +The optimization level affects how many different stochastic seeds are attempted for the full +routing, and the amount of work done by :class:`.VF2PostLayout` to post-optimize the initial layout. + +This is almost invariably the best-performing built-in plugin, and the one Qiskit uses by default in +all cases where routing is necessary. + +.. _transpiler-preset-stage-translation: + +Translation stage +----------------- + +.. seealso:: + `Translation stage explanation`__ + Higher-level user-facing explanation of the translation stage in the IBM Quantum guide. + +.. __: https://docs.quantum.ibm.com/guides/transpiler-stages#translation-stage + +The translation stage is responsible for rewriting all gates in the circuit into ones that are +supported by the target ISA. For example, if a ``cx`` is requested on hardware qubits 0 and 1, but +the ISA only contains a ``cz`` operation on those qubits, the translation stage must find a way of +representing the ``cx`` gate using the ``cz`` and available one-qubit gates. + +.. note:: + + In Qiskit 1.x, translation plugins need not output gates with the correct + directionality, provided the gate exists with opposite directionality on the given qubit pair. + For example, if ``cx(0, 1)`` is ISA-supported, the translation stage can output + ``cx(1, 0)``. + + This is likely to change in later versions of Qiskit. + +The translation stage is called before entering the optimization stage. Optimization plugins +(including Qiskit's built-in plugins) may also use the translation stage as a "fixup" stage after +the optimization loop, if the optimization loop returns a circuit that includes non-ISA gates. This +latter situation is fairly common; the optimization loop may only be concerned with minimizing +properties like "number of two-qubit gates", and will leave its output in terms of locally +equivalent gates, which the translation stage can easily rewrite without affecting the target +optimization properties. This allows easier separation of concerns between the two stages. Some +optimization plugins may be stricter in their output, and so this follow-up to the translation stage +may no longer be necessary. + +When writing :ref:`stage plugins `, the entry point for +``translation`` is ``qiskit.transpiler.translation``. The built-in plugins are: + +.. list-table:: + :header-rows: 1 + + * - Method + - Summary + + * - :ref:`translator ` + - Symbolic translation of gates to the target basis using known equivalences. + + * - :ref:`synthesis ` + - Collect each run of one- and two-qubit gates into a matrix representation, and resynthesize + from there. + +.. _transpiler-preset-stage-translation-synthesis: + +Built-in ``synthesis`` plugin +............................. + +Collect runs of gates on the same qubits into matrix form, and then resynthesize using the +:class:`.UnitarySynthesis` pass (with the configured ``unitary_synthesis_method``). This is, in +large part, similar to the optimization loop itself at high optimization levels. + +The collection into matrices is typically more expensive than matrix-free translations, but in +principle the quality of the translations can be better. In practice, this requires a synthesis +algorithm tailored to the target ISA, which makes this method less general than other methods. It +can produce higher-quality results when targeting simple ISAs that match the synthesis routines +already in Qiskit. + +If this method is used, you might not need the optimization loop. + +The optimization level has no effect on this plugin. + + +.. _transpiler-preset-stage-translation-translator: + +Built-in ``translator`` plugin +.............................. + +Uses the :class:`.BasisTranslator` algorithm to symbolically translate gates into the target basis. +At a high level, this starts from the set of gates requested by the circuit, and uses rules from a +given :class:`.EquivalenceLibrary` (typically the :data:`.SessionEquivalenceLibrary`) to move +towards the ISA. + +This is the default translation method. + +The optimization level has no effect on this plugin. + + +.. _transpiler-preset-stage-optimization: + +Optimization stage +------------------ + +.. seealso:: + `Optimization stage explanation`__ + Higher-level user-facing explanation of the optimization stage in the IBM Quantum guide. + +.. __: https://docs.quantum.ibm.com/guides/transpiler-stages#optimization-stage + +The optimization stage is for low-level hardware-aware optimizations. Unlike :ref:`the init stage +`, the input to this stage is a circuit that is already +ISA-compatible, so a low-level optimization plugin can be tailored for a particular ISA. + +There are very few requirements on an optimization plugin, other than it takes in ISA-supported +circuits, and returns ISA-supported circuits. An optimization plugin will often contain a loop, +such as the :class:`.DoWhileController`, and might include the configured translation stage +as a fix-up pipeline. + +Qiskit's built-in optimization plugins are general, and apply well to most real-world ISAs for +non-error-corrected devices. The built-in plugins are less well-suited to ISAs that have no +continuously parametrized single-qubit gate, such as a Clifford+T basis set. + +When writing :ref:`stage plugins `, the entry point for +``optimization`` is ``qiskit.transpiler.optimization``. The built-in plugins are: + +.. list-table:: + :header-rows: 1 + + * - Method + - Summary + + * - :ref:`default ` + - A default set of optimization passes. This varies significantly between optimization + levels. + +.. _transpiler-preset-stage-optimization-default: + +Built-in ``default`` plugin +........................... + +This varies significantly between optimization levels. + +The specifics of this pipeline are subject to change between Qiskit versions. The broad principles +are described below. + +At optimization level 0, the stage is empty. + +At optimization level 1, the stage does matrix-based resynthesis of runs of single-qubit gates, and +very simple symbolic inverse cancellation of two-qubit gates, if they appear consecutively. This +runs in a loop until the size and depth of the circuit are fixed. + +At optimization level 2, in addition the optimizations of level 1, the loop contains commutation +analysis of sets of gates to widen the range of gates that can be considered for cancellation. +Before the loop, runs of both one- and two-qubit gates undergo a single matrix-based resynthesis. + +At optimization level 3, the two-qubit matrix-based resynthesis runs inside the optimization loop. +The optimization loop condition also tries multiple runs and chooses the minimum point in the case +of fluctuating output; this is necessary because matrix-based resynthesis is relatively unstable in +terms of concrete gates. + +Optimization level 3 is typically very expensive for large circuits. + + +.. _transpiler-preset-stage-scheduling: + +Scheduling stage +---------------- + +.. seealso:: + :ref:`transpiler-scheduling-description` + A guide-level explanation of scheduling concepts. + +The scheduling stage, if requested, is responsible for inserting explicit :class:`~.circuit.Delay` +instructions to make idle periods of qubits explicit. Plugins may optionally choose to do +walltime-sensitive transformations, such as inserting dynamical decoupling sequences. + +The input to the scheduling stage is an ISA-compatible circuit. The output of the scheduling stage +must also be an ISA-compatible circuit, with explicit :class:`~.circuit.Delay` instructions that +satisfy the hardware's timing information, if appropriate. + +The scheduling stage should set the ``node_start_time`` property in the pipeline's +:class:`.PropertySet`. + +When writing :ref:`stage plugins `, the entry point for +``scheduling`` is ``qiskit.transpiler.scheduling``. The built-in plugins are: + +.. list-table:: + :header-rows: 1 + + * - Method + - Summary + + * - :ref:`default ` + - Attempt to satisfy timing alignment constraints without otherwise scheduling. + + * - :ref:`alap ` + - Schedule the circuit, preferring operations to be as late as possible. + + * - :ref:`asap ` + - Schedule the circuit, preferring operations to be as soon as possible. + +.. _transpiler-preset-stage-scheduling-default: + +Built-in ``default`` plugin +........................... + +Do nothing, unless the circuit already contains instructions with explicit timings. If there are +explicitly timed operations in the circuit, insert additional padding to ensure that these timings +satisfy the alignment and other hardware constraints. + +.. _transpiler-preset-stage-scheduling-alap: + +Builtin ``alap`` plugin +....................... + +Explicitly schedule all operations using an "as late as possible" strategy. This uses the +:class:`.ALAPScheduleAnalysis` algorithm to decide where to place gates. + +.. _transpiler-preset-stage-scheduling-asap: + +Builtin ``asap`` plugin +....................... + +Explicitly schedule all operations using an "as soon as possible" strategy. This uses the +:class:`.ASAPScheduleAnalysis` algorithm to decide where to place gates. + + +Custom pass managers ==================== In addition to modifying preset pass managers, it is also possible to construct a pass @@ -163,7 +822,11 @@ circuits. You can use the :class:`~.StagedPassManager` class directly to do this. You can define arbitrary stage names and populate them with a :class:`~.PassManager` instance. For example, the following code creates a new :class:`~.StagedPassManager` -that has 2 stages, ``init`` and ``translation``.:: +that has two stages, ``init`` and ``translation``. + +.. plot:: + :include-source: + :nofigs: from qiskit.transpiler.passes import ( UnitarySynthesis, @@ -188,12 +851,13 @@ stages=["init", "translation"], init=init, translation=translate ) -There is no limit on the number of stages you can put in a :class:`~.StagedPassManager`. +There is no limit on the number of stages you can put in a :class:`~.StagedPassManager`. The stages +do not need to correspond to the stages used by Qiskit's preset pipelines. -The :ref:`stage_generators` may be useful for the construction of custom :class:`~.StagedPassManager`s. -They generate pass managers which provide common functionality used in many stages. -For example, :func:`~.generate_embed_passmanager` generates a :class:`~.PassManager` -to "embed" a selected initial :class:`~.Layout` from a layout pass to the specified target device. +The :ref:`stage_generators` may be useful for the construction of custom :class:`~.StagedPassManager` +instances. They generate pass managers which provide common functionality used in many stages. For +example, :func:`~.generate_embed_passmanager` generates a :class:`~.PassManager` to "embed" a +selected initial :class:`~.Layout` from a layout pass to the specified target device. Representing Quantum Computers ============================== @@ -339,7 +1003,7 @@ .. plot:: :include-source: :alt: Output from the previous code. - + from qiskit.circuit import Parameter, Measure from qiskit.transpiler import Target, InstructionProperties from qiskit.circuit.library import UGate, RZGate, RXGate, RYGate, CXGate, CZGate @@ -509,466 +1173,19 @@ target.build_coupling_map('cz').draw() -.. _transpiler_stage_descriptions: - -Transpiler Stage Details -======================== - -Below are a description of the default transpiler stages and the problems -they solve. The default passes used for each stage are described, but -the specifics are configurable via the ``*_method`` keyword arguments for -the :func:`~.transpile` and :func:`~.generate_preset_pass_manager` functions -which can be used to override the methods described in this section. - -.. _translation_stage: - -Translation Stage ------------------ - -When writing a quantum circuit you are free to use any quantum gate (unitary operator) that -you like, along with a collection of non-gate operations such as qubit measurements and -reset operations. However, most quantum devices only natively support a handful of quantum gates -and non-gate operations. The allowed instructions for a given backend can be found by querying the -:class:`~.Target` for the devices: - -.. plot:: - :include-source: - :nofigs: - - from qiskit.providers.fake_provider import GenericBackendV2 - backend = GenericBackendV2(5) - - print(backend.target) - -Every quantum circuit run on the target device must be expressed using only these instructions. -For example, to run a simple phase estimation circuit: - -.. plot:: - :alt: Circuit diagram output by the previous code. - :include-source: - - import numpy as np - from qiskit import QuantumCircuit - from qiskit.providers.fake_provider import GenericBackendV2 - - backend = GenericBackendV2(5) - - qc = QuantumCircuit(2, 1) - - qc.h(0) - qc.x(1) - qc.cp(np.pi/4, 0, 1) - qc.h(0) - qc.measure([0], [0]) - qc.draw(output='mpl') - -We have :math:`H`, :math:`X`, and controlled-:math:`P` gates, none of which are -in our device's basis gate set, and thus must be translated. -We can -transpile the circuit to show what it will look like in the native gate set of -the target IBM Quantum device (the :class:`~.GenericBackendV2` class generates -a fake backend with a specified number of qubits for test purposes): - -.. plot:: - :alt: Circuit diagram output by the previous code. - :include-source: - :context: reset - - from qiskit import transpile - from qiskit import QuantumCircuit - from qiskit.providers.fake_provider import GenericBackendV2 - - backend = GenericBackendV2(5) - - qc = QuantumCircuit(2, 1) - - qc.h(0) - qc.x(1) - qc.cp(np.pi/4, 0, 1) - qc.h(0) - qc.measure([0], [0]) - - qc_basis = transpile(qc, backend) - qc_basis.draw(output='mpl') - -A few things to highlight. First, the circuit has gotten longer with respect to the -original. This can be verified by checking the depth of both circuits: - -.. plot:: - :include-source: - :nofigs: - :context: - - print('Original depth:', qc.depth(), 'Decomposed Depth:', qc_basis.depth()) - -.. code-block:: text - - Original depth: 4 Decomposed Depth: 10 - -Second, although we had a single controlled gate, the fact that it was not in the basis -set means that, when expanded, it requires more than a single :class:`~.CXGate` to implement. -All said, unrolling to the basis set of gates leads to an increase in the depth of a -quantum circuit and the number of gates. - -It is important to highlight two special cases: - -1. If A swap gate is not a native gate and must be decomposed this requires three CNOT gates: - - .. plot:: - :include-source: - :nofigs: - - from qiskit.providers.fake_provider import GenericBackendV2 - backend = GenericBackendV2(5) - - print(backend.operation_names) - - .. code-block:: text - - ['id', 'rz', 'sx', 'x', 'cx', 'measure', 'delay'] - - .. plot: - :alt: Circuit diagram output by the previous code. - :include-source: - - from qiskit.circuit import QuantumCircuit - - swap_circ = QuantumCircuit(2) - swap_circ.swap(0, 1) - swap_circ.decompose().draw(output='mpl') - - As a product of three CNOT gates, swap gates are expensive operations to perform on - noisy quantum devices. However, such operations are usually necessary for embedding a - circuit into the limited gate connectivities of many devices. Thus, - minimizing the number of swap gates in a circuit is a primary goal in the - transpilation process. +.. _transpiler-scheduling-description: -2. A Toffoli, or controlled-controlled-not gate (``ccx``), is a three-qubit gate. Given - that our basis gate set includes only single- and two-qubit gates, it is obvious that - this gate must be decomposed. This decomposition is quite costly: +Scheduling of circuits +====================== - .. plot:: - :alt: Circuit diagram output by the previous code. - :include-source: +.. + This section is still here because the content hasn't fully migrated to other places yet, unlike + other discussions of the components of quantum compilation. - from qiskit.circuit import QuantumCircuit - - ccx_circ = QuantumCircuit(3) - ccx_circ.ccx(0, 1, 2) - ccx_circ.decompose().draw(output='mpl') - - For every Toffoli gate in a quantum circuit, the hardware may execute up to six CNOT - gates, and a handful of single-qubit gates. From this example, it should be - clear that any algorithm that makes use of multiple Toffoli gates will end up as a - circuit with large depth and will therefore be appreciably affected by noise and gate - errors. - - -.. _layout_stage: - -Layout Stage ------------- - -Quantum circuits are abstract entities whose qubits are "virtual" representations of actual -qubits used in computations. We need to be able to map these virtual qubits in a one-to-one -manner to the "physical" qubits in an actual quantum device. - -.. image:: /source_images/mapping.png - :alt: Diagram illustrating how virtual qubits are mapped to physical qubits. - - -By default, qiskit will do this mapping for you. The choice of mapping depends on the -properties of the circuit, the particular device you are targeting, and the optimization -level that is chosen. The choice of initial layout is extremely important for minimizing the -number of swap operations needed to map the input circuit onto the device topology and -for minimizing the loss due to non-uniform noise properties across a device. Due to the -importance of this stage, the preset pass managers -try a few different methods to find the best layout. Typically this involves 2 steps: first, -trying to find a "perfect" layout (a layout which does not require any swap operations), and then, -a heuristic pass that tries to find the best layout to use if a perfect layout cannot be found. -There are 2 passes typically used for the first stage: - -- :class:`~.VF2Layout`: Models layout selection as a subgraph isomorphism problem and tries - to find a subgraph of the connectivity graph that is isomorphic to the - graph of 2 qubit interactions in the circuit. If more than one isomorphic mapping is found a - scoring heuristic is run to select the mapping which would result in the lowest average error - when executing the circuit. - -- :class:`~.TrivialLayout`: Maps each virtual qubit to the same numbered physical qubit on the device, - i.e. ``[0,1,2,3,4]`` -> ``[0,1,2,3,4]``. This is historical behavior used only in - ``optimization_level=1`` to try to find a perfect layout. If it fails to do so, :class:`~.VF2Layout` - is tried next. - -Next, for the heuristic stage, 2 passes are used by default: - -- :class:`~.SabreLayout`: Selects a layout by starting from an initial random layout and then - repeatedly running a routing algorithm (by default :class:`~.SabreSwap`) both forward and - backward over the circuit, using the permutation caused by swap insertions to adjust that - initial random layout. For more details you can refer to the paper describing the algorithm: - `arXiv:1809.02573 `__ - :class:`~.SabreLayout` is used to select a layout if a perfect layout isn't found for - optimization levels 1, 2, and 3. -- :class:`~.TrivialLayout`: Always used for the layout at optimization level 0. - -There are other passes than can be used for the heuristic stage, but are not included in the default -pipeline, such as: - -- :class:`~.DenseLayout`: Finds the sub-graph of the device with greatest connectivity - that has the same number of qubits as the circuit. - -Let's see what layouts are automatically picked at various optimization levels. The circuits -returned by :func:`qiskit.compiler.transpile` are annotated with this initial layout information, -and we can view this layout selection graphically using -:func:`qiskit.visualization.plot_circuit_layout`: - -.. plot:: - :alt: Circuit diagram output by the previous code. - :include-source: - - from qiskit import QuantumCircuit, transpile - from qiskit.visualization import plot_circuit_layout - from qiskit.providers.fake_provider import Fake5QV1 - backend = Fake5QV1() - - ghz = QuantumCircuit(3, 3) - ghz.h(0) - ghz.cx(0,range(1,3)) - ghz.barrier() - ghz.measure(range(3), range(3)) - ghz.draw(output='mpl') - - -- **Layout Using Optimization Level 0** - - .. plot:: - :alt: Output from the previous code. - :include-source: - - from qiskit import QuantumCircuit, transpile - from qiskit.visualization import plot_circuit_layout - from qiskit.providers.fake_provider import Fake5QV1 - backend = Fake5QV1() - - ghz = QuantumCircuit(3, 3) - ghz.h(0) - ghz.cx(0,range(1,3)) - ghz.barrier() - ghz.measure(range(3), range(3)) - - new_circ_lv0 = transpile(ghz, backend=backend, optimization_level=0) - plot_circuit_layout(new_circ_lv0, backend) - -- **Layout Using Optimization Level 3** - - .. plot:: - :alt: Output from the previous code. - :include-source: - - from qiskit import QuantumCircuit, transpile - from qiskit.visualization import plot_circuit_layout - from qiskit.providers.fake_provider import Fake5QV1 - backend = Fake5QV1() - - ghz = QuantumCircuit(3, 3) - ghz.h(0) - ghz.cx(0,range(1,3)) - ghz.barrier() - ghz.measure(range(3), range(3)) - - new_circ_lv3 = transpile(ghz, backend=backend, optimization_level=3) - plot_circuit_layout(new_circ_lv3, backend) - - -It is possible to override automatic layout selection by specifying an initial layout. To do so we can -pass a list of integers to :func:`qiskit.compiler.transpile` via the `initial_layout` -keyword argument, where the index labels the virtual qubit in the circuit and the -corresponding value is the label for the physical qubit to map onto: - -.. plot:: - :alt: Output from the previous code. - :include-source: - - from qiskit import QuantumCircuit, transpile - from qiskit.visualization import plot_circuit_layout - from qiskit.providers.fake_provider import Fake5QV1 - backend = Fake5QV1() - - ghz = QuantumCircuit(3, 3) - ghz.h(0) - ghz.cx(0,range(1,3)) - ghz.barrier() - ghz.measure(range(3), range(3)) - - # Virtual -> physical - # 0 -> 3 - # 1 -> 4 - # 2 -> 2 - - my_ghz = transpile(ghz, backend, initial_layout=[3, 4, 2]) - plot_circuit_layout(my_ghz, backend) - -.. _routing_stage: - -Routing Stage -------------- - -In order to implement a 2-qubit gate between qubits in a quantum circuit that are not directly -connected on a quantum device, one or more swap gates must be inserted into the circuit to -move the qubit states around until they are adjacent on the device gate map. Each swap -gate typically represents an expensive and noisy operation to perform. Thus, finding the -minimum number of swap gates needed to map a circuit onto a given device, is an important -step (if not the most important) in the whole execution process. - -However, as with many important things in life, finding the optimal swap mapping is hard. -In fact it is in a class of problems called NP-hard, and is thus prohibitively expensive -to compute for all but the smallest quantum devices and input circuits. To get around this, -by default Qiskit uses a stochastic heuristic algorithm called :class:`~.SabreSwap` to compute -a good, but not necessarily optimal swap mapping. The use of a stochastic method means the -circuits generated by :func:`~.transpile` -are not guaranteed to be the same over repeated runs. Indeed, running the same -circuit repeatedly will in general result in a distribution of circuit depths and gate counts -at the output. - -In order to highlight this, we run a GHZ circuit 100 times, using a "bad" (disconnected) -``initial_layout`` in a heavy hex coupling map: - -.. plot:: - :alt: Diagram illustrating the previously described circuit. - - from qiskit import QuantumCircuit, transpile - - ghz = QuantumCircuit(15) - ghz.h(0) - ghz.cx(0, range(1, 15)) - ghz.draw(output='mpl') - -.. plot:: - :alt: Output from the previous code. - :include-source: - - import matplotlib.pyplot as plt - from qiskit import QuantumCircuit, transpile - from qiskit.providers.fake_provider import GenericBackendV2 - from qiskit.transpiler import CouplingMap - - coupling_map = CouplingMap.from_heavy_hex(3) - backend = GenericBackendV2(coupling_map.size(), coupling_map=coupling_map) - - ghz = QuantumCircuit(15) - ghz.h(0) - ghz.cx(0, range(1, 15)) - - depths = [] - for i in range(100): - depths.append( - transpile( - ghz, - backend, - seed_transpiler=i, - layout_method='trivial' # Fixed layout mapped in circuit order - ).depth() - ) - - plt.figure(figsize=(8, 6)) - plt.hist(depths, align='left', color='#AC557C') - plt.xlabel('Depth', fontsize=14) - plt.ylabel('Counts', fontsize=14); - - -This distribution is quite wide, signaling the difficulty the swap mapper is having -in computing the best mapping. Most circuits will have a distribution of depths, -perhaps not as wide as this one, due to the stochastic nature of the default swap -mapper. Of course, we want the best circuit we can get, especially in cases where -the depth is critical to success or failure. The :class:`~.SabreSwap` pass will by default by run its -algorithm in parallel with multiple seed values and select the output which -uses the fewest swaps. If you would like to increase the number of trials -:class:`~.SabreSwap` runs you can refer to :ref:`working_with_preset_pass_managers` -and modify the ``routing`` stage with a custom instance of :class:`~.SabreSwap` -with a larger value for the ``trials`` argument. - -Typically, following the swap mapper, the routing stage in the preset pass managers -also includes running the :class:`~.VF2PostLayout` pass. As its name implies, -:class:`~.VF2PostLayout` uses the same basic algorithm as :class:`~.VF2Layout`, -but instead of using it to find a perfect initial layout, it is designed to run after -mapping and try to find a layout on qubits with lower error rates which will -result in better output fidelity when running the circuit. The details of this -algorithm are described in `arXiv:2209.15512 `__. - -.. _optimization_stage: - -Optimization Stage ------------------- - -Decomposing quantum circuits into the basis gate set of the target device, -and the addition of swap gates needed to match hardware topology, conspire to -increase the depth and gate count of quantum circuits. Fortunately many routines -for optimizing circuits by combining or eliminating gates exist. In some cases -these methods are so effective the output circuits have lower depth than the inputs. -In other cases, not much can be done, and the computation may be difficult to -perform on noisy devices. Different gate optimizations are turned on with -different ``optimization_level`` values. Below we show the benefits gained from -setting the optimization level higher: - -.. important:: - - The output from :func:`.transpile` varies due to the stochastic swap mapper. - So the numbers below will likely change each time you run the code. - - -.. plot:: - :alt: Diagram illustrating the previously described circuit. - - import matplotlib.pyplot as plt - from qiskit import QuantumCircuit, transpile - from qiskit.providers.fake_provider import GenericBackendV2 - backend = GenericBackendV2(16) - - ghz = QuantumCircuit(15) - ghz.h(0) - ghz.cx(0, range(1, 15)) - ghz.draw(output='mpl') - -.. plot:: - :alt: Output from the previous code. - :include-source: - - import matplotlib.pyplot as plt - from qiskit import QuantumCircuit, transpile - from qiskit.providers.fake_provider import GenericBackendV2 - backend = GenericBackendV2(16) - - ghz = QuantumCircuit(15) - ghz.h(0) - ghz.cx(0, range(1, 15)) - - depths = [] - gate_counts = [] - non_local_gate_counts = [] - levels = [str(x) for x in range(4)] - for level in range(4): - circ = transpile(ghz, backend, optimization_level=level) - depths.append(circ.depth()) - gate_counts.append(sum(circ.count_ops().values())) - non_local_gate_counts.append(circ.num_nonlocal_gates()) - fig, (ax1, ax2) = plt.subplots(2, 1) - ax1.bar(levels, depths, label='Depth') - ax1.set_xlabel("Optimization Level") - ax1.set_ylabel("Depth") - ax1.set_title("Output Circuit Depth") - ax2.bar(levels, gate_counts, label='Number of Circuit Operations') - ax2.bar(levels, non_local_gate_counts, label='Number of non-local gates') - ax2.set_xlabel("Optimization Level") - ax2.set_ylabel("Number of gates") - ax2.legend() - ax2.set_title("Number of output circuit gates") - fig.tight_layout() - plt.show() - -.. _scheduling_stage: - -Scheduling Stage ----------------- +.. seealso:: + :ref:`transpiler-preset-stage-scheduling` + How to configure the scheduling stages of the preset pass managers. After the circuit has been translated to the target basis, mapped to the device, and optimized, a scheduling phase can be applied to optionally account for all the idle time in the circuit. @@ -1040,8 +1257,8 @@ such as :class:`~.PadDelay` or :class:`~.PadDynamicalDecoupling` is run to insert the instructions into the circuit, which completes the scheduling. -Scheduling Analysis with control flow instructions -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +Scheduling analysis with control-flow instructions +-------------------------------------------------- When running scheduling analysis passes on a circuit, you must keep in mind that there are additional constraints on classical conditions and control flow instructions. This section @@ -1049,7 +1266,7 @@ constraints that any scheduling pass will need to account for. Topological node ordering in scheduling -'''''''''''''''''''''''''''''''''''''''' +....................................... The DAG representation of ``QuantumCircuit`` respects the node ordering in the classical register wires, though theoretically two conditional instructions @@ -1085,7 +1302,7 @@ not to break the topological ordering of the original circuit. Realistic control flow scheduling (respecting microarchitecture) -'''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''' +................................................................ In the dispersive QND readout scheme, the qubit (Q) is measured by sending a microwave stimulus, followed by a resonator ring-down (depopulation). This @@ -1239,8 +1456,8 @@ Transpiler API ============== -Transpiler Target ------------------ +Hardware description +-------------------- .. autosummary:: :toctree: ../stubs/ @@ -1248,8 +1465,8 @@ Target InstructionProperties -Pass Manager Construction -------------------------- +Pass Manager Definition +----------------------- .. autosummary:: :toctree: ../stubs/ @@ -1257,6 +1474,7 @@ StagedPassManager PassManager PassManagerConfig + generate_preset_pass_manager Layout and Topology ------------------- @@ -1295,6 +1513,8 @@ .. autoexception:: CircuitTooWideForTarget .. autoexception:: InvalidLayoutError +.. _sabre-original-paper: https://arxiv.org/abs/1809.02573 +.. _sabre-lightsabre-paper: https://arxiv.org/abs/2409.08368 """ # For backward compatibility diff --git a/qiskit/transpiler/layout.py b/qiskit/transpiler/layout.py index 4f7e8a98bf1a..ad659ab7ba43 100644 --- a/qiskit/transpiler/layout.py +++ b/qiskit/transpiler/layout.py @@ -445,10 +445,10 @@ class TranspileLayout: The :mod:`~qiskit.transpiler` is unitary-preserving up to the "initial layout" and "final layout" permutations. The initial layout permutation is caused by - setting and applying the initial layout during the :ref:`layout_stage`. + setting and applying the initial layout during the :ref:`transpiler-preset-stage-layout`. The final layout permutation is caused by :class:`~.SwapGate` insertion during - the :ref:`routing_stage`. This class provides an interface to reason about these - permutations using a variety of helper methods. + the :ref:`transpiler-preset-stage-routing`. This class provides an interface to reason about + these permutations using a variety of helper methods. During the layout stage, the transpiler can potentially remap the order of the qubits in the circuit as it fits the circuit to the target backend. For example, @@ -524,7 +524,7 @@ class TranspileLayout: state from the transpiler. They are defined as: * :attr:`initial_layout` - This attribute is used to model the - permutation caused by the :ref:`layout_stage`. It is a + permutation caused by the :ref:`transpiler-preset-stage-layout`. It is a :class:`~.Layout` object that maps the input :class:`~.QuantumCircuit`\s :class:`~.circuit.Qubit` objects to the position in the output :class:`.QuantumCircuit.qubits` list. @@ -536,12 +536,12 @@ class TranspileLayout: is needed when computing the permutation of the :class:`Operator` of the circuit (and used by :meth:`.Operator.from_circuit`). * :attr:`final_layout` - This attribute is used to model the - permutation caused by the :ref:`routing_stage`. It is a + permutation caused by the :ref:`transpiler-preset-stage-routing`. It is a :class:`~.Layout` object that maps the output circuit's qubits from :class:`.QuantumCircuit.qubits` in the output circuit to their final positions after routing. Importantly, this only represents the permutation caused by inserting :class:`~.SwapGate`\s into - the :class:`~.QuantumCircuit` during the :ref:`routing_stage`. + the :class:`~.QuantumCircuit` during the :ref:`transpiler-preset-stage-routing`. It is **not** a mapping from the original input circuit's position to the final position at the end of the transpiled circuit. If you need this, you can use the :meth:`.final_index_layout` to generate this. diff --git a/qiskit/transpiler/passes/scheduling/scheduling/alap.py b/qiskit/transpiler/passes/scheduling/scheduling/alap.py index 2001c3d66f2f..2d60c78ae070 100644 --- a/qiskit/transpiler/passes/scheduling/scheduling/alap.py +++ b/qiskit/transpiler/passes/scheduling/scheduling/alap.py @@ -20,7 +20,7 @@ class ALAPScheduleAnalysis(BaseScheduler): """ALAP Scheduling pass, which schedules the **stop** time of instructions as late as possible. - See the :ref:`scheduling_stage` section in the :mod:`qiskit.transpiler` + See the :ref:`transpiler-scheduling-description` section in the :mod:`qiskit.transpiler` module documentation for the detailed behavior of the control flow operation, i.e. ``c_if``. """ diff --git a/qiskit/transpiler/passes/scheduling/scheduling/asap.py b/qiskit/transpiler/passes/scheduling/scheduling/asap.py index fa07ae0c5f61..fc24aa9d70db 100644 --- a/qiskit/transpiler/passes/scheduling/scheduling/asap.py +++ b/qiskit/transpiler/passes/scheduling/scheduling/asap.py @@ -20,7 +20,7 @@ class ASAPScheduleAnalysis(BaseScheduler): """ASAP Scheduling pass, which schedules the start time of instructions as early as possible. - See the :ref:`scheduling_stage` section in the :mod:`qiskit.transpiler` + See the :ref:`transpiler-scheduling-description` section in the :mod:`qiskit.transpiler` module documentation for the detailed behavior of the control flow operation, i.e. ``c_if``. """ diff --git a/qiskit/transpiler/preset_passmanagers/__init__.py b/qiskit/transpiler/preset_passmanagers/__init__.py index fbbd5a6c202a..17118e1c5d6e 100644 --- a/qiskit/transpiler/preset_passmanagers/__init__.py +++ b/qiskit/transpiler/preset_passmanagers/__init__.py @@ -32,21 +32,23 @@ .. _preset_pass_manager_generators: -Preset Pass Manager Generation ------------------------------- +Low-level preset pass manager generation +---------------------------------------- -.. autofunction:: generate_preset_pass_manager .. autofunction:: level_0_pass_manager .. autofunction:: level_1_pass_manager .. autofunction:: level_2_pass_manager .. autofunction:: level_3_pass_manager +.. + `generate_preset_pass_manager` is not documented here because it's documented to be at the root + of `qiskit.transpiler`. + .. _stage_generators: -Stage Generator Functions +Stage generator functions ------------------------- -.. currentmodule:: qiskit.transpiler.preset_passmanagers.common .. autofunction:: generate_control_flow_options_check .. autofunction:: generate_error_on_control_flow .. autofunction:: generate_unroll_3q @@ -55,8 +57,18 @@ .. autofunction:: generate_pre_op_passmanager .. autofunction:: generate_translation_passmanager .. autofunction:: generate_scheduling -.. currentmodule:: qiskit.transpiler.preset_passmanagers """ + +from .common import ( + generate_control_flow_options_check, + generate_error_on_control_flow, + generate_unroll_3q, + generate_embed_passmanager, + generate_routing_passmanager, + generate_pre_op_passmanager, + generate_translation_passmanager, + generate_scheduling, +) from .generate_preset_pass_manager import generate_preset_pass_manager from .level0 import level_0_pass_manager from .level1 import level_1_pass_manager @@ -70,4 +82,12 @@ "level_2_pass_manager", "level_3_pass_manager", "generate_preset_pass_manager", + "generate_control_flow_options_check", + "generate_error_on_control_flow", + "generate_unroll_3q", + "generate_embed_passmanager", + "generate_routing_passmanager", + "generate_pre_op_passmanager", + "generate_translation_passmanager", + "generate_scheduling", ] diff --git a/qiskit/transpiler/preset_passmanagers/plugin.py b/qiskit/transpiler/preset_passmanagers/plugin.py index 920460a2b5f5..3ac1c4545fd5 100644 --- a/qiskit/transpiler/preset_passmanagers/plugin.py +++ b/qiskit/transpiler/preset_passmanagers/plugin.py @@ -11,6 +11,8 @@ # that they have been altered from the originals. """ +.. _transpiler-preset-stage-plugins: + ======================================================================================= Transpiler Stage Plugin Interface (:mod:`qiskit.transpiler.preset_passmanagers.plugin`) ======================================================================================= @@ -35,8 +37,10 @@ Plugin Stages ============= -Currently, there are 6 stages in the preset pass managers, all of which actively -load external plugins via corresponding entry points. +There are six stages in the preset pass managers, all of which actively +load external plugins using corresponding entry points. The following table summarizes +each stage. For more details on the description and expectations of each stage, follow the link +in the stages' names to the full documentation. .. list-table:: Stages :header-rows: 1 @@ -44,57 +48,44 @@ * - Stage Name - Entry Point - Reserved Names - - Description and expectations - * - ``init`` + - Summary + + * - :ref:`init ` - ``qiskit.transpiler.init`` - ``default`` - - This stage runs first and is typically used for any initial logical optimization. Because most - layout and routing algorithms are only designed to work with 1 and 2 qubit gates, this stage - is also used to translate any gates that operate on more than 2 qubits into gates that only - operate on 1 or 2 qubits. - * - ``layout`` + - High-level, logical optimizations on abstract circuits, and reduction of multi-qubit + operations to one- and two-qubit operations. + + * - :ref:`layout ` - ``qiskit.transpiler.layout`` - ``trivial``, ``dense``, ``sabre``, ``default`` - - The output from this stage is expected to have the ``layout`` property - set field set with a :class:`~.Layout` object. Additionally, the circuit is - typically expected to be embedded so that it is expanded to include all - qubits and the :class:`~.ApplyLayout` pass is expected to be run to apply the - layout. The embedding of the :class:`~.Layout` can be generated with - :func:`~.generate_embed_passmanager`. - * - ``routing`` + - Choose an initial mapping of virtual qubits to physical qubits, including expansion of the + circuit to include explicit ancillas. This stage is sometimes combined with ``routing``. + + * - :ref:`routing ` - ``qiskit.transpiler.routing`` - ``basic``, ``stochastic``, ``lookahead``, ``sabre`` - - The output from this stage is expected to have the circuit match the - connectivity constraints of the target backend. This does not necessarily - need to match the directionality of the edges in the target as a later - stage typically will adjust directional gates to match that constraint - (but there is no penalty for doing that in the ``routing`` stage). The output - of this stage is also expected to have the ``final_layout`` property set field - set with a :class:`~.Layout` object that maps the :class:`.Qubit` to the - output final position of that qubit in the circuit. If there is an - existing ``final_layout`` entry in the property set (such as might be set - by an optimization pass that introduces a permutation) it is expected - that the final layout will be the composition of the two layouts (this - can be computed using :meth:`.DAGCircuit.compose`, for example: - ``second_final_layout.compose(first_final_layout, dag.qubits)``). - * - ``translation`` + - Insert gates into the circuit to ensure it matches the connectivity constraints of the + :class:`.Target`. The inserted gates do not need to be in the target ISA yet, so are often + just output as ``swap`` instructions. This stage is sometimes subsumed by ``layout``. + + * - :ref:`translation ` - ``qiskit.transpiler.translation`` - ``translator``, ``synthesis`` - - The output of this stage is expected to have every operation be a native - instruction on the target backend. - * - ``optimization`` + - Rewrite all gates outside the target ISA to use only gates within the ISA. + + * - :ref:`optimization ` - ``qiskit.transpiler.optimization`` - ``default`` - - This stage is expected to perform optimization and simplification. - The constraints from earlier stages still apply to the output of this - stage. After the ``optimization`` stage is run we expect the circuit - to still be executable on the target. - * - ``scheduling`` + - Low-level, physical-circuit-aware optimizations. Unlike ``init``, the ``optimization`` stage + acts at the level of a physical circuit. + + * - :ref:`scheduling ` - ``qiskit.transpiler.scheduling`` - ``alap``, ``asap``, ``default`` - - This is the last stage run and it is expected to output a scheduled - circuit such that all idle periods in the circuit are marked by explicit - :class:`~qiskit.circuit.Delay` instructions. + - Insert :class:`~.circuit.Delay` instructions to make the wall-clock timing of the circuit + fully explicit. + Writing Plugins =============== diff --git a/releasenotes/notes/0.24/add-layout-attribute-c84e56c08ca93ada.yaml b/releasenotes/notes/0.24/add-layout-attribute-c84e56c08ca93ada.yaml index b08f699c583e..eb4406277a18 100644 --- a/releasenotes/notes/0.24/add-layout-attribute-c84e56c08ca93ada.yaml +++ b/releasenotes/notes/0.24/add-layout-attribute-c84e56c08ca93ada.yaml @@ -4,7 +4,7 @@ features: Added a new attribute, :attr:`~.QuantumCircuit.layout`, to the :class:`~.QuantumCircuit` class. This attribute is typically populated by :func:`~.transpile` or :class:`.PassManager.run` (when the - :ref:`layout_stage` and :ref:`routing_stage` are run in the + :ref:`transpiler-preset-stage-layout` and :ref:`routing_stage` are run in the :class:`~PassManager`) and contains a :class:`~.TranspileLayout` which contains the information about the permutation of the input circuit during :class:`~.transpile`. diff --git a/releasenotes/notes/1.1/add-elide-swaps-b0a4c373c9af1efd.yaml b/releasenotes/notes/1.1/add-elide-swaps-b0a4c373c9af1efd.yaml index a8da2921990a..2bb2a8a2a290 100644 --- a/releasenotes/notes/1.1/add-elide-swaps-b0a4c373c9af1efd.yaml +++ b/releasenotes/notes/1.1/add-elide-swaps-b0a4c373c9af1efd.yaml @@ -2,7 +2,7 @@ features: - | Added a new optimization transpiler pass, :class:`~.ElidePermutations`, - which is designed to run prior to the :ref:`layout_stage` and will + which is designed to run prior to the :ref:`transpiler-preset-stage-layout` and will optimize away any :class:`~.SwapGate`\s and :class:`~qiskit.circuit.library.PermutationGate`\s in a circuit by permuting virtual From 936ec8e2b2c2477c13bea22074f699a6893a1557 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Wed, 5 Feb 2025 17:43:15 -0500 Subject: [PATCH 07/48] Add unitary gate representation to rust (#13759) * Add unitary gate representation to rust This commit expands the rust circuit data model to include a definition of UnitaryGate. This is a more abstract operation than what we've defined so far in Rust, a gate represented solely by it's unitary matrix but during normal transpilation we create and interact with these operations for optimization purposes so having a native representation in rust is necessary to avoid calling to Python when working with them. This introduces a new UnitaryGate struct which represents the unpacked operation. It has 3 internal storage variants based on either an ndarray arbitrary sized array, a 2x2 nalgebra fixed sized array, or a 4x4 nalgebra fixed sized array. From the python perspective these all look the same, but being able to internally work with all 3 variants lets us optimize the code paths for 1q and 2q unitary gates (which are by far more common) and avoid one layer of pointer indirection. When stored in a circuit the packed representation is just a pointer to the actual UnitaryGate which we put in a Box. This is necessary because the struct size is too large to fit in our compact representation of operations. Even the ndarray which is a heap allocated type requires more than our allotted space in a PackedOperation so we need to reduce it's size by putting it in a Box. The one major difference here from the previous python based representation the unitary matrix was stored as an object type parameter in the PackedInstruction.params field, which now it is stored internally in the operation itself. There is arguably a behavior change around this because it's no longer possible to mutate the array of a UnitaryGate in place once it's inserted into the circuit. While doing this was horribly unsound, because there was no guardrails for doing it a release note is added because there is a small use case where it would have worked and it wasn't explicitly documented. Closes #13272 * Fix basic simulator and document difference in params One other small breaking change with this move in the data model is that UnitaryGate in rust doesn't store it's matrix as a parameter because it doesn't fit logically in the rust data model for circuits. This wasn't a problem before recent changes to BasicSimulator, but recent changes started assuming this. This commit updates this in the basic simulator and then adds a release note to document the change. * Update UnitaryGate docstring * Unify matching arms for unitary gate in DAGCircuit::__eq__ * Immediately box UnitaryGate from python --- Cargo.lock | 72 +++++++++++ Cargo.toml | 1 + .../accelerate/src/target_transpiler/mod.rs | 3 + crates/circuit/Cargo.toml | 6 +- crates/circuit/src/circuit_instruction.rs | 49 +++++++- crates/circuit/src/dag_circuit.rs | 66 +++++++++- crates/circuit/src/operations.rs | 117 +++++++++++++++++- crates/circuit/src/packed_instruction.rs | 28 ++++- .../basic_provider/basic_simulator.py | 2 +- .../unitary-gate-rs-e51f0928d053accd.yaml | 35 ++++++ 10 files changed, 362 insertions(+), 17 deletions(-) create mode 100644 releasenotes/notes/unitary-gate-rs-e51f0928d053accd.yaml diff --git a/Cargo.lock b/Cargo.lock index 717596ccfdbc..40f083bc5469 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -709,6 +709,33 @@ dependencies = [ "autocfg", ] +[[package]] +name = "nalgebra" +version = "0.33.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26aecdf64b707efd1310e3544d709c5c0ac61c13756046aaaba41be5c4f66a3b" +dependencies = [ + "approx 0.5.1", + "matrixmultiply", + "nalgebra-macros", + "num-complex", + "num-rational", + "num-traits", + "simba", + "typenum", +] + +[[package]] +name = "nalgebra-macros" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "254a5372af8fc138e36684761d3c0cdb758a4410e938babcff1c860ce14ddbfc" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.96", +] + [[package]] name = "nano-gemm" version = "0.1.2" @@ -848,6 +875,17 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-rational" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" +dependencies = [ + "num-bigint", + "num-integer", + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -865,6 +903,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b94caae805f998a07d33af06e6a3891e38556051b8045c615470a71590e13e78" dependencies = [ "libc", + "nalgebra", "ndarray", "num-complex", "num-integer", @@ -1226,6 +1265,7 @@ dependencies = [ "hashbrown 0.14.5", "indexmap", "itertools 0.13.0", + "nalgebra", "ndarray", "num-complex", "numpy", @@ -1475,6 +1515,15 @@ dependencies = [ "rayon-cond", ] +[[package]] +name = "safe_arch" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96b02de82ddbe1b636e6170c21be622223aea188ef2e139be0a5b219ec215323" +dependencies = [ + "bytemuck", +] + [[package]] name = "same-file" version = "1.0.6" @@ -1521,6 +1570,19 @@ dependencies = [ "digest", ] +[[package]] +name = "simba" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3a386a501cd104797982c15ae17aafe8b9261315b5d07e3ec803f2ea26be0fa" +dependencies = [ + "approx 0.5.1", + "num-complex", + "num-traits", + "paste", + "wide", +] + [[package]] name = "smallvec" version = "1.13.2" @@ -1694,6 +1756,16 @@ version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +[[package]] +name = "wide" +version = "0.7.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41b5576b9a81633f3e8df296ce0063042a73507636cbe956c61133dd7034ab22" +dependencies = [ + "bytemuck", + "safe_arch", +] + [[package]] name = "winapi-util" version = "0.1.9" diff --git a/Cargo.toml b/Cargo.toml index 50276f437ec5..4c898a2710ba 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,6 +20,7 @@ indexmap.version = "2.7.1" hashbrown.version = "0.14.5" num-bigint = "0.4" num-complex = "0.4" +nalgebra = "0.33" ndarray = "0.15" numpy = "0.23" smallvec = "1.13" diff --git a/crates/accelerate/src/target_transpiler/mod.rs b/crates/accelerate/src/target_transpiler/mod.rs index 0a14a3dec72f..9df6047a494b 100644 --- a/crates/accelerate/src/target_transpiler/mod.rs +++ b/crates/accelerate/src/target_transpiler/mod.rs @@ -783,6 +783,9 @@ impl Target { OperationRef::Gate(gate) => gate.gate.clone_ref(py), OperationRef::Instruction(instruction) => instruction.instruction.clone_ref(py), OperationRef::Operation(operation) => operation.operation.clone_ref(py), + OperationRef::Unitary(unitary) => unitary + .create_py_op(py, &ExtraInstructionAttributes::default())? + .into_any(), }, TargetOperation::Variadic(op_cls) => op_cls.clone_ref(py), }; diff --git a/crates/circuit/Cargo.toml b/crates/circuit/Cargo.toml index 477997f8004c..a776d03ea0b1 100644 --- a/crates/circuit/Cargo.toml +++ b/crates/circuit/Cargo.toml @@ -20,10 +20,10 @@ bytemuck.workspace = true bitfield-struct.workspace = true num-complex.workspace = true ndarray.workspace = true -numpy.workspace = true thiserror.workspace = true approx.workspace = true itertools.workspace = true +nalgebra.workspace = true [dependencies.pyo3] workspace = true @@ -41,6 +41,10 @@ features = ["rayon"] workspace = true features = ["union", "const_generics"] +[dependencies.numpy] +workspace = true +features = ["nalgebra"] + [features] cache_pygates = [] diff --git a/crates/circuit/src/circuit_instruction.rs b/crates/circuit/src/circuit_instruction.rs index 5d401de37826..db73ead2db8f 100644 --- a/crates/circuit/src/circuit_instruction.rs +++ b/crates/circuit/src/circuit_instruction.rs @@ -13,7 +13,7 @@ #[cfg(feature = "cache_pygates")] use std::sync::OnceLock; -use numpy::{IntoPyArray, PyArray2}; +use numpy::{IntoPyArray, PyArray2, PyReadonlyArray2}; use pyo3::basic::CompareOp; use pyo3::exceptions::{PyDeprecationWarning, PyTypeError}; use pyo3::prelude::*; @@ -21,6 +21,7 @@ use pyo3::types::{PyBool, PyList, PyString, PyTuple, PyType}; use pyo3::IntoPyObjectExt; use pyo3::{intern, PyObject, PyResult}; +use nalgebra::{MatrixView2, MatrixView4}; use num_complex::Complex64; use smallvec::SmallVec; @@ -28,8 +29,8 @@ use crate::imports::{ CONTROLLED_GATE, CONTROL_FLOW_OP, GATE, INSTRUCTION, OPERATION, WARNINGS_WARN, }; use crate::operations::{ - Operation, OperationRef, Param, PyGate, PyInstruction, PyOperation, StandardGate, - StandardInstruction, StandardInstructionType, + ArrayType, Operation, OperationRef, Param, PyGate, PyInstruction, PyOperation, StandardGate, + StandardInstruction, StandardInstructionType, UnitaryGate, }; use crate::packed_instruction::PackedOperation; @@ -341,6 +342,9 @@ impl CircuitInstruction { OperationRef::Gate(gate) => gate.gate.clone_ref(py), OperationRef::Instruction(instruction) => instruction.instruction.clone_ref(py), OperationRef::Operation(operation) => operation.operation.clone_ref(py), + OperationRef::Unitary(unitary) => { + unitary.create_py_op(py, &self.extra_attrs)?.into_any() + } }; #[cfg(feature = "cache_pygates")] @@ -762,6 +766,45 @@ impl<'py> FromPyObject<'py> for OperationFromPython { }); } + // We need to check by name here to avoid a circular import during initial loading + if ob.getattr(intern!(py, "name"))?.extract::()? == "unitary" { + let params = extract_params()?; + if let Param::Obj(data) = ¶ms[0] { + let py_matrix: PyReadonlyArray2 = data.extract(py)?; + let matrix: Option> = py_matrix.try_as_matrix(); + if let Some(x) = matrix { + let unitary_gate = Box::new(UnitaryGate { + array: ArrayType::OneQ(x.into_owned()), + }); + return Ok(OperationFromPython { + operation: PackedOperation::from_unitary(unitary_gate), + params: SmallVec::new(), + extra_attrs: extract_extra()?, + }); + } + let matrix: Option> = py_matrix.try_as_matrix(); + if let Some(x) = matrix { + let unitary_gate = Box::new(UnitaryGate { + array: ArrayType::TwoQ(x.into_owned()), + }); + return Ok(OperationFromPython { + operation: PackedOperation::from_unitary(unitary_gate), + params: SmallVec::new(), + extra_attrs: extract_extra()?, + }); + } else { + let unitary_gate = Box::new(UnitaryGate { + array: ArrayType::NDArray(py_matrix.as_array().to_owned()), + }); + return Ok(OperationFromPython { + operation: PackedOperation::from_unitary(unitary_gate), + params: SmallVec::new(), + extra_attrs: extract_extra()?, + }); + }; + } + } + if ob_type.is_subclass(GATE.get_bound(py))? { let params = extract_params()?; let gate = Box::new(PyGate { diff --git a/crates/circuit/src/dag_circuit.rs b/crates/circuit/src/dag_circuit.rs index 41de1e1d559b..42b334e88207 100644 --- a/crates/circuit/src/dag_circuit.rs +++ b/crates/circuit/src/dag_circuit.rs @@ -13,6 +13,7 @@ use std::hash::Hash; use ahash::RandomState; +use approx::relative_eq; use smallvec::SmallVec; use crate::bit_data::BitData; @@ -26,7 +27,7 @@ use crate::dot_utils::build_dot; use crate::error::DAGCircuitError; use crate::imports; use crate::interner::{Interned, InternedMap, Interner}; -use crate::operations::{Operation, OperationRef, Param, PyInstruction, StandardGate}; +use crate::operations::{ArrayType, Operation, OperationRef, Param, PyInstruction, StandardGate}; use crate::packed_instruction::{PackedInstruction, PackedOperation}; use crate::rustworkx_core_vnext::isomorphism; use crate::{BitType, Clbit, Qubit, TupleLikeArg}; @@ -2631,6 +2632,60 @@ def _format(operand): | [OperationRef::Instruction(_), OperationRef::StandardInstruction(_)] => { Ok(inst1.py_op_eq(py, inst2)? && check_args() && check_conditions()?) } + [OperationRef::Unitary(op_a), OperationRef::Unitary(op_b)] => { + match [&op_a.array, &op_b.array] { + [ArrayType::NDArray(a), ArrayType::NDArray(b)] => { + Ok(relative_eq!(a, b, max_relative = 1e-5, epsilon = 1e-8)) + } + [ArrayType::OneQ(a), ArrayType::NDArray(b)] + | [ArrayType::NDArray(b), ArrayType::OneQ(a)] => { + if b.shape()[0] == 2 { + for i in 0..2 { + for j in 0..2 { + if !relative_eq!( + b[[i, j]], + a[(i, j)], + max_relative = 1e-5, + epsilon = 1e-8 + ) { + return Ok(false); + } + } + } + Ok(true) + } else { + Ok(false) + } + } + [ArrayType::TwoQ(a), ArrayType::NDArray(b)] + | [ArrayType::NDArray(b), ArrayType::TwoQ(a)] => { + if b.shape()[0] == 4 { + for i in 0..4 { + for j in 0..4 { + if !relative_eq!( + b[[i, j]], + a[(i, j)], + max_relative = 1e-5, + epsilon = 1e-8 + ) { + return Ok(false); + } + } + } + Ok(true) + } else { + Ok(false) + } + } + [ArrayType::OneQ(a), ArrayType::OneQ(b)] => { + Ok(relative_eq!(a, b, max_relative = 1e-5, epsilon = 1e-8)) + } + [ArrayType::TwoQ(a), ArrayType::TwoQ(b)] => { + Ok(relative_eq!(a, b, max_relative = 1e-5, epsilon = 1e-8)) + } + _ => Ok(false), + } + } _ => Ok(false), } } @@ -3295,7 +3350,8 @@ def _format(operand): py_op.operation.setattr(py, "condition", new_condition)?; } OperationRef::StandardGate(_) - | OperationRef::StandardInstruction(_) => {} + | OperationRef::StandardInstruction(_) + | OperationRef::Unitary(_) => {} } } } @@ -6315,9 +6371,9 @@ impl DAGCircuit { }; #[cfg(feature = "cache_pygates")] let py_op = match new_op.operation.view() { - OperationRef::StandardGate(_) | OperationRef::StandardInstruction(_) => { - OnceLock::new() - } + OperationRef::StandardGate(_) + | OperationRef::StandardInstruction(_) + | OperationRef::Unitary(_) => OnceLock::new(), OperationRef::Gate(gate) => OnceLock::from(gate.gate.clone_ref(py)), OperationRef::Instruction(instruction) => { OnceLock::from(instruction.instruction.clone_ref(py)) diff --git a/crates/circuit/src/operations.rs b/crates/circuit/src/operations.rs index a3a917815150..e0add717824a 100644 --- a/crates/circuit/src/operations.rs +++ b/crates/circuit/src/operations.rs @@ -17,19 +17,21 @@ use std::{fmt, vec}; use crate::circuit_data::CircuitData; use crate::circuit_instruction::ExtraInstructionAttributes; use crate::imports::{get_std_gate_class, BARRIER, DELAY, MEASURE, RESET}; -use crate::imports::{PARAMETER_EXPRESSION, QUANTUM_CIRCUIT}; +use crate::imports::{PARAMETER_EXPRESSION, QUANTUM_CIRCUIT, UNITARY_GATE}; use crate::{gate_matrix, impl_intopyobject_for_copy_pyclass, Qubit}; -use ndarray::{aview2, Array2}; +use nalgebra::{Matrix2, Matrix4}; +use ndarray::{array, aview2, Array2}; use num_complex::Complex64; use smallvec::{smallvec, SmallVec}; use numpy::IntoPyArray; use numpy::PyArray2; use numpy::PyReadonlyArray2; +use numpy::ToPyArray; use pyo3::exceptions::PyValueError; use pyo3::prelude::*; -use pyo3::types::{IntoPyDict, PyFloat, PyIterator, PyList, PyTuple}; +use pyo3::types::{IntoPyDict, PyDict, PyFloat, PyIterator, PyList, PyTuple}; use pyo3::{intern, IntoPyObjectExt, Python}; #[derive(Clone, Debug, IntoPyObject, IntoPyObjectRef)] @@ -164,6 +166,7 @@ pub enum OperationRef<'a> { Gate(&'a PyGate), Instruction(&'a PyInstruction), Operation(&'a PyOperation), + Unitary(&'a UnitaryGate), } impl Operation for OperationRef<'_> { @@ -175,6 +178,7 @@ impl Operation for OperationRef<'_> { Self::Gate(gate) => gate.name(), Self::Instruction(instruction) => instruction.name(), Self::Operation(operation) => operation.name(), + Self::Unitary(unitary) => unitary.name(), } } #[inline] @@ -185,6 +189,7 @@ impl Operation for OperationRef<'_> { Self::Gate(gate) => gate.num_qubits(), Self::Instruction(instruction) => instruction.num_qubits(), Self::Operation(operation) => operation.num_qubits(), + Self::Unitary(unitary) => unitary.num_qubits(), } } #[inline] @@ -195,6 +200,7 @@ impl Operation for OperationRef<'_> { Self::Gate(gate) => gate.num_clbits(), Self::Instruction(instruction) => instruction.num_clbits(), Self::Operation(operation) => operation.num_clbits(), + Self::Unitary(unitary) => unitary.num_clbits(), } } #[inline] @@ -205,6 +211,7 @@ impl Operation for OperationRef<'_> { Self::Gate(gate) => gate.num_params(), Self::Instruction(instruction) => instruction.num_params(), Self::Operation(operation) => operation.num_params(), + Self::Unitary(unitary) => unitary.num_params(), } } #[inline] @@ -215,6 +222,7 @@ impl Operation for OperationRef<'_> { Self::Gate(gate) => gate.control_flow(), Self::Instruction(instruction) => instruction.control_flow(), Self::Operation(operation) => operation.control_flow(), + Self::Unitary(unitary) => unitary.control_flow(), } } #[inline] @@ -225,6 +233,7 @@ impl Operation for OperationRef<'_> { OperationRef::Gate(gate) => gate.blocks(), OperationRef::Instruction(instruction) => instruction.blocks(), OperationRef::Operation(operation) => operation.blocks(), + Self::Unitary(unitary) => unitary.blocks(), } } #[inline] @@ -235,6 +244,7 @@ impl Operation for OperationRef<'_> { Self::Gate(gate) => gate.matrix(params), Self::Instruction(instruction) => instruction.matrix(params), Self::Operation(operation) => operation.matrix(params), + Self::Unitary(unitary) => unitary.matrix(params), } } #[inline] @@ -245,6 +255,7 @@ impl Operation for OperationRef<'_> { Self::Gate(gate) => gate.definition(params), Self::Instruction(instruction) => instruction.definition(params), Self::Operation(operation) => operation.definition(params), + Self::Unitary(unitary) => unitary.definition(params), } } #[inline] @@ -255,6 +266,7 @@ impl Operation for OperationRef<'_> { Self::Gate(gate) => gate.standard_gate(), Self::Instruction(instruction) => instruction.standard_gate(), Self::Operation(operation) => operation.standard_gate(), + Self::Unitary(unitary) => unitary.standard_gate(), } } #[inline] @@ -265,6 +277,7 @@ impl Operation for OperationRef<'_> { Self::Gate(gate) => gate.directive(), Self::Instruction(instruction) => instruction.directive(), Self::Operation(operation) => operation.directive(), + Self::Unitary(unitary) => unitary.directive(), } } } @@ -2792,3 +2805,101 @@ impl Operation for PyOperation { }) } } + +#[derive(Clone, Debug)] +pub enum ArrayType { + NDArray(Array2), + OneQ(Matrix2), + TwoQ(Matrix4), +} + +/// This class is a rust representation of a UnitaryGate in Python, +/// a gate represented solely by it's unitary matrix. +#[derive(Clone, Debug)] +#[repr(align(8))] +pub struct UnitaryGate { + pub array: ArrayType, +} + +impl Operation for UnitaryGate { + fn name(&self) -> &str { + "unitary" + } + fn num_qubits(&self) -> u32 { + match &self.array { + ArrayType::NDArray(arr) => arr.shape()[0].ilog2(), + ArrayType::OneQ(_) => 1, + ArrayType::TwoQ(_) => 2, + } + } + fn num_clbits(&self) -> u32 { + 0 + } + fn num_params(&self) -> u32 { + 0 + } + fn control_flow(&self) -> bool { + false + } + fn blocks(&self) -> Vec { + vec![] + } + fn matrix(&self, _params: &[Param]) -> Option> { + match &self.array { + ArrayType::NDArray(arr) => Some(arr.clone()), + ArrayType::OneQ(mat) => Some(array!( + [mat[(0, 0)], mat[(0, 1)]], + [mat[(1, 0)], mat[(1, 1)]], + )), + ArrayType::TwoQ(mat) => Some(array!( + [mat[(0, 0)], mat[(0, 1)], mat[(0, 2)], mat[(0, 3)]], + [mat[(1, 0)], mat[(1, 1)], mat[(1, 2)], mat[(1, 3)]], + [mat[(2, 0)], mat[(2, 1)], mat[(2, 2)], mat[(2, 3)]], + [mat[(3, 0)], mat[(3, 1)], mat[(3, 2)], mat[(3, 3)]], + )), + } + } + fn definition(&self, _params: &[Param]) -> Option { + None + } + fn standard_gate(&self) -> Option { + None + } + + fn directive(&self) -> bool { + false + } +} + +impl UnitaryGate { + pub fn create_py_op( + &self, + py: Python, + extra_attrs: &ExtraInstructionAttributes, + ) -> PyResult> { + let (label, _unit, _duration, condition) = ( + extra_attrs.label(), + extra_attrs.unit(), + extra_attrs.duration(), + extra_attrs.condition(), + ); + let kwargs = PyDict::new(py); + if let Some(label) = label { + kwargs.set_item(intern!(py, "label"), label.into_py_any(py)?)?; + } + let out_array = match &self.array { + ArrayType::NDArray(arr) => arr.to_pyarray(py), + ArrayType::OneQ(arr) => arr.to_pyarray(py), + ArrayType::TwoQ(arr) => arr.to_pyarray(py), + }; + kwargs.set_item(intern!(py, "check_input"), false)?; + kwargs.set_item(intern!(py, "num_qubits"), self.num_qubits())?; + let mut gate = UNITARY_GATE + .get_bound(py) + .call((out_array,), Some(&kwargs))?; + if let Some(cond) = condition { + gate = gate.call_method1(intern!(py, "c_if"), (cond,))?; + } + Ok(gate.unbind()) + } +} diff --git a/crates/circuit/src/packed_instruction.rs b/crates/circuit/src/packed_instruction.rs index 76b1896d3a27..6ae8f1763395 100644 --- a/crates/circuit/src/packed_instruction.rs +++ b/crates/circuit/src/packed_instruction.rs @@ -23,11 +23,11 @@ use smallvec::SmallVec; use crate::circuit_data::CircuitData; use crate::circuit_instruction::ExtraInstructionAttributes; -use crate::imports::{get_std_gate_class, BARRIER, DEEPCOPY, DELAY, MEASURE, RESET}; +use crate::imports::{get_std_gate_class, BARRIER, DEEPCOPY, DELAY, MEASURE, RESET, UNITARY_GATE}; use crate::interner::Interned; use crate::operations::{ Operation, OperationRef, Param, PyGate, PyInstruction, PyOperation, StandardGate, - StandardInstruction, + StandardInstruction, UnitaryGate, }; use crate::{Clbit, Qubit}; @@ -43,13 +43,14 @@ enum PackedOperationType { PyGate = 2, PyInstruction = 3, PyOperation = 4, + UnitaryGate = 5, } unsafe impl ::bytemuck::CheckedBitPattern for PackedOperationType { type Bits = u8; fn is_valid_bit_pattern(bits: &Self::Bits) -> bool { - *bits < 5 + *bits < 6 } } unsafe impl ::bytemuck::NoUninit for PackedOperationType {} @@ -65,6 +66,7 @@ unsafe impl ::bytemuck::NoUninit for PackedOperationType {} /// Gate(Box), /// Instruction(Box), /// Operation(Box), +/// UnitaryGate(Box), /// } /// ``` /// @@ -251,7 +253,7 @@ mod standard_instruction { /// A private module to encapsulate the encoding of pointer types. mod pointer { - use crate::operations::{PyGate, PyInstruction, PyOperation}; + use crate::operations::{PyGate, PyInstruction, PyOperation, UnitaryGate}; use crate::packed_instruction::{PackedOperation, PackedOperationType}; use std::ptr::NonNull; @@ -333,6 +335,7 @@ mod pointer { impl_packable_pointer!(PyGate, PackedOperationType::PyGate); impl_packable_pointer!(PyInstruction, PackedOperationType::PyInstruction); impl_packable_pointer!(PyOperation, PackedOperationType::PyOperation); + impl_packable_pointer!(UnitaryGate, PackedOperationType::UnitaryGate); } impl PackedOperation { @@ -388,6 +391,7 @@ impl PackedOperation { OperationRef::Instruction(self.try_into().unwrap()) } PackedOperationType::PyOperation => OperationRef::Operation(self.try_into().unwrap()), + PackedOperationType::UnitaryGate => OperationRef::Unitary(self.try_into().unwrap()), } } @@ -421,6 +425,10 @@ impl PackedOperation { operation.into() } + pub fn from_unitary(unitary: Box) -> Self { + unitary.into() + } + /// Check equality of the operation, including Python-space checks, if appropriate. pub fn py_eq(&self, py: Python, other: &PackedOperation) -> PyResult { match (self.view(), other.view()) { @@ -480,6 +488,7 @@ impl PackedOperation { op_name: operation.op_name.clone(), } .into()), + OperationRef::Unitary(unitary) => Ok(unitary.clone().into()), } } @@ -517,6 +526,7 @@ impl PackedOperation { op_name: operation.op_name.clone(), }) .into()), + OperationRef::Unitary(unitary) => Ok(unitary.clone().into()), } } @@ -555,6 +565,12 @@ impl PackedOperation { OperationRef::Gate(gate) => gate.gate.bind(py), OperationRef::Instruction(instruction) => instruction.instruction.bind(py), OperationRef::Operation(operation) => operation.operation.bind(py), + OperationRef::Unitary(_) => { + return UNITARY_GATE + .get_bound(py) + .downcast::()? + .is_subclass(py_type); + } }; py_op.is_instance(py_type) } @@ -569,6 +585,7 @@ impl Operation for PackedOperation { OperationRef::Gate(gate) => gate.name(), OperationRef::Instruction(instruction) => instruction.name(), OperationRef::Operation(operation) => operation.name(), + OperationRef::Unitary(unitary) => unitary.name(), }; // SAFETY: all of the inner parts of the view are owned by `self`, so it's valid for us to // forcibly reborrowing up to our own lifetime. We avoid using `` @@ -632,6 +649,7 @@ impl Clone for PackedOperation { OperationRef::Operation(operation) => { Self::from_operation(Box::new(operation.to_owned())) } + OperationRef::Unitary(unitary) => Self::from_unitary(Box::new(unitary.clone())), } } } @@ -644,6 +662,7 @@ impl Drop for PackedOperation { PackedOperationType::PyGate => PyGate::drop_packed(self), PackedOperationType::PyInstruction => PyInstruction::drop_packed(self), PackedOperationType::PyOperation => PyOperation::drop_packed(self), + PackedOperationType::UnitaryGate => UnitaryGate::drop_packed(self), } } } @@ -748,6 +767,7 @@ impl PackedInstruction { OperationRef::Gate(gate) => Ok(gate.gate.clone_ref(py)), OperationRef::Instruction(instruction) => Ok(instruction.instruction.clone_ref(py)), OperationRef::Operation(operation) => Ok(operation.operation.clone_ref(py)), + OperationRef::Unitary(unitary) => unitary.create_py_op(py, &self.extra_attrs), } }; diff --git a/qiskit/providers/basic_provider/basic_simulator.py b/qiskit/providers/basic_provider/basic_simulator.py index 11ecb34feb40..6025cae3df7c 100644 --- a/qiskit/providers/basic_provider/basic_simulator.py +++ b/qiskit/providers/basic_provider/basic_simulator.py @@ -573,7 +573,7 @@ def _run_circuit(self, circuit) -> dict: for operation in circuit.data: if operation.name == "unitary": qubits = [circuit.find_bit(bit).index for bit in operation.qubits] - gate = operation.params[0] + gate = operation.operation.params[0] self._add_unitary(gate, qubits) elif operation.name in ("id", "u0", "delay"): pass diff --git a/releasenotes/notes/unitary-gate-rs-e51f0928d053accd.yaml b/releasenotes/notes/unitary-gate-rs-e51f0928d053accd.yaml new file mode 100644 index 000000000000..b3e66dec9dc6 --- /dev/null +++ b/releasenotes/notes/unitary-gate-rs-e51f0928d053accd.yaml @@ -0,0 +1,35 @@ +--- +upgrade_circuits: + - | + The internal representation of :class:`.UnitaryGate` has changed when they're + added to a :class:`.QuantumCircuit`. The object stored in the circuit will + not necessarily share a common reference to the object added to the circuit + anymore. This was never guaranteed to be the case and mutating the + :class:`.UnitaryGate` object directly or by reference was unsound and always + likely to corrupt the circuit, especially if you changed the matrix. + If you need to mutate an element in the circuit (which is **not** recommended + as it’s inefficient and error prone) you should ensure that you do something + like:: + + from qiskit.circuit import QuantumCircuit + from qiskit.quantum_info import random_unitary + from qiskit.circuit.library import UnitaryGate + import numpy as np + + qc = QuantumCircuit(2) + qc.unitary(np.eye(2, dtype=complex)) + + new_op = UnitaryGate(random_unitary(2)) + qc.data[0] = qc.data[0].replace(operation=new_op) + + This also applies to :class:`.DAGCircuit` too, but you can use + :meth:`.DAGCircuit.substitute_node` instead. + - | + The :attr:`.CircuitInstruction.params` attribute for a :class:`.CircuitInstruction` + that contains an :class:`.UnitaryGate` for its :attr:`~.CircuitInstruction.operation` + will no longer contain the underlying unitary matrix for the gate. + This is because the internal representation of the gate no longer + treats the matrix object as a parameter. If you need to access the + matrix of the gate you can do this either via the + :attr:`.CircuitInstruction.matrix` or the :attr:`.UnitaryGate.params` + field of the :attr:`.CircuitInstruction.operation`. From 4521122d88f5a775ac53b3fcd8794e7f165983b3 Mon Sep 17 00:00:00 2001 From: Luciano Bello Date: Thu, 6 Feb 2025 01:19:39 +0100 Subject: [PATCH 08/48] Pylint configuration to prevent accidentally committing debugging print statements (#13796) * ignore the current ones --- pyproject.toml | 5 ++++- qiskit/qasm2/export.py | 4 ++-- qiskit/quantum_info/operators/operator.py | 2 +- qiskit/quantum_info/states/densitymatrix.py | 2 +- qiskit/quantum_info/states/statevector.py | 2 +- qiskit/visualization/circuit/matplotlib.py | 2 +- test/randomized/test_transpiler_equivalence.py | 2 +- tools/find_deprecated.py | 1 + tools/find_optional_imports.py | 1 + tools/fix_mailmap.py | 2 ++ tools/pgo_scripts/test_utility_scale.py | 1 + tools/pylint_incr.py | 2 ++ tools/verify_images.py | 1 + 13 files changed, 19 insertions(+), 8 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index a89f191fe848..848165ae04f1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -215,7 +215,7 @@ extension-pkg-allow-list = [ "rustworkx", "tweedledum", ] -load-plugins = ["pylint.extensions.docparams", "pylint.extensions.docstyle"] +load-plugins = ["pylint.extensions.docparams", "pylint.extensions.docstyle", "pylint.extensions.bad_builtin"] py-version = "3.9" # update it when bumping minimum supported python version [tool.pylint.basic] @@ -267,3 +267,6 @@ exclude_also = [ "if TYPE_CHECKING:", # Code that only runs during type checks "@abstractmethod", # Abstract methods are not testable ] + +[tool.pylint.deprecated_builtins] +bad-functions = ["print"] diff --git a/qiskit/qasm2/export.py b/qiskit/qasm2/export.py index fbe9b533ea9a..2bd2538a6bd8 100644 --- a/qiskit/qasm2/export.py +++ b/qiskit/qasm2/export.py @@ -116,10 +116,10 @@ def dump(circuit: QuantumCircuit, filename_or_stream: os.PathLike | io.TextIOBas QASM2ExportError: if the circuit cannot be represented by OpenQASM 2. """ if isinstance(filename_or_stream, io.TextIOBase): - print(dumps(circuit), file=filename_or_stream) + print(dumps(circuit), file=filename_or_stream) # pylint: disable=bad-builtin return with open(filename_or_stream, "w") as stream: - print(dumps(circuit), file=stream) + print(dumps(circuit), file=stream) # pylint: disable=bad-builtin def dumps(circuit: QuantumCircuit, /) -> str: diff --git a/qiskit/quantum_info/operators/operator.py b/qiskit/quantum_info/operators/operator.py index 8479aa5dd991..d8c66e50ff31 100644 --- a/qiskit/quantum_info/operators/operator.py +++ b/qiskit/quantum_info/operators/operator.py @@ -238,7 +238,7 @@ def draw(self, output=None, **drawer_args): def _ipython_display_(self): out = self.draw() if isinstance(out, str): - print(out) + print(out) # pylint: disable=bad-builtin else: from IPython.display import display diff --git a/qiskit/quantum_info/states/densitymatrix.py b/qiskit/quantum_info/states/densitymatrix.py index 29170e75decf..7147ae5d2e7e 100644 --- a/qiskit/quantum_info/states/densitymatrix.py +++ b/qiskit/quantum_info/states/densitymatrix.py @@ -180,7 +180,7 @@ def draw(self, output: str | None = None, **drawer_args): def _ipython_display_(self): out = self.draw() if isinstance(out, str): - print(out) + print(out) # pylint: disable=bad-builtin else: from IPython.display import display diff --git a/qiskit/quantum_info/states/statevector.py b/qiskit/quantum_info/states/statevector.py index 15519fb2e24d..a392c2b44ceb 100644 --- a/qiskit/quantum_info/states/statevector.py +++ b/qiskit/quantum_info/states/statevector.py @@ -198,7 +198,7 @@ def draw(self, output: str | None = None, **drawer_args): def _ipython_display_(self): out = self.draw() if isinstance(out, str): - print(out) + print(out) # pylint: disable=bad-builtin else: from IPython.display import display diff --git a/qiskit/visualization/circuit/matplotlib.py b/qiskit/visualization/circuit/matplotlib.py index 62e1e0e7b156..6f7359021839 100644 --- a/qiskit/visualization/circuit/matplotlib.py +++ b/qiskit/visualization/circuit/matplotlib.py @@ -1074,7 +1074,7 @@ def _draw_ops( self._get_colors(node, node_data) if verbose: - print(op) + print(op) # pylint: disable=bad-builtin # add conditional if getattr(op, "condition", None) or isinstance(op, SwitchCaseOp): diff --git a/test/randomized/test_transpiler_equivalence.py b/test/randomized/test_transpiler_equivalence.py index 76bbf9ef08e3..77975b247d0c 100644 --- a/test/randomized/test_transpiler_equivalence.py +++ b/test/randomized/test_transpiler_equivalence.py @@ -288,7 +288,7 @@ def equivalent_transpile(self, kwargs): + ", ".join(f"{key:s}={value!r}" for key, value in kwargs.items() if value is not None) + ")" ) - print(f"Evaluating {call} for:\n{qasm2.dumps(self.qc)}") + print(f"Evaluating {call} for:\n{qasm2.dumps(self.qc)}") # pylint: disable=bad-builtin shots = 4096 diff --git a/tools/find_deprecated.py b/tools/find_deprecated.py index 2b3e4dedd315..27a41a35c4e0 100755 --- a/tools/find_deprecated.py +++ b/tools/find_deprecated.py @@ -10,6 +10,7 @@ # 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. +# pylint: disable=bad-builtin """List deprecated decorators.""" from __future__ import annotations diff --git a/tools/find_optional_imports.py b/tools/find_optional_imports.py index 58062cdfab9f..1f01b78dfec0 100755 --- a/tools/find_optional_imports.py +++ b/tools/find_optional_imports.py @@ -10,6 +10,7 @@ # 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. +# pylint: disable=bad-builtin """Utility to check that slow imports are not used in the default path.""" diff --git a/tools/fix_mailmap.py b/tools/fix_mailmap.py index 0bc48e642be8..c847c0cfe9d2 100755 --- a/tools/fix_mailmap.py +++ b/tools/fix_mailmap.py @@ -12,6 +12,8 @@ # copyright notice, and modified files need to carry a notice indicating # that they have been altered from the originals. +# pylint: disable=bad-builtin + """Check and fixup mailmaps files against git log.""" from __future__ import annotations diff --git a/tools/pgo_scripts/test_utility_scale.py b/tools/pgo_scripts/test_utility_scale.py index 4c2d44caac2a..55242e323eb2 100755 --- a/tools/pgo_scripts/test_utility_scale.py +++ b/tools/pgo_scripts/test_utility_scale.py @@ -11,6 +11,7 @@ # 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. +# pylint: disable=bad-builtin """Script to generate 'utility scale' load for profiling in a PGO context""" diff --git a/tools/pylint_incr.py b/tools/pylint_incr.py index 261f7d4cf582..6b2fc5a5c9fb 100755 --- a/tools/pylint_incr.py +++ b/tools/pylint_incr.py @@ -12,6 +12,8 @@ # copyright notice, and modified files need to carry a notice indicating # that they have been altered from the originals. +# pylint: disable=bad-builtin + """Run pylint incrementally on only changed files""" import subprocess diff --git a/tools/verify_images.py b/tools/verify_images.py index 5335cd3bc4eb..74e222588c57 100755 --- a/tools/verify_images.py +++ b/tools/verify_images.py @@ -10,6 +10,7 @@ # 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. +# pylint: disable=bad-builtin """Utility script to verify that all images have alt text""" From 05c4f01f4471a139661677213a336d07f3ebfea2 Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Thu, 6 Feb 2025 10:25:59 +0000 Subject: [PATCH 09/48] Add `default` translation stage plugin (#13794) This is added to give us semantic room to play with the default translation algorithm during the 2.x series. It starts life as an alias to `translator`, but may change to improve support for fractional-gate translation, more optimized synthesis routines, or discrete-basis translations (like a SK-based 1q translator for logical backends). This makes it programmatically possible to distinguish the "default" plugin choice from an explicit "translator" choice, even when (as right now) those are the same thing. Note that `pass_manager_config` uses `None` as an implicit default, but we active suggest using the plugin-stage getters, which need an actual plugin name. --- pyproject.toml | 1 + qiskit/compiler/transpiler.py | 8 ++++---- qiskit/transpiler/__init__.py | 12 ++++++++++++ .../preset_passmanagers/builtin_plugins.py | 13 ++++++++++++- .../transpiler/preset_passmanagers/common.py | 8 +++++--- .../transpiler/preset_passmanagers/level0.py | 2 +- .../transpiler/preset_passmanagers/level1.py | 2 +- .../transpiler/preset_passmanagers/level2.py | 2 +- .../transpiler/preset_passmanagers/level3.py | 2 +- .../transpiler/preset_passmanagers/plugin.py | 2 +- ...ult-translation-stage-9d0335e354751af0.yaml | 18 ++++++++++++++++++ 11 files changed, 57 insertions(+), 13 deletions(-) create mode 100644 releasenotes/notes/default-translation-stage-9d0335e354751af0.yaml diff --git a/pyproject.toml b/pyproject.toml index 848165ae04f1..0ff81bf9add7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -122,6 +122,7 @@ sk = "qiskit.transpiler.passes.synthesis.solovay_kitaev_synthesis:SolovayKitaevS default = "qiskit.transpiler.preset_passmanagers.builtin_plugins:DefaultInitPassManager" [project.entry-points."qiskit.transpiler.translation"] +default = "qiskit.transpiler.preset_passmanagers.builtin_plugins:DefaultTranslationPassManager" synthesis = "qiskit.transpiler.preset_passmanagers.builtin_plugins:UnitarySynthesisPassManager" translator = "qiskit.transpiler.preset_passmanagers.builtin_plugins:BasisTranslatorPassManager" diff --git a/qiskit/compiler/transpiler.py b/qiskit/compiler/transpiler.py index 5e361503091f..4d705481ce71 100644 --- a/qiskit/compiler/transpiler.py +++ b/qiskit/compiler/transpiler.py @@ -191,10 +191,10 @@ def transpile( # pylint: disable=too-many-return-statements This can also be the external plugin name to use for the ``routing`` stage. You can see a list of installed plugins by using :func:`~.list_stage_plugins` with ``"routing"`` for the ``stage_name`` argument. - translation_method: Name of translation pass (``"translator"`` or ``"synthesis"``) - This can also be the external plugin name to use for the ``translation`` stage. - You can see a list of installed plugins by using :func:`~.list_stage_plugins` with - ``"translation"`` for the ``stage_name`` argument. + translation_method: Name of translation pass (``"default"``, ``"translator"`` or + ``"synthesis"``). This can also be the external plugin name to use for the + ``translation`` stage. You can see a list of installed plugins by using + :func:`~.list_stage_plugins` with ``"translation"`` for the ``stage_name`` argument. scheduling_method: Name of scheduling pass. * ``'as_soon_as_possible'``: Schedule instructions greedily, as early as possible on a qubit resource. (alias: ``'asap'``) diff --git a/qiskit/transpiler/__init__.py b/qiskit/transpiler/__init__.py index 696df5151cc3..136dfb70d766 100644 --- a/qiskit/transpiler/__init__.py +++ b/qiskit/transpiler/__init__.py @@ -643,6 +643,9 @@ * - Method - Summary + * - :ref:`default ` + - Use a Qiskit-chosen default translation method. + * - :ref:`translator ` - Symbolic translation of gates to the target basis using known equivalences. @@ -650,6 +653,15 @@ - Collect each run of one- and two-qubit gates into a matrix representation, and resynthesize from there. +.. _transpiler-preset-stage-translation-default: + +Built-in ``default`` plugin +........................... + +Use a Qiskit-chosen default method for translation. As of Qiskit 2.0, this is the same as +:ref:`transpiler-preset-stage-translation-translator`, but the chosen algorithm might change during +the 2.x series, either for all targets, or only for certain classes of target. + .. _transpiler-preset-stage-translation-synthesis: Built-in ``synthesis`` plugin diff --git a/qiskit/transpiler/preset_passmanagers/builtin_plugins.py b/qiskit/transpiler/preset_passmanagers/builtin_plugins.py index 1e5ca4519864..4742de6fda49 100644 --- a/qiskit/transpiler/preset_passmanagers/builtin_plugins.py +++ b/qiskit/transpiler/preset_passmanagers/builtin_plugins.py @@ -197,6 +197,17 @@ def pass_manager(self, pass_manager_config, optimization_level=None) -> PassMana return init +class DefaultTranslationPassManager(PassManagerStagePlugin): + """Plugin class for the default-method translation stage.""" + + def pass_manager(self, pass_manager_config, optimization_level=None) -> PassManager: + # For now, this is just a wrapper around the `BasisTranslator`. It might expand in the + # future if we want to change the default method to do more context-aware switching, or to + # start transitioning the default method without breaking the semantics of the default + # string referring to the `BasisTranslator`. + return BasisTranslatorPassManager().pass_manager(pass_manager_config, optimization_level) + + class BasisTranslatorPassManager(PassManagerStagePlugin): """Plugin class for translation stage with :class:`~.BasisTranslator`""" @@ -535,7 +546,7 @@ class OptimizationPassManager(PassManagerStagePlugin): def pass_manager(self, pass_manager_config, optimization_level=None) -> PassManager: """Build pass manager for optimization stage.""" # Obtain the translation method required for this pass to work - translation_method = pass_manager_config.translation_method or "translator" + translation_method = pass_manager_config.translation_method or "default" optimization = PassManager() if optimization_level != 0: plugin_manager = PassManagerStagePluginManager() diff --git a/qiskit/transpiler/preset_passmanagers/common.py b/qiskit/transpiler/preset_passmanagers/common.py index 431089f657dd..4f360cd1998f 100644 --- a/qiskit/transpiler/preset_passmanagers/common.py +++ b/qiskit/transpiler/preset_passmanagers/common.py @@ -60,15 +60,17 @@ # Any method neither known good nor known bad (i.e. not a Terra-internal pass) is passed through # without error, since it is being supplied by a plugin and we don't have any knowledge of these. _CONTROL_FLOW_STATES = { - "layout_method": _ControlFlowState(working={"trivial", "dense", "sabre"}, not_working=set()), + "layout_method": _ControlFlowState( + working={"default", "trivial", "dense", "sabre"}, not_working=set() + ), "routing_method": _ControlFlowState( working={"none", "stochastic", "sabre"}, not_working={"lookahead", "basic"} ), "translation_method": _ControlFlowState( - working={"translator", "synthesis"}, + working={"default", "translator", "synthesis"}, not_working=set(), ), - "optimization_method": _ControlFlowState(working=set(), not_working=set()), + "optimization_method": _ControlFlowState(working={"default"}, not_working=set()), "scheduling_method": _ControlFlowState(working=set(), not_working={"alap", "asap"}), } diff --git a/qiskit/transpiler/preset_passmanagers/level0.py b/qiskit/transpiler/preset_passmanagers/level0.py index 58381b3db7a8..865cc13f2407 100644 --- a/qiskit/transpiler/preset_passmanagers/level0.py +++ b/qiskit/transpiler/preset_passmanagers/level0.py @@ -49,7 +49,7 @@ def level_0_pass_manager(pass_manager_config: PassManagerConfig) -> StagedPassMa init_method = pass_manager_config.init_method or "default" layout_method = pass_manager_config.layout_method or "default" routing_method = pass_manager_config.routing_method or "sabre" - translation_method = pass_manager_config.translation_method or "translator" + translation_method = pass_manager_config.translation_method or "default" optimization_method = pass_manager_config.optimization_method or "default" scheduling_method = pass_manager_config.scheduling_method or "default" target = pass_manager_config.target diff --git a/qiskit/transpiler/preset_passmanagers/level1.py b/qiskit/transpiler/preset_passmanagers/level1.py index 0a0b1c2a8e43..e665b9f31186 100644 --- a/qiskit/transpiler/preset_passmanagers/level1.py +++ b/qiskit/transpiler/preset_passmanagers/level1.py @@ -53,7 +53,7 @@ def level_1_pass_manager(pass_manager_config: PassManagerConfig) -> StagedPassMa # based on whether the input circuit has control flow. layout_method = pass_manager_config.layout_method or "default" routing_method = pass_manager_config.routing_method or "sabre" - translation_method = pass_manager_config.translation_method or "translator" + translation_method = pass_manager_config.translation_method or "default" optimization_method = pass_manager_config.optimization_method or "default" scheduling_method = pass_manager_config.scheduling_method or "default" target = pass_manager_config.target diff --git a/qiskit/transpiler/preset_passmanagers/level2.py b/qiskit/transpiler/preset_passmanagers/level2.py index b22743883fbe..4a438fe32037 100644 --- a/qiskit/transpiler/preset_passmanagers/level2.py +++ b/qiskit/transpiler/preset_passmanagers/level2.py @@ -54,7 +54,7 @@ def level_2_pass_manager(pass_manager_config: PassManagerConfig) -> StagedPassMa init_method = pass_manager_config.init_method or "default" layout_method = pass_manager_config.layout_method or "default" routing_method = pass_manager_config.routing_method or "sabre" - translation_method = pass_manager_config.translation_method or "translator" + translation_method = pass_manager_config.translation_method or "default" optimization_method = pass_manager_config.optimization_method or "default" scheduling_method = pass_manager_config.scheduling_method or "default" target = pass_manager_config.target diff --git a/qiskit/transpiler/preset_passmanagers/level3.py b/qiskit/transpiler/preset_passmanagers/level3.py index c1393a0b7c6e..a67737ed24f9 100644 --- a/qiskit/transpiler/preset_passmanagers/level3.py +++ b/qiskit/transpiler/preset_passmanagers/level3.py @@ -54,7 +54,7 @@ def level_3_pass_manager(pass_manager_config: PassManagerConfig) -> StagedPassMa init_method = pass_manager_config.init_method or "default" layout_method = pass_manager_config.layout_method or "default" routing_method = pass_manager_config.routing_method or "sabre" - translation_method = pass_manager_config.translation_method or "translator" + translation_method = pass_manager_config.translation_method or "default" scheduling_method = pass_manager_config.scheduling_method optimization_method = pass_manager_config.optimization_method or "default" scheduling_method = pass_manager_config.scheduling_method or "default" diff --git a/qiskit/transpiler/preset_passmanagers/plugin.py b/qiskit/transpiler/preset_passmanagers/plugin.py index 3ac1c4545fd5..26732c2343c6 100644 --- a/qiskit/transpiler/preset_passmanagers/plugin.py +++ b/qiskit/transpiler/preset_passmanagers/plugin.py @@ -71,7 +71,7 @@ * - :ref:`translation ` - ``qiskit.transpiler.translation`` - - ``translator``, ``synthesis`` + - ``default``, ``translator``, ``synthesis`` - Rewrite all gates outside the target ISA to use only gates within the ISA. * - :ref:`optimization ` diff --git a/releasenotes/notes/default-translation-stage-9d0335e354751af0.yaml b/releasenotes/notes/default-translation-stage-9d0335e354751af0.yaml new file mode 100644 index 000000000000..bc5d6202a249 --- /dev/null +++ b/releasenotes/notes/default-translation-stage-9d0335e354751af0.yaml @@ -0,0 +1,18 @@ +--- +features_transpiler: + - | + A new :ref:`translation plugin stage ` is added, called + ``"default"``. In Qiskit 2.0, this is simply an alias for the previous default ``"translator"``. + The underlying default algorithm may change over the course of the Qiskit 2.x series for some or + all targets, but you can always set ``translation_method=="translator"`` explicitly to maintain + the current behavior. +upgrade_transpiler: + - | + The :ref:`translation plugin stage ` name ``default`` is + now reserved for the Qiskit built-in plugin of the same name. + - | + The default :ref:`translation plugin stage ` is now + ``"default"``. In Qiskit 2.0, this is simply an alias for the previous default ``"translator"``. + The underlying default algorithm may change over the course of the Qiskit 2.x series for some or + all targets, but you can always set ``translation_method=="translator"`` explicitly to maintain + the current behavior. From 3e947a72b1f449b3b4a40f3c526494e9eb9fe2af Mon Sep 17 00:00:00 2001 From: Julien Gacon Date: Thu, 6 Feb 2025 13:08:59 +0100 Subject: [PATCH 10/48] there shall be silence (#13797) --- crates/circuit/src/packed_instruction.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/crates/circuit/src/packed_instruction.rs b/crates/circuit/src/packed_instruction.rs index 6ae8f1763395..9a5d10ae212b 100644 --- a/crates/circuit/src/packed_instruction.rs +++ b/crates/circuit/src/packed_instruction.rs @@ -821,11 +821,9 @@ impl PackedInstruction { } // Handle the case we end up with a pyinstruction for a standard instruction (OperationRef::StandardInstruction(_left), OperationRef::Instruction(right)) => { - println!("RHS is just instruction..."); self.unpack_py_op(py)?.bind(py).eq(&right.instruction) } (OperationRef::Instruction(left), OperationRef::StandardInstruction(_right)) => { - println!("LHS is just instruction..."); other.unpack_py_op(py)?.bind(py).eq(&left.instruction) } _ => Ok(false), From 9ba33ccebd5f5d8d9eb3577544271db91d9783aa Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Thu, 6 Feb 2025 13:17:06 +0000 Subject: [PATCH 11/48] Deny use of `println!` and related write macros (#13800) We lint against the `print` builtin in Python space, since it's a debugging artifact almost all of the time. This is the equivalent lint, but for Rust space. --- Cargo.toml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Cargo.toml b/Cargo.toml index 4c898a2710ba..2a3b14c53cc4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -49,6 +49,10 @@ qiskit-qasm3 = { path = "crates/qasm3" } # which uses the `::std::cmp::Ordering` enum as a return. Both styles are acceptable, and the `if` # chain can be more legible to people. comparison-chain = "allow" +# Forbid `{,e}print{,ln}!` calls. These can be allowed locally if absolutely required, but the +# vast majority of these are debug statements that we forget about. +print_stdout = "deny" +print_stderr = "deny" [workspace.lints.rust] # In Rust 2021, the bodies of `unsafe fn` may use `unsafe` functions themselves without marking From cd6e020656d4726f90a417eb865e532cab93a94a Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Thu, 6 Feb 2025 14:27:03 +0000 Subject: [PATCH 12/48] Ensure `translation` stage always outputs ISA circuits (#13792) This shifts the responsibility of the `translation` stage slightly to always output ISA circuits. It previously was not 100% required that they did this, at least implicitly because Qiskit's built-in plugins didn't always respect 2q direction. The builtin translation plugins now always respect 2q direction, which removes the need for the `pre_optimization` implicit stage, freeing it up for better user customisation. This (in theory) shouldn't have runtime impacts because the optimisation loop was already having to do this afterwards anyway. For potential plugins that _do_ respect gate direction (like the upcoming `BasisConstructor`), it can be a speedup in the default pipeline, since they won't need to run the gate-direction check any more. --- qiskit/transpiler/__init__.py | 9 ---- .../transpiler/preset_passmanagers/common.py | 47 ++++++++++++++++++- .../transpiler/preset_passmanagers/level0.py | 9 ---- .../transpiler/preset_passmanagers/level1.py | 10 ---- .../transpiler/preset_passmanagers/level2.py | 10 ---- .../transpiler/preset_passmanagers/level3.py | 9 ---- ...ranslation-direction-40059e267f77e178.yaml | 39 +++++++++++++++ 7 files changed, 84 insertions(+), 49 deletions(-) create mode 100644 releasenotes/notes/translation-direction-40059e267f77e178.yaml diff --git a/qiskit/transpiler/__init__.py b/qiskit/transpiler/__init__.py index 136dfb70d766..29739e054a14 100644 --- a/qiskit/transpiler/__init__.py +++ b/qiskit/transpiler/__init__.py @@ -615,15 +615,6 @@ the ISA only contains a ``cz`` operation on those qubits, the translation stage must find a way of representing the ``cx`` gate using the ``cz`` and available one-qubit gates. -.. note:: - - In Qiskit 1.x, translation plugins need not output gates with the correct - directionality, provided the gate exists with opposite directionality on the given qubit pair. - For example, if ``cx(0, 1)`` is ISA-supported, the translation stage can output - ``cx(1, 0)``. - - This is likely to change in later versions of Qiskit. - The translation stage is called before entering the optimization stage. Optimization plugins (including Qiskit's built-in plugins) may also use the translation stage as a "fixup" stage after the optimization loop, if the optimization loop returns a circuit that includes non-ISA gates. This diff --git a/qiskit/transpiler/preset_passmanagers/common.py b/qiskit/transpiler/preset_passmanagers/common.py index 4f360cd1998f..e3cd76bc9a47 100644 --- a/qiskit/transpiler/preset_passmanagers/common.py +++ b/qiskit/transpiler/preset_passmanagers/common.py @@ -52,6 +52,7 @@ from qiskit.transpiler.passes.layout.vf2_post_layout import VF2PostLayoutStopReason from qiskit.transpiler.exceptions import TranspilerError from qiskit.transpiler.layout import Layout +from qiskit.utils import deprecate_func from qiskit.utils.deprecate_pulse import deprecate_pulse_arg @@ -381,11 +382,19 @@ def filter_fn(node): return routing +@deprecate_func( + since="2.0", + additional_msg=( + "Translation plugins are now required to respect ISA directionality," + " so typically no replacement is necessary." + ), + removal_timeline="in Qiskit 3.0", +) def generate_pre_op_passmanager(target=None, coupling_map=None, remove_reset_in_zero=False): """Generate a pre-optimization loop :class:`~qiskit.transpiler.PassManager` This pass manager will check to ensure that directionality from the coupling - map is respected + map is respected. Args: target (Target): the :class:`~.Target` object representing the backend @@ -460,6 +469,7 @@ def generate_translation_passmanager( TranspilerError: If the ``method`` kwarg is not a valid value """ if method == "translator": + translator = BasisTranslator(sel, basis_gates, target) unroll = [ # Use unitary synthesis for basis aware decomposition of # UnitaryGates before custom unrolling @@ -481,8 +491,9 @@ def generate_translation_passmanager( basis_gates=basis_gates, qubits_initially_zero=qubits_initially_zero, ), - BasisTranslator(sel, basis_gates, target), + translator, ] + fix_1q = [translator] elif method == "synthesis": unroll = [ # # Use unitary synthesis for basis aware decomposition of @@ -530,8 +541,40 @@ def generate_translation_passmanager( qubits_initially_zero=qubits_initially_zero, ), ] + fix_1q = [ + Collect1qRuns(), + ConsolidateBlocks( + basis_gates=basis_gates, target=target, approximation_degree=approximation_degree + ), + UnitarySynthesis( + basis_gates=basis_gates, + approximation_degree=approximation_degree, + coupling_map=coupling_map, + backend_props=backend_props, + plugin_config=unitary_synthesis_plugin_config, + method=unitary_synthesis_method, + target=target, + ), + ] else: raise TranspilerError(f"Invalid translation method {method}.") + # Our built-ins don't 100% guarantee that 2q gate direction is respected, so we might need to + # run a little bit of fix up on them. `GateDirection` doesn't guarantee that 1q gates are + # ISA safe after it runs, so we need another run too. + if (coupling_map and not coupling_map.is_symmetric) or ( + target is not None and target.get_non_global_operation_names(strict_direction=True) + ): + unroll.append(CheckGateDirection(coupling_map, target=target)) + + def _direction_condition(property_set): + return not property_set["is_direction_mapped"] + + unroll.append( + ConditionalController( + [GateDirection(coupling_map, target=target)] + fix_1q, + condition=_direction_condition, + ) + ) return PassManager(unroll) diff --git a/qiskit/transpiler/preset_passmanagers/level0.py b/qiskit/transpiler/preset_passmanagers/level0.py index 865cc13f2407..9f311cc12496 100644 --- a/qiskit/transpiler/preset_passmanagers/level0.py +++ b/qiskit/transpiler/preset_passmanagers/level0.py @@ -73,14 +73,6 @@ def level_0_pass_manager(pass_manager_config: PassManagerConfig) -> StagedPassMa "translation", translation_method, pass_manager_config, optimization_level=0 ) - if (coupling_map and not coupling_map.is_symmetric) or ( - target is not None and target.get_non_global_operation_names(strict_direction=True) - ): - pre_opt = common.generate_pre_op_passmanager(target, coupling_map) - pre_opt += translation - else: - pre_opt = None - sched = plugin_manager.get_passmanager_stage( "scheduling", scheduling_method, pass_manager_config, optimization_level=0 ) @@ -107,7 +99,6 @@ def level_0_pass_manager(pass_manager_config: PassManagerConfig) -> StagedPassMa layout=layout, routing=routing, translation=translation, - pre_optimization=pre_opt, optimization=optimization, scheduling=sched, ) diff --git a/qiskit/transpiler/preset_passmanagers/level1.py b/qiskit/transpiler/preset_passmanagers/level1.py index e665b9f31186..8968364175a2 100644 --- a/qiskit/transpiler/preset_passmanagers/level1.py +++ b/qiskit/transpiler/preset_passmanagers/level1.py @@ -78,15 +78,6 @@ def level_1_pass_manager(pass_manager_config: PassManagerConfig) -> StagedPassMa "translation", translation_method, pass_manager_config, optimization_level=1 ) - if (coupling_map and not coupling_map.is_symmetric) or ( - target is not None and target.get_non_global_operation_names(strict_direction=True) - ): - pre_optimization = common.generate_pre_op_passmanager( - target, coupling_map, remove_reset_in_zero=False - ) - else: - pre_optimization = common.generate_pre_op_passmanager(remove_reset_in_zero=False) - optimization = plugin_manager.get_passmanager_stage( "optimization", optimization_method, pass_manager_config, optimization_level=1 ) @@ -114,7 +105,6 @@ def level_1_pass_manager(pass_manager_config: PassManagerConfig) -> StagedPassMa layout=layout, routing=routing, translation=translation, - pre_optimization=pre_optimization, optimization=optimization, scheduling=sched, ) diff --git a/qiskit/transpiler/preset_passmanagers/level2.py b/qiskit/transpiler/preset_passmanagers/level2.py index 4a438fe32037..d09038d536b2 100644 --- a/qiskit/transpiler/preset_passmanagers/level2.py +++ b/qiskit/transpiler/preset_passmanagers/level2.py @@ -77,15 +77,6 @@ def level_2_pass_manager(pass_manager_config: PassManagerConfig) -> StagedPassMa "translation", translation_method, pass_manager_config, optimization_level=2 ) - if (coupling_map and not coupling_map.is_symmetric) or ( - target is not None and target.get_non_global_operation_names(strict_direction=True) - ): - pre_optimization = common.generate_pre_op_passmanager( - target, coupling_map, remove_reset_in_zero=False - ) - else: - pre_optimization = common.generate_pre_op_passmanager(remove_reset_in_zero=False) - optimization = plugin_manager.get_passmanager_stage( "optimization", optimization_method, pass_manager_config, optimization_level=2 ) @@ -113,7 +104,6 @@ def level_2_pass_manager(pass_manager_config: PassManagerConfig) -> StagedPassMa layout=layout, routing=routing, translation=translation, - pre_optimization=pre_optimization, optimization=optimization, scheduling=sched, ) diff --git a/qiskit/transpiler/preset_passmanagers/level3.py b/qiskit/transpiler/preset_passmanagers/level3.py index a67737ed24f9..ab01cc95bbf0 100644 --- a/qiskit/transpiler/preset_passmanagers/level3.py +++ b/qiskit/transpiler/preset_passmanagers/level3.py @@ -94,14 +94,6 @@ def level_3_pass_manager(pass_manager_config: PassManagerConfig) -> StagedPassMa optimization = plugin_manager.get_passmanager_stage( "optimization", optimization_method, pass_manager_config, optimization_level=3 ) - if (coupling_map and not coupling_map.is_symmetric) or ( - target is not None and target.get_non_global_operation_names(strict_direction=True) - ): - pre_optimization = common.generate_pre_op_passmanager( - target, coupling_map, remove_reset_in_zero=False - ) - else: - pre_optimization = common.generate_pre_op_passmanager(remove_reset_in_zero=False) sched = plugin_manager.get_passmanager_stage( "scheduling", scheduling_method, pass_manager_config, optimization_level=3 @@ -113,7 +105,6 @@ def level_3_pass_manager(pass_manager_config: PassManagerConfig) -> StagedPassMa layout=layout, routing=routing, translation=translation, - pre_optimization=pre_optimization, optimization=optimization, scheduling=sched, ) diff --git a/releasenotes/notes/translation-direction-40059e267f77e178.yaml b/releasenotes/notes/translation-direction-40059e267f77e178.yaml new file mode 100644 index 000000000000..146c1e508587 --- /dev/null +++ b/releasenotes/notes/translation-direction-40059e267f77e178.yaml @@ -0,0 +1,39 @@ +--- +upgrade_transpiler: + - | + Plugins for the :ref:`translation stage of the preset compiler ` + are now required to respect gate directionality in the :class:`.Target` in their output. + Previously, :func:`.transpile` and :func:`.generate_preset_pass_manager` would generate a + :class:`.PassManager` that contained fix-up passes if needed. You must now include these in + your own custom stage, if your stage does not guarantee that it respects directionality. + + You can use the :class:`.GateDirection` pass to perform the same fix-ups that Qiskit used to do. + For example:: + + from qiskit.transpiler import PassManager + from qiskit.transpiler.passes import GateDirection + from qiskit.transpiler.preset_passmanagers.plugin import PassManagerStagePlugin + + class YourTranslationPlugin(PassManagerStagePlugin): + def pass_manager(self, pass_manager_config, optimization_level): + pm = PassManager([ + # ... whatever your current setup is ... + ]) + # Add the two-qubit directionality-fixing pass. + pm.append(GateDirection( + pass_manager_config.coupling_map, + pass_manager_config.target, + )) + return pm + - | + The :ref:`preset pass managers ` no longer populate the implicit ``pre_optimization`` + stage of their output :class:`.StagedPassManager`. You can now safely assign your own + :class:`.PassManager` to this field. You could previously only append to the existing + :class:`.PassManager`. +deprecations_transpiler: + - | + The function :func:`.generate_pre_op_passmanager` is deprecated. It is no longer used in the + Qiskit preset pass managers, and its purpose is defunct; it originally generated a fix-up stage + for translation plugins that did not respect ISA directionality. Translation stages are now + required to respect directionality, so the functionality is not needed, and most likely, + no replacement is required. From 78e6d3dde848860d2d423b65deb4286d6831f612 Mon Sep 17 00:00:00 2001 From: Julien Gacon Date: Thu, 6 Feb 2025 17:07:25 +0100 Subject: [PATCH 13/48] Fix ``CommutationChecker`` for 2q Pauli rotations (#13762) * Fix ``CommutationChecker`` for 2q rotations + non-cached gates * add reno * compensate for the inability to copy+paste --- crates/accelerate/src/commutation_checker.rs | 19 +++++++++++-------- ...ommchecker-2q-paulis-bcadef25247c7288.yaml | 16 ++++++++++++++++ .../circuit/test_commutation_checker.py | 16 ++++++++++++++++ .../test_commutative_inverse_cancellation.py | 17 ++++++++++++++++- 4 files changed, 59 insertions(+), 9 deletions(-) create mode 100644 releasenotes/notes/fix-commchecker-2q-paulis-bcadef25247c7288.yaml diff --git a/crates/accelerate/src/commutation_checker.rs b/crates/accelerate/src/commutation_checker.rs index 6484c562c2dd..55af5c72eeb9 100644 --- a/crates/accelerate/src/commutation_checker.rs +++ b/crates/accelerate/src/commutation_checker.rs @@ -354,13 +354,17 @@ impl CommutationChecker { // For our cache to work correctly, we require the gate's definition to only depend on the // ``params`` attribute. This cannot be guaranteed for custom gates, so we only check - // the cache for our standard gates, which we know are defined by the ``params`` AND - // that the ``params`` are float-only at this point. - let whitelist = get_standard_gate_names(); - let check_cache = whitelist.contains(&first_op.name()) - && whitelist.contains(&second_op.name()) - && first_params.iter().all(|p| matches!(p, Param::Float(_))) - && second_params.iter().all(|p| matches!(p, Param::Float(_))); + // the cache for + // * gates we know are in the cache (SUPPORTED_OPS), or + // * standard gates with float params (otherwise we cannot cache them) + let standard_gates = get_standard_gate_names(); + let is_cachable = |name: &str, params: &[Param]| { + SUPPORTED_OP.contains(name) + || (standard_gates.contains(&name) + && params.iter().all(|p| matches!(p, Param::Float(_)))) + }; + let check_cache = is_cachable(first_op.name(), first_params) + && is_cachable(second_op.name(), second_params); if !check_cache { return self.commute_matmul( @@ -681,7 +685,6 @@ fn map_rotation<'a>( if let Some(gate) = generator { return (gate, &[], false); }; - return (op, &[], false); } (op, params, false) } diff --git a/releasenotes/notes/fix-commchecker-2q-paulis-bcadef25247c7288.yaml b/releasenotes/notes/fix-commchecker-2q-paulis-bcadef25247c7288.yaml new file mode 100644 index 000000000000..7ad0624aa9cf --- /dev/null +++ b/releasenotes/notes/fix-commchecker-2q-paulis-bcadef25247c7288.yaml @@ -0,0 +1,16 @@ +--- +fixes: + - | + Fixed a bug in the :class:`.CommutationChecker` which could fail upon checking the commutation + relation of a two-qubit Pauli rotation with a gate that is not in the commutation cache. + For example:: + + import numpy as np + from qiskit.circuit.library import RXXGate, RGate + from qiskit.circuit.commutation_library import SessionCommutationChecker as scc + + res = scc.commute(RGate(2, 2), [1], [], RXXGate(np.pi / 2), [0, 1], []) + + This behavior is now resolved and the commutation relation correctly computed. + Fixed `#13742 `__. + diff --git a/test/python/circuit/test_commutation_checker.py b/test/python/circuit/test_commutation_checker.py index d0a7fc8eeb47..057b2ce17d60 100644 --- a/test/python/circuit/test_commutation_checker.py +++ b/test/python/circuit/test_commutation_checker.py @@ -45,6 +45,7 @@ PauliGate, PhaseGate, Reset, + RGate, RXGate, RXXGate, RYGate, @@ -57,6 +58,7 @@ ZGate, HGate, UnitaryGate, + UGate, ) from qiskit.dagcircuit import DAGOpNode @@ -483,6 +485,20 @@ def test_nonfloat_param(self): self.assertTrue(scc.commute(pauli_gate, [0, 1], [], rx_gate_theta, [0], [])) self.assertTrue(scc.commute(rx_gate_theta, [0], [], pauli_gate, [0, 1], [])) + def test_2q_pauli_rot_with_non_cached(self): + """Test the 2q-Pauli rotations with a gate that is not cached.""" + x_equiv = UGate(np.pi, -np.pi / 2, np.pi / 2) + self.assertTrue(scc.commute(x_equiv, [0], [], RXXGate(np.pi / 2), [0, 1], [])) + self.assertTrue(scc.commute(x_equiv, [1], [], RXXGate(np.pi / 2), [0, 1], [])) + self.assertFalse(scc.commute(x_equiv, [0], [], RYYGate(np.pi), [1, 0], [])) + self.assertFalse(scc.commute(x_equiv, [1], [], RYYGate(np.pi), [1, 0], [])) + + something_else = RGate(1, 2) + self.assertFalse(scc.commute(something_else, [0], [], RXXGate(np.pi / 2), [0, 1], [])) + self.assertFalse(scc.commute(something_else, [1], [], RXXGate(np.pi / 2), [0, 1], [])) + self.assertFalse(scc.commute(something_else, [0], [], RYYGate(np.pi), [1, 0], [])) + self.assertFalse(scc.commute(something_else, [1], [], RYYGate(np.pi), [1, 0], [])) + if __name__ == "__main__": unittest.main() diff --git a/test/python/transpiler/test_commutative_inverse_cancellation.py b/test/python/transpiler/test_commutative_inverse_cancellation.py index e9e85c5505f3..763b5acd0a15 100644 --- a/test/python/transpiler/test_commutative_inverse_cancellation.py +++ b/test/python/transpiler/test_commutative_inverse_cancellation.py @@ -19,7 +19,7 @@ from ddt import data, ddt from qiskit.circuit import Parameter, QuantumCircuit -from qiskit.circuit.library import RZGate, UnitaryGate +from qiskit.circuit.library import RZGate, UnitaryGate, U2Gate from qiskit.quantum_info import Operator from qiskit.transpiler import PassManager from qiskit.transpiler.passes import CommutativeInverseCancellation @@ -890,6 +890,21 @@ def test_max_qubits(self): new_circuit = passmanager.run(circuit) self.assertEqual(circuit, new_circuit) + def test_2q_pauli_rot_with_non_cached(self): + """Test a cached 2q-Pauli rotation with a non-cached gate. + + Regression test of #13742. + """ + circuit = QuantumCircuit(2) + circuit.rxx(np.pi / 2, 1, 0) + circuit.append(U2Gate(np.pi / 2, -np.pi), [1]) + + pm = PassManager(CommutativeInverseCancellation()) + tqc = pm.run(circuit) + + self.assertEqual(tqc.count_ops().get("u2", 0), 1) + self.assertEqual(tqc.count_ops().get("rxx", 0), 1) + if __name__ == "__main__": unittest.main() From 5c0f255c7cb3ebfd3d3b06950dc304fe307fef2e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 6 Feb 2025 18:12:53 +0000 Subject: [PATCH 14/48] Bump bitfield-struct from 0.9.3 to 0.9.5 (#13799) Bumps [bitfield-struct](https://github.com/wrenger/bitfield-struct-rs) from 0.9.3 to 0.9.5. - [Release notes](https://github.com/wrenger/bitfield-struct-rs/releases) - [Commits](wrenger/bitfield-struct-rs@0.9.3...0.9.5) --- updated-dependencies: - dependency-name: bitfield-struct dependency-type: direct:production update-type: version-update:semver-patch ... Co-authored-by: Jake Lishman --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 40f083bc5469..4be827893a45 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -79,9 +79,9 @@ checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" [[package]] name = "bitfield-struct" -version = "0.9.3" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d7a33e7b9505a52e33ed0ad66db6434f18cda0b1c72665fabf14e85cdd39e43" +checksum = "b2869c63ccf4f8bf0d485070b880e60e097fb7aeea80ee82a0a94a957e372a0b" dependencies = [ "proc-macro2", "quote", From c4d375c2f7bdeda89355e3ab2286a7c2b5a6304a Mon Sep 17 00:00:00 2001 From: Julien Gacon Date: Fri, 7 Feb 2025 13:02:04 +0100 Subject: [PATCH 15/48] Fix the sign in `SparseObservable.as_paulis` (#13803) * Fix sign Co-authored-by: Jake Lishman * add tests * fix reverse order and add another test -- -- just for the sake of it --------- Co-authored-by: Jake Lishman --- crates/accelerate/src/sparse_observable.rs | 6 +- .../quantum_info/test_sparse_observable.py | 64 +++++++++++++++++++ 2 files changed, 67 insertions(+), 3 deletions(-) diff --git a/crates/accelerate/src/sparse_observable.rs b/crates/accelerate/src/sparse_observable.rs index 6f25b8a22cfa..177ce64e04f6 100644 --- a/crates/accelerate/src/sparse_observable.rs +++ b/crates/accelerate/src/sparse_observable.rs @@ -687,10 +687,10 @@ impl SparseObservable { .multi_cartesian_product(); for combination in combinations { - let mut positive = true; + let mut positive = true; // keep track of the global sign for (index, (sign, bit)) in combination.iter().enumerate() { - positive &= sign; + positive ^= !sign; // accumulate the sign; global_sign *= local_sign if let Some(bit) = bit { paulis.push(*bit); indices.push(view.indices[index]); @@ -2481,7 +2481,7 @@ impl PySparseObservable { /// list and back. /// /// Examples: - /// + /// /// >>> obs = SparseObservable.from_list([("IIXIZ", 2j), ("IIZIX", 2j)]) /// >>> reconstructed = SparseObservable.from_sparse_list(obs.to_sparse_list(), obs.num_qubits) /// diff --git a/test/python/quantum_info/test_sparse_observable.py b/test/python/quantum_info/test_sparse_observable.py index 3c4d1c999360..720bb18af46f 100644 --- a/test/python/quantum_info/test_sparse_observable.py +++ b/test/python/quantum_info/test_sparse_observable.py @@ -2055,6 +2055,70 @@ def test_as_paulis(self): obs_paulis = obs.as_paulis() self.assertEqual(obs, obs_paulis) + # test multiple +1 projectors + with self.subTest(msg="00"): + obs = SparseObservable("00") + obs_paulis = obs.as_paulis() + expected = SparseObservable.from_sparse_list( + [ + ("", [], 1 / 4), + ("Z", [0], 1 / 4), + ("Z", [1], 1 / 4), + ("ZZ", [0, 1], 1 / 4), + ], + 2, + ) + self.assertEqual(expected.simplify(), obs_paulis.simplify()) + + # test multiple -1 projectors + with self.subTest(msg="11"): + obs = SparseObservable("11") + obs_paulis = obs.as_paulis() + expected = SparseObservable.from_sparse_list( + [ + ("", [], 1 / 4), + ("Z", [0], -1 / 4), + ("Z", [1], -1 / 4), + ("ZZ", [0, 1], 1 / 4), + ], + 2, + ) + self.assertEqual(expected.simplify(), obs_paulis.simplify()) + + # test +1 -1 projector + with self.subTest(msg="01"): + obs = SparseObservable("01") + obs_paulis = obs.as_paulis() + expected = SparseObservable.from_sparse_list( + [ + ("", [], 1 / 4), + ("Z", [0], -1 / 4), + ("Z", [1], 1 / 4), + ("ZZ", [0, 1], -1 / 4), + ], + 2, + ) + self.assertEqual(expected.simplify(), obs_paulis.simplify()) + + # test multiple negative projectors with a positive + with self.subTest(msg="011"): + obs = SparseObservable("011") + obs_paulis = obs.as_paulis() + expected = SparseObservable.from_sparse_list( + [ + ("", [], 1 / 8), + ("Z", [0], -1 / 8), + ("Z", [1], -1 / 8), + ("Z", [2], 1 / 8), + ("ZZ", [0, 1], 1 / 8), + ("ZZ", [0, 2], -1 / 8), + ("ZZ", [1, 2], -1 / 8), + ("ZZZ", [0, 1, 2], 1 / 8), + ], + 3, + ) + self.assertEqual(expected.simplify(), obs_paulis.simplify()) + # test explicitly on written-out projector with self.subTest(msg="lrI0"): obs = SparseObservable("lrI0") From d05ee47647f5b3c8718b81e0ac51e58635c193ac Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Fri, 7 Feb 2025 12:51:46 +0000 Subject: [PATCH 16/48] Remove redundant `CODEOWNERS` entries (#13807) Shelly and Sasha are both part of terra-core. Rules that "specialise" a directory to terra-core are redundant, since the blanket rule catches that case. Naoki is moved from being terra-core to instead being a specialised code-owner of the pulse and scheduling sections, which is more representative of the current reality. --- .github/CODEOWNERS | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index aa3b08be0a2d..c0868cbd5bde 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -24,11 +24,8 @@ # Qiskit folders (also their corresponding tests) providers/ @Qiskit/terra-core @jyu00 -quantum_info/ @Qiskit/terra-core -qpy/ @Qiskit/terra-core -pulse/ @Qiskit/terra-core @eggerdj @wshanks -synthesis/ @Qiskit/terra-core @alexanderivrii @ShellyGarion -scheduler/ @Qiskit/terra-core @eggerdj @wshanks +pulse/ @Qiskit/terra-core @eggerdj @wshanks @nkanazawa1989 +scheduler/ @Qiskit/terra-core @eggerdj @wshanks @nkanazawa1989 visualization/ @Qiskit/terra-core @nonhermitian primitives/ @Qiskit/terra-core @Qiskit/qiskit-primitives # Override the release notes directories to have _no_ code owners, so any review From f2234fcfa12149daea06d0979d38ab5944c14f65 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elena=20Pe=C3=B1a=20Tapia?= <57907331+ElePT@users.noreply.github.com> Date: Fri, 7 Feb 2025 14:43:37 +0100 Subject: [PATCH 17/48] Remove use of `BackendProperties` (BackendV1) in `UnitarySynthesis` pass (#13706) * Remove BackendV1 from unitary synthesis unit tests * Remove backend_properties input * Remove backend_properties argument from generate_translation_passmanager * Say use target instead of backend in docs * Fix conflict --- .../passes/synthesis/unitary_synthesis.py | 85 ++----- .../preset_passmanagers/builtin_plugins.py | 4 - .../transpiler/preset_passmanagers/common.py | 7 - .../transpiler/test_unitary_synthesis.py | 223 ++++++++---------- 4 files changed, 118 insertions(+), 201 deletions(-) diff --git a/qiskit/transpiler/passes/synthesis/unitary_synthesis.py b/qiskit/transpiler/passes/synthesis/unitary_synthesis.py index a2bd044c7341..ab66b38bd993 100644 --- a/qiskit/transpiler/passes/synthesis/unitary_synthesis.py +++ b/qiskit/transpiler/passes/synthesis/unitary_synthesis.py @@ -55,7 +55,6 @@ from qiskit.dagcircuit.dagcircuit import DAGCircuit from qiskit.dagcircuit.dagnode import DAGOpNode from qiskit.exceptions import QiskitError -from qiskit.providers.models.backendproperties import BackendProperties from qiskit.quantum_info import Operator from qiskit.synthesis.one_qubit import one_qubit_decompose from qiskit.synthesis.two_qubit.xx_decompose import XXDecomposer, XXEmbodiments @@ -129,7 +128,7 @@ def _find_matching_euler_bases(target, qubit): def _choose_bases(basis_gates, basis_dict=None): - """Find the matching basis string keys from the list of basis gates from the backend.""" + """Find the matching basis string keys from the list of basis gates from the target.""" if basis_gates is None: basis_set = set() else: @@ -320,7 +319,6 @@ def __init__( basis_gates: list[str] = None, approximation_degree: float | None = 1.0, coupling_map: CouplingMap = None, - backend_props: BackendProperties = None, pulse_optimize: bool | None = None, natural_direction: bool | None = None, synth_gates: list[str] | None = None, @@ -332,7 +330,7 @@ def __init__( """Synthesize unitaries over some basis gates. This pass can approximate 2-qubit unitaries given some - gate fidelities (either via ``backend_props`` or ``target``). + gate fidelities (via ``target``). More approximation can be forced by setting a heuristic dial ``approximation_degree``. @@ -345,13 +343,11 @@ def __init__( (1.0=no approximation, 0.0=maximal approximation). Approximation can make the synthesized circuit cheaper at the cost of straying from the original unitary. If None, approximation is done based on gate fidelities. - coupling_map (CouplingMap): the coupling map of the backend + coupling_map (CouplingMap): the coupling map of the target in case synthesis is done on a physical circuit. The directionality of the coupling_map will be taken into account if ``pulse_optimize`` is ``True``/``None`` and ``natural_direction`` is ``True``/``None``. - backend_props (BackendProperties): Properties of a backend to - synthesize for (e.g. gate fidelities). pulse_optimize (bool): Whether to optimize pulses during synthesis. A value of ``None`` will attempt it but fall back if it does not succeed. A value of ``True`` will raise @@ -363,7 +359,7 @@ def __init__( coupling map is unidirectional. If there is no coupling map or the coupling map is bidirectional, the gate direction with the shorter - duration from the backend properties will be used. If + duration from the target properties will be used. If set to True, and a natural direction can not be determined, raises :class:`.TranspilerError`. If set to None, no exception will be raised if a natural direction can @@ -383,7 +379,7 @@ def __init__( your unitary synthesis plugin on how to use this. target: The optional :class:`~.Target` for the target device the pass is compiling for. If specified this will supersede the values - set for ``basis_gates``, ``coupling_map``, and ``backend_props``. + set for ``basis_gates`` and ``coupling_map``. Raises: TranspilerError: if ``method`` was specified but is not found in the @@ -399,7 +395,6 @@ def __init__( if method != "default": self.plugins = plugin.UnitarySynthesisPluginManager() self._coupling_map = coupling_map - self._backend_props = backend_props self._pulse_optimize = pulse_optimize self._natural_direction = natural_direction self._plugin_config = plugin_config @@ -496,23 +491,19 @@ def run(self, dag: DAGCircuit) -> DAGCircuit: if method.supports_pulse_optimize: kwargs["pulse_optimize"] = self._pulse_optimize if method.supports_gate_lengths: - _gate_lengths = _gate_lengths or _build_gate_lengths( - self._backend_props, self._target - ) + _gate_lengths = _gate_lengths or _build_gate_lengths(self._target) kwargs["gate_lengths"] = _gate_lengths if method.supports_gate_errors: - _gate_errors = _gate_errors or _build_gate_errors( - self._backend_props, self._target - ) + _gate_errors = _gate_errors or _build_gate_errors(self._target) kwargs["gate_errors"] = _gate_errors if method.supports_gate_lengths_by_qubit: _gate_lengths_by_qubit = _gate_lengths_by_qubit or _build_gate_lengths_by_qubit( - self._backend_props, self._target + self._target ) kwargs["gate_lengths_by_qubit"] = _gate_lengths_by_qubit if method.supports_gate_errors_by_qubit: _gate_errors_by_qubit = _gate_errors_by_qubit or _build_gate_errors_by_qubit( - self._backend_props, self._target + self._target ) kwargs["gate_errors_by_qubit"] = _gate_errors_by_qubit supported_bases = method.supported_bases @@ -610,9 +601,8 @@ def _run_main_loop( return out_dag -def _build_gate_lengths(props=None, target=None): - """Builds a ``gate_lengths`` dictionary from either ``props`` (BackendV1) - or ``target`` (BackendV2). +def _build_gate_lengths(target=None): + """Builds a ``gate_lengths`` dictionary from ``target`` (BackendV2). The dictionary has the form: {gate_name: {(qubits,): duration}} @@ -624,21 +614,11 @@ def _build_gate_lengths(props=None, target=None): for qubit, gate_props in prop_dict.items(): if gate_props is not None and gate_props.duration is not None: gate_lengths[gate][qubit] = gate_props.duration - elif props is not None: - for gate in props._gates: - gate_lengths[gate] = {} - for k, v in props._gates[gate].items(): - length = v.get("gate_length") - if length: - gate_lengths[gate][k] = length[0] - if not gate_lengths[gate]: - del gate_lengths[gate] return gate_lengths -def _build_gate_errors(props=None, target=None): - """Builds a ``gate_error`` dictionary from either ``props`` (BackendV1) - or ``target`` (BackendV2). +def _build_gate_errors(target=None): + """Builds a ``gate_error`` dictionary from ``target`` (BackendV2). The dictionary has the form: {gate_name: {(qubits,): error_rate}} @@ -650,22 +630,12 @@ def _build_gate_errors(props=None, target=None): for qubit, gate_props in prop_dict.items(): if gate_props is not None and gate_props.error is not None: gate_errors[gate][qubit] = gate_props.error - if props is not None: - for gate in props._gates: - gate_errors[gate] = {} - for k, v in props._gates[gate].items(): - error = v.get("gate_error") - if error: - gate_errors[gate][k] = error[0] - if not gate_errors[gate]: - del gate_errors[gate] return gate_errors -def _build_gate_lengths_by_qubit(props=None, target=None): +def _build_gate_lengths_by_qubit(target=None): """ - Builds a `gate_lengths` dictionary from either `props` (BackendV1) - or `target (BackendV2)`. + Builds a `gate_lengths` dictionary from `target (BackendV2)`. The dictionary has the form: {(qubits): [Gate, duration]} @@ -682,23 +652,12 @@ def _build_gate_lengths_by_qubit(props=None, target=None): operation_and_durations.append((operation, duration)) if operation_and_durations: gate_lengths[qubits] = operation_and_durations - elif props is not None: - for gate_name, gate_props in props._gates.items(): - gate = GateNameToGate[gate_name] - for qubits, properties in gate_props.items(): - duration = properties.get("gate_length", [0.0])[0] - operation_and_durations = (gate, duration) - if qubits in gate_lengths: - gate_lengths[qubits].append(operation_and_durations) - else: - gate_lengths[qubits] = [operation_and_durations] return gate_lengths -def _build_gate_errors_by_qubit(props=None, target=None): +def _build_gate_errors_by_qubit(target=None): """ - Builds a `gate_error` dictionary from either `props` (BackendV1) - or `target (BackendV2)`. + Builds a `gate_error` dictionary from `target (BackendV2)`. The dictionary has the form: {(qubits): [Gate, error]} @@ -715,16 +674,6 @@ def _build_gate_errors_by_qubit(props=None, target=None): operation_and_errors.append((operation, error)) if operation_and_errors: gate_errors[qubits] = operation_and_errors - elif props is not None: - for gate_name, gate_props in props._gates.items(): - gate = GateNameToGate[gate_name] - for qubits, properties in gate_props.items(): - error = properties.get("gate_error", [0.0])[0] - operation_and_errors = (gate, error) - if qubits in gate_errors: - gate_errors[qubits].append(operation_and_errors) - else: - gate_errors[qubits] = [operation_and_errors] return gate_errors diff --git a/qiskit/transpiler/preset_passmanagers/builtin_plugins.py b/qiskit/transpiler/preset_passmanagers/builtin_plugins.py index 4742de6fda49..d1ffa8d3d1b0 100644 --- a/qiskit/transpiler/preset_passmanagers/builtin_plugins.py +++ b/qiskit/transpiler/preset_passmanagers/builtin_plugins.py @@ -218,7 +218,6 @@ def pass_manager(self, pass_manager_config, optimization_level=None) -> PassMana method="translator", approximation_degree=pass_manager_config.approximation_degree, coupling_map=pass_manager_config.coupling_map, - backend_props=pass_manager_config.backend_properties, unitary_synthesis_method=pass_manager_config.unitary_synthesis_method, unitary_synthesis_plugin_config=pass_manager_config.unitary_synthesis_plugin_config, hls_config=pass_manager_config.hls_config, @@ -236,7 +235,6 @@ def pass_manager(self, pass_manager_config, optimization_level=None) -> PassMana method="synthesis", approximation_degree=pass_manager_config.approximation_degree, coupling_map=pass_manager_config.coupling_map, - backend_props=pass_manager_config.backend_properties, unitary_synthesis_method=pass_manager_config.unitary_synthesis_method, unitary_synthesis_plugin_config=pass_manager_config.unitary_synthesis_plugin_config, hls_config=pass_manager_config.hls_config, @@ -617,7 +615,6 @@ def _opt_control(property_set): pass_manager_config.basis_gates, approximation_degree=pass_manager_config.approximation_degree, coupling_map=pass_manager_config.coupling_map, - backend_props=pass_manager_config.backend_properties, method=pass_manager_config.unitary_synthesis_method, plugin_config=pass_manager_config.unitary_synthesis_plugin_config, target=pass_manager_config.target, @@ -664,7 +661,6 @@ def _unroll_condition(property_set): pass_manager_config.basis_gates, approximation_degree=pass_manager_config.approximation_degree, coupling_map=pass_manager_config.coupling_map, - backend_props=pass_manager_config.backend_properties, method=pass_manager_config.unitary_synthesis_method, plugin_config=pass_manager_config.unitary_synthesis_plugin_config, target=pass_manager_config.target, diff --git a/qiskit/transpiler/preset_passmanagers/common.py b/qiskit/transpiler/preset_passmanagers/common.py index e3cd76bc9a47..8422ab209451 100644 --- a/qiskit/transpiler/preset_passmanagers/common.py +++ b/qiskit/transpiler/preset_passmanagers/common.py @@ -429,7 +429,6 @@ def generate_translation_passmanager( method="translator", approximation_degree=None, coupling_map=None, - backend_props=None, unitary_synthesis_method="default", unitary_synthesis_plugin_config=None, hls_config=None, @@ -452,8 +451,6 @@ def generate_translation_passmanager( unitary_synthesis_plugin_config (dict): The optional dictionary plugin configuration, this is plugin specific refer to the specified plugin's documentation for how to use. - backend_props (BackendProperties): Properties of a backend to - synthesize for (e.g. gate fidelities). unitary_synthesis_method (str): The unitary synthesis method to use. You can see a list of installed plugins with :func:`.unitary_synthesis_plugin_names`. hls_config (HLSConfig): An optional configuration class to use for @@ -477,7 +474,6 @@ def generate_translation_passmanager( basis_gates, approximation_degree=approximation_degree, coupling_map=coupling_map, - backend_props=backend_props, plugin_config=unitary_synthesis_plugin_config, method=unitary_synthesis_method, target=target, @@ -502,7 +498,6 @@ def generate_translation_passmanager( basis_gates, approximation_degree=approximation_degree, coupling_map=coupling_map, - backend_props=backend_props, plugin_config=unitary_synthesis_plugin_config, method=unitary_synthesis_method, min_qubits=3, @@ -527,7 +522,6 @@ def generate_translation_passmanager( basis_gates=basis_gates, approximation_degree=approximation_degree, coupling_map=coupling_map, - backend_props=backend_props, plugin_config=unitary_synthesis_plugin_config, method=unitary_synthesis_method, target=target, @@ -550,7 +544,6 @@ def generate_translation_passmanager( basis_gates=basis_gates, approximation_degree=approximation_degree, coupling_map=coupling_map, - backend_props=backend_props, plugin_config=unitary_synthesis_plugin_config, method=unitary_synthesis_method, target=target, diff --git a/test/python/transpiler/test_unitary_synthesis.py b/test/python/transpiler/test_unitary_synthesis.py index 767b126bbb3a..bc9e218e79cc 100644 --- a/test/python/transpiler/test_unitary_synthesis.py +++ b/test/python/transpiler/test_unitary_synthesis.py @@ -22,7 +22,7 @@ from ddt import ddt, data from qiskit import transpile, generate_preset_pass_manager -from qiskit.providers.fake_provider import Fake5QV1, GenericBackendV2 +from qiskit.providers.fake_provider import GenericBackendV2 from qiskit.circuit import QuantumCircuit, QuantumRegister, ClassicalRegister from qiskit.circuit.library import quantum_volume from qiskit.converters import circuit_to_dag, dag_to_circuit @@ -70,6 +70,7 @@ from test.python.providers.fake_mumbai_v2 import ( # pylint: disable=wrong-import-order FakeMumbaiFractionalCX, ) +from ..legacy_cmaps import YORKTOWN_CMAP class FakeBackend2QV2(GenericBackendV2): @@ -147,78 +148,18 @@ def test_two_qubit_synthesis_to_basis(self, basis_gates): out = UnitarySynthesis(basis_gates).run(dag) self.assertTrue(set(out.count_ops()).issubset(basis_gates)) - @combine(gate=["unitary", "swap"], natural_direction=[True, False]) - def test_two_qubit_synthesis_to_directional_cx(self, gate, natural_direction): - """Verify two qubit unitaries are synthesized to match basis gates.""" - # TODO: should make check more explicit e.g. explicitly set gate - # direction in test instead of using specific fake backend - with self.assertWarns(DeprecationWarning): - backend = Fake5QV1() - conf = backend.configuration() - coupling_map = CouplingMap(conf.coupling_map) - triv_layout_pass = TrivialLayout(coupling_map) - - qr = QuantumRegister(2) - qc = QuantumCircuit(qr) - if gate == "unitary": - qc.unitary(random_unitary(4, seed=12), [0, 1]) - elif gate == "swap": - qc.swap(qr[0], qr[1]) - - unisynth_pass = UnitarySynthesis( - basis_gates=conf.basis_gates, - coupling_map=None, - backend_props=backend.properties(), - pulse_optimize=True, - natural_direction=natural_direction, - ) - pm = PassManager([triv_layout_pass, unisynth_pass]) - qc_out = pm.run(qc) - self.assertEqual(Operator(qc), Operator(qc_out)) - - @data(True, False) - def test_two_qubit_synthesis_to_directional_cx_multiple_registers(self, natural_direction): - """Verify two qubit unitaries are synthesized to match basis gates - across multiple registers.""" - # TODO: should make check more explicit e.g. explicitly set gate - # direction in test instead of using specific fake backend - with self.assertWarns(DeprecationWarning): - backend = Fake5QV1() - conf = backend.configuration() - qr0 = QuantumRegister(1) - qr1 = QuantumRegister(1) - coupling_map = CouplingMap(conf.coupling_map) - triv_layout_pass = TrivialLayout(coupling_map) - qc = QuantumCircuit(qr0, qr1) - qc.unitary(random_unitary(4, seed=12), [qr0[0], qr1[0]]) - unisynth_pass = UnitarySynthesis( - basis_gates=conf.basis_gates, - coupling_map=None, - backend_props=backend.properties(), - pulse_optimize=True, - natural_direction=natural_direction, - ) - pm = PassManager([triv_layout_pass, unisynth_pass]) - qc_out = pm.run(qc) - self.assertEqual(Operator(qc), Operator(qc_out)) - @data(True, False, None) def test_two_qubit_synthesis_to_directional_cx_from_coupling_map(self, natural_direction): """Verify natural cx direction is used when specified in coupling map.""" - # TODO: should make check more explicit e.g. explicitly set gate - # direction in test instead of using specific fake backend - with self.assertWarns(DeprecationWarning): - backend = Fake5QV1() - conf = backend.configuration() + qr = QuantumRegister(2) coupling_map = CouplingMap([[0, 1], [1, 2], [1, 3], [3, 4]]) triv_layout_pass = TrivialLayout(coupling_map) qc = QuantumCircuit(qr) qc.unitary(random_unitary(4, seed=12), [0, 1]) unisynth_pass = UnitarySynthesis( - basis_gates=conf.basis_gates, + basis_gates=["id", "rz", "sx", "x", "cx", "reset"], coupling_map=coupling_map, - backend_props=backend.properties(), pulse_optimize=True, natural_direction=natural_direction, ) @@ -239,9 +180,7 @@ def test_two_qubit_synthesis_to_directional_cx_from_coupling_map(self, natural_d def test_two_qubit_synthesis_not_pulse_optimal(self): """Verify not attempting pulse optimal decomposition when pulse_optimize==False.""" - with self.assertWarns(DeprecationWarning): - backend = Fake5QV1() - conf = backend.configuration() + qr = QuantumRegister(2) qc = QuantumCircuit(qr) qc.unitary(random_unitary(4, seed=12), [0, 1]) @@ -250,9 +189,8 @@ def test_two_qubit_synthesis_not_pulse_optimal(self): [ TrivialLayout(coupling_map), UnitarySynthesis( - basis_gates=conf.basis_gates, + basis_gates=["id", "rz", "sx", "x", "cx", "reset"], coupling_map=coupling_map, - backend_props=backend.properties(), pulse_optimize=False, natural_direction=True, ), @@ -262,9 +200,8 @@ def test_two_qubit_synthesis_not_pulse_optimal(self): [ TrivialLayout(coupling_map), UnitarySynthesis( - basis_gates=conf.basis_gates, + basis_gates=["id", "rz", "sx", "x", "cx", "reset"], coupling_map=coupling_map, - backend_props=backend.properties(), pulse_optimize=True, natural_direction=True, ), @@ -275,21 +212,18 @@ def test_two_qubit_synthesis_not_pulse_optimal(self): self.assertGreater(qc_nonoptimal.count_ops()["sx"], qc_optimal.count_ops()["sx"]) def test_two_qubit_pulse_optimal_true_raises(self): - """Verify raises if pulse optimal==True but cx is not in the backend basis.""" - with self.assertWarns(DeprecationWarning): - backend = Fake5QV1() - conf = backend.configuration() + """Verify raises if pulse optimal==True but cx is not in the basis.""" + basis_gates = ["id", "rz", "sx", "x", "cx", "reset"] # this assumes iswap pulse optimal decomposition doesn't exist - conf.basis_gates = [gate if gate != "cx" else "iswap" for gate in conf.basis_gates] + basis_gates = [gate if gate != "cx" else "iswap" for gate in basis_gates] qr = QuantumRegister(2) coupling_map = CouplingMap([[0, 1], [1, 2], [1, 3], [3, 4]]) triv_layout_pass = TrivialLayout(coupling_map) qc = QuantumCircuit(qr) qc.unitary(random_unitary(4, seed=12), [0, 1]) unisynth_pass = UnitarySynthesis( - basis_gates=conf.basis_gates, + basis_gates=basis_gates, coupling_map=coupling_map, - backend_props=backend.properties(), pulse_optimize=True, natural_direction=True, ) @@ -297,47 +231,17 @@ def test_two_qubit_pulse_optimal_true_raises(self): with self.assertRaises(QiskitError): pm.run(qc) - def test_two_qubit_natural_direction_true_duration_fallback(self): - """Verify fallback path when pulse_optimize==True.""" - with self.assertWarns(DeprecationWarning): - backend = Fake5QV1() - conf = backend.configuration() - qr = QuantumRegister(2) - coupling_map = CouplingMap([[0, 1], [1, 0], [1, 2], [1, 3], [3, 4]]) - triv_layout_pass = TrivialLayout(coupling_map) - qc = QuantumCircuit(qr) - qc.unitary(random_unitary(4, seed=12), [0, 1]) - unisynth_pass = UnitarySynthesis( - basis_gates=conf.basis_gates, - coupling_map=coupling_map, - backend_props=backend.properties(), - pulse_optimize=True, - natural_direction=True, - ) - pm = PassManager([triv_layout_pass, unisynth_pass]) - qc_out = pm.run(qc) - self.assertTrue( - all(((qr[0], qr[1]) == instr.qubits for instr in qc_out.get_instructions("cx"))) - ) - def test_two_qubit_natural_direction_true_gate_length_raises(self): """Verify that error is raised if preferred direction cannot be inferred from gate lenghts/errors. """ - with self.assertWarns(DeprecationWarning): - backend = Fake5QV1() - conf = backend.configuration() - for _, nduv in backend.properties()._gates["cx"].items(): - nduv["gate_length"] = (4e-7, nduv["gate_length"][1]) - nduv["gate_error"] = (7e-3, nduv["gate_error"][1]) qr = QuantumRegister(2) coupling_map = CouplingMap([[0, 1], [1, 0], [1, 2], [1, 3], [3, 4]]) triv_layout_pass = TrivialLayout(coupling_map) qc = QuantumCircuit(qr) qc.unitary(random_unitary(4, seed=12), [0, 1]) unisynth_pass = UnitarySynthesis( - basis_gates=conf.basis_gates, - backend_props=backend.properties(), + basis_gates=["id", "rz", "sx", "x", "cx", "reset"], pulse_optimize=True, natural_direction=True, ) @@ -347,18 +251,14 @@ def test_two_qubit_natural_direction_true_gate_length_raises(self): def test_two_qubit_pulse_optimal_none_optimal(self): """Verify pulse optimal decomposition when pulse_optimize==None.""" - with self.assertWarns(DeprecationWarning): - backend = Fake5QV1() - conf = backend.configuration() qr = QuantumRegister(2) coupling_map = CouplingMap([[0, 1], [1, 2], [1, 3], [3, 4]]) triv_layout_pass = TrivialLayout(coupling_map) qc = QuantumCircuit(qr) qc.unitary(random_unitary(4, seed=12), [0, 1]) unisynth_pass = UnitarySynthesis( - basis_gates=conf.basis_gates, + basis_gates=["id", "rz", "sx", "x", "cx", "reset"], coupling_map=coupling_map, - backend_props=backend.properties(), pulse_optimize=None, natural_direction=True, ) @@ -374,20 +274,17 @@ def test_two_qubit_pulse_optimal_none_optimal(self): def test_two_qubit_pulse_optimal_none_no_raise(self): """Verify pulse optimal decomposition when pulse_optimize==None doesn't raise when pulse optimal decomposition unknown.""" - # this assumes iswawp pulse optimal decomposition doesn't exist - with self.assertWarns(DeprecationWarning): - backend = Fake5QV1() - conf = backend.configuration() - conf.basis_gates = [gate if gate != "cx" else "iswap" for gate in conf.basis_gates] + basis_gates = ["id", "rz", "sx", "x", "cx", "reset"] + # this assumes iswap pulse optimal decomposition doesn't exist + basis_gates = [gate if gate != "cx" else "iswap" for gate in basis_gates] qr = QuantumRegister(2) coupling_map = CouplingMap([[0, 1], [1, 2], [1, 3], [3, 4]]) triv_layout_pass = TrivialLayout(coupling_map) qc = QuantumCircuit(qr) qc.unitary(random_unitary(4, seed=12), [0, 1]) unisynth_pass = UnitarySynthesis( - basis_gates=conf.basis_gates, + basis_gates=basis_gates, coupling_map=coupling_map, - backend_props=backend.properties(), pulse_optimize=None, natural_direction=True, ) @@ -884,7 +781,7 @@ def test_3q_with_measure(self): def test_3q_series(self): """Test a series of 3-qubit blocks.""" - backend = GenericBackendV2(5, basis_gates=["u", "cx"]) + backend = GenericBackendV2(5, basis_gates=["u", "cx"], seed=1) x = QuantumCircuit(3) x.x(2) @@ -911,7 +808,7 @@ def test_3q_measure_all(self): qc.append(op.power(8), [0, 1, 2]) qc.measure_all() - backend = GenericBackendV2(5, basis_gates=["u", "cx"]) + backend = GenericBackendV2(5, basis_gates=["u", "cx"], seed=1) tqc = transpile(qc, backend) ops = tqc.count_ops() @@ -952,6 +849,88 @@ def test_determinism(self): for basis_gate in basis_gates: self.assertLessEqual(out.count_ops()[basis_gate], gate_counts[basis_gate]) + @combine(gate=["unitary", "swap"], natural_direction=[True, False]) + def test_two_qubit_synthesis_to_directional_cx_target(self, gate, natural_direction): + """Verify two qubit unitaries are synthesized to match basis gates.""" + # TODO: should make check more explicit e.g. explicitly set gate + # direction in test instead of using specific fake backend + backend = GenericBackendV2( + num_qubits=5, + basis_gates=["id", "rz", "sx", "x", "cx", "reset"], + coupling_map=YORKTOWN_CMAP, + seed=1, + ) + coupling_map = CouplingMap(backend.coupling_map) + triv_layout_pass = TrivialLayout(coupling_map) + + qr = QuantumRegister(2) + qc = QuantumCircuit(qr) + if gate == "unitary": + qc.unitary(random_unitary(4, seed=12), [0, 1]) + elif gate == "swap": + qc.swap(qr[0], qr[1]) + + unisynth_pass = UnitarySynthesis( + target=backend.target, + pulse_optimize=True, + natural_direction=natural_direction, + ) + pm = PassManager([triv_layout_pass, unisynth_pass]) + qc_out = pm.run(qc) + self.assertEqual(Operator(qc), Operator(qc_out)) + + @data(True, False) + def test_two_qubit_synthesis_to_directional_cx_multiple_registers_target( + self, natural_direction + ): + """Verify two qubit unitaries are synthesized to match basis gates + across multiple registers.""" + # TODO: should make check more explicit e.g. explicitly set gate + # direction in test instead of using specific fake backend + backend = GenericBackendV2( + num_qubits=5, + basis_gates=["id", "rz", "sx", "x", "cx", "reset"], + coupling_map=YORKTOWN_CMAP, + seed=1, + ) + qr0 = QuantumRegister(1) + qr1 = QuantumRegister(1) + coupling_map = CouplingMap(backend.coupling_map) + triv_layout_pass = TrivialLayout(coupling_map) + qc = QuantumCircuit(qr0, qr1) + qc.unitary(random_unitary(4, seed=12), [qr0[0], qr1[0]]) + unisynth_pass = UnitarySynthesis( + target=backend.target, + pulse_optimize=True, + natural_direction=natural_direction, + ) + pm = PassManager([triv_layout_pass, unisynth_pass]) + qc_out = pm.run(qc) + self.assertEqual(Operator(qc), Operator(qc_out)) + + def test_two_qubit_natural_direction_true_duration_fallback_target(self): + """Verify fallback path when pulse_optimize==True.""" + basis_gates = ["id", "rz", "sx", "x", "cx", "reset"] + qr = QuantumRegister(2) + coupling_map = CouplingMap([[0, 1], [1, 0], [1, 2], [1, 3], [3, 4]]) + backend = GenericBackendV2( + num_qubits=5, basis_gates=basis_gates, coupling_map=coupling_map, seed=1 + ) + + triv_layout_pass = TrivialLayout(coupling_map) + qc = QuantumCircuit(qr) + qc.unitary(random_unitary(4, seed=12), [0, 1]) + unisynth_pass = UnitarySynthesis( + target=backend.target, + pulse_optimize=True, + natural_direction=True, + ) + pm = PassManager([triv_layout_pass, unisynth_pass]) + qc_out = pm.run(qc) + self.assertTrue( + all(((qr[0], qr[1]) == instr.qubits for instr in qc_out.get_instructions("cx"))) + ) + if __name__ == "__main__": unittest.main() From 4ae023a32e2dc74da10eae279ff499e0db99610f Mon Sep 17 00:00:00 2001 From: Eli Arbel <46826214+eliarbel@users.noreply.github.com> Date: Fri, 7 Feb 2025 16:15:57 +0200 Subject: [PATCH 18/48] Remove pulse-related passes (#13798) * Remove pulse-related passes This commit removes the calibration builder and pulse-gate passes which were deprecated in Qiskit 1.3, together with the pulse package itself. Related unit test have also been removed. * Fix lint issues --- qiskit/transpiler/passes/__init__.py | 13 - .../transpiler/passes/calibration/__init__.py | 17 - .../transpiler/passes/calibration/builders.py | 20 - .../passes/calibration/pulse_gate.py | 100 --- .../passes/calibration/rx_builder.py | 166 ----- .../passes/calibration/rzx_builder.py | 411 ------------ .../passes/optimization/__init__.py | 1 - .../echo_rzx_weyl_decomposition.py | 162 ----- .../transpiler/passes/scheduling/__init__.py | 2 +- .../passes/scheduling/alignments/__init__.py | 1 - .../alignments/pulse_gate_validation.py | 107 --- .../transpiler/preset_passmanagers/common.py | 16 +- .../remove-pulse-passes-3128f27ed7e42bf6.yaml | 7 + test/python/compiler/test_transpiler.py | 52 +- test/python/qpy/test_circuit_load_from_qpy.py | 41 +- .../test_instruction_alignments.py | 102 +-- .../transpiler/test_calibrationbuilder.py | 619 ------------------ .../test_echo_rzx_weyl_decomposition.py | 260 -------- .../transpiler/test_instruction_alignments.py | 120 ---- test/python/transpiler/test_passmanager.py | 43 +- .../python/transpiler/test_pulse_gate_pass.py | 577 ---------------- 21 files changed, 38 insertions(+), 2799 deletions(-) delete mode 100644 qiskit/transpiler/passes/calibration/__init__.py delete mode 100644 qiskit/transpiler/passes/calibration/builders.py delete mode 100644 qiskit/transpiler/passes/calibration/pulse_gate.py delete mode 100644 qiskit/transpiler/passes/calibration/rx_builder.py delete mode 100644 qiskit/transpiler/passes/calibration/rzx_builder.py delete mode 100644 qiskit/transpiler/passes/optimization/echo_rzx_weyl_decomposition.py delete mode 100644 qiskit/transpiler/passes/scheduling/alignments/pulse_gate_validation.py create mode 100644 releasenotes/notes/remove-pulse-passes-3128f27ed7e42bf6.yaml delete mode 100644 test/python/transpiler/test_calibrationbuilder.py delete mode 100644 test/python/transpiler/test_echo_rzx_weyl_decomposition.py delete mode 100644 test/python/transpiler/test_instruction_alignments.py delete mode 100644 test/python/transpiler/test_pulse_gate_pass.py diff --git a/qiskit/transpiler/passes/__init__.py b/qiskit/transpiler/passes/__init__.py index 8823e1ce7233..1fd8454159a3 100644 --- a/qiskit/transpiler/passes/__init__.py +++ b/qiskit/transpiler/passes/__init__.py @@ -85,7 +85,6 @@ RemoveFinalReset HoareOptimizer TemplateOptimization - EchoRZXWeylDecomposition ResetAfterMeasureSimplification OptimizeCliffords ElidePermutations @@ -100,11 +99,6 @@ .. autosummary:: :toctree: ../stubs/ - PulseGates - RZXCalibrationBuilder - RZXCalibrationBuilderNoEcho - RXCalibrationBuilder - .. autofunction:: rzx_templates Scheduling @@ -119,7 +113,6 @@ PadDynamicalDecoupling PadDelay ConstrainedReschedule - ValidatePulseGates InstructionDurationCheck SetIOLatency ALAPSchedule @@ -237,7 +230,6 @@ from .optimization import HoareOptimizer from .optimization import TemplateOptimization from .optimization import InverseCancellation -from .optimization import EchoRZXWeylDecomposition from .optimization import CollectAndCollapse from .optimization import CollectLinearFunctions from .optimization import CollectCliffords @@ -270,10 +262,6 @@ from .synthesis import AQCSynthesisPlugin # calibration -from .calibration import PulseGates -from .calibration import RZXCalibrationBuilder -from .calibration import RZXCalibrationBuilderNoEcho -from .calibration import RXCalibrationBuilder from .calibration.rzx_templates import rzx_templates # circuit scheduling @@ -281,7 +269,6 @@ from .scheduling import ALAPScheduleAnalysis from .scheduling import ASAPScheduleAnalysis from .scheduling import PadDynamicalDecoupling -from .scheduling import ValidatePulseGates from .scheduling import PadDelay from .scheduling import ConstrainedReschedule from .scheduling import InstructionDurationCheck diff --git a/qiskit/transpiler/passes/calibration/__init__.py b/qiskit/transpiler/passes/calibration/__init__.py deleted file mode 100644 index 990249373f3f..000000000000 --- a/qiskit/transpiler/passes/calibration/__init__.py +++ /dev/null @@ -1,17 +0,0 @@ -# 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. - -"""Module containing transpiler calibration passes.""" - -from .pulse_gate import PulseGates -from .rzx_builder import RZXCalibrationBuilder, RZXCalibrationBuilderNoEcho -from .rx_builder import RXCalibrationBuilder diff --git a/qiskit/transpiler/passes/calibration/builders.py b/qiskit/transpiler/passes/calibration/builders.py deleted file mode 100644 index 49c2c6427317..000000000000 --- a/qiskit/transpiler/passes/calibration/builders.py +++ /dev/null @@ -1,20 +0,0 @@ -# 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. - -"""Calibration creators.""" - -# TODO This import path will be deprecated. - -# pylint: disable=unused-import -from .pulse_gate import PulseGates -from .rzx_builder import RZXCalibrationBuilder, RZXCalibrationBuilderNoEcho -from .rx_builder import RXCalibrationBuilder diff --git a/qiskit/transpiler/passes/calibration/pulse_gate.py b/qiskit/transpiler/passes/calibration/pulse_gate.py deleted file mode 100644 index f5b56ad0f359..000000000000 --- a/qiskit/transpiler/passes/calibration/pulse_gate.py +++ /dev/null @@ -1,100 +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. - -"""Instruction schedule map reference pass.""" - -from typing import List, Union - -from qiskit.circuit import Instruction as CircuitInst -from qiskit.pulse import Schedule, ScheduleBlock -from qiskit.pulse.instruction_schedule_map import InstructionScheduleMap -from qiskit.transpiler.target import Target -from qiskit.transpiler.exceptions import TranspilerError -from qiskit.utils.deprecate_pulse import deprecate_pulse_dependency - -from .base_builder import CalibrationBuilder - - -class PulseGates(CalibrationBuilder): - """Pulse gate adding pass. - - This pass adds gate calibrations from the supplied ``InstructionScheduleMap`` - to a quantum circuit. - - This pass checks each DAG circuit node and acquires a corresponding schedule from - the instruction schedule map object that may be provided by the target backend. - Because this map is a mutable object, the end-user can provide a configured backend to - execute the circuit with customized gate implementations. - - This mapping object returns a schedule with "publisher" metadata which is an integer Enum - value representing who created the gate schedule. - If the gate schedule is provided by end-users, this pass attaches the schedule to - the DAG circuit as a calibration. - - This pass allows users to easily override quantum circuit with custom gate definitions - without directly dealing with those schedules. - - References - * [1] OpenQASM 3: A broader and deeper quantum assembly language - https://arxiv.org/abs/2104.14722 - """ - - @deprecate_pulse_dependency - def __init__( - self, - inst_map: InstructionScheduleMap = None, - target: Target = None, - ): - """Create new pass. - - Args: - inst_map: Instruction schedule map that user may override. - target: The :class:`~.Target` representing the target backend, if both - ``inst_map`` and ``target`` are specified then it updates instructions - in the ``target`` with ``inst_map``. - """ - super().__init__() - - if inst_map is None and target is None: - raise TranspilerError("inst_map and target cannot be None simulataneously.") - - if target is None: - target = Target() - target.update_from_instruction_schedule_map(inst_map) - self.target = target - - def supported(self, node_op: CircuitInst, qubits: List) -> bool: - """Determine if a given node supports the calibration. - - Args: - node_op: Target instruction object. - qubits: Integer qubit indices to check. - - Returns: - Return ``True`` is calibration can be provided. - """ - return self.target._has_calibration(node_op.name, tuple(qubits)) - - def get_calibration(self, node_op: CircuitInst, qubits: List) -> Union[Schedule, ScheduleBlock]: - """Gets the calibrated schedule for the given instruction and qubits. - - Args: - node_op: Target instruction object. - qubits: Integer qubit indices to check. - - Returns: - Return Schedule of target gate instruction. - - Raises: - TranspilerError: When node is parameterized and calibration is raw schedule object. - """ - return self.target._get_calibration(node_op.name, tuple(qubits), *node_op.params) diff --git a/qiskit/transpiler/passes/calibration/rx_builder.py b/qiskit/transpiler/passes/calibration/rx_builder.py deleted file mode 100644 index 7dfeb56c68b3..000000000000 --- a/qiskit/transpiler/passes/calibration/rx_builder.py +++ /dev/null @@ -1,166 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2023. -# -# 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. - -"""Add single-pulse RX calibrations that are bootstrapped from the SX calibration.""" - -from typing import Union -from functools import lru_cache -import numpy as np - -from qiskit.circuit import Instruction -from qiskit.circuit.library.standard_gates import RXGate -from qiskit.exceptions import QiskitError -from qiskit.pulse import Schedule, ScheduleBlock, builder, ScalableSymbolicPulse -from qiskit.pulse.channels import Channel -from qiskit.pulse.library.symbolic_pulses import Drag -from qiskit.transpiler.passes.calibration.base_builder import CalibrationBuilder -from qiskit.transpiler.target import Target -from qiskit.utils.deprecate_pulse import deprecate_pulse_dependency - - -class RXCalibrationBuilder(CalibrationBuilder): - """Add single-pulse RX calibrations that are bootstrapped from the SX calibration. - - .. note:: - - Requirement: NormalizeRXAngles pass (one of the optimization passes). - - It is recommended to place this pass in the post-optimization stage of a passmanager. - A simple demo: - - .. plot:: - :include-source: - :nofigs: - - from qiskit.providers.fake_provider import GenericBackendV2 - from qiskit.transpiler import PassManager, PassManagerConfig - from qiskit.transpiler.preset_passmanagers import level_1_pass_manager - from qiskit.circuit import Parameter - from qiskit.circuit.library import QuantumVolume - from qiskit.circuit.library.standard_gates import RXGate - - from qiskit.transpiler.passes import RXCalibrationBuilder - - qv = QuantumVolume(4, 4, seed=1004) - - # Transpiling with single pulse RX gates enabled - backend_with_single_pulse_rx = GenericBackendV2(5) - rx_inst_props = {} - for i in range(backend_with_single_pulse_rx.num_qubits): - rx_inst_props[(i,)] = None - backend_with_single_pulse_rx.target.add_instruction(RXGate(Parameter("theta")), rx_inst_props) - config_with_rx = PassManagerConfig.from_backend(backend=backend_with_single_pulse_rx) - pm_with_rx = level_1_pass_manager(pass_manager_config=config_with_rx) - rx_builder = RXCalibrationBuilder(target=backend_with_single_pulse_rx.target) - pm_with_rx.post_optimization = PassManager([rx_builder]) - transpiled_circ_with_single_pulse_rx = pm_with_rx.run(qv) - transpiled_circ_with_single_pulse_rx.count_ops() - - # Conventional transpilation: each RX gate is decomposed into a sequence with two SX gates - original_backend = GenericBackendV2(5) - original_config = PassManagerConfig.from_backend(backend=original_backend) - original_pm = level_1_pass_manager(pass_manager_config=original_config) - original_transpiled_circ = original_pm.run(qv) - original_transpiled_circ.count_ops() - - References - * [1]: Gokhale et al. (2020), Optimized Quantum Compilation for - Near-Term Algorithms with OpenPulse. - `arXiv:2004.11205 ` - """ - - @deprecate_pulse_dependency - def __init__( - self, - target: Target = None, - ): - """Bootstrap single-pulse RX gate calibrations from the - (hardware-calibrated) SX gate calibration. - - Args: - target (Target): Should contain a SX calibration that will be - used for bootstrapping RX calibrations. - """ - from qiskit.transpiler.passes.optimization import NormalizeRXAngle - - super().__init__() - self.target = target - self.already_generated = {} - self.requires = [NormalizeRXAngle(self.target)] - - def supported(self, node_op: Instruction, qubits: list) -> bool: - """ - Check if the calibration for SX gate exists and it's a single DRAG pulse. - """ - return ( - isinstance(node_op, RXGate) - and self.target._has_calibration("sx", tuple(qubits)) - and (len(self.target._get_calibration("sx", tuple(qubits)).instructions) == 1) - and isinstance( - self.target._get_calibration("sx", tuple(qubits)).instructions[0][1].pulse, - ScalableSymbolicPulse, - ) - and self.target._get_calibration("sx", tuple(qubits)) - .instructions[0][1] - .pulse.pulse_type - == "Drag" - ) - - def get_calibration(self, node_op: Instruction, qubits: list) -> Union[Schedule, ScheduleBlock]: - """ - Generate RX calibration for the rotation angle specified in node_op. - """ - # already within [0, pi] by NormalizeRXAngles pass - angle = node_op.params[0] - - try: - angle = float(angle) - except TypeError as ex: - raise QiskitError("Target rotation angle is not assigned.") from ex - - params = ( - self.target._get_calibration("sx", tuple(qubits)) - .instructions[0][1] - .pulse.parameters.copy() - ) - new_rx_sched = _create_rx_sched( - rx_angle=angle, - channel=self.target._get_calibration("sx", tuple(qubits)).channels[0], - duration=params["duration"], - amp=params["amp"], - sigma=params["sigma"], - beta=params["beta"], - ) - - return new_rx_sched - - -@lru_cache -def _create_rx_sched( - rx_angle: float, - duration: int, - amp: float, - sigma: float, - beta: float, - channel: Channel, -): - """Generates (and caches) pulse calibrations for RX gates. - Assumes that the rotation angle is in [0, pi]. - """ - new_amp = rx_angle / (np.pi / 2) * amp - with builder.build() as new_rx_sched: - builder.play( - Drag(duration=duration, amp=new_amp, sigma=sigma, beta=beta, angle=0), - channel=channel, - ) - - return new_rx_sched diff --git a/qiskit/transpiler/passes/calibration/rzx_builder.py b/qiskit/transpiler/passes/calibration/rzx_builder.py deleted file mode 100644 index 72cf347db9bb..000000000000 --- a/qiskit/transpiler/passes/calibration/rzx_builder.py +++ /dev/null @@ -1,411 +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. - -"""RZX calibration builders.""" -from __future__ import annotations - -import enum -import math -import warnings -from collections.abc import Sequence -from math import pi, erf - -import numpy as np -from qiskit.circuit import Instruction as CircuitInst -from qiskit.circuit.library.standard_gates import RZXGate -from qiskit.exceptions import QiskitError -from qiskit.pulse import ( - Play, - Schedule, - ScheduleBlock, - ControlChannel, - DriveChannel, - GaussianSquare, - Waveform, -) -from qiskit.pulse import builder -from qiskit.pulse.filters import filter_instructions -from qiskit.pulse.instruction_schedule_map import InstructionScheduleMap -from qiskit.transpiler.target import Target -from qiskit.utils.deprecate_pulse import deprecate_pulse_dependency - -from .base_builder import CalibrationBuilder -from .exceptions import CalibrationNotAvailable - - -class CRCalType(enum.Enum): - """Estimated calibration type of backend cross resonance operations.""" - - ECR_FORWARD = "Echoed Cross Resonance corresponding to native operation" - ECR_REVERSE = "Echoed Cross Resonance reverse of native operation" - ECR_CX_FORWARD = "Echoed Cross Resonance CX corresponding to native operation" - ECR_CX_REVERSE = "Echoed Cross Resonance CX reverse of native operation" - DIRECT_CX_FORWARD = "Direct CX corresponding to native operation" - DIRECT_CX_REVERSE = "Direct CX reverse of native operation" - - -class RZXCalibrationBuilder(CalibrationBuilder): - """ - Creates calibrations for RZXGate(theta) by stretching and compressing - Gaussian square pulses in the CX gate. This is done by retrieving (for a given pair of - qubits) the CX schedule in the instruction schedule map of the backend defaults. - The CX schedule must be an echoed cross-resonance gate optionally with rotary tones. - The cross-resonance drive tones and rotary pulses must be Gaussian square pulses. - The width of the Gaussian square pulse is adjusted so as to match the desired rotation angle. - If the rotation angle is small such that the width disappears then the amplitude of the - zero width Gaussian square pulse (i.e. a Gaussian) is reduced to reach the target rotation - angle. Additional details can be found in https://arxiv.org/abs/2012.11660. - """ - - @deprecate_pulse_dependency - def __init__( - self, - instruction_schedule_map: InstructionScheduleMap = None, - verbose: bool = True, - target: Target = None, - ): - """ - Initializes a RZXGate calibration builder. - - Args: - instruction_schedule_map: The :obj:`InstructionScheduleMap` object representing the - default pulse calibrations for the target backend - verbose: Set True to raise a user warning when RZX schedule cannot be built. - target: The :class:`~.Target` representing the target backend, if both - ``instruction_schedule_map`` and this are specified then this argument will take - precedence and ``instruction_schedule_map`` will be ignored. - - Raises: - QiskitError: Instruction schedule map is not provided. - """ - super().__init__() - self._inst_map = instruction_schedule_map - self._verbose = verbose - if target: - self._inst_map = target._instruction_schedule_map() - if self._inst_map is None: - raise QiskitError("Calibrations can only be added to Pulse-enabled backends") - - def supported(self, node_op: CircuitInst, qubits: list) -> bool: - """Determine if a given node supports the calibration. - - Args: - node_op: Target instruction object. - qubits: Integer qubit indices to check. - - Returns: - Return ``True`` is calibration can be provided. - """ - return isinstance(node_op, RZXGate) and ( - "cx" in self._inst_map.instructions or "ecr" in self._inst_map.instructions - ) - - @staticmethod - @builder.macro - def rescale_cr_inst(instruction: Play, theta: float, sample_mult: int = 16) -> int: - """A builder macro to play stretched pulse. - - Args: - instruction: The instruction from which to create a new shortened or lengthened pulse. - theta: desired angle, pi/2 is assumed to be the angle that the pulse in the given - play instruction implements. - sample_mult: All pulses must be a multiple of sample_mult. - - Returns: - Duration of stretched pulse. - - Raises: - QiskitError: if rotation angle is not assigned. - """ - try: - theta = float(theta) - except TypeError as ex: - raise QiskitError("Target rotation angle is not assigned.") from ex - - # This method is called for instructions which are guaranteed to play GaussianSquare pulse - params = instruction.pulse.parameters.copy() - risefall_sigma_ratio = (params["duration"] - params["width"]) / params["sigma"] - - # The error function is used because the Gaussian may have chopped tails. - # Area is normalized by amplitude. - # This makes widths robust to the rounding error. - risefall_area = params["sigma"] * math.sqrt(2 * pi) * erf(risefall_sigma_ratio) - full_area = params["width"] + risefall_area - - # Get estimate of target area. Assume this is pi/2 controlled rotation. - cal_angle = pi / 2 - target_area = abs(theta) / cal_angle * full_area - new_width = target_area - risefall_area - - if new_width >= 0: - width = new_width - params["amp"] *= np.sign(theta) - else: - width = 0 - params["amp"] *= np.sign(theta) * target_area / risefall_area - - round_duration = ( - round((width + risefall_sigma_ratio * params["sigma"]) / sample_mult) * sample_mult - ) - params["duration"] = round_duration - params["width"] = width - - stretched_pulse = GaussianSquare(**params) - builder.play(stretched_pulse, instruction.channel) - - return round_duration - - def get_calibration(self, node_op: CircuitInst, qubits: list) -> Schedule | ScheduleBlock: - """Builds the calibration schedule for the RZXGate(theta) with echos. - - Args: - node_op: Instruction of the RZXGate(theta). I.e. params[0] is theta. - qubits: List of qubits for which to get the schedules. The first qubit is - the control and the second is the target. - - Returns: - schedule: The calibration schedule for the RZXGate(theta). - - Raises: - QiskitError: if rotation angle is not assigned. - QiskitError: If the control and target qubits cannot be identified. - CalibrationNotAvailable: RZX schedule cannot be built for input node. - """ - theta = node_op.params[0] - - try: - theta = float(theta) - except TypeError as ex: - raise QiskitError("Target rotation angle is not assigned.") from ex - - if np.isclose(theta, 0.0): - return ScheduleBlock(name="rzx(0.000)") - - cal_type, cr_tones, comp_tones = _check_calibration_type(self._inst_map, qubits) - - if cal_type in [CRCalType.DIRECT_CX_FORWARD, CRCalType.DIRECT_CX_REVERSE]: - if self._verbose: - warnings.warn( - f"CR instruction for qubits {qubits} is likely {cal_type.value} sequence. " - "Pulse stretch for this calibration is not currently implemented. " - "RZX schedule is not generated for this qubit pair.", - UserWarning, - ) - raise CalibrationNotAvailable - - # The CR instruction is in the forward (native) direction - if cal_type in [CRCalType.ECR_CX_FORWARD, CRCalType.ECR_FORWARD]: - with warnings.catch_warnings(): - warnings.simplefilter(action="ignore", category=DeprecationWarning) - # `InstructionScheduleMap.get` and the pulse builder emit deprecation warnings - # as they use classes and methods which are deprecated in Qiskit 1.3 as part of the - # Qiskit Pulse deprecation - xgate = self._inst_map.get("x", qubits[0]) - with builder.build( - default_alignment="sequential", name=f"rzx({theta:.3f})" - ) as rzx_theta_native: - for cr_tone, comp_tone in zip(cr_tones, comp_tones): - with builder.align_left(): - self.rescale_cr_inst(cr_tone, theta) - self.rescale_cr_inst(comp_tone, theta) - builder.call(xgate) - return rzx_theta_native - - # The direction is not native. Add Hadamard gates to flip the direction. - xgate = self._inst_map.get("x", qubits[1]) - szc = self._inst_map.get("rz", qubits[1], pi / 2) - sxc = self._inst_map.get("sx", qubits[1]) - szt = self._inst_map.get("rz", qubits[0], pi / 2) - sxt = self._inst_map.get("sx", qubits[0]) - with builder.build(name="hadamard") as hadamard: - # Control qubit - builder.call(szc, name="szc") - builder.call(sxc, name="sxc") - builder.call(szc, name="szc") - # Target qubit - builder.call(szt, name="szt") - builder.call(sxt, name="sxt") - builder.call(szt, name="szt") - - with builder.build( - default_alignment="sequential", name=f"rzx({theta:.3f})" - ) as rzx_theta_flip: - builder.call(hadamard, name="hadamard") - for cr_tone, comp_tone in zip(cr_tones, comp_tones): - with builder.align_left(): - self.rescale_cr_inst(cr_tone, theta) - self.rescale_cr_inst(comp_tone, theta) - builder.call(xgate) - builder.call(hadamard, name="hadamard") - return rzx_theta_flip - - -class RZXCalibrationBuilderNoEcho(RZXCalibrationBuilder): - """ - Creates calibrations for RZXGate(theta) by stretching and compressing - Gaussian square pulses in the CX gate. - - The ``RZXCalibrationBuilderNoEcho`` is a variation of the - :class:`~qiskit.transpiler.passes.RZXCalibrationBuilder` pass - that creates calibrations for the cross-resonance pulses without inserting - the echo pulses in the pulse schedule. This enables exposing the echo in - the cross-resonance sequence as gates so that the transpiler can simplify them. - The ``RZXCalibrationBuilderNoEcho`` only supports the hardware-native direction - of the CX gate. - """ - - def get_calibration(self, node_op: CircuitInst, qubits: list) -> Schedule | ScheduleBlock: - """Builds the calibration schedule for the RZXGate(theta) without echos. - - Args: - node_op: Instruction of the RZXGate(theta). I.e. params[0] is theta. - qubits: List of qubits for which to get the schedules. The first qubit is - the control and the second is the target. - - Returns: - schedule: The calibration schedule for the RZXGate(theta). - - Raises: - QiskitError: if rotation angle is not assigned. - QiskitError: If the control and target qubits cannot be identified, - or the backend does not natively support the specified direction of the cx. - CalibrationNotAvailable: RZX schedule cannot be built for input node. - """ - theta = node_op.params[0] - - try: - theta = float(theta) - except TypeError as ex: - raise QiskitError("Target rotation angle is not assigned.") from ex - - if np.isclose(theta, 0.0): - return ScheduleBlock(name="rzx(0.000)") - - cal_type, cr_tones, comp_tones = _check_calibration_type(self._inst_map, qubits) - - if cal_type in [CRCalType.DIRECT_CX_FORWARD, CRCalType.DIRECT_CX_REVERSE]: - if self._verbose: - warnings.warn( - f"CR instruction for qubits {qubits} is likely {cal_type.value} sequence. " - "Pulse stretch for this calibration is not currently implemented. " - "RZX schedule is not generated for this qubit pair.", - UserWarning, - ) - raise CalibrationNotAvailable - - # RZXCalibrationNoEcho only good for forward CR direction - if cal_type in [CRCalType.ECR_CX_FORWARD, CRCalType.ECR_FORWARD]: - with warnings.catch_warnings(): - warnings.simplefilter(action="ignore", category=DeprecationWarning) - # Pulse builder emits deprecation warnings as part of the - # Qiskit Pulse deprecation - with builder.build(default_alignment="left", name=f"rzx({theta:.3f})") as rzx_theta: - stretched_dur = self.rescale_cr_inst(cr_tones[0], 2 * theta) - self.rescale_cr_inst(comp_tones[0], 2 * theta) - # Placeholder to make pulse gate work - builder.delay(stretched_dur, DriveChannel(qubits[0])) - return rzx_theta - - raise QiskitError("RZXCalibrationBuilderNoEcho only supports hardware-native RZX gates.") - - -def _filter_cr_tone(time_inst_tup): - """A helper function to filter pulses on control channels.""" - valid_types = ["GaussianSquare"] - - _, inst = time_inst_tup - if isinstance(inst, Play) and isinstance(inst.channel, ControlChannel): - pulse = inst.pulse - if isinstance(pulse, Waveform) or pulse.pulse_type in valid_types: - return True - return False - - -def _filter_comp_tone(time_inst_tup): - """A helper function to filter pulses on drive channels.""" - valid_types = ["GaussianSquare"] - - _, inst = time_inst_tup - if isinstance(inst, Play) and isinstance(inst.channel, DriveChannel): - pulse = inst.pulse - if isinstance(pulse, Waveform) or pulse.pulse_type in valid_types: - return True - return False - - -def _check_calibration_type( - inst_sched_map: InstructionScheduleMap, qubits: Sequence[int] -) -> tuple[CRCalType, list[Play], list[Play]]: - """A helper function to check type of CR calibration. - - Args: - inst_sched_map: instruction schedule map of the backends - qubits: ordered tuple of qubits for cross resonance (q_control, q_target) - - Returns: - Filtered instructions and most-likely type of calibration. - - Raises: - QiskitError: Unknown calibration type is detected. - """ - cal_type = None - with warnings.catch_warnings(): - warnings.simplefilter(action="ignore", category=DeprecationWarning) - # `InstructionScheduleMap.get` and `filter_instructions` emit deprecation warnings - # as they use classes and methods which are deprecated in Qiskit 1.3 as part of the - # Qiskit Pulse deprecation - if inst_sched_map.has("cx", qubits): - cr_sched = inst_sched_map.get("cx", qubits=qubits) - elif inst_sched_map.has("ecr", qubits): - cr_sched = inst_sched_map.get("ecr", qubits=qubits) - cal_type = CRCalType.ECR_FORWARD - elif inst_sched_map.has("ecr", tuple(reversed(qubits))): - cr_sched = inst_sched_map.get("ecr", tuple(reversed(qubits))) - cal_type = CRCalType.ECR_REVERSE - else: - raise QiskitError( - f"Native direction cannot be determined: operation on qubits {qubits} " - f"for the following instruction schedule map:\n{inst_sched_map}" - ) - - cr_tones = [t[1] for t in filter_instructions(cr_sched, [_filter_cr_tone]).instructions] - comp_tones = [t[1] for t in filter_instructions(cr_sched, [_filter_comp_tone]).instructions] - - if cal_type is None: - if len(comp_tones) == 0: - raise QiskitError( - f"{repr(cr_sched)} has no target compensation tones. " - "Native ECR direction cannot be determined." - ) - # Determine native direction, assuming only single drive channel per qubit. - # This guarantees channel and qubit index equality. - if comp_tones[0].channel.index == qubits[1]: - cal_type = CRCalType.ECR_CX_FORWARD - else: - cal_type = CRCalType.ECR_CX_REVERSE - - if len(cr_tones) == 2 and len(comp_tones) in (0, 2): - # ECR can be implemented without compensation tone at price of lower fidelity. - # Remarkable noisy terms are usually eliminated by echo. - return cal_type, cr_tones, comp_tones - - if len(cr_tones) == 1 and len(comp_tones) == 1: - # Direct CX must have compensation tone on target qubit. - # Otherwise, it cannot eliminate IX interaction. - if comp_tones[0].channel.index == qubits[1]: - return CRCalType.DIRECT_CX_FORWARD, cr_tones, comp_tones - else: - return CRCalType.DIRECT_CX_REVERSE, cr_tones, comp_tones - raise QiskitError( - f"{repr(cr_sched)} is undefined pulse sequence. " - "Check if this is a calibration for cross resonance operation." - ) diff --git a/qiskit/transpiler/passes/optimization/__init__.py b/qiskit/transpiler/passes/optimization/__init__.py index 0e5108f44d2a..683b5d8e9e1f 100644 --- a/qiskit/transpiler/passes/optimization/__init__.py +++ b/qiskit/transpiler/passes/optimization/__init__.py @@ -29,7 +29,6 @@ from .template_optimization import TemplateOptimization from .inverse_cancellation import InverseCancellation from .collect_1q_runs import Collect1qRuns -from .echo_rzx_weyl_decomposition import EchoRZXWeylDecomposition from .collect_linear_functions import CollectLinearFunctions from .reset_after_measure_simplification import ResetAfterMeasureSimplification from .optimize_cliffords import OptimizeCliffords diff --git a/qiskit/transpiler/passes/optimization/echo_rzx_weyl_decomposition.py b/qiskit/transpiler/passes/optimization/echo_rzx_weyl_decomposition.py deleted file mode 100644 index c926e15800ae..000000000000 --- a/qiskit/transpiler/passes/optimization/echo_rzx_weyl_decomposition.py +++ /dev/null @@ -1,162 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2017, 2021. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. - -"""Weyl decomposition of two-qubit gates in terms of echoed cross-resonance gates.""" - -from typing import Tuple - -from qiskit.circuit import QuantumRegister -from qiskit.circuit.library.standard_gates import RZXGate, HGate, XGate - -from qiskit.transpiler.basepasses import TransformationPass -from qiskit.transpiler.exceptions import TranspilerError -from qiskit.transpiler.layout import Layout -from qiskit.transpiler.passes.calibration.rzx_builder import _check_calibration_type, CRCalType -from qiskit.utils.deprecate_pulse import deprecate_pulse_dependency - -from qiskit.dagcircuit import DAGCircuit -from qiskit.converters import circuit_to_dag - - -class EchoRZXWeylDecomposition(TransformationPass): - """Rewrite two-qubit gates using the Weyl decomposition. - - This transpiler pass rewrites two-qubit gates in terms of echoed cross-resonance gates according - to the Weyl decomposition. A two-qubit gate will be replaced with at most six non-echoed RZXGates. - Each pair of RZXGates forms an echoed RZXGate. - """ - - @deprecate_pulse_dependency - def __init__(self, instruction_schedule_map=None, target=None): - """EchoRZXWeylDecomposition pass. - - Args: - instruction_schedule_map (InstructionScheduleMap): the mapping from circuit - :class:`~.circuit.Instruction` names and arguments to :class:`.Schedule`\\ s. - target (Target): The :class:`~.Target` representing the target backend, if both - ``instruction_schedule_map`` and ``target`` are specified then this argument will take - precedence and ``instruction_schedule_map`` will be ignored. - """ - super().__init__() - self._inst_map = instruction_schedule_map - if target is not None: - self._inst_map = target.instruction_schedule_map() - - def _is_native(self, qubit_pair: Tuple) -> bool: - """Return the direction of the qubit pair that is native.""" - cal_type, _, _ = _check_calibration_type(self._inst_map, qubit_pair) - return cal_type in [ - CRCalType.ECR_CX_FORWARD, - CRCalType.ECR_FORWARD, - CRCalType.DIRECT_CX_FORWARD, - ] - - @staticmethod - def _echo_rzx_dag(theta): - """Return the following circuit - - .. code-block:: text - - ┌───────────────┐┌───┐┌────────────────┐┌───┐ - q_0: ┤0 ├┤ X ├┤0 ├┤ X ├ - │ Rzx(theta/2) │└───┘│ Rzx(-theta/2) │└───┘ - q_1: ┤1 ├─────┤1 ├───── - └───────────────┘ └────────────────┘ - """ - rzx_dag = DAGCircuit() - qr = QuantumRegister(2) - rzx_dag.add_qreg(qr) - rzx_dag.apply_operation_back(RZXGate(theta / 2), [qr[0], qr[1]], []) - rzx_dag.apply_operation_back(XGate(), [qr[0]], []) - rzx_dag.apply_operation_back(RZXGate(-theta / 2), [qr[0], qr[1]], []) - rzx_dag.apply_operation_back(XGate(), [qr[0]], []) - return rzx_dag - - @staticmethod - def _reverse_echo_rzx_dag(theta): - """Return the following circuit - - .. code-block:: text - - ┌───┐┌───────────────┐ ┌────────────────┐┌───┐ - q_0: ┤ H ├┤1 ├─────┤1 ├┤ H ├───── - ├───┤│ Rzx(theta/2) │┌───┐│ Rzx(-theta/2) │├───┤┌───┐ - q_1: ┤ H ├┤0 ├┤ X ├┤0 ├┤ X ├┤ H ├ - └───┘└───────────────┘└───┘└────────────────┘└───┘└───┘ - """ - reverse_rzx_dag = DAGCircuit() - qr = QuantumRegister(2) - reverse_rzx_dag.add_qreg(qr) - reverse_rzx_dag.apply_operation_back(HGate(), [qr[0]], []) - reverse_rzx_dag.apply_operation_back(HGate(), [qr[1]], []) - reverse_rzx_dag.apply_operation_back(RZXGate(theta / 2), [qr[1], qr[0]], []) - reverse_rzx_dag.apply_operation_back(XGate(), [qr[1]], []) - reverse_rzx_dag.apply_operation_back(RZXGate(-theta / 2), [qr[1], qr[0]], []) - reverse_rzx_dag.apply_operation_back(XGate(), [qr[1]], []) - reverse_rzx_dag.apply_operation_back(HGate(), [qr[0]], []) - reverse_rzx_dag.apply_operation_back(HGate(), [qr[1]], []) - return reverse_rzx_dag - - def run(self, dag: DAGCircuit): - """Run the EchoRZXWeylDecomposition pass on `dag`. - - Rewrites two-qubit gates in an arbitrary circuit in terms of echoed cross-resonance - gates by computing the Weyl decomposition of the corresponding unitary. Modifies the - input dag. - - Args: - dag (DAGCircuit): DAG to rewrite. - - Returns: - DAGCircuit: The modified dag. - - Raises: - TranspilerError: If the circuit cannot be rewritten. - """ - - # pylint: disable=cyclic-import - from qiskit.quantum_info import Operator - from qiskit.synthesis.two_qubit.two_qubit_decompose import TwoQubitControlledUDecomposer - - if len(dag.qregs) > 1: - raise TranspilerError( - "EchoRZXWeylDecomposition expects a single qreg input DAG," - f"but input DAG had qregs: {dag.qregs}." - ) - - trivial_layout = Layout.generate_trivial_layout(*dag.qregs.values()) - - decomposer = TwoQubitControlledUDecomposer(RZXGate) - - for node in dag.two_qubit_ops(): - - unitary = Operator(node.op).data - dag_weyl = circuit_to_dag(decomposer(unitary)) - dag.substitute_node_with_dag(node, dag_weyl) - - for node in dag.two_qubit_ops(): - if node.name == "rzx": - control = node.qargs[0] - target = node.qargs[1] - - physical_q0 = trivial_layout[control] - physical_q1 = trivial_layout[target] - - is_native = self._is_native((physical_q0, physical_q1)) - - theta = node.op.params[0] - if is_native: - dag.substitute_node_with_dag(node, self._echo_rzx_dag(theta)) - else: - dag.substitute_node_with_dag(node, self._reverse_echo_rzx_dag(theta)) - - return dag diff --git a/qiskit/transpiler/passes/scheduling/__init__.py b/qiskit/transpiler/passes/scheduling/__init__.py index 0d120911b06f..2eeb29661d5e 100644 --- a/qiskit/transpiler/passes/scheduling/__init__.py +++ b/qiskit/transpiler/passes/scheduling/__init__.py @@ -18,7 +18,7 @@ from .scheduling import ALAPScheduleAnalysis, ASAPScheduleAnalysis, SetIOLatency from .time_unit_conversion import TimeUnitConversion from .padding import PadDelay, PadDynamicalDecoupling -from .alignments import InstructionDurationCheck, ValidatePulseGates, ConstrainedReschedule +from .alignments import InstructionDurationCheck, ConstrainedReschedule # For backward compatibility from . import alignments as instruction_alignments diff --git a/qiskit/transpiler/passes/scheduling/alignments/__init__.py b/qiskit/transpiler/passes/scheduling/alignments/__init__.py index 8478f241c267..8ecd68eacdbb 100644 --- a/qiskit/transpiler/passes/scheduling/alignments/__init__.py +++ b/qiskit/transpiler/passes/scheduling/alignments/__init__.py @@ -76,6 +76,5 @@ """ from .check_durations import InstructionDurationCheck -from .pulse_gate_validation import ValidatePulseGates from .reschedule import ConstrainedReschedule from .align_measures import AlignMeasures diff --git a/qiskit/transpiler/passes/scheduling/alignments/pulse_gate_validation.py b/qiskit/transpiler/passes/scheduling/alignments/pulse_gate_validation.py deleted file mode 100644 index 885730aac48a..000000000000 --- a/qiskit/transpiler/passes/scheduling/alignments/pulse_gate_validation.py +++ /dev/null @@ -1,107 +0,0 @@ -# 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. - -"""Analysis passes for hardware alignment constraints.""" - -from qiskit.dagcircuit import DAGCircuit -from qiskit.pulse import Play -from qiskit.transpiler.basepasses import AnalysisPass -from qiskit.transpiler.exceptions import TranspilerError -from qiskit.transpiler.target import Target -from qiskit.utils.deprecate_pulse import deprecate_pulse_dependency - - -class ValidatePulseGates(AnalysisPass): - """Check custom gate length. - - This is a control electronics aware analysis pass. - - Quantum gates (instructions) are often implemented with shaped analog stimulus signals. - These signals may be digitally stored in the waveform memory of the control electronics - and converted into analog voltage signals by electronic components known as - digital to analog converters (DAC). - - In Qiskit SDK, we can define the pulse-level implementation of custom quantum gate - instructions, as a `pulse gate - `__, - thus user gates should satisfy all waveform memory constraints imposed by the backend. - - This pass validates all attached calibration entries and raises ``TranspilerError`` to - kill the transpilation process if any invalid calibration entry is found. - This pass saves users from waiting until job execution time to get an invalid pulse error from - the backend control electronics. - """ - - @deprecate_pulse_dependency - def __init__( - self, - granularity: int = 1, - min_length: int = 1, - target: Target = None, - ): - """Create new pass. - - Args: - granularity: Integer number representing the minimum time resolution to - define the pulse gate length in units of ``dt``. This value depends on - the control electronics of your quantum processor. - min_length: Integer number representing the minimum data point length to - define the pulse gate in units of ``dt``. This value depends on - the control electronics of your quantum processor. - target: The :class:`~.Target` representing the target backend, if - ``target`` is specified then this argument will take - precedence and ``granularity`` and ``min_length`` will be ignored. - """ - super().__init__() - self.granularity = granularity - self.min_length = min_length - if target is not None: - self.granularity = target.granularity - self.min_length = target.min_length - - def run(self, dag: DAGCircuit): - """Run the pulse gate validation attached to ``dag``. - - Args: - dag: DAG to be validated. - - Returns: - DAGCircuit: DAG with consistent timing and op nodes annotated with duration. - - Raises: - TranspilerError: When pulse gate violate pulse controller constraints. - """ - if self.granularity == 1 and self.min_length == 1: - # we can define arbitrary length pulse with dt resolution - return - - for gate, insts in dag.calibrations.items(): - for qubit_param_pair, schedule in insts.items(): - for _, inst in schedule.instructions: - if isinstance(inst, Play): - pulse = inst.pulse - if pulse.duration % self.granularity != 0: - raise TranspilerError( - f"Pulse duration is not multiple of {self.granularity}. " - "This pulse cannot be played on the specified backend. " - f"Please modify the duration of the custom gate pulse {pulse.name} " - f"which is associated with the gate {gate} of " - f"qubit {qubit_param_pair[0]}." - ) - if pulse.duration < self.min_length: - raise TranspilerError( - f"Pulse gate duration is less than {self.min_length}. " - "This pulse cannot be played on the specified backend. " - f"Please modify the duration of the custom gate pulse {pulse.name} " - f"which is associated with the gate {gate} of " - "qubit {qubit_param_pair[0]}." - ) diff --git a/qiskit/transpiler/preset_passmanagers/common.py b/qiskit/transpiler/preset_passmanagers/common.py index 8422ab209451..0f0a6b7ea0a7 100644 --- a/qiskit/transpiler/preset_passmanagers/common.py +++ b/qiskit/transpiler/preset_passmanagers/common.py @@ -41,11 +41,9 @@ from qiskit.transpiler.passes import ApplyLayout from qiskit.transpiler.passes import RemoveResetInZeroState from qiskit.transpiler.passes import FilterOpNodes -from qiskit.transpiler.passes import ValidatePulseGates from qiskit.transpiler.passes import PadDelay from qiskit.transpiler.passes import InstructionDurationCheck from qiskit.transpiler.passes import ConstrainedReschedule -from qiskit.transpiler.passes import PulseGates from qiskit.transpiler.passes import ContainsInstruction from qiskit.transpiler.passes import VF2PostLayout from qiskit.transpiler.passes.layout.vf2_layout import VF2LayoutStopReason @@ -53,7 +51,6 @@ from qiskit.transpiler.exceptions import TranspilerError from qiskit.transpiler.layout import Layout from qiskit.utils import deprecate_func -from qiskit.utils.deprecate_pulse import deprecate_pulse_arg _ControlFlowState = collections.namedtuple("_ControlFlowState", ("working", "not_working")) @@ -571,9 +568,8 @@ def _direction_condition(property_set): return PassManager(unroll) -@deprecate_pulse_arg("inst_map", predicate=lambda inst_map: inst_map is not None) def generate_scheduling( - instruction_durations, scheduling_method, timing_constraints, inst_map, target=None + instruction_durations, scheduling_method, timing_constraints, _, target=None ): """Generate a post optimization scheduling :class:`~qiskit.transpiler.PassManager` @@ -583,7 +579,6 @@ def generate_scheduling( ``'asap'``/``'as_soon_as_possible'`` or ``'alap'``/``'as_late_as_possible'`` timing_constraints (TimingConstraints): Hardware time alignment restrictions. - inst_map (InstructionScheduleMap): DEPRECATED. Mapping object that maps gate to schedule. target (Target): The :class:`~.Target` object representing the backend Returns: @@ -593,8 +588,6 @@ def generate_scheduling( TranspilerError: If the ``scheduling_method`` kwarg is not a valid value """ scheduling = PassManager() - if inst_map and inst_map.has_custom_gate(): - scheduling.append(PulseGates(inst_map=inst_map, target=target)) if scheduling_method: # Do scheduling after unit conversion. scheduler = { @@ -647,13 +640,6 @@ def _require_alignment(property_set): condition=_require_alignment, ) ) - scheduling.append( - ValidatePulseGates( - granularity=timing_constraints.granularity, - min_length=timing_constraints.min_length, - target=target, - ) - ) if scheduling_method: # Call padding pass if circuit is scheduled scheduling.append(PadDelay(target=target)) diff --git a/releasenotes/notes/remove-pulse-passes-3128f27ed7e42bf6.yaml b/releasenotes/notes/remove-pulse-passes-3128f27ed7e42bf6.yaml new file mode 100644 index 000000000000..49e176ce93b1 --- /dev/null +++ b/releasenotes/notes/remove-pulse-passes-3128f27ed7e42bf6.yaml @@ -0,0 +1,7 @@ +--- +upgrade_transpiler: + - | + The ``PulseGates``, ``ValidatePulseGates``, ``RXCalibrationBuilder``, ``RZXCalibrationBuilder``, + ``RZXCalibrationBuilderNoEcho`` and ``EchoRZXWeylDecomposition`` passes have been removed, + following their deprecation in Qiskit 1.3. These passes depend on and relate to the Pulse + package which is also being removed in Qiskit 2.0. diff --git a/test/python/compiler/test_transpiler.py b/test/python/compiler/test_transpiler.py index 851c6817d82f..4bf2b437d05c 100644 --- a/test/python/compiler/test_transpiler.py +++ b/test/python/compiler/test_transpiler.py @@ -79,7 +79,7 @@ from qiskit.providers.fake_provider import Fake20QV1, Fake27QPulseV1, GenericBackendV2 from qiskit.providers.basic_provider import BasicSimulator from qiskit.providers.options import Options -from qiskit.pulse import InstructionScheduleMap, Schedule, Play, Gaussian, DriveChannel +from qiskit.pulse import InstructionScheduleMap from qiskit.quantum_info import Operator, random_unitary from qiskit.utils import parallel from qiskit.transpiler import CouplingMap, Layout, PassManager, TransformationPass @@ -91,14 +91,13 @@ from qiskit.transpiler.target import ( InstructionProperties, Target, - TimingConstraints, InstructionDurations, target_to_backend_properties, ) from test import QiskitTestCase, combine, slow_test # pylint: disable=wrong-import-order -from ..legacy_cmaps import MELBOURNE_CMAP, RUESCHLIKON_CMAP, MUMBAI_CMAP, TOKYO_CMAP +from ..legacy_cmaps import MELBOURNE_CMAP, RUESCHLIKON_CMAP, TOKYO_CMAP class CustomCX(Gate): @@ -1567,53 +1566,6 @@ def test_scheduling_backend_v2(self): self.assertIn("delay", out[0].count_ops()) self.assertIn("delay", out[1].count_ops()) - def test_scheduling_timing_constraints(self): - """Test that scheduling-related loose transpile constraints - work with both BackendV1 and BackendV2.""" - - with self.assertWarns(DeprecationWarning): - backend_v1 = Fake27QPulseV1() - backend_v2 = GenericBackendV2( - num_qubits=27, - calibrate_instructions=True, - control_flow=True, - coupling_map=MUMBAI_CMAP, - seed=42, - ) - # the original timing constraints are granularity = min_length = 16 - timing_constraints = TimingConstraints(granularity=32, min_length=64) - error_msgs = { - 65: "Pulse duration is not multiple of 32", - 32: "Pulse gate duration is less than 64", - } - - for backend, duration in zip([backend_v1, backend_v2], [65, 32]): - with self.subTest(backend=backend, duration=duration): - qc = QuantumCircuit(2) - qc.h(0) - qc.cx(0, 1) - qc.measure_all() - with self.assertWarns(DeprecationWarning): - qc.add_calibration( - "h", - [0], - Schedule(Play(Gaussian(duration, 0.2, 4), DriveChannel(0))), - [0, 0], - ) - qc.add_calibration( - "cx", - [0, 1], - Schedule(Play(Gaussian(duration, 0.2, 4), DriveChannel(1))), - [0, 0], - ) - with self.assertRaisesRegex(TranspilerError, error_msgs[duration]): - with self.assertWarns(DeprecationWarning): - _ = transpile( - qc, - backend=backend, - timing_constraints=timing_constraints, - ) - def test_scheduling_instruction_constraints_backend(self): """Test that scheduling-related loose transpile constraints work with both BackendV1 and BackendV2.""" diff --git a/test/python/qpy/test_circuit_load_from_qpy.py b/test/python/qpy/test_circuit_load_from_qpy.py index 8890a45ffe9e..c95e54857759 100644 --- a/test/python/qpy/test_circuit_load_from_qpy.py +++ b/test/python/qpy/test_circuit_load_from_qpy.py @@ -18,12 +18,11 @@ from ddt import ddt, data from qiskit.circuit import QuantumCircuit, QuantumRegister, Qubit, Parameter, Gate -from qiskit.providers.fake_provider import Fake27QPulseV1, GenericBackendV2 +from qiskit.providers.fake_provider import GenericBackendV2 from qiskit.exceptions import QiskitError from qiskit.qpy import dump, load, formats, QPY_COMPATIBILITY_VERSION from qiskit.qpy.common import QPY_VERSION -from qiskit.transpiler import PassManager, TranspileLayout -from qiskit.transpiler import passes +from qiskit.transpiler import TranspileLayout from qiskit.compiler import transpile from qiskit.utils import optionals from qiskit.qpy.formats import FILE_HEADER_V10_PACK, FILE_HEADER_V10, FILE_HEADER_V10_SIZE @@ -55,42 +54,6 @@ def assert_roundtrip_equal(self, circuit, version=None, use_symengine=None): ) -@ddt -class TestCalibrationPasses(QpyCircuitTestCase): - """QPY round-trip test case of transpiled circuits with pulse level optimization.""" - - def setUp(self): - super().setUp() - # TODO remove context once https://github.com/Qiskit/qiskit/issues/12759 is fixed - with self.assertWarns(DeprecationWarning): - # This backend provides CX(0,1) with native ECR direction. - self.inst_map = Fake27QPulseV1().defaults().instruction_schedule_map - - @data(0.1, 0.7, 1.5) - def test_rzx_calibration(self, angle): - """RZX builder calibration pass with echo.""" - with self.assertWarns(DeprecationWarning): - pass_ = passes.RZXCalibrationBuilder(self.inst_map) - pass_manager = PassManager(pass_) - test_qc = QuantumCircuit(2) - test_qc.rzx(angle, 0, 1) - rzx_qc = pass_manager.run(test_qc) - with self.assertWarns(DeprecationWarning): - self.assert_roundtrip_equal(rzx_qc) - - @data(0.1, 0.7, 1.5) - def test_rzx_calibration_echo(self, angle): - """RZX builder calibration pass without echo.""" - with self.assertWarns(DeprecationWarning): - pass_ = passes.RZXCalibrationBuilderNoEcho(self.inst_map) - pass_manager = PassManager(pass_) - test_qc = QuantumCircuit(2) - test_qc.rzx(angle, 0, 1) - rzx_qc = pass_manager.run(test_qc) - with self.assertWarns(DeprecationWarning): - self.assert_roundtrip_equal(rzx_qc) - - class TestVersions(QpyCircuitTestCase): """Test version handling in qpy.""" diff --git a/test/python/transpiler/legacy_scheduling/test_instruction_alignments.py b/test/python/transpiler/legacy_scheduling/test_instruction_alignments.py index 38f84492ee86..aee567444cff 100644 --- a/test/python/transpiler/legacy_scheduling/test_instruction_alignments.py +++ b/test/python/transpiler/legacy_scheduling/test_instruction_alignments.py @@ -12,12 +12,10 @@ """Testing legacy instruction alignment pass.""" -from qiskit import QuantumCircuit, pulse +from qiskit import QuantumCircuit from qiskit.transpiler import InstructionDurations -from qiskit.transpiler.exceptions import TranspilerError from qiskit.transpiler.passes import ( AlignMeasures, - ValidatePulseGates, ALAPSchedule, TimeUnitConversion, ) @@ -327,101 +325,3 @@ def test_circuit_using_clbit(self): ref_circuit.measure(2, 0) self.assertEqual(aligned_circuit, ref_circuit) - - -class TestPulseGateValidation(QiskitTestCase): - """A test for pulse gate validation pass.""" - - def setUp(self): - super().setUp() - with self.assertWarns(DeprecationWarning): - self.pulse_gate_validation_pass = ValidatePulseGates(granularity=16, min_length=64) - - def test_invalid_pulse_duration(self): - """Kill pass manager if invalid pulse gate is found.""" - - # this is invalid duration pulse - # this will cause backend error since this doesn't fit with waveform memory chunk. - - with self.assertWarns(DeprecationWarning): - custom_gate = pulse.Schedule(name="custom_x_gate") - custom_gate.insert( - 0, pulse.Play(pulse.Constant(100, 0.1), pulse.DriveChannel(0)), inplace=True - ) - - circuit = QuantumCircuit(1) - circuit.x(0) - with self.assertWarns(DeprecationWarning): - circuit.add_calibration("x", qubits=(0,), schedule=custom_gate) - - with self.assertRaises(TranspilerError): - self.pulse_gate_validation_pass(circuit) - - def test_short_pulse_duration(self): - """Kill pass manager if invalid pulse gate is found.""" - - # this is invalid duration pulse - # this will cause backend error since this doesn't fit with waveform memory chunk. - with self.assertWarns(DeprecationWarning): - custom_gate = pulse.Schedule(name="custom_x_gate") - custom_gate.insert( - 0, pulse.Play(pulse.Constant(32, 0.1), pulse.DriveChannel(0)), inplace=True - ) - - circuit = QuantumCircuit(1) - circuit.x(0) - with self.assertWarns(DeprecationWarning): - circuit.add_calibration("x", qubits=(0,), schedule=custom_gate) - - with self.assertRaises(TranspilerError): - self.pulse_gate_validation_pass(circuit) - - def test_short_pulse_duration_multiple_pulse(self): - """Kill pass manager if invalid pulse gate is found.""" - - # this is invalid duration pulse - # however total gate schedule length is 64, which accidentally satisfies the constraints - # this should fail in the validation - with self.assertWarns(DeprecationWarning): - custom_gate = pulse.Schedule(name="custom_x_gate") - custom_gate.insert( - 0, pulse.Play(pulse.Constant(32, 0.1), pulse.DriveChannel(0)), inplace=True - ) - custom_gate.insert( - 32, pulse.Play(pulse.Constant(32, 0.1), pulse.DriveChannel(0)), inplace=True - ) - - circuit = QuantumCircuit(1) - circuit.x(0) - with self.assertWarns(DeprecationWarning): - circuit.add_calibration("x", qubits=(0,), schedule=custom_gate) - - with self.assertRaises(TranspilerError): - self.pulse_gate_validation_pass(circuit) - - def test_valid_pulse_duration(self): - """No error raises if valid calibration is provided.""" - - # this is valid duration pulse - with self.assertWarns(DeprecationWarning): - custom_gate = pulse.Schedule(name="custom_x_gate") - custom_gate.insert( - 0, pulse.Play(pulse.Constant(160, 0.1), pulse.DriveChannel(0)), inplace=True - ) - - circuit = QuantumCircuit(1) - circuit.x(0) - with self.assertWarns(DeprecationWarning): - circuit.add_calibration("x", qubits=(0,), schedule=custom_gate) - - # just not raise an error - self.pulse_gate_validation_pass(circuit) - - def test_no_calibration(self): - """No error raises if no calibration is added.""" - - circuit = QuantumCircuit(1) - circuit.x(0) - - # just not raise an error - self.pulse_gate_validation_pass(circuit) diff --git a/test/python/transpiler/test_calibrationbuilder.py b/test/python/transpiler/test_calibrationbuilder.py deleted file mode 100644 index 04bc8aef00da..000000000000 --- a/test/python/transpiler/test_calibrationbuilder.py +++ /dev/null @@ -1,619 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2020, 2024. -# -# 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 CalibrationBuilder subclasses.""" - -from math import pi, erf - -import numpy as np -from ddt import data, ddt - -from qiskit.converters import circuit_to_dag -from qiskit import circuit, schedule, QiskitError, QuantumCircuit -from qiskit.circuit import Parameter -from qiskit.circuit.library.standard_gates import SXGate, RXGate -from qiskit.providers.fake_provider import Fake7QPulseV1, Fake27QPulseV1, GenericBackendV2 -from qiskit.pulse import ( - ScheduleBlock, - ControlChannel, - DriveChannel, - GaussianSquare, - Waveform, - Play, - InstructionScheduleMap, - Schedule, - Drag, - Square, -) - -from qiskit.pulse import builder -from qiskit.pulse.transforms import target_qobj_transform -from qiskit.dagcircuit import DAGOpNode -from qiskit.transpiler import PassManager, Target, InstructionProperties -from qiskit.transpiler.passes.calibration.builders import ( - RZXCalibrationBuilder, - RZXCalibrationBuilderNoEcho, - RXCalibrationBuilder, -) -from test import QiskitTestCase # pylint: disable=wrong-import-order -from qiskit.utils.deprecate_pulse import decorate_test_methods, ignore_pulse_deprecation_warnings - - -@decorate_test_methods(ignore_pulse_deprecation_warnings) -class TestCalibrationBuilder(QiskitTestCase): - """Test the Calibration Builder.""" - - # CR parameters - __risefall = 4 - __angle = np.pi / 2 - __granularity = 16 - - @staticmethod - def get_cr_play(cr_schedule, name): - """A helper function to filter CR pulses.""" - - def _filter_func(time_inst): - return isinstance(time_inst[1], Play) and time_inst[1].pulse.name.startswith(name) - - return cr_schedule.filter(_filter_func).instructions[0][1] - - def compute_stretch_duration(self, play_gaussian_square_pulse, theta): - """Compute duration of stretched Gaussian Square pulse.""" - pulse = play_gaussian_square_pulse.pulse - sigma = pulse.sigma - width = self.compute_stretch_width(play_gaussian_square_pulse, theta) - - duration = width + sigma * self.__risefall - return round(duration / self.__granularity) * self.__granularity - - def compute_stretch_width(self, play_gaussian_square_pulse, theta): - """Compute width of stretched Gaussian Square pulse.""" - pulse = play_gaussian_square_pulse.pulse - sigma = pulse.sigma - width = pulse.width - - risefall_area = sigma * np.sqrt(2 * np.pi) * erf(self.__risefall) - full_area = risefall_area + width - - target_area = abs(theta) / self.__angle * full_area - return max(0, target_area - risefall_area) - - def u0p_play(self, cr_schedule): - """Returns the positive CR pulse from cr_schedule.""" - return self.get_cr_play(cr_schedule, "CR90p_u") - - def u0m_play(self, cr_schedule): - """Returns the negative CR pulse from cr_schedule.""" - return self.get_cr_play(cr_schedule, "CR90m_u") - - def d1p_play(self, cr_schedule): - """Returns the positive rotary echo pulse from cr_schedule.""" - return self.get_cr_play(cr_schedule, "CR90p_d") - - def d1m_play(self, cr_schedule): - """Returns the negative rotary echo pulse from cr_schedule.""" - return self.get_cr_play(cr_schedule, "CR90m_d") - - -@ddt -@decorate_test_methods(ignore_pulse_deprecation_warnings) -class TestRZXCalibrationBuilder(TestCalibrationBuilder): - """Test RZXCalibrationBuilder.""" - - def build_forward( - self, - backend, - theta, - u0p_play, - d1p_play, - u0m_play, - d1m_play, - ): - """A helper function to generate reference pulse schedule for forward direction.""" - duration = self.compute_stretch_duration(u0p_play, theta) - width = self.compute_stretch_width(u0p_play, theta) - inst_sched_map = backend.defaults().instruction_schedule_map - - with builder.build( - backend, - default_alignment="sequential", - ) as ref_sched: - - with builder.align_left(): - # Positive CRs - u0p_params = u0p_play.pulse.parameters - u0p_params["duration"] = duration - u0p_params["width"] = width - builder.play( - GaussianSquare(**u0p_params), - ControlChannel(0), - ) - d1p_params = d1p_play.pulse.parameters - d1p_params["duration"] = duration - d1p_params["width"] = width - builder.play( - GaussianSquare(**d1p_params), - DriveChannel(1), - ) - # Get Schedule for 'x' gate from the backend. - builder.call(inst_sched_map._get_calibration_entry("x", (0,)).get_schedule()) - - with builder.align_left(): - # Negative CRs - u0m_params = u0m_play.pulse.parameters - u0m_params["duration"] = duration - u0m_params["width"] = width - builder.play( - GaussianSquare(**u0m_params), - ControlChannel(0), - ) - d1m_params = d1m_play.pulse.parameters - d1m_params["duration"] = duration - d1m_params["width"] = width - builder.play( - GaussianSquare(**d1m_params), - DriveChannel(1), - ) - # Get Schedule for 'x' gate from the backend. - builder.call(inst_sched_map._get_calibration_entry("x", (0,)).get_schedule()) - - return ref_sched - - def build_reverse( - self, - backend, - theta, - u0p_play, - d1p_play, - u0m_play, - d1m_play, - ): - """A helper function to generate reference pulse schedule for backward direction.""" - duration = self.compute_stretch_duration(u0p_play, theta) - width = self.compute_stretch_width(u0p_play, theta) - inst_sched_map = backend.defaults().instruction_schedule_map - - rz_qc_q0 = QuantumCircuit(1) - rz_qc_q0.rz(pi / 2, 0) - - rz_qc_q1 = QuantumCircuit(2) - rz_qc_q1.rz(pi / 2, 1) - - with self.assertWarns(DeprecationWarning): - rz_sched_q0 = schedule(rz_qc_q0, backend) - rz_sched_q1 = schedule(rz_qc_q1, backend) - - with builder.build( - backend, - default_alignment="sequential", - ) as ref_sched: - - # Get Schedule from the backend for Gates equivalent to Hadamard gates. - with builder.align_left(): - builder.call(rz_sched_q0) - builder.call( - inst_sched_map._get_calibration_entry(SXGate(), qubits=(0,)).get_schedule() - ) - builder.call(rz_sched_q0) - - builder.call(rz_sched_q1) - builder.call( - inst_sched_map._get_calibration_entry(SXGate(), qubits=(1,)).get_schedule() - ) - builder.call(rz_sched_q1) - - with builder.align_left(): - # Positive CRs - u0p_params = u0p_play.pulse.parameters - u0p_params["duration"] = duration - u0p_params["width"] = width - builder.play( - GaussianSquare(**u0p_params), - ControlChannel(0), - ) - d1p_params = d1p_play.pulse.parameters - d1p_params["duration"] = duration - d1p_params["width"] = width - builder.play( - GaussianSquare(**d1p_params), - DriveChannel(1), - ) - - # Get Schedule for 'x' gate from the backend. - builder.call(inst_sched_map._get_calibration_entry("x", (0,)).get_schedule()) - - with builder.align_left(): - # Negative CRs - u0m_params = u0m_play.pulse.parameters - u0m_params["duration"] = duration - u0m_params["width"] = width - builder.play( - GaussianSquare(**u0m_params), - ControlChannel(0), - ) - d1m_params = d1m_play.pulse.parameters - d1m_params["duration"] = duration - d1m_params["width"] = width - builder.play( - GaussianSquare(**d1m_params), - DriveChannel(1), - ) - - # Get Schedule for 'x' gate from the backend. - builder.call(inst_sched_map._get_calibration_entry("x", (0,)).get_schedule()) - - # Get Schedule from the backend for Gates equivalent to Hadamard gates. - with builder.align_left(): - builder.call(rz_sched_q0) - builder.call( - inst_sched_map._get_calibration_entry(SXGate(), qubits=(0,)).get_schedule() - ) - builder.call(rz_sched_q0) - - builder.call(rz_sched_q1) - builder.call( - inst_sched_map._get_calibration_entry(SXGate(), qubits=(1,)).get_schedule() - ) - builder.call(rz_sched_q1) - - return ref_sched - - @data(-np.pi / 4, 0.1, np.pi / 4, np.pi / 2, np.pi) - def test_rzx_calibration_cr_pulse_stretch(self, theta: float): - """Test that cross resonance pulse durations are computed correctly.""" - with self.assertWarns(DeprecationWarning): - # TODO this tests does not work with BackendV2/GenericBackendV2 - # https://github.com/Qiskit/qiskit/issues/12834 - backend = Fake27QPulseV1() - inst_map = backend.defaults().instruction_schedule_map - cr_schedule = inst_map.get("cx", (0, 1)) - with builder.build() as test_sched: - RZXCalibrationBuilder.rescale_cr_inst(self.u0p_play(cr_schedule), theta) - - self.assertEqual( - test_sched.duration, self.compute_stretch_duration(self.u0p_play(cr_schedule), theta) - ) - - @data(-np.pi / 4, 0.1, np.pi / 4, np.pi / 2, np.pi) - def test_rzx_calibration_rotary_pulse_stretch(self, theta: float): - """Test that rotary pulse durations are computed correctly.""" - with self.assertWarns(DeprecationWarning): - backend = Fake27QPulseV1() - inst_map = backend.defaults().instruction_schedule_map - cr_schedule = inst_map.get("cx", (0, 1)) - with builder.build() as test_sched: - RZXCalibrationBuilder.rescale_cr_inst(self.d1p_play(cr_schedule), theta) - - self.assertEqual( - test_sched.duration, self.compute_stretch_duration(self.d1p_play(cr_schedule), theta) - ) - - def test_raise(self): - """Test that the correct error is raised.""" - theta = np.pi / 4 - - qc = circuit.QuantumCircuit(2) - qc.rzx(theta, 0, 1) - dag = circuit_to_dag(qc) - with self.assertWarns(DeprecationWarning): - backend = Fake7QPulseV1() - # The error is raised when calibrations in multi-qubit - # gates are not detected. - # We force this by removing the 'cx' entries from the - # instruction schedule map. - inst_map = backend.defaults().instruction_schedule_map - for qubits in inst_map.qubits_with_instruction("cx"): - inst_map.remove("cx", qubits) - inst_map = backend.defaults().instruction_schedule_map - with self.assertWarns(DeprecationWarning): - _pass = RZXCalibrationBuilder(inst_map) - - qubit_map = {qubit: i for i, qubit in enumerate(dag.qubits)} - with self.assertRaises(QiskitError): - for node in dag.gate_nodes(): - qubits = [qubit_map[q] for q in node.qargs] - _pass.get_calibration(node.op, qubits) - - def test_ecr_cx_forward(self): - """Test that correct pulse sequence is generated for native CR pair.""" - # Sufficiently large angle to avoid minimum duration, i.e. amplitude rescaling - theta = np.pi / 4 - - qc = circuit.QuantumCircuit(2) - qc.rzx(theta, 0, 1) - - with self.assertWarns(DeprecationWarning): - backend = Fake27QPulseV1() - inst_map = backend.defaults().instruction_schedule_map - _pass = RZXCalibrationBuilder(inst_map) - test_qc = PassManager(_pass).run(qc) - - cr_schedule = inst_map.get("cx", (0, 1)) - ref_sched = self.build_forward( - backend, - theta, - self.u0p_play(cr_schedule), - self.d1p_play(cr_schedule), - self.u0m_play(cr_schedule), - self.d1m_play(cr_schedule), - ) - - with self.assertWarns(DeprecationWarning): - self.assertEqual(schedule(test_qc, backend), target_qobj_transform(ref_sched)) - - def test_ecr_cx_reverse(self): - """Test that correct pulse sequence is generated for non-native CR pair.""" - # Sufficiently large angle to avoid minimum duration, i.e. amplitude rescaling - theta = np.pi / 4 - - qc = circuit.QuantumCircuit(2) - qc.rzx(theta, 1, 0) - - with self.assertWarns(DeprecationWarning): - backend = Fake27QPulseV1() - inst_map = backend.defaults().instruction_schedule_map - _pass = RZXCalibrationBuilder(inst_map) - test_qc = PassManager(_pass).run(qc) - - cr_schedule = inst_map.get("cx", (0, 1)) - ref_sched = self.build_reverse( - backend, - theta, - self.u0p_play(cr_schedule), - self.d1p_play(cr_schedule), - self.u0m_play(cr_schedule), - self.d1m_play(cr_schedule), - ) - - with self.assertWarns(DeprecationWarning): - self.assertEqual(schedule(test_qc, backend), target_qobj_transform(ref_sched)) - - def test_pass_alive_with_dcx_ish(self): - """Test if the pass is not terminated by error with direct CX input.""" - cx_sched = Schedule() - # Fake direct cr - cx_sched.insert(0, Play(GaussianSquare(800, 0.2, 64, 544), ControlChannel(1)), inplace=True) - # Fake direct compensation tone - # Compensation tone doesn't have dedicated pulse class. - # So it's reported as a waveform now. - compensation_tone = Waveform(0.1 * np.ones(800, dtype=complex)) - cx_sched.insert(0, Play(compensation_tone, DriveChannel(0)), inplace=True) - - with self.assertWarns(DeprecationWarning): - inst_map = InstructionScheduleMap() - inst_map.add("cx", (1, 0), schedule=cx_sched) - - theta = pi / 3 - rzx_qc = circuit.QuantumCircuit(2) - rzx_qc.rzx(theta, 1, 0) - - with self.assertWarns(DeprecationWarning): - pass_ = RZXCalibrationBuilder(instruction_schedule_map=inst_map) - with self.assertWarns(UserWarning): - # User warning that says q0 q1 is invalid - cal_qc = PassManager(pass_).run(rzx_qc) - self.assertEqual(cal_qc, rzx_qc) - - -@decorate_test_methods(ignore_pulse_deprecation_warnings) -class TestRZXCalibrationBuilderNoEcho(TestCalibrationBuilder): - """Test RZXCalibrationBuilderNoEcho.""" - - def build_forward( - self, - theta, - u0p_play, - d1p_play, - ): - """A helper function to generate reference pulse schedule for forward direction.""" - duration = self.compute_stretch_duration(u0p_play, 2.0 * theta) - width = self.compute_stretch_width(u0p_play, 2.0 * theta) - - with builder.build() as ref_sched: - # Positive CRs - u0p_params = u0p_play.pulse.parameters - u0p_params["duration"] = duration - u0p_params["width"] = width - builder.play( - GaussianSquare(**u0p_params), - ControlChannel(0), - ) - d1p_params = d1p_play.pulse.parameters - d1p_params["duration"] = duration - d1p_params["width"] = width - builder.play( - GaussianSquare(**d1p_params), - DriveChannel(1), - ) - builder.delay(duration, DriveChannel(0)) - - return ref_sched - - def test_ecr_cx_forward(self): - """Test that correct pulse sequence is generated for native CR pair. - - .. notes:: - No echo builder only supports native direction. - """ - # Sufficiently large angle to avoid minimum duration, i.e. amplitude rescaling - theta = np.pi / 4 - - qc = circuit.QuantumCircuit(2) - qc.rzx(theta, 0, 1) - - with self.assertWarns(DeprecationWarning): - backend = Fake27QPulseV1() - inst_map = backend.defaults().instruction_schedule_map - - _pass = RZXCalibrationBuilderNoEcho(inst_map) - test_qc = PassManager(_pass).run(qc) - - cr_schedule = inst_map.get("cx", (0, 1)) - ref_sched = self.build_forward( - theta, - self.u0p_play(cr_schedule), - self.d1p_play(cr_schedule), - ) - - with self.assertWarns(DeprecationWarning): - self.assertEqual(schedule(test_qc, backend), target_qobj_transform(ref_sched)) - - # # TODO - write test for forward ECR native pulse - # def test_ecr_forward(self): - - def test_pass_alive_with_dcx_ish(self): - """Test if the pass is not terminated by error with direct CX input.""" - cx_sched = Schedule() - # Fake direct cr - cx_sched.insert(0, Play(GaussianSquare(800, 0.2, 64, 544), ControlChannel(1)), inplace=True) - # Fake direct compensation tone - # Compensation tone doesn't have dedicated pulse class. - # So it's reported as a waveform now. - compensation_tone = Waveform(0.1 * np.ones(800, dtype=complex)) - cx_sched.insert(0, Play(compensation_tone, DriveChannel(0)), inplace=True) - - with self.assertWarns(DeprecationWarning): - inst_map = InstructionScheduleMap() - inst_map.add("cx", (1, 0), schedule=cx_sched) - - theta = pi / 3 - rzx_qc = circuit.QuantumCircuit(2) - rzx_qc.rzx(theta, 1, 0) - - with self.assertWarns(DeprecationWarning): - pass_ = RZXCalibrationBuilderNoEcho(instruction_schedule_map=inst_map) - with self.assertWarns(UserWarning): - # User warning that says q0 q1 is invalid - cal_qc = PassManager(pass_).run(rzx_qc) - self.assertEqual(cal_qc, rzx_qc) - - -@ddt -@decorate_test_methods(ignore_pulse_deprecation_warnings) -class TestRXCalibrationBuilder(QiskitTestCase): - """Test RXCalibrationBuilder.""" - - def compute_correct_rx_amplitude(self, rx_theta: float, sx_amp: float): - """A helper function to compute the amplitude of the bootstrapped RX pulse.""" - return sx_amp * (np.abs(rx_theta) / (0.5 * np.pi)) - - def test_not_supported_if_no_sx_schedule(self): - """Test that supported() returns False when the target does not have SX calibration.""" - empty_target = Target() - with self.assertWarns(DeprecationWarning): - tp = RXCalibrationBuilder(empty_target) - qubits = (0,) - node_op = DAGOpNode(RXGate(0.5), qubits, []) - self.assertFalse(tp.supported(node_op, qubits)) - - def test_not_supported_if_sx_not_drag(self): - """Test that supported() returns False when the default SX calibration is not a DRAG.""" - target = Target() - with builder.build() as square_sx_cal: - builder.play(Square(amp=0.1, duration=160, phase=0), DriveChannel(0)) - with self.assertWarns(DeprecationWarning): - target.add_instruction( - SXGate(), {(0,): InstructionProperties(calibration=square_sx_cal)} - ) - tp = RXCalibrationBuilder(target) - qubits = (0,) - node_op = DAGOpNode(RXGate(0.5), qubits, []) - self.assertFalse(tp.supported(node_op, qubits)) - - def test_raises_error_when_rotation_angle_not_assigned(self): - """Test that get_calibration() fails when the RX gate's rotation angle is - an unassigned Parameter, not a number. - The QiskitError occurs while trying to typecast the Parameter into a float. - """ - backend = GenericBackendV2(num_qubits=5, seed=42) - with self.assertWarns(DeprecationWarning): - tp = RXCalibrationBuilder(backend.target) - qubits = (0,) - rx = RXGate(Parameter("theta")) - with self.assertRaises(QiskitError): - tp.get_calibration(rx, qubits) - - # Note: These input data values should be within [0, pi] because - # the required NormalizeRXAngles pass ensures that. - @data(0, np.pi / 3, (2 / 3) * np.pi) - def test_pulse_schedule(self, theta: float): - """Test that get_calibration() returns a schedule with correct amplitude.""" - backend = GenericBackendV2(num_qubits=5, seed=42) - dummy_target = Target() - sx_amp, sx_beta, sx_sigma, sx_duration, sx_angle = 0.6, 2, 40, 160, 0.5 - with builder.build(backend=backend) as dummy_sx_cal: - builder.play( - Drag( - amp=sx_amp, beta=sx_beta, sigma=sx_sigma, duration=sx_duration, angle=sx_angle - ), - DriveChannel(0), - ) - - with self.assertWarns(DeprecationWarning): - dummy_target.add_instruction( - SXGate(), {(0,): InstructionProperties(calibration=dummy_sx_cal)} - ) - tp = RXCalibrationBuilder(dummy_target) - test = tp.get_calibration(RXGate(theta), qubits=(0,)) - - with builder.build(backend=backend) as correct_rx_schedule: - builder.play( - Drag( - amp=self.compute_correct_rx_amplitude(rx_theta=theta, sx_amp=sx_amp), - beta=sx_beta, - sigma=sx_sigma, - duration=sx_duration, - angle=0, - ), - channel=DriveChannel(0), - ) - - self.assertEqual(test, correct_rx_schedule) - - def test_with_normalizerxangles(self): - """Checks that this pass works well with the NormalizeRXAngles pass.""" - # add Drag pulse to 'sx' calibrations - sched = ScheduleBlock() - sched.append( - Play( - Drag( - duration=160, - sigma=40, - beta=-2.4030014266125312, - amp=0.11622814090041741, - angle=0.04477749999041481, - name="drag_2276", - ), - DriveChannel(0), - ), - inplace=True, - ) - with self.assertWarns(DeprecationWarning): - ism = InstructionScheduleMap() - ism.add("sx", (0,), sched) - with self.assertWarns(DeprecationWarning): - backend = GenericBackendV2(num_qubits=5, calibrate_instructions=ism, seed=42) - - # NormalizeRXAngle pass should also be included because it's a required pass. - with self.assertWarns(DeprecationWarning): - pm = PassManager(RXCalibrationBuilder(backend.target)) - - qc = QuantumCircuit(1) - qc.rx(np.pi / 3, 0) - qc.rx(np.pi / 2, 0) - qc.rx(np.pi, 0) - - # Only RX(pi/3) should get a rx calibration. - # The others should be converted to SX and X - tc = pm.run(qc) - with self.assertWarns(DeprecationWarning): - self.assertEqual(len(tc.calibrations["rx"]), 1) diff --git a/test/python/transpiler/test_echo_rzx_weyl_decomposition.py b/test/python/transpiler/test_echo_rzx_weyl_decomposition.py deleted file mode 100644 index 0f279e4bb8c5..000000000000 --- a/test/python/transpiler/test_echo_rzx_weyl_decomposition.py +++ /dev/null @@ -1,260 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2017, 2024. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. - -"""Test the EchoRZXWeylDecomposition pass""" - -import unittest -from math import pi -import numpy as np - -from qiskit import QuantumRegister, QuantumCircuit -from qiskit.transpiler.passes.optimization.echo_rzx_weyl_decomposition import ( - EchoRZXWeylDecomposition, -) -from qiskit.converters import circuit_to_dag, dag_to_circuit -from qiskit.providers.fake_provider import Fake27QPulseV1 -import qiskit.quantum_info as qi -from qiskit.synthesis.two_qubit.two_qubit_decompose import TwoQubitWeylDecomposition -from test import QiskitTestCase # pylint: disable=wrong-import-order - - -class TestEchoRZXWeylDecomposition(QiskitTestCase): - """Tests the EchoRZXWeylDecomposition pass.""" - - def setUp(self): - super().setUp() - # TODO once https://github.com/Qiskit/qiskit/issues/12759 is fixed, replace with - # backend = GenericBackendV2(num_qubits=27, calibrate_instructions=True, - # control_flow=True, seed=42) - # self.inst_map = backend.instruction_schedule_map - with self.assertWarns(DeprecationWarning): - self.backend = Fake27QPulseV1() - self.inst_map = self.backend.defaults().instruction_schedule_map - - def assertRZXgates(self, unitary_circuit, after): - """Check the number of rzx gates""" - alpha = TwoQubitWeylDecomposition(unitary_circuit).a - beta = TwoQubitWeylDecomposition(unitary_circuit).b - gamma = TwoQubitWeylDecomposition(unitary_circuit).c - - expected_rzx_number = 0 - if not alpha == 0: - expected_rzx_number += 2 - if not beta == 0: - expected_rzx_number += 2 - if not gamma == 0: - expected_rzx_number += 2 - - circuit_rzx_number = QuantumCircuit.count_ops(after)["rzx"] - - self.assertEqual(expected_rzx_number, circuit_rzx_number) - - @staticmethod - def count_gate_number(gate, circuit): - """Count the number of a specific gate type in a circuit""" - if gate not in QuantumCircuit.count_ops(circuit): - gate_number = 0 - else: - gate_number = QuantumCircuit.count_ops(circuit)[gate] - return gate_number - - def test_rzx_number_native_weyl_decomposition(self): - """Check the number of RZX gates for a hardware-native cx""" - qr = QuantumRegister(2, "qr") - circuit = QuantumCircuit(qr) - circuit.cx(qr[0], qr[1]) - - unitary_circuit = qi.Operator(circuit).data - with self.assertWarnsRegex( - DeprecationWarning, - expected_regex="The entire Qiskit Pulse package", - ): - after = EchoRZXWeylDecomposition(self.inst_map)(circuit) - - unitary_after = qi.Operator(after).data - - self.assertTrue(np.allclose(unitary_circuit, unitary_after)) - - # check whether the after circuit has the correct number of rzx gates. - self.assertRZXgates(unitary_circuit, after) - - def test_h_number_non_native_weyl_decomposition_1(self): - """Check the number of added Hadamard gates for a native and non-native rzz gate""" - theta = pi / 11 - qr = QuantumRegister(2, "qr") - # rzz gate in native direction - circuit = QuantumCircuit(qr) - circuit.rzz(theta, qr[0], qr[1]) - - # rzz gate in non-native direction - circuit_non_native = QuantumCircuit(qr) - circuit_non_native.rzz(theta, qr[1], qr[0]) - - dag = circuit_to_dag(circuit) - with self.assertWarnsRegex( - DeprecationWarning, - expected_regex="The entire Qiskit Pulse package", - ): - pass_ = EchoRZXWeylDecomposition(self.inst_map) - after = dag_to_circuit(pass_.run(dag)) - - dag_non_native = circuit_to_dag(circuit_non_native) - with self.assertWarnsRegex( - DeprecationWarning, - expected_regex="The entire Qiskit Pulse package", - ): - pass_ = EchoRZXWeylDecomposition(self.inst_map) - after_non_native = dag_to_circuit(pass_.run(dag_non_native)) - - circuit_rzx_number = self.count_gate_number("rzx", after) - - circuit_h_number = self.count_gate_number("h", after) - circuit_non_native_h_number = self.count_gate_number("h", after_non_native) - - # for each pair of rzx gates four hadamard gates have to be added in - # the case of a non-hardware-native directed gate. - self.assertEqual( - (circuit_rzx_number / 2) * 4, circuit_non_native_h_number - circuit_h_number - ) - - def test_h_number_non_native_weyl_decomposition_2(self): - """Check the number of added Hadamard gates for a swap gate""" - qr = QuantumRegister(2, "qr") - # swap gate in native direction. - circuit = QuantumCircuit(qr) - circuit.swap(qr[0], qr[1]) - - # swap gate in non-native direction. - circuit_non_native = QuantumCircuit(qr) - circuit_non_native.swap(qr[1], qr[0]) - - dag = circuit_to_dag(circuit) - with self.assertWarnsRegex( - DeprecationWarning, - expected_regex="The entire Qiskit Pulse package", - ): - pass_ = EchoRZXWeylDecomposition(self.inst_map) - after = dag_to_circuit(pass_.run(dag)) - - dag_non_native = circuit_to_dag(circuit_non_native) - with self.assertWarnsRegex( - DeprecationWarning, - expected_regex="The entire Qiskit Pulse package", - ): - pass_ = EchoRZXWeylDecomposition(self.inst_map) - after_non_native = dag_to_circuit(pass_.run(dag_non_native)) - - circuit_rzx_number = self.count_gate_number("rzx", after) - - circuit_h_number = self.count_gate_number("h", after) - circuit_non_native_h_number = self.count_gate_number("h", after_non_native) - - # for each pair of rzx gates four hadamard gates have to be added in - # the case of a non-hardware-native directed gate. - self.assertEqual( - (circuit_rzx_number / 2) * 4, circuit_non_native_h_number - circuit_h_number - ) - - def test_weyl_decomposition_gate_angles(self): - """Check the number and angles of the RZX gates for different gates""" - thetas = [pi / 9, 2.1, -0.2] - - qr = QuantumRegister(2, "qr") - circuit_rxx = QuantumCircuit(qr) - circuit_rxx.rxx(thetas[0], qr[1], qr[0]) - - circuit_ryy = QuantumCircuit(qr) - circuit_ryy.ryy(thetas[1], qr[0], qr[1]) - - circuit_rzz = QuantumCircuit(qr) - circuit_rzz.rzz(thetas[2], qr[1], qr[0]) - - circuits = [circuit_rxx, circuit_ryy, circuit_rzz] - - for circuit in circuits: - - unitary_circuit = qi.Operator(circuit).data - - dag = circuit_to_dag(circuit) - with self.assertWarnsRegex( - DeprecationWarning, - expected_regex="The entire Qiskit Pulse package", - ): - pass_ = EchoRZXWeylDecomposition(self.inst_map) - after = dag_to_circuit(pass_.run(dag)) - dag_after = circuit_to_dag(after) - - unitary_after = qi.Operator(after).data - - # check whether the unitaries are equivalent. - self.assertTrue(np.allclose(unitary_circuit, unitary_after)) - - # check whether the after circuit has the correct number of rzx gates. - self.assertRZXgates(unitary_circuit, after) - - alpha = TwoQubitWeylDecomposition(unitary_circuit).a - - rzx_angles = [] - for node in dag_after.two_qubit_ops(): - if node.name == "rzx": - rzx_angle = node.op.params[0] - # check whether the absolute values of the RZX gate angles - # are equivalent to the corresponding Weyl parameter. - self.assertAlmostEqual(np.abs(rzx_angle), alpha) - rzx_angles.append(rzx_angle) - - # check whether the angles of every RZX gate pair of an echoed RZX gate - # have opposite signs. - for idx in range(1, len(rzx_angles), 2): - self.assertAlmostEqual(rzx_angles[idx - 1], -rzx_angles[idx]) - - def test_weyl_unitaries_random_circuit(self): - """Weyl decomposition for a random two-qubit circuit.""" - theta = pi / 9 - epsilon = 5 - delta = -1 - eta = 0.2 - qr = QuantumRegister(2, "qr") - circuit = QuantumCircuit(qr) - - # random two-qubit circuit. - circuit.rzx(theta, 0, 1) - circuit.rzz(epsilon, 0, 1) - circuit.rz(eta, 0) - circuit.swap(1, 0) - circuit.h(0) - circuit.rzz(delta, 1, 0) - circuit.swap(0, 1) - circuit.cx(1, 0) - circuit.swap(0, 1) - circuit.h(1) - circuit.rxx(theta, 0, 1) - circuit.ryy(theta, 1, 0) - circuit.ecr(0, 1) - - unitary_circuit = qi.Operator(circuit).data - - dag = circuit_to_dag(circuit) - with self.assertWarnsRegex( - DeprecationWarning, - expected_regex="The entire Qiskit Pulse package", - ): - pass_ = EchoRZXWeylDecomposition(self.inst_map) - after = dag_to_circuit(pass_.run(dag)) - - unitary_after = qi.Operator(after).data - - self.assertTrue(np.allclose(unitary_circuit, unitary_after)) - - -if __name__ == "__main__": - unittest.main() diff --git a/test/python/transpiler/test_instruction_alignments.py b/test/python/transpiler/test_instruction_alignments.py deleted file mode 100644 index 74d5cc5e825e..000000000000 --- a/test/python/transpiler/test_instruction_alignments.py +++ /dev/null @@ -1,120 +0,0 @@ -# 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. - -"""Testing instruction alignment pass.""" - -from qiskit import QuantumCircuit, pulse -from qiskit.transpiler import PassManager -from qiskit.transpiler.exceptions import TranspilerError -from qiskit.transpiler.passes import ValidatePulseGates -from test import QiskitTestCase # pylint: disable=wrong-import-order -from qiskit.utils.deprecate_pulse import decorate_test_methods, ignore_pulse_deprecation_warnings - - -@decorate_test_methods(ignore_pulse_deprecation_warnings) -class TestPulseGateValidation(QiskitTestCase): - """A test for pulse gate validation pass.""" - - def test_invalid_pulse_duration(self): - """Kill pass manager if invalid pulse gate is found.""" - - # this is invalid duration pulse - # this will cause backend error since this doesn't fit with waveform memory chunk. - with self.assertWarns(DeprecationWarning): - custom_gate = pulse.Schedule(name="custom_x_gate") - custom_gate.insert( - 0, pulse.Play(pulse.Constant(100, 0.1), pulse.DriveChannel(0)), inplace=True - ) - - circuit = QuantumCircuit(1) - circuit.x(0) - with self.assertWarns(DeprecationWarning): - circuit.add_calibration("x", qubits=(0,), schedule=custom_gate) - - with self.assertWarns(DeprecationWarning): - pm = PassManager(ValidatePulseGates(granularity=16, min_length=64)) - with self.assertRaises(TranspilerError): - pm.run(circuit) - - def test_short_pulse_duration(self): - """Kill pass manager if invalid pulse gate is found.""" - - # this is invalid duration pulse - # this will cause backend error since this doesn't fit with waveform memory chunk. - custom_gate = pulse.Schedule(name="custom_x_gate") - custom_gate.insert( - 0, pulse.Play(pulse.Constant(32, 0.1), pulse.DriveChannel(0)), inplace=True - ) - - circuit = QuantumCircuit(1) - circuit.x(0) - with self.assertWarns(DeprecationWarning): - circuit.add_calibration("x", qubits=(0,), schedule=custom_gate) - - with self.assertWarns(DeprecationWarning): - pm = PassManager(ValidatePulseGates(granularity=16, min_length=64)) - with self.assertRaises(TranspilerError): - pm.run(circuit) - - def test_short_pulse_duration_multiple_pulse(self): - """Kill pass manager if invalid pulse gate is found.""" - - # this is invalid duration pulse - # however total gate schedule length is 64, which accidentally satisfies the constraints - # this should fail in the validation - custom_gate = pulse.Schedule(name="custom_x_gate") - custom_gate.insert( - 0, pulse.Play(pulse.Constant(32, 0.1), pulse.DriveChannel(0)), inplace=True - ) - custom_gate.insert( - 32, pulse.Play(pulse.Constant(32, 0.1), pulse.DriveChannel(0)), inplace=True - ) - - circuit = QuantumCircuit(1) - circuit.x(0) - with self.assertWarns(DeprecationWarning): - circuit.add_calibration("x", qubits=(0,), schedule=custom_gate) - - with self.assertWarns(DeprecationWarning): - pm = PassManager(ValidatePulseGates(granularity=16, min_length=64)) - with self.assertRaises(TranspilerError): - pm.run(circuit) - - def test_valid_pulse_duration(self): - """No error raises if valid calibration is provided.""" - - # this is valid duration pulse - custom_gate = pulse.Schedule(name="custom_x_gate") - custom_gate.insert( - 0, pulse.Play(pulse.Constant(160, 0.1), pulse.DriveChannel(0)), inplace=True - ) - - circuit = QuantumCircuit(1) - circuit.x(0) - with self.assertWarns(DeprecationWarning): - circuit.add_calibration("x", qubits=(0,), schedule=custom_gate) - - # just not raise an error - with self.assertWarns(DeprecationWarning): - pm = PassManager(ValidatePulseGates(granularity=16, min_length=64)) - pm.run(circuit) - - def test_no_calibration(self): - """No error raises if no calibration is added.""" - - circuit = QuantumCircuit(1) - circuit.x(0) - - # just not raise an error - with self.assertWarns(DeprecationWarning): - pm = PassManager(ValidatePulseGates(granularity=16, min_length=64)) - pm.run(circuit) diff --git a/test/python/transpiler/test_passmanager.py b/test/python/transpiler/test_passmanager.py index 6764745facd3..6a72ffb16a86 100644 --- a/test/python/transpiler/test_passmanager.py +++ b/test/python/transpiler/test_passmanager.py @@ -27,8 +27,7 @@ DoWhileController, ) from qiskit.transpiler import PassManager, PropertySet, TransformationPass -from qiskit.transpiler.passes import RXCalibrationBuilder -from qiskit.transpiler.passes import Optimize1qGates, BasisTranslator +from qiskit.transpiler.passes import Optimize1qGates, BasisTranslator, ResourceEstimation from qiskit.circuit.library.standard_gates.equivalence_library import ( StandardEquivalenceLibrary as std_eqlib, ) @@ -83,7 +82,7 @@ def callback(**kwargs): self.assertEqual("MyCircuit", calls[1]["dag"].name) def test_callback_with_pass_requires(self): - """Test the callback with a pass with another pass requirement.""" + """Test the callback with a pass with pass requirements.""" qr = QuantumRegister(3, "qr") circuit = QuantumCircuit(qr, name="MyCircuit") circuit.z(qr[0]) @@ -106,23 +105,29 @@ def callback(**kwargs): calls.append(out_dict) passmanager = PassManager() - with self.assertWarns(DeprecationWarning): - passmanager.append(RXCalibrationBuilder()) + passmanager.append(ResourceEstimation()) passmanager.run(circuit, callback=callback) - self.assertEqual(len(calls), 2) - self.assertEqual(len(calls[0]), 5) - self.assertEqual(calls[0]["count"], 0) - self.assertEqual(calls[0]["pass_"].name(), "NormalizeRXAngle") - self.assertEqual(expected_start_dag, calls[0]["dag"]) - self.assertIsInstance(calls[0]["time"], float) - self.assertIsInstance(calls[0]["property_set"], PropertySet) - self.assertEqual("MyCircuit", calls[0]["dag"].name) - self.assertEqual(len(calls[1]), 5) - self.assertEqual(calls[1]["count"], 1) - self.assertEqual(calls[1]["pass_"].name(), "RXCalibrationBuilder") - self.assertIsInstance(calls[0]["time"], float) - self.assertIsInstance(calls[0]["property_set"], PropertySet) - self.assertEqual("MyCircuit", calls[1]["dag"].name) + + self.assertEqual(len(calls), 7) + + required_passes = [ + "Depth", + "Width", + "Size", + "CountOps", + "NumTensorFactors", + "NumQubits", + "ResourceEstimation", + ] + + for call_entry in range(7): + self.assertEqual(len(calls[call_entry]), 5) + self.assertEqual(calls[call_entry]["count"], call_entry) + self.assertEqual(calls[call_entry]["pass_"].name(), required_passes[call_entry]) + self.assertEqual(expected_start_dag, calls[call_entry]["dag"]) + self.assertIsInstance(calls[call_entry]["time"], float) + self.assertIsInstance(calls[call_entry]["property_set"], PropertySet) + self.assertEqual("MyCircuit", calls[call_entry]["dag"].name) def test_to_flow_controller(self): """Test that conversion to a `FlowController` works, and the result can be added to a diff --git a/test/python/transpiler/test_pulse_gate_pass.py b/test/python/transpiler/test_pulse_gate_pass.py deleted file mode 100644 index e617f64d8859..000000000000 --- a/test/python/transpiler/test_pulse_gate_pass.py +++ /dev/null @@ -1,577 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2021, 2024. -# -# 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 pulse gate pass testing.""" - -import ddt - -from qiskit import pulse, circuit, transpile -from qiskit.providers.fake_provider import Fake27QPulseV1, GenericBackendV2 -from qiskit.providers.models.backendconfiguration import GateConfig -from qiskit.quantum_info.random import random_unitary -from test import QiskitTestCase # pylint: disable=wrong-import-order -from qiskit.utils.deprecate_pulse import decorate_test_methods, ignore_pulse_deprecation_warnings -from ..legacy_cmaps import BOGOTA_CMAP - - -@ddt.ddt -@decorate_test_methods(ignore_pulse_deprecation_warnings) -class TestPulseGate(QiskitTestCase): - """Integration test of pulse gate pass with custom backend.""" - - @ignore_pulse_deprecation_warnings - def setUp(self): - super().setUp() - - self.sched_param = circuit.Parameter("P0") - - with pulse.build(name="sx_q0") as custom_sx_q0: - pulse.play(pulse.Constant(100, 0.1), pulse.DriveChannel(0)) - self.custom_sx_q0 = custom_sx_q0 - - with pulse.build(name="sx_q1") as custom_sx_q1: - pulse.play(pulse.Constant(100, 0.2), pulse.DriveChannel(1)) - self.custom_sx_q1 = custom_sx_q1 - - with pulse.build(name="cx_q01") as custom_cx_q01: - pulse.play(pulse.Constant(100, 0.4), pulse.ControlChannel(0)) - self.custom_cx_q01 = custom_cx_q01 - - with pulse.build(name="my_gate_q0") as my_gate_q0: - pulse.shift_phase(self.sched_param, pulse.DriveChannel(0)) - pulse.play(pulse.Constant(120, 0.1), pulse.DriveChannel(0)) - self.my_gate_q0 = my_gate_q0 - - with pulse.build(name="my_gate_q1") as my_gate_q1: - pulse.shift_phase(self.sched_param, pulse.DriveChannel(1)) - pulse.play(pulse.Constant(120, 0.2), pulse.DriveChannel(1)) - self.my_gate_q1 = my_gate_q1 - - def test_transpile_with_bare_backend(self): - """Test transpile without custom calibrations.""" - with self.assertWarns(DeprecationWarning): - # TODO Move this test to backendV2 - backend = Fake27QPulseV1() - # Remove timing constraints to avoid triggering - # scheduling passes. - backend.configuration().timing_constraints = {} - - qc = circuit.QuantumCircuit(2) - qc.sx(0) - qc.x(0) - qc.rz(0, 0) - qc.sx(1) - qc.measure_all() - - with self.assertWarnsRegex( - DeprecationWarning, - expected_regex="The `transpile` function will " - "stop supporting inputs of type `BackendV1`", - ): - transpiled_qc = transpile(qc, backend, initial_layout=[0, 1]) - - ref_calibration = {} - with self.assertWarns(DeprecationWarning): - self.assertDictEqual(transpiled_qc.calibrations, ref_calibration) - - def test_transpile_with_backend_target(self): - """Test transpile without custom calibrations from target.""" - - with self.assertWarns(DeprecationWarning): - target = GenericBackendV2( - num_qubits=5, coupling_map=BOGOTA_CMAP, calibrate_instructions=True, seed=42 - ).target - - qc = circuit.QuantumCircuit(2) - qc.sx(0) - qc.x(0) - qc.rz(0, 0) - qc.sx(1) - qc.measure_all() - - transpiled_qc = transpile(qc, initial_layout=[0, 1], target=target) - - ref_calibration = {} - with self.assertWarns(DeprecationWarning): - self.assertDictEqual(transpiled_qc.calibrations, ref_calibration) - - def test_transpile_with_custom_basis_gate(self): - """Test transpile with custom calibrations.""" - with self.assertWarns(DeprecationWarning): - # TODO Move this test to backendV2 - backend = Fake27QPulseV1() - backend.defaults().instruction_schedule_map.add("sx", (0,), self.custom_sx_q0) - backend.defaults().instruction_schedule_map.add("sx", (1,), self.custom_sx_q1) - # Remove timing constraints to avoid triggering - # scheduling passes. - backend.configuration().timing_constraints = {} - - qc = circuit.QuantumCircuit(2) - qc.sx(0) - qc.x(0) - qc.rz(0, 0) - qc.sx(1) - qc.measure_all() - - with self.assertWarnsRegex( - DeprecationWarning, - expected_regex="The `transpile` function will " - "stop supporting inputs of type `BackendV1`", - ): - transpiled_qc = transpile(qc, backend, initial_layout=[0, 1]) - - ref_calibration = { - "sx": { - ((0,), ()): self.custom_sx_q0, - ((1,), ()): self.custom_sx_q1, - } - } - with self.assertWarns(DeprecationWarning): - self.assertDictEqual(transpiled_qc.calibrations, ref_calibration) - - def test_transpile_with_custom_basis_gate_in_target(self): - """Test transpile with custom calibrations.""" - with self.assertWarns(DeprecationWarning): - backend_pulse = Fake27QPulseV1() - target = GenericBackendV2( - num_qubits=5, - coupling_map=BOGOTA_CMAP, - calibrate_instructions=backend_pulse.defaults().instruction_schedule_map, - seed=42, - ).target - - target["sx"][(0,)].calibration = self.custom_sx_q0 - target["sx"][(1,)].calibration = self.custom_sx_q1 - - qc = circuit.QuantumCircuit(2) - qc.sx(0) - qc.x(0) - qc.rz(0, 0) - qc.sx(1) - qc.measure_all() - - transpiled_qc = transpile(qc, initial_layout=[0, 1], target=target) - - ref_calibration = { - "sx": { - ((0,), ()): self.custom_sx_q0, - ((1,), ()): self.custom_sx_q1, - } - } - with self.assertWarns(DeprecationWarning): - self.assertDictEqual(transpiled_qc.calibrations, ref_calibration) - - def test_transpile_with_instmap(self): - """Test providing instruction schedule map.""" - with self.assertWarns(DeprecationWarning): - # TODO Move this test to backendV2 - backend = Fake27QPulseV1() - instmap = backend.defaults().instruction_schedule_map - instmap.add("sx", (0,), self.custom_sx_q0) - instmap.add("sx", (1,), self.custom_sx_q1) - - # Inst map is renewed - with self.assertWarns(DeprecationWarning): - # TODO Move this test to backendV2 - backend = Fake27QPulseV1() - # Remove timing constraints to avoid triggering - # scheduling passes. - backend.configuration().timing_constraints = {} - - qc = circuit.QuantumCircuit(2) - qc.sx(0) - qc.x(0) - qc.rz(0, 0) - qc.sx(1) - qc.measure_all() - - with self.assertWarnsRegex( - DeprecationWarning, - expected_regex="The `transpile` function will " - "stop supporting inputs of type `BackendV1`", - ): - transpiled_qc = transpile(qc, backend, inst_map=instmap, initial_layout=[0, 1]) - - ref_calibration = { - "sx": { - ((0,), ()): self.custom_sx_q0, - ((1,), ()): self.custom_sx_q1, - } - } - with self.assertWarns(DeprecationWarning): - self.assertDictEqual(transpiled_qc.calibrations, ref_calibration) - - def test_transpile_with_custom_gate(self): - """Test providing non-basis gate.""" - with self.assertWarns(DeprecationWarning): - # TODO Move this test to backendV2 - backend = Fake27QPulseV1() - backend.defaults().instruction_schedule_map.add( - "my_gate", (0,), self.my_gate_q0, arguments=["P0"] - ) - backend.defaults().instruction_schedule_map.add( - "my_gate", (1,), self.my_gate_q1, arguments=["P0"] - ) - # Add gate to backend configuration - backend.configuration().basis_gates.append("my_gate") - with self.assertWarns(DeprecationWarning): - dummy_config = GateConfig( - name="my_gate", parameters=[], qasm_def="", coupling_map=[(0,), (1,)] - ) - backend.configuration().gates.append(dummy_config) - # Remove timing constraints to avoid triggering - # scheduling passes. - backend.configuration().timing_constraints = {} - - qc = circuit.QuantumCircuit(2) - qc.append(circuit.Gate("my_gate", 1, [1.0]), [0]) - qc.append(circuit.Gate("my_gate", 1, [2.0]), [1]) - - with self.assertWarnsRegex( - DeprecationWarning, - expected_regex="The `transpile` function will " - "stop supporting inputs of type `BackendV1`", - ): - transpiled_qc = transpile(qc, backend, basis_gates=["my_gate"], initial_layout=[0, 1]) - - my_gate_q0_1_0 = self.my_gate_q0.assign_parameters({self.sched_param: 1.0}, inplace=False) - my_gate_q1_2_0 = self.my_gate_q1.assign_parameters({self.sched_param: 2.0}, inplace=False) - - ref_calibration = { - "my_gate": { - ((0,), (1.0,)): my_gate_q0_1_0, - ((1,), (2.0,)): my_gate_q1_2_0, - } - } - with self.assertWarns(DeprecationWarning): - self.assertDictEqual(transpiled_qc.calibrations, ref_calibration) - - def test_transpile_with_parameterized_custom_gate(self): - """Test providing non-basis gate, which is kept parameterized throughout transpile.""" - with self.assertWarns(DeprecationWarning): - # TODO convert this to BackendV2/Target - backend = Fake27QPulseV1() - backend.defaults().instruction_schedule_map.add( - "my_gate", (0,), self.my_gate_q0, arguments=["P0"] - ) - # Add gate to backend configuration - backend.configuration().basis_gates.append("my_gate") - with self.assertWarns(DeprecationWarning): - dummy_config = GateConfig( - name="my_gate", parameters=[], qasm_def="", coupling_map=[(0,)] - ) - backend.configuration().gates.append(dummy_config) - # Remove timing constraints to avoid triggering - # scheduling passes. - backend.configuration().timing_constraints = {} - - param = circuit.Parameter("new_P0") - qc = circuit.QuantumCircuit(1) - qc.append(circuit.Gate("my_gate", 1, [param]), [0]) - - with self.assertWarnsRegex( - DeprecationWarning, - expected_regex="The `transpile` function will " - "stop supporting inputs of type `BackendV1`", - ): - transpiled_qc = transpile(qc, backend, basis_gates=["my_gate"], initial_layout=[0]) - - my_gate_q0_p = self.my_gate_q0.assign_parameters({self.sched_param: param}, inplace=False) - - ref_calibration = { - "my_gate": { - ((0,), (param,)): my_gate_q0_p, - } - } - with self.assertWarns(DeprecationWarning): - self.assertDictEqual(transpiled_qc.calibrations, ref_calibration) - - def test_transpile_with_multiple_circuits(self): - """Test transpile with multiple circuits with custom gate.""" - with self.assertWarns(DeprecationWarning): - # TODO move this test to backendV2 - backend = Fake27QPulseV1() - backend.defaults().instruction_schedule_map.add( - "my_gate", (0,), self.my_gate_q0, arguments=["P0"] - ) - # Add gate to backend configuration - backend.configuration().basis_gates.append("my_gate") - with self.assertWarns(DeprecationWarning): - dummy_config = GateConfig( - name="my_gate", parameters=[], qasm_def="", coupling_map=[(0,)] - ) - backend.configuration().gates.append(dummy_config) - # Remove timing constraints to avoid triggering - # scheduling passes. - backend.configuration().timing_constraints = {} - - params = [0.0, 1.0, 2.0, 3.0] - circs = [] - for param in params: - qc = circuit.QuantumCircuit(1) - qc.append(circuit.Gate("my_gate", 1, [param]), [0]) - circs.append(qc) - - with self.assertWarnsRegex( - DeprecationWarning, - expected_regex="The `transpile` function will " - "stop supporting inputs of type `BackendV1`", - ): - transpiled_qcs = transpile(circs, backend, basis_gates=["my_gate"], initial_layout=[0]) - - for param, transpiled_qc in zip(params, transpiled_qcs): - my_gate_q0_x = self.my_gate_q0.assign_parameters( - {self.sched_param: param}, inplace=False - ) - ref_calibration = {"my_gate": {((0,), (param,)): my_gate_q0_x}} - with self.assertWarns(DeprecationWarning): - self.assertDictEqual(transpiled_qc.calibrations, ref_calibration) - - def test_multiple_instructions_with_different_parameters(self): - """Test adding many instruction with different parameter binding.""" - with self.assertWarns(DeprecationWarning): - # TODO Move this test to backendV2 - backend = Fake27QPulseV1() - backend.defaults().instruction_schedule_map.add( - "my_gate", (0,), self.my_gate_q0, arguments=["P0"] - ) - # Add gate to backend configuration - backend.configuration().basis_gates.append("my_gate") - with self.assertWarns(DeprecationWarning): - dummy_config = GateConfig( - name="my_gate", parameters=[], qasm_def="", coupling_map=[(0,)] - ) - backend.configuration().gates.append(dummy_config) - # Remove timing constraints to avoid triggering - # scheduling passes. - backend.configuration().timing_constraints = {} - - qc = circuit.QuantumCircuit(1) - qc.append(circuit.Gate("my_gate", 1, [1.0]), [0]) - qc.append(circuit.Gate("my_gate", 1, [2.0]), [0]) - qc.append(circuit.Gate("my_gate", 1, [3.0]), [0]) - - with self.assertWarnsRegex( - DeprecationWarning, - expected_regex="The `transpile` function will " - "stop supporting inputs of type `BackendV1`", - ): - transpiled_qc = transpile(qc, backend, basis_gates=["my_gate"], initial_layout=[0]) - - my_gate_q0_1_0 = self.my_gate_q0.assign_parameters({self.sched_param: 1.0}, inplace=False) - my_gate_q0_2_0 = self.my_gate_q0.assign_parameters({self.sched_param: 2.0}, inplace=False) - my_gate_q0_3_0 = self.my_gate_q0.assign_parameters({self.sched_param: 3.0}, inplace=False) - - ref_calibration = { - "my_gate": { - ((0,), (1.0,)): my_gate_q0_1_0, - ((0,), (2.0,)): my_gate_q0_2_0, - ((0,), (3.0,)): my_gate_q0_3_0, - } - } - with self.assertWarns(DeprecationWarning): - self.assertDictEqual(transpiled_qc.calibrations, ref_calibration) - - def test_transpile_with_different_qubit(self): - """Test transpile with qubit without custom gate.""" - with self.assertWarns(DeprecationWarning): - # TODO Move this test to backendV2 - backend = Fake27QPulseV1() - backend.defaults().instruction_schedule_map.add("sx", (0,), self.custom_sx_q0) - # Remove timing constraints to avoid triggering - # scheduling passes. - backend.configuration().timing_constraints = {} - - qc = circuit.QuantumCircuit(1) - qc.sx(0) - qc.measure_all() - - with self.assertWarnsRegex( - DeprecationWarning, - expected_regex="The `transpile` function will " - "stop supporting inputs of type `BackendV1`", - ): - transpiled_qc = transpile(qc, backend, initial_layout=[3]) - - with self.assertWarns(DeprecationWarning): - self.assertDictEqual(transpiled_qc.calibrations, {}) - - @ddt.data(0, 1, 2, 3) - def test_transpile_with_both_instmap_and_empty_target(self, opt_level): - """Test when instmap and target are both provided - and only instmap contains custom schedules. - - Test case from Qiskit/qiskit-terra/#9489 - """ - with self.assertWarns(DeprecationWarning): - backend = Fake27QPulseV1() - instmap = backend.defaults().instruction_schedule_map - instmap.add("sx", (0,), self.custom_sx_q0) - instmap.add("sx", (1,), self.custom_sx_q1) - instmap.add("cx", (0, 1), self.custom_cx_q01) - - with self.assertWarns(DeprecationWarning): - backend_pulse = Fake27QPulseV1() - # This doesn't have custom schedule definition - target = GenericBackendV2( - num_qubits=5, - coupling_map=BOGOTA_CMAP, - calibrate_instructions=backend_pulse.defaults().instruction_schedule_map, - seed=42, - ).target - - qc = circuit.QuantumCircuit(2) - qc.append(random_unitary(4, seed=123), [0, 1]) - qc.measure_all() - - with self.assertWarns(DeprecationWarning): - transpiled_qc = transpile( - qc, - optimization_level=opt_level, - basis_gates=["sx", "rz", "x", "cx"], - inst_map=instmap, - target=target, - initial_layout=[0, 1], - ) - ref_calibration = { - "sx": { - ((0,), ()): self.custom_sx_q0, - ((1,), ()): self.custom_sx_q1, - }, - "cx": { - ((0, 1), ()): self.custom_cx_q01, - }, - } - with self.assertWarns(DeprecationWarning): - self.assertDictEqual(transpiled_qc.calibrations, ref_calibration) - - @ddt.data(0, 1, 2, 3) - def test_transpile_with_instmap_with_v2backend(self, opt_level): - """Test when instmap is provided with V2 backend. - - Test case from Qiskit/qiskit-terra/#9489 - """ - with self.assertWarns(DeprecationWarning): - backend = Fake27QPulseV1() - - instmap = backend.defaults().instruction_schedule_map - instmap.add("sx", (0,), self.custom_sx_q0) - instmap.add("sx", (1,), self.custom_sx_q1) - instmap.add("cx", (0, 1), self.custom_cx_q01) - - qc = circuit.QuantumCircuit(2) - qc.append(random_unitary(4, seed=123), [0, 1]) - qc.measure_all() - - with self.assertWarns(DeprecationWarning): - backend_pulse = Fake27QPulseV1() - - backend = GenericBackendV2( - num_qubits=5, - calibrate_instructions=backend_pulse.defaults().instruction_schedule_map, - seed=42, - ) - - with self.assertWarns(DeprecationWarning): - transpiled_qc = transpile( - qc, - backend, - optimization_level=opt_level, - inst_map=instmap, - initial_layout=[0, 1], - ) - ref_calibration = { - "sx": { - ((0,), ()): self.custom_sx_q0, - ((1,), ()): self.custom_sx_q1, - }, - "cx": { - ((0, 1), ()): self.custom_cx_q01, - }, - } - with self.assertWarns(DeprecationWarning): - self.assertDictEqual(transpiled_qc.calibrations, ref_calibration) - - @ddt.data(0, 1, 2, 3) - def test_transpile_with_instmap_with_v2backend_with_custom_gate(self, opt_level): - """Test when instmap is provided with V2 backend. - - In this test case, instmap contains a custom gate which doesn't belong to - Qiskit standard gate. Target must define a custom gete on the fly - to reflect user-provided instmap. - - Test case from Qiskit/qiskit-terra/#9489 - """ - with pulse.build(name="custom") as rabi12: - pulse.play(pulse.Constant(100, 0.4), pulse.DriveChannel(0)) - with self.assertWarns(DeprecationWarning): - backend = Fake27QPulseV1() - instmap = backend.defaults().instruction_schedule_map - instmap.add("rabi12", (0,), rabi12) - - gate = circuit.Gate("rabi12", 1, []) - qc = circuit.QuantumCircuit(1) - qc.append(gate, [0]) - qc.measure_all() - - with self.assertWarns(DeprecationWarning): - backend = GenericBackendV2(num_qubits=5, calibrate_instructions=True, seed=42) - transpiled_qc = transpile( - qc, - backend, - optimization_level=opt_level, - inst_map=instmap, - initial_layout=[0], - ) - ref_calibration = { - "rabi12": { - ((0,), ()): rabi12, - } - } - with self.assertWarns(DeprecationWarning): - self.assertDictEqual(transpiled_qc.calibrations, ref_calibration) - - def test_transpile_with_instmap_not_mutate_backend(self): - """Do not override default backend target when transpile with inst map. - - Providing an instmap for the transpile arguments may override target, - which might be pulled from the provided backend instance. - This should not override the source object since the same backend may - be used for future transpile without intention of instruction overriding. - """ - with self.assertWarns(DeprecationWarning): - backend = GenericBackendV2(num_qubits=5, calibrate_instructions=True, seed=42) - original_sx0 = backend.target["sx"][(0,)].calibration - - with self.assertWarns(DeprecationWarning): - backend_pulse = Fake27QPulseV1() - - instmap = backend_pulse.defaults().instruction_schedule_map - instmap.add("sx", (0,), self.custom_sx_q0) - - qc = circuit.QuantumCircuit(1) - qc.sx(0) - qc.measure_all() - - with self.assertWarns(DeprecationWarning): - transpiled_qc = transpile( - qc, - backend, - inst_map=instmap, - initial_layout=[0], - ) - self.assertTrue(transpiled_qc.has_calibration_for(transpiled_qc.data[0])) - - self.assertEqual( - backend.target["sx"][(0,)].calibration, - original_sx0, - ) From 28a33d7658f0eda94bbecf93bd093c393b544fcc Mon Sep 17 00:00:00 2001 From: Luciano Bello Date: Fri, 7 Feb 2025 16:21:31 +0100 Subject: [PATCH 19/48] Deprecation of MCMT in favor of MCMTGate (#13584) * remove pending deprecations in mcmt * reno * testing a regression --- .../circuit/library/generalized_gates/mcmt.py | 11 ++++---- .../followup_13150-5bd0c77248601e1a.yaml | 8 ++++++ test/python/circuit/library/test_mcmt.py | 28 ++++++++++++------- 3 files changed, 31 insertions(+), 16 deletions(-) create mode 100644 releasenotes/notes/followup_13150-5bd0c77248601e1a.yaml diff --git a/qiskit/circuit/library/generalized_gates/mcmt.py b/qiskit/circuit/library/generalized_gates/mcmt.py index 8e649b6a36cb..da580be9bf7f 100644 --- a/qiskit/circuit/library/generalized_gates/mcmt.py +++ b/qiskit/circuit/library/generalized_gates/mcmt.py @@ -49,7 +49,7 @@ class MCMT(QuantumCircuit): :class:`~qiskit.circuit.library.MCMTVChain`. """ - @deprecate_func(since="1.3", additional_msg="Use MCMTGate instead.", pending=True) + @deprecate_func(since="1.4", additional_msg="Use MCMTGate instead.") def __init__( self, gate: Gate | Callable[[QuantumCircuit, circuit.Qubit, circuit.Qubit], circuit.Instruction], @@ -76,7 +76,7 @@ def __init__( warnings.warn( "Passing a callable to MCMT is pending deprecation since Qiskit 1.3. Pass a " "gate instance or the gate name instead, e.g. pass 'h' instead of QuantumCircuit.h.", - category=PendingDeprecationWarning, + category=DeprecationWarning, stacklevel=2, ) gate = gate.__name__ @@ -84,7 +84,7 @@ def __init__( warnings.warn( "Passing a QuantumCircuit is pending deprecation since Qiskit 1.3. Pass a gate " "or turn the circuit into a gate using the ``to_gate`` method, instead.", - category=PendingDeprecationWarning, + category=DeprecationWarning, stacklevel=2, ) gate = gate.to_gate() @@ -158,9 +158,8 @@ class MCMTVChain(MCMT): """ @deprecate_func( - since="1.3", + since="1.4", additional_msg="Use MCMTGate with the V-chain synthesis plugin instead.", - pending=True, ) def __init__( self, @@ -277,7 +276,7 @@ def _identify_base_gate(gate): warnings.warn( "Passing a controlled gate to MCMT is pending deprecation since Qiskit 1.3. Pass a " "single-qubit gate instance or the gate name instead, e.g. pass 'h' instead of 'ch'.", - category=PendingDeprecationWarning, + category=DeprecationWarning, stacklevel=2, ) base_gate = gate.base_gate diff --git a/releasenotes/notes/followup_13150-5bd0c77248601e1a.yaml b/releasenotes/notes/followup_13150-5bd0c77248601e1a.yaml new file mode 100644 index 000000000000..6eaa0e7a0d43 --- /dev/null +++ b/releasenotes/notes/followup_13150-5bd0c77248601e1a.yaml @@ -0,0 +1,8 @@ +--- +deprecations_circuits: + - | + The Multiple-Control-Multiple-Target in :class:`~qiskit.circuit.library.generalized_gates.MCMT` is now deprecated + and replaced by :class:`.MCMTGate`, which is a proper :class:`.Gate` subclass. Using + a gate instead of a circuit allows the compiler to reason about the object at a higher + level of abstraction and allows for multiple synthesis plugins. + diff --git a/test/python/circuit/library/test_mcmt.py b/test/python/circuit/library/test_mcmt.py index 435ee0629593..cbc45710734e 100644 --- a/test/python/circuit/library/test_mcmt.py +++ b/test/python/circuit/library/test_mcmt.py @@ -51,7 +51,8 @@ class TestMCMT(QiskitTestCase): def test_mcmt_as_normal_control(self, mcmt_class): """Test that the MCMT can act as normal control gate.""" qc = QuantumCircuit(2) - mcmt = mcmt_class(gate=CHGate(), num_ctrl_qubits=1, num_target_qubits=1) + with self.assertWarns(DeprecationWarning): + mcmt = mcmt_class(gate=CHGate(), num_ctrl_qubits=1, num_target_qubits=1) qc = qc.compose(mcmt, [0, 1]) ref = QuantumCircuit(2) @@ -65,12 +66,14 @@ def test_mcmt_as_normal_control(self, mcmt_class): def test_missing_qubits(self): """Test that an error is raised if qubits are missing.""" with self.subTest(msg="no control qubits"): - with self.assertRaises(AttributeError): - _ = MCMT(XGate(), num_ctrl_qubits=0, num_target_qubits=1) + with self.assertWarns(DeprecationWarning): + with self.assertRaises(AttributeError): + _ = MCMT(XGate(), num_ctrl_qubits=0, num_target_qubits=1) with self.subTest(msg="no target qubits"): - with self.assertRaises(AttributeError): - _ = MCMT(ZGate(), num_ctrl_qubits=4, num_target_qubits=0) + with self.assertWarns(DeprecationWarning): + with self.assertRaises(AttributeError): + _ = MCMT(ZGate(), num_ctrl_qubits=4, num_target_qubits=0) def test_different_gate_types(self): """Test the different supported input types for the target gate.""" @@ -78,7 +81,8 @@ def test_different_gate_types(self): x_circ.x(0) for input_gate in [x_circ, QuantumCircuit.cx, QuantumCircuit.x, "cx", "x", CXGate()]: with self.subTest(input_gate=input_gate): - mcmt = MCMT(input_gate, 2, 2) + with self.assertWarns(DeprecationWarning): + mcmt = MCMT(input_gate, 2, 2) if isinstance(input_gate, QuantumCircuit): self.assertEqual(mcmt.gate.definition[0].operation, XGate()) self.assertEqual(len(mcmt.gate.definition), 1) @@ -89,13 +93,15 @@ def test_mcmt_v_chain_ancilla_test(self): """Test too few and too many ancillas for the MCMT V-chain mode.""" with self.subTest(msg="insufficient number of auxiliary qubits on gate"): qc = QuantumCircuit(5) - mcmt = MCMTVChain(ZGate(), 3, 1) + with self.assertWarns(DeprecationWarning): + mcmt = MCMTVChain(ZGate(), 3, 1) with self.assertRaises(QiskitError): qc.append(mcmt, range(5)) with self.subTest(msg="too many auxiliary qubits on gate"): qc = QuantumCircuit(9) - mcmt = MCMTVChain(ZGate(), 3, 1) + with self.assertWarns(DeprecationWarning): + mcmt = MCMTVChain(ZGate(), 3, 1) with self.assertRaises(QiskitError): qc.append(mcmt, range(9)) @@ -135,7 +141,8 @@ def test_mcmt_v_chain_simulation(self, cgate, num_controls, num_targets): for i in subset: qc.x(controls[i]) - mcmt = MCMTVChain(cgate, num_controls, num_targets) + with self.assertWarns(DeprecationWarning): + mcmt = MCMTVChain(cgate, num_controls, num_targets) qc.compose(mcmt, qubits, inplace=True) for i in subset: @@ -292,7 +299,8 @@ def test_mcmt_circuit_as_gate(self): """ circuit = QuantumCircuit(2) gate = RYGate(0.1) - mcmt = MCMT(gate=gate, num_ctrl_qubits=1, num_target_qubits=1) + with self.assertWarns(DeprecationWarning): + mcmt = MCMT(gate=gate, num_ctrl_qubits=1, num_target_qubits=1) circuit.append(mcmt, circuit.qubits) # append the MCMT circuit as gate called "MCMT" transpiled = transpile(circuit, basis_gates=["u", "cx"]) From 9ae0a808b45249d6f185baf54f078cb1ed00648a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elena=20Pe=C3=B1a=20Tapia?= <57907331+ElePT@users.noreply.github.com> Date: Fri, 7 Feb 2025 17:47:56 +0100 Subject: [PATCH 20/48] Remove `schedule`, `sequence` and related unit tests (#13809) * Remove schedule, sequence and related unit tests * Forgot the reno * Fix tests, lint, update reno * Attempt to fix docs (removing examples, but the functionality is deprecated and the whole docstring will go soon) --- qiskit/__init__.py | 4 +- qiskit/compiler/__init__.py | 6 +- qiskit/compiler/scheduler.py | 109 -- qiskit/compiler/sequencer.py | 71 - qiskit/pulse/builder.py | 149 +- qiskit/visualization/timeline/interface.py | 6 +- ...ve-schedule-sequence-a6249577da8d1c86.yaml | 10 + test/python/compiler/test_scheduler.py | 103 -- test/python/compiler/test_sequencer.py | 109 -- test/python/pulse/test_builder.py | 99 +- test/python/scheduler/__init__.py | 15 - test/python/scheduler/test_basic_scheduler.py | 1218 ----------------- 12 files changed, 17 insertions(+), 1882 deletions(-) delete mode 100644 qiskit/compiler/scheduler.py delete mode 100644 qiskit/compiler/sequencer.py create mode 100644 releasenotes/notes/remove-schedule-sequence-a6249577da8d1c86.yaml delete mode 100644 test/python/compiler/test_scheduler.py delete mode 100644 test/python/compiler/test_sequencer.py delete mode 100644 test/python/scheduler/__init__.py delete mode 100644 test/python/scheduler/test_basic_scheduler.py diff --git a/qiskit/__init__.py b/qiskit/__init__.py index c121c99112f8..376881a56799 100644 --- a/qiskit/__init__.py +++ b/qiskit/__init__.py @@ -127,7 +127,7 @@ _config = _user_config.get_config() -from qiskit.compiler import transpile, schedule, sequence +from qiskit.compiler import transpile from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager from .version import __version__ @@ -138,8 +138,6 @@ "QiskitError", "QuantumCircuit", "QuantumRegister", - "schedule", - "sequence", "transpile", "generate_preset_pass_manager", ] diff --git a/qiskit/compiler/__init__.py b/qiskit/compiler/__init__.py index cd3ed166a4dd..3d412611cf0c 100644 --- a/qiskit/compiler/__init__.py +++ b/qiskit/compiler/__init__.py @@ -17,15 +17,11 @@ .. currentmodule:: qiskit.compiler -Circuit and Pulse Compilation Functions +Circuit Compilation Functions ======================================= -.. autofunction:: schedule .. autofunction:: transpile -.. autofunction:: sequence """ from .transpiler import transpile -from .scheduler import schedule -from .sequencer import sequence diff --git a/qiskit/compiler/scheduler.py b/qiskit/compiler/scheduler.py deleted file mode 100644 index 9004dc24fd11..000000000000 --- a/qiskit/compiler/scheduler.py +++ /dev/null @@ -1,109 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2019. -# -# 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. - -""" -Convenience entry point into pulse scheduling, requiring only a circuit and a backend. For more -control over pulse scheduling, look at `qiskit.scheduler.schedule_circuit`. -""" -import logging - -from time import time -from typing import List, Optional, Union - -from qiskit.circuit.quantumcircuit import QuantumCircuit -from qiskit.exceptions import QiskitError -from qiskit.pulse import InstructionScheduleMap, Schedule -from qiskit.providers.backend import Backend -from qiskit.scheduler.config import ScheduleConfig -from qiskit.scheduler.schedule_circuit import schedule_circuit -from qiskit.utils.parallel import parallel_map -from qiskit.utils.deprecate_pulse import deprecate_pulse_dependency - -logger = logging.getLogger(__name__) - - -def _log_schedule_time(start_time, end_time): - log_msg = f"Total Scheduling Time - {((end_time - start_time) * 1000):.5f} (ms)" - logger.info(log_msg) - - -@deprecate_pulse_dependency(moving_to_dynamics=True) -def schedule( - circuits: Union[QuantumCircuit, List[QuantumCircuit]], - backend: Optional[Backend] = None, - inst_map: Optional[InstructionScheduleMap] = None, - meas_map: Optional[List[List[int]]] = None, - dt: Optional[float] = None, - method: Optional[Union[str, List[str]]] = None, -) -> Union[Schedule, List[Schedule]]: - """ - Schedule a circuit to a pulse ``Schedule``, using the backend, according to any specified - methods. Supported methods are documented in :py:mod:`qiskit.scheduler.schedule_circuit`. - - Args: - circuits: The quantum circuit or circuits to translate - backend: A backend instance, which contains hardware-specific data required for scheduling - inst_map: Mapping of circuit operations to pulse schedules. If ``None``, defaults to the - ``backend``\'s ``instruction_schedule_map`` - meas_map: List of sets of qubits that must be measured together. If ``None``, defaults to - the ``backend``\'s ``meas_map`` - dt: The output sample rate of backend control electronics. For scheduled circuits - which contain time information, dt is required. If not provided, it will be - obtained from the backend configuration - method: Optionally specify a particular scheduling method - - Returns: - A pulse ``Schedule`` that implements the input circuit - - Raises: - QiskitError: If ``inst_map`` and ``meas_map`` are not passed and ``backend`` is not passed - """ - arg_circuits_list = isinstance(circuits, list) - start_time = time() - if backend and getattr(backend, "version", 0) > 1: - if inst_map is None: - inst_map = backend.instruction_schedule_map - if meas_map is None: - meas_map = backend.meas_map - if dt is None: - dt = backend.dt - else: - if inst_map is None: - if backend is None: - raise QiskitError( - "Must supply either a backend or InstructionScheduleMap for scheduling passes." - ) - defaults = backend.defaults() - if defaults is None: - raise QiskitError( - "The backend defaults are unavailable. The backend may not support pulse." - ) - inst_map = defaults.instruction_schedule_map - if meas_map is None: - if backend is None: - raise QiskitError( - "Must supply either a backend or a meas_map for scheduling passes." - ) - meas_map = backend.configuration().meas_map - if dt is None: - if backend is not None: - dt = backend.configuration().dt - - schedule_config = ScheduleConfig(inst_map=inst_map, meas_map=meas_map, dt=dt) - circuits = circuits if isinstance(circuits, list) else [circuits] - schedules = parallel_map(schedule_circuit, circuits, (schedule_config, method, backend)) - end_time = time() - _log_schedule_time(start_time, end_time) - if arg_circuits_list: - return schedules - else: - return schedules[0] diff --git a/qiskit/compiler/sequencer.py b/qiskit/compiler/sequencer.py deleted file mode 100644 index 5a381918417b..000000000000 --- a/qiskit/compiler/sequencer.py +++ /dev/null @@ -1,71 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2020. -# -# 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. - -""" -Mapping a scheduled ``QuantumCircuit`` to a pulse ``Schedule``. -""" -from typing import List, Optional, Union - -from qiskit.circuit.quantumcircuit import QuantumCircuit -from qiskit.exceptions import QiskitError -from qiskit.providers.backend import Backend -from qiskit.pulse import InstructionScheduleMap, Schedule -from qiskit.scheduler import ScheduleConfig -from qiskit.scheduler.sequence import sequence as _sequence -from qiskit.utils.deprecate_pulse import deprecate_pulse_dependency - - -@deprecate_pulse_dependency(moving_to_dynamics=True) -def sequence( - scheduled_circuits: Union[QuantumCircuit, List[QuantumCircuit]], - backend: Optional[Backend] = None, - inst_map: Optional[InstructionScheduleMap] = None, - meas_map: Optional[List[List[int]]] = None, - dt: Optional[float] = None, -) -> Union[Schedule, List[Schedule]]: - """ - Schedule a scheduled circuit to a pulse ``Schedule``, using the backend. - - Args: - scheduled_circuits: Scheduled circuit(s) to be translated - backend: A backend instance, which contains hardware-specific data required for scheduling - inst_map: Mapping of circuit operations to pulse schedules. If ``None``, defaults to the - ``backend``\'s ``instruction_schedule_map`` - meas_map: List of sets of qubits that must be measured together. If ``None``, defaults to - the ``backend``\'s ``meas_map`` - dt: The output sample rate of backend control electronics. For scheduled circuits - which contain time information, dt is required. If not provided, it will be - obtained from the backend configuration - - Returns: - A pulse ``Schedule`` that implements the input circuit - - Raises: - QiskitError: If ``inst_map`` and ``meas_map`` are not passed and ``backend`` is not passed - """ - if inst_map is None: - if backend is None: - raise QiskitError("Must supply either a backend or inst_map for sequencing.") - inst_map = backend.defaults().instruction_schedule_map - if meas_map is None: - if backend is None: - raise QiskitError("Must supply either a backend or a meas_map for sequencing.") - meas_map = backend.configuration().meas_map - if dt is None: - if backend is None: - raise QiskitError("Must supply either a backend or a dt for sequencing.") - dt = backend.configuration().dt - - schedule_config = ScheduleConfig(inst_map=inst_map, meas_map=meas_map, dt=dt) - circuits = scheduled_circuits if isinstance(scheduled_circuits, list) else [scheduled_circuits] - schedules = [_sequence(circuit, schedule_config) for circuit in circuits] - return schedules[0] if len(schedules) == 1 else schedules diff --git a/qiskit/pulse/builder.py b/qiskit/pulse/builder.py index bc057980cf09..70ecc9d11dfa 100644 --- a/qiskit/pulse/builder.py +++ b/qiskit/pulse/builder.py @@ -89,160 +89,13 @@ representations, while simultaneously applying a long decoupling pulse to a neighboring qubit. We terminate the experiment with a measurement to observe the state we prepared. This program which mixes circuits and pulses will be -automatically lowered to be run as a pulse program: - -.. plot:: - :alt: Output from the previous code. - :include-source: - - from math import pi - from qiskit.compiler import schedule - from qiskit.circuit import QuantumCircuit - - from qiskit import pulse - from qiskit.providers.fake_provider import GenericBackendV2 - - backend = GenericBackendV2(num_qubits=5, calibrate_instructions=True) - - d2 = pulse.DriveChannel(2) - - qc = QuantumCircuit(2) - # Hadamard - qc.rz(pi/2, 0) - qc.sx(0) - qc.rz(pi/2, 0) - - qc.cx(0, 1) - - bell_sched = schedule(qc, backend) - - with pulse.build(backend) as decoupled_bell_prep_and_measure: - # We call our bell state preparation schedule constructed above. - with pulse.align_right(): - pulse.call(bell_sched) - pulse.play(pulse.Constant(bell_sched.duration, 0.02), d2) - pulse.barrier(0, 1, 2) - registers = pulse.measure_all() - - decoupled_bell_prep_and_measure.draw() - +automatically lowered to be run as a pulse program. With the pulse builder we are able to blend programming on qubits and channels. While the pulse schedule is based on instructions that operate on channels, the pulse builder automatically handles the mapping from qubits to channels for you. -In the example below we demonstrate some more features of the pulse builder: - -.. plot:: - :include-source: - :nofigs: - - import math - from qiskit.compiler import schedule - - from qiskit import pulse, QuantumCircuit - from qiskit.providers.fake_provider import FakeOpenPulse2Q - - backend = FakeOpenPulse2Q() - - qc = QuantumCircuit(2, 2) - qc.cx(0, 1) - - with pulse.build(backend) as pulse_prog: - # Create a pulse. - gaussian_pulse = pulse.Gaussian(10, 1.0, 2) - # Get the qubit's corresponding drive channel from the backend. - d0 = pulse.drive_channel(0) - d1 = pulse.drive_channel(1) - # Play a pulse at t=0. - pulse.play(gaussian_pulse, d0) - # Play another pulse directly after the previous pulse at t=10. - pulse.play(gaussian_pulse, d0) - # The default scheduling behavior is to schedule pulses in parallel - # across channels. For example, the statement below - # plays the same pulse on a different channel at t=0. - pulse.play(gaussian_pulse, d1) - - # We also provide pulse scheduling alignment contexts. - # The default alignment context is align_left. - - # The sequential context schedules pulse instructions sequentially in time. - # This context starts at t=10 due to earlier pulses above. - with pulse.align_sequential(): - pulse.play(gaussian_pulse, d0) - # Play another pulse after at t=20. - pulse.play(gaussian_pulse, d1) - - # We can also nest contexts as each instruction is - # contained in its local scheduling context. - # The output of a child context is a context-schedule - # with the internal instructions timing fixed relative to - # one another. This is schedule is then called in the parent context. - - # Context starts at t=30. - with pulse.align_left(): - # Start at t=30. - pulse.play(gaussian_pulse, d0) - # Start at t=30. - pulse.play(gaussian_pulse, d1) - # Context ends at t=40. - - # Alignment context where all pulse instructions are - # aligned to the right, ie., as late as possible. - with pulse.align_right(): - # Shift the phase of a pulse channel. - pulse.shift_phase(math.pi, d1) - # Starts at t=40. - pulse.delay(100, d0) - # Ends at t=140. - - # Starts at t=130. - pulse.play(gaussian_pulse, d1) - # Ends at t=140. - - # Acquire data for a qubit and store in a memory slot. - pulse.acquire(100, 0, pulse.MemorySlot(0)) - - # We also support a variety of macros for common operations. - - # Measure all qubits. - pulse.measure_all() - - # Delay on some qubits. - # This requires knowledge of which channels belong to which qubits. - # delay for 100 cycles on qubits 0 and 1. - pulse.delay_qubits(100, 0, 1) - - # Call a schedule for a quantum circuit thereby inserting into - # the pulse schedule. - qc = QuantumCircuit(2, 2) - qc.cx(0, 1) - qc_sched = schedule(qc, backend) - pulse.call(qc_sched) - - - # It is also be possible to call a preexisting schedule - tmp_sched = pulse.Schedule() - tmp_sched += pulse.Play(gaussian_pulse, d0) - pulse.call(tmp_sched) - - # We also support: - - # frequency instructions - pulse.set_frequency(5.0e9, d0) - - # phase instructions - pulse.shift_phase(0.1, d0) - - # offset contexts - with pulse.phase_offset(math.pi, d0): - pulse.play(gaussian_pulse, d0) - - -The above is just a small taste of what is possible with the builder. See the rest of the module -documentation for more information on its capabilities. - .. autofunction:: build diff --git a/qiskit/visualization/timeline/interface.py b/qiskit/visualization/timeline/interface.py index 50dd006633a7..1818f78a9073 100644 --- a/qiskit/visualization/timeline/interface.py +++ b/qiskit/visualization/timeline/interface.py @@ -301,7 +301,7 @@ def draw( :alt: Output from the previous code. :include-source: - from qiskit import QuantumCircuit, transpile, schedule + from qiskit import QuantumCircuit, transpile from qiskit.visualization.timeline import draw from qiskit.providers.fake_provider import GenericBackendV2 @@ -318,7 +318,7 @@ def draw( :alt: Output from the previous code. :include-source: - from qiskit import QuantumCircuit, transpile, schedule + from qiskit import QuantumCircuit, transpile from qiskit.visualization.timeline import draw, IQXSimple from qiskit.providers.fake_provider import GenericBackendV2 @@ -335,7 +335,7 @@ def draw( :alt: Output from the previous code. :include-source: - from qiskit import QuantumCircuit, transpile, schedule + from qiskit import QuantumCircuit, transpile from qiskit.visualization.timeline import draw, IQXDebugging from qiskit.providers.fake_provider import GenericBackendV2 diff --git a/releasenotes/notes/remove-schedule-sequence-a6249577da8d1c86.yaml b/releasenotes/notes/remove-schedule-sequence-a6249577da8d1c86.yaml new file mode 100644 index 000000000000..b52843040085 --- /dev/null +++ b/releasenotes/notes/remove-schedule-sequence-a6249577da8d1c86.yaml @@ -0,0 +1,10 @@ +--- +upgrade: + - | + The functions ``sequence`` and ``schedule`` from the :mod:`.compiler` + module have been removed following their deprecation in Qiskit 1.3. + They relied on being able to translate circuits to pulse using backend + definitions, a capability that is no longer present. For this reason + they have been removed with no proposed alternative. + Note that this removals relate to the Pulse package which is + also being removed in Qiskit 2.0. \ No newline at end of file diff --git a/test/python/compiler/test_scheduler.py b/test/python/compiler/test_scheduler.py deleted file mode 100644 index c349bf054c3b..000000000000 --- a/test/python/compiler/test_scheduler.py +++ /dev/null @@ -1,103 +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. - -"""Scheduler Test.""" - -from qiskit.circuit import QuantumRegister, ClassicalRegister, QuantumCircuit -from qiskit.exceptions import QiskitError -from qiskit.pulse import InstructionScheduleMap, Schedule -from qiskit.providers.fake_provider import FakeOpenPulse3Q, GenericBackendV2 -from qiskit.compiler.scheduler import schedule -from test import QiskitTestCase # pylint: disable=wrong-import-order - - -class TestCircuitScheduler(QiskitTestCase): - """Tests for scheduling.""" - - def setUp(self): - super().setUp() - qr = QuantumRegister(2, name="q") - cr = ClassicalRegister(2, name="c") - self.circ = QuantumCircuit(qr, cr, name="circ") - self.circ.cx(qr[0], qr[1]) - self.circ.measure(qr, cr) - - qr2 = QuantumRegister(2, name="q") - cr2 = ClassicalRegister(2, name="c") - self.circ2 = QuantumCircuit(qr2, cr2, name="circ2") - self.circ2.cx(qr2[0], qr2[1]) - self.circ2.measure(qr2, cr2) - - with self.assertWarns(DeprecationWarning): - self.backend = GenericBackendV2( - 3, calibrate_instructions=True, basis_gates=["cx", "u1", "u2", "u3"], seed=42 - ) - - def test_instruction_map_and_backend_not_supplied(self): - """Test instruction map and backend not supplied.""" - with self.assertRaisesRegex( - QiskitError, - r"Must supply either a backend or InstructionScheduleMap for scheduling passes.", - ): - with self.assertWarns(DeprecationWarning): - schedule(self.circ) - - def test_instruction_map_and_backend_defaults_unavailable(self): - """Test backend defaults unavailable when backend is provided, but instruction map is not.""" - with self.assertWarns(DeprecationWarning): - self.backend = FakeOpenPulse3Q() - self.backend._defaults = None - with self.assertRaisesRegex( - QiskitError, r"The backend defaults are unavailable. The backend may not support pulse." - ): - with self.assertWarns(DeprecationWarning): - schedule(self.circ, self.backend) - - def test_measurement_map_and_backend_not_supplied(self): - """Test measurement map and backend not supplied.""" - with self.assertRaisesRegex( - QiskitError, - r"Must supply either a backend or a meas_map for scheduling passes.", - ): - with self.assertWarns(DeprecationWarning): - schedule(self.circ, inst_map=InstructionScheduleMap()) - - def test_schedules_single_circuit(self): - """Test scheduling of a single circuit.""" - with self.assertWarns(DeprecationWarning): - circuit_schedule = schedule(self.circ, self.backend) - - self.assertIsInstance(circuit_schedule, Schedule) - self.assertEqual(circuit_schedule.name, "circ") - - def test_schedules_multiple_circuits(self): - """Test scheduling of multiple circuits.""" - self.enable_parallel_processing() - - circuits = [self.circ, self.circ2] - with self.assertWarns(DeprecationWarning): - circuit_schedules = schedule(circuits, self.backend, method="asap") - self.assertEqual(len(circuit_schedules), len(circuits)) - - circuit_one_schedule = circuit_schedules[0] - circuit_two_schedule = circuit_schedules[1] - - with self.assertWarns(DeprecationWarning): - self.assertEqual( - circuit_one_schedule, - schedule(self.circ, self.backend, method="asap"), - ) - - self.assertEqual( - circuit_two_schedule, - schedule(self.circ2, self.backend, method="asap"), - ) diff --git a/test/python/compiler/test_sequencer.py b/test/python/compiler/test_sequencer.py deleted file mode 100644 index 3fcfc16674a6..000000000000 --- a/test/python/compiler/test_sequencer.py +++ /dev/null @@ -1,109 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2020, 2024. -# -# 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. - -# pylint: disable=missing-function-docstring - -"""Tests basic functionality of the sequence function""" -# TODO with the removal of pulses, this file can be removed too. - -import unittest - -from qiskit import QuantumCircuit, pulse -from qiskit.compiler import sequence, transpile, schedule -from qiskit.pulse.transforms import pad -from qiskit.providers.fake_provider import Fake127QPulseV1 -from test import QiskitTestCase # pylint: disable=wrong-import-order - - -class TestSequence(QiskitTestCase): - """Test sequence function.""" - - def setUp(self): - super().setUp() - with self.assertWarns(DeprecationWarning): - self.backend = Fake127QPulseV1() - self.backend.configuration().timing_constraints = {} - - def test_sequence_empty(self): - with self.assertWarns(DeprecationWarning): - self.assertEqual(sequence([], self.backend), []) - - def test_transpile_and_sequence_agree_with_schedule(self): - qc = QuantumCircuit(2, name="bell") - qc.h(0) - qc.cx(0, 1) - qc.measure_all() - with self.assertWarnsRegex( - DeprecationWarning, - expected_regex="The `transpile` function will " - "stop supporting inputs of type `BackendV1`", - ): - sc = transpile(qc, self.backend, scheduling_method="alap") - with self.assertWarns(DeprecationWarning): - actual = sequence(sc, self.backend) - with self.assertWarnsRegex( - DeprecationWarning, - expected_regex="The `transpile` function will " - "stop supporting inputs of type `BackendV1`", - ): - expected = schedule(transpile(qc, self.backend), self.backend) - with self.assertWarns(DeprecationWarning): - # pad adds Delay which is deprecated - self.assertEqual(actual, pad(expected)) - - def test_transpile_and_sequence_agree_with_schedule_for_circuit_with_delay(self): - qc = QuantumCircuit(1, 1, name="t2") - qc.h(0) - qc.delay(500, 0, unit="ns") - qc.h(0) - qc.measure(0, 0) - with self.assertWarnsRegex( - DeprecationWarning, - expected_regex="The `transpile` function will " - "stop supporting inputs of type `BackendV1`", - ): - sc = transpile(qc, self.backend, scheduling_method="alap") - with self.assertWarns(DeprecationWarning): - actual = sequence(sc, self.backend) - with self.assertWarnsRegex( - DeprecationWarning, - expected_regex="The `transpile` function will " - "stop supporting inputs of type `BackendV1`", - ): - expected = schedule(transpile(qc, self.backend), self.backend) - with self.assertWarns(DeprecationWarning): - self.assertEqual( - actual.exclude(instruction_types=[pulse.Delay]), - expected.exclude(instruction_types=[pulse.Delay]), - ) - - @unittest.skip("not yet determined if delays on ancilla should be removed or not") - def test_transpile_and_sequence_agree_with_schedule_for_circuits_without_measures(self): - qc = QuantumCircuit(2, name="bell_without_measurement") - qc.h(0) - qc.cx(0, 1) - with self.assertWarnsRegex( - DeprecationWarning, - expected_regex="The `transpile` function will " - "stop supporting inputs of type `BackendV1`", - ): - sc = transpile(qc, self.backend, scheduling_method="alap") - with self.assertWarns(DeprecationWarning): - actual = sequence(sc, self.backend) - with self.assertWarnsRegex( - DeprecationWarning, - expected_regex="The `transpile` function will " - "stop supporting inputs of type `BackendV1`", - ): - with self.assertWarns(DeprecationWarning): - expected = schedule(transpile(qc, self.backend), self.backend) - self.assertEqual(actual, pad(expected)) diff --git a/test/python/pulse/test_builder.py b/test/python/pulse/test_builder.py index 9501d176c9e0..dcc113f5742a 100644 --- a/test/python/pulse/test_builder.py +++ b/test/python/pulse/test_builder.py @@ -12,10 +12,9 @@ """Test pulse builder context utilities.""" -from math import pi import numpy as np -from qiskit import circuit, compiler, pulse +from qiskit import circuit, pulse from qiskit.pulse import builder, exceptions, macros from qiskit.pulse.instructions import directives from qiskit.pulse.transforms import target_qobj_transform @@ -764,102 +763,6 @@ def test_delay_qubits(self): class TestBuilderComposition(TestBuilder): """Test more sophisticated composite builder examples.""" - def test_complex_build(self): - """Test a general program build with nested contexts, - circuits and macros.""" - d0 = pulse.DriveChannel(0) - d1 = pulse.DriveChannel(1) - d2 = pulse.DriveChannel(2) - delay_dur = 30 - short_dur = 20 - long_dur = 49 - - def get_sched(qubit_idx: [int], backend): - qc = circuit.QuantumCircuit(2) - for idx in qubit_idx: - qc.append(circuit.library.U2Gate(0, pi / 2), [idx]) - with self.assertWarnsRegex( - DeprecationWarning, - expected_regex="The `transpile` function will " - "stop supporting inputs of type `BackendV1`", - ): - transpiled = compiler.transpile(qc, backend=backend, optimization_level=1) - with self.assertWarns(DeprecationWarning): - return compiler.schedule(transpiled, backend) - - with pulse.build(self.backend) as schedule: - with pulse.align_sequential(): - pulse.delay(delay_dur, d0) - pulse.call(get_sched([1], self.backend)) - - with pulse.align_right(): - pulse.play(library.Constant(short_dur, 0.1), d1) - pulse.play(library.Constant(long_dur, 0.1), d2) - pulse.call(get_sched([1], self.backend)) - - with pulse.align_left(): - pulse.call(get_sched([0, 1, 0], self.backend)) - - pulse.measure(0) - - # prepare and schedule circuits that will be used. - single_u2_qc = circuit.QuantumCircuit(2) - single_u2_qc.append(circuit.library.U2Gate(0, pi / 2), [1]) - with self.assertWarnsRegex( - DeprecationWarning, - expected_regex="The `transpile` function will " - "stop supporting inputs of type `BackendV1`", - ): - single_u2_qc = compiler.transpile(single_u2_qc, self.backend, optimization_level=1) - with self.assertWarns(DeprecationWarning): - single_u2_sched = compiler.schedule(single_u2_qc, self.backend) - - # sequential context - sequential_reference = pulse.Schedule() - sequential_reference += instructions.Delay(delay_dur, d0) - sequential_reference.insert(delay_dur, single_u2_sched, inplace=True) - - # align right - align_right_reference = pulse.Schedule() - align_right_reference += pulse.Play(library.Constant(long_dur, 0.1), d2) - align_right_reference.insert( - long_dur - single_u2_sched.duration, single_u2_sched, inplace=True - ) - align_right_reference.insert( - long_dur - single_u2_sched.duration - short_dur, - pulse.Play(library.Constant(short_dur, 0.1), d1), - inplace=True, - ) - - # align left - triple_u2_qc = circuit.QuantumCircuit(2) - triple_u2_qc.append(circuit.library.U2Gate(0, pi / 2), [0]) - triple_u2_qc.append(circuit.library.U2Gate(0, pi / 2), [1]) - triple_u2_qc.append(circuit.library.U2Gate(0, pi / 2), [0]) - with self.assertWarnsRegex( - DeprecationWarning, - expected_regex="The `transpile` function will " - "stop supporting inputs of type `BackendV1`", - ): - triple_u2_qc = compiler.transpile(triple_u2_qc, self.backend, optimization_level=1) - with self.assertWarns(DeprecationWarning): - align_left_reference = compiler.schedule(triple_u2_qc, self.backend, method="alap") - - # measurement - measure_reference = macros.measure( - qubits=[0], inst_map=self.inst_map, meas_map=self.configuration.meas_map - ) - reference = pulse.Schedule() - reference += sequential_reference - # Insert so that the long pulse on d2 occurs as early as possible - # without an overval on d1. - insert_time = reference.ch_stop_time(d1) - align_right_reference.ch_start_time(d1) - reference.insert(insert_time, align_right_reference, inplace=True) - reference.insert(reference.ch_stop_time(d0, d1), align_left_reference, inplace=True) - reference += measure_reference - - self.assertScheduleEqual(schedule, reference) - @decorate_test_methods(ignore_pulse_deprecation_warnings) class TestSubroutineCall(TestBuilder): diff --git a/test/python/scheduler/__init__.py b/test/python/scheduler/__init__.py deleted file mode 100644 index b56983c88a1e..000000000000 --- a/test/python/scheduler/__init__.py +++ /dev/null @@ -1,15 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2017, 2018. -# -# 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. - -# pylint: disable=cyclic-import - -"""Qiskit pulse scheduling tests.""" diff --git a/test/python/scheduler/test_basic_scheduler.py b/test/python/scheduler/test_basic_scheduler.py deleted file mode 100644 index 5adbb3fdc1ff..000000000000 --- a/test/python/scheduler/test_basic_scheduler.py +++ /dev/null @@ -1,1218 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2019, 2024. -# -# 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 cases for the pulse scheduler passes.""" - -import numpy as np -from numpy import pi -from qiskit import QuantumRegister, ClassicalRegister, QuantumCircuit, schedule -from qiskit.circuit import Gate, Parameter -from qiskit.circuit.library import U1Gate, U2Gate, U3Gate, SXGate -from qiskit.exceptions import QiskitError -from qiskit.pulse import ( - Schedule, - DriveChannel, - AcquireChannel, - Acquire, - MeasureChannel, - MemorySlot, - Gaussian, - GaussianSquare, - Play, - Waveform, - transforms, -) -from qiskit.pulse import build, macros, play, InstructionScheduleMap -from qiskit.providers.fake_provider import ( - FakeBackend, - FakeOpenPulse2Q, - FakeOpenPulse3Q, - GenericBackendV2, -) -from test import QiskitTestCase # pylint: disable=wrong-import-order - - -class TestBasicSchedule(QiskitTestCase): - """Scheduling tests.""" - - def setUp(self): - super().setUp() - with self.assertWarns(DeprecationWarning): - self.backend = FakeOpenPulse2Q() - self.inst_map = self.backend.defaults().instruction_schedule_map - - def test_unavailable_defaults(self): - """Test backend with unavailable defaults.""" - qr = QuantumRegister(1) - qc = QuantumCircuit(qr) - with self.assertWarns(DeprecationWarning): - backend = FakeBackend(None) - backend.defaults = backend.configuration - with self.assertWarns(DeprecationWarning): - self.assertRaises(QiskitError, lambda: schedule(qc, backend)) - - def test_alap_pass(self): - """Test ALAP scheduling.""" - - # ┌───────────────┐ ░ ┌─┐ - # q0_0: ┤ U2(3.14,1.57) ├────────────────────░───■──┤M├─── - # └┬──────────────┤ ░ ┌──────────────┐ ░ ┌─┴─┐└╥┘┌─┐ - # q0_1: ─┤ U2(0.5,0.25) ├─░─┤ U2(0.5,0.25) ├─░─┤ X ├─╫─┤M├ - # └──────────────┘ ░ └──────────────┘ ░ └───┘ ║ └╥┘ - # c0: 2/═════════════════════════════════════════════╩══╩═ - # 0 1 - q = QuantumRegister(2) - c = ClassicalRegister(2) - qc = QuantumCircuit(q, c) - qc.append(U2Gate(3.14, 1.57), [q[0]]) - qc.append(U2Gate(0.5, 0.25), [q[1]]) - qc.barrier(q[1]) - qc.append(U2Gate(0.5, 0.25), [q[1]]) - qc.barrier(q[0], [q[1]]) - qc.cx(q[0], q[1]) - qc.measure(q, c) - with self.assertWarns(DeprecationWarning): - sched = schedule(qc, self.backend) - # X pulse on q0 should end at the start of the CNOT - with self.assertWarns(DeprecationWarning): - expected = Schedule( - (2, self.inst_map.get("u2", [0], 3.14, 1.57)), - self.inst_map.get("u2", [1], 0.5, 0.25), - (2, self.inst_map.get("u2", [1], 0.5, 0.25)), - (4, self.inst_map.get("cx", [0, 1])), - (26, self.inst_map.get("measure", [0, 1])), - ) - for actual, expected in zip(sched.instructions, expected.instructions): - self.assertEqual(actual[0], expected[0]) - self.assertEqual(actual[1], expected[1]) - - def test_single_circuit_list_schedule(self): - """Test that passing a single circuit list to schedule() returns a list.""" - q = QuantumRegister(2) - c = ClassicalRegister(2) - qc = QuantumCircuit(q, c) - with self.assertWarns(DeprecationWarning): - sched = schedule([qc], self.backend, method="alap") - expected = Schedule() - self.assertIsInstance(sched, list) - self.assertEqual(sched[0].instructions, expected.instructions) - - def test_alap_with_barriers(self): - """Test that ALAP respects barriers on new qubits.""" - q = QuantumRegister(2) - c = ClassicalRegister(2) - qc = QuantumCircuit(q, c) - qc.append(U2Gate(0, 0), [q[0]]) - qc.barrier(q[0], q[1]) - qc.append(U2Gate(0, 0), [q[1]]) - with self.assertWarns(DeprecationWarning): - sched = schedule(qc, self.backend, method="alap") - expected = Schedule( - self.inst_map.get("u2", [0], 0, 0), (2, self.inst_map.get("u2", [1], 0, 0)) - ) - for actual, expected in zip(sched.instructions, expected.instructions): - self.assertEqual(actual[0], expected[0]) - self.assertEqual(actual[1], expected[1]) - - def test_empty_circuit_schedule(self): - """Test empty circuit being scheduled.""" - q = QuantumRegister(2) - c = ClassicalRegister(2) - qc = QuantumCircuit(q, c) - with self.assertWarns(DeprecationWarning): - sched = schedule(qc, self.backend, method="alap") - expected = Schedule() - self.assertEqual(sched.instructions, expected.instructions) - - def test_alap_aligns_end(self): - """Test that ALAP always acts as though there is a final global barrier.""" - q = QuantumRegister(2) - c = ClassicalRegister(2) - qc = QuantumCircuit(q, c) - qc.append(U3Gate(0, 0, 0), [q[0]]) - qc.append(U2Gate(0, 0), [q[1]]) - with self.assertWarns(DeprecationWarning): - sched = schedule(qc, self.backend, method="alap") - expected_sched = Schedule( - (2, self.inst_map.get("u2", [1], 0, 0)), self.inst_map.get("u3", [0], 0, 0, 0) - ) - for actual, expected in zip(sched.instructions, expected_sched.instructions): - self.assertEqual(actual[0], expected[0]) - self.assertEqual(actual[1], expected[1]) - with self.assertWarns(DeprecationWarning): - self.assertEqual( - sched.ch_duration(DriveChannel(0)), expected_sched.ch_duration(DriveChannel(1)) - ) - - def test_asap_pass(self): - """Test ASAP scheduling.""" - - # ┌───────────────┐ ░ ┌─┐ - # q0_0: ┤ U2(3.14,1.57) ├────────────────────░───■──┤M├─── - # └┬──────────────┤ ░ ┌──────────────┐ ░ ┌─┴─┐└╥┘┌─┐ - # q0_1: ─┤ U2(0.5,0.25) ├─░─┤ U2(0.5,0.25) ├─░─┤ X ├─╫─┤M├ - # └──────────────┘ ░ └──────────────┘ ░ └───┘ ║ └╥┘ - # c0: 2/═════════════════════════════════════════════╩══╩═ - # 0 1 - q = QuantumRegister(2) - c = ClassicalRegister(2) - qc = QuantumCircuit(q, c) - qc.append(U2Gate(3.14, 1.57), [q[0]]) - qc.append(U2Gate(0.5, 0.25), [q[1]]) - qc.barrier(q[1]) - qc.append(U2Gate(0.5, 0.25), [q[1]]) - qc.barrier(q[0], q[1]) - qc.cx(q[0], q[1]) - qc.measure(q, c) - with self.assertWarns(DeprecationWarning): - sched = schedule(qc, self.backend, method="as_soon_as_possible") - # X pulse on q0 should start at t=0 - expected = Schedule( - self.inst_map.get("u2", [0], 3.14, 1.57), - self.inst_map.get("u2", [1], 0.5, 0.25), - (2, self.inst_map.get("u2", [1], 0.5, 0.25)), - (4, self.inst_map.get("cx", [0, 1])), - (26, self.inst_map.get("measure", [0, 1])), - ) - for actual, expected in zip(sched.instructions, expected.instructions): - self.assertEqual(actual[0], expected[0]) - self.assertEqual(actual[1], expected[1]) - - def test_alap_resource_respecting(self): - """Test that the ALAP pass properly respects busy resources when backwards scheduling. - For instance, a CX on 0 and 1 followed by an X on only 1 must respect both qubits' - timeline.""" - q = QuantumRegister(2) - c = ClassicalRegister(2) - qc = QuantumCircuit(q, c) - qc.cx(q[0], q[1]) - qc.append(U2Gate(0.5, 0.25), [q[1]]) - with self.assertWarns(DeprecationWarning): - sched = schedule(qc, self.backend, method="as_late_as_possible") - insts = sched.instructions - self.assertEqual(insts[0][0], 0) - self.assertEqual(insts[6][0], 22) - - qc = QuantumCircuit(q, c) - qc.cx(q[0], q[1]) - qc.append(U2Gate(0.5, 0.25), [q[1]]) - qc.measure(q, c) - with self.assertWarns(DeprecationWarning): - sched = schedule(qc, self.backend, method="as_late_as_possible") - self.assertEqual(sched.instructions[-1][0], 24) - - def test_inst_map_schedules_unaltered(self): - """Test that forward scheduling doesn't change relative timing with a command.""" - q = QuantumRegister(2) - c = ClassicalRegister(2) - qc = QuantumCircuit(q, c) - qc.cx(q[0], q[1]) - with self.assertWarns(DeprecationWarning): - sched1 = schedule(qc, self.backend, method="as_soon_as_possible") - sched2 = schedule(qc, self.backend, method="as_late_as_possible") - for asap, alap in zip(sched1.instructions, sched2.instructions): - self.assertEqual(asap[0], alap[0]) - self.assertEqual(asap[1], alap[1]) - insts = sched1.instructions - self.assertEqual(insts[0][0], 0) # shift phase - self.assertEqual(insts[1][0], 0) # ym_d0 - self.assertEqual(insts[2][0], 0) # x90p_d1 - self.assertEqual(insts[3][0], 2) # cr90p_u0 - self.assertEqual(insts[4][0], 11) # xp_d0 - self.assertEqual(insts[5][0], 13) # cr90m_u0 - - def test_measure_combined(self): - """ - Test to check for measure on the same qubit which generated another measure schedule. - - The measures on different qubits are combined, but measures on the same qubit - adds another measure to the schedule. - """ - q = QuantumRegister(2) - c = ClassicalRegister(2) - qc = QuantumCircuit(q, c) - qc.append(U2Gate(3.14, 1.57), [q[0]]) - qc.cx(q[0], q[1]) - qc.measure(q[0], c[0]) - qc.measure(q[1], c[1]) - qc.measure(q[1], c[1]) - with self.assertWarns(DeprecationWarning): - sched = schedule(qc, self.backend, method="as_soon_as_possible") - expected = Schedule( - self.inst_map.get("u2", [0], 3.14, 1.57), - (2, self.inst_map.get("cx", [0, 1])), - (24, self.inst_map.get("measure", [0, 1])), - (34, self.inst_map.get("measure", [0, 1]).filter(channels=[MeasureChannel(1)])), - (34, Acquire(10, AcquireChannel(1), MemorySlot(1))), - ) - self.assertEqual(sched.instructions, expected.instructions) - - # - def test_3q_schedule(self): - """Test a schedule that was recommended by David McKay :D""" - - # ┌─────────────────┐ - # q0_0: ─────────■─────────┤ U3(3.14,1.57,0) ├──────────────────────── - # ┌─┴─┐ └┬───────────────┬┘ - # q0_1: ───────┤ X ├────────┤ U2(3.14,1.57) ├───■───────────────────── - # ┌──────┴───┴──────┐ └───────────────┘ ┌─┴─┐┌─────────────────┐ - # q0_2: ┤ U2(0.778,0.122) ├───────────────────┤ X ├┤ U2(0.778,0.122) ├ - # └─────────────────┘ └───┘└─────────────────┘ - with self.assertWarns(DeprecationWarning): - backend = FakeOpenPulse3Q() - inst_map = backend.defaults().instruction_schedule_map - q = QuantumRegister(3) - c = ClassicalRegister(3) - qc = QuantumCircuit(q, c) - qc.cx(q[0], q[1]) - qc.append(U2Gate(0.778, 0.122), [q[2]]) - qc.append(U3Gate(3.14, 1.57, 0), [q[0]]) - qc.append(U2Gate(3.14, 1.57), [q[1]]) - qc.cx(q[1], q[2]) - qc.append(U2Gate(0.778, 0.122), [q[2]]) - with self.assertWarns(DeprecationWarning): - sched = schedule(qc, backend) - expected = Schedule( - inst_map.get("cx", [0, 1]), - (22, inst_map.get("u2", [1], 3.14, 1.57)), - (22, inst_map.get("u2", [2], 0.778, 0.122)), - (24, inst_map.get("cx", [1, 2])), - (44, inst_map.get("u3", [0], 3.14, 1.57, 0)), - (46, inst_map.get("u2", [2], 0.778, 0.122)), - ) - for actual, expected in zip(sched.instructions, expected.instructions): - self.assertEqual(actual[0], expected[0]) - self.assertEqual(actual[1], expected[1]) - - def test_schedule_multi(self): - """Test scheduling multiple circuits at once.""" - q = QuantumRegister(2) - c = ClassicalRegister(2) - qc0 = QuantumCircuit(q, c) - qc0.cx(q[0], q[1]) - qc1 = QuantumCircuit(q, c) - qc1.cx(q[0], q[1]) - with self.assertWarns(DeprecationWarning): - schedules = schedule([qc0, qc1], self.backend) - expected_insts = schedule(qc0, self.backend).instructions - for actual, expected in zip(schedules[0].instructions, expected_insts): - self.assertEqual(actual[0], expected[0]) - self.assertEqual(actual[1], expected[1]) - - def test_circuit_name_kept(self): - """Test that the new schedule gets its name from the circuit.""" - q = QuantumRegister(2) - c = ClassicalRegister(2) - qc = QuantumCircuit(q, c, name="CIRCNAME") - qc.cx(q[0], q[1]) - with self.assertWarns(DeprecationWarning): - sched = schedule(qc, self.backend, method="asap") - self.assertEqual(sched.name, qc.name) - with self.assertWarns(DeprecationWarning): - sched = schedule(qc, self.backend, method="alap") - self.assertEqual(sched.name, qc.name) - - def test_can_add_gates_into_free_space(self): - """The scheduler does some time bookkeeping to know when qubits are free to be - scheduled. Make sure this works for qubits that are used in the future. This was - a bug, uncovered by this example: - - q0 = - - - - |X| - q1 = |X| |u2| |X| - - In ALAP scheduling, the next operation on qubit 0 would be added at t=0 rather - than immediately before the X gate. - """ - qr = QuantumRegister(2) - qc = QuantumCircuit(qr) - for i in range(2): - qc.append(U2Gate(0, 0), [qr[i]]) - qc.append(U1Gate(3.14), [qr[i]]) - qc.append(U2Gate(0, 0), [qr[i]]) - with self.assertWarns(DeprecationWarning): - sched = schedule(qc, self.backend, method="alap") - expected = Schedule( - self.inst_map.get("u2", [0], 0, 0), - self.inst_map.get("u2", [1], 0, 0), - (2, self.inst_map.get("u1", [0], 3.14)), - (2, self.inst_map.get("u1", [1], 3.14)), - (2, self.inst_map.get("u2", [0], 0, 0)), - (2, self.inst_map.get("u2", [1], 0, 0)), - ) - for actual, expected in zip(sched.instructions, expected.instructions): - self.assertEqual(actual[0], expected[0]) - self.assertEqual(actual[1], expected[1]) - - def test_barriers_in_middle(self): - """As a follow on to `test_can_add_gates_into_free_space`, similar issues - arose for barriers, specifically. - """ - qr = QuantumRegister(2) - qc = QuantumCircuit(qr) - for i in range(2): - qc.append(U2Gate(0, 0), [qr[i]]) - qc.barrier(qr[i]) - qc.append(U1Gate(3.14), [qr[i]]) - qc.barrier(qr[i]) - qc.append(U2Gate(0, 0), [qr[i]]) - with self.assertWarns(DeprecationWarning): - sched = schedule(qc, self.backend, method="alap") - expected = Schedule( - self.inst_map.get("u2", [0], 0, 0), - self.inst_map.get("u2", [1], 0, 0), - (2, self.inst_map.get("u1", [0], 3.14)), - (2, self.inst_map.get("u1", [1], 3.14)), - (2, self.inst_map.get("u2", [0], 0, 0)), - (2, self.inst_map.get("u2", [1], 0, 0)), - ) - for actual, expected in zip(sched.instructions, expected.instructions): - self.assertEqual(actual[0], expected[0]) - self.assertEqual(actual[1], expected[1]) - - def test_parametric_input(self): - """Test that scheduling works with parametric pulses as input.""" - qr = QuantumRegister(1) - qc = QuantumCircuit(qr) - qc.append(Gate("gauss", 1, []), qargs=[qr[0]]) - with self.assertWarns(DeprecationWarning): - custom_gauss = Schedule( - Play(Gaussian(duration=25, sigma=4, amp=0.5, angle=pi / 2), DriveChannel(0)) - ) - self.inst_map.add("gauss", [0], custom_gauss) - with self.assertWarns(DeprecationWarning): - sched = schedule(qc, self.backend, inst_map=self.inst_map) - self.assertEqual(sched.instructions[0], custom_gauss.instructions[0]) - - def test_pulse_gates(self): - """Test scheduling calibrated pulse gates.""" - q = QuantumRegister(2) - qc = QuantumCircuit(q) - qc.append(U2Gate(0, 0), [q[0]]) - qc.barrier(q[0], q[1]) - qc.append(U2Gate(0, 0), [q[1]]) - with self.assertWarns(DeprecationWarning): - qc.add_calibration( - "u2", [0], Schedule(Play(Gaussian(28, 0.2, 4), DriveChannel(0))), [0, 0] - ) - qc.add_calibration( - "u2", [1], Schedule(Play(Gaussian(28, 0.2, 4), DriveChannel(1))), [0, 0] - ) - - with self.assertWarns(DeprecationWarning): - sched = schedule(qc, self.backend) - expected = Schedule( - Play(Gaussian(28, 0.2, 4), DriveChannel(0)), - (28, Schedule(Play(Gaussian(28, 0.2, 4), DriveChannel(1)))), - ) - self.assertEqual(sched.instructions, expected.instructions) - - def test_calibrated_measurements(self): - """Test scheduling calibrated measurements.""" - q = QuantumRegister(2) - c = ClassicalRegister(2) - qc = QuantumCircuit(q, c) - qc.append(U2Gate(0, 0), [q[0]]) - qc.measure(q[0], c[0]) - - with self.assertWarns(DeprecationWarning): - meas_sched = Play(Gaussian(1200, 0.2, 4), MeasureChannel(0)) - meas_sched |= Acquire(1200, AcquireChannel(0), MemorySlot(0)) - qc.add_calibration("measure", [0], meas_sched) - - sched = schedule(qc, self.backend) - expected = Schedule(self.inst_map.get("u2", [0], 0, 0), (2, meas_sched)) - self.assertEqual(sched.instructions, expected.instructions) - - def test_subset_calibrated_measurements(self): - """Test that measurement calibrations can be added and used for some qubits, even - if the other qubits do not also have calibrated measurements.""" - qc = QuantumCircuit(3, 3) - qc.measure(0, 0) - qc.measure(1, 1) - qc.measure(2, 2) - meas_scheds = [] - for qubit in [0, 2]: - with self.assertWarns(DeprecationWarning): - meas = Play(Gaussian(1200, 0.2, 4), MeasureChannel(qubit)) + Acquire( - 1200, AcquireChannel(qubit), MemorySlot(qubit) - ) - meas_scheds.append(meas) - with self.assertWarns(DeprecationWarning): - qc.add_calibration("measure", [qubit], meas) - - with self.assertWarns(DeprecationWarning): - backend = FakeOpenPulse3Q() - meas = macros.measure([1], backend) - meas = meas.exclude(channels=[AcquireChannel(0), AcquireChannel(2)]) - with self.assertWarns(DeprecationWarning): - backend = FakeOpenPulse3Q() - with self.assertWarns(DeprecationWarning): - sched = schedule(qc, backend) - expected = Schedule(meas_scheds[0], meas_scheds[1], meas) - self.assertEqual(sched.instructions, expected.instructions) - - def test_clbits_of_calibrated_measurements(self): - """Test that calibrated measurements are only used when the classical bits also match.""" - q = QuantumRegister(2) - c = ClassicalRegister(2) - qc = QuantumCircuit(q, c) - qc.measure(q[0], c[1]) - - with self.assertWarns(DeprecationWarning): - meas_sched = Play(Gaussian(1200, 0.2, 4), MeasureChannel(0)) - meas_sched |= Acquire(1200, AcquireChannel(0), MemorySlot(0)) - qc.add_calibration("measure", [0], meas_sched) - - with self.assertWarns(DeprecationWarning): - sched = schedule(qc, self.backend) - # Doesn't use the calibrated schedule because the classical memory slots do not match - with self.assertWarns(DeprecationWarning): - expected = Schedule(macros.measure([0], self.backend, qubit_mem_slots={0: 1})) - self.assertEqual(sched.instructions, expected.instructions) - - def test_metadata_is_preserved_alap(self): - """Test that circuit metadata is preserved in output schedule with alap.""" - q = QuantumRegister(2) - qc = QuantumCircuit(q) - qc.append(U2Gate(0, 0), [q[0]]) - qc.barrier(q[0], q[1]) - qc.append(U2Gate(0, 0), [q[1]]) - qc.metadata = {"experiment_type": "gst", "execution_number": "1234"} - with self.assertWarns(DeprecationWarning): - sched = schedule(qc, self.backend, method="alap") - self.assertEqual({"experiment_type": "gst", "execution_number": "1234"}, sched.metadata) - - def test_metadata_is_preserved_asap(self): - """Test that circuit metadata is preserved in output schedule with asap.""" - q = QuantumRegister(2) - qc = QuantumCircuit(q) - qc.append(U2Gate(0, 0), [q[0]]) - qc.barrier(q[0], q[1]) - qc.append(U2Gate(0, 0), [q[1]]) - qc.metadata = {"experiment_type": "gst", "execution_number": "1234"} - with self.assertWarns(DeprecationWarning): - sched = schedule(qc, self.backend, method="asap") - self.assertEqual({"experiment_type": "gst", "execution_number": "1234"}, sched.metadata) - - def test_scheduler_with_params_bound(self): - """Test scheduler with parameters defined and bound""" - x = Parameter("x") - qc = QuantumCircuit(2) - qc.append(Gate("pulse_gate", 1, [x]), [0]) - with self.assertWarns(DeprecationWarning): - expected_schedule = Schedule() - qc.add_calibration( - gate="pulse_gate", qubits=[0], schedule=expected_schedule, params=[x] - ) - qc = qc.assign_parameters({x: 1}) - with self.assertWarns(DeprecationWarning): - sched = schedule(qc, self.backend) - self.assertEqual(sched, expected_schedule) - - def test_scheduler_with_params_not_bound(self): - """Test scheduler with parameters defined but not bound""" - x = Parameter("amp") - qc = QuantumCircuit(2) - qc.append(Gate("pulse_gate", 1, [x]), [0]) - with self.assertWarns(DeprecationWarning): - with build() as expected_schedule: - play(Gaussian(duration=160, amp=x, sigma=40), DriveChannel(0)) - qc.add_calibration( - gate="pulse_gate", qubits=[0], schedule=expected_schedule, params=[x] - ) - sched = schedule(qc, self.backend) - self.assertEqual(sched, transforms.target_qobj_transform(expected_schedule)) - - def test_schedule_block_in_instmap(self): - """Test schedule block in instmap can be scheduled.""" - duration = Parameter("duration") - - with self.assertWarns(DeprecationWarning): - with build() as pulse_prog: - play(Gaussian(duration, 0.1, 10), DriveChannel(0)) - - instmap = InstructionScheduleMap() - instmap.add("block_gate", (0,), pulse_prog, ["duration"]) - - qc = QuantumCircuit(1) - qc.append(Gate("block_gate", 1, [duration]), [0]) - qc.assign_parameters({duration: 100}, inplace=True) - - with self.assertWarns(DeprecationWarning): - sched = schedule(qc, self.backend, inst_map=instmap) - - with self.assertWarns(DeprecationWarning): - ref_sched = Schedule() - ref_sched += Play(Gaussian(100, 0.1, 10), DriveChannel(0)) - - self.assertEqual(sched, ref_sched) - - -class TestBasicScheduleV2(QiskitTestCase): - """Scheduling tests.""" - - def setUp(self): - super().setUp() - with self.assertWarns(DeprecationWarning): - self.backend = GenericBackendV2(num_qubits=3, calibrate_instructions=True, seed=42) - self.inst_map = self.backend.instruction_schedule_map - # self.pulse_2_samples is the pulse sequence used to calibrate "measure" in - # GenericBackendV2. See class construction for more details. - self.pulse_2_samples = np.linspace(0, 1.0, 32, dtype=np.complex128) - - def test_alap_pass(self): - """Test ALAP scheduling.""" - - # ┌────┐ ░ ┌─┐ - # q0_0: ┤ √X ├──────────░───■──┤M├─── - # ├────┤ ░ ┌────┐ ░ ┌─┴─┐└╥┘┌─┐ - # q0_1: ┤ √X ├─░─┤ √X ├─░─┤ X ├─╫─┤M├ - # └────┘ ░ └────┘ ░ └───┘ ║ └╥┘ - # c0: 2/════════════════════════╩══╩═ - # 0 1 - - q = QuantumRegister(2) - c = ClassicalRegister(2) - qc = QuantumCircuit(q, c) - - qc.sx(q[0]) - qc.sx(q[1]) - qc.barrier(q[1]) - - qc.sx(q[1]) - qc.barrier(q[0], q[1]) - - qc.cx(q[0], q[1]) - qc.measure(q, c) - - with self.assertWarns(DeprecationWarning): - sched = schedule(circuits=qc, backend=self.backend, method="alap") - - # Since, the method of scheduling chosen here is 'as_late_as_possible' - # so all the π/2 pulse here should be right shifted. - # - # Calculations: - # Duration of the π/2 pulse for GenericBackendV2 backend is 16dt - # first π/2 pulse on q0 should start at 16dt because of 'as_late_as_possible'. - # first π/2 pulse on q1 should start 0dt. - # second π/2 pulse on q1 should start with a delay of 16dt. - # cx pulse( pulse on drive channel, control channel) should start with a delay - # of 16dt+16dt. - # measure pulse should start with a delay of 16dt+16dt+64dt(64dt for cx gate). - with self.assertWarns(DeprecationWarning): - expected = Schedule( - (0, self.inst_map.get("sx", [1])), - (0 + 16, self.inst_map.get("sx", [0])), # Right shifted because of alap. - (0 + 16, self.inst_map.get("sx", [1])), - (0 + 16 + 16, self.inst_map.get("cx", [0, 1])), - ( - 0 + 16 + 16 + 64, - Play( - Waveform(samples=self.pulse_2_samples, name="pulse_2"), - MeasureChannel(0), - name="pulse_2", - ), - ), - ( - 0 + 16 + 16 + 64, - Play( - Waveform(samples=self.pulse_2_samples, name="pulse_2"), - MeasureChannel(1), - name="pulse_2", - ), - ), - (0 + 16 + 16 + 64, Acquire(1792, AcquireChannel(0), MemorySlot(0))), - (0 + 16 + 16 + 64, Acquire(1792, AcquireChannel(1), MemorySlot(1))), - ) - for actual, expected in zip(sched.instructions, expected.instructions): - self.assertEqual(actual[0], expected[0]) - self.assertEqual(actual[1], expected[1]) - - def test_single_circuit_list_schedule(self): - """Test that passing a single circuit list to schedule() returns a list.""" - q = QuantumRegister(2) - c = ClassicalRegister(2) - qc = QuantumCircuit(q, c) - with self.assertWarns(DeprecationWarning): - sched = schedule([qc], self.backend, method="alap") - expected = Schedule() - self.assertIsInstance(sched, list) - self.assertEqual(sched[0].instructions, expected.instructions) - - def test_alap_with_barriers(self): - """Test that ALAP respects barriers on new qubits.""" - - # ┌────┐ ░ - # q0_0: ┤ √X ├─░─────── - # └────┘ ░ ┌────┐ - # q0_1: ───────░─┤ √X ├ - # ░ └────┘ - # c0: 2/═══════════════ - # - - q = QuantumRegister(2) - c = ClassicalRegister(2) - qc = QuantumCircuit(q, c) - qc.append(SXGate(), [q[0]]) - qc.barrier(q[0], q[1]) - qc.append(SXGate(), [q[1]]) - with self.assertWarns(DeprecationWarning): - sched = schedule(qc, self.backend, method="alap") - # If there wasn't a barrier the π/2 pulse on q1 would have started from 0dt, but since, - # there is a barrier so the π/2 pulse on q1 should start with a delay of 160dt. - expected = Schedule( - (0, self.inst_map.get("sx", [0])), (16, self.inst_map.get("sx", [1])) - ) - for actual, expected in zip(sched.instructions, expected.instructions): - self.assertEqual(actual, expected) - - def test_empty_circuit_schedule(self): - """Test empty circuit being scheduled.""" - q = QuantumRegister(2) - c = ClassicalRegister(2) - qc = QuantumCircuit(q, c) - with self.assertWarns(DeprecationWarning): - sched = schedule(qc, self.backend, method="alap") - expected = Schedule() - self.assertEqual(sched.instructions, expected.instructions) - - def test_alap_aligns_end(self): - """Test that ALAP always acts as though there is a final global barrier.""" - # ┌────┐ - # q1_0: ┤ √X ├ - # ├────┤ - # q1_1: ┤ √X ├ - # └────┘ - # c1: 2/══════ - - q = QuantumRegister(2) - c = ClassicalRegister(2) - qc = QuantumCircuit(q, c) - qc.sx(q[0]) - qc.sx(q[1]) - with self.assertWarns(DeprecationWarning): - sched = schedule(qc, self.backend, method="alap") - expected_sched = Schedule( - (0, self.inst_map.get("sx", [1])), (0, self.inst_map.get("sx", [0])) - ) - for actual, expected in zip(sched.instructions, expected_sched.instructions): - self.assertEqual(actual[0], expected[0]) - self.assertEqual(actual[1], expected[1]) - with self.assertWarns(DeprecationWarning): - self.assertEqual( - sched.ch_duration(DriveChannel(0)), expected_sched.ch_duration(DriveChannel(1)) - ) - - def test_asap_pass(self): - """Test ASAP scheduling.""" - - # ┌────┐ ░ ┌─┐ - # q0_0: ┤ √X ├──────────░───■──┤M├─── - # ├────┤ ░ ┌────┐ ░ ┌─┴─┐└╥┘┌─┐ - # q0_1: ┤ √X ├─░─┤ √X ├─░─┤ X ├─╫─┤M├ - # └────┘ ░ └────┘ ░ └───┘ ║ └╥┘ - # c0: 2/════════════════════════╩══╩═ - # 0 1 - - q = QuantumRegister(2) - c = ClassicalRegister(2) - qc = QuantumCircuit(q, c) - - qc.sx(q[0]) - qc.sx(q[1]) - qc.barrier(q[1]) - - qc.sx(q[1]) - qc.barrier(q[0], q[1]) - - qc.cx(q[0], q[1]) - qc.measure(q, c) - - with self.assertWarns(DeprecationWarning): - sched = schedule(circuits=qc, backend=self.backend, method="asap") - # Since, the method of scheduling chosen here is 'as_soon_as_possible' - # so all the π/2 pulse here should be left shifted. - # - # Calculations: - # Duration of the π/2 pulse for FakePerth backend is 16dt - # first π/2 pulse on q0 should start at 0dt because of 'as_soon_as_possible'. - # first π/2 pulse on q1 should start 0dt. - # second π/2 pulse on q1 should start with a delay of 16dt. - # cx pulse( pulse on drive channel, control channel) should start with a delay - # of 16dt+16dt. - # measure pulse should start with a delay of 16dt+16dt+64dt(64dt for cx gate). - with self.assertWarns(DeprecationWarning): - expected = Schedule( - (0, self.inst_map.get("sx", [1])), - (0, self.inst_map.get("sx", [0])), # Left shifted because of asap. - (0 + 16, self.inst_map.get("sx", [1])), - (0 + 16 + 16, self.inst_map.get("cx", [0, 1])), - ( - 0 + 16 + 16 + 64, - Play( - Waveform(samples=self.pulse_2_samples, name="pulse_2"), - MeasureChannel(0), - name="pulse_2", - ), - ), - ( - 0 + 16 + 16 + 64, - Play( - Waveform(samples=self.pulse_2_samples, name="pulse_2"), - MeasureChannel(1), - name="pulse_2", - ), - ), - (0 + 16 + 16 + 64, Acquire(1792, AcquireChannel(0), MemorySlot(0))), - (0 + 16 + 16 + 64, Acquire(1792, AcquireChannel(1), MemorySlot(1))), - ) - for actual, expected in zip(sched.instructions, expected.instructions): - self.assertEqual(actual[0], expected[0]) - self.assertEqual(actual[1], expected[1]) - - def test_alap_resource_respecting(self): - """Test that the ALAP pass properly respects busy resources when backwards scheduling. - For instance, a CX on 0 and 1 followed by an X on only 1 must respect both qubits' - timeline.""" - q = QuantumRegister(2) - c = ClassicalRegister(2) - qc = QuantumCircuit(q, c) - qc.cx(q[0], q[1]) - qc.sx(q[1]) - with self.assertWarns(DeprecationWarning): - sched = schedule(qc, self.backend, method="as_late_as_possible") - insts = sched.instructions - - # This is ShiftPhase for the cx operation. - self.assertEqual(insts[0][0], 0) - - # It takes 4 pulse operations on DriveChannel and ControlChannel to do a - # cx operation on this backend. - # 64dt is duration of cx operation on this backend. - self.assertEqual(insts[4][0], 64) - - qc = QuantumCircuit(q, c) - qc.cx(q[0], q[1]) - qc.sx(q[1]) - qc.measure(q, c) - with self.assertWarns(DeprecationWarning): - sched = schedule(qc, self.backend, method="as_late_as_possible") - - # 64dt for cx operation + 16dt for sx operation - # So, the pulses in MeasureChannel0 and 1 starts from 80dt. - self.assertEqual(sched.instructions[-1][0], 80) - - def test_inst_map_schedules_unaltered(self): - """Test that forward scheduling doesn't change relative timing with a command.""" - q = QuantumRegister(2) - c = ClassicalRegister(2) - qc = QuantumCircuit(q, c) - qc.cx(q[0], q[1]) - with self.assertWarns(DeprecationWarning): - sched1 = schedule(qc, self.backend, method="as_soon_as_possible") - sched2 = schedule(qc, self.backend, method="as_late_as_possible") - for asap, alap in zip(sched1.instructions, sched2.instructions): - self.assertEqual(asap[0], alap[0]) - self.assertEqual(asap[1], alap[1]) - insts = sched1.instructions - self.assertEqual(insts[0][0], 0) # ShiftPhase at DriveChannel(0) no dt required. - self.assertEqual(insts[1][0], 0) # ShiftPhase at ControlChannel(1) no dt required. - self.assertEqual(insts[2][0], 0) # Pulse pulse_2 of duration 32dt. - self.assertEqual(insts[3][0], 0) # Pulse pulse_3 of duration 160dt. - - def test_measure_combined(self): - """ - Test to check for measure on the same qubit which generated another measure schedule. - - The measures on different qubits are combined, but measures on the same qubit - adds another measure to the schedule. - """ - q = QuantumRegister(2) - c = ClassicalRegister(2) - qc = QuantumCircuit(q, c) - qc.sx(q[0]) - qc.cx(q[0], q[1]) - qc.measure(q[0], c[0]) - qc.measure(q[1], c[1]) - qc.measure(q[1], c[1]) - - with self.assertWarns(DeprecationWarning): - sched = schedule(qc, self.backend, method="as_soon_as_possible") - - expected_sched = Schedule( - # This is the schedule to implement sx gate. - (0, self.inst_map.get("sx", [0])), - # This is the schedule to implement cx gate - (0 + 16, self.inst_map.get("cx", [0, 1])), - # This is the schedule for the measurements on qubits 0 and 1 (combined) - ( - 0 + 16 + 64, - self.inst_map.get("measure", [0]).filter( - channels=[MeasureChannel(0), MeasureChannel(1)] - ), - ), - ( - 0 + 16 + 64, - self.inst_map.get("measure", [0]).filter( - channels=[AcquireChannel(0), AcquireChannel(1)] - ), - ), - # This is the schedule for the second measurement on qubit 1 - ( - 0 + 16 + 64 + 1792, - self.inst_map.get("measure", [1]).filter(channels=[MeasureChannel(1)]), - ), - ( - 0 + 16 + 64 + 1792, - self.inst_map.get("measure", [1]).filter(channels=[AcquireChannel(1)]), - ), - ) - self.assertEqual(sched.instructions, expected_sched.instructions) - - def test_3q_schedule(self): - """Test a schedule that was recommended by David McKay :D""" - - # ┌────┐ - # q0_0: ──■───┤ √X ├─────────── - # ┌─┴─┐ ├───┬┘ - # q0_1: ┤ X ├─┤ X ├───■──────── - # ├───┴┐└───┘ ┌─┴─┐┌────┐ - # q0_2: ┤ √X ├──────┤ X ├┤ √X ├ - # └────┘ └───┘└────┘ - # c0: 3/═══════════════════════ - - q = QuantumRegister(3) - c = ClassicalRegister(3) - qc = QuantumCircuit(q, c) - qc.cx(q[0], q[1]) - qc.sx(q[0]) - qc.x(q[1]) - qc.sx(q[2]) - qc.cx(q[1], q[2]) - qc.sx(q[2]) - - with self.assertWarns(DeprecationWarning): - sched = schedule(qc, self.backend, method="asap") - expected = Schedule( - (0, self.inst_map.get("cx", [0, 1])), - (0, self.inst_map.get("sx", [2])), - (0 + 64, self.inst_map.get("sx", [0])), - (0 + 64, self.inst_map.get("x", [1])), - (0 + 64 + 16, self.inst_map.get("cx", [1, 2])), - (0 + 64 + 16 + 64, self.inst_map.get("sx", [2])), - ) - for actual, expected in zip(sched.instructions, expected.instructions): - self.assertEqual(actual[0], expected[0]) - self.assertEqual(actual[1], expected[1]) - - def test_schedule_multi(self): - """Test scheduling multiple circuits at once.""" - q = QuantumRegister(2) - c = ClassicalRegister(2) - qc0 = QuantumCircuit(q, c) - qc0.cx(q[0], q[1]) - qc1 = QuantumCircuit(q, c) - qc1.cx(q[0], q[1]) - with self.assertWarns(DeprecationWarning): - schedules = schedule([qc0, qc1], self.backend) - expected_insts = schedule(qc0, self.backend).instructions - for actual, expected in zip(schedules[0].instructions, expected_insts): - self.assertEqual(actual[0], expected[0]) - self.assertEqual(actual[1], expected[1]) - - def test_circuit_name_kept(self): - """Test that the new schedule gets its name from the circuit.""" - q = QuantumRegister(2) - c = ClassicalRegister(2) - qc = QuantumCircuit(q, c, name="CIRCNAME") - qc.cx(q[0], q[1]) - with self.assertWarns(DeprecationWarning): - sched = schedule(qc, self.backend, method="asap") - self.assertEqual(sched.name, qc.name) - with self.assertWarns(DeprecationWarning): - sched = schedule(qc, self.backend, method="alap") - self.assertEqual(sched.name, qc.name) - - def test_can_add_gates_into_free_space(self): - """The scheduler does some time bookkeeping to know when qubits are free to be - scheduled. Make sure this works for qubits that are used in the future. This was - a bug, uncovered by this example: - - q0 = - - - - |X| - q1 = |X| |u2| |X| - - In ALAP scheduling, the next operation on qubit 0 would be added at t=0 rather - than immediately before the X gate. - """ - qr = QuantumRegister(2) - qc = QuantumCircuit(qr) - for i in range(2): - qc.sx(qr[i]) - qc.x(qr[i]) - qc.sx(qr[i]) - with self.assertWarns(DeprecationWarning): - sched = schedule(qc, self.backend, method="alap") - expected = Schedule( - (0, self.inst_map.get("sx", [0])), - (0, self.inst_map.get("sx", [1])), - (0 + 16, self.inst_map.get("x", [0])), - (0 + 16, self.inst_map.get("x", [1])), - (0 + 16 + 16, self.inst_map.get("sx", [0])), - (0 + 16 + 16, self.inst_map.get("sx", [1])), - ) - for actual, expected in zip(sched.instructions, expected.instructions): - self.assertEqual(actual[0], expected[0]) - self.assertEqual(actual[1], expected[1]) - - def test_barriers_in_middle(self): - """As a follow on to `test_can_add_gates_into_free_space`, similar issues - arose for barriers, specifically. - """ - qr = QuantumRegister(2) - qc = QuantumCircuit(qr) - for i in range(2): - qc.sx(qr[i]) - qc.barrier(qr[i]) - qc.x(qr[i]) - qc.barrier(qr[i]) - qc.sx(qr[i]) - with self.assertWarns(DeprecationWarning): - sched = schedule(qc, self.backend, method="alap") - expected = Schedule( - (0, self.inst_map.get("sx", [0])), - (0, self.inst_map.get("sx", [1])), - (0 + 16, self.inst_map.get("x", [0])), - (0 + 16, self.inst_map.get("x", [1])), - (0 + 16 + 16, self.inst_map.get("sx", [0])), - (0 + 16 + 16, self.inst_map.get("sx", [1])), - ) - for actual, expected in zip(sched.instructions, expected.instructions): - self.assertEqual(actual[0], expected[0]) - self.assertEqual(actual[1], expected[1]) - - def test_parametric_input(self): - """Test that scheduling works with parametric pulses as input.""" - qr = QuantumRegister(1) - qc = QuantumCircuit(qr) - qc.append(Gate("gauss", 1, []), qargs=[qr[0]]) - with self.assertWarns(DeprecationWarning): - custom_gauss = Schedule( - Play(Gaussian(duration=25, sigma=4, amp=0.5, angle=pi / 2), DriveChannel(0)) - ) - self.inst_map.add("gauss", [0], custom_gauss) - with self.assertWarns(DeprecationWarning): - sched = schedule(qc, self.backend, inst_map=self.inst_map) - self.assertEqual(sched.instructions[0], custom_gauss.instructions[0]) - - def test_pulse_gates(self): - """Test scheduling calibrated pulse gates.""" - q = QuantumRegister(2) - qc = QuantumCircuit(q) - qc.append(U2Gate(0, 0), [q[0]]) - qc.barrier(q[0], q[1]) - qc.append(U2Gate(0, 0), [q[1]]) - with self.assertWarns(DeprecationWarning): - qc.add_calibration( - "u2", [0], Schedule(Play(Gaussian(28, 0.2, 4), DriveChannel(0))), [0, 0] - ) - qc.add_calibration( - "u2", [1], Schedule(Play(Gaussian(28, 0.2, 4), DriveChannel(1))), [0, 0] - ) - - with self.assertWarns(DeprecationWarning): - sched = schedule(qc, self.backend) - expected = Schedule( - Play(Gaussian(28, 0.2, 4), DriveChannel(0)), - (28, Schedule(Play(Gaussian(28, 0.2, 4), DriveChannel(1)))), - ) - self.assertEqual(sched.instructions, expected.instructions) - - def test_calibrated_measurements(self): - """Test scheduling calibrated measurements.""" - q = QuantumRegister(2) - c = ClassicalRegister(2) - qc = QuantumCircuit(q, c) - qc.sx(0) - qc.measure(q[0], c[0]) - - with self.assertWarns(DeprecationWarning): - meas_sched = Play( - GaussianSquare( - duration=1472, - sigma=64, - width=1216, - amp=0.2400000000002, - angle=-0.247301694, - name="my_custom_calibration", - ), - MeasureChannel(0), - ) - meas_sched |= Acquire(1472, AcquireChannel(0), MemorySlot(0)) - qc.add_calibration("measure", [0], meas_sched) - - sched = schedule(qc, self.backend) - expected = Schedule(self.inst_map.get("sx", [0]), (16, meas_sched)) - self.assertEqual(sched.instructions, expected.instructions) - - def test_subset_calibrated_measurements(self): - """Test that measurement calibrations can be added and used for some qubits, even - if the other qubits do not also have calibrated measurements.""" - qc = QuantumCircuit(3, 3) - qc.measure(0, 0) - qc.measure(1, 1) - qc.measure(2, 2) - meas_scheds = [] - with self.assertWarns(DeprecationWarning): - for qubit in [0, 2]: - meas = Play(Gaussian(1200, 0.2, 4), MeasureChannel(qubit)) + Acquire( - 1200, AcquireChannel(qubit), MemorySlot(qubit) - ) - meas_scheds.append(meas) - with self.assertWarns(DeprecationWarning): - qc.add_calibration("measure", [qubit], meas) - - meas = macros.measure(qubits=[1], backend=self.backend, qubit_mem_slots={0: 0, 1: 1}) - meas = meas.exclude(channels=[AcquireChannel(0), AcquireChannel(2)]) - sched = schedule(qc, self.backend) - expected = Schedule(meas_scheds[0], meas_scheds[1], meas) - self.assertEqual(sched.instructions, expected.instructions) - - def test_clbits_of_calibrated_measurements(self): - """Test that calibrated measurements are only used when the classical bits also match.""" - q = QuantumRegister(2) - c = ClassicalRegister(2) - qc = QuantumCircuit(q, c) - qc.measure(q[0], c[1]) - - with self.assertWarns(DeprecationWarning): - meas_sched = Play(Gaussian(1200, 0.2, 4), MeasureChannel(0)) - meas_sched |= Acquire(1200, AcquireChannel(0), MemorySlot(0)) - qc.add_calibration("measure", [0], meas_sched) - - sched = schedule(qc, self.backend) - # Doesn't use the calibrated schedule because the classical memory slots do not match - expected = Schedule(macros.measure([0], self.backend, qubit_mem_slots={0: 1})) - self.assertEqual(sched.instructions, expected.instructions) - - def test_metadata_is_preserved_alap(self): - """Test that circuit metadata is preserved in output schedule with alap.""" - q = QuantumRegister(2) - qc = QuantumCircuit(q) - qc.sx(q[0]) - qc.barrier(q[0], q[1]) - qc.sx(q[1]) - qc.metadata = {"experiment_type": "gst", "execution_number": "1234"} - with self.assertWarns(DeprecationWarning): - sched = schedule(qc, self.backend, method="alap") - self.assertEqual({"experiment_type": "gst", "execution_number": "1234"}, sched.metadata) - - def test_metadata_is_preserved_asap(self): - """Test that circuit metadata is preserved in output schedule with asap.""" - q = QuantumRegister(2) - qc = QuantumCircuit(q) - qc.sx(q[0]) - qc.barrier(q[0], q[1]) - qc.sx(q[1]) - qc.metadata = {"experiment_type": "gst", "execution_number": "1234"} - with self.assertWarns(DeprecationWarning): - sched = schedule(qc, self.backend, method="asap") - self.assertEqual({"experiment_type": "gst", "execution_number": "1234"}, sched.metadata) - - def test_scheduler_with_params_bound(self): - """Test scheduler with parameters defined and bound""" - x = Parameter("x") - qc = QuantumCircuit(2) - qc.append(Gate("pulse_gate", 1, [x]), [0]) - with self.assertWarns(DeprecationWarning): - expected_schedule = Schedule() - qc.add_calibration( - gate="pulse_gate", qubits=[0], schedule=expected_schedule, params=[x] - ) - qc = qc.assign_parameters({x: 1}) - with self.assertWarns(DeprecationWarning): - sched = schedule(qc, self.backend) - self.assertEqual(sched, expected_schedule) - - def test_scheduler_with_params_not_bound(self): - """Test scheduler with parameters defined but not bound""" - x = Parameter("amp") - qc = QuantumCircuit(2) - qc.append(Gate("pulse_gate", 1, [x]), [0]) - with self.assertWarns(DeprecationWarning): - with build() as expected_schedule: - play(Gaussian(duration=160, amp=x, sigma=40), DriveChannel(0)) - qc.add_calibration( - gate="pulse_gate", qubits=[0], schedule=expected_schedule, params=[x] - ) - sched = schedule(qc, self.backend) - with self.assertWarns(DeprecationWarning): - self.assertEqual(sched, transforms.target_qobj_transform(expected_schedule)) - - def test_schedule_block_in_instmap(self): - """Test schedule block in instmap can be scheduled.""" - duration = Parameter("duration") - - with self.assertWarns(DeprecationWarning): - with build() as pulse_prog: - play(Gaussian(duration, 0.1, 10), DriveChannel(0)) - - instmap = InstructionScheduleMap() - instmap.add("block_gate", (0,), pulse_prog, ["duration"]) - - qc = QuantumCircuit(1) - qc.append(Gate("block_gate", 1, [duration]), [0]) - qc.assign_parameters({duration: 100}, inplace=True) - - with self.assertWarns(DeprecationWarning): - sched = schedule(qc, self.backend, inst_map=instmap) - - ref_sched = Schedule() - ref_sched += Play(Gaussian(100, 0.1, 10), DriveChannel(0)) - - self.assertEqual(sched, ref_sched) - - def test_inst_sched_map_get_measure_0(self): - """Test that Schedule returned by backend.instruction_schedule_map.get('measure', [0]) - is actually Schedule for just qubit_0""" - with self.assertWarns(DeprecationWarning): - sched_from_backend = self.backend.instruction_schedule_map.get("measure", [0]) - expected_sched = Schedule( - ( - 0, - Play( - Waveform(samples=self.pulse_2_samples, name="pulse_2"), - MeasureChannel(0), - name="pulse_2", - ), - ), - ( - 0, - Play( - Waveform(samples=self.pulse_2_samples, name="pulse_2"), - MeasureChannel(1), - name="pulse_2", - ), - ), - ( - 0, - Play( - Waveform(samples=self.pulse_2_samples, name="pulse_2"), - MeasureChannel(2), - name="pulse_2", - ), - ), - (0, Acquire(1792, AcquireChannel(0), MemorySlot(0))), - (0, Acquire(1792, AcquireChannel(1), MemorySlot(1))), - (0, Acquire(1792, AcquireChannel(2), MemorySlot(2))), - name="measure", - ) - self.assertEqual(sched_from_backend, expected_sched) From 02850cfa95a2e724b6a1c64c74a15126761e72c3 Mon Sep 17 00:00:00 2001 From: Shelly Garion <46566946+ShellyGarion@users.noreply.github.com> Date: Mon, 10 Feb 2025 12:40:18 +0200 Subject: [PATCH 21/48] Add 2q fractional gates to the `UnitarySynthesis` transpiler pass (#13568) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * TwoQubitControlledUDecomposer to _decomposer_2q_from_basis_gates * update (temporarily) basis gates in test * minor fix * add EulerBasis as a parameter to TwoQubitControlledUDecomposer * fix global_phase calculation in TwoQubitContolledUDecomposer * add TwoQubitControlledUDecomposer to the docs * make the choice of kak_gate deterministic * remove XXDecomposer from _decomposer_2q_from_basis_gates * make call_inner pub, add Clone, Debug * add TwoQubitControlledUDecomposer to unitary_synthesis.rs * Fix exit condition for GOODBYE_SET and PARAM_SET * make DEFAULT_ATOL public * add TwoQubitControlledUDecomposer to synth_su4_sequence * Add support for parametrized decomposer gate in apply_synth_sequence * change DecomposerType enum to fix clippy error * add a random unitary test to test_parametrized_basis_gate_in_target * add public new_inner for TwoQubitControlledUDecomposer * replace default 'ZYZ' by 'ZXZ' in TwoQubitControlledUDecomposer * remove using py in rust functions * minor update to test * make atol optional * add a test with fractional gates in the backend * add release notes * enhance tests following review * Add support for non-standard parametrized gates, add new tests. TODO: address TwoQubitControlledUDecomposer issue, it appends gates outside of basis set (h/s/sdg) * decompose S, Sdg, H into euler_basis * update test * Overwrite Python-side gate parameters as well as Rust-side parameters. * add examples to release notes --------- Co-authored-by: Elena Peña Tapia --- crates/accelerate/src/two_qubit_decompose.rs | 205 ++++++++++------ crates/accelerate/src/unitary_synthesis.rs | 227 +++++++++++++----- qiskit/synthesis/__init__.py | 2 + .../two_qubit/two_qubit_decompose.py | 24 +- .../passes/synthesis/unitary_synthesis.py | 48 +++- ...nitarysynthesis-pass-f66eee29903f5639.yaml | 37 +++ test/python/compiler/test_transpiler.py | 2 +- test/python/synthesis/test_synthesis.py | 30 ++- .../transpiler/test_unitary_synthesis.py | 107 ++++++++- 9 files changed, 528 insertions(+), 154 deletions(-) create mode 100644 releasenotes/notes/add-2q-fractional-gates-to-unitarysynthesis-pass-f66eee29903f5639.yaml diff --git a/crates/accelerate/src/two_qubit_decompose.rs b/crates/accelerate/src/two_qubit_decompose.rs index de20430d25bf..5ce6d248c4eb 100644 --- a/crates/accelerate/src/two_qubit_decompose.rs +++ b/crates/accelerate/src/two_qubit_decompose.rs @@ -54,7 +54,9 @@ use rand_pcg::Pcg64Mcg; use qiskit_circuit::circuit_data::CircuitData; use qiskit_circuit::circuit_instruction::OperationFromPython; -use qiskit_circuit::gate_matrix::{CX_GATE, H_GATE, ONE_QUBIT_IDENTITY, SX_GATE, X_GATE}; +use qiskit_circuit::gate_matrix::{ + CX_GATE, H_GATE, ONE_QUBIT_IDENTITY, SDG_GATE, SX_GATE, S_GATE, X_GATE, +}; use qiskit_circuit::operations::{Operation, Param, StandardGate}; use qiskit_circuit::packed_instruction::PackedOperation; use qiskit_circuit::slice::{PySequenceIndex, SequenceIndex}; @@ -2448,23 +2450,25 @@ pub enum RXXEquivalent { } impl RXXEquivalent { - fn matrix(&self, py: Python, param: f64) -> PyResult> { + fn matrix(&self, param: f64) -> PyResult> { match self { Self::Standard(gate) => Ok(gate.matrix(&[Param::Float(param)]).unwrap()), - Self::CustomPython(gate_cls) => { + Self::CustomPython(gate_cls) => Python::with_gil(|py: Python| { let gate_obj = gate_cls.bind(py).call1((param,))?; let raw_matrix = gate_obj .call_method0(intern!(py, "to_matrix"))? .extract::>()?; Ok(raw_matrix.as_array().to_owned()) - } + }), } } } +#[derive(Clone, Debug)] #[pyclass(module = "qiskit._accelerate.two_qubit_decompose", subclass)] pub struct TwoQubitControlledUDecomposer { rxx_equivalent_gate: RXXEquivalent, + euler_basis: EulerBasis, #[pyo3(get)] scale: f64, } @@ -2479,7 +2483,6 @@ impl TwoQubitControlledUDecomposer { /// invert 2q gate sequence fn invert_2q_gate( &self, - py: Python, gate: (Option, SmallVec<[f64; 3]>, SmallVec<[u8; 2]>), ) -> PyResult { let (gate, params, qubits) = gate; @@ -2516,7 +2519,7 @@ impl TwoQubitControlledUDecomposer { .collect::>(); Ok((Some(inv_gate.0), inv_gate_params, qubits)) } - RXXEquivalent::CustomPython(gate_cls) => { + RXXEquivalent::CustomPython(gate_cls) => Python::with_gil(|py: Python| { let gate_obj = gate_cls.bind(py).call1(PyTuple::new(py, params)?)?; let raw_inverse = gate_obj.call_method0(intern!(py, "inverse"))?; let inverse: OperationFromPython = raw_inverse.extract()?; @@ -2537,7 +2540,7 @@ impl TwoQubitControlledUDecomposer { "rxx gate inverse is not valid for this decomposer", )) } - } + }), } } } @@ -2550,20 +2553,19 @@ impl TwoQubitControlledUDecomposer { /// Circuit: Circuit equivalent to an RXXGate. /// Raises: /// QiskitError: If the circuit is not equivalent to an RXXGate. - fn to_rxx_gate(&self, py: Python, angle: f64) -> PyResult { + fn to_rxx_gate(&self, angle: f64) -> PyResult { // The user-provided RXXGate equivalent gate may be locally equivalent to the RXXGate // but with some scaling in the rotation angle. For example, RXXGate(angle) has Weyl // parameters (angle, 0, 0) for angle in [0, pi/2] but the user provided gate, i.e. // :code:`self.rxx_equivalent_gate(angle)` might produce the Weyl parameters // (scale * angle, 0, 0) where scale != 1. This is the case for the CPhaseGate. - let mat = self.rxx_equivalent_gate.matrix(py, self.scale * angle)?; + let mat = self.rxx_equivalent_gate.matrix(self.scale * angle)?; let decomposer_inv = TwoQubitWeylDecomposition::new_inner(mat.view(), Some(DEFAULT_FIDELITY), None)?; - let euler_basis = EulerBasis::ZYZ; let mut target_1q_basis_list = EulerBasisSet::new(); - target_1q_basis_list.add_basis(euler_basis); + target_1q_basis_list.add_basis(self.euler_basis); // Express the RXXGate in terms of the user-provided RXXGate equivalent gate. let mut gates = Vec::with_capacity(13); @@ -2600,14 +2602,14 @@ impl TwoQubitControlledUDecomposer { gates.push((None, smallvec![self.scale * angle], smallvec![0, 1])); if let Some(unitary_k1r) = unitary_k1r { - global_phase += unitary_k1r.global_phase; + global_phase -= unitary_k1r.global_phase; for gate in unitary_k1r.gates.into_iter().rev() { let (inv_gate_name, inv_gate_params) = invert_1q_gate(gate); gates.push((Some(inv_gate_name), inv_gate_params, smallvec![0])); } } if let Some(unitary_k1l) = unitary_k1l { - global_phase += unitary_k1l.global_phase; + global_phase -= unitary_k1l.global_phase; for gate in unitary_k1l.gates.into_iter().rev() { let (inv_gate_name, inv_gate_params) = invert_1q_gate(gate); gates.push((Some(inv_gate_name), inv_gate_params, smallvec![1])); @@ -2623,28 +2625,65 @@ impl TwoQubitControlledUDecomposer { /// Appends U_d(a, b, c) to the circuit. fn weyl_gate( &self, - py: Python, circ: &mut TwoQubitGateSequence, target_decomposed: TwoQubitWeylDecomposition, atol: f64, ) -> PyResult<()> { - let circ_a = self.to_rxx_gate(py, -2.0 * target_decomposed.a)?; + let circ_a = self.to_rxx_gate(-2.0 * target_decomposed.a)?; circ.gates.extend(circ_a.gates); let mut global_phase = circ_a.global_phase; + let mut target_1q_basis_list = EulerBasisSet::new(); + target_1q_basis_list.add_basis(self.euler_basis); + + let s_decomp = unitary_to_gate_sequence_inner( + aview2(&S_GATE), + &target_1q_basis_list, + 0, + None, + true, + None, + ); + let sdg_decomp = unitary_to_gate_sequence_inner( + aview2(&SDG_GATE), + &target_1q_basis_list, + 0, + None, + true, + None, + ); + let h_decomp = unitary_to_gate_sequence_inner( + aview2(&H_GATE), + &target_1q_basis_list, + 0, + None, + true, + None, + ); + // translate the RYYGate(b) into a circuit based on the desired Ctrl-U gate. if (target_decomposed.b).abs() > atol { - let circ_b = self.to_rxx_gate(py, -2.0 * target_decomposed.b)?; + let circ_b = self.to_rxx_gate(-2.0 * target_decomposed.b)?; global_phase += circ_b.global_phase; - circ.gates - .push((Some(StandardGate::SdgGate), smallvec![], smallvec![0])); - circ.gates - .push((Some(StandardGate::SdgGate), smallvec![], smallvec![1])); + if let Some(sdg_decomp) = sdg_decomp { + global_phase += 2.0 * sdg_decomp.global_phase; + for gate in sdg_decomp.gates.into_iter() { + let gate_params = gate.1; + circ.gates + .push((Some(gate.0), gate_params.clone(), smallvec![0])); + circ.gates.push((Some(gate.0), gate_params, smallvec![1])); + } + } circ.gates.extend(circ_b.gates); - circ.gates - .push((Some(StandardGate::SGate), smallvec![], smallvec![0])); - circ.gates - .push((Some(StandardGate::SGate), smallvec![], smallvec![1])); + if let Some(s_decomp) = s_decomp { + global_phase += 2.0 * s_decomp.global_phase; + for gate in s_decomp.gates.into_iter() { + let gate_params = gate.1; + circ.gates + .push((Some(gate.0), gate_params.clone(), smallvec![0])); + circ.gates.push((Some(gate.0), gate_params, smallvec![1])); + } + } } // # translate the RZZGate(c) into a circuit based on the desired Ctrl-U gate. @@ -2656,36 +2695,57 @@ impl TwoQubitControlledUDecomposer { // circuit if c < 0. let mut gamma = -2.0 * target_decomposed.c; if gamma <= 0.0 { - let circ_c = self.to_rxx_gate(py, gamma)?; + let circ_c = self.to_rxx_gate(gamma)?; global_phase += circ_c.global_phase; - circ.gates - .push((Some(StandardGate::HGate), smallvec![], smallvec![0])); - circ.gates - .push((Some(StandardGate::HGate), smallvec![], smallvec![1])); + + if let Some(ref h_decomp) = h_decomp { + global_phase += 2.0 * h_decomp.global_phase; + for gate in h_decomp.gates.clone().into_iter() { + let gate_params = gate.1; + circ.gates + .push((Some(gate.0), gate_params.clone(), smallvec![0])); + circ.gates.push((Some(gate.0), gate_params, smallvec![1])); + } + } circ.gates.extend(circ_c.gates); - circ.gates - .push((Some(StandardGate::HGate), smallvec![], smallvec![0])); - circ.gates - .push((Some(StandardGate::HGate), smallvec![], smallvec![1])); + if let Some(ref h_decomp) = h_decomp { + global_phase += 2.0 * h_decomp.global_phase; + for gate in h_decomp.gates.clone().into_iter() { + let gate_params = gate.1; + circ.gates + .push((Some(gate.0), gate_params.clone(), smallvec![0])); + circ.gates.push((Some(gate.0), gate_params, smallvec![1])); + } + } } else { // invert the circuit above gamma *= -1.0; - let circ_c = self.to_rxx_gate(py, gamma)?; + let circ_c = self.to_rxx_gate(gamma)?; global_phase -= circ_c.global_phase; - circ.gates - .push((Some(StandardGate::HGate), smallvec![], smallvec![0])); - circ.gates - .push((Some(StandardGate::HGate), smallvec![], smallvec![1])); + if let Some(ref h_decomp) = h_decomp { + global_phase += 2.0 * h_decomp.global_phase; + for gate in h_decomp.gates.clone().into_iter() { + let gate_params = gate.1; + circ.gates + .push((Some(gate.0), gate_params.clone(), smallvec![0])); + circ.gates.push((Some(gate.0), gate_params, smallvec![1])); + } + } for gate in circ_c.gates.into_iter().rev() { let (inv_gate_name, inv_gate_params, inv_gate_qubits) = - self.invert_2q_gate(py, gate)?; + self.invert_2q_gate(gate)?; circ.gates .push((inv_gate_name, inv_gate_params, inv_gate_qubits)); } - circ.gates - .push((Some(StandardGate::HGate), smallvec![], smallvec![0])); - circ.gates - .push((Some(StandardGate::HGate), smallvec![], smallvec![1])); + if let Some(ref h_decomp) = h_decomp { + global_phase += 2.0 * h_decomp.global_phase; + for gate in h_decomp.gates.clone().into_iter() { + let gate_params = gate.1; + circ.gates + .push((Some(gate.0), gate_params.clone(), smallvec![0])); + circ.gates.push((Some(gate.0), gate_params, smallvec![1])); + } + } } } @@ -2695,18 +2755,16 @@ impl TwoQubitControlledUDecomposer { /// Returns the Weyl decomposition in circuit form. /// Note: atol is passed to OneQubitEulerDecomposer. - fn call_inner( + pub fn call_inner( &self, - py: Python, unitary: ArrayView2, - atol: f64, + atol: Option, ) -> PyResult { let target_decomposed = TwoQubitWeylDecomposition::new_inner(unitary, Some(DEFAULT_FIDELITY), None)?; - let euler_basis = EulerBasis::ZYZ; let mut target_1q_basis_list = EulerBasisSet::new(); - target_1q_basis_list.add_basis(euler_basis); + target_1q_basis_list.add_basis(self.euler_basis); let c1r = target_decomposed.K1r.view(); let c2r = target_decomposed.K2r.view(); @@ -2741,17 +2799,17 @@ impl TwoQubitControlledUDecomposer { gates, global_phase, }; - self.weyl_gate(py, &mut gates1, target_decomposed, atol)?; + self.weyl_gate(&mut gates1, target_decomposed, atol.unwrap_or(DEFAULT_ATOL))?; global_phase += gates1.global_phase; if let Some(unitary_c1r) = unitary_c1r { - global_phase -= unitary_c1r.global_phase; + global_phase += unitary_c1r.global_phase; for gate in unitary_c1r.gates.into_iter() { gates1.gates.push((Some(gate.0), gate.1, smallvec![0])); } } if let Some(unitary_c1l) = unitary_c1l { - global_phase -= unitary_c1l.global_phase; + global_phase += unitary_c1l.global_phase; for gate in unitary_c1l.gates.into_iter() { gates1.gates.push((Some(gate.0), gate.1, smallvec![1])); } @@ -2760,19 +2818,9 @@ impl TwoQubitControlledUDecomposer { gates1.global_phase = global_phase; Ok(gates1) } -} -#[pymethods] -impl TwoQubitControlledUDecomposer { - /// Initialize the KAK decomposition. - /// Args: - /// rxx_equivalent_gate: Gate that is locally equivalent to an :class:`.RXXGate`: - /// :math:`U \sim U_d(\alpha, 0, 0) \sim \text{Ctrl-U}` gate. - /// Raises: - /// QiskitError: If the gate is not locally equivalent to an :class:`.RXXGate`. - #[new] - #[pyo3(signature=(rxx_equivalent_gate))] - pub fn new(py: Python, rxx_equivalent_gate: RXXEquivalent) -> PyResult { + /// Initialize the KAK decomposition. + pub fn new_inner(rxx_equivalent_gate: RXXEquivalent, euler_basis: &str) -> PyResult { let atol = DEFAULT_ATOL; let test_angles = [0.2, 0.3, PI2]; @@ -2788,14 +2836,17 @@ impl TwoQubitControlledUDecomposer { } } RXXEquivalent::CustomPython(gate_cls) => { - if gate_cls.bind(py).call1((test_angle,)).ok().is_none() { + let takes_param = Python::with_gil(|py: Python| { + gate_cls.bind(py).call1((test_angle,)).ok().is_none() + }); + if takes_param { return Err(QiskitError::new_err( "Equivalent gate needs to take exactly 1 angle parameter.", )); } } }; - let mat = rxx_equivalent_gate.matrix(py, test_angle)?; + let mat = rxx_equivalent_gate.matrix(test_angle)?; let decomp = TwoQubitWeylDecomposition::new_inner(mat.view(), Some(DEFAULT_FIDELITY), None)?; let mat_rxx = StandardGate::RXXGate @@ -2836,17 +2887,35 @@ impl TwoQubitControlledUDecomposer { Ok(TwoQubitControlledUDecomposer { scale, rxx_equivalent_gate, + euler_basis: EulerBasis::__new__(euler_basis)?, }) } +} - #[pyo3(signature=(unitary, atol))] +#[pymethods] +impl TwoQubitControlledUDecomposer { + /// Initialize the KAK decomposition. + /// Args: + /// rxx_equivalent_gate: Gate that is locally equivalent to an :class:`.RXXGate`: + /// :math:`U \sim U_d(\alpha, 0, 0) \sim \text{Ctrl-U}` gate. + /// euler_basis: Basis string to be provided to :class:`.OneQubitEulerDecomposer` + /// for 1Q synthesis. + /// Raises: + /// QiskitError: If the gate is not locally equivalent to an :class:`.RXXGate`. + #[new] + #[pyo3(signature=(rxx_equivalent_gate, euler_basis="ZXZ"))] + pub fn new(rxx_equivalent_gate: RXXEquivalent, euler_basis: &str) -> PyResult { + TwoQubitControlledUDecomposer::new_inner(rxx_equivalent_gate, euler_basis) + } + + #[pyo3(signature=(unitary, atol=None))] fn __call__( &self, py: Python, unitary: PyReadonlyArray2, - atol: f64, + atol: Option, ) -> PyResult { - let sequence = self.call_inner(py, unitary.as_array(), atol)?; + let sequence = self.call_inner(unitary.as_array(), atol)?; match &self.rxx_equivalent_gate { RXXEquivalent::Standard(rxx_gate) => CircuitData::from_standard_gates( py, diff --git a/crates/accelerate/src/unitary_synthesis.rs b/crates/accelerate/src/unitary_synthesis.rs index 2721c58c9237..b36715bd2cf9 100644 --- a/crates/accelerate/src/unitary_synthesis.rs +++ b/crates/accelerate/src/unitary_synthesis.rs @@ -27,14 +27,14 @@ use smallvec::{smallvec, SmallVec}; use pyo3::intern; use pyo3::prelude::*; -use pyo3::types::{IntoPyDict, PyDict, PyString}; +use pyo3::types::{IntoPyDict, PyDict, PyString, PyType}; use pyo3::wrap_pyfunction; use pyo3::Python; use qiskit_circuit::converters::{circuit_to_dag, QuantumCircuitData}; use qiskit_circuit::dag_circuit::{DAGCircuit, NodeType}; use qiskit_circuit::imports; -use qiskit_circuit::operations::{Operation, OperationRef, Param, StandardGate}; +use qiskit_circuit::operations::{Operation, OperationRef, Param, PyGate, StandardGate}; use qiskit_circuit::packed_instruction::{PackedInstruction, PackedOperation}; use qiskit_circuit::Qubit; @@ -44,7 +44,8 @@ use crate::euler_one_qubit_decomposer::{ use crate::nlayout::PhysicalQubit; use crate::target_transpiler::{NormalOperation, Target}; use crate::two_qubit_decompose::{ - TwoQubitBasisDecomposer, TwoQubitGateSequence, TwoQubitWeylDecomposition, + RXXEquivalent, TwoQubitBasisDecomposer, TwoQubitControlledUDecomposer, TwoQubitGateSequence, + TwoQubitWeylDecomposition, }; use crate::QiskitError; @@ -53,10 +54,12 @@ const PI4: f64 = PI / 4.; #[derive(Clone, Debug)] enum DecomposerType { - TwoQubitBasisDecomposer(Box), - XXDecomposer(PyObject), + TwoQubitBasis(Box), + TwoQubitControlledU(Box), + XX(PyObject), } +#[derive(Clone, Debug)] struct DecomposerElement { decomposer: DecomposerType, gate: NormalOperation, @@ -72,6 +75,7 @@ struct TwoQubitUnitarySequence { // then we know TwoQubitBasisDecomposer is an ideal decomposition and there is // no need to bother trying the XXDecomposer. static GOODBYE_SET: [&str; 3] = ["cx", "cz", "ecr"]; +static PARAM_SET: [&str; 8] = ["rzz", "rxx", "ryy", "rzx", "crx", "cry", "crz", "cphase"]; fn get_target_basis_set(target: &Target, qubit: PhysicalQubit) -> EulerBasisSet { let mut target_basis_set: EulerBasisSet = EulerBasisSet::new(); @@ -133,17 +137,56 @@ fn apply_synth_sequence( ) -> PyResult<()> { let mut instructions = Vec::with_capacity(sequence.gate_sequence.gates().len()); for (gate, params, qubit_ids) in sequence.gate_sequence.gates() { - let gate_node = match gate { - None => sequence.decomp_gate.operation.standard_gate(), - Some(gate) => *gate, + let packed_op = match gate { + None => &sequence.decomp_gate.operation, + Some(gate) => &PackedOperation::from_standard_gate(*gate), }; let mapped_qargs: Vec = qubit_ids.iter().map(|id| out_qargs[*id as usize]).collect(); let new_params: Option>> = match gate { Some(_) => Some(Box::new(params.iter().map(|p| Param::Float(*p)).collect())), - None => Some(Box::new(sequence.decomp_gate.params.clone())), + None => { + if !sequence.decomp_gate.params.is_empty() + && matches!(sequence.decomp_gate.params[0], Param::Float(_)) + { + Some(Box::new(sequence.decomp_gate.params.clone())) + } else { + Some(Box::new(params.iter().map(|p| Param::Float(*p)).collect())) + } + } + }; + + let new_op: PackedOperation = match packed_op.py_copy(py)?.view() { + OperationRef::Gate(gate) => { + gate.gate.setattr( + py, + "params", + new_params + .as_deref() + .map(SmallVec::as_slice) + .unwrap_or(&[]) + .iter() + .map(|param| param.clone_ref(py)) + .collect::>(), + )?; + Box::new(PyGate { + gate: gate.gate.clone(), + qubits: gate.qubits, + clbits: gate.clbits, + params: gate.params, + op_name: gate.op_name.clone(), + }) + .into() + } + OperationRef::StandardGate(_) => packed_op.clone(), + _ => { + return Err(QiskitError::new_err( + "Decomposed gate sequence contains unexpected operations.", + )) + } }; + let instruction = PackedInstruction { - op: PackedOperation::from_standard_gate(gate_node), + op: new_op, qubits: out_dag.qargs_interner.insert(&mapped_qargs), clbits: out_dag.cargs_interner.get_default(), params: new_params, @@ -407,8 +450,18 @@ fn run_2q_unitary_synthesis( coupling_edges, target, )?; + match decomposer_item.decomposer { - DecomposerType::TwoQubitBasisDecomposer(_) => { + DecomposerType::TwoQubitBasis(_) => { + let synth = synth_su4_sequence( + &unitary, + decomposer_item, + preferred_dir, + approximation_degree, + )?; + apply_synth_sequence(py, out_dag, out_qargs, &synth)?; + } + DecomposerType::TwoQubitControlledU(_) => { let synth = synth_su4_sequence( &unitary, decomposer_item, @@ -417,7 +470,7 @@ fn run_2q_unitary_synthesis( )?; apply_synth_sequence(py, out_dag, out_qargs, &synth)?; } - DecomposerType::XXDecomposer(_) => { + DecomposerType::XX(_) => { let synth = synth_su4_dag( py, &unitary, @@ -442,7 +495,34 @@ fn run_2q_unitary_synthesis( target, )?; match &decomposer.decomposer { - DecomposerType::TwoQubitBasisDecomposer(_) => { + DecomposerType::TwoQubitBasis(_) => { + let sequence = + synth_su4_sequence(&unitary, decomposer, preferred_dir, approximation_degree)?; + let scoring_info = + sequence + .gate_sequence + .gates() + .iter() + .map(|(gate, params, qubit_ids)| { + let inst_qubits = + qubit_ids.iter().map(|q| ref_qubits[*q as usize]).collect(); + match gate { + Some(gate) => ( + gate.name().to_string(), + Some(params.iter().map(|p| Param::Float(*p)).collect()), + inst_qubits, + ), + None => ( + sequence.decomp_gate.operation.name().to_string(), + Some(params.iter().map(|p| Param::Float(*p)).collect()), + inst_qubits, + ), + } + }); + let synth_error_from_target = synth_error(py, scoring_info, target); + synth_errors_sequence.push((sequence, synth_error_from_target)); + } + DecomposerType::TwoQubitControlledU(_) => { let sequence = synth_su4_sequence(&unitary, decomposer, preferred_dir, approximation_degree)?; let scoring_info = @@ -460,12 +540,7 @@ fn run_2q_unitary_synthesis( inst_qubits, ), None => ( - sequence - .decomp_gate - .operation - .standard_gate() - .name() - .to_string(), + sequence.decomp_gate.operation.name().to_string(), Some(params.iter().map(|p| Param::Float(*p)).collect()), inst_qubits, ), @@ -474,7 +549,7 @@ fn run_2q_unitary_synthesis( let synth_error_from_target = synth_error(py, scoring_info, target); synth_errors_sequence.push((sequence, synth_error_from_target)); } - DecomposerType::XXDecomposer(_) => { + DecomposerType::XX(_) => { let synth_dag = synth_su4_dag( py, &unitary, @@ -543,6 +618,8 @@ fn get_2q_decomposers_from_target( let reverse_qubits: SmallVec<[PhysicalQubit; 2]> = qubits.iter().rev().copied().collect(); let mut available_2q_basis: IndexMap<&str, NormalOperation> = IndexMap::new(); let mut available_2q_props: IndexMap<&str, (Option, Option)> = IndexMap::new(); + let mut available_2q_param_basis: IndexMap<&str, NormalOperation> = IndexMap::new(); + let mut available_2q_param_props: IndexMap<&str, (Option, Option)> = IndexMap::new(); let mut qubit_gate_map = IndexMap::new(); @@ -565,28 +642,10 @@ fn get_2q_decomposers_from_target( } #[inline] - fn replace_parametrized_gate(mut op: NormalOperation) -> NormalOperation { - if let Some(std_gate) = op.operation.try_standard_gate() { - match std_gate.name() { - "rxx" => { - if let Param::ParameterExpression(_) = op.params[0] { - op.params[0] = Param::Float(PI2) - } - } - "rzx" => { - if let Param::ParameterExpression(_) = op.params[0] { - op.params[0] = Param::Float(PI4) - } - } - "rzz" => { - if let Param::ParameterExpression(_) = op.params[0] { - op.params[0] = Param::Float(PI2) - } - } - _ => (), - } - } - op + fn check_parametrized_gate(op: &NormalOperation) -> bool { + // The gate counts as parametrized if there is any + // non-float parameter + !op.params.iter().all(|p| matches!(p, Param::Float(_))) } for (q_pair, gates) in qubit_gate_map { @@ -602,8 +661,21 @@ fn get_2q_decomposers_from_target( if op.operation.num_qubits() != 2 { continue; } - available_2q_basis.insert(key, replace_parametrized_gate(op.clone())); - + if check_parametrized_gate(op) { + available_2q_param_basis.insert(key, op.clone()); + if target.contains_key(key) { + available_2q_param_props.insert( + key, + match &target[key].get(Some(q_pair)) { + Some(Some(props)) => (props.duration, props.error), + _ => (None, None), + }, + ); + } else { + continue; + } + } + available_2q_basis.insert(key, op.clone()); if target.contains_key(key) { available_2q_props.insert( key, @@ -620,7 +692,8 @@ fn get_2q_decomposers_from_target( } } } - if available_2q_basis.is_empty() { + + if available_2q_basis.is_empty() && available_2q_param_basis.is_empty() { return Err(QiskitError::new_err( "Target has no gates available on qubits to synthesize over.", )); @@ -655,7 +728,6 @@ fn get_2q_decomposers_from_target( } } - // Iterate over 1q and 2q supercontrolled basis, append TwoQubitBasisDecomposers let supercontrolled_basis: IndexMap<&str, NormalOperation> = available_2q_basis .iter() .filter(|(_, v)| is_supercontrolled(v)) @@ -680,26 +752,67 @@ fn get_2q_decomposers_from_target( )?; decomposers.push(DecomposerElement { - decomposer: DecomposerType::TwoQubitBasisDecomposer(Box::new(decomposer)), + decomposer: DecomposerType::TwoQubitBasis(Box::new(decomposer)), gate: gate.clone(), }); } } // If our 2q basis gates are a subset of cx, ecr, or cz then we know TwoQubitBasisDecomposer - // is an ideal decomposition and there is no need to bother calculating the XX embodiments - // or try the XX decomposer + // is an ideal decomposition and there is no need to try other decomposers let available_basis_set: IndexSet<&str> = available_2q_basis.keys().copied().collect(); #[inline] fn check_goodbye(basis_set: &IndexSet<&str>) -> bool { - basis_set.iter().all(|gate| GOODBYE_SET.contains(gate)) + !basis_set.is_empty() && basis_set.iter().all(|gate| GOODBYE_SET.contains(gate)) } if check_goodbye(&available_basis_set) { return Ok(Some(decomposers)); } + for basis_1q in &available_1q_basis { + for (_basis_2q, gate) in available_2q_param_basis.iter() { + let rxx_equivalent_gate = if let Some(std_gate) = gate.operation.try_standard_gate() { + RXXEquivalent::Standard(std_gate) + } else { + let module = PyModule::import(py, "builtins")?; + let py_type = module.getattr("type")?; + let gate_type = py_type + .call1((gate.clone().into_pyobject(py)?,))? + .downcast_into::()? + .unbind(); + + RXXEquivalent::CustomPython(gate_type) + }; + + match TwoQubitControlledUDecomposer::new_inner(rxx_equivalent_gate, basis_1q) { + Ok(decomposer) => { + decomposers.push(DecomposerElement { + decomposer: DecomposerType::TwoQubitControlledU(Box::new(decomposer)), + gate: gate.clone(), + }); + } + Err(_) => continue, + }; + } + } + + // If our 2q basis gates are a subset of PARAM_SET, then we will use the TwoQubitControlledUDecomposer + // and there is no need to try other decomposers + + let available_basis_param_set: IndexSet<&str> = + available_2q_param_basis.keys().copied().collect(); + + #[inline] + fn check_parametrized_goodbye(basis_set: &IndexSet<&str>) -> bool { + !basis_set.is_empty() && basis_set.iter().all(|gate| PARAM_SET.contains(gate)) + } + + if check_parametrized_goodbye(&available_basis_param_set) { + return Ok(Some(decomposers)); + } + // Let's now look for possible controlled decomposers (i.e. XXDecomposer) let controlled_basis: IndexMap<&str, NormalOperation> = available_2q_basis .iter() @@ -787,7 +900,7 @@ fn get_2q_decomposers_from_target( .extract::()?; decomposers.push(DecomposerElement { - decomposer: DecomposerType::XXDecomposer(decomposer.into()), + decomposer: DecomposerType::XX(decomposer.into()), gate: decomposer_gate, }); } @@ -887,8 +1000,10 @@ fn synth_su4_sequence( approximation_degree: Option, ) -> PyResult { let is_approximate = approximation_degree.is_none() || approximation_degree.unwrap() != 1.0; - let synth = if let DecomposerType::TwoQubitBasisDecomposer(decomp) = &decomposer_2q.decomposer { + let synth = if let DecomposerType::TwoQubitBasis(decomp) = &decomposer_2q.decomposer { decomp.call_inner(su4_mat.view(), None, is_approximate, None)? + } else if let DecomposerType::TwoQubitControlledU(decomp) = &decomposer_2q.decomposer { + decomp.call_inner(su4_mat.view(), None)? } else { unreachable!("synth_su4_sequence should only be called for TwoQubitBasisDecomposer.") }; @@ -947,8 +1062,10 @@ fn reversed_synth_su4_sequence( let (mut col_1, mut col_2) = su4_mat.multi_slice_mut((s![.., 1], s![.., 2])); azip!((x in &mut col_1, y in &mut col_2) (*x, *y) = (*y, *x)); - let synth = if let DecomposerType::TwoQubitBasisDecomposer(decomp) = &decomposer_2q.decomposer { + let synth = if let DecomposerType::TwoQubitBasis(decomp) = &decomposer_2q.decomposer { decomp.call_inner(su4_mat.view(), None, is_approximate, None)? + } else if let DecomposerType::TwoQubitControlledU(decomp) = &decomposer_2q.decomposer { + decomp.call_inner(su4_mat.view(), None)? } else { unreachable!( "reversed_synth_su4_sequence should only be called for TwoQubitBasisDecomposer." @@ -982,7 +1099,7 @@ fn synth_su4_dag( approximation_degree: Option, ) -> PyResult { let is_approximate = approximation_degree.is_none() || approximation_degree.unwrap() != 1.0; - let synth_dag = if let DecomposerType::XXDecomposer(decomposer) = &decomposer_2q.decomposer { + let synth_dag = if let DecomposerType::XX(decomposer) = &decomposer_2q.decomposer { let kwargs: HashMap<&str, bool> = [("approximate", is_approximate), ("use_dag", true)] .into_iter() .collect(); @@ -1048,7 +1165,7 @@ fn reversed_synth_su4_dag( let (mut col_1, mut col_2) = su4_mat.multi_slice_mut((s![.., 1], s![.., 2])); azip!((x in &mut col_1, y in &mut col_2) (*x, *y) = (*y, *x)); - let synth_dag = if let DecomposerType::XXDecomposer(decomposer) = &decomposer_2q.decomposer { + let synth_dag = if let DecomposerType::XX(decomposer) = &decomposer_2q.decomposer { let kwargs: HashMap<&str, bool> = [("approximate", is_approximate), ("use_dag", true)] .into_iter() .collect(); diff --git a/qiskit/synthesis/__init__.py b/qiskit/synthesis/__init__.py index a86ec6681400..4b29c434b72c 100644 --- a/qiskit/synthesis/__init__.py +++ b/qiskit/synthesis/__init__.py @@ -119,6 +119,7 @@ TwoQubitBasisDecomposer XXDecomposer TwoQubitWeylDecomposition + TwoQubitControlledUDecomposer .. autofunction:: two_qubit_cnot_decompose @@ -200,6 +201,7 @@ TwoQubitBasisDecomposer, two_qubit_cnot_decompose, TwoQubitWeylDecomposition, + TwoQubitControlledUDecomposer, ) from .multi_controlled import ( synth_mcmt_vchain, diff --git a/qiskit/synthesis/two_qubit/two_qubit_decompose.py b/qiskit/synthesis/two_qubit/two_qubit_decompose.py index 79a444e6220c..4af8c3a7eef6 100644 --- a/qiskit/synthesis/two_qubit/two_qubit_decompose.py +++ b/qiskit/synthesis/two_qubit/two_qubit_decompose.py @@ -270,32 +270,46 @@ class TwoQubitControlledUDecomposer: :math:`U \sim U_d(\alpha, 0, 0) \sim \text{Ctrl-U}` gate that is locally equivalent to an :class:`.RXXGate`.""" - def __init__(self, rxx_equivalent_gate: Type[Gate]): + def __init__(self, rxx_equivalent_gate: Type[Gate], euler_basis: str = "ZXZ"): r"""Initialize the KAK decomposition. Args: rxx_equivalent_gate: Gate that is locally equivalent to an :class:`.RXXGate`: - :math:`U \sim U_d(\alpha, 0, 0) \sim \text{Ctrl-U}` gate. + :math:`U \sim U_d(\alpha, 0, 0) \sim \text{Ctrl-U}` gate. + Valid options are [:class:`.RZZGate`, :class:`.RXXGate`, :class:`.RYYGate`, + :class:`.RZXGate`, :class:`.CPhaseGate`, :class:`.CRXGate`, :class:`.CRYGate`, + :class:`.CRZGate`]. + euler_basis: Basis string to be provided to :class:`.OneQubitEulerDecomposer` + for 1Q synthesis. + Valid options are [``'ZXZ'``, ``'ZYZ'``, ``'XYX'``, ``'XZX'``, ``'U'``, ``'U3'``, + ``'U321'``, ``'U1X'``, ``'PSX'``, ``'ZSX'``, ``'ZSXX'``, ``'RR'``]. + Raises: QiskitError: If the gate is not locally equivalent to an :class:`.RXXGate`. """ if rxx_equivalent_gate._standard_gate is not None: self._inner_decomposition = two_qubit_decompose.TwoQubitControlledUDecomposer( - rxx_equivalent_gate._standard_gate + rxx_equivalent_gate._standard_gate, euler_basis ) else: self._inner_decomposition = two_qubit_decompose.TwoQubitControlledUDecomposer( - rxx_equivalent_gate + rxx_equivalent_gate, euler_basis ) self.rxx_equivalent_gate = rxx_equivalent_gate self.scale = self._inner_decomposition.scale + self.euler_basis = euler_basis - def __call__(self, unitary: Operator | np.ndarray, *, atol=DEFAULT_ATOL) -> QuantumCircuit: + def __call__( + self, unitary: Operator | np.ndarray, approximate=False, use_dag=False, *, atol=DEFAULT_ATOL + ) -> QuantumCircuit: """Returns the Weyl decomposition in circuit form. + Args: unitary (Operator or ndarray): :math:`4 \times 4` unitary to synthesize. + Returns: QuantumCircuit: Synthesized quantum circuit. + Note: atol is passed to OneQubitEulerDecomposer. """ circ_data = self._inner_decomposition(np.asarray(unitary, dtype=complex), atol) diff --git a/qiskit/transpiler/passes/synthesis/unitary_synthesis.py b/qiskit/transpiler/passes/synthesis/unitary_synthesis.py index ab66b38bd993..3dfd6caff65c 100644 --- a/qiskit/transpiler/passes/synthesis/unitary_synthesis.py +++ b/qiskit/transpiler/passes/synthesis/unitary_synthesis.py @@ -38,6 +38,7 @@ RXXGate, RZXGate, RZZGate, + RYYGate, ECRGate, RXGate, SXGate, @@ -50,6 +51,10 @@ U3Gate, RYGate, RGate, + CRXGate, + CRYGate, + CRZGate, + CPhaseGate, ) from qiskit.converters import circuit_to_dag, dag_to_circuit from qiskit.dagcircuit.dagcircuit import DAGCircuit @@ -61,6 +66,7 @@ from qiskit.synthesis.two_qubit.two_qubit_decompose import ( TwoQubitBasisDecomposer, TwoQubitWeylDecomposition, + TwoQubitControlledUDecomposer, ) from qiskit.transpiler.basepasses import TransformationPass from qiskit.transpiler.coupling import CouplingMap @@ -87,16 +93,32 @@ "u3": U3Gate._standard_gate, "ry": RYGate._standard_gate, "r": RGate._standard_gate, + "rzz": RZZGate._standard_gate, + "ryy": RYYGate._standard_gate, + "rxx": RXXGate._standard_gate, + "rzx": RXXGate._standard_gate, + "cp": CPhaseGate._standard_gate, + "crx": RXXGate._standard_gate, + "cry": RXXGate._standard_gate, + "crz": RXXGate._standard_gate, } +KAK_GATE_PARAM_NAMES = { + "rxx": RXXGate, + "rzz": RZZGate, + "ryy": RYYGate, + "rzx": RZXGate, + "cphase": CPhaseGate, + "crx": CRXGate, + "cry": CRYGate, + "crz": CRZGate, +} KAK_GATE_NAMES = { "cx": CXGate(), "cz": CZGate(), "iswap": iSwapGate(), - "rxx": RXXGate(pi / 2), "ecr": ECRGate(), - "rzx": RZXGate(pi / 4), # typically pi/6 is also available } GateNameToGate = get_standard_gate_name_mapping() @@ -105,9 +127,14 @@ def _choose_kak_gate(basis_gates): """Choose the first available 2q gate to use in the KAK decomposition.""" kak_gate = None - kak_gates = set(basis_gates or []).intersection(KAK_GATE_NAMES.keys()) - if kak_gates: - kak_gate = KAK_GATE_NAMES[kak_gates.pop()] + kak_gates = sorted(set(basis_gates or []).intersection(KAK_GATE_NAMES.keys())) + kak_gates_params = sorted(set(basis_gates or []).intersection(KAK_GATE_PARAM_NAMES.keys())) + + if kak_gates_params: + kak_gate = KAK_GATE_PARAM_NAMES[kak_gates_params[0]] + + elif kak_gates: + kak_gate = KAK_GATE_NAMES[kak_gates[0]] return kak_gate @@ -150,14 +177,9 @@ def _decomposer_2q_from_basis_gates(basis_gates, pulse_optimize=None, approximat kak_gate = _choose_kak_gate(basis_gates) euler_basis = _choose_euler_basis(basis_gates) basis_fidelity = approximation_degree or 1.0 - if isinstance(kak_gate, RZXGate): - backup_optimizer = TwoQubitBasisDecomposer( - CXGate(), - basis_fidelity=basis_fidelity, - euler_basis=euler_basis, - pulse_optimize=pulse_optimize, - ) - decomposer2q = XXDecomposer(euler_basis=euler_basis, backup_optimizer=backup_optimizer) + + if kak_gate in KAK_GATE_PARAM_NAMES.values(): + decomposer2q = TwoQubitControlledUDecomposer(kak_gate, euler_basis) elif kak_gate is not None: decomposer2q = TwoQubitBasisDecomposer( kak_gate, diff --git a/releasenotes/notes/add-2q-fractional-gates-to-unitarysynthesis-pass-f66eee29903f5639.yaml b/releasenotes/notes/add-2q-fractional-gates-to-unitarysynthesis-pass-f66eee29903f5639.yaml new file mode 100644 index 000000000000..a55fd7f3ea52 --- /dev/null +++ b/releasenotes/notes/add-2q-fractional-gates-to-unitarysynthesis-pass-f66eee29903f5639.yaml @@ -0,0 +1,37 @@ +--- +features_synthesis: + - | + Add a :class:`.TwoQubitControlledUDecomposer` that decomposes any two-qubit unitary + in terms of basis two-qubit fractional gates, such as :class:`.RZZGate` + (or two-gates gates which are locally equivalent to :class:`.RZZGate` up to single qubit gates). + + For example:: + + from qiskit.circuit.library import RZZGate + from qiskit.synthesis import TwoQubitControlledUDecomposer + from qiskit.quantum_info import random_unitary + + unitary = random_unitary(4, seed=1) + decomposer = TwoQubitControlledUDecomposer(RZZGate, euler_basis="ZXZ") + circ = decomposer(unitary) + circ.draw(output='mpl') + +features_transpiler: + - | + Added support for two-qubit fractional basis gates, such as :class:`.RZZGate`, to the + :class:`.UnitarySynthesis` transpiler pass. The decomposition is done using the + :class:`.TwoQubitControlledUDecomposer`, and supports both standard and custom basis gates. + + For example:: + + from qiskit import QuantumCircuit + from qiskit.quantum_info import random_unitary + from qiskit.transpiler.passes import UnitarySynthesis + from qiskit.converters import circuit_to_dag, dag_to_circuit + + unitary = random_unitary(4, seed=1) + qc = QuantumCircuit(2) + qc.append(unitary, [0, 1]) + dag = circuit_to_dag(qc) + circ = UnitarySynthesis(basis_gates=['rzz', 'rx', 'rz']).run(dag) + dag_to_circuit(circ).draw(output='mpl') diff --git a/test/python/compiler/test_transpiler.py b/test/python/compiler/test_transpiler.py index 4bf2b437d05c..5c707c00d884 100644 --- a/test/python/compiler/test_transpiler.py +++ b/test/python/compiler/test_transpiler.py @@ -1276,7 +1276,7 @@ def test_block_collection_reduces_1q_gate(self, basis_gates, gate_counts): basis_gates=[ ["u3", "cx"], ["rx", "rz", "iswap"], - ["rx", "ry", "rxx"], + ["ry", "rz", "rxx"], ], ) def test_translation_method_synthesis(self, optimization_level, basis_gates): diff --git a/test/python/synthesis/test_synthesis.py b/test/python/synthesis/test_synthesis.py index b26a049b5567..98e20d21c009 100644 --- a/test/python/synthesis/test_synthesis.py +++ b/test/python/synthesis/test_synthesis.py @@ -1426,14 +1426,32 @@ def test_approx_supercontrolled_decompose_phase_3_use_random(self, seed, delta=0 class TestTwoQubitControlledUDecompose(CheckDecompositions): """Test TwoQubitControlledUDecomposer() for exact decompositions and raised exceptions""" - @combine(seed=range(10), name="seed_{seed}") - def test_correct_unitary(self, seed): + @combine( + seed=range(5), + gate=[RXXGate, RYYGate, RZZGate, RZXGate, CPhaseGate, CRZGate, CRXGate, CRYGate], + euler_basis=[ + "ZYZ", + "ZXZ", + "XYX", + "XZX", + "RR", + "U", + "U3", + "U321", + "PSX", + "ZSX", + "ZSXX", + "U1X", + ], + name="seed_{seed}", + ) + def test_correct_unitary(self, seed, gate, euler_basis): """Verify unitary for different gates in the decomposition""" unitary = random_unitary(4, seed=seed) - for gate in [RXXGate, RYYGate, RZZGate, RZXGate, CPhaseGate, CRZGate, CRXGate, CRYGate]: - decomposer = TwoQubitControlledUDecomposer(gate) - circ = decomposer(unitary) - self.assertEqual(Operator(unitary), Operator(circ)) + + decomposer = TwoQubitControlledUDecomposer(gate, euler_basis) + circ = decomposer(unitary) + self.assertEqual(Operator(unitary), Operator(circ)) def test_not_rxx_equivalent(self): """Test that an exception is raised if the gate is not equivalent to an RXXGate""" diff --git a/test/python/transpiler/test_unitary_synthesis.py b/test/python/transpiler/test_unitary_synthesis.py index bc9e218e79cc..d93d6800985c 100644 --- a/test/python/transpiler/test_unitary_synthesis.py +++ b/test/python/transpiler/test_unitary_synthesis.py @@ -17,6 +17,7 @@ """ import unittest +import math import numpy as np import scipy from ddt import ddt, data @@ -25,6 +26,7 @@ from qiskit.providers.fake_provider import GenericBackendV2 from qiskit.circuit import QuantumCircuit, QuantumRegister, ClassicalRegister from qiskit.circuit.library import quantum_volume +from qiskit.circuit.parameterexpression import ParameterValueType from qiskit.converters import circuit_to_dag, dag_to_circuit from qiskit.transpiler.passes import UnitarySynthesis from qiskit.quantum_info.operators import Operator @@ -58,6 +60,7 @@ RZZGate, RXXGate, PauliEvolutionGate, + CPhaseGate, ) from qiskit.quantum_info import SparsePauliOp from qiskit.circuit import Measure @@ -130,7 +133,8 @@ def test_empty_basis_gates(self): @data( ["u3", "cx"], ["u1", "u2", "u3", "cx"], - ["rx", "ry", "rxx"], + ["ry", "rz", "rxx"], + ["rx", "rz", "rzz"], ["rx", "rz", "iswap"], ["u3", "rx", "rz", "cz", "iswap"], ) @@ -740,19 +744,110 @@ def test_iswap_no_cx_synthesis_succeeds(self): result_qc = dag_to_circuit(result_dag) self.assertTrue(np.allclose(Operator(result_qc.to_gate()).to_matrix(), cxmat)) - def test_parameterized_basis_gate_in_target(self): - """Test synthesis with parameterized RXX gate.""" + @combine(is_random=[True, False], param_gate=[RXXGate, RZZGate, CPhaseGate]) + def test_parameterized_basis_gate_in_target(self, is_random, param_gate): + """Test synthesis with parameterized RZZ/RXX gate.""" theta = Parameter("θ") lam = Parameter("λ") + phi = Parameter("ϕ") target = Target(num_qubits=2) target.add_instruction(RZGate(lam)) - target.add_instruction(RXGate(theta)) - target.add_instruction(RXXGate(theta)) + target.add_instruction(RXGate(phi)) + target.add_instruction(param_gate(theta)) qc = QuantumCircuit(2) + if is_random: + qc.unitary(random_unitary(4, seed=1234), [0, 1]) qc.cp(np.pi / 2, 0, 1) qc_transpiled = transpile(qc, target=target, optimization_level=3, seed_transpiler=42) opcount = qc_transpiled.count_ops() - self.assertTrue(set(opcount).issubset({"rz", "rx", "rxx"})) + self.assertTrue(set(opcount).issubset({"rz", "rx", param_gate(theta).name})) + self.assertTrue(np.allclose(Operator(qc_transpiled), Operator(qc))) + + def test_custom_parameterized_gate_in_target(self): + """Test synthesis with custom parameterized gate in target.""" + + class CustomXXGate(RXXGate): + """Custom RXXGate subclass that's not a standard gate""" + + _standard_gate = None + + def __init__(self, theta, label=None): + super().__init__(theta, label) + self.name = "MyCustomXXGate" + + theta = Parameter("θ") + lam = Parameter("λ") + phi = Parameter("ϕ") + + target = Target(num_qubits=2) + target.add_instruction(RZGate(lam)) + target.add_instruction(RXGate(phi)) + target.add_instruction(CustomXXGate(theta)) + + qc = QuantumCircuit(2) + qc.unitary(random_unitary(4, seed=1234), [0, 1]) + qc_transpiled = UnitarySynthesis(target=target)(qc) + opcount = qc_transpiled.count_ops() + self.assertTrue(set(opcount).issubset({"rz", "rx", "MyCustomXXGate"})) + + self.assertTrue(np.allclose(Operator(qc_transpiled), Operator(qc))) + + def test_custom_parameterized_gate_in_target_skips(self): + """Test that synthesis is skipped with custom parameterized + gate in target that is not RXX equivalent.""" + + class CustomXYGate(Gate): + """Custom Gate subclass that's not a standard gate and not RXX equivalent""" + + _standard_gate = None + + def __init__(self, theta: ParameterValueType, label=None): + """Create new custom rotstion XY gate.""" + super().__init__("MyCustomXYGate", 2, [theta]) + + def __array__(self, dtype=None): + """Return a Numpy.array for the custom gate.""" + theta = self.params[0] + cos = math.cos(theta) + isin = 1j * math.sin(theta) + return np.array( + [[1, 0, 0, 0], [0, cos, -isin, 0], [0, -isin, cos, 0], [0, 0, 0, 1]], + dtype=dtype, + ) + + def inverse(self, annotated: bool = False): + return CustomXYGate(-self.params[0]) + + theta = Parameter("θ") + lam = Parameter("λ") + phi = Parameter("ϕ") + + target = Target(num_qubits=2) + target.add_instruction(RZGate(lam)) + target.add_instruction(RXGate(phi)) + target.add_instruction(CustomXYGate(theta)) + + qc = QuantumCircuit(2) + qc.unitary(random_unitary(4, seed=1234), [0, 1]) + qc_transpiled = UnitarySynthesis(target=target)(qc) + opcount = qc_transpiled.count_ops() + self.assertTrue(set(opcount).issubset({"unitary"})) + self.assertTrue(np.allclose(Operator(qc_transpiled), Operator(qc))) + + @data( + ["rx", "ry", "rxx"], + ["rx", "rz", "rzz"], + ) + def test_parameterized_backend(self, basis_gates): + """Test synthesis with parameterized backend.""" + backend = GenericBackendV2(3, basis_gates=basis_gates, seed=0) + qc = QuantumCircuit(3) + qc.unitary(random_unitary(4, seed=1234), [0, 1]) + qc.unitary(random_unitary(4, seed=4321), [0, 2]) + qc.cp(np.pi / 2, 0, 1) + qc_transpiled = transpile(qc, backend, optimization_level=3, seed_transpiler=42) + opcount = qc_transpiled.count_ops() + self.assertTrue(set(opcount).issubset(basis_gates)) self.assertTrue(np.allclose(Operator(qc_transpiled), Operator(qc))) @data(1, 2, 3) From 94504f21b0d82441e789a42714cb520d6e27fb48 Mon Sep 17 00:00:00 2001 From: Shelly Garion <46566946+ShellyGarion@users.noreply.github.com> Date: Mon, 10 Feb 2025 14:06:24 +0200 Subject: [PATCH 22/48] Better decomposition for multi-controlled 1-qubit gates (#13801) * improve add_control for the gates H, SX, SXdg, U * update use_basis_gates=False * add SXdgGate to the test * add release notes * update release notes --- qiskit/circuit/add_control.py | 39 +++++++++++++------ ...-one-qubit-unitaries-3ae333a106274b79.yaml | 10 +++++ test/python/circuit/test_controlled_gate.py | 2 + 3 files changed, 39 insertions(+), 12 deletions(-) create mode 100644 releasenotes/notes/improve-decomposition-controlled-one-qubit-unitaries-3ae333a106274b79.yaml diff --git a/qiskit/circuit/add_control.py b/qiskit/circuit/add_control.py index 3e463c30ea9e..8111fed9f700 100644 --- a/qiskit/circuit/add_control.py +++ b/qiskit/circuit/add_control.py @@ -109,7 +109,7 @@ def control( global_phase = 0 - basis = ["p", "u", "x", "z", "rx", "ry", "rz", "cx"] + basis = ["p", "u", "x", "z", "y", "h", "sx", "sxdg", "rx", "ry", "rz", "cx"] if operation.name in basis: apply_basic_controlled_gate(controlled_circ, operation, q_control, q_target[0]) @@ -187,7 +187,7 @@ def apply_basic_controlled_gate(circuit, gate, controls, target): This implements multi-control operations for the following basis gates: - ["p", "u", "x", "z", "rx", "ry", "rz", "cx"] + ["p", "u", "x", "z", "y", "h", "sx", "sxdg", "rx", "ry", "rz", "cx"] """ num_ctrl_qubits = len(controls) @@ -239,31 +239,46 @@ def apply_basic_controlled_gate(circuit, gate, controls, target): circuit.cu(theta, phi, lamb, 0, controls[0], target) else: if phi == -pi / 2 and lamb == pi / 2: - circuit.mcrx(theta, controls, target, use_basis_gates=True) + circuit.mcrx(theta, controls, target, use_basis_gates=False) elif phi == 0 and lamb == 0: circuit.mcry( theta, controls, target, - use_basis_gates=True, + use_basis_gates=False, ) elif theta == 0 and phi == 0: circuit.mcp(lamb, controls, target) else: - circuit.mcp(lamb, controls, target) - circuit.mcry( - theta, - controls, - target, - use_basis_gates=True, - ) - circuit.mcp(phi, controls, target) + circuit.mcrz(lamb, controls, target, use_basis_gates=False) + circuit.mcry(theta, controls, target, use_basis_gates=False) + circuit.mcrz(phi, controls, target, use_basis_gates=False) + circuit.mcp((phi + lamb) / 2, controls[1:], controls[0]) elif gate.name == "z": circuit.h(target) circuit.mcx(controls, target) circuit.h(target) + elif gate.name == "y": + circuit.sdg(target) + circuit.mcx(controls, target) + circuit.s(target) + + elif gate.name == "h": + circuit.mcry(pi / 2, controls, target, use_basis_gates=False) + circuit.mcx(controls, target) + + elif gate.name == "sx": + circuit.h(target) + circuit.mcp(pi / 2, controls, target) + circuit.h(target) + + elif gate.name == "sxdg": + circuit.h(target) + circuit.mcp(3 * pi / 2, controls, target) + circuit.h(target) + else: raise CircuitError(f"Gate {gate} not in supported basis.") diff --git a/releasenotes/notes/improve-decomposition-controlled-one-qubit-unitaries-3ae333a106274b79.yaml b/releasenotes/notes/improve-decomposition-controlled-one-qubit-unitaries-3ae333a106274b79.yaml new file mode 100644 index 000000000000..e7217229f3be --- /dev/null +++ b/releasenotes/notes/improve-decomposition-controlled-one-qubit-unitaries-3ae333a106274b79.yaml @@ -0,0 +1,10 @@ +--- +features_circuits: + - | + Reduce the number of two-qubit gates when decomposing some multi-controlled + single-qubit unitary gates. For example, + + * For multi-controlled :class:`.YGate` on 10 qubits, we reduce the :class:`.CXGate` count by 56%, + * For multi-controlled :class:`.HGate` on 10 qubits, we reduce the :class:`.CXGate` count by 44%, + * For multi-controlled :class:`.SXGate` and :class:`.SXdgGate` on 10 qubits, we reduce the :class:`.CXGate` count by 80%, + * For multi-controlled :class:`.UGate` on 10 qubits, we reduce the :class:`.CXGate` count by 31%. diff --git a/test/python/circuit/test_controlled_gate.py b/test/python/circuit/test_controlled_gate.py index 8ad5ca7d473a..4b3e69d11e99 100644 --- a/test/python/circuit/test_controlled_gate.py +++ b/test/python/circuit/test_controlled_gate.py @@ -64,6 +64,7 @@ CU3Gate, CUGate, SXGate, + SXdgGate, CSXGate, MSGate, Barrier, @@ -1730,6 +1731,7 @@ class TestControlledGateLabel(QiskitTestCase): (CU1Gate, [0.1]), (SwapGate, []), (SXGate, []), + (SXdgGate, []), (CSXGate, []), (CCXGate, []), (RZGate, [0.1]), From 1b6fccfbbeb3b45e122dbcdc39a84f987f217e31 Mon Sep 17 00:00:00 2001 From: Raynel Sanchez <87539502+raynelfss@users.noreply.github.com> Date: Mon, 10 Feb 2025 09:19:47 -0500 Subject: [PATCH 23/48] Add: Implement `From` for `Param` (#13817) - Add a convenient way of converting an `f64` into a `Param` by implementing the `From` trait. This would enable us to simply call `f64::into()`. --- crates/circuit/src/operations.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/crates/circuit/src/operations.rs b/crates/circuit/src/operations.rs index e0add717824a..0b6de35ae017 100644 --- a/crates/circuit/src/operations.rs +++ b/crates/circuit/src/operations.rs @@ -130,6 +130,13 @@ impl AsRef for Param { } } +// Conveniently converts an f64 into a `Param`. +impl From for Param { + fn from(value: f64) -> Self { + Param::Float(value) + } +} + /// Struct to provide iteration over Python-space `Parameter` instances within a `Param`. pub struct ParamParameterIter<'py>(Option>); impl<'py> Iterator for ParamParameterIter<'py> { From 7995cabbdf47021b5d1b6d4c8f1daec206a4f6af Mon Sep 17 00:00:00 2001 From: Alexander Ivrii Date: Tue, 11 Feb 2025 10:12:15 +0200 Subject: [PATCH 24/48] Correctly updating global phase when removing gates that are identity up to a global phase (#13785) * correctly updating global phase when removing -I gates from the circuit * similar fix for specialized rotation gates * Correctly updating global phase when removing gates that are equivalent to identity up to a global phase * also handling UnitaryGates after the unitary gates PR was merged * applying review suggestions * removing code duplication --- .../accelerate/src/remove_identity_equiv.rs | 44 +++++++++++-------- .../optimization/remove_identity_equiv.py | 5 +-- ...remove-id-equivalent-6480da0c62f20df1.yaml | 8 ++++ .../test_remove_identity_equivalent.py | 37 +++++++++++++--- 4 files changed, 65 insertions(+), 29 deletions(-) create mode 100644 releasenotes/notes/fix-phase-in-remove-id-equivalent-6480da0c62f20df1.yaml diff --git a/crates/accelerate/src/remove_identity_equiv.rs b/crates/accelerate/src/remove_identity_equiv.rs index d94250f3c399..7a65a9972019 100644 --- a/crates/accelerate/src/remove_identity_equiv.rs +++ b/crates/accelerate/src/remove_identity_equiv.rs @@ -9,7 +9,6 @@ // 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. - use num_complex::Complex64; use num_complex::ComplexFloat; use pyo3::prelude::*; @@ -27,11 +26,13 @@ use qiskit_circuit::packed_instruction::PackedInstruction; #[pyfunction] #[pyo3(signature=(dag, approx_degree=Some(1.0), target=None))] fn remove_identity_equiv( + py: Python, dag: &mut DAGCircuit, approx_degree: Option, target: Option<&Target>, ) { let mut remove_list: Vec = Vec::new(); + let mut global_phase_update: f64 = 0.; let get_error_cutoff = |inst: &PackedInstruction| -> f64 { match approx_degree { @@ -75,12 +76,17 @@ fn remove_identity_equiv( }; for (op_node, inst) in dag.op_nodes(false) { - match inst.op.view() { + if inst.is_parameterized() { + // Skip parameterized gates + continue; + } + let view = inst.op.view(); + match view { OperationRef::StandardGate(gate) => { let (dim, trace) = match gate { StandardGate::RXGate | StandardGate::RYGate | StandardGate::RZGate => { if let Param::Float(theta) = inst.params_view()[0] { - let trace = (theta / 2.).cos() * 2.; + let trace = Complex64::new((theta / 2.).cos() * 2., 0.); (2., trace) } else { continue; @@ -91,20 +97,16 @@ fn remove_identity_equiv( | StandardGate::RZZGate | StandardGate::RZXGate => { if let Param::Float(theta) = inst.params_view()[0] { - let trace = (theta / 2.).cos() * 4.; + let trace = Complex64::new((theta / 2.).cos() * 4., 0.); (4., trace) } else { continue; } } _ => { - // Skip global phase gate - if gate.num_qubits() < 1 { - continue; - } if let Some(matrix) = gate.matrix(inst.params_view()) { let dim = matrix.shape()[0] as f64; - let trace = matrix.diag().iter().sum::().abs(); + let trace = matrix.diag().iter().sum::(); (dim, trace) } else { continue; @@ -112,34 +114,38 @@ fn remove_identity_equiv( } }; let error = get_error_cutoff(inst); - let f_pro = (trace / dim).powi(2); + let f_pro = (trace / dim).abs().powi(2); let gate_fidelity = (dim * f_pro + 1.) / (dim + 1.); if (1. - gate_fidelity).abs() < error { - remove_list.push(op_node) + remove_list.push(op_node); + global_phase_update += (trace / dim).arg(); } } - OperationRef::Gate(gate) => { - // Skip global phase like gate - if gate.num_qubits() < 1 { - continue; - } - if let Some(matrix) = gate.matrix(inst.params_view()) { + _ => { + let matrix = view.matrix(inst.params_view()); + // If view.matrix() returns None, then there is no matrix and we skip the operation. + if let Some(matrix) = matrix { let error = get_error_cutoff(inst); let dim = matrix.shape()[0] as f64; let trace: Complex64 = matrix.diag().iter().sum(); let f_pro = (trace / dim).abs().powi(2); let gate_fidelity = (dim * f_pro + 1.) / (dim + 1.); if (1. - gate_fidelity).abs() < error { - remove_list.push(op_node) + remove_list.push(op_node); + global_phase_update += (trace / dim).arg(); } } } - _ => continue, } } for node in remove_list { dag.remove_op_node(node); } + + if global_phase_update != 0. { + dag.add_global_phase(py, &Param::Float(global_phase_update)) + .expect("The global phase is guaranteed to be a float"); + } } pub fn remove_identity_equiv_mod(m: &Bound) -> PyResult<()> { diff --git a/qiskit/transpiler/passes/optimization/remove_identity_equiv.py b/qiskit/transpiler/passes/optimization/remove_identity_equiv.py index fbf132d958a2..17445eb5ad2a 100644 --- a/qiskit/transpiler/passes/optimization/remove_identity_equiv.py +++ b/qiskit/transpiler/passes/optimization/remove_identity_equiv.py @@ -23,9 +23,8 @@ class RemoveIdentityEquivalent(TransformationPass): r"""Remove gates with negligible effects. - Removes gates whose effect is close to an identity operation, up to the specified - tolerance. Zero qubit gates such as :class:`.GlobalPhaseGate` are not considered - by this pass. + Removes gates whose effect is close to an identity operation up to a global phase + and up to the specified tolerance. Parameterized gates are not considered by this pass. For a cutoff fidelity :math:`f`, this pass removes gates whose average gate fidelity with respect to the identity is below :math:`f`. Concretely, diff --git a/releasenotes/notes/fix-phase-in-remove-id-equivalent-6480da0c62f20df1.yaml b/releasenotes/notes/fix-phase-in-remove-id-equivalent-6480da0c62f20df1.yaml new file mode 100644 index 000000000000..3ee4c4accc21 --- /dev/null +++ b/releasenotes/notes/fix-phase-in-remove-id-equivalent-6480da0c62f20df1.yaml @@ -0,0 +1,8 @@ +--- +fixes: + - | + Fixed a bug in the :class:`.RemoveIdentityEquivalent` transpiler pass, where gates close + to identity up to a global phase were removed from the circuit, + but the global phase of the circuit was not updated. In particular, + :class:`.RemoveIdentityEquivalent` now removes non-parameterized :class:`.GlobalPhaseGate` + gates. diff --git a/test/python/transpiler/test_remove_identity_equivalent.py b/test/python/transpiler/test_remove_identity_equivalent.py index 1db392d3654b..736c02b765bd 100644 --- a/test/python/transpiler/test_remove_identity_equivalent.py +++ b/test/python/transpiler/test_remove_identity_equivalent.py @@ -12,6 +12,7 @@ """Tests for the DropNegligible transpiler pass.""" +import ddt import numpy as np from qiskit.circuit import Parameter, QuantumCircuit, QuantumRegister, Gate @@ -26,6 +27,7 @@ XXMinusYYGate, XXPlusYYGate, GlobalPhaseGate, + UnitaryGate, ) from qiskit.quantum_info import Operator from qiskit.transpiler.passes import RemoveIdentityEquivalent @@ -34,6 +36,7 @@ from test import QiskitTestCase # pylint: disable=wrong-import-order +@ddt.ddt class TestDropNegligible(QiskitTestCase): """Test the DropNegligible pass.""" @@ -173,13 +176,33 @@ def to_matrix(self): expected = QuantumCircuit(3) self.assertEqual(expected, transpiled) - def test_global_phase_ignored(self): - """Test that global phase gate isn't considered.""" + @ddt.data( + RXGate(0), + RXGate(2 * np.pi), + RYGate(0), + RYGate(2 * np.pi), + RZGate(0), + RZGate(2 * np.pi), + UnitaryGate(np.array([[1, 0], [0, 1]])), + UnitaryGate(np.array([[-1, 0], [0, -1]])), + UnitaryGate(np.array([[np.exp(1j * np.pi / 4), 0], [0, np.exp(1j * np.pi / 4)]])), + GlobalPhaseGate(0), + GlobalPhaseGate(np.pi / 4), + ) + def test_remove_identity_up_to_global_phase(self, gate): + """Test that gates equivalent to identity up to a global phase are removed from the circuit, + and the global phase of the circuit is updated correctly. + """ + qc = QuantumCircuit(gate.num_qubits) + qc.append(gate, qc.qubits) + transpiled = RemoveIdentityEquivalent()(qc) + self.assertEqual(transpiled.size(), 0) + self.assertEqual(Operator(qc), Operator(transpiled)) + def test_parameterized_global_phase_ignored(self): + """Test that parameterized global phase gates are not removed by the pass.""" + theta = Parameter("theta") qc = QuantumCircuit(1) - qc.id(0) - qc.append(GlobalPhaseGate(0)) + qc.append(GlobalPhaseGate(theta), []) transpiled = RemoveIdentityEquivalent()(qc) - expected = QuantumCircuit(1) - expected.append(GlobalPhaseGate(0)) - self.assertEqual(transpiled, expected) + self.assertEqual(qc, transpiled) From 288d2f41b295e93114290a8c0c7e69cb18283aad Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Tue, 11 Feb 2025 16:49:18 +0000 Subject: [PATCH 25/48] Do not contract control-flow operations during `SabreSwap` (#13790) The previous implementation of control-flow handling for `SabreSwap` caused control-flow blocks to contract away from idle qubit wires as a side effect of its algorithm. This will no longer be always valid with the addition of `Box`, and besides, that optimisation isn't part of a routing algorithm's responsibilities, so it's cleaner to have it in a separate pass (now `ContractIdleWiresInControlFlow`) where it can be applied at times other than routing, like in the optimisation loop. --- .../transpiler/passes/routing/sabre_swap.py | 12 ++- .../sabre-contraction-cbb7bffaeb826d67.yaml | 6 ++ test/python/transpiler/test_sabre_swap.py | 100 ++++++++++++------ 3 files changed, 85 insertions(+), 33 deletions(-) create mode 100644 releasenotes/notes/sabre-contraction-cbb7bffaeb826d67.yaml diff --git a/qiskit/transpiler/passes/routing/sabre_swap.py b/qiskit/transpiler/passes/routing/sabre_swap.py index 238444067168..ce653f97880c 100644 --- a/qiskit/transpiler/passes/routing/sabre_swap.py +++ b/qiskit/transpiler/passes/routing/sabre_swap.py @@ -416,6 +416,12 @@ def recurse(dest_dag, source_dag, result, root_logical_map, layout): block_root_logical_map = { inner: root_logical_map[outer] for inner, outer in zip(block.qubits, node.qargs) } + # The virtual qubits originally incident to the block should be retained even if not + # actually used; the user might be marking them out specially (like in `box`). + # There are other transpiler passes to remove those dependencies if desired. + incident_qubits = { + layout.virtual_to_physical(block_root_logical_map[bit]) for bit in block.qubits + } block_dag, block_layout = recurse( empty_dag(block), circuit_to_dag_dict[id(block)], @@ -429,7 +435,11 @@ def recurse(dest_dag, source_dag, result, root_logical_map, layout): ) apply_swaps(block_dag, block_result.swap_epilogue, block_layout) mapped_block_dags.append(block_dag) - idle_qubits.intersection_update(block_dag.idle_wires()) + idle_qubits.intersection_update( + bit + for bit in block_dag.idle_wires() + if block_dag.find_bit(bit).index not in incident_qubits + ) mapped_blocks = [] for mapped_block_dag in mapped_block_dags: diff --git a/releasenotes/notes/sabre-contraction-cbb7bffaeb826d67.yaml b/releasenotes/notes/sabre-contraction-cbb7bffaeb826d67.yaml new file mode 100644 index 000000000000..fc54b83784e5 --- /dev/null +++ b/releasenotes/notes/sabre-contraction-cbb7bffaeb826d67.yaml @@ -0,0 +1,6 @@ +--- +fixes: + - | + :class:`.SabreSwap` will no longer contract idle qubit wires out of control-flow blocks during routing. + This was generally a valid optimization, but not an expected side effect of a routing pass. + You can now use the :class:`.ContractIdleWiresInControlFlow` pass to perform this contraction. diff --git a/test/python/transpiler/test_sabre_swap.py b/test/python/transpiler/test_sabre_swap.py index 7cf86356ab1f..109053ee8324 100644 --- a/test/python/transpiler/test_sabre_swap.py +++ b/test/python/transpiler/test_sabre_swap.py @@ -492,12 +492,12 @@ def test_pre_if_else_route(self): expected.swap(1, 2) expected.cx(0, 1) expected.measure(1, 2) - etrue_body = QuantumCircuit(qreg[[3, 4]], creg[[2]]) - etrue_body.x(0) - efalse_body = QuantumCircuit(qreg[[3, 4]], creg[[2]]) - efalse_body.x(1) + etrue_body = QuantumCircuit(qreg, creg[[2]]) + etrue_body.x(3) + efalse_body = QuantumCircuit(qreg, creg[[2]]) + efalse_body.x(4) new_order = [0, 2, 1, 3, 4] - expected.if_else((creg[2], 0), etrue_body, efalse_body, qreg[[3, 4]], creg[[2]]) + expected.if_else((creg[2], 0), etrue_body, efalse_body, qreg, creg[[2]]) expected.barrier(qreg) expected.measure(qreg, creg[new_order]) self.assertEqual(dag_to_circuit(cdag), expected) @@ -533,11 +533,11 @@ def test_pre_if_else_route_post_x(self): expected.cx(0, 1) expected.measure(1, 2) new_order = [0, 2, 1, 3, 4] - etrue_body = QuantumCircuit(qreg[[3, 4]], creg[[0]]) - etrue_body.x(0) - efalse_body = QuantumCircuit(qreg[[3, 4]], creg[[0]]) - efalse_body.x(1) - expected.if_else((creg[2], 0), etrue_body, efalse_body, qreg[[3, 4]], creg[[0]]) + etrue_body = QuantumCircuit(qreg, creg[[0]]) + etrue_body.x(3) + efalse_body = QuantumCircuit(qreg, creg[[0]]) + efalse_body.x(4) + expected.if_else((creg[2], 0), etrue_body, efalse_body, qreg, creg[[0]]) expected.x(2) expected.barrier(qreg) expected.measure(qreg, creg[new_order]) @@ -552,12 +552,12 @@ def test_post_if_else_route(self): qc = QuantumCircuit(qreg, creg) qc.h(0) qc.measure(0, 0) - true_body = QuantumCircuit(qreg, creg[[0]]) - true_body.x(3) - false_body = QuantumCircuit(qreg, creg[[0]]) - false_body.x(4) + true_body = QuantumCircuit(qreg[[3, 4]], creg[[0]]) + true_body.x(0) + false_body = QuantumCircuit(qreg[[3, 4]], creg[[0]]) + false_body.x(1) qc.barrier(qreg) - qc.if_else((creg[0], 0), true_body, false_body, qreg, creg[[0]]) + qc.if_else((creg[0], 0), true_body, false_body, qreg[[3, 4]], creg[[0]]) qc.barrier(qreg) qc.cx(0, 2) qc.barrier(qreg) @@ -596,10 +596,10 @@ def test_pre_if_else2(self): qc.cx(0, 2) qc.x(1) qc.measure(0, 0) - true_body = QuantumCircuit(qreg, creg[[0]]) + true_body = QuantumCircuit(qreg[[0]], creg[[0]]) true_body.x(0) - false_body = QuantumCircuit(qreg, creg[[0]]) - qc.if_else((creg[0], 0), true_body, false_body, qreg, creg[[0]]) + false_body = QuantumCircuit(qreg[[0]], creg[[0]]) + qc.if_else((creg[0], 0), true_body, false_body, qreg[[0]], creg[[0]]) qc.barrier(qreg) qc.measure(qreg, creg) @@ -748,16 +748,16 @@ def test_pre_intra_post_if_else(self): expected.swap(0, 1) expected.cx(1, 2) expected.measure(1, 0) - etrue_body = QuantumCircuit(qreg[[1, 2, 3, 4]], creg[[0]]) - etrue_body.cx(0, 1) - efalse_body = QuantumCircuit(qreg[[1, 2, 3, 4]], creg[[0]]) - efalse_body.swap(0, 1) - efalse_body.swap(2, 3) - efalse_body.cx(1, 2) - efalse_body.swap(0, 1) - efalse_body.swap(2, 3) + etrue_body = QuantumCircuit(qreg, creg[[0]]) + etrue_body.cx(1, 2) + efalse_body = QuantumCircuit(qreg, creg[[0]]) + efalse_body.swap(1, 2) + efalse_body.swap(3, 4) + efalse_body.cx(2, 3) + efalse_body.swap(1, 2) + efalse_body.swap(3, 4) - expected.if_else((creg[0], 0), etrue_body, efalse_body, qreg[[1, 2, 3, 4]], creg[[0]]) + expected.if_else((creg[0], 0), etrue_body, efalse_body, qreg, creg[[0]]) expected.swap(1, 2) expected.h(3) expected.cx(3, 2) @@ -834,11 +834,11 @@ def test_no_layout_change(self): expected.swap(1, 2) expected.cx(0, 1) expected.measure(0, 0) - etrue_body = QuantumCircuit(qreg[[1, 4]], creg[[0]]) - etrue_body.x(0) - efalse_body = QuantumCircuit(qreg[[1, 4]], creg[[0]]) - efalse_body.x(1) - expected.if_else((creg[0], 0), etrue_body, efalse_body, qreg[[1, 4]], creg[[0]]) + etrue_body = QuantumCircuit(qreg, creg[[0]]) + etrue_body.x(1) + efalse_body = QuantumCircuit(qreg, creg[[0]]) + efalse_body.x(4) + expected.if_else((creg[0], 0), etrue_body, efalse_body, qreg, creg[[0]]) expected.barrier(qreg) expected.measure(qreg, creg[[0, 2, 1, 3, 4]]) self.assertEqual(dag_to_circuit(cdag), expected) @@ -1336,6 +1336,42 @@ def test_if_no_else_restores_layout(self): running_layout.swap(*instruction.qubits) self.assertEqual(initial_layout, running_layout) + def test_idle_qubit_contraction(self): + """Incident virtual qubits to a control-flow block should be maintained, even if idle, but + the blocks shouldn't contain further unnecessary qubits.""" + qc = QuantumCircuit(8) + with qc.if_test(expr.lift(True)): + qc.cx(0, 3) + qc.noop(4) + # Both of these qubits will have been moved around by the prior necessary layout + # changes, so this is testing the recursion works for modified layouts. + with qc.if_test(expr.lift(True)) as else_: + qc.noop(0) + with else_: + qc.noop(3) + + coupling = CouplingMap.from_line(8) + + # With the `decay` heuristic set to penalise re-use of the same qubit swap, this expected + # circuit should be the only valid output (except for symmetries in the swap operation, + # which the equality check should handle). + expected = QuantumCircuit(8) + with expected.if_test(expr.lift(True)): + expected.noop(4) + expected.swap(0, 1) + expected.swap(2, 3) + expected.cx(1, 2) + with expected.if_test(expr.lift(True)) as else_: + expected.noop(1) + with else_: + expected.noop(2) + # We have to restore the output layout. + expected.swap(0, 1) + expected.swap(2, 3) + + pass_ = SabreSwap(coupling, "decay", seed=2025_02_05, trials=1) + self.assertEqual(pass_(qc), expected) + @ddt.ddt class TestSabreSwapRandomCircuitValidOutput(QiskitTestCase): From 32eae98a2282fdd056a84b12836b124e1726b843 Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Tue, 11 Feb 2025 19:06:11 +0000 Subject: [PATCH 26/48] Consider `unit` in `Delay` comparisons (#13816) The `unit` field was previously ignored, allowing delays in `dt` units to compare equal to those in `s`. This commit does not add additional on-the-fly unit conversion to the comparison: if the user specified durations in different time multiples, they may have had a reason to consider them non-equal. This could be revisiting after the new system for duration handling lands (i.e. the new functionality for `stretch` and other delayed scheduling). --- qiskit/circuit/delay.py | 5 +++ .../notes/delay-compare-b7ecb26b94ff0cd3.yaml | 5 +++ test/python/circuit/test_delay.py | 37 +++++++++++++++++++ 3 files changed, 47 insertions(+) create mode 100644 releasenotes/notes/delay-compare-b7ecb26b94ff0cd3.yaml diff --git a/qiskit/circuit/delay.py b/qiskit/circuit/delay.py index 97a7f935d0f8..6376c8b1daaa 100644 --- a/qiskit/circuit/delay.py +++ b/qiskit/circuit/delay.py @@ -84,6 +84,11 @@ def to_matrix(self) -> np.ndarray: """ return self.__array__(dtype=complex) + def __eq__(self, other): + return ( + isinstance(other, Delay) and self.unit == other.unit and self._compare_parameters(other) + ) + def __repr__(self): """Return the official string representing the delay.""" return f"{self.__class__.__name__}(duration={self.params[0]}[unit={self.unit}])" diff --git a/releasenotes/notes/delay-compare-b7ecb26b94ff0cd3.yaml b/releasenotes/notes/delay-compare-b7ecb26b94ff0cd3.yaml new file mode 100644 index 000000000000..583dc1752838 --- /dev/null +++ b/releasenotes/notes/delay-compare-b7ecb26b94ff0cd3.yaml @@ -0,0 +1,5 @@ +--- +fixes: + - | + Comparisons of :class:`~.circuit.Delay` instructions, including within circuits, now require the + units to be equal as well as the duration value. diff --git a/test/python/circuit/test_delay.py b/test/python/circuit/test_delay.py index 4259c5604606..fb989789e97d 100644 --- a/test/python/circuit/test_delay.py +++ b/test/python/circuit/test_delay.py @@ -99,6 +99,43 @@ def test_to_matrix_return_identity_matrix(self): expected = np.array([[1, 0], [0, 1]], dtype=complex) self.assertTrue(np.array_equal(actual, expected)) + def test_equality(self): + # At the time `__eq__` was specialised for `Delay`, the class was undergoing changes and + # moving to Rust, so we didn't also modify the Python-space semantics to declare equality + # between (say) 1000ms and 1s. We could revisit that decision once the data model settles. + # + # This test then deliberately doesn't assert about mixed-scale comparisons, only comparisons + # between the same units, and 'dt' to absolute times. + def circuit_from(delay): + out = QuantumCircuit(1) + out.append(delay, [0], []) + return out + + a = Parameter("a") + left_instructions, right_instructions = [], [] + left_circuits, right_circuits = [], [] + for unit in ("s", "ms", "us", "ns", "ps", "dt"): + for base in (left_instructions, right_instructions): + base.append(Delay(1, unit)) + base.append(Delay(5.0, unit)) + base.append(Delay(a, unit)) + for base in (left_circuits, right_circuits): + base.append(circuit_from(Delay(1, unit))) + base.append(circuit_from(Delay(5.0, unit))) + base.append(circuit_from(Delay(a, unit))) + self.assertEqual(left_instructions, right_instructions) + self.assertEqual(left_circuits, right_circuits) + + # We can't do all the non-equal tests in a single list comparison, since any single list + # failure would mask any spurious successes. + for unit in ("s", "ms", "us", "ns", "ps"): + self.assertNotEqual(Delay(2, unit), Delay(2, "dt")) + self.assertNotEqual(circuit_from(Delay(2, unit)), circuit_from(Delay(2, "dt"))) + self.assertNotEqual(Delay(2, "dt"), Delay(2, unit)) + self.assertNotEqual(circuit_from(Delay(2, "dt")), circuit_from(Delay(2, unit))) + self.assertNotEqual(Delay(a, unit), Delay(a, "dt")) + self.assertNotEqual(circuit_from(Delay(a, unit)), circuit_from(Delay(a, "dt"))) + class TestParameterizedDelay(QiskitTestCase): """Test delay instruction with parameterized duration.""" From fb7648e0bb875c4581433568005cedb9f5c5c2d1 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Wed, 12 Feb 2025 08:47:45 -0500 Subject: [PATCH 27/48] Leverage native UnitaryGate from rust (#13765) This commit builds off of the native rust representation of a UnitaryGate added in #13759 and uses the native representation everywhere we were using UnitaryGate in rust via python previously: the quantum_volume() function, consolidate blocks, split2qunitaries, and unitary synthesis. One future item is consolidate blocks can be updated to use nalgebra types internally instead of ndarray as for the 1 and 2q cases we know the fixed size of the array ahead of time. However the block consolidation code is built using ndarray currently and later synthesis code also works in ndarray so there isn't any real benefit yet, and we'd just add unecessary conversions and allocations. However, once #13649 merges this will change and it would make more sense to add the unitary gate with a Matrix4. But this can be handled separately after this merges. --- Cargo.lock | 1 + crates/accelerate/Cargo.toml | 1 + .../src/circuit_library/quantum_volume.rs | 56 ++----- crates/accelerate/src/consolidate_blocks.rs | 89 ++++++---- crates/accelerate/src/split_2q_unitaries.rs | 49 +++--- crates/accelerate/src/two_qubit_decompose.rs | 17 ++ crates/accelerate/src/unitary_synthesis.rs | 2 +- crates/circuit/src/dag_circuit.rs | 154 ++++++++++++------ 8 files changed, 221 insertions(+), 148 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4be827893a45..1d757fd122b6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1234,6 +1234,7 @@ dependencies = [ "hashbrown 0.14.5", "indexmap", "itertools 0.13.0", + "nalgebra", "ndarray", "ndarray_einsum_beta", "num-bigint", diff --git a/crates/accelerate/Cargo.toml b/crates/accelerate/Cargo.toml index b17098930b5c..8058139c4f2c 100644 --- a/crates/accelerate/Cargo.toml +++ b/crates/accelerate/Cargo.toml @@ -31,6 +31,7 @@ ndarray_einsum_beta = "0.7" once_cell = "1.20.2" rustiq-core = "0.0.10" bytemuck.workspace = true +nalgebra.workspace = true [dependencies.smallvec] workspace = true diff --git a/crates/accelerate/src/circuit_library/quantum_volume.rs b/crates/accelerate/src/circuit_library/quantum_volume.rs index 463c123eabb7..04b4fa3d4a0c 100644 --- a/crates/accelerate/src/circuit_library/quantum_volume.rs +++ b/crates/accelerate/src/circuit_library/quantum_volume.rs @@ -15,19 +15,15 @@ use pyo3::prelude::*; use pyo3::types::PyDict; use crate::getenv_use_multiple_threads; -use faer_ext::{IntoFaerComplex, IntoNdarrayComplex}; -use ndarray::prelude::*; -use num_complex::Complex64; -use numpy::IntoPyArray; +use nalgebra::Matrix4; +use num_complex::{Complex64, ComplexFloat}; use rand::prelude::*; use rand_distr::StandardNormal; use rand_pcg::Pcg64Mcg; use rayon::prelude::*; use qiskit_circuit::circuit_data::CircuitData; -use qiskit_circuit::imports::UNITARY_GATE; -use qiskit_circuit::operations::Param; -use qiskit_circuit::operations::PyGate; +use qiskit_circuit::operations::{ArrayType, Param, UnitaryGate}; use qiskit_circuit::packed_instruction::PackedOperation; use qiskit_circuit::{Clbit, Qubit}; use smallvec::{smallvec, SmallVec}; @@ -50,11 +46,11 @@ fn random_complex(rng: &mut Pcg64Mcg) -> Complex64 { // // https://github.com/scipy/scipy/blob/v1.14.1/scipy/stats/_multivariate.py#L4224-L4256 #[inline] -fn random_unitaries(seed: u64, size: usize) -> impl Iterator> { +fn random_unitaries(seed: u64, size: usize) -> impl Iterator> { let mut rng = Pcg64Mcg::seed_from_u64(seed); (0..size).map(move |_| { - let raw_numbers: [[Complex64; 4]; 4] = [ + let mat: Matrix4 = [ [ random_complex(&mut rng), random_complex(&mut rng), @@ -79,23 +75,11 @@ fn random_unitaries(seed: u64, size: usize) -> impl Iterator), + let mut build_instruction = |(unitary_index, unitary_array): (usize, Matrix4), rng: &mut Pcg64Mcg| -> PyResult { let layer_index = unitary_index % width; if layer_index == 0 { permutation.shuffle(rng); } - let unitary = unitary_array.into_pyarray(py); - let unitary_gate = UNITARY_GATE - .get_bound(py) - .call((unitary.clone(), py.None(), false), Some(&kwargs))?; - let instruction = PyGate { - qubits: 2, - clbits: 0, - params: 1, - op_name: "unitary".to_string(), - gate: unitary_gate.unbind(), + let unitary_gate = UnitaryGate { + array: ArrayType::TwoQ(unitary_array), }; let qubit = layer_index * 2; Ok(( - PackedOperation::from_gate(Box::new(instruction)), - smallvec![Param::Obj(unitary.into_any().unbind())], + PackedOperation::from_unitary(Box::new(unitary_gate)), + smallvec![], vec![permutation[qubit], permutation[qubit + 1]], vec![], )) @@ -156,7 +132,7 @@ pub fn quantum_volume( .take(num_unitaries) .collect(); - let unitaries: Vec> = if getenv_use_multiple_threads() && num_unitaries > 200 + let unitaries: Vec> = if getenv_use_multiple_threads() && num_unitaries > 200 { seed_vec .par_chunks(per_thread) diff --git a/crates/accelerate/src/consolidate_blocks.rs b/crates/accelerate/src/consolidate_blocks.rs index 30022158e2e5..a4909aef6b5f 100644 --- a/crates/accelerate/src/consolidate_blocks.rs +++ b/crates/accelerate/src/consolidate_blocks.rs @@ -11,18 +11,22 @@ // that they have been altered from the originals. use hashbrown::{HashMap, HashSet}; +use nalgebra::Matrix2; use ndarray::{aview2, Array2}; use num_complex::Complex64; -use numpy::{IntoPyArray, PyReadonlyArray2}; +use numpy::PyReadonlyArray2; use pyo3::intern; use pyo3::prelude::*; use rustworkx_core::petgraph::stable_graph::NodeIndex; +use smallvec::smallvec; use qiskit_circuit::circuit_data::CircuitData; +use qiskit_circuit::circuit_instruction::ExtraInstructionAttributes; use qiskit_circuit::dag_circuit::DAGCircuit; use qiskit_circuit::gate_matrix::{ONE_QUBIT_IDENTITY, TWO_QUBIT_IDENTITY}; -use qiskit_circuit::imports::{QI_OPERATOR, QUANTUM_CIRCUIT, UNITARY_GATE}; -use qiskit_circuit::operations::{Operation, Param}; +use qiskit_circuit::imports::{QI_OPERATOR, QUANTUM_CIRCUIT}; +use qiskit_circuit::operations::{ArrayType, Operation, Param, UnitaryGate}; +use qiskit_circuit::packed_instruction::PackedOperation; use qiskit_circuit::Qubit; use crate::convert_2q_block_matrix::{blocks_to_matrix, get_matrix_from_inst}; @@ -112,11 +116,17 @@ pub(crate) fn consolidate_blocks( Ok(mat) => mat, Err(_) => continue, }; - let array = matrix.into_pyarray(py); - let unitary_gate = UNITARY_GATE - .get_bound(py) - .call1((array, py.None(), false))?; - dag.substitute_node_with_py_op(py, inst_node, &unitary_gate, false)?; + // TODO: Use Matrix2/ArrayType::OneQ when we're using nalgebra + // for consolidation + let unitary_gate = UnitaryGate { + array: ArrayType::NDArray(matrix), + }; + dag.substitute_op( + inst_node, + PackedOperation::from_unitary(Box::new(unitary_gate)), + smallvec![], + ExtraInstructionAttributes::default(), + )?; continue; } } @@ -180,16 +190,16 @@ pub(crate) fn consolidate_blocks( dag.remove_op_node(node); } } else { - let unitary_gate = UNITARY_GATE.get_bound(py).call1(( - array.as_ref().into_pyobject(py)?, - py.None(), - false, - ))?; + let matrix = array.as_array().to_owned(); + let unitary_gate = UnitaryGate { + array: ArrayType::NDArray(matrix), + }; let clbit_pos_map = HashMap::new(); - dag.replace_block_with_py_op( - py, + dag.replace_block( &block, - unitary_gate, + PackedOperation::from_unitary(Box::new(unitary_gate)), + smallvec![], + ExtraInstructionAttributes::default(), false, &block_index_map, &clbit_pos_map, @@ -213,21 +223,22 @@ pub(crate) fn consolidate_blocks( dag.remove_op_node(node); } } else { - let array = matrix.into_pyarray(py); - let unitary_gate = - UNITARY_GATE - .get_bound(py) - .call1((array, py.None(), false))?; + // TODO: Use Matrix4/ArrayType::TwoQ when we're using nalgebra + // for consolidation + let unitary_gate = UnitaryGate { + array: ArrayType::NDArray(matrix), + }; let qubit_pos_map = block_index_map .into_iter() .enumerate() .map(|(idx, qubit)| (qubit, idx)) .collect(); let clbit_pos_map = HashMap::new(); - dag.replace_block_with_py_op( - py, + dag.replace_block( &block, - unitary_gate, + PackedOperation::from_unitary(Box::new(unitary_gate)), + smallvec![], + ExtraInstructionAttributes::default(), false, &qubit_pos_map, &clbit_pos_map, @@ -258,11 +269,15 @@ pub(crate) fn consolidate_blocks( Ok(mat) => mat, Err(_) => continue, }; - let array = matrix.into_pyarray(py); - let unitary_gate = UNITARY_GATE - .get_bound(py) - .call1((array, py.None(), false))?; - dag.substitute_node_with_py_op(py, first_inst_node, &unitary_gate, false)?; + let unitary_gate = UnitaryGate { + array: ArrayType::NDArray(matrix), + }; + dag.substitute_op( + first_inst_node, + PackedOperation::from_unitary(Box::new(unitary_gate)), + smallvec![], + ExtraInstructionAttributes::default(), + )?; continue; } let qubit = first_qubits[0]; @@ -293,17 +308,19 @@ pub(crate) fn consolidate_blocks( dag.remove_op_node(node); } } else { - let array = aview2(&matrix).to_owned().into_pyarray(py); - let unitary_gate = UNITARY_GATE - .get_bound(py) - .call1((array, py.None(), false))?; + let array: Matrix2 = + Matrix2::from_row_iterator(matrix.into_iter().flat_map(|x| x.into_iter())); + let unitary_gate = UnitaryGate { + array: ArrayType::OneQ(array), + }; let mut block_index_map: HashMap = HashMap::with_capacity(1); block_index_map.insert(qubit, 0); let clbit_pos_map = HashMap::new(); - dag.replace_block_with_py_op( - py, + dag.replace_block( &run, - unitary_gate, + PackedOperation::from_unitary(Box::new(unitary_gate)), + smallvec![], + ExtraInstructionAttributes::default(), false, &block_index_map, &clbit_pos_map, diff --git a/crates/accelerate/src/split_2q_unitaries.rs b/crates/accelerate/src/split_2q_unitaries.rs index 8d54e8875c42..366a333c7a78 100644 --- a/crates/accelerate/src/split_2q_unitaries.rs +++ b/crates/accelerate/src/split_2q_unitaries.rs @@ -10,15 +10,15 @@ // copyright notice, and modified files need to carry a notice indicating // that they have been altered from the originals. -use pyo3::intern; +use nalgebra::Matrix2; +use num_complex::Complex64; use pyo3::prelude::*; -use pyo3::types::PyDict; use rustworkx_core::petgraph::stable_graph::NodeIndex; +use smallvec::{smallvec, SmallVec}; -use qiskit_circuit::circuit_instruction::OperationFromPython; use qiskit_circuit::dag_circuit::{DAGCircuit, NodeType, Wire}; -use qiskit_circuit::imports::UNITARY_GATE; -use qiskit_circuit::operations::{Operation, Param}; +use qiskit_circuit::operations::{ArrayType, Operation, OperationRef, Param, UnitaryGate}; +use qiskit_circuit::packed_instruction::PackedOperation; use crate::two_qubit_decompose::{Specialization, TwoQubitWeylDecomposition}; @@ -39,7 +39,7 @@ pub fn split_2q_unitaries( // We only attempt to split UnitaryGate objects, but this could be extended in future // -- however we need to ensure that we can compile the resulting single-qubit unitaries // to the supported basis gate set. - if qubits.len() != 2 || inst.op.name() != "unitary" { + if qubits.len() != 2 || !matches!(inst.op.view(), OperationRef::Unitary(_)) { continue; } let matrix = inst @@ -52,22 +52,33 @@ pub fn split_2q_unitaries( None, )?; if matches!(decomp.specialization, Specialization::IdEquiv) { - let k1r_arr = decomp.K1r(py); - let k1l_arr = decomp.K1l(py); - let kwargs = PyDict::new(py); - kwargs.set_item(intern!(py, "num_qubits"), 1)?; - let k1r_gate = UNITARY_GATE - .get_bound(py) - .call((k1r_arr, py.None(), false), Some(&kwargs))?; - let k1l_gate = UNITARY_GATE - .get_bound(py) - .call((k1l_arr, py.None(), false), Some(&kwargs))?; - let insert_fn = |edge: &Wire| -> PyResult { + let k1r_arr = decomp.k1r_view(); + let k1l_arr = decomp.k1l_view(); + + let insert_fn = |edge: &Wire| -> (PackedOperation, SmallVec<[Param; 3]>) { if let Wire::Qubit(qubit) = edge { if *qubit == qubits[0] { - k1r_gate.extract() + let mat: Matrix2 = [ + [k1r_arr[[0, 0]], k1r_arr[[0, 1]]], + [k1r_arr[[1, 0]], k1r_arr[[1, 1]]], + ] + .into(); + let k1r_gate = Box::new(UnitaryGate { + array: ArrayType::OneQ(mat), + }); + (PackedOperation::from_unitary(k1r_gate), smallvec![]) } else { - k1l_gate.extract() + let mat: Matrix2 = [ + [k1l_arr[[0, 0]], k1l_arr[[0, 1]]], + [k1l_arr[[1, 0]], k1l_arr[[1, 1]]], + ] + .into(); + + let k1l_gate = Box::new(UnitaryGate { + array: ArrayType::OneQ(mat), + }); + + (PackedOperation::from_unitary(k1l_gate), smallvec![]) } } else { unreachable!("This will only be called on ops with no classical wires."); diff --git a/crates/accelerate/src/two_qubit_decompose.rs b/crates/accelerate/src/two_qubit_decompose.rs index 5ce6d248c4eb..0f886322a09d 100644 --- a/crates/accelerate/src/two_qubit_decompose.rs +++ b/crates/accelerate/src/two_qubit_decompose.rs @@ -533,6 +533,23 @@ impl TwoQubitWeylDecomposition { pub fn c(&self) -> f64 { self.c } + + pub fn k1l_view(&self) -> ArrayView2 { + self.K1l.view() + } + + pub fn k2l_view(&self) -> ArrayView2 { + self.K2l.view() + } + + pub fn k1r_view(&self) -> ArrayView2 { + self.K1r.view() + } + + pub fn k2r_view(&self) -> ArrayView2 { + self.K2r.view() + } + fn weyl_gate( &self, simplify: bool, diff --git a/crates/accelerate/src/unitary_synthesis.rs b/crates/accelerate/src/unitary_synthesis.rs index b36715bd2cf9..9d167c5f083a 100644 --- a/crates/accelerate/src/unitary_synthesis.rs +++ b/crates/accelerate/src/unitary_synthesis.rs @@ -332,7 +332,7 @@ fn py_run_main_loop( py_op: new_node.unbind().into(), }; } - if !(packed_instr.op.name() == "unitary" + if !(matches!(packed_instr.op.view(), OperationRef::Unitary(_)) && packed_instr.op.num_qubits() >= min_qubits as u32) { out_dag.push_back(py, packed_instr)?; diff --git a/crates/circuit/src/dag_circuit.rs b/crates/circuit/src/dag_circuit.rs index 42b334e88207..5531aa7a8524 100644 --- a/crates/circuit/src/dag_circuit.rs +++ b/crates/circuit/src/dag_circuit.rs @@ -2864,10 +2864,13 @@ def _format(operand): } let block_ids: Vec<_> = node_block.iter().map(|n| n.node.unwrap()).collect(); - let new_node = self.replace_block_with_py_op( - py, + let py_op = op.extract::()?; + + let new_node = self.replace_block( &block_ids, - op, + py_op.operation, + py_op.params, + py_op.extra_attrs, cycle_check, &qubit_pos_map, &clbit_pos_map, @@ -6343,7 +6346,7 @@ impl DAGCircuit { insert: F, ) -> PyResult<()> where - F: Fn(&Wire) -> PyResult, + F: Fn(&Wire) -> (PackedOperation, SmallVec<[Param; 3]>), { let mut edge_list: Vec<(NodeIndex, NodeIndex, Wire)> = Vec::with_capacity(2); for (source, in_weight) in self @@ -6362,15 +6365,15 @@ impl DAGCircuit { } } for (source, target, weight) in edge_list { - let new_op = insert(&weight)?; - self.increment_op(new_op.operation.name()); + let (new_op, params) = insert(&weight); + self.increment_op(new_op.name()); let qubits = if let Wire::Qubit(qubit) = weight { vec![qubit] } else { panic!("This method only works if the gate being replaced has no classical incident wires") }; #[cfg(feature = "cache_pygates")] - let py_op = match new_op.operation.view() { + let py_op = match new_op.view() { OperationRef::StandardGate(_) | OperationRef::StandardInstruction(_) | OperationRef::Unitary(_) => OnceLock::new(), @@ -6381,11 +6384,11 @@ impl DAGCircuit { OperationRef::Operation(op) => OnceLock::from(op.operation.clone_ref(py)), }; let inst = PackedInstruction { - op: new_op.operation, + op: new_op, qubits: self.qargs_interner.insert_owned(qubits), clbits: self.cargs_interner.get_default(), - params: (!new_op.params.is_empty()).then(|| Box::new(new_op.params)), - extra_attrs: new_op.extra_attrs, + params: (!params.is_empty()).then(|| Box::new(params)), + extra_attrs: ExtraInstructionAttributes::default(), #[cfg(feature = "cache_pygates")] py_op, }; @@ -6850,12 +6853,14 @@ impl DAGCircuit { Self::from_circuit(py, circ, copy_op, None, None) } - /// Replace a block of node indices with a new python operation - pub fn replace_block_with_py_op( + #[allow(clippy::too_many_arguments)] + /// Replace a block of node indices with a new packed operation + pub fn replace_block( &mut self, - py: Python, block_ids: &[NodeIndex], - op: Bound, + op: PackedOperation, + params: SmallVec<[Param; 3]>, + extra_attrs: ExtraInstructionAttributes, cycle_check: bool, qubit_pos_map: &HashMap, clbit_pos_map: &HashMap, @@ -6872,41 +6877,47 @@ impl DAGCircuit { block_cargs.extend(self.cargs_interner.get(packed.clbits)); if let Some(condition) = packed.condition() { - block_cargs.extend( - self.clbits.map_bits( - self.control_flow_module - .condition_resources(condition.bind(py))? - .clbits - .bind(py), - )?, - ); + Python::with_gil(|py| -> PyResult<()> { + block_cargs.extend( + self.clbits.map_bits( + self.control_flow_module + .condition_resources(condition.bind(py))? + .clbits + .bind(py), + )?, + ); + Ok(()) + })?; continue; } // Add classical bits from SwitchCaseOp, if applicable. if let OperationRef::Instruction(op) = packed.op.view() { if op.name() == "switch_case" { - let op_bound = op.instruction.bind(py); - let target = op_bound.getattr(intern!(py, "target"))?; - if target.is_instance(imports::CLBIT.get_bound(py))? { - block_cargs.insert(self.clbits.find(&target).unwrap()); - } else if target - .is_instance(imports::CLASSICAL_REGISTER.get_bound(py))? - { - block_cargs.extend( - self.clbits - .map_bits(target.extract::>>()?)?, - ); - } else { - block_cargs.extend( - self.clbits.map_bits( - self.control_flow_module - .node_resources(&target)? - .clbits - .bind(py), - )?, - ); - } + Python::with_gil(|py| -> PyResult<()> { + let op_bound = op.instruction.bind(py); + let target = op_bound.getattr(intern!(py, "target"))?; + if target.is_instance(imports::CLBIT.get_bound(py))? { + block_cargs.insert(self.clbits.find(&target).unwrap()); + } else if target + .is_instance(imports::CLASSICAL_REGISTER.get_bound(py))? + { + block_cargs.extend( + self.clbits + .map_bits(target.extract::>>()?)?, + ); + } else { + block_cargs.extend( + self.clbits.map_bits( + self.control_flow_module + .node_resources(&target)? + .clbits + .bind(py), + )?, + ); + } + Ok(()) + })?; } } } @@ -6935,25 +6946,23 @@ impl DAGCircuit { .collect(); block_cargs.sort_by_key(|c| clbit_pos_map[c]); - let py_op = op.extract::()?; - - if py_op.operation.num_qubits() as usize != block_qargs.len() { + if op.num_qubits() as usize != block_qargs.len() { return Err(DAGCircuitError::new_err(format!( - "Number of qubits in the replacement operation ({}) is not equal to the number of qubits in the block ({})!", py_op.operation.num_qubits(), block_qargs.len() + "Number of qubits in the replacement operation ({}) is not equal to the number of qubits in the block ({})!", op.num_qubits(), block_qargs.len() ))); } - let op_name = py_op.operation.name().to_string(); + let op_name = op.name().to_string(); let qubits = self.qargs_interner.insert_owned(block_qargs); let clbits = self.cargs_interner.insert_owned(block_cargs); let weight = NodeType::Operation(PackedInstruction { - op: py_op.operation, + op, qubits, clbits, - params: (!py_op.params.is_empty()).then(|| Box::new(py_op.params)), - extra_attrs: py_op.extra_attrs, + params: (!params.is_empty()).then(|| Box::new(params)), + extra_attrs, #[cfg(feature = "cache_pygates")] - py_op: op.unbind().into(), + py_op: OnceLock::new(), }); let new_node = self @@ -6972,6 +6981,47 @@ impl DAGCircuit { Ok(new_node) } + /// Substitute an operation in a node with a new one. The wire counts must match and the same + /// argument order will be used. + pub fn substitute_op( + &mut self, + node_index: NodeIndex, + new_op: PackedOperation, + params: SmallVec<[Param; 3]>, + extra_attrs: ExtraInstructionAttributes, + ) -> PyResult<()> { + let old_packed = self.dag[node_index].unwrap_operation(); + let op_name = old_packed.op.name().to_string(); + + if old_packed.op.num_qubits() != new_op.num_qubits() + || old_packed.op.num_clbits() != new_op.num_clbits() + { + return Err(DAGCircuitError::new_err( + format!( + "Cannot replace node of width ({} qubits, {} clbits) with operation of mismatched width ({} qubits, {} clbits)", + old_packed.op.num_qubits(), old_packed.op.num_clbits(), new_op.num_qubits(), new_op.num_clbits() + ))); + } + let new_op_name = new_op.name().to_string(); + let new_weight = NodeType::Operation(PackedInstruction { + op: new_op, + qubits: old_packed.qubits, + clbits: old_packed.clbits, + params: (!params.is_empty()).then(|| params.into()), + extra_attrs, + #[cfg(feature = "cache_pygates")] + py_op: OnceLock::new(), + }); + if let Some(weight) = self.dag.node_weight_mut(node_index) { + *weight = new_weight; + } + + // Update self.op_names + self.decrement_op(op_name.as_str()); + self.increment_op(new_op_name.as_str()); + Ok(()) + } + /// Substitute a give node in the dag with a new operation from python pub fn substitute_node_with_py_op( &mut self, From 9ca951c81c34a5065de9532bf58b08e0aac4e2b8 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Wed, 12 Feb 2025 10:19:42 -0500 Subject: [PATCH 28/48] Perform BarrierBeforeFinalMeasurements analysis in parallel (#13411) * Use OnceLock instead of OnceCell OnceLock is a thread-safe version of OnceCell that enables us to use PackedInstruction from a threaded environment. There is some overhead associated with this, primarily in memory as the OnceLock is a larger type than a OnceCell. But the tradeoff is worth it to start leverage multithreading for circuits. Fixes #13219 * Update twirling too * Perform BarrierBeforeFinalMeasurements analysis in paralle With #13410 removing the non-threadsafe structure from our circuit representation we're now able to read and iterate over a DAGCircuit from multiple threads. This commit is the first small piece doing this, it moves the analysis portion of the BarrierBeforeFinalMeasurements pass to execure in parallel. The pass checks every node to ensure all it's decendents are either a measure or a barrier before reaching the end of the circuit. This commit iterates over all the nodes and does the check in parallel. * Remove allocation for node scan * Refactor pass to optimize search and set parallel threshold This commit updates the logic in the pass to simplify the search algorithm and improve it's overall efficiency. Previously the pass would search the entire dag for all barrier and measurements and then did a BFS from each found node to check that all descendants are either barriers or measurements. Then with the set of nodes matching that condition a full topological sort of the dag was run, then the topologically ordered nodes were filtered for the matching set. That sorted set is then used for filtering This commit refactors this to do a reverse search from the output nodes which reduces the complexity of the algorithm. This new algorithm is also conducive for parallel execution because it does a search starting from each qubit's output node. Doing a test with a quantum volume circuit from 10 to 1000 qubits which scales linearly in depth and number of qubits a crossover point between the parallel and serial implementations was found around 150 qubits. * Update crates/circuit/src/dag_circuit.rs Co-authored-by: Raynel Sanchez <87539502+raynelfss@users.noreply.github.com> * Rework logic to check using StandardInstruction * Add comments explaining the search function * Update crates/circuit/src/dag_circuit.rs Co-authored-by: Raynel Sanchez <87539502+raynelfss@users.noreply.github.com> --------- Co-authored-by: Raynel Sanchez <87539502+raynelfss@users.noreply.github.com> --- .../src/barrier_before_final_measurement.rs | 135 ++++++++++++++---- crates/circuit/src/dag_circuit.rs | 26 +++- 2 files changed, 129 insertions(+), 32 deletions(-) diff --git a/crates/accelerate/src/barrier_before_final_measurement.rs b/crates/accelerate/src/barrier_before_final_measurement.rs index 1ae258e34042..54d75d9d09ee 100644 --- a/crates/accelerate/src/barrier_before_final_measurement.rs +++ b/crates/accelerate/src/barrier_before_final_measurement.rs @@ -10,17 +10,17 @@ // copyright notice, and modified files need to carry a notice indicating // that they have been altered from the originals. -use hashbrown::HashSet; use pyo3::prelude::*; +use rayon::prelude::*; use rustworkx_core::petgraph::stable_graph::NodeIndex; use qiskit_circuit::circuit_instruction::ExtraInstructionAttributes; use qiskit_circuit::dag_circuit::{DAGCircuit, NodeType}; -use qiskit_circuit::operations::{Operation, StandardInstruction}; +use qiskit_circuit::operations::{OperationRef, StandardInstruction}; use qiskit_circuit::packed_instruction::{PackedInstruction, PackedOperation}; use qiskit_circuit::Qubit; -static FINAL_OP_NAMES: [&str; 2] = ["measure", "barrier"]; +const PARALLEL_THRESHOLD: usize = 150; #[pyfunction] #[pyo3(signature=(dag, label=None))] @@ -29,39 +29,116 @@ pub fn barrier_before_final_measurements( dag: &mut DAGCircuit, label: Option, ) -> PyResult<()> { - let is_exactly_final = |inst: &PackedInstruction| FINAL_OP_NAMES.contains(&inst.op.name()); - let final_ops: HashSet = dag - .op_nodes(true) - .filter_map(|(node, inst)| { - if !is_exactly_final(inst) { - return None; - } - dag.bfs_successors(node) - .all(|(_, child_successors)| { - child_successors.iter().all(|suc| match dag[*suc] { - NodeType::Operation(ref suc_inst) => is_exactly_final(suc_inst), + // Get a list of the node indices which are final measurement or barriers that are ancestors + // of a given qubit's output node. + let find_final_nodes = |[_in_index, out_index]: &[NodeIndex; 2]| -> Vec { + // Next nodes is the stack of parent nodes to investigate. It starts with any predecessors + // of a qubit's output node that are Barrier or Measure + let mut next_nodes: Vec = dag + .quantum_predecessors(*out_index) + .filter(|index| { + let node = &dag[*index]; + match node { + NodeType::Operation(inst) => { + if let OperationRef::StandardInstruction(op) = inst.op.view() { + if matches!( + op, + StandardInstruction::Measure | StandardInstruction::Barrier(_) + ) { + dag.bfs_successors(*index).all(|(_, child_successors)| { + child_successors.iter().all(|suc| match &dag[*suc] { + NodeType::Operation(suc_inst) => match suc_inst.op.view() { + OperationRef::StandardInstruction(suc_op) => { + matches!( + suc_op, + StandardInstruction::Measure + | StandardInstruction::Barrier(_) + ) + } + _ => false, + }, + _ => true, + }) + }) + } else { + false + } + } else { + false + } + } + _ => false, + } + }) + .collect(); + let mut nodes: Vec = Vec::new(); + // Reverse traverse the dag from next nodes until we encounter no more barriers or measures + while let Some(node_index) = next_nodes.pop() { + // If node on the stack is a barrier or measure we can add it to the output list + if node_index != *out_index + && dag.bfs_successors(node_index).all(|(_, child_successors)| { + child_successors.iter().all(|suc| match &dag[*suc] { + NodeType::Operation(suc_inst) => match suc_inst.op.view() { + OperationRef::StandardInstruction(suc_op) => matches!( + suc_op, + StandardInstruction::Measure | StandardInstruction::Barrier(_) + ), + _ => false, + }, _ => true, }) }) - .then_some(node) - }) - .collect(); + { + nodes.push(node_index); + } + // For this node if any parent nodes are barrier or measure add those to the stack + for pred in dag.quantum_predecessors(node_index) { + match &dag[pred] { + NodeType::Operation(inst) => { + if let OperationRef::StandardInstruction(op) = inst.op.view() { + if matches!( + op, + StandardInstruction::Measure | StandardInstruction::Barrier(_) + ) { + next_nodes.push(pred) + } + } + } + _ => continue, + } + } + } + nodes.reverse(); + nodes + }; + + let final_ops: Vec = + if dag.num_qubits() >= PARALLEL_THRESHOLD && crate::getenv_use_multiple_threads() { + dag.qubit_io_map() + .par_iter() + .flat_map(find_final_nodes) + .collect() + } else { + dag.qubit_io_map() + .iter() + .flat_map(find_final_nodes) + .collect() + }; + if final_ops.is_empty() { return Ok(()); } - let ordered_node_indices: Vec = dag - .topological_op_nodes()? - .filter(|node| final_ops.contains(node)) - .collect(); - let final_packed_ops: Vec = ordered_node_indices + let final_packed_ops: Vec = final_ops .into_iter() - .map(|node| { - let NodeType::Operation(ref inst) = dag[node] else { - unreachable!() - }; - let res = inst.clone(); - dag.remove_op_node(node); - res + .filter_map(|node| match dag.dag().node_weight(node) { + Some(weight) => { + let NodeType::Operation(_) = weight else { + return None; + }; + let res = dag.remove_op_node(node); + Some(res) + } + None => None, }) .collect(); let qargs: Vec = (0..dag.num_qubits() as u32).map(Qubit).collect(); diff --git a/crates/circuit/src/dag_circuit.rs b/crates/circuit/src/dag_circuit.rs index 5531aa7a8524..0dbf6aff8979 100644 --- a/crates/circuit/src/dag_circuit.rs +++ b/crates/circuit/src/dag_circuit.rs @@ -60,8 +60,8 @@ use rustworkx_core::petgraph::visit::{ }; use rustworkx_core::petgraph::Incoming; use rustworkx_core::traversal::{ - ancestors as core_ancestors, bfs_successors as core_bfs_successors, - descendants as core_descendants, + ancestors as core_ancestors, bfs_predecessors as core_bfs_predecessors, + bfs_successors as core_bfs_successors, descendants as core_descendants, }; use std::cmp::Ordering; @@ -4830,6 +4830,12 @@ def _format(operand): } impl DAGCircuit { + /// Returns an immutable view of the qubit io map + #[inline(always)] + pub fn qubit_io_map(&self) -> &[[NodeIndex; 2]] { + &self.qubit_io_map + } + /// Returns an immutable view of the inner StableGraph managed by the circuit. #[inline(always)] pub fn dag(&self) -> &StableDiGraph { @@ -5639,7 +5645,11 @@ impl DAGCircuit { /// Remove an operation node n. /// /// Add edges from predecessors to successors. - pub fn remove_op_node(&mut self, index: NodeIndex) { + /// + /// # Returns + /// + /// The removed [PackedInstruction] is returned + pub fn remove_op_node(&mut self, index: NodeIndex) -> PackedInstruction { let mut edge_list: Vec<(NodeIndex, NodeIndex, Wire)> = Vec::new(); for (source, in_weight) in self .dag @@ -5664,6 +5674,7 @@ impl DAGCircuit { Some(NodeType::Operation(packed)) => { let op_name = packed.op.name(); self.decrement_op(op_name); + packed } _ => panic!("Must be called with valid operation node!"), } @@ -5688,6 +5699,15 @@ impl DAGCircuit { core_bfs_successors(&self.dag, node).filter(move |(_, others)| !others.is_empty()) } + /// Returns an iterator of tuples of (DAGNode, [DAGNodes]) where the DAGNode is the current node + /// and [DAGNode] is its predecessors in BFS order. + pub fn bfs_predecessors( + &self, + node: NodeIndex, + ) -> impl Iterator)> + '_ { + core_bfs_predecessors(&self.dag, node).filter(move |(_, others)| !others.is_empty()) + } + fn pack_into(&mut self, py: Python, b: &Bound) -> Result { Ok(if let Ok(in_node) = b.downcast::() { let in_node = in_node.borrow(); From 3eb35385f9579669f5c375ff065ec5036d79456e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elena=20Pe=C3=B1a=20Tapia?= <57907331+ElePT@users.noreply.github.com> Date: Wed, 12 Feb 2025 18:27:50 +0100 Subject: [PATCH 29/48] Add dt to GenericBackendV2 (#13830) --- qiskit/providers/fake_provider/generic_backend_v2.py | 9 +++++++-- .../add-dt-generic-backend-v2-822f8806517e5dd1.yaml | 12 ++++++++++++ .../fake_provider/test_generic_backend_v2.py | 9 +++++++++ 3 files changed, 28 insertions(+), 2 deletions(-) create mode 100644 releasenotes/notes/add-dt-generic-backend-v2-822f8806517e5dd1.yaml diff --git a/qiskit/providers/fake_provider/generic_backend_v2.py b/qiskit/providers/fake_provider/generic_backend_v2.py index afb2c6b2bf82..163c8abfdc38 100644 --- a/qiskit/providers/fake_provider/generic_backend_v2.py +++ b/qiskit/providers/fake_provider/generic_backend_v2.py @@ -530,6 +530,7 @@ def __init__( control_flow: bool = False, calibrate_instructions: bool | InstructionScheduleMap | None = None, dtm: float | None = None, + dt: float | None = None, seed: int | None = None, pulse_channels: bool = True, noise_info: bool = True, @@ -579,6 +580,9 @@ def __init__( dtm: System time resolution of output signals in nanoseconds. None by default. + dt: System time resolution of input signals in nanoseconds. + None by default. + seed: Optional seed for generation of default values. pulse_channels: DEPRECATED. If true, sets default pulse channel information on the backend. @@ -596,6 +600,7 @@ def __init__( self._sim = None self._rng = np.random.default_rng(seed=seed) self._dtm = dtm + self._dt = dt self._num_qubits = num_qubits self._control_flow = control_flow self._calibrate_instructions = calibrate_instructions @@ -788,7 +793,7 @@ def _build_generic_target(self): self._target = Target( description=f"Generic Target with {self._num_qubits} qubits", num_qubits=self._num_qubits, - dt=properties["dt"], + dt=properties["dt"] if self._dt is None else self._dt, qubit_properties=None, concurrent_measurements=[list(range(self._num_qubits))], ) @@ -796,7 +801,7 @@ def _build_generic_target(self): self._target = Target( description=f"Generic Target with {self._num_qubits} qubits", num_qubits=self._num_qubits, - dt=properties["dt"], + dt=properties["dt"] if self._dt is None else self._dt, qubit_properties=[ QubitProperties( t1=self._rng.uniform(properties["t1"][0], properties["t1"][1]), diff --git a/releasenotes/notes/add-dt-generic-backend-v2-822f8806517e5dd1.yaml b/releasenotes/notes/add-dt-generic-backend-v2-822f8806517e5dd1.yaml new file mode 100644 index 000000000000..d1aefc0f3343 --- /dev/null +++ b/releasenotes/notes/add-dt-generic-backend-v2-822f8806517e5dd1.yaml @@ -0,0 +1,12 @@ +--- +features_providers: + - | + Added the ability to set the ``dt`` property of :class:`.GenericBackendV2` in the class initializer + with a new ``dt`` argument. Example usage:: + + from qiskit.providers.fake_provider import GenericBackendV2 + backend = GenericBackendV2( + num_qubits=5, + basis_gates=["cx", "id", "rz", "sx", "x"], + dt= 2.22*e-10, + seed=42) diff --git a/test/python/providers/fake_provider/test_generic_backend_v2.py b/test/python/providers/fake_provider/test_generic_backend_v2.py index d42a6dbf7e07..797a91828555 100644 --- a/test/python/providers/fake_provider/test_generic_backend_v2.py +++ b/test/python/providers/fake_provider/test_generic_backend_v2.py @@ -227,3 +227,12 @@ def test_duration_defaults(self): if inst not in ["delay", "reset"]: self.assertGreaterEqual(duration, expected_durations[inst][0]) self.assertLessEqual(duration, expected_durations[inst][1]) + + def test_custom_dt(self): + """Test that the custom dt is respected.""" + + ref_backend = GenericBackendV2(num_qubits=2, basis_gates=["cx", "id"], seed=42) + double_dt_backend = GenericBackendV2( + num_qubits=2, basis_gates=["cx", "id"], dt=ref_backend.dt * 2, seed=42 + ) + self.assertEqual(ref_backend.dt * 2, double_dt_backend.dt) From b933179837a831223ce84b9bee8002c92cdf23e9 Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Thu, 13 Feb 2025 09:08:25 +0000 Subject: [PATCH 30/48] Fix tracking of routing permutation in Sabre with disjoint backends (#13833) If the backing coupling graph is disjoint, and unused components of the coupling graph would not be considered when constructing the complete routing permutation. In practice, Sabre aborts immediately after layout without attempting to route, if it needed to split the DAG across more than one disjoint component, because it can't guarantee correctness of the final routing in the presence of component-spanning barriers or classical communication, so the only way for a component to be forgotten is if the backend is disjoint, but the DAG fits into a single component. --- qiskit/transpiler/passes/layout/sabre_layout.py | 13 +++++++++++++ .../sabre-disjoint-routing-85c6f6481c9ffca4.yaml | 15 +++++++++++++++ test/python/transpiler/test_sabre_layout.py | 15 +++++++++++++++ 3 files changed, 43 insertions(+) create mode 100644 releasenotes/notes/sabre-disjoint-routing-85c6f6481c9ffca4.yaml diff --git a/qiskit/transpiler/passes/layout/sabre_layout.py b/qiskit/transpiler/passes/layout/sabre_layout.py index af17cc226cb7..360e8d003e79 100644 --- a/qiskit/transpiler/passes/layout/sabre_layout.py +++ b/qiskit/transpiler/passes/layout/sabre_layout.py @@ -328,6 +328,19 @@ def run(self, dag): for initial, final in enumerate(component.final_permutation) } ) + + # The coupling map may have been split into more components than the DAG. In this case, + # there will be some physical qubits unaccounted for in our `final_layout`. Strictly the + # `if` check is unnecessary, but we can avoid the loop for most circuits and backends. + if len(final_layout) != len(physical_qubits): + used_qubits = { + qubit for component in components for qubit in component.coupling_map.graph.nodes() + } + for index, qubit in enumerate(physical_qubits): + if index in used_qubits: + continue + final_layout[qubit] = index + if self.property_set["final_layout"] is None: self.property_set["final_layout"] = final_layout else: diff --git a/releasenotes/notes/sabre-disjoint-routing-85c6f6481c9ffca4.yaml b/releasenotes/notes/sabre-disjoint-routing-85c6f6481c9ffca4.yaml new file mode 100644 index 000000000000..e0618dd601f7 --- /dev/null +++ b/releasenotes/notes/sabre-disjoint-routing-85c6f6481c9ffca4.yaml @@ -0,0 +1,15 @@ +--- +fixes: + - | + When :class:`.SabreLayout` is used to do both layout and routing simultaneously (as is the case + for the default options to :func:`.transpile` and :func:`.generate_preset_pass_manager`) on a + :class:`.Target` or :class:`.CouplingMap` with disjoint connectivity, and the input circuit fits + into a single component of the coupling map, the routing permutation will now be tracked + correctly. + + Previously, any qubits in the coupling map that were not connected, even indirectly, to a qubit + used by the routed circuit would not be included in the final routing permutation. This could + cause surprising behaviour a long way from the point of failure, even if compilation appeared to + succeed, such as calls to :meth:`.TranspileLayout.final_index_layout` raising :exc:`KeyError`. + + This bug did not affect backends that were fully connected, as most are. diff --git a/test/python/transpiler/test_sabre_layout.py b/test/python/transpiler/test_sabre_layout.py index f8da34636cc7..e189355d0b98 100644 --- a/test/python/transpiler/test_sabre_layout.py +++ b/test/python/transpiler/test_sabre_layout.py @@ -470,6 +470,21 @@ def test_with_partial_layout(self): layout = pm.property_set["layout"] self.assertEqual([layout[q] for q in qc.qubits], [3, 1, 2, 5, 4, 6, 7, 8]) + def test_dag_fits_in_one_component(self): + """Test that the output is valid if the DAG all fits in a single component of a disjoint + coupling map..""" + qc = QuantumCircuit(3) + qc.cx(0, 1) + qc.cx(1, 2) + qc.cx(2, 0) + + disjoint = CouplingMap([(0, 1), (1, 2), (3, 4), (4, 5)]) + layout_routing_pass = SabreLayout(disjoint, seed=2025_02_12, swap_trials=1, layout_trials=1) + out = layout_routing_pass(qc) + self.assertEqual(len(out.layout.initial_layout), len(out.layout.final_layout)) + self.assertEqual(out.layout.initial_index_layout(filter_ancillas=False), [1, 0, 2, 3, 4, 5]) + self.assertEqual(out.layout.final_index_layout(filter_ancillas=False), [2, 0, 1, 3, 4, 5]) + class TestSabrePreLayout(QiskitTestCase): """Tests the SabreLayout pass with starting layout created by SabrePreLayout.""" From af87dda4cbfb8fca99e6e4a2d6e84c19f87d38bc Mon Sep 17 00:00:00 2001 From: Matteo Paltenghi Date: Thu, 13 Feb 2025 14:05:01 +0100 Subject: [PATCH 31/48] Add test for exceptional behavior when initializing `PauliList.from_symplectic` (#13751) * add test for exceptional behavior PauliList.from_symplectic * fix black format * add docstring + use self.assertRaisesRegex --- .../quantum_info/operators/symplectic/test_pauli_list.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/test/python/quantum_info/operators/symplectic/test_pauli_list.py b/test/python/quantum_info/operators/symplectic/test_pauli_list.py index 9abc473dc333..cac8153ad0f2 100644 --- a/test/python/quantum_info/operators/symplectic/test_pauli_list.py +++ b/test/python/quantum_info/operators/symplectic/test_pauli_list.py @@ -221,6 +221,14 @@ def test_init_from_settings(self): from_settings = PauliList(**pauli_list.settings) self.assertEqual(pauli_list, from_settings) + def test_from_symplectic_phase_check(self): + """Test the from_symplectic method of PauliList for phase dimension check.""" + z = np.array([[0], [0]]) + x = np.array([[0], [0]]) + phase = np.array([[0], [0]]) # 2D phase + with self.assertRaisesRegex(ValueError, "phase should be at most 1D but has 2 dimensions."): + PauliList.from_symplectic(z, x, phase) + @ddt class TestPauliListProperties(QiskitTestCase): From 655088434f0bdc61bc666ee3ee7bbc9374804dd2 Mon Sep 17 00:00:00 2001 From: Eli Arbel <46826214+eliarbel@users.noreply.github.com> Date: Thu, 13 Feb 2025 18:08:52 +0200 Subject: [PATCH 32/48] Remove pulse from GenericBackendV2 (#13829) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Remove pulse from GenericBackendV2 This commit removes pulse-related functionality from GenericBackendV2, as part of Pulse removal in Qiskit 2.0. This includes the ability to initialize the backend with custom calibrations and query it for channel information. Also, various clean ups where made to accommodate for the updated API of GenericBackendV2. * Update releasenotes/notes/remove-pulse-generic-backendv2-738ad9f7ab64b8fd.yaml Co-authored-by: Elena Peña Tapia <57907331+ElePT@users.noreply.github.com> --------- Co-authored-by: Elena Peña Tapia <57907331+ElePT@users.noreply.github.com> --- .../fake_provider/generic_backend_v2.py | 708 +----------------- qiskit/pulse/builder.py | 78 +- ...se-generic-backendv2-738ad9f7ab64b8fd.yaml | 6 + test/python/circuit/test_scheduled_circuit.py | 22 +- test/python/compiler/test_transpiler.py | 117 +-- .../python/primitives/test_backend_sampler.py | 26 +- .../primitives/test_backend_sampler_v2.py | 5 +- .../fake_provider/test_generic_backend_v2.py | 43 +- test/python/providers/test_backend_v2.py | 49 +- test/python/providers/test_pulse_defaults.py | 73 -- test/python/pulse/test_builder_v2.py | 324 -------- test/python/pulse/test_macros.py | 256 ------- .../transpiler/test_instruction_durations.py | 3 +- .../transpiler/test_passmanager_config.py | 14 +- test/python/transpiler/test_sabre_swap.py | 18 +- .../python/transpiler/test_stochastic_swap.py | 9 +- test/python/transpiler/test_target.py | 285 ------- test/python/transpiler/test_vf2_layout.py | 3 +- 18 files changed, 94 insertions(+), 1945 deletions(-) create mode 100644 releasenotes/notes/remove-pulse-generic-backendv2-738ad9f7ab64b8fd.yaml delete mode 100644 test/python/providers/test_pulse_defaults.py delete mode 100644 test/python/pulse/test_builder_v2.py delete mode 100644 test/python/pulse/test_macros.py diff --git a/qiskit/providers/fake_provider/generic_backend_v2.py b/qiskit/providers/fake_provider/generic_backend_v2.py index 163c8abfdc38..28a1061a9499 100644 --- a/qiskit/providers/fake_provider/generic_backend_v2.py +++ b/qiskit/providers/fake_provider/generic_backend_v2.py @@ -15,12 +15,8 @@ from __future__ import annotations import warnings -from collections.abc import Iterable -from typing import List, Dict, Any, Union import numpy as np -from qiskit import pulse -from qiskit.pulse.instruction_schedule_map import InstructionScheduleMap from qiskit.circuit import QuantumCircuit, Instruction from qiskit.circuit.controlflow import ( IfElseOp, @@ -37,12 +33,6 @@ from qiskit.providers.basic_provider import BasicSimulator from qiskit.providers.backend import BackendV2 from qiskit.utils import optionals as _optionals -from qiskit.providers.models.pulsedefaults import Command -from qiskit.qobj.converters.pulse_instruction import QobjToInstructionConverter -from qiskit.pulse.calibration_entries import PulseQobjDef -from qiskit.providers.models.pulsedefaults import MeasurementKernel, Discriminator -from qiskit.qobj.pulse_qobj import QobjMeasurementOption -from qiskit.utils.deprecate_pulse import deprecate_pulse_dependency, deprecate_pulse_arg # Noise default values/ranges for duration and error of supported # instructions. There are two possible formats: @@ -77,440 +67,13 @@ } -class PulseDefaults: - """Internal - Description of default settings for Pulse systems. These are instructions - or settings that - may be good starting points for the Pulse user. The user may modify these defaults for custom - scheduling. - """ - - # Copy from the deprecated from qiskit.providers.models.pulsedefaults.PulseDefaults - - _data = {} - - def __init__( - self, - qubit_freq_est: List[float], - meas_freq_est: List[float], - buffer: int, - pulse_library: List[PulseLibraryItem], - cmd_def: List[Command], - meas_kernel: MeasurementKernel = None, - discriminator: Discriminator = None, - **kwargs: Dict[str, Any], - ): - """ - Validate and reformat transport layer inputs to initialize. - Args: - qubit_freq_est: Estimated qubit frequencies in GHz. - meas_freq_est: Estimated measurement cavity frequencies in GHz. - buffer: Default buffer time (in units of dt) between pulses. - pulse_library: Pulse name and sample definitions. - cmd_def: Operation name and definition in terms of Commands. - meas_kernel: The measurement kernels - discriminator: The discriminators - **kwargs: Other attributes for the super class. - """ - self._data = {} - self.buffer = buffer - self.qubit_freq_est = [freq * 1e9 for freq in qubit_freq_est] - """Qubit frequencies in Hertz.""" - self.meas_freq_est = [freq * 1e9 for freq in meas_freq_est] - """Measurement frequencies in Hertz.""" - self.pulse_library = pulse_library - self.cmd_def = cmd_def - self.instruction_schedule_map = InstructionScheduleMap() - self.converter = QobjToInstructionConverter(pulse_library) - - for inst in cmd_def: - entry = PulseQobjDef(converter=self.converter, name=inst.name) - entry.define(inst.sequence, user_provided=False) - self.instruction_schedule_map._add( - instruction_name=inst.name, - qubits=tuple(inst.qubits), - entry=entry, - ) - - if meas_kernel is not None: - self.meas_kernel = meas_kernel - if discriminator is not None: - self.discriminator = discriminator - - self._data.update(kwargs) - - def __getattr__(self, name): - try: - return self._data[name] - except KeyError as ex: - raise AttributeError(f"Attribute {name} is not defined") from ex - - def to_dict(self): - """Return a dictionary format representation of the PulseDefaults. - Returns: - dict: The dictionary form of the PulseDefaults. - """ - out_dict = { - "qubit_freq_est": self.qubit_freq_est, - "meas_freq_est": self.qubit_freq_est, - "buffer": self.buffer, - "pulse_library": [x.to_dict() for x in self.pulse_library], - "cmd_def": [x.to_dict() for x in self.cmd_def], - } - if hasattr(self, "meas_kernel"): - out_dict["meas_kernel"] = self.meas_kernel.to_dict() - if hasattr(self, "discriminator"): - out_dict["discriminator"] = self.discriminator.to_dict() - for key, value in self.__dict__.items(): - if key not in [ - "qubit_freq_est", - "meas_freq_est", - "buffer", - "pulse_library", - "cmd_def", - "meas_kernel", - "discriminator", - "converter", - "instruction_schedule_map", - ]: - out_dict[key] = value - out_dict.update(self._data) - - out_dict["qubit_freq_est"] = [freq * 1e-9 for freq in self.qubit_freq_est] - out_dict["meas_freq_est"] = [freq * 1e-9 for freq in self.meas_freq_est] - return out_dict - - @classmethod - def from_dict(cls, data): - """Create a new PulseDefaults object from a dictionary. - - Args: - data (dict): A dictionary representing the PulseDefaults - to create. It will be in the same format as output by - :meth:`to_dict`. - Returns: - PulseDefaults: The PulseDefaults from the input dictionary. - """ - schema = { - "pulse_library": PulseLibraryItem, # The class PulseLibraryItem is deprecated - "cmd_def": Command, - "meas_kernel": MeasurementKernel, - "discriminator": Discriminator, - } - - # Pulse defaults data is nested dictionary. - # To avoid deepcopy and avoid mutating the source object, create new dict here. - in_data = {} - for key, value in data.items(): - if key in schema: - with warnings.catch_warnings(): - # The class PulseLibraryItem is deprecated - warnings.filterwarnings("ignore", category=DeprecationWarning, module="qiskit") - if isinstance(value, list): - in_data[key] = list(map(schema[key].from_dict, value)) - else: - in_data[key] = schema[key].from_dict(value) - else: - in_data[key] = value - - return cls(**in_data) - - def __str__(self): - qubit_freqs = [freq / 1e9 for freq in self.qubit_freq_est] - meas_freqs = [freq / 1e9 for freq in self.meas_freq_est] - qfreq = f"Qubit Frequencies [GHz]\n{qubit_freqs}" - mfreq = f"Measurement Frequencies [GHz]\n{meas_freqs} " - return f"<{self.__class__.__name__}({str(self.instruction_schedule_map)}{qfreq}\n{mfreq})>" - - -def _to_complex(value: Union[List[float], complex]) -> complex: - """Convert the input value to type ``complex``. - Args: - value: Value to be converted. - Returns: - Input value in ``complex``. - Raises: - TypeError: If the input value is not in the expected format. - """ - if isinstance(value, list) and len(value) == 2: - return complex(value[0], value[1]) - elif isinstance(value, complex): - return value - - raise TypeError(f"{value} is not in a valid complex number format.") - - -class PulseLibraryItem: - """INTERNAL - An item in a pulse library.""" - - # Copy from the deprecated from qiskit.qobj.PulseLibraryItem - def __init__(self, name, samples): - """Instantiate a pulse library item. - - Args: - name (str): A name for the pulse. - samples (list[complex]): A list of complex values defining pulse - shape. - """ - self.name = name - if isinstance(samples[0], list): - self.samples = np.array([complex(sample[0], sample[1]) for sample in samples]) - else: - self.samples = samples - - def to_dict(self): - """Return a dictionary format representation of the pulse library item. - - Returns: - dict: The dictionary form of the PulseLibraryItem. - """ - return {"name": self.name, "samples": self.samples} - - @classmethod - def from_dict(cls, data): - """Create a new PulseLibraryItem object from a dictionary. - - Args: - data (dict): A dictionary for the experiment config - - Returns: - PulseLibraryItem: The object from the input dictionary. - """ - return cls(**data) - - def __repr__(self): - return f"PulseLibraryItem({self.name}, {repr(self.samples)})" - - def __str__(self): - return f"Pulse Library Item:\n\tname: {self.name}\n\tsamples: {self.samples}" - - def __eq__(self, other): - if isinstance(other, PulseLibraryItem): - if self.to_dict() == other.to_dict(): - return True - return False - - -class PulseQobjInstruction: - """Internal - A class representing a single instruction in a PulseQobj Experiment.""" - - # Copy from the deprecated from qiskit.qobj.PulseQobjInstruction - - _COMMON_ATTRS = [ - "ch", - "conditional", - "val", - "phase", - "frequency", - "duration", - "qubits", - "memory_slot", - "register_slot", - "label", - "type", - "pulse_shape", - "parameters", - ] - - def __init__( - self, - name, - t0, - ch=None, - conditional=None, - val=None, - phase=None, - duration=None, - qubits=None, - memory_slot=None, - register_slot=None, - kernels=None, - discriminators=None, - label=None, - type=None, # pylint: disable=invalid-name,redefined-builtin - pulse_shape=None, - parameters=None, - frequency=None, - ): - """Instantiate a new PulseQobjInstruction object. - - Args: - name (str): The name of the instruction - t0 (int): Pulse start time in integer **dt** units. - ch (str): The channel to apply the pulse instruction. - conditional (int): The register to use for a conditional for this - instruction - val (complex): Complex value to apply, bounded by an absolute value - of 1. - phase (float): if a ``fc`` instruction, the frame change phase in - radians. - frequency (float): if a ``sf`` instruction, the frequency in Hz. - duration (int): The duration of the pulse in **dt** units. - qubits (list): A list of ``int`` representing the qubits the - instruction operates on - memory_slot (list): If a ``measure`` instruction this is a list - of ``int`` containing the list of memory slots to store the - measurement results in (must be the same length as qubits). - If a ``bfunc`` instruction this is a single ``int`` of the - memory slot to store the boolean function result in. - register_slot (list): If a ``measure`` instruction this is a list - of ``int`` containing the list of register slots in which to - store the measurement results (must be the same length as - qubits). If a ``bfunc`` instruction this is a single ``int`` - of the register slot in which to store the result. - kernels (list): List of :class:`QobjMeasurementOption` objects - defining the measurement kernels and set of parameters if the - measurement level is 1 or 2. Only used for ``acquire`` - instructions. - discriminators (list): A list of :class:`QobjMeasurementOption` - used to set the discriminators to be used if the measurement - level is 2. Only used for ``acquire`` instructions. - label (str): Label of instruction - type (str): Type of instruction - pulse_shape (str): The shape of the parametric pulse - parameters (dict): The parameters for a parametric pulse - """ - self.name = name - self.t0 = t0 - if ch is not None: - self.ch = ch - if conditional is not None: - self.conditional = conditional - if val is not None: - self.val = val - if phase is not None: - self.phase = phase - if frequency is not None: - self.frequency = frequency - if duration is not None: - self.duration = duration - if qubits is not None: - self.qubits = qubits - if memory_slot is not None: - self.memory_slot = memory_slot - if register_slot is not None: - self.register_slot = register_slot - if kernels is not None: - self.kernels = kernels - if discriminators is not None: - self.discriminators = discriminators - if label is not None: - self.label = label - if type is not None: - self.type = type - if pulse_shape is not None: - self.pulse_shape = pulse_shape - if parameters is not None: - self.parameters = parameters - - def to_dict(self): - """Return a dictionary format representation of the Instruction. - - Returns: - dict: The dictionary form of the PulseQobjInstruction. - """ - out_dict = {"name": self.name, "t0": self.t0} - for attr in self._COMMON_ATTRS: - if hasattr(self, attr): - out_dict[attr] = getattr(self, attr) - if hasattr(self, "kernels"): - out_dict["kernels"] = [x.to_dict() for x in self.kernels] - if hasattr(self, "discriminators"): - out_dict["discriminators"] = [x.to_dict() for x in self.discriminators] - return out_dict - - def __repr__(self): - out = f'PulseQobjInstruction(name="{self.name}", t0={self.t0}' - for attr in self._COMMON_ATTRS: - attr_val = getattr(self, attr, None) - if attr_val is not None: - if isinstance(attr_val, str): - out += f', {attr}="{attr_val}"' - else: - out += f", {attr}={attr_val}" - out += ")" - return out - - def __str__(self): - out = f"Instruction: {self.name}\n" - out += f"\t\tt0: {self.t0}\n" - for attr in self._COMMON_ATTRS: - if hasattr(self, attr): - out += f"\t\t{attr}: {getattr(self, attr)}\n" - return out - - @classmethod - def from_dict(cls, data): - """Create a new PulseQobjExperimentConfig object from a dictionary. - - Args: - data (dict): A dictionary for the experiment config - - Returns: - PulseQobjInstruction: The object from the input dictionary. - """ - schema = { - "discriminators": QobjMeasurementOption, - "kernels": QobjMeasurementOption, - } - skip = ["t0", "name"] - - # Pulse instruction data is nested dictionary. - # To avoid deepcopy and avoid mutating the source object, create new dict here. - in_data = {} - for key, value in data.items(): - if key in skip: - continue - if key == "parameters": - # This is flat dictionary of parametric pulse parameters - formatted_value = value.copy() - if "amp" in formatted_value: - formatted_value["amp"] = _to_complex(formatted_value["amp"]) - in_data[key] = formatted_value - continue - if key in schema: - if isinstance(value, list): - in_data[key] = list(map(schema[key].from_dict, value)) - else: - in_data[key] = schema[key].from_dict(value) - else: - in_data[key] = value - - return cls(data["name"], data["t0"], **in_data) - - def __eq__(self, other): - if isinstance(other, PulseQobjInstruction): - if self.to_dict() == other.to_dict(): - return True - return False - - -def _pulse_library(): - # The number of samples determines the pulse durations of the corresponding - # instructions. This default defines pulses with durations in multiples of - # 16 dt for consistency with the pulse granularity of real IBM devices, but - # keeps the number smaller than what would be realistic for - # manageability. If needed, more realistic durations could be added in the - # future (order of 160dt for 1q gates, 1760dt for 2q gates and measure). - return [ - PulseLibraryItem( - name="pulse_1", samples=np.linspace(0, 1.0, 16, dtype=np.complex128) - ), # 16dt - PulseLibraryItem( - name="pulse_2", samples=np.linspace(0, 1.0, 32, dtype=np.complex128) - ), # 32dt - PulseLibraryItem( - name="pulse_3", samples=np.linspace(0, 1.0, 64, dtype=np.complex128) - ), # 64dt - ] - - class GenericBackendV2(BackendV2): """Generic :class:`~.BackendV2` implementation with a configurable constructor. This class will return a :class:`~.BackendV2` instance that runs on a local simulator (in the spirit of fake backends) and contains all the necessary information to test backend-interfacing components, such as the transpiler. A :class:`.GenericBackendV2` instance can be constructed from as little as a specified ``num_qubits``, but users can additionally configure the basis gates, coupling map, - ability to run dynamic circuits (control flow instructions), instruction calibrations and dtm. + ability to run dynamic circuits (control flow instructions) and dtm. The remainder of the backend properties are generated by randomly sampling from default ranges extracted from historical IBM backend data. The seed for this random generation can be fixed to ensure the reproducibility of the backend output. @@ -519,8 +82,6 @@ class GenericBackendV2(BackendV2): transpilation. """ - @deprecate_pulse_arg("pulse_channels") - @deprecate_pulse_arg("calibrate_instructions") def __init__( self, num_qubits: int, @@ -528,11 +89,9 @@ def __init__( *, coupling_map: list[list[int]] | CouplingMap | None = None, control_flow: bool = False, - calibrate_instructions: bool | InstructionScheduleMap | None = None, dtm: float | None = None, dt: float | None = None, seed: int | None = None, - pulse_channels: bool = True, noise_info: bool = True, ): """ @@ -564,19 +123,6 @@ def __init__( control_flow: Flag to enable control flow directives on the target (defaults to False). - calibrate_instructions: DEPRECATED. Instruction calibration settings, this argument - supports both boolean and :class:`.InstructionScheduleMap` as - input types, and is ``None`` by default: - - #. If ``calibrate_instructions==None``, no calibrations will be added to the target. - #. If ``calibrate_instructions==True``, all gates will be calibrated for all - qubits using the default pulse schedules generated internally. - #. If ``calibrate_instructions==False``, all gates will be "calibrated" for - all qubits with an empty pulse schedule. - #. If an :class:`.InstructionScheduleMap` instance is given, the calibrations - in this instruction schedule map will be appended to the target - instead of the default pulse schedules (this allows for custom calibrations). - dtm: System time resolution of output signals in nanoseconds. None by default. @@ -585,8 +131,6 @@ def __init__( seed: Optional seed for generation of default values. - pulse_channels: DEPRECATED. If true, sets default pulse channel information on the backend. - noise_info: If true, associates gates and qubits with default noise information. """ @@ -603,13 +147,9 @@ def __init__( self._dt = dt self._num_qubits = num_qubits self._control_flow = control_flow - self._calibrate_instructions = calibrate_instructions self._supported_gates = get_standard_gate_name_mapping() self._noise_info = noise_info - if calibrate_instructions and not noise_info: - raise QiskitError("Must set parameter noise_info when calibrating instructions.") - if coupling_map is None: self._coupling_map = CouplingMap().from_full(num_qubits) else: @@ -632,10 +172,6 @@ def __init__( self._basis_gates.append(name) self._build_generic_target() - if pulse_channels: - self._build_default_channels() - else: - self.channels_map = {} @property def target(self): @@ -655,20 +191,6 @@ def dtm(self) -> float: def meas_map(self) -> list[list[int]]: return self._target.concurrent_measurements - def _build_default_channels(self) -> None: - with warnings.catch_warnings(): - warnings.simplefilter(action="ignore", category=DeprecationWarning) - # Prevent pulse deprecation warnings from being emitted - channels_map = { - "acquire": {(i,): [pulse.AcquireChannel(i)] for i in range(self.num_qubits)}, - "drive": {(i,): [pulse.DriveChannel(i)] for i in range(self.num_qubits)}, - "measure": {(i,): [pulse.MeasureChannel(i)] for i in range(self.num_qubits)}, - "control": { - (edge): [pulse.ControlChannel(i)] for i, edge in enumerate(self._coupling_map) - }, - } - setattr(self, "channels_map", channels_map) - def _get_noise_defaults(self, name: str, num_qubits: int) -> tuple: """Return noise default values/ranges for duration and error of supported instructions. There are two possible formats: @@ -682,110 +204,9 @@ def _get_noise_defaults(self, name: str, num_qubits: int) -> tuple: return _NOISE_DEFAULTS_FALLBACK["1-q"] return _NOISE_DEFAULTS_FALLBACK["multi-q"] - def _get_calibration_sequence( - self, inst: str, num_qubits: int, qargs: tuple[int] - ) -> list[PulseQobjInstruction]: - """Return calibration pulse sequence for given instruction (defined by name and num_qubits) - acting on qargs. - """ - - pulse_library = _pulse_library() - # Note that the calibration pulses are different for - # 1q gates vs 2q gates vs measurement instructions. - if inst == "measure": - with warnings.catch_warnings(): - # The class PulseQobjInstruction is deprecated - warnings.filterwarnings("ignore", category=DeprecationWarning, module="qiskit") - sequence = [ - PulseQobjInstruction( - name="acquire", - duration=1792, - t0=0, - qubits=qargs, - memory_slot=qargs, - ) - ] + [ - PulseQobjInstruction(name=pulse_library[1].name, ch=f"m{i}", t0=0) - for i in qargs - ] - return sequence - with warnings.catch_warnings(): - # The class PulseQobjInstruction is deprecated - warnings.filterwarnings("ignore", category=DeprecationWarning, module="qiskit") - if num_qubits == 1: - return [ - PulseQobjInstruction(name="fc", ch=f"u{qargs[0]}", t0=0, phase="-P0"), - PulseQobjInstruction(name=pulse_library[0].name, ch=f"d{qargs[0]}", t0=0), - ] - return [ - PulseQobjInstruction(name=pulse_library[1].name, ch=f"d{qargs[0]}", t0=0), - PulseQobjInstruction(name=pulse_library[2].name, ch=f"u{qargs[0]}", t0=0), - PulseQobjInstruction(name=pulse_library[1].name, ch=f"d{qargs[1]}", t0=0), - PulseQobjInstruction(name="fc", ch=f"d{qargs[1]}", t0=0, phase=2.1), - ] - - def _generate_calibration_defaults(self) -> PulseDefaults: - """Generate pulse calibration defaults as specified with `self._calibrate_instructions`. - If `self._calibrate_instructions` is True, the pulse schedules will be generated from - a series of default calibration sequences. If `self._calibrate_instructions` is False, - the pulse schedules will contain empty calibration sequences, but still be generated and - added to the target. - """ - - # If self._calibrate_instructions==True, this method - # will generate default pulse schedules for all gates in self._basis_gates, - # except for `delay` and `reset`. - calibration_buffer = self._basis_gates.copy() - for inst in ["delay", "reset"]: - calibration_buffer.remove(inst) - - # List of calibration commands (generated from sequences of PulseQobjInstructions) - # corresponding to each calibrated instruction. Note that the calibration pulses - # are different for 1q gates vs 2q gates vs measurement instructions. - cmd_def = [] - for inst in calibration_buffer: - num_qubits = self._supported_gates[inst].num_qubits - qarg_set = self._coupling_map if num_qubits > 1 else list(range(self.num_qubits)) - if inst == "measure": - cmd_def.append( - Command( - name=inst, - qubits=qarg_set, - sequence=( - self._get_calibration_sequence(inst, num_qubits, qarg_set) - if self._calibrate_instructions - else [] - ), - ) - ) - else: - for qarg in qarg_set: - qubits = [qarg] if num_qubits == 1 else qarg - cmd_def.append( - Command( - name=inst, - qubits=qubits, - sequence=( - self._get_calibration_sequence(inst, num_qubits, qubits) - if self._calibrate_instructions - else [] - ), - ) - ) - - qubit_freq_est = np.random.normal(4.8, scale=0.01, size=self.num_qubits).tolist() - meas_freq_est = np.linspace(6.4, 6.6, self.num_qubits).tolist() - return PulseDefaults( - qubit_freq_est=qubit_freq_est, - meas_freq_est=meas_freq_est, - buffer=0, - pulse_library=_pulse_library(), - cmd_def=cmd_def, - ) - def _build_generic_target(self): """This method generates a :class:`~.Target` instance with - default qubit, instruction and calibration properties. + default qubit and instruction properties. """ # the qubit properties are sampled from default ranges properties = _QUBIT_PROPERTIES @@ -815,17 +236,8 @@ def _build_generic_target(self): concurrent_measurements=[list(range(self._num_qubits))], ) - # Generate instruction schedule map with calibrations to add to target. - calibration_inst_map = None - if self._calibrate_instructions is not None: - if isinstance(self._calibrate_instructions, InstructionScheduleMap): - calibration_inst_map = self._calibrate_instructions - else: - defaults = self._generate_calibration_defaults() - calibration_inst_map = defaults.instruction_schedule_map - # Iterate over gates, generate noise params from defaults, - # and add instructions, noise and calibrations to target. + # and add instructions and noise information to the target. for name in self._basis_gates: if name not in self._supported_gates: raise QiskitError( @@ -840,7 +252,7 @@ def _build_generic_target(self): ) if self._noise_info: noise_params = self._get_noise_defaults(name, gate.num_qubits) - self._add_noisy_instruction_to_target(gate, noise_params, calibration_inst_map) + self._add_noisy_instruction_to_target(gate, noise_params) else: qarg_set = self._coupling_map if gate.num_qubits > 1 else range(self.num_qubits) props = {(qarg,) if isinstance(qarg, int) else qarg: None for qarg in qarg_set} @@ -858,7 +270,6 @@ def _add_noisy_instruction_to_target( self, instruction: Instruction, noise_params: tuple[float, ...] | None, - calibration_inst_map: InstructionScheduleMap | None, ) -> None: """Add instruction properties to target for specified instruction. @@ -866,7 +277,6 @@ def _add_noisy_instruction_to_target( instruction: Instance of instruction to be added to the target noise_params: Error and duration noise values/ranges to include in instruction properties. - calibration_inst_map: Instruction schedule map with calibration defaults """ qarg_set = self._coupling_map if instruction.num_qubits > 1 else range(self.num_qubits) props = {} @@ -883,46 +293,16 @@ def _add_noisy_instruction_to_target( self._rng.uniform(*noise_params[2:]), ) ) - with warnings.catch_warnings(): - warnings.simplefilter(action="ignore", category=DeprecationWarning) - # Prevent pulse deprecations from being emitted - if ( - calibration_inst_map is not None - and instruction.name not in ["reset", "delay"] - and qarg in calibration_inst_map.qubits_with_instruction(instruction.name) - ): - # Do NOT call .get method. This parses Qobj immediately. - # This operation is computationally expensive and should be bypassed. - calibration_entry = calibration_inst_map._get_calibration_entry( - instruction.name, qargs - ) - else: - calibration_entry = None - if duration is not None and len(noise_params) > 2: - # Ensure exact conversion of duration from seconds to dt - dt = _QUBIT_PROPERTIES["dt"] - rounded_duration = round(duration / dt) * dt - # Clamp rounded duration to be between min and max values - duration = max(noise_params[0], min(rounded_duration, noise_params[1])) - props.update({qargs: InstructionProperties(duration, error, calibration_entry)}) - self._target.add_instruction(instruction, props) - # The "measure" instruction calibrations need to be added qubit by qubit, once the - # instruction has been added to the target. - if calibration_inst_map is not None and instruction.name == "measure": - for qarg in calibration_inst_map.qubits_with_instruction(instruction.name): - try: - qargs = tuple(qarg) - except TypeError: - qargs = (qarg,) - # Do NOT call .get method. This parses Qobj immediately. - # This operation is computationally expensive and should be bypassed. - calibration_entry = calibration_inst_map._get_calibration_entry( - instruction.name, qargs - ) - for qubit in qargs: - if qubit < self.num_qubits: - self._target[instruction.name][(qubit,)].calibration = calibration_entry + if duration is not None and len(noise_params) > 2: + # Ensure exact conversion of duration from seconds to dt + dt = _QUBIT_PROPERTIES["dt"] + rounded_duration = round(duration / dt) * dt + # Clamp rounded duration to be between min and max values + duration = max(noise_params[0], min(rounded_duration, noise_params[1])) + props.update({qargs: InstructionProperties(duration, error, None)}) + + self._target.add_instruction(instruction, props) def run(self, run_input, **options): """Run on the backend using a simulator. @@ -939,11 +319,9 @@ def run(self, run_input, **options): Noisy simulations of pulse jobs are not yet supported in :class:`~.GenericBackendV2`. Args: - run_input (QuantumCircuit or Schedule or ScheduleBlock or list): An - individual or a list of - :class:`~qiskit.circuit.QuantumCircuit`, - :class:`~qiskit.pulse.ScheduleBlock`, or - :class:`~qiskit.pulse.Schedule` objects to run on the backend. + run_input (QuantumCircuit or list): An + individual or a list of :class:`~qiskit.circuit.QuantumCircuit` + objects to run on the backend. options: Any kwarg options to pass to the backend for running the config. If a key is also present in the options attribute/object, then the expectation is that the value @@ -957,25 +335,15 @@ def run(self, run_input, **options): QiskitError: If a pulse job is supplied and qiskit_aer is not installed. """ circuits = run_input - pulse_job = None - if isinstance(circuits, (pulse.Schedule, pulse.ScheduleBlock)): - pulse_job = True - elif isinstance(circuits, QuantumCircuit): - pulse_job = False - elif isinstance(circuits, list): - if circuits: - if all(isinstance(x, (pulse.Schedule, pulse.ScheduleBlock)) for x in circuits): - pulse_job = True - elif all(isinstance(x, QuantumCircuit) for x in circuits): - pulse_job = False - if pulse_job is None: # submitted job is invalid + if not isinstance(circuits, QuantumCircuit) and ( + not isinstance(circuits, list) + or not all(isinstance(x, QuantumCircuit) for x in circuits) + ): raise QiskitError( f"Invalid input object {circuits}, must be either a " - "QuantumCircuit, Schedule, or a list of either" + "QuantumCircuit or a list of QuantumCircuit objects" ) - if pulse_job: # pulse job - raise QiskitError("Pulse simulation is currently not supported for V2 backends.") - # circuit job + if not _optionals.HAS_AER: warnings.warn("Aer not found using BasicSimulator and no noise", RuntimeWarning) if self._sim is None: @@ -1006,35 +374,3 @@ def _default_options(cls) -> Options: return AerSimulator._default_options() else: return BasicSimulator._default_options() - - @deprecate_pulse_dependency - def drive_channel(self, qubit: int): - drive_channels_map = getattr(self, "channels_map", {}).get("drive", {}) - qubits = (qubit,) - if qubits in drive_channels_map: - return drive_channels_map[qubits][0] - return None - - @deprecate_pulse_dependency - def measure_channel(self, qubit: int): - measure_channels_map = getattr(self, "channels_map", {}).get("measure", {}) - qubits = (qubit,) - if qubits in measure_channels_map: - return measure_channels_map[qubits][0] - return None - - @deprecate_pulse_dependency - def acquire_channel(self, qubit: int): - acquire_channels_map = getattr(self, "channels_map", {}).get("acquire", {}) - qubits = (qubit,) - if qubits in acquire_channels_map: - return acquire_channels_map[qubits][0] - return None - - @deprecate_pulse_dependency - def control_channel(self, qubits: Iterable[int]): - control_channels_map = getattr(self, "channels_map", {}).get("control", {}) - qubits = tuple(qubits) - if qubits in control_channels_map: - return control_channels_map[qubits] - return [] diff --git a/qiskit/pulse/builder.py b/qiskit/pulse/builder.py index 70ecc9d11dfa..5d6c5fbd2d41 100644 --- a/qiskit/pulse/builder.py +++ b/qiskit/pulse/builder.py @@ -104,14 +104,12 @@ Methods to return the correct channels for the respective qubit indices. -.. plot:: - :include-source: - :nofigs: +.. code-block:: python from qiskit import pulse from qiskit.providers.fake_provider import GenericBackendV2 - backend = GenericBackendV2(num_qubits=2, calibrate_instructions=True) + backend = GenericBackendV2(num_qubits=2) with pulse.build(backend) as drive_sched: d0 = pulse.drive_channel(0) @@ -132,14 +130,12 @@ Pulse instructions are available within the builder interface. Here's an example: -.. plot:: - :alt: Output from the previous code. - :include-source: +.. code-block:: python from qiskit import pulse from qiskit.providers.fake_provider import GenericBackendV2 - backend = GenericBackendV2(num_qubits=2, calibrate_instructions=True) + backend = GenericBackendV2(num_qubits=2) with pulse.build(backend) as drive_sched: d0 = pulse.drive_channel(0) @@ -181,9 +177,7 @@ example an alignment context like :func:`align_right` may be used to align all pulses as late as possible in a pulse program. -.. plot:: - :alt: Output from the previous code. - :include-source: +.. code-block:: python from qiskit import pulse @@ -213,14 +207,12 @@ Macros help you add more complex functionality to your pulse program. -.. plot:: - :include-source: - :nofigs: +.. code-block:: python from qiskit import pulse from qiskit.providers.fake_provider import GenericBackendV2 - backend = GenericBackendV2(num_qubits=2, calibrate_instructions=True) + backend = GenericBackendV2(num_qubits=2) with pulse.build(backend) as measure_sched: mem_slot = pulse.measure(0) @@ -241,15 +233,13 @@ The utility functions can be used to gather attributes about the backend and modify how the program is built. -.. plot:: - :include-source: - :nofigs: +.. code-block:: python from qiskit import pulse from qiskit.providers.fake_provider import GenericBackendV2 - backend = GenericBackendV2(num_qubits=2, calibrate_instructions=True) + backend = GenericBackendV2(num_qubits=2) with pulse.build(backend) as u3_sched: print('Number of qubits in backend: {}'.format(pulse.num_qubits())) @@ -651,10 +641,7 @@ def build( To enter a building context and starting building a pulse program: - .. plot:: - :include-source: - :nofigs: - :context: reset + .. code-block:: python from qiskit import transpile, pulse from qiskit.providers.fake_provider import FakeOpenPulse2Q @@ -750,9 +737,7 @@ def append_instruction(instruction: instructions.Instruction): Examples: - .. plot:: - :include-source: - :nofigs: + .. code-block:: python from qiskit import pulse @@ -1663,15 +1648,12 @@ def call( 1. Calling a schedule block (recommended) - .. plot:: - :include-source: - :nofigs: - :context: reset + .. code-block:: python from qiskit import circuit, pulse from qiskit.providers.fake_provider import GenericBackendV2 - backend = GenericBackendV2(num_qubits=5, calibrate_instructions=True) + backend = GenericBackendV2(num_qubits=5) with pulse.build() as x_sched: pulse.play(pulse.Gaussian(160, 0.1, 40), pulse.DriveChannel(0)) @@ -1698,10 +1680,7 @@ def call( The actual program is stored in the reference table attached to the schedule. - .. plot:: - :include-source: - :nofigs: - :context: + .. code-block:: python print(pulse_prog.references) @@ -1712,10 +1691,7 @@ def call( In addition, you can call a parameterized target program with parameter assignment. - .. plot:: - :include-source: - :nofigs: - :context: + .. code-block:: python amp = circuit.Parameter("amp") @@ -1754,10 +1730,7 @@ def call( If there is a name collision between parameters, you can distinguish them by specifying each parameter object in a python dictionary. For example, - .. plot:: - :include-source: - :nofigs: - :context: + .. code-block:: python amp1 = circuit.Parameter('amp') amp2 = circuit.Parameter('amp') @@ -1786,10 +1759,7 @@ def call( 2. Calling a schedule - .. plot:: - :include-source: - :nofigs: - :context: + .. code-block:: python x_sched = backend.instruction_schedule_map.get("x", (0,)) @@ -1927,9 +1897,7 @@ def barrier(*channels_or_qubits: chans.Channel | int, name: str | None = None): in the case where we are calling an outside circuit or schedule and want to align a pulse at the end of one call: - .. plot:: - :include-source: - :nofigs: + .. code-block:: python import math from qiskit import pulse @@ -2040,10 +2008,7 @@ def measure( To use the measurement it is as simple as specifying the qubit you wish to measure: - .. plot:: - :include-source: - :nofigs: - :context: reset + .. code-block:: python from qiskit import pulse from qiskit.providers.fake_provider import FakeOpenPulse2Q @@ -2063,10 +2028,7 @@ def measure( future we will support using this handle to a result register to build up ones program. It is also possible to supply this register: - .. plot:: - :include-source: - :nofigs: - :context: + .. code-block:: python with pulse.build(backend) as pulse_prog: pulse.play(pulse.Constant(100, 1.0), qubit_drive_chan) diff --git a/releasenotes/notes/remove-pulse-generic-backendv2-738ad9f7ab64b8fd.yaml b/releasenotes/notes/remove-pulse-generic-backendv2-738ad9f7ab64b8fd.yaml new file mode 100644 index 000000000000..d6fa07c823df --- /dev/null +++ b/releasenotes/notes/remove-pulse-generic-backendv2-738ad9f7ab64b8fd.yaml @@ -0,0 +1,6 @@ +--- +upgrade_providers: + - | + As part of Pulse removal in Qiskit 2.0, pulse support has been removed from + :class:`.GenericBackendV2`. This includes the ability to initialize the backend + with custom calibrations (``calibrate_instructions`` argument) and pulse channels information. diff --git a/test/python/circuit/test_scheduled_circuit.py b/test/python/circuit/test_scheduled_circuit.py index 45238dc79e74..f72b6d2adb23 100644 --- a/test/python/circuit/test_scheduled_circuit.py +++ b/test/python/circuit/test_scheduled_circuit.py @@ -57,8 +57,7 @@ def test_schedule_circuit_when_backend_tells_dt(self): qc.h(0) # 195[dt] qc.h(1) # 210[dt] - with self.assertWarns(DeprecationWarning): - backend = GenericBackendV2(2, calibrate_instructions=True, seed=42) + backend = GenericBackendV2(2, seed=42) sc = transpile(qc, backend, scheduling_method="alap", layout_method="trivial") self.assertEqual(sc.duration, 451095) @@ -382,17 +381,12 @@ def test_per_qubit_durations_loose_constrain(self): def test_per_qubit_durations(self): """Test target with custom instruction_durations""" - with self.assertWarnsRegex( - DeprecationWarning, - expected_regex="argument ``calibrate_instructions`` is deprecated", - ): - target = GenericBackendV2( - 3, - calibrate_instructions=True, - coupling_map=[[0, 1], [1, 2]], - basis_gates=["cx", "h"], - seed=42, - ).target + target = GenericBackendV2( + 3, + coupling_map=[[0, 1], [1, 2]], + basis_gates=["cx", "h"], + seed=42, + ).target target.update_instruction_properties("cx", (0, 1), InstructionProperties(0.00001)) target.update_instruction_properties("cx", (1, 2), InstructionProperties(0.00001)) target.update_instruction_properties("h", (0,), InstructionProperties(0.000002)) @@ -435,8 +429,8 @@ def test_convert_duration_to_dt(self): """Test that circuit duration unit conversion is applied only when necessary. Tests fix for bug reported in PR #11782.""" + backend = GenericBackendV2(num_qubits=3, seed=42) with self.assertWarns(DeprecationWarning): - backend = GenericBackendV2(num_qubits=3, calibrate_instructions=True, seed=42) schedule_config = ScheduleConfig( inst_map=backend.target.instruction_schedule_map(), meas_map=backend.meas_map, diff --git a/test/python/compiler/test_transpiler.py b/test/python/compiler/test_transpiler.py index 5c707c00d884..f1f691da5a89 100644 --- a/test/python/compiler/test_transpiler.py +++ b/test/python/compiler/test_transpiler.py @@ -19,7 +19,6 @@ import sys from logging import StreamHandler, getLogger from unittest.mock import patch -import warnings import numpy as np import rustworkx as rx from ddt import data, ddt, unpack @@ -82,7 +81,7 @@ from qiskit.pulse import InstructionScheduleMap from qiskit.quantum_info import Operator, random_unitary from qiskit.utils import parallel -from qiskit.transpiler import CouplingMap, Layout, PassManager, TransformationPass +from qiskit.transpiler import CouplingMap, Layout, PassManager from qiskit.transpiler.exceptions import TranspilerError, CircuitTooWideForTarget from qiskit.transpiler.passes import BarrierBeforeFinalMeasurements, GateDirection, VF2PostLayout @@ -1461,57 +1460,6 @@ def test_inst_durations_from_calibrations(self): with self.assertWarns(DeprecationWarning): self.assertEqual(out.duration, cal.duration) - @data(0, 1, 2, 3) - def test_multiqubit_gates_calibrations(self, opt_level): - """Test multiqubit gate > 2q with calibrations works - - Adapted from issue description in https://github.com/Qiskit/qiskit-terra/issues/6572 - """ - circ = QuantumCircuit(5) - custom_gate = Gate("my_custom_gate", 5, []) - circ.append(custom_gate, [0, 1, 2, 3, 4]) - circ.measure_all() - backend = GenericBackendV2(num_qubits=6) - - with self.assertWarns(DeprecationWarning): - with pulse.build(backend=backend, name="custom") as my_schedule: - pulse.play( - pulse.library.Gaussian(duration=128, amp=0.1, sigma=16), pulse.drive_channel(0) - ) - pulse.play( - pulse.library.Gaussian(duration=128, amp=0.1, sigma=16), pulse.drive_channel(1) - ) - pulse.play( - pulse.library.Gaussian(duration=128, amp=0.1, sigma=16), pulse.drive_channel(2) - ) - pulse.play( - pulse.library.Gaussian(duration=128, amp=0.1, sigma=16), pulse.drive_channel(3) - ) - pulse.play( - pulse.library.Gaussian(duration=128, amp=0.1, sigma=16), pulse.drive_channel(4) - ) - pulse.play( - pulse.library.Gaussian(duration=128, amp=0.1, sigma=16), pulse.ControlChannel(1) - ) - pulse.play( - pulse.library.Gaussian(duration=128, amp=0.1, sigma=16), pulse.ControlChannel(2) - ) - pulse.play( - pulse.library.Gaussian(duration=128, amp=0.1, sigma=16), pulse.ControlChannel(3) - ) - pulse.play( - pulse.library.Gaussian(duration=128, amp=0.1, sigma=16), pulse.ControlChannel(4) - ) - circ.add_calibration("my_custom_gate", [0, 1, 2, 3, 4], my_schedule, []) - trans_circ = transpile( - circ, - backend=backend, - optimization_level=opt_level, - layout_method="trivial", - seed_transpiler=42, - ) - self.assertEqual({"measure": 5, "my_custom_gate": 1, "barrier": 1}, trans_circ.count_ops()) - @data(0, 1, 2, 3) def test_circuit_with_delay(self, optimization_level): """Verify a circuit with delay can transpile to a scheduled circuit.""" @@ -1609,17 +1557,12 @@ def test_scheduling_instruction_constraints_backend(self): def test_scheduling_instruction_constraints(self): """Test that scheduling-related loose transpile constraints work with target.""" - with self.assertWarnsRegex( - DeprecationWarning, - expected_regex="argument ``calibrate_instructions`` is deprecated", - ): - target = GenericBackendV2( - 2, - calibrate_instructions=True, - coupling_map=[[0, 1]], - basis_gates=["cx", "h"], - seed=42, - ).target + target = GenericBackendV2( + 2, + coupling_map=[[0, 1]], + basis_gates=["cx", "h"], + seed=42, + ).target qc = QuantumCircuit(2) qc.h(0) qc.delay(0.000001, 1, "s") @@ -2790,52 +2733,6 @@ def test_parallel_dispatch(self, opt_level): self.assertTrue(math.isclose(count["00000"], 500, rel_tol=0.1)) self.assertTrue(math.isclose(count["01111"], 500, rel_tol=0.1)) - def test_parallel_dispatch_lazy_cal_loading(self): - """Test adding calibration by lazy loading in parallel environment.""" - - class TestAddCalibration(TransformationPass): - """A fake pass to test lazy pulse qobj loading in parallel environment.""" - - def __init__(self, target): - """Instantiate with target.""" - super().__init__() - self.target = target - - def run(self, dag): - """Run test pass that adds calibration of SX gate of qubit 0.""" - with warnings.catch_warnings(): - warnings.simplefilter("ignore", category=DeprecationWarning) - # DAGCircuit.add_calibration() is deprecated but we can't use self.assertWarns() here - dag.add_calibration( - "sx", - qubits=(0,), - schedule=self.target["sx"][(0,)].calibration, # PulseQobj is parsed here - ) - return dag - - # Create backend with empty calibrations (PulseQobjEntries) - with self.assertWarns(DeprecationWarning): - backend = GenericBackendV2( - num_qubits=4, - calibrate_instructions=False, - ) - - # This target has PulseQobj entries that provide a serialized schedule data - pass_ = TestAddCalibration(backend.target) - pm = PassManager(passes=[pass_]) - self.assertIsNone(backend.target["sx"][(0,)]._calibration._definition) - - qc = QuantumCircuit(1) - qc.sx(0) - qc_copied = [qc for _ in range(10)] - - qcs_cal_added = pm.run(qc_copied) - with self.assertWarns(DeprecationWarning): - ref_cal = backend.target["sx"][(0,)].calibration - for qc_test in qcs_cal_added: - added_cal = qc_test.calibrations["sx"][((0,), ())] - self.assertEqual(added_cal, ref_cal) - @data(0, 1, 2, 3) def test_parallel_singleton_conditional_gate(self, opt_level): """Test that singleton mutable instance doesn't lose state in parallel.""" diff --git a/test/python/primitives/test_backend_sampler.py b/test/python/primitives/test_backend_sampler.py index dc77a3c47ce6..6acfa6443530 100644 --- a/test/python/primitives/test_backend_sampler.py +++ b/test/python/primitives/test_backend_sampler.py @@ -368,10 +368,7 @@ def max_circuits(self): def test_primitive_job_size_limit_backend_v1(self): """Test primitive respects backend's job size limit.""" - with self.assertWarns(DeprecationWarning): - backend = GenericBackendV2( - 7, calibrate_instructions=True, basis_gates=["cx", "u1", "u2", "u3"], seed=42 - ) + backend = GenericBackendV2(7, basis_gates=["cx", "u1", "u2", "u3"], seed=42) qc = QuantumCircuit(1) qc.measure_all() qc2 = QuantumCircuit(1) @@ -410,10 +407,7 @@ def test_circuit_with_dynamic_circuit(self): def test_sequential_run(self): """Test sequential run.""" - with self.assertWarns(DeprecationWarning): - backend = GenericBackendV2( - 7, calibrate_instructions=True, basis_gates=["cx", "u1", "u2", "u3"], seed=42 - ) + backend = GenericBackendV2(7, basis_gates=["cx", "u1", "u2", "u3"], seed=42) qc = QuantumCircuit(1) qc.measure_all() qc2 = QuantumCircuit(1) @@ -461,10 +455,8 @@ def callback(msg): bound_counter = CallbackPass("bound_pass_manager", callback) bound_pass = PassManager(bound_counter) + backend = GenericBackendV2(7, basis_gates=["cx", "u1", "u2", "u3"], seed=42) with self.assertWarns(DeprecationWarning): - backend = GenericBackendV2( - 7, calibrate_instructions=True, basis_gates=["cx", "u1", "u2", "u3"], seed=42 - ) sampler = BackendSampler(backend=backend, bound_pass_manager=bound_pass) _ = sampler.run([self._circuit[0]]).result() expected = [ @@ -485,13 +477,11 @@ def callback(msg): # pylint: disable=function-redefined bound_counter = CallbackPass("bound_pass_manager", callback) bound_pass = PassManager(bound_counter) - with self.assertWarns(DeprecationWarning): - backend = GenericBackendV2( - 7, - calibrate_instructions=True, - basis_gates=["cx", "u1", "u2", "u3"], - seed=42, - ) + backend = GenericBackendV2( + 7, + basis_gates=["cx", "u1", "u2", "u3"], + seed=42, + ) with self.assertWarns(DeprecationWarning): sampler = BackendSampler(backend=backend, bound_pass_manager=bound_pass) _ = sampler.run([self._circuit[0], self._circuit[0]]).result() diff --git a/test/python/primitives/test_backend_sampler_v2.py b/test/python/primitives/test_backend_sampler_v2.py index 77830d5c9224..dc5c9ee82e9d 100644 --- a/test/python/primitives/test_backend_sampler_v2.py +++ b/test/python/primitives/test_backend_sampler_v2.py @@ -1484,10 +1484,7 @@ def max_circuits(self): def test_job_size_limit_backend_v1(self): """Test BackendSamplerV2 respects backend's job size limit.""" - with self.assertWarns(DeprecationWarning): - backend = GenericBackendV2( - 2, calibrate_instructions=True, basis_gates=["cx", "u1", "u2", "u3"], seed=42 - ) + backend = GenericBackendV2(2, basis_gates=["cx", "u1", "u2", "u3"], seed=42) qc = QuantumCircuit(1) qc.measure_all() qc2 = QuantumCircuit(1) diff --git a/test/python/providers/fake_provider/test_generic_backend_v2.py b/test/python/providers/fake_provider/test_generic_backend_v2.py index 797a91828555..164766dbf4d6 100644 --- a/test/python/providers/fake_provider/test_generic_backend_v2.py +++ b/test/python/providers/fake_provider/test_generic_backend_v2.py @@ -47,18 +47,6 @@ def test_ccx_2Q(self): with self.assertRaises(QiskitError): GenericBackendV2(num_qubits=2, basis_gates=["ccx", "id"], seed=42) - def test_calibration_no_noise_info(self): - """Test failing with a backend with calibration and no noise info""" - with self.assertRaises(QiskitError): - with self.assertWarns(DeprecationWarning): - GenericBackendV2( - num_qubits=2, - basis_gates=["ccx", "id"], - calibrate_instructions=True, - noise_info=False, - seed=42, - ) - def test_no_noise(self): """Test no noise info when parameter is false""" backend = GenericBackendV2( @@ -91,14 +79,12 @@ def test_no_noise_fully_connected(self): def test_no_info(self): """Test no noise info when parameter is false""" - with self.assertWarns(DeprecationWarning): - backend = GenericBackendV2( - num_qubits=5, - coupling_map=CouplingMap.from_line(5), - noise_info=False, - pulse_channels=False, - seed=42, - ) + backend = GenericBackendV2( + num_qubits=5, + coupling_map=CouplingMap.from_line(5), + noise_info=False, + seed=42, + ) qc = QuantumCircuit(5) qc.h(0) qc.cx(0, 1) @@ -110,23 +96,6 @@ def test_no_info(self): self.assertTrue(Operator.from_circuit(qc_res).equiv(qc)) self.assertEqual(backend.target.qubit_properties, None) - def test_no_pulse_channels(self): - """Test no/empty pulse channels when parameter is false""" - with self.assertWarns(DeprecationWarning): - backend = GenericBackendV2( - num_qubits=5, coupling_map=CouplingMap.from_line(5), pulse_channels=False, seed=42 - ) - qc = QuantumCircuit(5) - qc.h(0) - qc.cx(0, 1) - qc.cx(0, 2) - qc.cx(1, 4) - qc.cx(3, 0) - qc.cx(2, 4) - qc_res = generate_preset_pass_manager(optimization_level=2, backend=backend).run(qc) - self.assertTrue(Operator.from_circuit(qc_res).equiv(qc)) - self.assertTrue(len(backend.channels_map) == 0) - def test_operation_names(self): """Test that target basis gates include "delay", "measure" and "reset" even if not provided by user.""" diff --git a/test/python/providers/test_backend_v2.py b/test/python/providers/test_backend_v2.py index b3c5d3531ff1..6f9f68a21a52 100644 --- a/test/python/providers/test_backend_v2.py +++ b/test/python/providers/test_backend_v2.py @@ -28,12 +28,11 @@ from qiskit.compiler import transpile from qiskit.providers.basic_provider import BasicSimulator from qiskit.providers.fake_provider import GenericBackendV2 -from qiskit.pulse import channels from qiskit.quantum_info import Operator from qiskit.transpiler import InstructionProperties from test import QiskitTestCase # pylint: disable=wrong-import-order -from ..legacy_cmaps import BOGOTA_CMAP, TENERIFE_CMAP +from ..legacy_cmaps import TENERIFE_CMAP from .fake_mumbai_v2 import FakeMumbaiFractionalCX @@ -203,49 +202,3 @@ def test_transpile_mumbai_target(self): expected.measure(qr[0], cr[0]) expected.measure(qr[1], cr[1]) self.assertEqual(expected, tqc) - - @data(0, 1, 2, 3, 4) - def test_drive_channel(self, qubit): - """Test getting drive channel with qubit index.""" - backend = GenericBackendV2(num_qubits=5, seed=42) - with self.assertWarns(DeprecationWarning): - chan = backend.drive_channel(qubit) - ref = channels.DriveChannel(qubit) - self.assertEqual(chan, ref) - - @data(0, 1, 2, 3, 4) - def test_measure_channel(self, qubit): - """Test getting measure channel with qubit index.""" - backend = GenericBackendV2(num_qubits=5, seed=42) - with self.assertWarns(DeprecationWarning): - chan = backend.measure_channel(qubit) - ref = channels.MeasureChannel(qubit) - self.assertEqual(chan, ref) - - @data(0, 1, 2, 3, 4) - def test_acquire_channel(self, qubit): - """Test getting acquire channel with qubit index.""" - backend = GenericBackendV2(num_qubits=5, seed=42) - with self.assertWarns(DeprecationWarning): - chan = backend.acquire_channel(qubit) - ref = channels.AcquireChannel(qubit) - self.assertEqual(chan, ref) - - @data((4, 3), (3, 4), (3, 2), (2, 3), (1, 2), (2, 1), (1, 0), (0, 1)) - def test_control_channel(self, qubits): - """Test getting acquire channel with qubit index.""" - bogota_cr_channels_map = { - (4, 3): 7, - (3, 4): 6, - (3, 2): 5, - (2, 3): 4, - (1, 2): 2, - (2, 1): 3, - (1, 0): 1, - (0, 1): 0, - } - backend = GenericBackendV2(num_qubits=5, coupling_map=BOGOTA_CMAP, seed=42) - with self.assertWarns(DeprecationWarning): - chan = backend.control_channel(qubits)[0] - ref = channels.ControlChannel(bogota_cr_channels_map[qubits]) - self.assertEqual(chan, ref) diff --git a/test/python/providers/test_pulse_defaults.py b/test/python/providers/test_pulse_defaults.py deleted file mode 100644 index 2d8fe5b7bf11..000000000000 --- a/test/python/providers/test_pulse_defaults.py +++ /dev/null @@ -1,73 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2019. -# -# 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 PulseDefaults part of the backend.""" -import copy -import warnings - -import numpy as np - -from qiskit.providers.fake_provider import FakeOpenPulse2Q, GenericBackendV2 -from test import QiskitTestCase # pylint: disable=wrong-import-order - - -class TestPulseDefaults(QiskitTestCase): - """Test the PulseDefaults creation and method usage.""" - - def setUp(self): - super().setUp() - with self.assertWarns(DeprecationWarning): - # BackendV2 does not have defaults - self.defs = FakeOpenPulse2Q().defaults() - backend = GenericBackendV2( - 2, calibrate_instructions=True, basis_gates=["cx", "u1", "u2", "u3"], seed=42 - ) - self.inst_map = backend.instruction_schedule_map - - def test_buffer(self): - """Test getting the buffer value.""" - self.assertEqual(self.defs.buffer, 10) - - def test_freq_est(self): - """Test extracting qubit frequencies.""" - warnings.simplefilter("ignore") - self.assertEqual(self.defs.qubit_freq_est[1], 5.0 * 1e9) - self.assertEqual(self.defs.meas_freq_est[0], 6.5 * 1e9) - warnings.simplefilter("default") - - def test_default_building(self): - """Test building of ops definition is properly built from backend.""" - self.assertTrue(self.inst_map.has("u1", (0,))) - self.assertTrue(self.inst_map.has("u3", (0,))) - self.assertTrue(self.inst_map.has("u3", 1)) - self.assertTrue(self.inst_map.has("cx", (0, 1))) - self.assertEqual(self.inst_map.get_parameters("u1", 0), ("P0",)) - u1_minus_pi = self.inst_map.get("u1", 0, P0=np.pi) - fc_cmd = u1_minus_pi.instructions[0][-1] - self.assertAlmostEqual(fc_cmd.phase, -np.pi) - - def test_str(self): - """Test that __str__ method works.""" - self.assertEqual( - "" in str(self.defs)[100:] - ) - - def test_deepcopy(self): - """Test that deepcopy creates an identical object.""" - copy_defs = copy.deepcopy(self.defs) - self.assertEqual(list(copy_defs.to_dict().keys()), list(self.defs.to_dict().keys())) diff --git a/test/python/pulse/test_builder_v2.py b/test/python/pulse/test_builder_v2.py deleted file mode 100644 index 29248f6179ca..000000000000 --- a/test/python/pulse/test_builder_v2.py +++ /dev/null @@ -1,324 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2024. -# -# 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 pulse builder with backendV2 context utilities.""" - -import numpy as np - -from qiskit import pulse -from qiskit.pulse import macros - -from qiskit.pulse.instructions import directives -from qiskit.pulse.transforms import target_qobj_transform -from qiskit.providers.fake_provider import GenericBackendV2 -from qiskit.pulse import instructions -from test import QiskitTestCase # pylint: disable=wrong-import-order -from qiskit.utils.deprecate_pulse import decorate_test_methods, ignore_pulse_deprecation_warnings - -from ..legacy_cmaps import MUMBAI_CMAP - - -@decorate_test_methods(ignore_pulse_deprecation_warnings) -class TestBuilderV2(QiskitTestCase): - """Test the pulse builder context with backendV2.""" - - def setUp(self): - super().setUp() - with self.assertWarns(DeprecationWarning): - self.backend = GenericBackendV2( - num_qubits=27, coupling_map=MUMBAI_CMAP, calibrate_instructions=True, seed=42 - ) - - def assertScheduleEqual(self, program, target): - """Assert an error when two pulse programs are not equal. - - .. note:: Two programs are converted into standard execution format then compared. - """ - self.assertEqual(target_qobj_transform(program), target_qobj_transform(target)) - - -@decorate_test_methods(ignore_pulse_deprecation_warnings) -class TestContextsV2(TestBuilderV2): - """Test builder contexts.""" - - def test_phase_compensated_frequency_offset(self): - """Test that the phase offset context properly compensates for phase - accumulation with backendV2.""" - d0 = pulse.DriveChannel(0) - with pulse.build(self.backend) as schedule: - with pulse.frequency_offset(1e9, d0, compensate_phase=True): - pulse.delay(10, d0) - - reference = pulse.Schedule() - reference += instructions.ShiftFrequency(1e9, d0) - reference += instructions.Delay(10, d0) - reference += instructions.ShiftPhase( - -2 * np.pi * ((1e9 * 10 * self.backend.target.dt) % 1), d0 - ) - reference += instructions.ShiftFrequency(-1e9, d0) - self.assertScheduleEqual(schedule, reference) - - -@decorate_test_methods(ignore_pulse_deprecation_warnings) -class TestChannelsV2(TestBuilderV2): - """Test builder channels.""" - - def test_drive_channel(self): - """Text context builder drive channel.""" - with pulse.build(self.backend): - with self.assertWarns(DeprecationWarning): - self.assertEqual(pulse.drive_channel(0), pulse.DriveChannel(0)) - - def test_measure_channel(self): - """Text context builder measure channel.""" - with pulse.build(self.backend): - with self.assertWarns(DeprecationWarning): - self.assertEqual(pulse.measure_channel(0), pulse.MeasureChannel(0)) - - def test_acquire_channel(self): - """Text context builder acquire channel.""" - with self.assertWarns(DeprecationWarning): - with pulse.build(self.backend): - self.assertEqual(pulse.acquire_channel(0), pulse.AcquireChannel(0)) - - def test_control_channel(self): - """Text context builder control channel.""" - with pulse.build(self.backend): - with self.assertWarns(DeprecationWarning): - self.assertEqual(pulse.control_channels(0, 1)[0], pulse.ControlChannel(0)) - - -@decorate_test_methods(ignore_pulse_deprecation_warnings) -class TestDirectivesV2(TestBuilderV2): - """Test builder directives.""" - - def test_barrier_on_qubits(self): - """Test barrier directive on qubits with backendV2. - A part of qubits map of Mumbai - 0 -- 1 -- 4 -- - | - | - 2 - """ - with pulse.build(self.backend) as schedule: - with self.assertWarns(DeprecationWarning): - pulse.barrier(0, 1) - reference = pulse.ScheduleBlock() - reference += directives.RelativeBarrier( - pulse.DriveChannel(0), - pulse.DriveChannel(1), - pulse.MeasureChannel(0), - pulse.MeasureChannel(1), - pulse.ControlChannel(0), - pulse.ControlChannel(1), - pulse.ControlChannel(2), - pulse.ControlChannel(3), - pulse.ControlChannel(4), - pulse.ControlChannel(8), - pulse.AcquireChannel(0), - pulse.AcquireChannel(1), - ) - self.assertEqual(schedule, reference) - - -@decorate_test_methods(ignore_pulse_deprecation_warnings) -class TestUtilitiesV2(TestBuilderV2): - """Test builder utilities.""" - - def test_active_backend(self): - """Test getting active builder backend.""" - with pulse.build(self.backend): - self.assertEqual(pulse.active_backend(), self.backend) - - def test_qubit_channels(self): - """Test getting the qubit channels of the active builder's backend.""" - with pulse.build(self.backend): - with self.assertWarns(DeprecationWarning): - qubit_channels = pulse.qubit_channels(0) - - self.assertEqual( - qubit_channels, - { - pulse.DriveChannel(0), - pulse.MeasureChannel(0), - pulse.AcquireChannel(0), - pulse.ControlChannel(0), - pulse.ControlChannel(1), - }, - ) - - def test_num_qubits(self): - """Test builder utility to get number of qubits with backendV2.""" - with pulse.build(self.backend): - self.assertEqual(pulse.num_qubits(), 27) - - def test_samples_to_seconds(self): - """Test samples to time with backendV2""" - target = self.backend.target - target.dt = 0.1 - with pulse.build(self.backend): - time = pulse.samples_to_seconds(100) - self.assertTrue(isinstance(time, float)) - self.assertEqual(pulse.samples_to_seconds(100), 10) - - def test_samples_to_seconds_array(self): - """Test samples to time (array format) with backendV2.""" - target = self.backend.target - target.dt = 0.1 - with pulse.build(self.backend): - samples = np.array([100, 200, 300]) - times = pulse.samples_to_seconds(samples) - self.assertTrue(np.issubdtype(times.dtype, np.floating)) - np.testing.assert_allclose(times, np.array([10, 20, 30])) - - def test_seconds_to_samples(self): - """Test time to samples with backendV2""" - target = self.backend.target - target.dt = 0.1 - with pulse.build(self.backend): - samples = pulse.seconds_to_samples(10) - self.assertTrue(isinstance(samples, int)) - self.assertEqual(pulse.seconds_to_samples(10), 100) - - def test_seconds_to_samples_array(self): - """Test time to samples (array format) with backendV2.""" - target = self.backend.target - target.dt = 0.1 - with pulse.build(self.backend): - times = np.array([10, 20, 30]) - samples = pulse.seconds_to_samples(times) - self.assertTrue(np.issubdtype(samples.dtype, np.integer)) - np.testing.assert_allclose(pulse.seconds_to_samples(times), np.array([100, 200, 300])) - - -@decorate_test_methods(ignore_pulse_deprecation_warnings) -class TestMacrosV2(TestBuilderV2): - """Test builder macros with backendV2.""" - - def test_macro(self): - """Test builder macro decorator.""" - - @pulse.macro - def nested(a): - with self.assertWarns(DeprecationWarning): - pulse.play(pulse.Gaussian(100, a, 20), pulse.drive_channel(0)) - return a * 2 - - @pulse.macro - def test(): - with self.assertWarns(DeprecationWarning): - pulse.play(pulse.Constant(100, 1.0), pulse.drive_channel(0)) - output = nested(0.5) - return output - - with pulse.build(self.backend) as schedule: - output = test() - self.assertEqual(output, 0.5 * 2) - - reference = pulse.Schedule() - reference += pulse.Play(pulse.Constant(100, 1.0), pulse.DriveChannel(0)) - reference += pulse.Play(pulse.Gaussian(100, 0.5, 20), pulse.DriveChannel(0)) - - self.assertScheduleEqual(schedule, reference) - - def test_measure(self): - """Test utility function - measure with backendV2.""" - with pulse.build(self.backend) as schedule: - with self.assertWarns(DeprecationWarning): - reg = pulse.measure(0) - - self.assertEqual(reg, pulse.MemorySlot(0)) - - reference = macros.measure(qubits=[0], backend=self.backend, meas_map=self.backend.meas_map) - - self.assertScheduleEqual(schedule, reference) - - def test_measure_multi_qubits(self): - """Test utility function - measure with multi qubits with backendV2.""" - with pulse.build(self.backend) as schedule: - with self.assertWarns(DeprecationWarning): - regs = pulse.measure([0, 1]) - - self.assertListEqual(regs, [pulse.MemorySlot(0), pulse.MemorySlot(1)]) - - reference = macros.measure( - qubits=[0, 1], backend=self.backend, meas_map=self.backend.meas_map - ) - - self.assertScheduleEqual(schedule, reference) - - def test_measure_all(self): - """Test utility function - measure with backendV2..""" - with pulse.build(self.backend) as schedule: - with self.assertWarns(DeprecationWarning): - regs = pulse.measure_all() - - self.assertEqual(regs, [pulse.MemorySlot(i) for i in range(self.backend.num_qubits)]) - reference = macros.measure_all(self.backend) - - self.assertScheduleEqual(schedule, reference) - - def test_delay_qubit(self): - """Test delaying on a qubit macro.""" - with pulse.build(self.backend) as schedule: - with self.assertWarns(DeprecationWarning): - pulse.delay_qubits(10, 0) - - d0 = pulse.DriveChannel(0) - m0 = pulse.MeasureChannel(0) - a0 = pulse.AcquireChannel(0) - u0 = pulse.ControlChannel(0) - u1 = pulse.ControlChannel(1) - - reference = pulse.Schedule() - reference += instructions.Delay(10, d0) - reference += instructions.Delay(10, m0) - reference += instructions.Delay(10, a0) - reference += instructions.Delay(10, u0) - reference += instructions.Delay(10, u1) - - self.assertScheduleEqual(schedule, reference) - - def test_delay_qubits(self): - """Test delaying on multiple qubits with backendV2 to make sure we don't insert delays twice.""" - with pulse.build(self.backend) as schedule: - with self.assertWarns(DeprecationWarning): - pulse.delay_qubits(10, 0, 1) - - d0 = pulse.DriveChannel(0) - d1 = pulse.DriveChannel(1) - m0 = pulse.MeasureChannel(0) - m1 = pulse.MeasureChannel(1) - a0 = pulse.AcquireChannel(0) - a1 = pulse.AcquireChannel(1) - u0 = pulse.ControlChannel(0) - u1 = pulse.ControlChannel(1) - u2 = pulse.ControlChannel(2) - u3 = pulse.ControlChannel(3) - u4 = pulse.ControlChannel(4) - u8 = pulse.ControlChannel(8) - - reference = pulse.Schedule() - reference += instructions.Delay(10, d0) - reference += instructions.Delay(10, d1) - reference += instructions.Delay(10, m0) - reference += instructions.Delay(10, m1) - reference += instructions.Delay(10, a0) - reference += instructions.Delay(10, a1) - reference += instructions.Delay(10, u0) - reference += instructions.Delay(10, u1) - reference += instructions.Delay(10, u2) - reference += instructions.Delay(10, u3) - reference += instructions.Delay(10, u4) - reference += instructions.Delay(10, u8) - - self.assertScheduleEqual(schedule, reference) diff --git a/test/python/pulse/test_macros.py b/test/python/pulse/test_macros.py deleted file mode 100644 index c1f0b93339ab..000000000000 --- a/test/python/pulse/test_macros.py +++ /dev/null @@ -1,256 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2019, 2024. -# -# 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 cases for Pulse Macro functions.""" - -from qiskit.pulse import ( - Schedule, - AcquireChannel, - Acquire, - InstructionScheduleMap, - MeasureChannel, - MemorySlot, - GaussianSquare, - Play, -) -from qiskit.pulse import macros -from qiskit.pulse.exceptions import PulseError -from qiskit.providers.fake_provider import FakeOpenPulse2Q, Fake27QPulseV1, GenericBackendV2 -from test import QiskitTestCase # pylint: disable=wrong-import-order -from qiskit.utils.deprecate_pulse import decorate_test_methods, ignore_pulse_deprecation_warnings - - -@decorate_test_methods(ignore_pulse_deprecation_warnings) -class TestMeasure(QiskitTestCase): - """Pulse measure macro.""" - - @ignore_pulse_deprecation_warnings - def setUp(self): - super().setUp() - with self.assertWarns(DeprecationWarning): - self.backend = FakeOpenPulse2Q() - self.backend_v1 = Fake27QPulseV1() - - self.inst_map = self.backend.defaults().instruction_schedule_map - with self.assertWarns(DeprecationWarning): - self.backend_v2 = GenericBackendV2( - num_qubits=27, - calibrate_instructions=self.backend_v1.defaults().instruction_schedule_map, - seed=42, - ) - - def test_measure(self): - """Test macro - measure.""" - sched = macros.measure(qubits=[0], backend=self.backend) - expected = Schedule( - self.inst_map.get("measure", [0, 1]).filter(channels=[MeasureChannel(0)]), - Acquire(10, AcquireChannel(0), MemorySlot(0)), - ) - self.assertEqual(sched.instructions, expected.instructions) - - def test_measure_sched_with_qubit_mem_slots(self): - """Test measure with custom qubit_mem_slots.""" - sched = macros.measure(qubits=[0], backend=self.backend, qubit_mem_slots={0: 1}) - expected = Schedule( - self.inst_map.get("measure", [0, 1]).filter(channels=[MeasureChannel(0)]), - Acquire(10, AcquireChannel(0), MemorySlot(1)), - ) - self.assertEqual(sched.instructions, expected.instructions) - - def test_measure_sched_with_meas_map(self): - """Test measure with custom meas_map as list and dict.""" - sched_with_meas_map_list = macros.measure( - qubits=[0], backend=self.backend, meas_map=[[0, 1]] - ) - sched_with_meas_map_dict = macros.measure( - qubits=[0], backend=self.backend, meas_map={0: [0, 1], 1: [0, 1]} - ) - expected = Schedule( - self.inst_map.get("measure", [0, 1]).filter(channels=[MeasureChannel(0)]), - Acquire(10, AcquireChannel(0), MemorySlot(0)), - ) - self.assertEqual(sched_with_meas_map_list.instructions, expected.instructions) - self.assertEqual(sched_with_meas_map_dict.instructions, expected.instructions) - - def test_measure_with_custom_inst_map(self): - """Test measure with custom inst_map, meas_map with measure_name.""" - q0_sched = Play(GaussianSquare(1200, 1, 0.4, 1150), MeasureChannel(0)) - q0_sched += Acquire(1200, AcquireChannel(0), MemorySlot(0)) - inst_map = InstructionScheduleMap() - inst_map.add("my_sched", 0, q0_sched) - sched = macros.measure( - qubits=[0], measure_name="my_sched", inst_map=inst_map, meas_map=[[0]] - ) - self.assertEqual(sched.instructions, q0_sched.instructions) - - with self.assertRaises(PulseError): - macros.measure(qubits=[0], measure_name="name", inst_map=inst_map, meas_map=[[0]]) - - def test_fail_measure(self): - """Test failing measure.""" - with self.assertRaises(PulseError): - macros.measure(qubits=[0], meas_map=self.backend.configuration().meas_map) - with self.assertRaises(PulseError): - macros.measure(qubits=[0], inst_map=self.inst_map) - - def test_measure_v2(self): - """Test macro - measure with backendV2.""" - sched = macros.measure(qubits=[0], backend=self.backend_v2) - with self.assertWarns(DeprecationWarning): - expected = self.backend_v2.target.get_calibration("measure", (0,)).filter( - channels=[MeasureChannel(0), AcquireChannel(0)] - ) - self.assertEqual(sched.instructions, expected.instructions) - - def test_measure_v2_sched_with_qubit_mem_slots(self): - """Test measure with backendV2 and custom qubit_mem_slots.""" - sched = macros.measure(qubits=[0], backend=self.backend_v2, qubit_mem_slots={0: 2}) - with self.assertWarns(DeprecationWarning): - expected = self.backend_v2.target.get_calibration("measure", (0,)).filter( - channels=[ - MeasureChannel(0), - ] - ) - measure_duration = expected.filter(instruction_types=[Play]).duration - expected += Acquire(measure_duration, AcquireChannel(0), MemorySlot(2)) - self.assertEqual(sched.instructions, expected.instructions) - - def test_measure_v2_sched_with_meas_map(self): - """Test measure with backendV2 custom meas_map as list and dict.""" - sched_with_meas_map_list = macros.measure( - qubits=[0], backend=self.backend_v2, meas_map=[[0, 1]] - ) - sched_with_meas_map_dict = macros.measure( - qubits=[0], backend=self.backend_v2, meas_map={0: [0, 1], 1: [0, 1]} - ) - with self.assertWarns(DeprecationWarning): - expected = self.backend_v2.target.get_calibration("measure", (0,)).filter( - channels=[ - MeasureChannel(0), - ] - ) - measure_duration = expected.filter(instruction_types=[Play]).duration - expected += Acquire(measure_duration, AcquireChannel(0), MemorySlot(0)) - self.assertEqual(sched_with_meas_map_list.instructions, expected.instructions) - self.assertEqual(sched_with_meas_map_dict.instructions, expected.instructions) - - def test_multiple_measure_v2(self): - """Test macro - multiple qubit measure with backendV2.""" - sched = macros.measure(qubits=[0, 1], backend=self.backend_v2) - with self.assertWarns(DeprecationWarning): - expected = self.backend_v2.target.get_calibration("measure", (0,)).filter( - channels=[ - MeasureChannel(0), - ] - ) - expected += self.backend_v2.target.get_calibration("measure", (1,)).filter( - channels=[ - MeasureChannel(1), - ] - ) - measure_duration = expected.filter(instruction_types=[Play]).duration - expected += Acquire(measure_duration, AcquireChannel(0), MemorySlot(0)) - expected += Acquire(measure_duration, AcquireChannel(1), MemorySlot(1)) - self.assertEqual(sched.instructions, expected.instructions) - - def test_output_with_measure_v1_and_measure_v2(self): - """Test make outputs of measure_v1 and measure_v2 consistent.""" - sched_measure_v1 = macros.measure(qubits=[0, 1], backend=self.backend_v1) - sched_measure_v2 = macros.measure(qubits=[0, 1], backend=self.backend_v2) - - self.assertEqual(sched_measure_v1.instructions, sched_measure_v2.instructions) - - def test_output_with_measure_v1_and_measure_v2_sched_with_qubit_mem_slots(self): - """Test make outputs of measure_v1 and measure_v2 with custom qubit_mem_slots consistent.""" - sched_measure_v1 = macros.measure( - qubits=[0], backend=self.backend_v1, qubit_mem_slots={0: 2} - ) - sched_measure_v2 = macros.measure( - qubits=[0], backend=self.backend_v2, qubit_mem_slots={0: 2} - ) - self.assertEqual(sched_measure_v1.instructions, sched_measure_v2.instructions) - - def test_output_with_measure_v1_and_measure_v2_sched_with_meas_map(self): - """Test make outputs of measure_v1 and measure_v2 - with custom meas_map as list and dict consistent.""" - with self.assertWarns(DeprecationWarning): - backend = Fake27QPulseV1() - num_qubits_list_measure_v1 = list(range(backend.configuration().num_qubits)) - num_qubits_list_measure_v2 = list(range(self.backend_v2.num_qubits)) - sched_with_meas_map_list_v1 = macros.measure( - qubits=[0], backend=self.backend_v1, meas_map=[num_qubits_list_measure_v1] - ) - sched_with_meas_map_dict_v1 = macros.measure( - qubits=[0], - backend=self.backend_v1, - meas_map={0: num_qubits_list_measure_v1, 1: num_qubits_list_measure_v1}, - ) - sched_with_meas_map_list_v2 = macros.measure( - qubits=[0], backend=self.backend_v2, meas_map=[num_qubits_list_measure_v2] - ) - sched_with_meas_map_dict_v2 = macros.measure( - qubits=[0], - backend=self.backend_v2, - meas_map={0: num_qubits_list_measure_v2, 1: num_qubits_list_measure_v2}, - ) - self.assertEqual( - sched_with_meas_map_list_v1.instructions, - sched_with_meas_map_list_v2.instructions, - ) - self.assertEqual( - sched_with_meas_map_dict_v1.instructions, - sched_with_meas_map_dict_v2.instructions, - ) - - def test_output_with_multiple_measure_v1_and_measure_v2(self): - """Test macro - consistent output of multiple qubit measure with backendV1 and backendV2.""" - sched_measure_v1 = macros.measure(qubits=[0, 1], backend=self.backend_v1) - sched_measure_v2 = macros.measure(qubits=[0, 1], backend=self.backend_v2) - self.assertEqual(sched_measure_v1.instructions, sched_measure_v2.instructions) - - -@decorate_test_methods(ignore_pulse_deprecation_warnings) -class TestMeasureAll(QiskitTestCase): - """Pulse measure all macro.""" - - @ignore_pulse_deprecation_warnings - def setUp(self): - super().setUp() - with self.assertWarns(DeprecationWarning): - self.backend_v1 = FakeOpenPulse2Q() - self.inst_map = self.backend_v1.defaults().instruction_schedule_map - with self.assertWarns(DeprecationWarning): - self.backend_v2 = GenericBackendV2( - num_qubits=2, - calibrate_instructions=self.backend_v1.defaults().instruction_schedule_map, - seed=42, - ) - - def test_measure_all(self): - """Test measure_all function.""" - sched = macros.measure_all(self.backend_v1) - expected = Schedule(self.inst_map.get("measure", [0, 1])) - self.assertEqual(sched.instructions, expected.instructions) - - def test_measure_all_v2(self): - """Test measure_all function with backendV2.""" - sched = macros.measure_all(self.backend_v1) - expected = Schedule( - self.inst_map.get("measure", list(range(self.backend_v1.configuration().num_qubits))) - ) - self.assertEqual(sched.instructions, expected.instructions) - - def test_output_of_measure_all_with_backend_v1_and_v2(self): - """Test make outputs of measure_all with backendV1 and backendV2 consistent.""" - sched_measure_v1 = macros.measure_all(backend=self.backend_v1) - sched_measure_v2 = macros.measure_all(backend=self.backend_v2) - self.assertEqual(sched_measure_v1.instructions, sched_measure_v2.instructions) diff --git a/test/python/transpiler/test_instruction_durations.py b/test/python/transpiler/test_instruction_durations.py index d9a3ef2b1773..9813dc4e7303 100644 --- a/test/python/transpiler/test_instruction_durations.py +++ b/test/python/transpiler/test_instruction_durations.py @@ -96,8 +96,7 @@ def test_fail_if_get_unbounded_duration_with_unit_conversion_when_dt_is_not_prov def test_from_backend_with_backendv2(self): """Test if `from_backend()` method allows using BackendV2""" - with self.assertWarns(DeprecationWarning): - backend = GenericBackendV2(num_qubits=4, calibrate_instructions=True, seed=42) + backend = GenericBackendV2(num_qubits=4, seed=42) inst_durations = InstructionDurations.from_backend(backend) self.assertEqual(inst_durations, backend.target.durations()) self.assertIsInstance(inst_durations, InstructionDurations) diff --git a/test/python/transpiler/test_passmanager_config.py b/test/python/transpiler/test_passmanager_config.py index 85cbb7909aef..fccd68f0a261 100644 --- a/test/python/transpiler/test_passmanager_config.py +++ b/test/python/transpiler/test_passmanager_config.py @@ -87,14 +87,12 @@ def test_from_backend_and_user(self): qr = QuantumRegister(4, "qr") initial_layout = [None, qr[0], qr[1], qr[2], None, qr[3]] - with self.assertWarns(DeprecationWarning): - backend = GenericBackendV2( - num_qubits=20, - coupling_map=ALMADEN_CMAP, - basis_gates=["id", "u1", "u2", "u3", "cx"], - calibrate_instructions=None, - seed=42, - ) + backend = GenericBackendV2( + num_qubits=20, + coupling_map=ALMADEN_CMAP, + basis_gates=["id", "u1", "u2", "u3", "cx"], + seed=42, + ) config = PassManagerConfig.from_backend( backend, basis_gates=["user_gate"], initial_layout=initial_layout ) diff --git a/test/python/transpiler/test_sabre_swap.py b/test/python/transpiler/test_sabre_swap.py index 109053ee8324..23a54433ed62 100644 --- a/test/python/transpiler/test_sabre_swap.py +++ b/test/python/transpiler/test_sabre_swap.py @@ -13,7 +13,6 @@ """Test the Sabre Swap pass""" import unittest -import warnings import itertools import ddt @@ -1380,17 +1379,12 @@ class TestSabreSwapRandomCircuitValidOutput(QiskitTestCase): @classmethod def setUpClass(cls): super().setUpClass() - with warnings.catch_warnings(): - # Catch warnings since self.assertWarns cannot be used here. - # The `calibrate_instructions` argument is deprecated in Qiksit 1.3 - warnings.simplefilter("ignore", category=DeprecationWarning) - cls.backend = GenericBackendV2( - num_qubits=27, - calibrate_instructions=True, - control_flow=True, - coupling_map=MUMBAI_CMAP, - seed=42, - ) + cls.backend = GenericBackendV2( + num_qubits=27, + control_flow=True, + coupling_map=MUMBAI_CMAP, + seed=42, + ) cls.coupling_edge_set = {tuple(x) for x in cls.backend.coupling_map} cls.basis_gates = set(cls.backend.operation_names) diff --git a/test/python/transpiler/test_stochastic_swap.py b/test/python/transpiler/test_stochastic_swap.py index 7e4dfe63de3c..bae6328a9b84 100644 --- a/test/python/transpiler/test_stochastic_swap.py +++ b/test/python/transpiler/test_stochastic_swap.py @@ -13,7 +13,6 @@ """Test the Stochastic Swap pass""" import unittest -import warnings import numpy.random @@ -1528,13 +1527,7 @@ class TestStochasticSwapRandomCircuitValidOutput(QiskitTestCase): @classmethod def setUpClass(cls): super().setUpClass() - with warnings.catch_warnings(): - # Catch warnings since self.assertWarns cannot be used here. - # The `calibrate_instructions` argument is deprecated in Qiksit 1.3 - warnings.simplefilter("ignore", category=DeprecationWarning) - cls.backend = GenericBackendV2( - num_qubits=27, calibrate_instructions=True, control_flow=True, seed=42 - ) + cls.backend = GenericBackendV2(num_qubits=27, control_flow=True, seed=42) cls.coupling_edge_set = {tuple(x) for x in cls.backend.coupling_map} cls.basis_gates = set(cls.backend.operation_names) diff --git a/test/python/transpiler/test_target.py b/test/python/transpiler/test_target.py index f7aae24eff54..694ef38dc4fa 100644 --- a/test/python/transpiler/test_target.py +++ b/test/python/transpiler/test_target.py @@ -36,9 +36,6 @@ from qiskit.circuit import IfElseOp, ForLoopOp, WhileLoopOp, SwitchCaseOp from qiskit.circuit.measure import Measure from qiskit.circuit.parameter import Parameter -from qiskit import pulse -from qiskit.pulse.instruction_schedule_map import InstructionScheduleMap -from qiskit.pulse.calibration_entries import CalibrationPublisher, ScheduleDef from qiskit.transpiler.coupling import CouplingMap from qiskit.transpiler.instruction_durations import InstructionDurations from qiskit.transpiler.timing_constraints import TimingConstraints @@ -1209,288 +1206,6 @@ def test_target_no_num_qubits_qubit_properties(self): self.assertEqual(target.num_qubits, len(qubit_properties)) -class TestPulseTarget(QiskitTestCase): - def setUp(self): - super().setUp() - self.pulse_target = Target( - dt=3e-7, granularity=2, min_length=4, pulse_alignment=8, acquire_alignment=8 - ) - with self.assertWarns(DeprecationWarning): - with pulse.build(name="sx_q0") as self.custom_sx_q0: - pulse.play(pulse.Constant(100, 0.1), pulse.DriveChannel(0)) - with pulse.build(name="sx_q1") as self.custom_sx_q1: - pulse.play(pulse.Constant(100, 0.2), pulse.DriveChannel(1)) - sx_props = { - (0,): InstructionProperties( - duration=35.5e-9, error=0.000413, calibration=self.custom_sx_q0 - ), - (1,): InstructionProperties( - duration=35.5e-9, error=0.000502, calibration=self.custom_sx_q1 - ), - } - self.pulse_target.add_instruction(SXGate(), sx_props) - - def test_instruction_schedule_map(self): - with self.assertWarns(DeprecationWarning): - inst_map = self.pulse_target.instruction_schedule_map() - self.assertIn("sx", inst_map.instructions) - self.assertEqual(inst_map.qubits_with_instruction("sx"), [0, 1]) - self.assertTrue("sx" in inst_map.qubit_instructions(0)) - - def test_instruction_schedule_map_ideal_sim_backend(self): - ideal_sim_target = Target(num_qubits=3) - theta = Parameter("theta") - phi = Parameter("phi") - lam = Parameter("lambda") - for inst in [ - UGate(theta, phi, lam), - RXGate(theta), - RYGate(theta), - RZGate(theta), - CXGate(), - ECRGate(), - CCXGate(), - Measure(), - ]: - ideal_sim_target.add_instruction(inst, {None: None}) - with self.assertWarns(DeprecationWarning): - inst_map = ideal_sim_target.instruction_schedule_map() - self.assertEqual(InstructionScheduleMap(), inst_map) - - def test_str(self): - expected = """Target -Number of qubits: 2 -Instructions: - sx - (0,): - Duration: 3.55e-08 sec. - Error Rate: 0.000413 - With pulse schedule calibration - (1,): - Duration: 3.55e-08 sec. - Error Rate: 0.000502 - With pulse schedule calibration -""" - self.assertEqual(expected, str(self.pulse_target)) - - def test_update_from_instruction_schedule_map_add_instruction(self): - target = Target() - with self.assertWarns(DeprecationWarning): - inst_map = InstructionScheduleMap() - inst_map.add("sx", 0, self.custom_sx_q0) - inst_map.add("sx", 1, self.custom_sx_q1) - with self.assertWarns(DeprecationWarning): - target.update_from_instruction_schedule_map(inst_map, {"sx": SXGate()}) - self.assertEqual(inst_map, target.instruction_schedule_map()) - - def test_update_from_instruction_schedule_map_with_schedule_parameter(self): - self.pulse_target.dt = None - with self.assertWarns(DeprecationWarning): - inst_map = InstructionScheduleMap() - duration = Parameter("duration") - - with self.assertWarns(DeprecationWarning): - with pulse.build(name="sx_q0") as custom_sx: - pulse.play(pulse.Constant(duration, 0.2), pulse.DriveChannel(0)) - - inst_map.add("sx", 0, custom_sx, ["duration"]) - - target = Target(dt=3e-7) - with self.assertWarns(DeprecationWarning): - target.update_from_instruction_schedule_map(inst_map, {"sx": SXGate()}) - self.assertEqual(inst_map, target.instruction_schedule_map()) - - def test_update_from_instruction_schedule_map_update_schedule(self): - self.pulse_target.dt = None - with self.assertWarns(DeprecationWarning): - inst_map = InstructionScheduleMap() - with pulse.build(name="sx_q1") as custom_sx: - pulse.play(pulse.Constant(1000, 0.2), pulse.DriveChannel(1)) - - inst_map.add("sx", 0, self.custom_sx_q0) - inst_map.add("sx", 1, custom_sx) - with self.assertWarns(DeprecationWarning): - self.pulse_target.update_from_instruction_schedule_map(inst_map, {"sx": SXGate()}) - self.assertEqual(inst_map, self.pulse_target.instruction_schedule_map()) - # Calibration doesn't change for q0 - self.assertEqual(self.pulse_target["sx"][(0,)].duration, 35.5e-9) - self.assertEqual(self.pulse_target["sx"][(0,)].error, 0.000413) - # Calibration is updated for q1 without error dict and gate time - self.assertIsNone(self.pulse_target["sx"][(1,)].duration) - self.assertIsNone(self.pulse_target["sx"][(1,)].error) - - def test_update_from_instruction_schedule_map_new_instruction_no_name_map(self): - target = Target() - with self.assertWarns(DeprecationWarning): - inst_map = InstructionScheduleMap() - inst_map.add("sx", 0, self.custom_sx_q0) - inst_map.add("sx", 1, self.custom_sx_q1) - with self.assertWarns(DeprecationWarning): - target.update_from_instruction_schedule_map(inst_map) - self.assertEqual(target["sx"][(0,)].calibration, self.custom_sx_q0) - self.assertEqual(target["sx"][(1,)].calibration, self.custom_sx_q1) - - def test_update_from_instruction_schedule_map_new_qarg_raises(self): - with self.assertWarns(DeprecationWarning): - inst_map = InstructionScheduleMap() - inst_map.add("sx", 0, self.custom_sx_q0) - inst_map.add("sx", 1, self.custom_sx_q1) - inst_map.add("sx", 2, self.custom_sx_q1) - with self.assertWarns(DeprecationWarning): - self.pulse_target.update_from_instruction_schedule_map(inst_map) - self.assertFalse(self.pulse_target.instruction_supported("sx", (2,))) - - def test_update_from_instruction_schedule_map_with_dt_set(self): - with self.assertWarns(DeprecationWarning): - inst_map = InstructionScheduleMap() - with pulse.build(name="sx_q1") as custom_sx: - pulse.play(pulse.Constant(1000, 0.2), pulse.DriveChannel(1)) - - inst_map.add("sx", 0, self.custom_sx_q0) - inst_map.add("sx", 1, custom_sx) - self.pulse_target.dt = 1.0 - with self.assertWarns(DeprecationWarning): - self.pulse_target.update_from_instruction_schedule_map(inst_map, {"sx": SXGate()}) - self.assertEqual(inst_map, self.pulse_target.instruction_schedule_map()) - self.assertEqual(self.pulse_target["sx"][(1,)].duration, 1000.0) - self.assertIsNone(self.pulse_target["sx"][(1,)].error) - # This is an edge case. - # System dt is read-only property and changing it will break all underlying calibrations. - # duration of sx0 returns previous value since calibration doesn't change. - self.assertEqual(self.pulse_target["sx"][(0,)].duration, 35.5e-9) - self.assertEqual(self.pulse_target["sx"][(0,)].error, 0.000413) - - def test_update_from_instruction_schedule_map_with_error_dict(self): - with self.assertWarns(DeprecationWarning): - inst_map = InstructionScheduleMap() - with pulse.build(name="sx_q1") as custom_sx: - pulse.play(pulse.Constant(1000, 0.2), pulse.DriveChannel(1)) - - inst_map.add("sx", 0, self.custom_sx_q0) - inst_map.add("sx", 1, custom_sx) - self.pulse_target.dt = 1.0 - error_dict = {"sx": {(1,): 1.0}} - - with self.assertWarns(DeprecationWarning): - self.pulse_target.update_from_instruction_schedule_map( - inst_map, {"sx": SXGate()}, error_dict=error_dict - ) - self.assertEqual(self.pulse_target["sx"][(1,)].error, 1.0) - self.assertEqual(self.pulse_target["sx"][(0,)].error, 0.000413) - - def test_timing_constraints(self): - generated_constraints = self.pulse_target.timing_constraints() - expected_constraints = TimingConstraints(2, 4, 8, 8) - for i in ["granularity", "min_length", "pulse_alignment", "acquire_alignment"]: - self.assertEqual( - getattr(generated_constraints, i), - getattr(expected_constraints, i), - f"Generated constraints differs from expected for attribute {i}" - f"{getattr(generated_constraints, i)}!={getattr(expected_constraints, i)}", - ) - - def test_default_instmap_has_no_custom_gate(self): - with self.assertWarns(DeprecationWarning): - backend = GenericBackendV2(num_qubits=27, calibrate_instructions=True) - target = backend.target - - # This copies .calibration of InstructionProperties of each instruction - # This must not convert PulseQobj to Schedule during this. - # See qiskit-terra/#9595 - with self.assertWarns(DeprecationWarning): - inst_map = target.instruction_schedule_map() - self.assertFalse(inst_map.has_custom_gate()) - - # Get pulse schedule. This generates Schedule provided by backend. - with self.assertWarns(DeprecationWarning): - sched = inst_map.get("sx", (0,)) - self.assertEqual(sched.metadata["publisher"], CalibrationPublisher.BACKEND_PROVIDER) - self.assertFalse(inst_map.has_custom_gate()) - - # Update target with custom instruction. This is user provided schedule. - with self.assertWarns(DeprecationWarning): - new_prop = InstructionProperties( - duration=self.custom_sx_q0.duration, - error=None, - calibration=self.custom_sx_q0, - ) - target.update_instruction_properties(instruction="sx", qargs=(0,), properties=new_prop) - with self.assertWarns(DeprecationWarning): - inst_map = target.instruction_schedule_map() - self.assertTrue(inst_map.has_custom_gate()) - - empty = InstructionProperties() - target.update_instruction_properties(instruction="sx", qargs=(0,), properties=empty) - with self.assertWarns(DeprecationWarning): - inst_map = target.instruction_schedule_map() - self.assertFalse(inst_map.has_custom_gate()) - - def test_get_empty_target_calibration(self): - target = Target() - properties = {(0,): InstructionProperties(duration=100, error=0.1)} - target.add_instruction(XGate(), properties) - - with self.assertWarns(DeprecationWarning): - self.assertIsNone(target["x"][(0,)].calibration) - - def test_has_calibration(self): - target = Target() - properties = { - (0,): InstructionProperties(duration=100, error=0.1), - (1,): None, - } - target.add_instruction(XGate(), properties) - - with self.assertWarns(DeprecationWarning): - # Test false for properties with no calibration - self.assertFalse(target.has_calibration("x", (0,))) - # Test false for no properties - self.assertFalse(target.has_calibration("x", (1,))) - - with self.assertWarns(DeprecationWarning): - properties = { - (0,): InstructionProperties( - duration=self.custom_sx_q0.duration, - error=None, - calibration=self.custom_sx_q0, - ) - } - target.add_instruction(SXGate(), properties) - - # Test true for properties with calibration - with self.assertWarns(DeprecationWarning): - self.assertTrue(target.has_calibration("sx", (0,))) - - def test_loading_legacy_ugate_instmap(self): - # This is typical IBM backend situation. - # IBM provider used to have u1, u2, u3 in the basis gates and - # these have been replaced with sx and rz. - # However, IBM provider still provides calibration of these u gates, - # and the inst map loads them as backend calibrations. - # Target is implicitly updated with inst map when it is set in transpile. - # If u gates are not excluded, they may appear in the transpiled circuit. - # These gates are no longer supported by hardware. - with self.assertWarns(DeprecationWarning): - entry = ScheduleDef() - entry.define(pulse.Schedule(name="fake_u3"), user_provided=False) # backend provided - instmap = InstructionScheduleMap() - instmap._add("u3", (0,), entry) - - # Today's standard IBM backend target with sx, rz basis - target = Target() - target.add_instruction(SXGate(), {(0,): InstructionProperties()}) - target.add_instruction(RZGate(Parameter("θ")), {(0,): InstructionProperties()}) - target.add_instruction(Measure(), {(0,): InstructionProperties()}) - names_before = set(target.operation_names) - - with self.assertWarns(DeprecationWarning): - target.update_from_instruction_schedule_map(instmap) - names_after = set(target.operation_names) - - # Otherwise u3 and sx-rz basis conflict in 1q decomposition. - self.assertSetEqual(names_before, names_after) - - class TestGlobalVariableWidthOperations(QiskitTestCase): def setUp(self): super().setUp() diff --git a/test/python/transpiler/test_vf2_layout.py b/test/python/transpiler/test_vf2_layout.py index 5257a9d1cee2..9fbc9ac45480 100644 --- a/test/python/transpiler/test_vf2_layout.py +++ b/test/python/transpiler/test_vf2_layout.py @@ -713,8 +713,7 @@ def test_reasonable_limits_for_simple_layouts_v1(self): def test_reasonable_limits_for_simple_layouts(self): """Test that the default trials is set to a reasonable number.""" - with self.assertWarns(DeprecationWarning): - backend = GenericBackendV2(27, calibrate_instructions=True, seed=42) + backend = GenericBackendV2(27, seed=42) qc = QuantumCircuit(5) qc.cx(2, 3) qc.cx(0, 1) From 8d7404b8d462bed4c2072baa26e97b13447f9871 Mon Sep 17 00:00:00 2001 From: "Kevin J. Sung" Date: Thu, 13 Feb 2025 12:06:29 -0500 Subject: [PATCH 33/48] delete erroneous note in XXMinusYY documentation (#13837) --- .../library/standard_gates/xx_minus_yy.py | 30 ------------------- 1 file changed, 30 deletions(-) diff --git a/qiskit/circuit/library/standard_gates/xx_minus_yy.py b/qiskit/circuit/library/standard_gates/xx_minus_yy.py index 9e6be64f6570..1fa040ba6be2 100644 --- a/qiskit/circuit/library/standard_gates/xx_minus_yy.py +++ b/qiskit/circuit/library/standard_gates/xx_minus_yy.py @@ -63,36 +63,6 @@ class XXMinusYYGate(Gate): 0 & 0 & 1 & 0 \\ -i\sin\left(\rotationangle\right)e^{i\beta} & 0 & 0 & \cos\left(\rotationangle\right) \end{pmatrix} - - .. note:: - - In Qiskit's convention, higher qubit indices are more significant - (little endian convention). In the above example we apply the gate - on (q_0, q_1) which results in adding the (optional) phase defined - by :math:`\beta` on q_1. Instead, if we apply it on (q_1, q_0), the - phase is added on q_0. If :math:`\beta` is set to its default value - of :math:`0`, the gate is equivalent in big and little endian. - - .. code-block:: text - - ┌───────────────┐ - q_0: ┤1 ├ - │ (XX-YY)(θ,β) │ - q_1: ┤0 ├ - └───────────────┘ - - .. math:: - - \newcommand{\rotationangle}{\frac{\theta}{2}} - - R_{XX-YY}(\theta, \beta) q_1, q_0 = - RZ_0(\beta) \cdot \exp\left(-i \frac{\theta}{2} \frac{XX-YY}{2}\right) \cdot RZ_0(-\beta) = - \begin{pmatrix} - \cos\left(\rotationangle\right) & 0 & 0 & -i\sin\left(\rotationangle\right)e^{i\beta} \\ - 0 & 1 & 0 & 0 \\ - 0 & 0 & 1 & 0 \\ - -i\sin\left(\rotationangle\right)e^{-i\beta} & 0 & 0 & \cos\left(\rotationangle\right) - \end{pmatrix} """ _standard_gate = StandardGate.XXMinusYYGate From 6169ba4e0218319af252183b424c8d4a88b1690c Mon Sep 17 00:00:00 2001 From: Shelly Garion <46566946+ShellyGarion@users.noreply.github.com> Date: Thu, 13 Feb 2025 22:38:21 +0200 Subject: [PATCH 34/48] Small improvement for multi-controlled 1-qubit gates (#13824) * improve MCH decomposition * update release notes * added tests --- qiskit/circuit/add_control.py | 7 ++- ...-one-qubit-unitaries-3ae333a106274b79.yaml | 2 +- test/python/circuit/test_controlled_gate.py | 43 ++++++++++++++++--- 3 files changed, 45 insertions(+), 7 deletions(-) diff --git a/qiskit/circuit/add_control.py b/qiskit/circuit/add_control.py index 8111fed9f700..fc33d7daa32f 100644 --- a/qiskit/circuit/add_control.py +++ b/qiskit/circuit/add_control.py @@ -266,8 +266,13 @@ def apply_basic_controlled_gate(circuit, gate, controls, target): circuit.s(target) elif gate.name == "h": - circuit.mcry(pi / 2, controls, target, use_basis_gates=False) + circuit.s(target) + circuit.h(target) + circuit.t(target) circuit.mcx(controls, target) + circuit.tdg(target) + circuit.h(target) + circuit.sdg(target) elif gate.name == "sx": circuit.h(target) diff --git a/releasenotes/notes/improve-decomposition-controlled-one-qubit-unitaries-3ae333a106274b79.yaml b/releasenotes/notes/improve-decomposition-controlled-one-qubit-unitaries-3ae333a106274b79.yaml index e7217229f3be..8da7e65918bf 100644 --- a/releasenotes/notes/improve-decomposition-controlled-one-qubit-unitaries-3ae333a106274b79.yaml +++ b/releasenotes/notes/improve-decomposition-controlled-one-qubit-unitaries-3ae333a106274b79.yaml @@ -5,6 +5,6 @@ features_circuits: single-qubit unitary gates. For example, * For multi-controlled :class:`.YGate` on 10 qubits, we reduce the :class:`.CXGate` count by 56%, - * For multi-controlled :class:`.HGate` on 10 qubits, we reduce the :class:`.CXGate` count by 44%, + * For multi-controlled :class:`.HGate` on 10 qubits, we reduce the :class:`.CXGate` count by 56%, * For multi-controlled :class:`.SXGate` and :class:`.SXdgGate` on 10 qubits, we reduce the :class:`.CXGate` count by 80%, * For multi-controlled :class:`.UGate` on 10 qubits, we reduce the :class:`.CXGate` count by 31%. diff --git a/test/python/circuit/test_controlled_gate.py b/test/python/circuit/test_controlled_gate.py index 4b3e69d11e99..02190e07f882 100644 --- a/test/python/circuit/test_controlled_gate.py +++ b/test/python/circuit/test_controlled_gate.py @@ -63,8 +63,12 @@ CRZGate, CU3Gate, CUGate, + SGate, + SdgGate, SXGate, SXdgGate, + TGate, + TdgGate, CSXGate, MSGate, Barrier, @@ -710,18 +714,47 @@ def test_mcx_gates_yield_explicit_gates(self, num_ctrl_qubits): explicit = {1: CXGate, 2: CCXGate} self.assertEqual(cls, explicit[num_ctrl_qubits]) - @data(1, 2, 3, 4) - def test_small_mcx_gates_yield_cx_count(self, num_ctrl_qubits): - """Test the creating a MCX gate with small number of controls (with no ancillas) - yields the expected number of cx gates.""" + @combine(num_ctrl_qubits=[1, 2, 3, 4], base_gate=[XGate(), YGate(), ZGate(), HGate()]) + def test_small_mcx_gates_yield_cx_count(self, num_ctrl_qubits, base_gate): + """Test the creating a MCX gate (and other locally equivalent multi-controlled gates) + with small number of controls (with no ancillas) yields the expected number of cx gates + and provides the correct unitary. + """ qc = QuantumCircuit(num_ctrl_qubits + 1) - qc.append(MCXGate(num_ctrl_qubits), range(num_ctrl_qubits + 1)) + qc.append(base_gate.control(num_ctrl_qubits), range(num_ctrl_qubits + 1)) + + base_mat = base_gate.to_matrix() + test_op = Operator(qc) + cop_mat = _compute_control_matrix(base_mat, num_ctrl_qubits) + self.assertTrue(matrix_equal(cop_mat, test_op.data)) cqc = transpile(qc, basis_gates=["u", "cx"]) cx_count = cqc.count_ops()["cx"] expected = {1: 1, 2: 6, 3: 14, 4: 36} self.assertEqual(cx_count, expected[num_ctrl_qubits]) + @combine( + num_ctrl_qubits=[1, 2, 3, 4], + base_gate=[PhaseGate(0.123), SGate(), SdgGate(), TGate(), TdgGate(), SXGate(), SXdgGate()], + ) + def test_small_mcp_gates_yield_cx_count(self, num_ctrl_qubits, base_gate): + """Test the creating a MCPhase gate (and other locally equivalent multi-controlled gates) + with small number of controls (with no ancillas) yields the expected number of cx gates + and provides the correct unitary. + """ + qc = QuantumCircuit(num_ctrl_qubits + 1) + qc.append(base_gate.control(num_ctrl_qubits), range(num_ctrl_qubits + 1)) + + base_mat = base_gate.to_matrix() + test_op = Operator(qc) + cop_mat = _compute_control_matrix(base_mat, num_ctrl_qubits) + self.assertTrue(matrix_equal(cop_mat, test_op.data)) + + cqc = transpile(qc, basis_gates=["u", "cx"]) + cx_count = cqc.count_ops()["cx"] + expected = {1: 2, 2: 6, 3: 20, 4: 44} + self.assertEqual(cx_count, expected[num_ctrl_qubits]) + @data(1, 2, 3, 4) def test_mcxgraycode_gates_yield_explicit_gates(self, num_ctrl_qubits): """Test an MCXGrayCode yields explicit definition.""" From 6842268a591b43e494e4dc8dcca5c845d70ecdfa Mon Sep 17 00:00:00 2001 From: Justin Woodring Date: Thu, 13 Feb 2025 16:24:36 -0600 Subject: [PATCH 35/48] Fix incorrect stop time bug #8729 (#11097) * Fix incorrect stop time bug #8729 * Apply suggestions from code review Minor update on the release note * Update qiskit/circuit/quantumcircuit.py Co-authored-by: Luciano Bello * Update releasenotes/notes/fix-incorrect-qubit-stop-time-d0e056f60a01dd52.yaml Co-authored-by: Luciano Bello --------- Co-authored-by: Toshinari Itoko <15028342+itoko@users.noreply.github.com> Co-authored-by: Luciano Bello --- qiskit/circuit/quantumcircuit.py | 5 ++++- .../fix-incorrect-qubit-stop-time-d0e056f60a01dd52.yaml | 7 +++++++ test/python/circuit/test_scheduled_circuit.py | 1 + 3 files changed, 12 insertions(+), 1 deletion(-) create mode 100644 releasenotes/notes/fix-incorrect-qubit-stop-time-d0e056f60a01dd52.yaml diff --git a/qiskit/circuit/quantumcircuit.py b/qiskit/circuit/quantumcircuit.py index 7ab1bae5d8cd..21709fad4333 100644 --- a/qiskit/circuit/quantumcircuit.py +++ b/qiskit/circuit/quantumcircuit.py @@ -6916,7 +6916,10 @@ def qubit_stop_time(self, *qubits: Union[Qubit, int]) -> float: if len(qubits) == len([done for done in dones.values() if done]): # all done return max(stop for stop in stops.values()) - return 0 # If there are no instructions over bits + if len(stops) > 0: # not all but some qubits has instructions + return max(stops.values()) + else: + return 0 # If there are no instructions over bits class _OuterCircuitScopeInterface(CircuitScopeInterface): diff --git a/releasenotes/notes/fix-incorrect-qubit-stop-time-d0e056f60a01dd52.yaml b/releasenotes/notes/fix-incorrect-qubit-stop-time-d0e056f60a01dd52.yaml new file mode 100644 index 000000000000..e9fa92f3f5a8 --- /dev/null +++ b/releasenotes/notes/fix-incorrect-qubit-stop-time-d0e056f60a01dd52.yaml @@ -0,0 +1,7 @@ +--- +fixes: + - | + An issue where :meth:`.QuantumCircuit.qubit_stop_time` and + :meth:`.QuantumCircuit.qubit_duration` returned incorrect time (duration) was fixed. + It was triggered when some qubits have instructions but other qubits are idle. + Fixes `#8729 `__. diff --git a/test/python/circuit/test_scheduled_circuit.py b/test/python/circuit/test_scheduled_circuit.py index f72b6d2adb23..c09265d51b8b 100644 --- a/test/python/circuit/test_scheduled_circuit.py +++ b/test/python/circuit/test_scheduled_circuit.py @@ -352,6 +352,7 @@ def test_per_qubit_durations_loose_constrain(self): self.assertEqual(sc.qubit_stop_time(2), 0) self.assertEqual(sc.qubit_start_time(0, 1), 300) self.assertEqual(sc.qubit_stop_time(0, 1), 1400) + self.assertEqual(sc.qubit_stop_time(0, 1, 2), 1400) qc.measure_all() From 8a7803e2dc3f29e07726bff210aaee5b1fd4d8bb Mon Sep 17 00:00:00 2001 From: gadial Date: Fri, 14 Feb 2025 11:54:17 +0200 Subject: [PATCH 36/48] Update Split2QUnitaries to handle SWAP unitary gates (#13531) * Update Split2QUnitaries to handle SWAP unitaries as well * Linting, additional test, bugfix in layout composition * Docstring updates * Add a flag for toggling swap split * Changes according to PR review * Fix indexing in pass --------- Co-authored-by: Matthew Treinish --- crates/accelerate/src/split_2q_unitaries.rs | 116 ++++++++++++++++-- .../passes/optimization/split_2q_unitaries.py | 29 ++++- .../preset_passmanagers/builtin_plugins.py | 11 +- ...-unitaries-with-swap-557a1252e3208257.yaml | 10 ++ .../transpiler/test_split_2q_unitaries.py | 111 +++++++++++++++++ 5 files changed, 261 insertions(+), 16 deletions(-) create mode 100644 releasenotes/notes/update-split-2q-unitaries-with-swap-557a1252e3208257.yaml diff --git a/crates/accelerate/src/split_2q_unitaries.rs b/crates/accelerate/src/split_2q_unitaries.rs index 366a333c7a78..542d76280de8 100644 --- a/crates/accelerate/src/split_2q_unitaries.rs +++ b/crates/accelerate/src/split_2q_unitaries.rs @@ -9,6 +9,8 @@ // 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. +use std::f64::consts::PI; +const PI4: f64 = PI / 4.; use nalgebra::Matrix2; use num_complex::Complex64; @@ -16,9 +18,11 @@ use pyo3::prelude::*; use rustworkx_core::petgraph::stable_graph::NodeIndex; use smallvec::{smallvec, SmallVec}; +use qiskit_circuit::circuit_instruction::ExtraInstructionAttributes; use qiskit_circuit::dag_circuit::{DAGCircuit, NodeType, Wire}; use qiskit_circuit::operations::{ArrayType, Operation, OperationRef, Param, UnitaryGate}; use qiskit_circuit::packed_instruction::PackedOperation; +use qiskit_circuit::Qubit; use crate::two_qubit_decompose::{Specialization, TwoQubitWeylDecomposition}; @@ -27,12 +31,13 @@ pub fn split_2q_unitaries( py: Python, dag: &mut DAGCircuit, requested_fidelity: f64, -) -> PyResult<()> { + split_swaps: bool, +) -> PyResult)>> { if !dag.get_op_counts().contains_key("unitary") { - return Ok(()); + return Ok(None); } let nodes: Vec = dag.op_node_indices(false).collect(); - + let mut has_swaps = false; for node in nodes { if let NodeType::Operation(inst) = &dag[node] { let qubits = dag.get_qargs(inst.qubits).to_vec(); @@ -51,6 +56,9 @@ pub fn split_2q_unitaries( Some(requested_fidelity), None, )?; + if matches!(decomp.specialization, Specialization::SWAPEquiv) { + has_swaps = true; + } if matches!(decomp.specialization, Specialization::IdEquiv) { let k1r_arr = decomp.k1r_view(); let k1l_arr = decomp.k1l_view(); @@ -59,8 +67,8 @@ pub fn split_2q_unitaries( if let Wire::Qubit(qubit) = edge { if *qubit == qubits[0] { let mat: Matrix2 = [ - [k1r_arr[[0, 0]], k1r_arr[[0, 1]]], - [k1r_arr[[1, 0]], k1r_arr[[1, 1]]], + [k1r_arr[[0, 0]], k1r_arr[[1, 0]]], + [k1r_arr[[0, 1]], k1r_arr[[1, 1]]], ] .into(); let k1r_gate = Box::new(UnitaryGate { @@ -69,8 +77,8 @@ pub fn split_2q_unitaries( (PackedOperation::from_unitary(k1r_gate), smallvec![]) } else { let mat: Matrix2 = [ - [k1l_arr[[0, 0]], k1l_arr[[0, 1]]], - [k1l_arr[[1, 0]], k1l_arr[[1, 1]]], + [k1l_arr[[0, 0]], k1l_arr[[1, 0]]], + [k1l_arr[[0, 1]], k1l_arr[[1, 1]]], ] .into(); @@ -87,12 +95,98 @@ pub fn split_2q_unitaries( dag.replace_node_with_1q_ops(py, node, insert_fn)?; dag.add_global_phase(py, &Param::Float(decomp.global_phase))?; } - // TODO: also look into splitting on Specialization::Swap and just - // swap the virtual qubits. Doing this we will need to update the - // permutation like in ElidePermutations } } - Ok(()) + if !split_swaps || !has_swaps { + return Ok(None); + } + // We have swap-like unitaries, so we create a new DAG in a manner similar to + // The Elide Permutations pass, while also splitting the unitaries to 1-qubit gates + let mut mapping: Vec = (0..dag.num_qubits()).collect(); + let mut new_dag = dag.copy_empty_like(py, "alike")?; + for node in dag.topological_op_nodes()? { + if let NodeType::Operation(inst) = &dag.dag()[node] { + let qubits = dag.get_qargs(inst.qubits).to_vec(); + if qubits.len() == 2 && inst.op.name() == "unitary" { + let matrix = inst + .op + .matrix(inst.params_view()) + .expect("'unitary' gates should always have a matrix form"); + let decomp = TwoQubitWeylDecomposition::new_inner( + matrix.view(), + Some(requested_fidelity), + None, + )?; + if matches!(decomp.specialization, Specialization::SWAPEquiv) { + let k1r_arr = decomp.k1r_view(); + let k1r_mat: Matrix2 = [ + [k1r_arr[[0, 0]], k1r_arr[[1, 0]]], + [k1r_arr[[0, 1]], k1r_arr[[1, 1]]], + ] + .into(); + let k1r_gate = Box::new(UnitaryGate { + array: ArrayType::OneQ(k1r_mat), + }); + let k1l_arr = decomp.k1l_view(); + let k1l_mat: Matrix2 = [ + [k1l_arr[[0, 0]], k1l_arr[[1, 0]]], + [k1l_arr[[0, 1]], k1l_arr[[1, 1]]], + ] + .into(); + let k1l_gate = Box::new(UnitaryGate { + array: ArrayType::OneQ(k1l_mat), + }); + // perform the virtual swap + let qargs = dag.get_qargs(inst.qubits); + let index0 = qargs[0].index(); + let index1 = qargs[1].index(); + mapping.swap(index0, index1); + // now add the two 1-qubit gates + new_dag.apply_operation_back( + py, + PackedOperation::from_unitary(k1r_gate), + &[Qubit::new(mapping[index0])], + &[], + None, + ExtraInstructionAttributes::default(), + #[cfg(feature = "cache_pygates")] + None, + )?; + new_dag.apply_operation_back( + py, + PackedOperation::from_unitary(k1l_gate), + &[Qubit::new(mapping[index1])], + &[], + None, + ExtraInstructionAttributes::default(), + #[cfg(feature = "cache_pygates")] + None, + )?; + new_dag.add_global_phase(py, &Param::Float(decomp.global_phase + PI4))?; + continue; // skip the general instruction handling code + } + } + // General instruction + let qargs = dag.get_qargs(inst.qubits); + let cargs = dag.get_cargs(inst.clbits); + let mapped_qargs: Vec = qargs + .iter() + .map(|q| Qubit::new(mapping[q.index()])) + .collect(); + + new_dag.apply_operation_back( + py, + inst.op.clone(), + &mapped_qargs, + cargs, + inst.params.as_deref().cloned(), + inst.extra_attrs.clone(), + #[cfg(feature = "cache_pygates")] + inst.py_op.get().map(|x| x.clone_ref(py)), + )?; + } + } + Ok(Some((new_dag, mapping))) } pub fn split_2q_unitaries_mod(m: &Bound) -> PyResult<()> { diff --git a/qiskit/transpiler/passes/optimization/split_2q_unitaries.py b/qiskit/transpiler/passes/optimization/split_2q_unitaries.py index f6958a00a4c1..ee7dbe3af2d9 100644 --- a/qiskit/transpiler/passes/optimization/split_2q_unitaries.py +++ b/qiskit/transpiler/passes/optimization/split_2q_unitaries.py @@ -13,6 +13,7 @@ """Splits each two-qubit gate in the `dag` into two single-qubit gates, if possible without error.""" from qiskit.transpiler.basepasses import TransformationPass +from qiskit.transpiler.layout import Layout from qiskit.dagcircuit.dagcircuit import DAGCircuit from qiskit._accelerate.split_2q_unitaries import split_2q_unitaries @@ -24,17 +25,39 @@ class Split2QUnitaries(TransformationPass): matrix is actually a product of 2 single qubit gates. In these cases the 2q gate can be simplified into two single qubit gates and this pass will perform this optimization and will replace the two qubit gate with two single qubit :class:`.UnitaryGate`. + + If some of the gates can be viewed as a swap joined by the product of 2 single qubit gates, + the pass will recreate the DAG, permuting the swapped qubits similar + to how it's done in :class:`ElidePermutations`. """ - def __init__(self, fidelity: float = 1.0 - 1e-16): + def __init__(self, fidelity: float = 1.0 - 1e-16, split_swap: bool = False): """ Args: fidelity: Allowed tolerance for splitting two-qubit unitaries and gate decompositions. + split_swap: Whether to attempt to split swap gates, resulting in a permutation of the qubits. """ super().__init__() self.requested_fidelity = fidelity + self.split_swap = split_swap def run(self, dag: DAGCircuit) -> DAGCircuit: """Run the Split2QUnitaries pass on `dag`.""" - split_2q_unitaries(dag, self.requested_fidelity) - return dag + result = split_2q_unitaries(dag, self.requested_fidelity, self.split_swap) + if result is None: + return dag + + (new_dag, qubit_mapping) = result + input_qubit_mapping = {qubit: index for index, qubit in enumerate(dag.qubits)} + self.property_set["original_layout"] = Layout(input_qubit_mapping) + if self.property_set["original_qubit_indices"] is None: + self.property_set["original_qubit_indices"] = input_qubit_mapping + + new_layout = Layout({dag.qubits[out]: idx for idx, out in enumerate(qubit_mapping)}) + if current_layout := self.property_set["virtual_permutation_layout"]: + self.property_set["virtual_permutation_layout"] = new_layout.compose( + current_layout, dag.qubits + ) + else: + self.property_set["virtual_permutation_layout"] = new_layout + return new_dag diff --git a/qiskit/transpiler/preset_passmanagers/builtin_plugins.py b/qiskit/transpiler/preset_passmanagers/builtin_plugins.py index d1ffa8d3d1b0..a120a14c51a4 100644 --- a/qiskit/transpiler/preset_passmanagers/builtin_plugins.py +++ b/qiskit/transpiler/preset_passmanagers/builtin_plugins.py @@ -188,10 +188,17 @@ def pass_manager(self, pass_manager_config, optimization_level=None) -> PassMana # error rates in the target. However, in the init stage we don't yet know the target # qubits being used to figure out the fidelity so just use the default fidelity parameter # in this case. + split_2q_unitaries_swap = False + if pass_manager_config.routing_method != "none": + split_2q_unitaries_swap = True if pass_manager_config.approximation_degree is not None: - init.append(Split2QUnitaries(pass_manager_config.approximation_degree)) + init.append( + Split2QUnitaries( + pass_manager_config.approximation_degree, split_swap=split_2q_unitaries_swap + ) + ) else: - init.append(Split2QUnitaries()) + init.append(Split2QUnitaries(split_swap=split_2q_unitaries_swap)) else: raise TranspilerError(f"Invalid optimization level {optimization_level}") return init diff --git a/releasenotes/notes/update-split-2q-unitaries-with-swap-557a1252e3208257.yaml b/releasenotes/notes/update-split-2q-unitaries-with-swap-557a1252e3208257.yaml new file mode 100644 index 000000000000..688e4990e195 --- /dev/null +++ b/releasenotes/notes/update-split-2q-unitaries-with-swap-557a1252e3208257.yaml @@ -0,0 +1,10 @@ +--- +features_transpiler: + - | + The :class:`.Split2QUnitaries` transpiler pass has been upgraded to + handle the case where the unitary in consideration can be written + as a SWAP gate and two 1-qubit gates. In this case, it splits the + unitary and also applies virtual swapping similar to what is done in + :class:`.ElidePermutations`. This functionality can be controlled with a new argument, + ``split_swap``, on the constructor of :class`.Split2QUnitaries` which can be used to disable + splitting swap equivalent gates. diff --git a/test/python/transpiler/test_split_2q_unitaries.py b/test/python/transpiler/test_split_2q_unitaries.py index f5727bf5313a..ed275434b4ef 100644 --- a/test/python/transpiler/test_split_2q_unitaries.py +++ b/test/python/transpiler/test_split_2q_unitaries.py @@ -266,3 +266,114 @@ def to_matrix(self): no_split = Split2QUnitaries()(qc) self.assertDictEqual({"mygate": 1}, no_split.count_ops()) + + def test_2q_swap(self): + """Test that a 2q unitary matching a swap gate is correctly processed.""" + qc = QuantumCircuit(2) + qc.swap(0, 1) + qc.global_phase += 1.2345 + qc_split = QuantumCircuit(2) + qc_split.append(UnitaryGate(Operator(qc)), [0, 1]) + + pm = PassManager() + pm.append(Collect2qBlocks()) + pm.append(ConsolidateBlocks()) + pm.append(Split2QUnitaries(split_swap=True)) + res = pm.run(qc_split) + res_op = Operator.from_circuit(res) + expected_op = Operator(qc_split) + self.assertNotIn("swap", res.count_ops()) + self.assertEqual( + res.count_ops()["unitary"], 2 + ) # the original 2-qubit unitary should be split into 2 1-qubit unitaries. + self.assertTrue(expected_op.equiv(res_op)) + self.assertTrue(matrix_equal(expected_op.data, res_op.data, ignore_phase=False)) + + def test_2q_swap_with_1_qubit_gates(self): + """Test that a 2q unitary matching a swap gate with 1-qubit + gates before and after is correctly processed.""" + qc = QuantumCircuit(2) + qc.h(0) + qc.x(1) + qc.swap(0, 1) + qc.sx(0) + qc.sdg(1) + qc.global_phase += 1.2345 + qc_split = QuantumCircuit(2) + qc_split.append(UnitaryGate(Operator(qc)), [0, 1]) + + pm = PassManager() + pm.append(Collect2qBlocks()) + pm.append(ConsolidateBlocks()) + pm.append(Split2QUnitaries(split_swap=True)) + res = pm.run(qc_split) + res_op = Operator.from_circuit(res) + expected_op = Operator(qc_split) + + self.assertNotIn("swap", res.count_ops()) + self.assertEqual( + res.count_ops()["unitary"], 2 + ) # the original 2-qubit unitary should be split into 2 1-qubit unitaries. + self.assertTrue(expected_op.equiv(res_op)) + self.assertTrue(matrix_equal(expected_op.data, res_op.data, ignore_phase=False)) + + def test_2q_swap_with_non_unitary_swaps(self): + """Test a 2q unitary matching a swap gate in a + circuit containing explicit swap gates.""" + qc = QuantumCircuit(2) + qc.swap(0, 1) + + qc_split = QuantumCircuit(3) + qc_split.swap(0, 2) + qc_split.swap(0, 1) + + qc_split.append(UnitaryGate(Operator(qc)), [0, 1]) + + pm = PassManager() + pm.append(Collect2qBlocks()) + pm.append(ConsolidateBlocks()) + pm.append(Split2QUnitaries(split_swap=True)) + res = pm.run(qc_split) + res_op = Operator.from_circuit(res) + expected_op = Operator(qc_split) + + self.assertEqual( + res.count_ops()["unitary"], 2 + ) # the original 2-qubit unitary should be split into 2 1-qubit unitaries. + self.assertTrue(expected_op.equiv(res_op)) + self.assertTrue(matrix_equal(expected_op.data, res_op.data, ignore_phase=False)) + + def test_2q_swap_with_large_circuit(self): + """Test that a 2q unitary matching a swap gate with 1-qubit gates is + correctly processed as part of a full transpilation pass on a large circuit.""" + qc = QuantumCircuit(2) + qc.h(0) + qc.x(1) + qc.swap(0, 1) + qc.sx(0) + qc.sdg(1) + qc.global_phase += 1.2345 + + qc_split = QuantumCircuit(5) + qc_split.h(0) + qc_split.swap(0, 2) + qc_split.cx(0, 1) + qc_split.swap(1, 0) + qc_split.cx(0, 1) + qc_split.append(UnitaryGate(Operator(qc)), [3, 1]) + qc_split.cx(0, 2) + qc_split.cx(0, 3) + qc_split.cx(0, 4) + qc_split.h(1) + + pm = PassManager() + pm.append(Split2QUnitaries(split_swap=True)) + res = pm.run(qc_split) + res_op = Operator.from_circuit(res) + expected_op = Operator(qc_split) + + self.assertEqual( + res.count_ops()["unitary"], 2 + ) # the original 2-qubit unitary should be split into 2 1-qubit unitaries. + self.assertTrue(expected_op.equiv(res_op)) + self.assertTrue(matrix_equal(expected_op.data, res_op.data, ignore_phase=False)) From e6e479836f65720a7776b735bc2fd44634c6dfaf Mon Sep 17 00:00:00 2001 From: Raynel Sanchez <87539502+raynelfss@users.noreply.github.com> Date: Fri, 14 Feb 2025 06:46:28 -0500 Subject: [PATCH 37/48] Implement `CircuitData::copy_empty_like()` (#13818) * Add: Implement `CircuitData::copy_empty_like()` - Implement a separate `copy_empty_like()` method for `CircuitData`. - Separate use of `copy()` and `copy_empty_like()` for `QuantumCircuit` to avoid creating repeated instances of `CircuitData`. * Fix: Remove interner copying from `copy_empty_like()` - Revert changes to `QuantumCircuit.copy()` --- crates/circuit/src/circuit_data.rs | 26 ++++++++++++++++++-------- qiskit/circuit/quantumcircuit.py | 4 +--- 2 files changed, 19 insertions(+), 11 deletions(-) diff --git a/crates/circuit/src/circuit_data.rs b/crates/circuit/src/circuit_data.rs index be36aed73f6b..bc7d912d99c6 100644 --- a/crates/circuit/src/circuit_data.rs +++ b/crates/circuit/src/circuit_data.rs @@ -284,16 +284,10 @@ impl CircuitData { /// CircuitData: The shallow copy. #[pyo3(signature = (copy_instructions=true, deepcopy=false))] pub fn copy(&self, py: Python<'_>, copy_instructions: bool, deepcopy: bool) -> PyResult { - let mut res = CircuitData::new( - py, - Some(self.qubits.cached().bind(py)), - Some(self.clbits.cached().bind(py)), - None, - self.data.len(), - self.global_phase.clone(), - )?; + let mut res = self.copy_empty_like(py)?; res.qargs_interner = self.qargs_interner.clone(); res.cargs_interner = self.cargs_interner.clone(); + res.reserve(py, self.data().len()); res.param_table.clone_from(&self.param_table); if deepcopy { @@ -327,6 +321,22 @@ impl CircuitData { Ok(res) } + /// Performs a copy with no instruction. + /// + /// Returns: + /// CircuitData: The shallow copy. + pub fn copy_empty_like(&self, py: Python<'_>) -> PyResult { + let res = CircuitData::new( + py, + Some(self.qubits.cached().bind(py)), + Some(self.clbits.cached().bind(py)), + None, + 0, + self.global_phase.clone(), + )?; + + Ok(res) + } /// Reserves capacity for at least ``additional`` more /// :class:`.CircuitInstruction` instances to be added to this container. /// diff --git a/qiskit/circuit/quantumcircuit.py b/qiskit/circuit/quantumcircuit.py index 21709fad4333..da0749226434 100644 --- a/qiskit/circuit/quantumcircuit.py +++ b/qiskit/circuit/quantumcircuit.py @@ -3755,9 +3755,7 @@ def copy_empty_like( _copy_metadata(self, cpy, vars_mode) - cpy._data = CircuitData( - self._data.qubits, self._data.clbits, global_phase=self._data.global_phase - ) + cpy._data = self._data.copy_empty_like() if name: cpy.name = name From 1e634df711adbaf59daa5c7ac6a9e8211b32e5b0 Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Fri, 14 Feb 2025 14:52:24 +0000 Subject: [PATCH 38/48] Bump `rand` to 0.9 and associated crates (#13806) * Bump `rand` to 0.9 and associated crates This is a breaking API change, but our exposure is mostly just to some minor renames. Unfortunately the whole ecosystem hasn't migrated yet, so we're still compiling in multiple versions of `rand`, but with any luck, bumping hashbrown and dropping faer will help. * Fix random tests for new seeded behaviour * Fix mistaken test * Fix lint * Choose seed to match visual reference * Fix randomisation change in new test --- Cargo.lock | 142 +++++++++++++++--- crates/accelerate/Cargo.toml | 6 +- crates/accelerate/src/circuit_library/iqp.rs | 6 +- .../src/circuit_library/quantum_volume.rs | 4 +- crates/accelerate/src/sabre/layout.rs | 4 +- crates/accelerate/src/sabre/route.rs | 4 +- crates/accelerate/src/stochastic_swap.rs | 4 +- .../src/synthesis/clifford/random_clifford.rs | 12 +- crates/accelerate/src/synthesis/linear/lnn.rs | 4 +- .../accelerate/src/synthesis/linear/utils.rs | 4 +- crates/accelerate/src/twirling.rs | 2 +- test/python/compiler/test_compiler.py | 11 +- test/python/compiler/test_transpiler.py | 2 +- .../transpiler/test_preset_passmanagers.py | 66 +------- test/python/transpiler/test_sabre_layout.py | 28 ++-- test/python/transpiler/test_sabre_swap.py | 6 +- .../transpiler/test_swap_strategy_router.py | 7 +- .../transpiler/test_transpile_layout.py | 96 +----------- .../circuit/test_circuit_matplotlib_drawer.py | 2 +- 19 files changed, 185 insertions(+), 225 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1d757fd122b6..e7df450059b5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8,7 +8,7 @@ version = "0.7.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9" dependencies = [ - "getrandom", + "getrandom 0.2.15", "once_cell", "version_check", ] @@ -20,10 +20,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" dependencies = [ "cfg-if", - "getrandom", + "getrandom 0.2.15", "once_cell", "version_check", - "zerocopy", + "zerocopy 0.7.35", ] [[package]] @@ -366,8 +366,8 @@ dependencies = [ "num-complex", "num-traits", "paste", - "rand", - "rand_distr", + "rand 0.8.5", + "rand_distr 0.4.3", "rayon", "reborrow", "serde", @@ -542,7 +542,19 @@ checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" dependencies = [ "cfg-if", "libc", - "wasi", + "wasi 0.11.0+wasi-snapshot-preview1", +] + +[[package]] +name = "getrandom" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43a49c392881ce6d5c3b8cb70f98717b7c07aabbdff06687b9030dbfbe2725f8" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.13.3+wasi-0.2.2", + "windows-targets 0.52.6", ] [[package]] @@ -863,7 +875,7 @@ checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" dependencies = [ "bytemuck", "num-traits", - "rand", + "rand 0.8.5", ] [[package]] @@ -1056,7 +1068,7 @@ version = "0.2.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" dependencies = [ - "zerocopy", + "zerocopy 0.7.35", ] [[package]] @@ -1245,9 +1257,9 @@ dependencies = [ "pulp 0.21.4", "pyo3", "qiskit-circuit", - "rand", - "rand_distr", - "rand_pcg", + "rand 0.9.0", + "rand_distr 0.5.0", + "rand_pcg 0.9.0", "rayon", "rustiq-core", "rustworkx-core", @@ -1331,8 +1343,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "libc", - "rand_chacha", - "rand_core", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3779b94aeb87e8bd4e834cee3650289ee9e0d5677f976ecdb6d219e5f4f6cd94" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.0", + "zerocopy 0.8.17", ] [[package]] @@ -1342,7 +1365,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", - "rand_core", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.0", ] [[package]] @@ -1351,7 +1384,17 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom", + "getrandom 0.2.15", +] + +[[package]] +name = "rand_core" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b08f3c9802962f7e1b25113931d94f43ed9725bebc59db9d0c3e9a23b67e15ff" +dependencies = [ + "getrandom 0.3.1", + "zerocopy 0.8.17", ] [[package]] @@ -1361,7 +1404,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32cb0b9bc82b0a0876c2dd994a7e7a2683d3e7390ca40e6886785ef0c7e3ee31" dependencies = [ "num-traits", - "rand", + "rand 0.8.5", +] + +[[package]] +name = "rand_distr" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddc3b5afe4c995c44540865b8ca5c52e6a59fa362da96c5d30886930ddc8da1c" +dependencies = [ + "num-traits", + "rand 0.9.0", ] [[package]] @@ -1370,7 +1423,16 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "59cad018caf63deb318e5a4586d99a24424a364f40f1e5778c29aca23f4fc73e" dependencies = [ - "rand_core", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_pcg" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b48ac3f7ffaab7fac4d2376632268aa5f89abdb55f7ebf8f4d11fffccb2320f7" +dependencies = [ + "rand_core 0.9.0", ] [[package]] @@ -1487,7 +1549,7 @@ checksum = "666b580cb45b60a39f5a17b284bbe8343cfcd67929931729b4afee19ec94d308" dependencies = [ "itertools 0.10.5", "petgraph", - "rand", + "rand 0.8.5", ] [[package]] @@ -1510,8 +1572,8 @@ dependencies = [ "num-traits", "petgraph", "priority-queue", - "rand", - "rand_pcg", + "rand 0.8.5", + "rand_pcg 0.3.1", "rayon", "rayon-cond", ] @@ -1757,6 +1819,15 @@ version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +[[package]] +name = "wasi" +version = "0.13.3+wasi-0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26816d2e1a4a36a2940b96c5296ce403917633dff8f3440e9b236ed6f6bacad2" +dependencies = [ + "wit-bindgen-rt", +] + [[package]] name = "wide" version = "0.7.32" @@ -1924,6 +1995,15 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "wit-bindgen-rt" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3268f3d866458b787f390cf61f4bbb563b922d091359f9608842999eaee3943c" +dependencies = [ + "bitflags 2.8.0", +] + [[package]] name = "xshell" version = "0.2.7" @@ -1952,7 +2032,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" dependencies = [ "byteorder", - "zerocopy-derive", + "zerocopy-derive 0.7.35", +] + +[[package]] +name = "zerocopy" +version = "0.8.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa91407dacce3a68c56de03abe2760159582b846c6a4acd2f456618087f12713" +dependencies = [ + "zerocopy-derive 0.8.17", ] [[package]] @@ -1965,3 +2054,14 @@ dependencies = [ "quote", "syn 2.0.96", ] + +[[package]] +name = "zerocopy-derive" +version = "0.8.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06718a168365cad3d5ff0bb133aad346959a2074bd4a85c121255a11304a8626" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.96", +] diff --git a/crates/accelerate/Cargo.toml b/crates/accelerate/Cargo.toml index 8058139c4f2c..e64131a762bf 100644 --- a/crates/accelerate/Cargo.toml +++ b/crates/accelerate/Cargo.toml @@ -15,9 +15,9 @@ workspace = true [dependencies] rayon.workspace = true numpy.workspace = true -rand = "0.8" -rand_pcg = "0.3" -rand_distr = "0.4.3" +rand = "0.9" +rand_pcg = "0.9" +rand_distr = "0.5" ahash.workspace = true num-traits = "0.2" num-complex.workspace = true diff --git a/crates/accelerate/src/circuit_library/iqp.rs b/crates/accelerate/src/circuit_library/iqp.rs index 4cb931f8c228..d55e74754722 100644 --- a/crates/accelerate/src/circuit_library/iqp.rs +++ b/crates/accelerate/src/circuit_library/iqp.rs @@ -80,14 +80,14 @@ fn generate_random_interactions(num_qubits: u32, seed: Option) -> Array2 Pcg64Mcg::seed_from_u64(seed), - None => Pcg64Mcg::from_entropy(), + None => Pcg64Mcg::from_os_rng(), }; let mut mat = Array2::zeros((num_qubits, num_qubits)); for i in 0..num_qubits { - mat[[i, i]] = rng.gen_range(0..8) as i64; + mat[[i, i]] = rng.random_range(0..8) as i64; for j in 0..i { - mat[[i, j]] = rng.gen_range(0..8) as i64; + mat[[i, j]] = rng.random_range(0..8) as i64; mat[[j, i]] = mat[[i, j]]; } } diff --git a/crates/accelerate/src/circuit_library/quantum_volume.rs b/crates/accelerate/src/circuit_library/quantum_volume.rs index 04b4fa3d4a0c..4c7899a72af1 100644 --- a/crates/accelerate/src/circuit_library/quantum_volume.rs +++ b/crates/accelerate/src/circuit_library/quantum_volume.rs @@ -125,9 +125,9 @@ pub fn quantum_volume( } let mut outer_rng = match seed { Some(seed) => Pcg64Mcg::seed_from_u64(seed), - None => Pcg64Mcg::from_entropy(), + None => Pcg64Mcg::from_os_rng(), }; - let seed_vec: Vec = rand::distributions::Standard + let seed_vec: Vec = rand::distr::StandardUniform .sample_iter(&mut outer_rng) .take(num_unitaries) .collect(); diff --git a/crates/accelerate/src/sabre/layout.rs b/crates/accelerate/src/sabre/layout.rs index cc4ab7ff19f7..d9f6fb3c938b 100644 --- a/crates/accelerate/src/sabre/layout.rs +++ b/crates/accelerate/src/sabre/layout.rs @@ -128,10 +128,10 @@ pub fn sabre_layout_and_routing( } let outer_rng = match seed { Some(seed) => Pcg64Mcg::seed_from_u64(seed), - None => Pcg64Mcg::from_entropy(), + None => Pcg64Mcg::from_os_rng(), }; let seed_vec: Vec = outer_rng - .sample_iter(&rand::distributions::Standard) + .sample_iter(&rand::distr::StandardUniform) .take(starting_layouts.len()) .collect(); let res = if run_in_parallel && starting_layouts.len() > 1 { diff --git a/crates/accelerate/src/sabre/route.rs b/crates/accelerate/src/sabre/route.rs index a1ec511f1aad..58ae257e73fc 100644 --- a/crates/accelerate/src/sabre/route.rs +++ b/crates/accelerate/src/sabre/route.rs @@ -502,10 +502,10 @@ pub fn swap_map( }; let outer_rng = match seed { Some(seed) => Pcg64Mcg::seed_from_u64(seed), - None => Pcg64Mcg::from_entropy(), + None => Pcg64Mcg::from_os_rng(), }; let seed_vec: Vec = outer_rng - .sample_iter(&rand::distributions::Standard) + .sample_iter(&rand::distr::StandardUniform) .take(num_trials) .collect(); if run_in_parallel { diff --git a/crates/accelerate/src/stochastic_swap.rs b/crates/accelerate/src/stochastic_swap.rs index a102d4525771..91568c863b44 100644 --- a/crates/accelerate/src/stochastic_swap.rs +++ b/crates/accelerate/src/stochastic_swap.rs @@ -260,10 +260,10 @@ pub fn swap_trials( RwLock::new(&mut best_possible); let outer_rng: Pcg64Mcg = match seed { Some(seed) => Pcg64Mcg::seed_from_u64(seed), - None => Pcg64Mcg::from_entropy(), + None => Pcg64Mcg::from_os_rng(), }; let seed_vec: Vec = outer_rng - .sample_iter(&rand::distributions::Standard) + .sample_iter(&rand::distr::StandardUniform) .take(num_trials as usize) .collect(); // Run in parallel only if we're not already in a multiprocessing context diff --git a/crates/accelerate/src/synthesis/clifford/random_clifford.rs b/crates/accelerate/src/synthesis/clifford/random_clifford.rs index 57531efcea55..21478314819d 100644 --- a/crates/accelerate/src/synthesis/clifford/random_clifford.rs +++ b/crates/accelerate/src/synthesis/clifford/random_clifford.rs @@ -29,7 +29,7 @@ fn sample_qmallows(n: usize, rng: &mut Pcg64Mcg) -> (Array1, Array1 for i in 0..n { let m = n - i; let eps: f64 = 4f64.powi(-(m as i32)); - let r: f64 = rng.gen(); + let r: f64 = rng.random(); let index: usize = -((r + (1f64 - r) * eps).log2().ceil() as isize) as usize; had[i] = index < m; let k = if index < m { index } else { 2 * m - index - 1 }; @@ -44,7 +44,7 @@ fn fill_tril(mut mat: ArrayViewMut2, rng: &mut Pcg64Mcg, symmetric: bool) let n = mat.shape()[0]; for i in 0..n { for j in 0..i { - mat[[i, j]] = rng.gen(); + mat[[i, j]] = rng.random(); if symmetric { mat[[j, i]] = mat[[i, j]]; } @@ -67,20 +67,20 @@ fn inverse_tril(mat: ArrayView2) -> Array2 { pub fn random_clifford_tableau_inner(num_qubits: usize, seed: Option) -> Array2 { let mut rng = match seed { Some(seed) => Pcg64Mcg::seed_from_u64(seed), - None => Pcg64Mcg::from_entropy(), + None => Pcg64Mcg::from_os_rng(), }; let (had, perm) = sample_qmallows(num_qubits, &mut rng); let mut gamma1: Array2 = Array2::from_elem((num_qubits, num_qubits), false); for i in 0..num_qubits { - gamma1[[i, i]] = rng.gen(); + gamma1[[i, i]] = rng.random(); } fill_tril(gamma1.view_mut(), &mut rng, true); let mut gamma2: Array2 = Array2::from_elem((num_qubits, num_qubits), false); for i in 0..num_qubits { - gamma2[[i, i]] = rng.gen(); + gamma2[[i, i]] = rng.random(); } fill_tril(gamma2.view_mut(), &mut rng, true); @@ -149,7 +149,7 @@ pub fn random_clifford_tableau_inner(num_qubits: usize, seed: Option) -> Ar let random_symplectic_mat = binary_matmul_inner(table1.view(), table.view()).unwrap(); // Generate random phases - let random_phases: Array2 = Array2::from_shape_fn((2 * num_qubits, 1), |_| rng.gen()); + let random_phases: Array2 = Array2::from_shape_fn((2 * num_qubits, 1), |_| rng.random()); let random_tableau: Array2 = concatenate( Axis(1), diff --git a/crates/accelerate/src/synthesis/linear/lnn.rs b/crates/accelerate/src/synthesis/linear/lnn.rs index edb1ae0913a2..eaec1faf1bf7 100644 --- a/crates/accelerate/src/synthesis/linear/lnn.rs +++ b/crates/accelerate/src/synthesis/linear/lnn.rs @@ -255,10 +255,10 @@ fn _north_west_to_identity(n: usize, mut mat: ArrayViewMut2) -> Instructio /// [1, 1, 1, 0] /// [0, 1, 0, 0] /// [1, 0, 0, 0]] - +/// /// According to [1] the synthesis is done on the inverse matrix /// so the matrix mat is inverted at this step - +/// /// References: /// [1]: Kutin, S., Moulton, D. P., Smithline, L. (2007). /// Computation at a Distance. diff --git a/crates/accelerate/src/synthesis/linear/utils.rs b/crates/accelerate/src/synthesis/linear/utils.rs index c620acb5eec0..b4d52692abf1 100644 --- a/crates/accelerate/src/synthesis/linear/utils.rs +++ b/crates/accelerate/src/synthesis/linear/utils.rs @@ -223,14 +223,14 @@ pub fn _row_sum(row_1: ArrayView1, row_2: ArrayView1) -> Result) -> Array2 { let mut rng = match seed { Some(seed) => Pcg64Mcg::seed_from_u64(seed), - None => Pcg64Mcg::from_entropy(), + None => Pcg64Mcg::from_os_rng(), }; let mut matrix = Array2::from_elem((num_qubits, num_qubits), false); loop { for value in matrix.iter_mut() { - *value = rng.gen_bool(0.5); + *value = rng.random_bool(0.5); } let rank = compute_rank_inner(matrix.view()); diff --git a/crates/accelerate/src/twirling.rs b/crates/accelerate/src/twirling.rs index 480b3924ea76..334f8fa555b2 100644 --- a/crates/accelerate/src/twirling.rs +++ b/crates/accelerate/src/twirling.rs @@ -398,7 +398,7 @@ pub(crate) fn twirl_circuit( ) -> PyResult> { let mut rng = match seed { Some(seed) => Pcg64Mcg::seed_from_u64(seed), - None => Pcg64Mcg::from_entropy(), + None => Pcg64Mcg::from_os_rng(), }; let twirling_mask: u8 = match twirled_gate { Some(gates) => { diff --git a/test/python/compiler/test_compiler.py b/test/python/compiler/test_compiler.py index 4b16e2ee45ab..455d6b81f1b7 100644 --- a/test/python/compiler/test_compiler.py +++ b/test/python/compiler/test_compiler.py @@ -16,7 +16,7 @@ import unittest from qiskit import QuantumRegister, ClassicalRegister, QuantumCircuit -from qiskit.transpiler import PassManager +from qiskit.transpiler import PassManager, CouplingMap from qiskit.circuit.library import U1Gate, U2Gate from qiskit.compiler import transpile from qiskit.providers.basic_provider import BasicSimulator @@ -172,13 +172,12 @@ def test_example_swap_bits(self): qc.measure(qr1[j], ans[j + n]) # First version: no mapping result = backend.run( - transpile(qc, backend), coupling_map=None, shots=1024, seed_simulator=14 + transpile(qc, backend, coupling_map=None), shots=1024, seed_simulator=14 ).result() self.assertEqual(result.get_counts(qc), {"010000": 1024}) # Second version: map to coupling graph result = backend.run( transpile(qc, backend, coupling_map=coupling_map), - coupling_map=coupling_map, shots=1024, seed_simulator=14, ).result() @@ -364,7 +363,6 @@ def test_grovers_circuit(self): self.backend, coupling_map=coupling_map, ), - coupling_map=coupling_map, seed_simulator=self.seed_simulator, shots=shots, ) @@ -416,8 +414,8 @@ def test_math_domain_error(self): transpile( circ, backend=self.backend, + coupling_map=coupling_map, ), - coupling_map=coupling_map, seed_simulator=self.seed_simulator, shots=shots, ) @@ -430,7 +428,8 @@ def test_random_parameter_circuit(self): """Run a circuit with randomly generated parameters.""" qasm_dir = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "qasm") circ = QuantumCircuit.from_qasm_file(os.path.join(qasm_dir, "random_n5_d5.qasm")) - coupling_map = [[0, 1], [1, 2], [2, 3], [3, 4]] + coupling_map = CouplingMap([[0, 1], [1, 2], [2, 3], [3, 4]]) + coupling_map.make_symmetric() shots = 1024 qobj = self.backend.run( transpile(circ, backend=self.backend, coupling_map=coupling_map, seed_transpiler=42), diff --git a/test/python/compiler/test_transpiler.py b/test/python/compiler/test_transpiler.py index f1f691da5a89..a0ea0dc118da 100644 --- a/test/python/compiler/test_transpiler.py +++ b/test/python/compiler/test_transpiler.py @@ -2590,7 +2590,7 @@ def callback(**kwargs): tqc = transpile(qc, backend=backend, seed_transpiler=4242, callback=callback) self.assertTrue(vf2_post_layout_called) - self.assertEqual([0, 2, 1], _get_index_layout(tqc, qubits)) + self.assertEqual([2, 1, 0], _get_index_layout(tqc, qubits)) class StreamHandlerRaiseException(StreamHandler): diff --git a/test/python/transpiler/test_preset_passmanagers.py b/test/python/transpiler/test_preset_passmanagers.py index 5b23e777fac4..d26df3a24f66 100644 --- a/test/python/transpiler/test_preset_passmanagers.py +++ b/test/python/transpiler/test_preset_passmanagers.py @@ -976,69 +976,17 @@ def test_layout_tokyo_fully_connected_cx(self, level): for qubit_control in qr: if qubit_control != qubit_target: qc.cx(qubit_control, qubit_target) - - ancilla = QuantumRegister(15, "ancilla") - - trivial_layout = { - 0: qr[0], - 1: qr[1], - 2: qr[2], - 3: qr[3], - 4: qr[4], - 5: ancilla[0], - 6: ancilla[1], - 7: ancilla[2], - 8: ancilla[3], - 9: ancilla[4], - 10: ancilla[5], - 11: ancilla[6], - 12: ancilla[7], - 13: ancilla[8], - 14: ancilla[9], - 15: ancilla[10], - 16: ancilla[11], - 17: ancilla[12], - 18: ancilla[13], - 19: ancilla[14], - } - - sabre_layout = { - 0: ancilla[0], - 1: ancilla[1], - 2: ancilla[2], - 3: ancilla[3], - 4: ancilla[4], - 5: qr[1], - 6: qr[0], - 7: qr[4], - 8: ancilla[6], - 9: ancilla[7], - 10: qr[2], - 11: qr[3], - 12: ancilla[5], - 13: ancilla[8], - 14: ancilla[9], - 15: ancilla[10], - 16: ancilla[11], - 17: ancilla[12], - 18: ancilla[13], - 19: ancilla[14], - } - - expected_layout_level0 = trivial_layout - expected_layout_level1 = sabre_layout - expected_layout_level2 = sabre_layout - expected_layout_level3 = sabre_layout - expected_layouts = [ - expected_layout_level0, - expected_layout_level1, - expected_layout_level2, - expected_layout_level3, + [0, 1, 2, 3, 4], + [6, 5, 11, 10, 2], + [6, 5, 2, 11, 10], + [6, 5, 2, 11, 10], ] backend = GenericBackendV2(num_qubits=20, coupling_map=TOKYO_CMAP, seed=42) result = transpile(qc, backend, optimization_level=level, seed_transpiler=42) - self.assertEqual(result._layout.initial_layout._p2v, expected_layouts[level]) + self.assertEqual( + result.layout.initial_index_layout(filter_ancillas=True), expected_layouts[level] + ) @data(0, 1, 2, 3) def test_all_levels_use_trivial_if_perfect(self, level): diff --git a/test/python/transpiler/test_sabre_layout.py b/test/python/transpiler/test_sabre_layout.py index e189355d0b98..fdec5b98be85 100644 --- a/test/python/transpiler/test_sabre_layout.py +++ b/test/python/transpiler/test_sabre_layout.py @@ -66,7 +66,7 @@ def test_5q_circuit_20q_coupling(self): pass_.run(dag) layout = pass_.property_set["layout"] - self.assertEqual([layout[q] for q in circuit.qubits], [11, 10, 16, 5, 17]) + self.assertEqual([layout[q] for q in circuit.qubits], [3, 6, 8, 7, 12]) def test_6q_circuit_20q_coupling(self): """Test finds layout for 6q circuit on 20q device.""" @@ -98,7 +98,7 @@ def test_6q_circuit_20q_coupling(self): pass_.run(dag) layout = pass_.property_set["layout"] - self.assertEqual([layout[q] for q in circuit.qubits], [7, 8, 12, 6, 11, 13]) + self.assertEqual([layout[q] for q in circuit.qubits], [7, 8, 11, 12, 13, 6]) def test_6q_circuit_20q_coupling_with_partial(self): """Test finds layout for 6q circuit on 20q device.""" @@ -166,7 +166,7 @@ def test_6q_circuit_20q_coupling_with_target(self): pass_.run(dag) layout = pass_.property_set["layout"] - self.assertEqual([layout[q] for q in circuit.qubits], [7, 8, 12, 6, 11, 13]) + self.assertEqual([layout[q] for q in circuit.qubits], [7, 8, 11, 12, 13, 6]) def test_layout_with_classical_bits(self): """Test sabre layout with classical bits recreate from issue #8635.""" @@ -207,7 +207,7 @@ def test_layout_with_classical_bits(self): self.assertIsInstance(res, QuantumCircuit) layout = res._layout.initial_layout self.assertEqual( - [layout[q] for q in qc.qubits], [2, 0, 5, 1, 7, 3, 14, 6, 9, 8, 10, 11, 4, 12] + [layout[q] for q in qc.qubits], [14, 12, 5, 13, 26, 11, 19, 25, 18, 8, 17, 16, 9, 4] ) # pylint: disable=line-too-long @@ -271,7 +271,7 @@ def test_layout_many_search_trials(self): self.assertIsInstance(res, QuantumCircuit) layout = res._layout.initial_layout self.assertEqual( - [layout[q] for q in qc.qubits], [0, 12, 7, 3, 6, 11, 1, 10, 4, 9, 2, 5, 13, 8] + [layout[q] for q in qc.qubits], [0, 12, 7, 8, 6, 3, 1, 10, 4, 9, 2, 11, 13, 5] ) def test_support_var_with_rust_fastpath(self): @@ -291,7 +291,7 @@ def test_support_var_with_rust_fastpath(self): out = SabreLayout(CouplingMap.from_line(8), seed=0, swap_trials=2, layout_trials=2)(qc) self.assertIsInstance(out, QuantumCircuit) - self.assertEqual(out.layout.initial_index_layout(), [4, 5, 6, 3, 2, 0, 1, 7]) + self.assertEqual(out.layout.initial_index_layout(), [6, 5, 4, 2, 3, 0, 1, 7]) def test_support_var_with_explicit_routing_pass(self): """Test that the logic works if an explicit routing pass is given.""" @@ -372,7 +372,7 @@ def test_dual_ghz(self): ) layout_routing_pass(qc) layout = layout_routing_pass.property_set["layout"] - self.assertEqual([layout[q] for q in qc.qubits], [3, 1, 2, 5, 4, 6, 7, 8]) + self.assertEqual([layout[q] for q in qc.qubits], [3, 2, 1, 5, 4, 7, 6, 8]) def test_dual_ghz_with_wide_barrier(self): """Test a basic example with 2 circuit components and 2 cmap components.""" @@ -391,7 +391,7 @@ def test_dual_ghz_with_wide_barrier(self): ) layout_routing_pass(qc) layout = layout_routing_pass.property_set["layout"] - self.assertEqual([layout[q] for q in qc.qubits], [3, 1, 2, 5, 4, 6, 7, 8]) + self.assertEqual([layout[q] for q in qc.qubits], [3, 2, 1, 5, 4, 7, 6, 8]) def test_dual_ghz_with_intermediate_barriers(self): """Test dual ghz circuit with intermediate barriers local to each component.""" @@ -412,7 +412,7 @@ def test_dual_ghz_with_intermediate_barriers(self): ) layout_routing_pass(qc) layout = layout_routing_pass.property_set["layout"] - self.assertEqual([layout[q] for q in qc.qubits], [3, 1, 2, 5, 4, 6, 7, 8]) + self.assertEqual([layout[q] for q in qc.qubits], [3, 2, 1, 5, 4, 7, 6, 8]) def test_dual_ghz_with_intermediate_spanning_barriers(self): """Test dual ghz circuit with barrier in the middle across components.""" @@ -432,7 +432,7 @@ def test_dual_ghz_with_intermediate_spanning_barriers(self): ) layout_routing_pass(qc) layout = layout_routing_pass.property_set["layout"] - self.assertEqual([layout[q] for q in qc.qubits], [3, 1, 2, 5, 4, 6, 7, 8]) + self.assertEqual([layout[q] for q in qc.qubits], [3, 2, 1, 5, 4, 7, 6, 8]) def test_too_large_components(self): """Assert trying to run a circuit with too large a connected component raises.""" @@ -468,7 +468,7 @@ def test_with_partial_layout(self): ) pm.run(qc) layout = pm.property_set["layout"] - self.assertEqual([layout[q] for q in qc.qubits], [3, 1, 2, 5, 4, 6, 7, 8]) + self.assertEqual([layout[q] for q in qc.qubits], [3, 2, 1, 5, 4, 7, 6, 8]) def test_dag_fits_in_one_component(self): """Test that the output is valid if the DAG all fits in a single component of a disjoint @@ -482,8 +482,8 @@ def test_dag_fits_in_one_component(self): layout_routing_pass = SabreLayout(disjoint, seed=2025_02_12, swap_trials=1, layout_trials=1) out = layout_routing_pass(qc) self.assertEqual(len(out.layout.initial_layout), len(out.layout.final_layout)) - self.assertEqual(out.layout.initial_index_layout(filter_ancillas=False), [1, 0, 2, 3, 4, 5]) - self.assertEqual(out.layout.final_index_layout(filter_ancillas=False), [2, 0, 1, 3, 4, 5]) + self.assertEqual(out.layout.initial_index_layout(filter_ancillas=False), [1, 2, 0, 3, 4, 5]) + self.assertEqual(out.layout.final_index_layout(filter_ancillas=False), [0, 2, 1, 3, 4, 5]) class TestSabrePreLayout(QiskitTestCase): @@ -523,7 +523,7 @@ def test_integration_with_pass_manager(self): qct_initial_layout = qct.layout.initial_layout self.assertEqual( [qct_initial_layout[q] for q in self.circuit.qubits], - [3, 8, 7, 12, 13, 14, 18, 17, 16, 11, 10, 5, 6, 1, 2, 4], + [17, 16, 11, 12, 7, 6, 5, 1, 2, 3, 8, 9, 14, 13, 19, 18], ) diff --git a/test/python/transpiler/test_sabre_swap.py b/test/python/transpiler/test_sabre_swap.py index 23a54433ed62..fbc70fa362f9 100644 --- a/test/python/transpiler/test_sabre_swap.py +++ b/test/python/transpiler/test_sabre_swap.py @@ -757,11 +757,11 @@ def test_pre_intra_post_if_else(self): efalse_body.swap(3, 4) expected.if_else((creg[0], 0), etrue_body, efalse_body, qreg, creg[[0]]) - expected.swap(1, 2) expected.h(3) - expected.cx(3, 2) + expected.swap(2, 3) + expected.cx(2, 1) expected.barrier() - expected.measure(qreg[[2, 0, 1, 3, 4]], creg) + expected.measure(qreg[[1, 0, 3, 2, 4]], creg) self.assertEqual(dag_to_circuit(cdag), expected) def test_if_expr(self): diff --git a/test/python/transpiler/test_swap_strategy_router.py b/test/python/transpiler/test_swap_strategy_router.py index 0a800bdad642..08e9159ee6d0 100644 --- a/test/python/transpiler/test_swap_strategy_router.py +++ b/test/python/transpiler/test_swap_strategy_router.py @@ -577,9 +577,6 @@ def test_permutation_tracking(self): op = SparsePauliOp.from_list([("IZZI", 1), ("ZIIZ", 2), ("ZIZI", 3)]) circ = QAOAAnsatz(op, reps=2, mixer_operator=mixer) - expected_swap_permutation = [3, 1, 2, 0] - expected_full_permutation = [1, 3, 2, 0] - cmap = CouplingMap(couplinglist=[(0, 1), (1, 2), (2, 3)]) swap_strat = SwapStrategy(cmap, swap_layers=[[(0, 1), (2, 3)], [(1, 2)]]) @@ -600,8 +597,8 @@ def test_permutation_tracking(self): pm.pre_routing = swap_pm full = pm.run(circ.decompose()) - self.assertEqual(swapped.layout.routing_permutation(), expected_swap_permutation) - self.assertEqual(full.layout.routing_permutation(), expected_full_permutation) + self.assertEqual(swapped.layout.routing_permutation(), [3, 1, 2, 0]) + self.assertEqual(full.layout.routing_permutation(), [0, 1, 2, 3]) class TestSwapRouterExceptions(QiskitTestCase): diff --git a/test/python/transpiler/test_transpile_layout.py b/test/python/transpiler/test_transpile_layout.py index 01e06b57fee4..778ed8d467ca 100644 --- a/test/python/transpiler/test_transpile_layout.py +++ b/test/python/transpiler/test_transpile_layout.py @@ -51,29 +51,8 @@ def test_final_index_layout_full_path_with_ancilla(self): qc.cx(0, 2) cmap = CouplingMap.from_line(10, bidirectional=False) tqc = transpile(qc, coupling_map=cmap, initial_layout=[9, 4, 0], seed_transpiler=42) - # tqc: - # q_2 -> 0 ──X───────────────────────────────────────────────── - # │ - # ancilla_0 -> 1 ──X───X───────────────────────────────────────────── - # │ - # ancilla_1 -> 2 ──────X──X────────────────────────────────────────── - # │ ┌───┐ ┌───┐ - # ancilla_2 -> 3 ─────────X─┤ H ├────────────────────────────■──┤ H ├ - # ┌───┐ └───┘ ┌───┐ ┌───┐┌─┴─┐├───┤ - # q_1 -> 4 ┤ H ├─────────────────────■──┤ H ├─X─┤ H ├┤ X ├┤ H ├ - # └───┘ ┌───┐┌─┴─┐├───┤ │ └───┘└───┘└───┘ - # ancilla_3 -> 5 ─────────────────X─┤ H ├┤ X ├┤ H ├─X──────────────── - # │ └───┘└───┘└───┘ - # ancilla_4 -> 6 ─────────────X───X────────────────────────────────── - # │ - # ancilla_5 -> 7 ─────────X───X────────────────────────────────────── - # │ - # ancilla_6 -> 8 ──────X──X────────────────────────────────────────── - # ┌───┐ │ - # q_0 -> 9 ┤ H ├─X───────────────────────────────────────────── - # └───┘ res = tqc.layout.final_index_layout() - self.assertEqual(res, [4, 5, 3]) + self.assertEqual(res, [3, 5, 2]) def test_final_index_layout_full_path_with_ancilla_no_filter(self): qc = QuantumCircuit(3) @@ -82,29 +61,8 @@ def test_final_index_layout_full_path_with_ancilla_no_filter(self): qc.cx(0, 2) cmap = CouplingMap.from_line(10, bidirectional=False) tqc = transpile(qc, coupling_map=cmap, initial_layout=[9, 4, 0], seed_transpiler=42) - # tqc: - # q_2 -> 0 ──X───────────────────────────────────────────────── - # │ - # ancilla_0 -> 1 ──X───X───────────────────────────────────────────── - # │ - # ancilla_1 -> 2 ──────X──X────────────────────────────────────────── - # │ ┌───┐ ┌───┐ - # ancilla_2 -> 3 ─────────X─┤ H ├────────────────────────────■──┤ H ├ - # ┌───┐ └───┘ ┌───┐ ┌───┐┌─┴─┐├───┤ - # q_1 -> 4 ┤ H ├─────────────────────■──┤ H ├─X─┤ H ├┤ X ├┤ H ├ - # └───┘ ┌───┐┌─┴─┐├───┤ │ └───┘└───┘└───┘ - # ancilla_3 -> 5 ─────────────────X─┤ H ├┤ X ├┤ H ├─X──────────────── - # │ └───┘└───┘└───┘ - # ancilla_4 -> 6 ─────────────X───X────────────────────────────────── - # │ - # ancilla_5 -> 7 ─────────X───X────────────────────────────────────── - # │ - # ancilla_6 -> 8 ──────X──X────────────────────────────────────────── - # ┌───┐ │ - # q_0 -> 9 ┤ H ├─X───────────────────────────────────────────── - # └───┘ res = tqc.layout.final_index_layout(filter_ancillas=False) - self.assertEqual(res, [4, 5, 3, 0, 1, 2, 6, 7, 8, 9]) + self.assertEqual(res, [3, 5, 2, 0, 1, 4, 6, 7, 8, 9]) def test_final_virtual_layout_full_path_with_ancilla(self): qc = QuantumCircuit(3) @@ -113,29 +71,8 @@ def test_final_virtual_layout_full_path_with_ancilla(self): qc.cx(0, 2) cmap = CouplingMap.from_line(10, bidirectional=False) tqc = transpile(qc, coupling_map=cmap, initial_layout=[9, 4, 0], seed_transpiler=42) - # tqc: - # q_2 -> 0 ──X───────────────────────────────────────────────── - # │ - # ancilla_0 -> 1 ──X───X───────────────────────────────────────────── - # │ - # ancilla_1 -> 2 ──────X──X────────────────────────────────────────── - # │ ┌───┐ ┌───┐ - # ancilla_2 -> 3 ─────────X─┤ H ├────────────────────────────■──┤ H ├ - # ┌───┐ └───┘ ┌───┐ ┌───┐┌─┴─┐├───┤ - # q_1 -> 4 ┤ H ├─────────────────────■──┤ H ├─X─┤ H ├┤ X ├┤ H ├ - # └───┘ ┌───┐┌─┴─┐├───┤ │ └───┘└───┘└───┘ - # ancilla_3 -> 5 ─────────────────X─┤ H ├┤ X ├┤ H ├─X──────────────── - # │ └───┘└───┘└───┘ - # ancilla_4 -> 6 ─────────────X───X────────────────────────────────── - # │ - # ancilla_5 -> 7 ─────────X───X────────────────────────────────────── - # │ - # ancilla_6 -> 8 ──────X──X────────────────────────────────────────── - # ┌───┐ │ - # q_0 -> 9 ┤ H ├─X───────────────────────────────────────────── - # └───┘ res = tqc.layout.final_virtual_layout() - self.assertEqual(res, Layout({qc.qubits[0]: 4, qc.qubits[1]: 5, qc.qubits[2]: 3})) + self.assertEqual(res, Layout({qc.qubits[0]: 3, qc.qubits[1]: 5, qc.qubits[2]: 2})) def test_final_virtual_layout_full_path_with_ancilla_no_filter(self): qc = QuantumCircuit(3) @@ -144,37 +81,16 @@ def test_final_virtual_layout_full_path_with_ancilla_no_filter(self): qc.cx(0, 2) cmap = CouplingMap.from_line(10, bidirectional=False) tqc = transpile(qc, coupling_map=cmap, initial_layout=[9, 4, 0], seed_transpiler=42) - # tqc: - # q_2 -> 0 ──X───────────────────────────────────────────────── - # │ - # ancilla_0 -> 1 ──X───X───────────────────────────────────────────── - # │ - # ancilla_1 -> 2 ──────X──X────────────────────────────────────────── - # │ ┌───┐ ┌───┐ - # ancilla_2 -> 3 ─────────X─┤ H ├────────────────────────────■──┤ H ├ - # ┌───┐ └───┘ ┌───┐ ┌───┐┌─┴─┐├───┤ - # q_1 -> 4 ┤ H ├─────────────────────■──┤ H ├─X─┤ H ├┤ X ├┤ H ├ - # └───┘ ┌───┐┌─┴─┐├───┤ │ └───┘└───┘└───┘ - # ancilla_3 -> 5 ─────────────────X─┤ H ├┤ X ├┤ H ├─X──────────────── - # │ └───┘└───┘└───┘ - # ancilla_4 -> 6 ─────────────X───X────────────────────────────────── - # │ - # ancilla_5 -> 7 ─────────X───X────────────────────────────────────── - # │ - # ancilla_6 -> 8 ──────X──X────────────────────────────────────────── - # ┌───┐ │ - # q_0 -> 9 ┤ H ├─X───────────────────────────────────────────── - # └───┘ res = tqc.layout.final_virtual_layout(filter_ancillas=False) pos_to_virt = {v: k for k, v in tqc.layout.input_qubit_mapping.items()} expected = Layout( { - pos_to_virt[0]: 4, + pos_to_virt[0]: 3, pos_to_virt[1]: 5, - pos_to_virt[2]: 3, + pos_to_virt[2]: 2, pos_to_virt[3]: 0, pos_to_virt[4]: 1, - pos_to_virt[5]: 2, + pos_to_virt[5]: 4, pos_to_virt[6]: 6, pos_to_virt[7]: 7, pos_to_virt[8]: 8, diff --git a/test/visual/mpl/circuit/test_circuit_matplotlib_drawer.py b/test/visual/mpl/circuit/test_circuit_matplotlib_drawer.py index 0a2aaad0e718..f52338251438 100644 --- a/test/visual/mpl/circuit/test_circuit_matplotlib_drawer.py +++ b/test/visual/mpl/circuit/test_circuit_matplotlib_drawer.py @@ -2325,7 +2325,7 @@ def test_no_qreg_names_after_layout(self): qc.cx(1, 2) qc.cx(2, 0) circuit = transpile( - qc, backend, basis_gates=["rz", "sx", "cx"], layout_method="sabre", seed_transpiler=42 + qc, backend, basis_gates=["rz", "sx", "cx"], layout_method="sabre", seed_transpiler=15 ) fname = "qreg_names_after_layout.png" From 581dd3d84c9517df0011f0a5a2305102527d6c79 Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Fri, 14 Feb 2025 18:57:33 +0000 Subject: [PATCH 39/48] Fix Y-eigenstate convention in `SparsePauliOp.as_paulis` (#13847) `|r>` is the positive eigenstate of Y, not the negative one. The tests also asserted this backwards. --- crates/accelerate/src/sparse_observable.rs | 4 ++-- .../operators/symplectic/test_sparse_pauli_op.py | 2 +- test/python/quantum_info/test_sparse_observable.py | 10 +++++----- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/crates/accelerate/src/sparse_observable.rs b/crates/accelerate/src/sparse_observable.rs index 177ce64e04f6..1ba9a0f5b35a 100644 --- a/crates/accelerate/src/sparse_observable.rs +++ b/crates/accelerate/src/sparse_observable.rs @@ -198,8 +198,8 @@ fn bit_term_as_pauli(bit: &BitTerm) -> &'static [(bool, Option)] { BitTerm::Z => &[(true, Some(BitTerm::Z))], BitTerm::Plus => &[(true, None), (true, Some(BitTerm::X))], BitTerm::Minus => &[(true, None), (false, Some(BitTerm::X))], - BitTerm::Left => &[(true, None), (true, Some(BitTerm::Y))], - BitTerm::Right => &[(true, None), (false, Some(BitTerm::Y))], + BitTerm::Right => &[(true, None), (true, Some(BitTerm::Y))], + BitTerm::Left => &[(true, None), (false, Some(BitTerm::Y))], BitTerm::Zero => &[(true, None), (true, Some(BitTerm::Z))], BitTerm::One => &[(true, None), (false, Some(BitTerm::Z))], } diff --git a/test/python/quantum_info/operators/symplectic/test_sparse_pauli_op.py b/test/python/quantum_info/operators/symplectic/test_sparse_pauli_op.py index 0704353ab333..b64783120c7c 100644 --- a/test/python/quantum_info/operators/symplectic/test_sparse_pauli_op.py +++ b/test/python/quantum_info/operators/symplectic/test_sparse_pauli_op.py @@ -392,7 +392,7 @@ def test_from_sparse_observable(self): with self.subTest("XrZ"): obs = SparseObservable("XrZ") spo = SparsePauliOp.from_sparse_observable(obs) - expected = SparsePauliOp(["XIZ", "XYZ"], coeffs=[0.5, -0.5]) + expected = SparsePauliOp(["XIZ", "XYZ"], coeffs=[0.5, 0.5]) # we don't guarantee the order of Paulis, so check equality by comparing # the matrix representation and that all Pauli strings are present diff --git a/test/python/quantum_info/test_sparse_observable.py b/test/python/quantum_info/test_sparse_observable.py index 720bb18af46f..fde075618da9 100644 --- a/test/python/quantum_info/test_sparse_observable.py +++ b/test/python/quantum_info/test_sparse_observable.py @@ -2126,13 +2126,13 @@ def test_as_paulis(self): expected = SparseObservable.from_sparse_list( [ ("", [], 1 / 8), - ("Y", [2], -1 / 8), + ("Y", [2], 1 / 8), ("YY", [3, 2], -1 / 8), ("Z", [0], 1 / 8), - ("YZ", [2, 0], -1 / 8), + ("YZ", [2, 0], 1 / 8), ("YYZ", [3, 2, 0], -1 / 8), - ("Y", [3], 1 / 8), - ("YZ", [3, 0], 1 / 8), + ("Y", [3], -1 / 8), + ("YZ", [3, 0], -1 / 8), ], 4, ) @@ -2140,7 +2140,7 @@ def test_as_paulis(self): # test multiple terms with self.subTest(msg="+X + lY - ZI"): - obs = SparseObservable.from_list([("+X", 1), ("rY", 1), ("ZI", -1)]) + obs = SparseObservable.from_list([("+X", 1), ("lY", 1), ("ZI", -1)]) obs_paulis = obs.as_paulis() expected = SparseObservable.from_list( From 26906d335a204a8b39554fc6190c15c318bd4e9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elena=20Pe=C3=B1a=20Tapia?= <57907331+ElePT@users.noreply.github.com> Date: Mon, 17 Feb 2025 18:24:33 +0100 Subject: [PATCH 40/48] Oxidize `UnitarySynthesis` path using `basis_gates` (#13704) * Oxidize basis gates path in UnitarySynthesis pass * Fix lint * Rust refactoring: reduce code repetition, reorder file to avoid jumps in logic, add comments and docstrings * Move DefaultUnitarySynthesisPlugin to a standalone location to separate logic from UnitarySynthesis pass. This plugin is currently ONLY USED as a fallback method if a custom unitary synthesis plugin is provided and fails. We don't have any unit tests currently covering this scenario. For more context, the _run_main_loop method in UnitarySynthesis has been replaced by a Rust function when the plugin is set to default. * Fix import path in test --- crates/accelerate/src/unitary_synthesis.rs | 1018 ++++++++++------- pyproject.toml | 2 +- .../synthesis/default_unitary_synth_plugin.py | 653 +++++++++++ .../passes/synthesis/unitary_synthesis.py | 649 +---------- .../transpiler/test_unitary_synthesis.py | 4 +- .../test_unitary_synthesis_plugin.py | 2 +- 6 files changed, 1252 insertions(+), 1076 deletions(-) create mode 100644 qiskit/transpiler/passes/synthesis/default_unitary_synth_plugin.py diff --git a/crates/accelerate/src/unitary_synthesis.rs b/crates/accelerate/src/unitary_synthesis.rs index 9d167c5f083a..1d88f66e72de 100644 --- a/crates/accelerate/src/unitary_synthesis.rs +++ b/crates/accelerate/src/unitary_synthesis.rs @@ -62,53 +62,76 @@ enum DecomposerType { #[derive(Clone, Debug)] struct DecomposerElement { decomposer: DecomposerType, - gate: NormalOperation, + packed_op: PackedOperation, + params: SmallVec<[Param; 3]>, } #[derive(Clone, Debug)] struct TwoQubitUnitarySequence { gate_sequence: TwoQubitGateSequence, - decomp_gate: NormalOperation, + decomp_op: PackedOperation, + decomp_params: SmallVec<[Param; 3]>, } -// Used in get_2q_decomposers. If the found 2q basis is a subset of GOODBYE_SET, -// then we know TwoQubitBasisDecomposer is an ideal decomposition and there is -// no need to bother trying the XXDecomposer. +// These two variables are used to exit the decomposer search early in +// `get_2q_decomposers_from_target`. +// If the available 2q basis is a subset of GOODBYE_SET, TwoQubitBasisDecomposer provides +// an ideal decomposition and we can exit the decomposer search. Similarly, if it is a +// subset of PARAM_SET, TwoQubitControlledUDecomposer provides an ideal decompostion. static GOODBYE_SET: [&str; 3] = ["cx", "cz", "ecr"]; static PARAM_SET: [&str; 8] = ["rzz", "rxx", "ryy", "rzx", "crx", "cry", "crz", "cphase"]; +/// Given a list of basis gates, find a corresponding euler basis to use. +/// This will determine the available 1q synthesis basis for different decomposers. +fn get_euler_basis_set(basis_list: IndexSet<&str>) -> EulerBasisSet { + let mut euler_basis_set: EulerBasisSet = EulerBasisSet::new(); + EULER_BASES + .iter() + .enumerate() + .filter_map(|(idx, gates)| { + if !gates.iter().all(|gate| basis_list.contains(gate)) { + return None; + } + let basis = EULER_BASIS_NAMES[idx]; + Some(basis) + }) + .for_each(|basis| euler_basis_set.add_basis(basis)); + + if euler_basis_set.basis_supported(EulerBasis::U3) + && euler_basis_set.basis_supported(EulerBasis::U321) + { + euler_basis_set.remove(EulerBasis::U3); + } + if euler_basis_set.basis_supported(EulerBasis::ZSX) + && euler_basis_set.basis_supported(EulerBasis::ZSXX) + { + euler_basis_set.remove(EulerBasis::ZSX); + } + euler_basis_set +} + +/// Given a `Target`, find an euler basis that is supported for a specific `PhysicalQubit`. +/// This will determine the available 1q synthesis basis for different decomposers. fn get_target_basis_set(target: &Target, qubit: PhysicalQubit) -> EulerBasisSet { let mut target_basis_set: EulerBasisSet = EulerBasisSet::new(); let target_basis_list = target.operation_names_for_qargs(Some(&smallvec![qubit])); match target_basis_list { Ok(basis_list) => { - EULER_BASES - .iter() - .enumerate() - .filter_map(|(idx, gates)| { - if !gates.iter().all(|gate| basis_list.contains(gate)) { - return None; - } - let basis = EULER_BASIS_NAMES[idx]; - Some(basis) - }) - .for_each(|basis| target_basis_set.add_basis(basis)); + target_basis_set = get_euler_basis_set(basis_list.into_iter().collect()); + } + Err(_) => { + target_basis_set.support_all(); + target_basis_set.remove(EulerBasis::U3); + target_basis_set.remove(EulerBasis::ZSX); } - Err(_) => target_basis_set.support_all(), - } - if target_basis_set.basis_supported(EulerBasis::U3) - && target_basis_set.basis_supported(EulerBasis::U321) - { - target_basis_set.remove(EulerBasis::U3); - } - if target_basis_set.basis_supported(EulerBasis::ZSX) - && target_basis_set.basis_supported(EulerBasis::ZSXX) - { - target_basis_set.remove(EulerBasis::ZSX); } target_basis_set } +/// Apply synthesis output (`synth_dag`) to final `DAGCircuit` (`out_dag`). +/// `synth_dag` is a subgraph, and the `qubit_ids` are relative to the subgraph +/// size/orientation, so `out_qargs` is used to track the final qubit ids where +/// it should be applied. fn apply_synth_dag( py: Python<'_>, out_dag: &mut DAGCircuit, @@ -129,6 +152,10 @@ fn apply_synth_dag( Ok(()) } +/// Apply synthesis output (`sequence`) to final `DAGCircuit` (`out_dag`). +/// `sequence` contains a representation of gates to be applied to a subgraph, +/// and the `qubit_ids` are relative to the subgraph size/orientation, +/// so `out_qargs` is used to track the final qubit ids where they should be applied. fn apply_synth_sequence( py: Python<'_>, out_dag: &mut DAGCircuit, @@ -138,17 +165,17 @@ fn apply_synth_sequence( let mut instructions = Vec::with_capacity(sequence.gate_sequence.gates().len()); for (gate, params, qubit_ids) in sequence.gate_sequence.gates() { let packed_op = match gate { - None => &sequence.decomp_gate.operation, + None => &sequence.decomp_op, Some(gate) => &PackedOperation::from_standard_gate(*gate), }; let mapped_qargs: Vec = qubit_ids.iter().map(|id| out_qargs[*id as usize]).collect(); let new_params: Option>> = match gate { Some(_) => Some(Box::new(params.iter().map(|p| Param::Float(*p)).collect())), None => { - if !sequence.decomp_gate.params.is_empty() - && matches!(sequence.decomp_gate.params[0], Param::Float(_)) + if !sequence.decomp_params.is_empty() + && matches!(sequence.decomp_params[0], Param::Float(_)) { - Some(Box::new(sequence.decomp_gate.params.clone())) + Some(Box::new(sequence.decomp_params.clone())) } else { Some(Box::new(params.iter().map(|p| Param::Float(*p)).collect())) } @@ -201,76 +228,27 @@ fn apply_synth_sequence( Ok(()) } -fn synth_error( - py: Python<'_>, - synth_circuit: impl Iterator< - Item = ( - String, - Option>, - SmallVec<[PhysicalQubit; 2]>, - ), - >, - target: &Target, -) -> f64 { - let (lower_bound, upper_bound) = synth_circuit.size_hint(); - let mut gate_fidelities = match upper_bound { - Some(bound) => Vec::with_capacity(bound), - None => Vec::with_capacity(lower_bound), - }; - let mut score_instruction = - |inst_name: &str, - inst_params: &Option>, - inst_qubits: &SmallVec<[PhysicalQubit; 2]>| { - if let Ok(names) = target.operation_names_for_qargs(Some(inst_qubits)) { - for name in names { - if let Ok(target_op) = target.operation_from_name(name) { - let are_params_close = if let Some(params) = inst_params { - params.iter().zip(target_op.params.iter()).all(|(p1, p2)| { - p1.is_close(py, p2, 1e-10) - .expect("Unexpected parameter expression error.") - }) - } else { - false - }; - let is_parametrized = target_op - .params - .iter() - .any(|param| matches!(param, Param::ParameterExpression(_))); - if target_op.operation.name() == inst_name - && (is_parametrized || are_params_close) - { - match target[name].get(Some(inst_qubits)) { - Some(Some(props)) => { - gate_fidelities.push(1.0 - props.error.unwrap_or(0.0)) - } - _ => gate_fidelities.push(1.0), - } - break; - } - } - } - } - }; - - for (inst_name, inst_params, inst_qubits) in synth_circuit { - score_instruction(&inst_name, &inst_params, &inst_qubits); - } - 1.0 - gate_fidelities.into_iter().product::() -} - -// This is the outer-most run function. It is meant to be called from Python -// in `UnitarySynthesis.run()`. +/// Iterate over `DAGCircuit` to perform unitary synthesis. +/// For each elegible gate: find decomposers, select the synthesis +/// method with the highest fidelity score and apply decompositions. The available methods are: +/// * 1q synthesis: OneQubitEulerDecomposer +/// * 2q synthesis: TwoQubitBasisDecomposer, TwoQubitControlledUDecomposer, XXDecomposer (Python, only if target is provided) +/// * 3q+ synthesis: QuantumShannonDecomposer (Python) +/// This function is currently used in the Python `UnitarySynthesis`` transpiler pass as a replacement for the `_run_main_loop` method. +/// It returns a new `DAGCircuit` with the different synthesized gates. #[pyfunction] -#[pyo3(name = "run_default_main_loop", signature=(dag, qubit_indices, min_qubits, target, coupling_edges, approximation_degree=None, natural_direction=None))] +#[pyo3(name = "run_main_loop", signature=(dag, qubit_indices, min_qubits, target, basis_gates, coupling_edges, approximation_degree=None, natural_direction=None, pulse_optimize=None))] fn py_run_main_loop( py: Python, dag: &mut DAGCircuit, qubit_indices: Vec, min_qubits: usize, - target: &Target, + target: Option<&Target>, + basis_gates: HashSet, coupling_edges: HashSet<[PhysicalQubit; 2]>, approximation_degree: Option, natural_direction: Option, + pulse_optimize: Option, ) -> PyResult { // We need to use the python converter because the currently available Rust conversion // is lossy. We need `QuantumCircuit` instances to be used in `replace_blocks`. @@ -311,9 +289,11 @@ fn py_run_main_loop( new_ids, min_qubits, target, + basis_gates.clone(), coupling_edges.clone(), approximation_degree, natural_direction, + pulse_optimize, )?; new_blocks.push(dag_to_circuit.call1((res,))?); } @@ -332,7 +312,7 @@ fn py_run_main_loop( py_op: new_node.unbind().into(), }; } - if !(matches!(packed_instr.op.view(), OperationRef::Unitary(_)) + if !(packed_instr.op.name() == "unitary" && packed_instr.op.num_qubits() >= min_qubits as u32) { out_dag.push_back(py, packed_instr)?; @@ -346,7 +326,14 @@ fn py_run_main_loop( // Run 1q synthesis [2, 2] => { let qubit = dag.get_qargs(packed_instr.qubits)[0]; - let target_basis_set = get_target_basis_set(target, PhysicalQubit::new(qubit.0)); + let target_basis_set = match target { + Some(target) => get_target_basis_set(target, PhysicalQubit::new(qubit.0)), + None => { + let basis_gates: IndexSet<&str> = + basis_gates.iter().map(String::as_str).collect(); + get_euler_basis_set(basis_gates) + } + }; let sequence = unitary_to_gate_sequence_inner( unitary.view(), &target_basis_set, @@ -397,8 +384,10 @@ fn py_run_main_loop( ref_qubits, &coupling_edges, target, + basis_gates.clone(), approximation_degree, natural_direction, + pulse_optimize, &mut out_dag, out_qargs, apply_original_op, @@ -406,223 +395,130 @@ fn py_run_main_loop( } // Run 3q+ synthesis _ => { - let qs_decomposition: &Bound<'_, PyAny> = imports::QS_DECOMPOSITION.get_bound(py); - let synth_circ = qs_decomposition.call1((unitary.into_pyarray(py),))?; - let synth_dag = circuit_to_dag( - py, - QuantumCircuitData::extract_bound(&synth_circ)?, - false, - None, - None, - )?; - let out_qargs = dag.get_qargs(packed_instr.qubits); - apply_synth_dag(py, &mut out_dag, out_qargs, &synth_dag)?; + if basis_gates.is_empty() && target.is_none() { + out_dag.push_back(py, packed_instr.clone())?; + } else { + let qs_decomposition: &Bound<'_, PyAny> = + imports::QS_DECOMPOSITION.get_bound(py); + let synth_circ = qs_decomposition.call1((unitary.into_pyarray(py),))?; + let synth_dag = circuit_to_dag( + py, + QuantumCircuitData::extract_bound(&synth_circ)?, + false, + None, + None, + )?; + let out_qargs = dag.get_qargs(packed_instr.qubits); + apply_synth_dag(py, &mut out_dag, out_qargs, &synth_dag)?; + } } } } Ok(out_dag) } -fn run_2q_unitary_synthesis( - py: Python, - unitary: Array2, - ref_qubits: &[PhysicalQubit; 2], - coupling_edges: &HashSet<[PhysicalQubit; 2]>, - target: &Target, +/// Return a single decomposer for the given `basis_gates`. If no decomposer is found, +/// return `None``. If a decomposer is found, the return type will be either +/// `DecomposerElement::TwoQubitBasis` or `DecomposerElement::TwoQubitControlledU`. +fn get_2q_decomposer_from_basis( + basis_gates: IndexSet<&str>, approximation_degree: Option, - natural_direction: Option, - out_dag: &mut DAGCircuit, - out_qargs: &[Qubit], - mut apply_original_op: impl FnMut(&mut DAGCircuit) -> PyResult<()>, -) -> PyResult<()> { - let decomposers = { - let decomposers_2q = - get_2q_decomposers_from_target(py, target, ref_qubits, approximation_degree)?; - decomposers_2q.unwrap_or_default() + pulse_optimize: Option, +) -> PyResult> { + // Non-parametrized 2q basis candidates (TwoQubitBasisDecomposer) + let basis_names: IndexMap<&str, StandardGate> = [ + ("cx", StandardGate::CXGate), + ("cz", StandardGate::CZGate), + ("iswap", StandardGate::ISwapGate), + ("ecr", StandardGate::ECRGate), + ] + .into_iter() + .collect(); + // Parametrized 2q basis candidates (TwoQubitControlledUDecomposer) + let param_basis_names: IndexMap<&str, StandardGate> = [ + ("rxx", StandardGate::RXXGate), + ("rzx", StandardGate::RZXGate), + ("rzz", StandardGate::RZZGate), + ("ryy", StandardGate::RYYGate), + ("cphase", StandardGate::CPhaseGate), + ("crx", StandardGate::CRXGate), + ("cry", StandardGate::CRYGate), + ("crz", StandardGate::CRZGate), + ] + .into_iter() + .collect(); + // 1q basis (both decomposers) + let euler_basis = match get_euler_basis_set(basis_gates.clone()) + .get_bases() + .map(|basis| basis.as_str()) + .next() + { + Some(basis) => basis, + None => return Ok(None), }; - // If there's a single decomposer, avoid computing synthesis score - if decomposers.len() == 1 { - let decomposer_item = decomposers.first().unwrap(); - let preferred_dir = preferred_direction( - decomposer_item, - ref_qubits, - natural_direction, - coupling_edges, - target, - )?; - match decomposer_item.decomposer { - DecomposerType::TwoQubitBasis(_) => { - let synth = synth_su4_sequence( - &unitary, - decomposer_item, - preferred_dir, - approximation_degree, - )?; - apply_synth_sequence(py, out_dag, out_qargs, &synth)?; - } - DecomposerType::TwoQubitControlledU(_) => { - let synth = synth_su4_sequence( - &unitary, - decomposer_item, - preferred_dir, - approximation_degree, - )?; - apply_synth_sequence(py, out_dag, out_qargs, &synth)?; - } - DecomposerType::XX(_) => { - let synth = synth_su4_dag( - py, - &unitary, - decomposer_item, - preferred_dir, - approximation_degree, - )?; - apply_synth_dag(py, out_dag, out_qargs, &synth)?; - } - } - return Ok(()); - } + // Try TwoQubitControlledUDecomposer first. + let kak_gates: Vec<&str> = param_basis_names + .keys() + .copied() + .collect::>() + .intersection(&basis_gates) + .copied() + .collect(); + if !kak_gates.is_empty() { + let std_gate = *param_basis_names.get(kak_gates[0]).unwrap(); + let rxx_equivalent_gate = RXXEquivalent::Standard(std_gate); + if let Ok(decomposer) = + TwoQubitControlledUDecomposer::new_inner(rxx_equivalent_gate, euler_basis) + { + return Ok(Some(DecomposerElement { + decomposer: DecomposerType::TwoQubitControlledU(Box::new(decomposer)), + packed_op: PackedOperation::from_standard_gate(std_gate), + params: SmallVec::new(), + })); + }; + }; - let mut synth_errors_sequence = Vec::new(); - let mut synth_errors_dag = Vec::new(); - for decomposer in &decomposers { - let preferred_dir = preferred_direction( - decomposer, - ref_qubits, - natural_direction, - coupling_edges, - target, + // If there is no suitable TwoQubitControlledUDecomposer, try TwoQubitBasisDecomposer. + let kak_gates: Vec<&str> = basis_names + .keys() + .copied() + .collect::>() + .intersection(&basis_gates) + .copied() + .collect(); + if !kak_gates.is_empty() { + let std_gate = *basis_names.get(kak_gates[0]).unwrap(); + let decomposer = TwoQubitBasisDecomposer::new_inner( + std_gate.name().to_string(), + std_gate.matrix(&[]).unwrap().view(), + approximation_degree.unwrap_or(1.0), + euler_basis, + pulse_optimize, )?; - match &decomposer.decomposer { - DecomposerType::TwoQubitBasis(_) => { - let sequence = - synth_su4_sequence(&unitary, decomposer, preferred_dir, approximation_degree)?; - let scoring_info = - sequence - .gate_sequence - .gates() - .iter() - .map(|(gate, params, qubit_ids)| { - let inst_qubits = - qubit_ids.iter().map(|q| ref_qubits[*q as usize]).collect(); - match gate { - Some(gate) => ( - gate.name().to_string(), - Some(params.iter().map(|p| Param::Float(*p)).collect()), - inst_qubits, - ), - None => ( - sequence.decomp_gate.operation.name().to_string(), - Some(params.iter().map(|p| Param::Float(*p)).collect()), - inst_qubits, - ), - } - }); - let synth_error_from_target = synth_error(py, scoring_info, target); - synth_errors_sequence.push((sequence, synth_error_from_target)); - } - DecomposerType::TwoQubitControlledU(_) => { - let sequence = - synth_su4_sequence(&unitary, decomposer, preferred_dir, approximation_degree)?; - let scoring_info = - sequence - .gate_sequence - .gates() - .iter() - .map(|(gate, params, qubit_ids)| { - let inst_qubits = - qubit_ids.iter().map(|q| ref_qubits[*q as usize]).collect(); - match gate { - Some(gate) => ( - gate.name().to_string(), - Some(params.iter().map(|p| Param::Float(*p)).collect()), - inst_qubits, - ), - None => ( - sequence.decomp_gate.operation.name().to_string(), - Some(params.iter().map(|p| Param::Float(*p)).collect()), - inst_qubits, - ), - } - }); - let synth_error_from_target = synth_error(py, scoring_info, target); - synth_errors_sequence.push((sequence, synth_error_from_target)); - } - DecomposerType::XX(_) => { - let synth_dag = synth_su4_dag( - py, - &unitary, - decomposer, - preferred_dir, - approximation_degree, - )?; - let scoring_info = synth_dag - .topological_op_nodes() - .expect("Unexpected error in dag.topological_op_nodes()") - .map(|node| { - let NodeType::Operation(inst) = &synth_dag[node] else { - unreachable!("DAG node must be an instruction") - }; - let inst_qubits = synth_dag - .get_qargs(inst.qubits) - .iter() - .map(|q| ref_qubits[q.0 as usize]) - .collect(); - ( - inst.op.name().to_string(), - inst.params.clone().map(|boxed| *boxed), - inst_qubits, - ) - }); - let synth_error_from_target = synth_error(py, scoring_info, target); - synth_errors_dag.push((synth_dag, synth_error_from_target)); - } - } + return Ok(Some(DecomposerElement { + decomposer: DecomposerType::TwoQubitBasis(Box::new(decomposer)), + packed_op: PackedOperation::from_standard_gate(std_gate), + params: SmallVec::new(), + })); } - - let synth_sequence = synth_errors_sequence - .iter() - .enumerate() - .min_by(|error1, error2| error1.1 .1.partial_cmp(&error2.1 .1).unwrap()) - .map(|(index, _)| &synth_errors_sequence[index]); - - let synth_dag = synth_errors_dag - .iter() - .enumerate() - .min_by(|error1, error2| error1.1 .1.partial_cmp(&error2.1 .1).unwrap()) - .map(|(index, _)| &synth_errors_dag[index]); - - match (synth_sequence, synth_dag) { - (None, None) => apply_original_op(out_dag)?, - (Some((sequence, _)), None) => apply_synth_sequence(py, out_dag, out_qargs, sequence)?, - (None, Some((dag, _))) => apply_synth_dag(py, out_dag, out_qargs, dag)?, - (Some((sequence, sequence_error)), Some((dag, dag_error))) => { - if sequence_error > dag_error { - apply_synth_dag(py, out_dag, out_qargs, dag)? - } else { - apply_synth_sequence(py, out_dag, out_qargs, sequence)? - } - } - }; - Ok(()) + Ok(None) } +/// Return a list of decomposers for the given `target`. If no decomposer is found, +/// return `None``. The list can contain any `DecomposerElement`. This function +/// will exit early if an ideal decomposition is found. fn get_2q_decomposers_from_target( py: Python, target: &Target, qubits: &[PhysicalQubit; 2], approximation_degree: Option, + pulse_optimize: Option, ) -> PyResult>> { + // Store elegible basis gates (1q and 2q) with corresponding qargs (PhysicalQubit) let qubits: SmallVec<[PhysicalQubit; 2]> = SmallVec::from_buf(*qubits); let reverse_qubits: SmallVec<[PhysicalQubit; 2]> = qubits.iter().rev().copied().collect(); - let mut available_2q_basis: IndexMap<&str, NormalOperation> = IndexMap::new(); - let mut available_2q_props: IndexMap<&str, (Option, Option)> = IndexMap::new(); - let mut available_2q_param_basis: IndexMap<&str, NormalOperation> = IndexMap::new(); - let mut available_2q_param_props: IndexMap<&str, (Option, Option)> = IndexMap::new(); - let mut qubit_gate_map = IndexMap::new(); - match target.operation_names_for_qargs(Some(&qubits)) { Ok(direct_keys) => { qubit_gate_map.insert(&qubits, direct_keys); @@ -641,13 +537,17 @@ fn get_2q_decomposers_from_target( } } - #[inline] - fn check_parametrized_gate(op: &NormalOperation) -> bool { - // The gate counts as parametrized if there is any - // non-float parameter - !op.params.iter().all(|p| matches!(p, Param::Float(_))) - } + // Define available 1q basis + let available_1q_basis: IndexSet<&str> = IndexSet::from_iter( + get_target_basis_set(target, qubits[0]) + .get_bases() + .map(|basis| basis.as_str()), + ); + // Define available 2q basis (setting apart parametrized 2q gates) + let mut available_2q_basis: IndexMap<&str, (NormalOperation, Option)> = IndexMap::new(); + let mut available_2q_param_basis: IndexMap<&str, (NormalOperation, Option)> = + IndexMap::new(); for (q_pair, gates) in qubit_gate_map { for key in gates { match target.operation_from_name(key) { @@ -661,49 +561,44 @@ fn get_2q_decomposers_from_target( if op.operation.num_qubits() != 2 { continue; } - if check_parametrized_gate(op) { - available_2q_param_basis.insert(key, op.clone()); - if target.contains_key(key) { - available_2q_param_props.insert( - key, + // Add to param_basis if the gate parameters aren't bound (not Float) + if !op.params.iter().all(|p| matches!(p, Param::Float(_))) { + available_2q_param_basis.insert( + key, + ( + op.clone(), match &target[key].get(Some(q_pair)) { - Some(Some(props)) => (props.duration, props.error), - _ => (None, None), + Some(Some(props)) => props.error, + _ => None, }, - ); - } else { - continue; - } + ), + ); } - available_2q_basis.insert(key, op.clone()); - if target.contains_key(key) { - available_2q_props.insert( - key, + available_2q_basis.insert( + key, + ( + op.clone(), match &target[key].get(Some(q_pair)) { - Some(Some(props)) => (props.duration, props.error), - _ => (None, None), + Some(Some(props)) => props.error, + _ => None, }, - ); - } else { - continue; - } + ), + ); } _ => continue, } } } - if available_2q_basis.is_empty() && available_2q_param_basis.is_empty() { return Err(QiskitError::new_err( "Target has no gates available on qubits to synthesize over.", )); } - let target_basis_set = get_target_basis_set(target, qubits[0]); - let available_1q_basis: IndexSet<&str> = - IndexSet::from_iter(target_basis_set.get_bases().map(|basis| basis.as_str())); + // If there are available 2q gates, start search for decomposers: let mut decomposers: Vec = Vec::new(); + // Step 1: Try TwoQubitBasisDecomposers #[inline] fn is_supercontrolled(op: &NormalOperation) -> bool { match op.operation.matrix(&op.params) { @@ -715,29 +610,15 @@ fn get_2q_decomposers_from_target( } } } - - #[inline] - fn is_controlled(op: &NormalOperation) -> bool { - match op.operation.matrix(&op.params) { - None => false, - Some(unitary_matrix) => { - let kak = TwoQubitWeylDecomposition::new_inner(unitary_matrix.view(), None, None) - .unwrap(); - relative_eq!(kak.b(), 0.0) && relative_eq!(kak.c(), 0.0) - } - } - } - - let supercontrolled_basis: IndexMap<&str, NormalOperation> = available_2q_basis + let supercontrolled_basis: IndexMap<&str, (NormalOperation, Option)> = available_2q_basis .iter() - .filter(|(_, v)| is_supercontrolled(v)) - .map(|(k, v)| (*k, v.clone())) + .filter(|(_, (gate, _))| is_supercontrolled(gate)) + .map(|(k, (gate, props))| (*k, (gate.clone(), *props))) .collect(); - for basis_1q in &available_1q_basis { - for (basis_2q, gate) in supercontrolled_basis.iter() { - let mut basis_2q_fidelity: f64 = match available_2q_props.get(basis_2q) { - Some(&(_, Some(e))) => 1.0 - e, + for (_, (gate, props)) in supercontrolled_basis.iter() { + let mut basis_2q_fidelity: f64 = match props { + Some(error) => 1.0 - error, _ => 1.0, }; if let Some(approx_degree) = approximation_degree { @@ -748,31 +629,28 @@ fn get_2q_decomposers_from_target( gate.operation.matrix(&gate.params).unwrap().view(), basis_2q_fidelity, basis_1q, - None, + pulse_optimize, )?; decomposers.push(DecomposerElement { decomposer: DecomposerType::TwoQubitBasis(Box::new(decomposer)), - gate: gate.clone(), + packed_op: gate.operation.clone(), + params: gate.params.clone(), }); } } - - // If our 2q basis gates are a subset of cx, ecr, or cz then we know TwoQubitBasisDecomposer - // is an ideal decomposition and there is no need to try other decomposers - let available_basis_set: IndexSet<&str> = available_2q_basis.keys().copied().collect(); - - #[inline] - fn check_goodbye(basis_set: &IndexSet<&str>) -> bool { - !basis_set.is_empty() && basis_set.iter().all(|gate| GOODBYE_SET.contains(gate)) - } - - if check_goodbye(&available_basis_set) { + // If the 2q basis gates are a subset of GOODBYE_SET, exit here. + if available_2q_basis + .keys() + .all(|gate| GOODBYE_SET.contains(gate)) + && !available_2q_basis.is_empty() + { return Ok(Some(decomposers)); } + // Step 2: Try TwoQubitControlledUDecomposers for basis_1q in &available_1q_basis { - for (_basis_2q, gate) in available_2q_param_basis.iter() { + for (_, (gate, _)) in available_2q_param_basis.iter() { let rxx_equivalent_gate = if let Some(std_gate) = gate.operation.try_standard_gate() { RXXEquivalent::Standard(std_gate) } else { @@ -790,42 +668,46 @@ fn get_2q_decomposers_from_target( Ok(decomposer) => { decomposers.push(DecomposerElement { decomposer: DecomposerType::TwoQubitControlledU(Box::new(decomposer)), - gate: gate.clone(), + packed_op: gate.operation.clone(), + params: gate.params.clone(), }); } Err(_) => continue, }; } } - - // If our 2q basis gates are a subset of PARAM_SET, then we will use the TwoQubitControlledUDecomposer - // and there is no need to try other decomposers - - let available_basis_param_set: IndexSet<&str> = - available_2q_param_basis.keys().copied().collect(); - - #[inline] - fn check_parametrized_goodbye(basis_set: &IndexSet<&str>) -> bool { - !basis_set.is_empty() && basis_set.iter().all(|gate| PARAM_SET.contains(gate)) - } - - if check_parametrized_goodbye(&available_basis_param_set) { + // If the 2q basis gates are a subset of PARAM_SET, exit here + if available_2q_param_basis + .keys() + .all(|gate| PARAM_SET.contains(gate)) + && !available_2q_param_basis.is_empty() + { return Ok(Some(decomposers)); } - // Let's now look for possible controlled decomposers (i.e. XXDecomposer) - let controlled_basis: IndexMap<&str, NormalOperation> = available_2q_basis + // Step 3: Try XXDecomposers (Python) + #[inline] + fn is_controlled(op: &NormalOperation) -> bool { + match op.operation.matrix(&op.params) { + None => false, + Some(unitary_matrix) => { + let kak = TwoQubitWeylDecomposition::new_inner(unitary_matrix.view(), None, None) + .unwrap(); + relative_eq!(kak.b(), 0.0) && relative_eq!(kak.c(), 0.0) + } + } + } + let controlled_basis: IndexMap<&str, (NormalOperation, Option)> = available_2q_basis .iter() - .filter(|(_, v)| is_controlled(v)) - .map(|(k, v)| (*k, v.clone())) + .filter(|(_, (gate, _))| is_controlled(gate)) + .map(|(k, (gate, props))| (*k, (gate.clone(), *props))) .collect(); let mut pi2_basis: Option<&str> = None; let xx_embodiments: &Bound<'_, PyAny> = imports::XX_EMBODIMENTS.get_bound(py); - - // The xx decomposer args are the interaction strength (f64), basis_2q_fidelity (f64), + // The Python XXDecomposer args are the interaction strength (f64), basis_2q_fidelity (f64), // and embodiments (Bound<'_, PyAny>). let xx_decomposer_args = controlled_basis.iter().map( - |(name, op)| -> PyResult<(f64, f64, pyo3::Bound<'_, pyo3::PyAny>)> { + |(name, (op, props))| -> PyResult<(f64, f64, pyo3::Bound<'_, pyo3::PyAny>)> { let strength = 2.0 * TwoQubitWeylDecomposition::new_inner( op.operation.matrix(&op.params).unwrap().view(), @@ -834,8 +716,8 @@ fn get_2q_decomposers_from_target( ) .unwrap() .a(); - let mut fidelity_value = match available_2q_props.get(name) { - Some(&(_, error)) => 1.0 - error.unwrap_or_default(), // default is 0.0 + let mut fidelity_value = match props { + Some(error) => 1.0 - error, None => 1.0, }; if let Some(approx_degree) = approximation_degree { @@ -854,15 +736,12 @@ fn get_2q_decomposers_from_target( Ok((strength, fidelity_value, embodiment)) }, ); - let basis_2q_fidelity_dict = PyDict::new(py); let embodiments_dict = PyDict::new(py); for (strength, fidelity, embodiment) in xx_decomposer_args.flatten() { basis_2q_fidelity_dict.set_item(strength, fidelity)?; embodiments_dict.set_item(strength, embodiment)?; } - - // Iterate over 2q fidelities and select decomposers if basis_2q_fidelity_dict.len() > 0 { let xx_decomposer: &Bound<'_, PyAny> = imports::XX_DECOMPOSER.get_bound(py); for basis_1q in available_1q_basis { @@ -901,49 +780,30 @@ fn get_2q_decomposers_from_target( decomposers.push(DecomposerElement { decomposer: DecomposerType::XX(decomposer.into()), - gate: decomposer_gate, + packed_op: decomposer_gate.operation, + params: decomposer_gate.params.clone(), }); } } Ok(Some(decomposers)) } +/// Function to evaluate hardware-native direction, this allows to correct +/// the synthesis output to match the target constraints. +/// Returns: +/// * `true` if gate qubits are in the hardware-native direction +/// * `false` if gate qubits must be flipped to match hardware-native direction fn preferred_direction( - decomposer: &DecomposerElement, ref_qubits: &[PhysicalQubit; 2], natural_direction: Option, coupling_edges: &HashSet<[PhysicalQubit; 2]>, - target: &Target, + target: Option<&Target>, + decomposer: &DecomposerElement, ) -> PyResult> { - // Returns: - // * true if gate qubits are in the hardware-native direction - // * false if gate qubits must be flipped to match hardware-native direction let qubits: [PhysicalQubit; 2] = *ref_qubits; let mut reverse_qubits: [PhysicalQubit; 2] = qubits; reverse_qubits.reverse(); - let compute_cost = - |lengths: bool, q_tuple: [PhysicalQubit; 2], in_cost: f64| -> PyResult { - let cost = match target.qargs_for_operation_name(decomposer.gate.operation.name()) { - Ok(_) => match target[decomposer.gate.operation.name()].get(Some( - &q_tuple - .into_iter() - .collect::>(), - )) { - Some(Some(_props)) => { - if lengths { - _props.duration.unwrap_or(in_cost) - } else { - _props.error.unwrap_or(in_cost) - } - } - _ => in_cost, - }, - Err(_) => in_cost, - }; - Ok(cost) - }; - let preferred_direction = match natural_direction { Some(false) => None, _ => { @@ -955,31 +815,60 @@ fn preferred_direction( (true, false) => Some(true), (false, true) => Some(false), _ => { - let mut cost_0_1: f64 = f64::INFINITY; - let mut cost_1_0: f64 = f64::INFINITY; - - // Try to find the cost in gate_lengths - cost_0_1 = compute_cost(true, qubits, cost_0_1)?; - cost_1_0 = compute_cost(true, reverse_qubits, cost_1_0)?; - - // If no valid cost was found in gate_lengths, check gate_errors - if !(cost_0_1 < f64::INFINITY || cost_1_0 < f64::INFINITY) { - cost_0_1 = compute_cost(false, qubits, cost_0_1)?; - cost_1_0 = compute_cost(false, reverse_qubits, cost_1_0)?; - } + match target { + Some(target) => { + let mut cost_0_1: f64 = f64::INFINITY; + let mut cost_1_0: f64 = f64::INFINITY; + + let compute_cost = |lengths: bool, + q_tuple: [PhysicalQubit; 2], + in_cost: f64| + -> PyResult { + let cost = match target + .qargs_for_operation_name(decomposer.packed_op.name()) + { + Ok(_) => match target[decomposer.packed_op.name()].get(Some( + &q_tuple + .into_iter() + .collect::>(), + )) { + Some(Some(_props)) => { + if lengths { + _props.duration.unwrap_or(in_cost) + } else { + _props.error.unwrap_or(in_cost) + } + } + _ => in_cost, + }, + Err(_) => in_cost, + }; + Ok(cost) + }; + // Try to find the cost in gate_lengths + cost_0_1 = compute_cost(true, qubits, cost_0_1)?; + cost_1_0 = compute_cost(true, reverse_qubits, cost_1_0)?; + + // If no valid cost was found in gate_lengths, check gate_errors + if !(cost_0_1 < f64::INFINITY || cost_1_0 < f64::INFINITY) { + cost_0_1 = compute_cost(false, qubits, cost_0_1)?; + cost_1_0 = compute_cost(false, reverse_qubits, cost_1_0)?; + } - if cost_0_1 < cost_1_0 { - Some(true) - } else if cost_1_0 < cost_0_1 { - Some(false) - } else { - None + if cost_0_1 < cost_1_0 { + Some(true) + } else if cost_1_0 < cost_0_1 { + Some(false) + } else { + None + } + } + None => None, } } } } }; - if natural_direction == Some(true) && preferred_direction.is_none() { return Err(QiskitError::new_err(format!( concat!( @@ -989,10 +878,10 @@ fn preferred_direction( qubits ))); } - Ok(preferred_direction) } +/// Apply synthesis for decomposers that return a SEQUENCE (TwoQubitBasis and TwoQubitControlledU). fn synth_su4_sequence( su4_mat: &Array2, decomposer_2q: &DecomposerElement, @@ -1009,9 +898,9 @@ fn synth_su4_sequence( }; let sequence = TwoQubitUnitarySequence { gate_sequence: synth, - decomp_gate: decomposer_2q.gate.clone(), + decomp_op: decomposer_2q.packed_op.clone(), + decomp_params: decomposer_2q.params.clone(), }; - match preferred_direction { None => Ok(sequence), Some(preferred_dir) => { @@ -1024,7 +913,6 @@ fn synth_su4_sequence( synth_direction = Some(qubits.clone()); } } - match synth_direction { None => Ok(sequence), Some(synth_direction) => { @@ -1048,6 +936,9 @@ fn synth_su4_sequence( } } +/// Apply reverse synthesis for decomposers that return a SEQUENCE (TwoQubitBasis and TwoQubitControlledU). +/// This function is called by `synth_su4_sequence`` if the "direct" synthesis +/// doesn't match the hardware restrictions. fn reversed_synth_su4_sequence( mut su4_mat: Array2, decomposer_2q: &DecomposerElement, @@ -1071,7 +962,6 @@ fn reversed_synth_su4_sequence( "reversed_synth_su4_sequence should only be called for TwoQubitBasisDecomposer." ) }; - let flip_bits: [u8; 2] = [1, 0]; let mut reversed_gates = Vec::with_capacity(synth.gates().len()); for (gate, params, qubit_ids) in synth.gates() { @@ -1081,16 +971,17 @@ fn reversed_synth_su4_sequence( .collect::>(); reversed_gates.push((*gate, params.clone(), new_qubit_ids.clone())); } - let mut reversed_synth: TwoQubitGateSequence = TwoQubitGateSequence::new(); reversed_synth.set_state((reversed_gates, synth.global_phase())); let sequence = TwoQubitUnitarySequence { gate_sequence: reversed_synth, - decomp_gate: decomposer_2q.gate.clone(), + decomp_op: decomposer_2q.packed_op.clone(), + decomp_params: decomposer_2q.params.clone(), }; Ok(sequence) } +/// Apply synthesis for decomposers that return a DAG (XX). fn synth_su4_dag( py: Python, su4_mat: &Array2, @@ -1113,7 +1004,6 @@ fn synth_su4_dag( } else { unreachable!("synth_su4_dag should only be called for XXDecomposer.") }; - match preferred_direction { None => Ok(synth_dag), Some(preferred_dir) => { @@ -1149,6 +1039,9 @@ fn synth_su4_dag( } } +/// Apply reverse synthesis for decomposers that return a DAG (XX). +/// This function is called by `synth_su4_dag`` if the "direct" synthesis +/// doesn't match the hardware restrictions. fn reversed_synth_su4_dag( py: Python<'_>, mut su4_mat: Array2, @@ -1196,6 +1089,263 @@ fn reversed_synth_su4_dag( Ok(target_dag) } +/// Score the synthesis output (DAG or sequence) based on the expected gate fidelity/error score. +fn synth_error( + py: Python<'_>, + synth_circuit: impl Iterator< + Item = ( + String, + Option>, + SmallVec<[PhysicalQubit; 2]>, + ), + >, + target: &Target, +) -> f64 { + let (lower_bound, upper_bound) = synth_circuit.size_hint(); + let mut gate_fidelities = match upper_bound { + Some(bound) => Vec::with_capacity(bound), + None => Vec::with_capacity(lower_bound), + }; + let mut score_instruction = + |inst_name: &str, + inst_params: &Option>, + inst_qubits: &SmallVec<[PhysicalQubit; 2]>| { + if let Ok(names) = target.operation_names_for_qargs(Some(inst_qubits)) { + for name in names { + if let Ok(target_op) = target.operation_from_name(name) { + let are_params_close = if let Some(params) = inst_params { + params.iter().zip(target_op.params.iter()).all(|(p1, p2)| { + p1.is_close(py, p2, 1e-10) + .expect("Unexpected parameter expression error.") + }) + } else { + false + }; + let is_parametrized = target_op + .params + .iter() + .any(|param| matches!(param, Param::ParameterExpression(_))); + if target_op.operation.name() == inst_name + && (is_parametrized || are_params_close) + { + match target[name].get(Some(inst_qubits)) { + Some(Some(props)) => { + gate_fidelities.push(1.0 - props.error.unwrap_or(0.0)) + } + _ => gate_fidelities.push(1.0), + } + break; + } + } + } + } + }; + + for (inst_name, inst_params, inst_qubits) in synth_circuit { + score_instruction(&inst_name, &inst_params, &inst_qubits); + } + 1.0 - gate_fidelities.into_iter().product::() +} + +/// Perform 2q unitary synthesis for a given `unitary`. If some `target` is provided, +/// the decomposition will be hardware-aware and take into the account the reported +/// gate errors to select the best method among the options. If `target` is `None``, +/// the decompostion will use the given `basis_gates` and the first valid decomposition +/// will be returned (no selection). +fn run_2q_unitary_synthesis( + py: Python, + unitary: Array2, + ref_qubits: &[PhysicalQubit; 2], + coupling_edges: &HashSet<[PhysicalQubit; 2]>, + target: Option<&Target>, + basis_gates: HashSet, + approximation_degree: Option, + natural_direction: Option, + pulse_optimize: Option, + out_dag: &mut DAGCircuit, + out_qargs: &[Qubit], + mut apply_original_op: impl FnMut(&mut DAGCircuit) -> PyResult<()>, +) -> PyResult<()> { + // Find decomposer candidates + let decomposers = match target { + Some(target) => { + let decomposers_2q = get_2q_decomposers_from_target( + py, + target, + ref_qubits, + approximation_degree, + pulse_optimize, + )?; + decomposers_2q.unwrap_or_default() + } + None => { + let basis_gates: IndexSet<&str> = basis_gates.iter().map(String::as_str).collect(); + let decomposer_item: Option = + get_2q_decomposer_from_basis(basis_gates, approximation_degree, pulse_optimize)?; + if decomposer_item.is_none() { + apply_original_op(out_dag)?; + return Ok(()); + }; + vec![decomposer_item.unwrap()] + } + }; + + // If there's a single decomposer candidate, avoid computing synthesis score. + // This will ALWAYS be the path if the `target` is `None` (`basis_gates` used). + if decomposers.len() == 1 { + let decomposer_item = decomposers.first().unwrap(); + let preferred_dir = preferred_direction( + ref_qubits, + natural_direction, + coupling_edges, + target, + decomposer_item, + )?; + + match decomposer_item.decomposer { + DecomposerType::TwoQubitBasis(_) => { + let synth = synth_su4_sequence( + &unitary, + decomposer_item, + preferred_dir, + approximation_degree, + )?; + apply_synth_sequence(py, out_dag, out_qargs, &synth)?; + } + DecomposerType::TwoQubitControlledU(_) => { + let synth = synth_su4_sequence( + &unitary, + decomposer_item, + preferred_dir, + approximation_degree, + )?; + apply_synth_sequence(py, out_dag, out_qargs, &synth)?; + } + DecomposerType::XX(_) => { + let synth = synth_su4_dag( + py, + &unitary, + decomposer_item, + preferred_dir, + approximation_degree, + )?; + apply_synth_dag(py, out_dag, out_qargs, &synth)?; + } + } + return Ok(()); + } + + // If there is more than one available decomposer, select the one with the best synthesis score. + // This will only happen if `target` is not `None`, so we can assume that there is some target from + // this point onwards. The scored SEQUENCEs and DAGs are stored in independent vectors to avoid defining + // yet another custom type. + let mut synth_errors_sequence = Vec::new(); + let mut synth_errors_dag = Vec::new(); + + // The sequence synthesis logic can be shared between TwoQubitBasis and TwoQubitControlledU, + // but the DAG logic needs to stay independent. + let synth_sequence = |decomposer, preferred_dir| -> PyResult<(TwoQubitUnitarySequence, f64)> { + let sequence = + synth_su4_sequence(&unitary, decomposer, preferred_dir, approximation_degree)?; + let scoring_info = + sequence + .gate_sequence + .gates() + .iter() + .map(|(gate, params, qubit_ids)| { + let inst_qubits = qubit_ids.iter().map(|q| ref_qubits[*q as usize]).collect(); + match gate { + Some(gate) => ( + gate.name().to_string(), + Some(params.iter().map(|p| Param::Float(*p)).collect()), + inst_qubits, + ), + None => ( + sequence.decomp_op.name().to_string(), + Some(params.iter().map(|p| Param::Float(*p)).collect()), + inst_qubits, + ), + } + }); + let score = synth_error(py, scoring_info, target.unwrap()); + Ok((sequence, score)) + }; + + for decomposer in &decomposers { + let preferred_dir = preferred_direction( + ref_qubits, + natural_direction, + coupling_edges, + target, + decomposer, + )?; + match &decomposer.decomposer { + DecomposerType::TwoQubitBasis(_) => { + synth_errors_sequence.push(synth_sequence(decomposer, preferred_dir)?); + } + DecomposerType::TwoQubitControlledU(_) => { + synth_errors_sequence.push(synth_sequence(decomposer, preferred_dir)?); + } + DecomposerType::XX(_) => { + let synth_dag = synth_su4_dag( + py, + &unitary, + decomposer, + preferred_dir, + approximation_degree, + )?; + let scoring_info = synth_dag + .topological_op_nodes() + .expect("Unexpected error in dag.topological_op_nodes()") + .map(|node| { + let NodeType::Operation(inst) = &synth_dag[node] else { + unreachable!("DAG node must be an instruction") + }; + let inst_qubits = synth_dag + .get_qargs(inst.qubits) + .iter() + .map(|q| ref_qubits[q.0 as usize]) + .collect(); + ( + inst.op.name().to_string(), + inst.params.clone().map(|boxed| *boxed), + inst_qubits, + ) + }); + let score = synth_error(py, scoring_info, target.unwrap()); + synth_errors_dag.push((synth_dag, score)); + } + } + } + + // Resolve synthesis scores between sequence and DAG. + let synth_sequence = synth_errors_sequence + .iter() + .enumerate() + .min_by(|error1, error2| error1.1 .1.partial_cmp(&error2.1 .1).unwrap()) + .map(|(index, _)| &synth_errors_sequence[index]); + + let synth_dag = synth_errors_dag + .iter() + .enumerate() + .min_by(|error1, error2| error1.1 .1.partial_cmp(&error2.1 .1).unwrap()) + .map(|(index, _)| &synth_errors_dag[index]); + + match (synth_sequence, synth_dag) { + (None, None) => apply_original_op(out_dag)?, + (Some((sequence, _)), None) => apply_synth_sequence(py, out_dag, out_qargs, sequence)?, + (None, Some((dag, _))) => apply_synth_dag(py, out_dag, out_qargs, dag)?, + (Some((sequence, sequence_error)), Some((dag, dag_error))) => { + if sequence_error > dag_error { + apply_synth_dag(py, out_dag, out_qargs, dag)? + } else { + apply_synth_sequence(py, out_dag, out_qargs, sequence)? + } + } + }; + Ok(()) +} + #[pymodule] pub fn unitary_synthesis(m: &Bound) -> PyResult<()> { m.add_wrapped(wrap_pyfunction!(py_run_main_loop))?; diff --git a/pyproject.toml b/pyproject.toml index 0ff81bf9add7..61d845484737 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -70,7 +70,7 @@ Issues = "https://github.com/Qiskit/qiskit/issues" Changelog = "https://docs.quantum.ibm.com/api/qiskit/release-notes" [project.entry-points."qiskit.unitary_synthesis"] -default = "qiskit.transpiler.passes.synthesis.unitary_synthesis:DefaultUnitarySynthesis" +default = "qiskit.transpiler.passes.synthesis.default_unitary_synth_plugin:DefaultUnitarySynthesis" aqc = "qiskit.transpiler.passes.synthesis.aqc_plugin:AQCSynthesisPlugin" sk = "qiskit.transpiler.passes.synthesis.solovay_kitaev_synthesis:SolovayKitaevSynthesis" diff --git a/qiskit/transpiler/passes/synthesis/default_unitary_synth_plugin.py b/qiskit/transpiler/passes/synthesis/default_unitary_synth_plugin.py new file mode 100644 index 000000000000..4cf675cb15b8 --- /dev/null +++ b/qiskit/transpiler/passes/synthesis/default_unitary_synth_plugin.py @@ -0,0 +1,653 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2017, 2020. +# +# 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. + +""" +========================================================================================= +Unitary Synthesis Plugin (in :mod:`qiskit.transpiler.passes.synthesis.unitary_synthesis`) +========================================================================================= + +.. autosummary:: + :toctree: ../stubs/ + + DefaultUnitarySynthesis +""" + +from __future__ import annotations +from math import pi, inf, isclose +from itertools import product +from functools import partial +import numpy as np + +from qiskit.circuit import Gate, Parameter +from qiskit.circuit.library.standard_gates import ( + iSwapGate, + CXGate, + CZGate, + RXXGate, + RZXGate, + RZZGate, + RYYGate, + ECRGate, + RXGate, + SXGate, + XGate, + RZGate, + UGate, + PhaseGate, + U1Gate, + U2Gate, + U3Gate, + RYGate, + RGate, + CRXGate, + CRYGate, + CRZGate, + CPhaseGate, +) +from qiskit.converters import circuit_to_dag +from qiskit.dagcircuit.dagcircuit import DAGCircuit +from qiskit.dagcircuit.dagnode import DAGOpNode +from qiskit.exceptions import QiskitError +from qiskit.quantum_info import Operator +from qiskit.synthesis.two_qubit.xx_decompose import XXDecomposer, XXEmbodiments +from qiskit.synthesis.two_qubit.two_qubit_decompose import ( + TwoQubitBasisDecomposer, + TwoQubitWeylDecomposition, + TwoQubitControlledUDecomposer, +) +from qiskit.transpiler.exceptions import TranspilerError +from qiskit.transpiler.passes.optimization.optimize_1q_decomposition import ( + Optimize1qGatesDecomposition, + _possible_decomposers, +) +from qiskit.transpiler.passes.synthesis import plugin + + +GATE_NAME_MAP = { + "cx": CXGate._standard_gate, + "rx": RXGate._standard_gate, + "sx": SXGate._standard_gate, + "x": XGate._standard_gate, + "rz": RZGate._standard_gate, + "u": UGate._standard_gate, + "p": PhaseGate._standard_gate, + "u1": U1Gate._standard_gate, + "u2": U2Gate._standard_gate, + "u3": U3Gate._standard_gate, + "ry": RYGate._standard_gate, + "r": RGate._standard_gate, + "rzz": RZZGate._standard_gate, + "ryy": RYYGate._standard_gate, + "rxx": RXXGate._standard_gate, + "rzx": RXXGate._standard_gate, + "cp": CPhaseGate._standard_gate, + "crx": RXXGate._standard_gate, + "cry": RXXGate._standard_gate, + "crz": RXXGate._standard_gate, +} + +KAK_GATE_PARAM_NAMES = { + "rxx": RXXGate, + "rzz": RZZGate, + "ryy": RYYGate, + "rzx": RZXGate, + "cphase": CPhaseGate, + "crx": CRXGate, + "cry": CRYGate, + "crz": CRZGate, +} + +KAK_GATE_NAMES = { + "cx": CXGate(), + "cz": CZGate(), + "iswap": iSwapGate(), + "ecr": ECRGate(), +} + + +def _choose_kak_gate(basis_gates): + """Choose the first available 2q gate to use in the KAK decomposition.""" + kak_gate = None + kak_gates = sorted(set(basis_gates or []).intersection(KAK_GATE_NAMES.keys())) + kak_gates_params = sorted(set(basis_gates or []).intersection(KAK_GATE_PARAM_NAMES.keys())) + + if kak_gates_params: + kak_gate = KAK_GATE_PARAM_NAMES[kak_gates_params[0]] + + elif kak_gates: + kak_gate = KAK_GATE_NAMES[kak_gates[0]] + + return kak_gate + + +def _choose_euler_basis(basis_gates): + """Choose the first available 1q basis to use in the Euler decomposition.""" + basis_set = set(basis_gates or []) + decomposers = _possible_decomposers(basis_set) + if decomposers: + return decomposers[0] + return "U" + + +def _find_matching_euler_bases(target, qubit): + """Find matching available 1q basis to use in the Euler decomposition.""" + basis_set = target.operation_names_for_qargs((qubit,)) + return _possible_decomposers(basis_set) + + +def _decomposer_2q_from_basis_gates(basis_gates, pulse_optimize=None, approximation_degree=None): + decomposer2q = None + kak_gate = _choose_kak_gate(basis_gates) + euler_basis = _choose_euler_basis(basis_gates) + basis_fidelity = approximation_degree or 1.0 + + if kak_gate in KAK_GATE_PARAM_NAMES.values(): + decomposer2q = TwoQubitControlledUDecomposer(kak_gate, euler_basis) + elif kak_gate is not None: + decomposer2q = TwoQubitBasisDecomposer( + kak_gate, + basis_fidelity=basis_fidelity, + euler_basis=euler_basis, + pulse_optimize=pulse_optimize, + ) + return decomposer2q + + +def _error(circuit, target=None, qubits=None): + """ + Calculate a rough error for a `circuit` that runs on specific + `qubits` of `target`. + + Use basis errors from target if available, otherwise use length + of circuit as a weak proxy for error. + """ + if target is None: + if isinstance(circuit, DAGCircuit): + return len(circuit.op_nodes()) + else: + return len(circuit) + gate_fidelities = [] + gate_durations = [] + + def score_instruction(inst, inst_qubits): + try: + keys = target.operation_names_for_qargs(inst_qubits) + for key in keys: + target_op = target.operation_from_name(key) + if isinstance(circuit, DAGCircuit): + op = inst.op + else: + op = inst.operation + if isinstance(target_op, op.base_class) and ( + target_op.is_parameterized() + or all( + isclose(float(p1), float(p2)) for p1, p2 in zip(target_op.params, op.params) + ) + ): + inst_props = target[key].get(inst_qubits, None) + if inst_props is not None: + error = getattr(inst_props, "error", 0.0) or 0.0 + duration = getattr(inst_props, "duration", 0.0) or 0.0 + gate_fidelities.append(1 - error) + gate_durations.append(duration) + else: + gate_fidelities.append(1.0) + gate_durations.append(0.0) + + break + else: + raise KeyError + except KeyError as error: + if isinstance(circuit, DAGCircuit): + op = inst.op + else: + op = inst.operation + raise TranspilerError( + f"Encountered a bad synthesis. " f"Target has no {op} on qubits {qubits}." + ) from error + + if isinstance(circuit, DAGCircuit): + for inst in circuit.topological_op_nodes(): + inst_qubits = tuple(qubits[circuit.find_bit(q).index] for q in inst.qargs) + score_instruction(inst, inst_qubits) + else: + for inst in circuit: + inst_qubits = tuple(qubits[circuit.find_bit(q).index] for q in inst.qubits) + score_instruction(inst, inst_qubits) + # TODO:return np.sum(gate_durations) + return 1 - np.prod(gate_fidelities) + + +def _preferred_direction( + decomposer2q, qubits, natural_direction, coupling_map=None, gate_lengths=None, gate_errors=None +): + """ + `decomposer2q` decomposes an SU(4) over `qubits`. A user sets `natural_direction` + to indicate whether they prefer synthesis in a hardware-native direction. + If yes, we return the `preferred_direction` here. If no hardware direction is + preferred, we raise an error (unless natural_direction is None). + We infer this from `coupling_map`, `gate_lengths`, `gate_errors`. + + Returns [0, 1] if qubits are correct in the hardware-native direction. + Returns [1, 0] if qubits must be flipped to match hardware-native direction. + """ + qubits_tuple = tuple(qubits) + reverse_tuple = qubits_tuple[::-1] + + preferred_direction = None + if natural_direction in {None, True}: + # find native gate directions from a (non-bidirectional) coupling map + if coupling_map is not None: + neighbors0 = coupling_map.neighbors(qubits[0]) + zero_one = qubits[1] in neighbors0 + neighbors1 = coupling_map.neighbors(qubits[1]) + one_zero = qubits[0] in neighbors1 + if zero_one and not one_zero: + preferred_direction = [0, 1] + if one_zero and not zero_one: + preferred_direction = [1, 0] + # otherwise infer natural directions from gate durations or gate errors + if preferred_direction is None and (gate_lengths or gate_errors): + cost_0_1 = inf + cost_1_0 = inf + try: + cost_0_1 = next( + duration + for gate, duration in gate_lengths.get(qubits_tuple, []) + if gate == decomposer2q.gate + ) + except StopIteration: + pass + try: + cost_1_0 = next( + duration + for gate, duration in gate_lengths.get(reverse_tuple, []) + if gate == decomposer2q.gate + ) + except StopIteration: + pass + if not (cost_0_1 < inf or cost_1_0 < inf): + try: + cost_0_1 = next( + error + for gate, error in gate_errors.get(qubits_tuple, []) + if gate == decomposer2q.gate + ) + except StopIteration: + pass + try: + cost_1_0 = next( + error + for gate, error in gate_errors.get(reverse_tuple, []) + if gate == decomposer2q.gate + ) + except StopIteration: + pass + if cost_0_1 < cost_1_0: + preferred_direction = [0, 1] + elif cost_1_0 < cost_0_1: + preferred_direction = [1, 0] + if natural_direction is True and preferred_direction is None: + raise TranspilerError( + f"No preferred direction of gate on qubits {qubits} " + "could be determined from coupling map or " + "gate lengths / gate errors." + ) + return preferred_direction + + +class DefaultUnitarySynthesis(plugin.UnitarySynthesisPlugin): + """The default unitary synthesis plugin.""" + + @property + def supports_basis_gates(self): + return True + + @property + def supports_coupling_map(self): + return True + + @property + def supports_natural_direction(self): + return True + + @property + def supports_pulse_optimize(self): + return True + + @property + def supports_gate_lengths(self): + return False + + @property + def supports_gate_errors(self): + return False + + @property + def supports_gate_lengths_by_qubit(self): + return True + + @property + def supports_gate_errors_by_qubit(self): + return True + + @property + def max_qubits(self): + return None + + @property + def min_qubits(self): + return None + + @property + def supported_bases(self): + return None + + @property + def supports_target(self): + return True + + def __init__(self): + super().__init__() + self._decomposer_cache = {} + + def _decomposer_2q_from_target(self, target, qubits, approximation_degree): + # we just need 2-qubit decomposers, in any direction. + # we'll fix the synthesis direction later. + qubits_tuple = tuple(sorted(qubits)) + reverse_tuple = qubits_tuple[::-1] + if qubits_tuple in self._decomposer_cache: + return self._decomposer_cache[qubits_tuple] + + # available instructions on this qubit pair, and their associated property. + available_2q_basis = {} + available_2q_props = {} + + # 2q gates sent to 2q decomposers must not have any symbolic parameters. The + # gates must be convertable to a numeric matrix. If a basis gate supports an arbitrary + # angle, we have to choose one angle (or more.) + def _replace_parameterized_gate(op): + if isinstance(op, RXXGate) and isinstance(op.params[0], Parameter): + op = RXXGate(pi / 2) + elif isinstance(op, RZXGate) and isinstance(op.params[0], Parameter): + op = RZXGate(pi / 4) + elif isinstance(op, RZZGate) and isinstance(op.params[0], Parameter): + op = RZZGate(pi / 2) + return op + + try: + keys = target.operation_names_for_qargs(qubits_tuple) + for key in keys: + op = target.operation_from_name(key) + if not isinstance(op, Gate): + continue + available_2q_basis[key] = _replace_parameterized_gate(op) + available_2q_props[key] = target[key][qubits_tuple] + except KeyError: + pass + try: + keys = target.operation_names_for_qargs(reverse_tuple) + for key in keys: + if key not in available_2q_basis: + op = target.operation_from_name(key) + if not isinstance(op, Gate): + continue + available_2q_basis[key] = _replace_parameterized_gate(op) + available_2q_props[key] = target[key][reverse_tuple] + except KeyError: + pass + if not available_2q_basis: + raise TranspilerError( + f"Target has no gates available on qubits {qubits} to synthesize over." + ) + # available decomposition basis on each of the qubits of the pair + # NOTE: assumes both qubits have the same single-qubit gates + available_1q_basis = _find_matching_euler_bases(target, qubits_tuple[0]) + + # find all decomposers + # TODO: reduce number of decomposers here somehow + decomposers = [] + + def is_supercontrolled(gate): + try: + operator = Operator(gate) + except QiskitError: + return False + kak = TwoQubitWeylDecomposition(operator.data) + return isclose(kak.a, pi / 4) and isclose(kak.c, 0.0) + + def is_controlled(gate): + try: + operator = Operator(gate) + except QiskitError: + return False + kak = TwoQubitWeylDecomposition(operator.data) + return isclose(kak.b, 0.0) and isclose(kak.c, 0.0) + + # possible supercontrolled decomposers (i.e. TwoQubitBasisDecomposer) + supercontrolled_basis = { + k: v for k, v in available_2q_basis.items() if is_supercontrolled(v) + } + for basis_1q, basis_2q in product(available_1q_basis, supercontrolled_basis.keys()): + props = available_2q_props.get(basis_2q) + if props is None: + basis_2q_fidelity = 1.0 + else: + error = getattr(props, "error", 0.0) + if error is None: + error = 0.0 + basis_2q_fidelity = 1 - error + if approximation_degree is not None: + basis_2q_fidelity *= approximation_degree + decomposer = TwoQubitBasisDecomposer( + supercontrolled_basis[basis_2q], + euler_basis=basis_1q, + basis_fidelity=basis_2q_fidelity, + ) + decomposers.append(decomposer) + + # If our 2q basis gates are a subset of cx, ecr, or cz then we know TwoQubitBasisDecomposer + # is an ideal decomposition and there is no need to bother calculating the XX embodiments + # or try the XX decomposer + if {"cx", "cz", "ecr"}.issuperset(available_2q_basis): + self._decomposer_cache[qubits_tuple] = decomposers + return decomposers + + # possible controlled decomposers (i.e. XXDecomposer) + controlled_basis = {k: v for k, v in available_2q_basis.items() if is_controlled(v)} + basis_2q_fidelity = {} + embodiments = {} + pi2_basis = None + for k, v in controlled_basis.items(): + strength = 2 * TwoQubitWeylDecomposition(Operator(v).data).a # pi/2: fully entangling + # each strength has its own fidelity + props = available_2q_props.get(k) + if props is None: + basis_2q_fidelity[strength] = 1.0 + else: + error = getattr(props, "error", 0.0) + if error is None: + error = 0.0 + basis_2q_fidelity[strength] = 1 - error + # rewrite XX of the same strength in terms of it + embodiment = XXEmbodiments[v.base_class] + if len(embodiment.parameters) == 1: + embodiments[strength] = embodiment.assign_parameters([strength]) + else: + embodiments[strength] = embodiment + # basis equivalent to CX are well optimized so use for the pi/2 angle if available + if isclose(strength, pi / 2) and k in supercontrolled_basis: + pi2_basis = v + # if we are using the approximation_degree knob, use it to scale already-given fidelities + if approximation_degree is not None: + basis_2q_fidelity = {k: v * approximation_degree for k, v in basis_2q_fidelity.items()} + if basis_2q_fidelity: + for basis_1q in available_1q_basis: + if isinstance(pi2_basis, CXGate) and basis_1q == "ZSX": + # If we're going to use the pulse optimal decomposition + # in TwoQubitBasisDecomposer we need to compute the basis + # fidelity to use for the decomposition. Either use the + # cx error rate if approximation degree is None, or + # the approximation degree value if it's a float + if approximation_degree is None: + props = target["cx"].get(qubits_tuple) + if props is not None: + fidelity = 1.0 - getattr(props, "error", 0.0) + else: + fidelity = 1.0 + else: + fidelity = approximation_degree + pi2_decomposer = TwoQubitBasisDecomposer( + pi2_basis, + euler_basis=basis_1q, + basis_fidelity=fidelity, + pulse_optimize=True, + ) + embodiments.update({pi / 2: XXEmbodiments[pi2_decomposer.gate.base_class]}) + else: + pi2_decomposer = None + decomposer = XXDecomposer( + basis_fidelity=basis_2q_fidelity, + euler_basis=basis_1q, + embodiments=embodiments, + backup_optimizer=pi2_decomposer, + ) + decomposers.append(decomposer) + + self._decomposer_cache[qubits_tuple] = decomposers + return decomposers + + def run(self, unitary, **options): + # Approximation degree is set directly as an attribute on the + # instance by the UnitarySynthesis pass here as it's not part of + # plugin interface. However if for some reason it's not set assume + # it's 1. + approximation_degree = getattr(self, "_approximation_degree", 1.0) + basis_gates = options["basis_gates"] + coupling_map = options["coupling_map"][0] + natural_direction = options["natural_direction"] + pulse_optimize = options["pulse_optimize"] + gate_lengths = options["gate_lengths_by_qubit"] + gate_errors = options["gate_errors_by_qubit"] + qubits = options["coupling_map"][1] + target = options["target"] + + if unitary.shape == (2, 2): + _decomposer1q = Optimize1qGatesDecomposition(basis_gates, target) + sequence = _decomposer1q._resynthesize_run(unitary, qubits[0]) + if sequence is None: + return None + return _decomposer1q._gate_sequence_to_dag(sequence) + elif unitary.shape == (4, 4): + # select synthesizers that can lower to the target + if target is not None: + decomposers2q = self._decomposer_2q_from_target( + target, qubits, approximation_degree + ) + else: + decomposer2q = _decomposer_2q_from_basis_gates( + basis_gates, pulse_optimize, approximation_degree + ) + decomposers2q = [decomposer2q] if decomposer2q is not None else [] + # choose the cheapest output among synthesized circuits + synth_circuits = [] + # If we have a single TwoQubitBasisDecomposer skip dag creation as we don't need to + # store and can instead manually create the synthesized gates directly in the output dag + if len(decomposers2q) == 1 and isinstance(decomposers2q[0], TwoQubitBasisDecomposer): + preferred_direction = _preferred_direction( + decomposers2q[0], + qubits, + natural_direction, + coupling_map, + gate_lengths, + gate_errors, + ) + return self._synth_su4_no_dag( + unitary, decomposers2q[0], preferred_direction, approximation_degree + ) + for decomposer2q in decomposers2q: + preferred_direction = _preferred_direction( + decomposer2q, qubits, natural_direction, coupling_map, gate_lengths, gate_errors + ) + synth_circuit = self._synth_su4( + unitary, decomposer2q, preferred_direction, approximation_degree + ) + synth_circuits.append(synth_circuit) + synth_circuit = min( + synth_circuits, + key=partial(_error, target=target, qubits=tuple(qubits)), + default=None, + ) + else: + from qiskit.synthesis.unitary.qsd import ( # pylint: disable=cyclic-import + qs_decomposition, + ) + + # only decompose if needed. TODO: handle basis better + synth_circuit = qs_decomposition(unitary) if (basis_gates or target) else None + if synth_circuit is None: + return None + if isinstance(synth_circuit, DAGCircuit): + return synth_circuit + return circuit_to_dag(synth_circuit) + + def _synth_su4_no_dag(self, unitary, decomposer2q, preferred_direction, approximation_degree): + approximate = not approximation_degree == 1.0 + synth_circ = decomposer2q._inner_decomposer(unitary, approximate=approximate) + if not preferred_direction: + return (synth_circ, synth_circ.global_phase, decomposer2q.gate) + + synth_direction = None + # if the gates in synthesis are in the opposite direction of the preferred direction + # resynthesize a new operator which is the original conjugated by swaps. + # this new operator is doubly mirrored from the original and is locally equivalent. + for gate, _params, qubits in synth_circ: + if gate is None or gate == CXGate._standard_gate: + synth_direction = qubits + if synth_direction is not None and synth_direction != preferred_direction: + # TODO: Avoid using a dag to correct the synthesis direction + return self._reversed_synth_su4(unitary, decomposer2q, approximation_degree) + return (synth_circ, synth_circ.global_phase, decomposer2q.gate) + + def _synth_su4(self, su4_mat, decomposer2q, preferred_direction, approximation_degree): + approximate = not approximation_degree == 1.0 + synth_circ = decomposer2q(su4_mat, approximate=approximate, use_dag=True) + if not preferred_direction: + return synth_circ + synth_direction = None + # if the gates in synthesis are in the opposite direction of the preferred direction + # resynthesize a new operator which is the original conjugated by swaps. + # this new operator is doubly mirrored from the original and is locally equivalent. + for inst in synth_circ.topological_op_nodes(): + if inst.op.num_qubits == 2: + synth_direction = [synth_circ.find_bit(q).index for q in inst.qargs] + if synth_direction is not None and synth_direction != preferred_direction: + return self._reversed_synth_su4(su4_mat, decomposer2q, approximation_degree) + return synth_circ + + def _reversed_synth_su4(self, su4_mat, decomposer2q, approximation_degree): + approximate = not approximation_degree == 1.0 + su4_mat_mm = su4_mat.copy() + su4_mat_mm[[1, 2]] = su4_mat_mm[[2, 1]] + su4_mat_mm[:, [1, 2]] = su4_mat_mm[:, [2, 1]] + synth_circ = decomposer2q(su4_mat_mm, approximate=approximate, use_dag=True) + out_dag = DAGCircuit() + out_dag.global_phase = synth_circ.global_phase + out_dag.add_qubits(list(reversed(synth_circ.qubits))) + flip_bits = out_dag.qubits[::-1] + for node in synth_circ.topological_op_nodes(): + qubits = tuple(flip_bits[synth_circ.find_bit(x).index] for x in node.qargs) + node = DAGOpNode.from_instruction( + node._to_circuit_instruction().replace(qubits=qubits, params=node.params) + ) + out_dag._apply_op_node_back(node) + return out_dag diff --git a/qiskit/transpiler/passes/synthesis/unitary_synthesis.py b/qiskit/transpiler/passes/synthesis/unitary_synthesis.py index 3dfd6caff65c..d0c98e956927 100644 --- a/qiskit/transpiler/passes/synthesis/unitary_synthesis.py +++ b/qiskit/transpiler/passes/synthesis/unitary_synthesis.py @@ -11,147 +11,26 @@ # that they have been altered from the originals. """ -========================================================================================= -Unitary Synthesis Plugin (in :mod:`qiskit.transpiler.passes.synthesis.unitary_synthesis`) -========================================================================================= - -.. autosummary:: - :toctree: ../stubs/ - - DefaultUnitarySynthesis +Unitary Synthesis Transpiler Pass """ from __future__ import annotations -from math import pi, inf, isclose from typing import Any -from itertools import product -from functools import partial -import numpy as np from qiskit.circuit.controlflow import CONTROL_FLOW_OP_NAMES -from qiskit.circuit import Gate, Parameter, CircuitInstruction -from qiskit.circuit.library.standard_gates import get_standard_gate_name_mapping -from qiskit.circuit.library.standard_gates import ( - iSwapGate, - CXGate, - CZGate, - RXXGate, - RZXGate, - RZZGate, - RYYGate, - ECRGate, - RXGate, - SXGate, - XGate, - RZGate, - UGate, - PhaseGate, - U1Gate, - U2Gate, - U3Gate, - RYGate, - RGate, - CRXGate, - CRYGate, - CRZGate, - CPhaseGate, -) +from qiskit.circuit import CircuitInstruction from qiskit.converters import circuit_to_dag, dag_to_circuit from qiskit.dagcircuit.dagcircuit import DAGCircuit from qiskit.dagcircuit.dagnode import DAGOpNode -from qiskit.exceptions import QiskitError -from qiskit.quantum_info import Operator from qiskit.synthesis.one_qubit import one_qubit_decompose -from qiskit.synthesis.two_qubit.xx_decompose import XXDecomposer, XXEmbodiments -from qiskit.synthesis.two_qubit.two_qubit_decompose import ( - TwoQubitBasisDecomposer, - TwoQubitWeylDecomposition, - TwoQubitControlledUDecomposer, -) + from qiskit.transpiler.basepasses import TransformationPass from qiskit.transpiler.coupling import CouplingMap from qiskit.transpiler.exceptions import TranspilerError -from qiskit.transpiler.passes.optimization.optimize_1q_decomposition import ( - Optimize1qGatesDecomposition, - _possible_decomposers, -) from qiskit.transpiler.passes.synthesis import plugin from qiskit.transpiler.target import Target -from qiskit._accelerate.unitary_synthesis import run_default_main_loop - -GATE_NAME_MAP = { - "cx": CXGate._standard_gate, - "rx": RXGate._standard_gate, - "sx": SXGate._standard_gate, - "x": XGate._standard_gate, - "rz": RZGate._standard_gate, - "u": UGate._standard_gate, - "p": PhaseGate._standard_gate, - "u1": U1Gate._standard_gate, - "u2": U2Gate._standard_gate, - "u3": U3Gate._standard_gate, - "ry": RYGate._standard_gate, - "r": RGate._standard_gate, - "rzz": RZZGate._standard_gate, - "ryy": RYYGate._standard_gate, - "rxx": RXXGate._standard_gate, - "rzx": RXXGate._standard_gate, - "cp": CPhaseGate._standard_gate, - "crx": RXXGate._standard_gate, - "cry": RXXGate._standard_gate, - "crz": RXXGate._standard_gate, -} - -KAK_GATE_PARAM_NAMES = { - "rxx": RXXGate, - "rzz": RZZGate, - "ryy": RYYGate, - "rzx": RZXGate, - "cphase": CPhaseGate, - "crx": CRXGate, - "cry": CRYGate, - "crz": CRZGate, -} - -KAK_GATE_NAMES = { - "cx": CXGate(), - "cz": CZGate(), - "iswap": iSwapGate(), - "ecr": ECRGate(), -} - -GateNameToGate = get_standard_gate_name_mapping() - - -def _choose_kak_gate(basis_gates): - """Choose the first available 2q gate to use in the KAK decomposition.""" - kak_gate = None - kak_gates = sorted(set(basis_gates or []).intersection(KAK_GATE_NAMES.keys())) - kak_gates_params = sorted(set(basis_gates or []).intersection(KAK_GATE_PARAM_NAMES.keys())) - - if kak_gates_params: - kak_gate = KAK_GATE_PARAM_NAMES[kak_gates_params[0]] - - elif kak_gates: - kak_gate = KAK_GATE_NAMES[kak_gates[0]] - - return kak_gate - - -def _choose_euler_basis(basis_gates): - """Choose the first available 1q basis to use in the Euler decomposition.""" - basis_set = set(basis_gates or []) - decomposers = _possible_decomposers(basis_set) - if decomposers: - return decomposers[0] - return "U" - - -def _find_matching_euler_bases(target, qubit): - """Find matching available 1q basis to use in the Euler decomposition.""" - basis_set = target.operation_names_for_qargs((qubit,)) - return _possible_decomposers(basis_set) +from qiskit._accelerate.unitary_synthesis import run_main_loop def _choose_bases(basis_gates, basis_dict=None): @@ -172,167 +51,6 @@ def _choose_bases(basis_gates, basis_dict=None): return out_basis -def _decomposer_2q_from_basis_gates(basis_gates, pulse_optimize=None, approximation_degree=None): - decomposer2q = None - kak_gate = _choose_kak_gate(basis_gates) - euler_basis = _choose_euler_basis(basis_gates) - basis_fidelity = approximation_degree or 1.0 - - if kak_gate in KAK_GATE_PARAM_NAMES.values(): - decomposer2q = TwoQubitControlledUDecomposer(kak_gate, euler_basis) - elif kak_gate is not None: - decomposer2q = TwoQubitBasisDecomposer( - kak_gate, - basis_fidelity=basis_fidelity, - euler_basis=euler_basis, - pulse_optimize=pulse_optimize, - ) - return decomposer2q - - -def _error(circuit, target=None, qubits=None): - """ - Calculate a rough error for a `circuit` that runs on specific - `qubits` of `target`. - - Use basis errors from target if available, otherwise use length - of circuit as a weak proxy for error. - """ - if target is None: - if isinstance(circuit, DAGCircuit): - return len(circuit.op_nodes()) - else: - return len(circuit) - gate_fidelities = [] - gate_durations = [] - - def score_instruction(inst, inst_qubits): - try: - keys = target.operation_names_for_qargs(inst_qubits) - for key in keys: - target_op = target.operation_from_name(key) - if isinstance(circuit, DAGCircuit): - op = inst.op - else: - op = inst.operation - if isinstance(target_op, op.base_class) and ( - target_op.is_parameterized() - or all( - isclose(float(p1), float(p2)) for p1, p2 in zip(target_op.params, op.params) - ) - ): - inst_props = target[key].get(inst_qubits, None) - if inst_props is not None: - error = getattr(inst_props, "error", 0.0) or 0.0 - duration = getattr(inst_props, "duration", 0.0) or 0.0 - gate_fidelities.append(1 - error) - gate_durations.append(duration) - else: - gate_fidelities.append(1.0) - gate_durations.append(0.0) - - break - else: - raise KeyError - except KeyError as error: - if isinstance(circuit, DAGCircuit): - op = inst.op - else: - op = inst.operation - raise TranspilerError( - f"Encountered a bad synthesis. " f"Target has no {op} on qubits {qubits}." - ) from error - - if isinstance(circuit, DAGCircuit): - for inst in circuit.topological_op_nodes(): - inst_qubits = tuple(qubits[circuit.find_bit(q).index] for q in inst.qargs) - score_instruction(inst, inst_qubits) - else: - for inst in circuit: - inst_qubits = tuple(qubits[circuit.find_bit(q).index] for q in inst.qubits) - score_instruction(inst, inst_qubits) - # TODO:return np.sum(gate_durations) - return 1 - np.prod(gate_fidelities) - - -def _preferred_direction( - decomposer2q, qubits, natural_direction, coupling_map=None, gate_lengths=None, gate_errors=None -): - """ - `decomposer2q` decomposes an SU(4) over `qubits`. A user sets `natural_direction` - to indicate whether they prefer synthesis in a hardware-native direction. - If yes, we return the `preferred_direction` here. If no hardware direction is - preferred, we raise an error (unless natural_direction is None). - We infer this from `coupling_map`, `gate_lengths`, `gate_errors`. - - Returns [0, 1] if qubits are correct in the hardware-native direction. - Returns [1, 0] if qubits must be flipped to match hardware-native direction. - """ - qubits_tuple = tuple(qubits) - reverse_tuple = qubits_tuple[::-1] - - preferred_direction = None - if natural_direction in {None, True}: - # find native gate directions from a (non-bidirectional) coupling map - if coupling_map is not None: - neighbors0 = coupling_map.neighbors(qubits[0]) - zero_one = qubits[1] in neighbors0 - neighbors1 = coupling_map.neighbors(qubits[1]) - one_zero = qubits[0] in neighbors1 - if zero_one and not one_zero: - preferred_direction = [0, 1] - if one_zero and not zero_one: - preferred_direction = [1, 0] - # otherwise infer natural directions from gate durations or gate errors - if preferred_direction is None and (gate_lengths or gate_errors): - cost_0_1 = inf - cost_1_0 = inf - try: - cost_0_1 = next( - duration - for gate, duration in gate_lengths.get(qubits_tuple, []) - if gate == decomposer2q.gate - ) - except StopIteration: - pass - try: - cost_1_0 = next( - duration - for gate, duration in gate_lengths.get(reverse_tuple, []) - if gate == decomposer2q.gate - ) - except StopIteration: - pass - if not (cost_0_1 < inf or cost_1_0 < inf): - try: - cost_0_1 = next( - error - for gate, error in gate_errors.get(qubits_tuple, []) - if gate == decomposer2q.gate - ) - except StopIteration: - pass - try: - cost_1_0 = next( - error - for gate, error in gate_errors.get(reverse_tuple, []) - if gate == decomposer2q.gate - ) - except StopIteration: - pass - if cost_0_1 < cost_1_0: - preferred_direction = [0, 1] - elif cost_1_0 < cost_0_1: - preferred_direction = [1, 0] - if natural_direction is True and preferred_direction is None: - raise TranspilerError( - f"No preferred direction of gate on qubits {qubits} " - "could be determined from coupling map or " - "gate lengths / gate errors." - ) - return preferred_direction - - class UnitarySynthesis(TransformationPass): """Synthesize gates according to their basis gates.""" @@ -454,6 +172,10 @@ def run(self, dag: DAGCircuit) -> DAGCircuit: if self.plugins: plugin_method = self.plugins.ext_plugins[self.method].obj else: + from qiskit.transpiler.passes.synthesis.default_unitary_synth_plugin import ( + DefaultUnitarySynthesis, + ) + plugin_method = DefaultUnitarySynthesis() plugin_kwargs: dict[str, Any] = {"config": self._plugin_config} _gate_lengths = _gate_errors = None @@ -489,19 +211,20 @@ def run(self, dag: DAGCircuit) -> DAGCircuit: else {} ) - if self.method == "default" and self._target is not None: + if self.method == "default": _coupling_edges = ( set(self._coupling_map.get_edges()) if self._coupling_map is not None else set() ) - - out = run_default_main_loop( + out = run_main_loop( dag, list(qubit_indices.values()), self._min_qubits, self._target, + self._basis_gates, _coupling_edges, self._approximation_degree, self._natural_direction, + self._pulse_optimize, ) return out else: @@ -697,351 +420,3 @@ def _build_gate_errors_by_qubit(target=None): if operation_and_errors: gate_errors[qubits] = operation_and_errors return gate_errors - - -class DefaultUnitarySynthesis(plugin.UnitarySynthesisPlugin): - """The default unitary synthesis plugin.""" - - @property - def supports_basis_gates(self): - return True - - @property - def supports_coupling_map(self): - return True - - @property - def supports_natural_direction(self): - return True - - @property - def supports_pulse_optimize(self): - return True - - @property - def supports_gate_lengths(self): - return False - - @property - def supports_gate_errors(self): - return False - - @property - def supports_gate_lengths_by_qubit(self): - return True - - @property - def supports_gate_errors_by_qubit(self): - return True - - @property - def max_qubits(self): - return None - - @property - def min_qubits(self): - return None - - @property - def supported_bases(self): - return None - - @property - def supports_target(self): - return True - - def __init__(self): - super().__init__() - self._decomposer_cache = {} - - def _decomposer_2q_from_target(self, target, qubits, approximation_degree): - # we just need 2-qubit decomposers, in any direction. - # we'll fix the synthesis direction later. - qubits_tuple = tuple(sorted(qubits)) - reverse_tuple = qubits_tuple[::-1] - if qubits_tuple in self._decomposer_cache: - return self._decomposer_cache[qubits_tuple] - - # available instructions on this qubit pair, and their associated property. - available_2q_basis = {} - available_2q_props = {} - - # 2q gates sent to 2q decomposers must not have any symbolic parameters. The - # gates must be convertable to a numeric matrix. If a basis gate supports an arbitrary - # angle, we have to choose one angle (or more.) - def _replace_parameterized_gate(op): - if isinstance(op, RXXGate) and isinstance(op.params[0], Parameter): - op = RXXGate(pi / 2) - elif isinstance(op, RZXGate) and isinstance(op.params[0], Parameter): - op = RZXGate(pi / 4) - elif isinstance(op, RZZGate) and isinstance(op.params[0], Parameter): - op = RZZGate(pi / 2) - return op - - try: - keys = target.operation_names_for_qargs(qubits_tuple) - for key in keys: - op = target.operation_from_name(key) - if not isinstance(op, Gate): - continue - available_2q_basis[key] = _replace_parameterized_gate(op) - available_2q_props[key] = target[key][qubits_tuple] - except KeyError: - pass - try: - keys = target.operation_names_for_qargs(reverse_tuple) - for key in keys: - if key not in available_2q_basis: - op = target.operation_from_name(key) - if not isinstance(op, Gate): - continue - available_2q_basis[key] = _replace_parameterized_gate(op) - available_2q_props[key] = target[key][reverse_tuple] - except KeyError: - pass - if not available_2q_basis: - raise TranspilerError( - f"Target has no gates available on qubits {qubits} to synthesize over." - ) - # available decomposition basis on each of the qubits of the pair - # NOTE: assumes both qubits have the same single-qubit gates - available_1q_basis = _find_matching_euler_bases(target, qubits_tuple[0]) - - # find all decomposers - # TODO: reduce number of decomposers here somehow - decomposers = [] - - def is_supercontrolled(gate): - try: - operator = Operator(gate) - except QiskitError: - return False - kak = TwoQubitWeylDecomposition(operator.data) - return isclose(kak.a, pi / 4) and isclose(kak.c, 0.0) - - def is_controlled(gate): - try: - operator = Operator(gate) - except QiskitError: - return False - kak = TwoQubitWeylDecomposition(operator.data) - return isclose(kak.b, 0.0) and isclose(kak.c, 0.0) - - # possible supercontrolled decomposers (i.e. TwoQubitBasisDecomposer) - supercontrolled_basis = { - k: v for k, v in available_2q_basis.items() if is_supercontrolled(v) - } - for basis_1q, basis_2q in product(available_1q_basis, supercontrolled_basis.keys()): - props = available_2q_props.get(basis_2q) - if props is None: - basis_2q_fidelity = 1.0 - else: - error = getattr(props, "error", 0.0) - if error is None: - error = 0.0 - basis_2q_fidelity = 1 - error - if approximation_degree is not None: - basis_2q_fidelity *= approximation_degree - decomposer = TwoQubitBasisDecomposer( - supercontrolled_basis[basis_2q], - euler_basis=basis_1q, - basis_fidelity=basis_2q_fidelity, - ) - decomposers.append(decomposer) - - # If our 2q basis gates are a subset of cx, ecr, or cz then we know TwoQubitBasisDecomposer - # is an ideal decomposition and there is no need to bother calculating the XX embodiments - # or try the XX decomposer - if {"cx", "cz", "ecr"}.issuperset(available_2q_basis): - self._decomposer_cache[qubits_tuple] = decomposers - return decomposers - - # possible controlled decomposers (i.e. XXDecomposer) - controlled_basis = {k: v for k, v in available_2q_basis.items() if is_controlled(v)} - basis_2q_fidelity = {} - embodiments = {} - pi2_basis = None - for k, v in controlled_basis.items(): - strength = 2 * TwoQubitWeylDecomposition(Operator(v).data).a # pi/2: fully entangling - # each strength has its own fidelity - props = available_2q_props.get(k) - if props is None: - basis_2q_fidelity[strength] = 1.0 - else: - error = getattr(props, "error", 0.0) - if error is None: - error = 0.0 - basis_2q_fidelity[strength] = 1 - error - # rewrite XX of the same strength in terms of it - embodiment = XXEmbodiments[v.base_class] - if len(embodiment.parameters) == 1: - embodiments[strength] = embodiment.assign_parameters([strength]) - else: - embodiments[strength] = embodiment - # basis equivalent to CX are well optimized so use for the pi/2 angle if available - if isclose(strength, pi / 2) and k in supercontrolled_basis: - pi2_basis = v - # if we are using the approximation_degree knob, use it to scale already-given fidelities - if approximation_degree is not None: - basis_2q_fidelity = {k: v * approximation_degree for k, v in basis_2q_fidelity.items()} - if basis_2q_fidelity: - for basis_1q in available_1q_basis: - if isinstance(pi2_basis, CXGate) and basis_1q == "ZSX": - # If we're going to use the pulse optimal decomposition - # in TwoQubitBasisDecomposer we need to compute the basis - # fidelity to use for the decomposition. Either use the - # cx error rate if approximation degree is None, or - # the approximation degree value if it's a float - if approximation_degree is None: - props = target["cx"].get(qubits_tuple) - if props is not None: - fidelity = 1.0 - getattr(props, "error", 0.0) - else: - fidelity = 1.0 - else: - fidelity = approximation_degree - pi2_decomposer = TwoQubitBasisDecomposer( - pi2_basis, - euler_basis=basis_1q, - basis_fidelity=fidelity, - pulse_optimize=True, - ) - embodiments.update({pi / 2: XXEmbodiments[pi2_decomposer.gate.base_class]}) - else: - pi2_decomposer = None - decomposer = XXDecomposer( - basis_fidelity=basis_2q_fidelity, - euler_basis=basis_1q, - embodiments=embodiments, - backup_optimizer=pi2_decomposer, - ) - decomposers.append(decomposer) - - self._decomposer_cache[qubits_tuple] = decomposers - return decomposers - - def run(self, unitary, **options): - # Approximation degree is set directly as an attribute on the - # instance by the UnitarySynthesis pass here as it's not part of - # plugin interface. However if for some reason it's not set assume - # it's 1. - approximation_degree = getattr(self, "_approximation_degree", 1.0) - basis_gates = options["basis_gates"] - coupling_map = options["coupling_map"][0] - natural_direction = options["natural_direction"] - pulse_optimize = options["pulse_optimize"] - gate_lengths = options["gate_lengths_by_qubit"] - gate_errors = options["gate_errors_by_qubit"] - qubits = options["coupling_map"][1] - target = options["target"] - - if unitary.shape == (2, 2): - _decomposer1q = Optimize1qGatesDecomposition(basis_gates, target) - sequence = _decomposer1q._resynthesize_run(unitary, qubits[0]) - if sequence is None: - return None - return _decomposer1q._gate_sequence_to_dag(sequence) - elif unitary.shape == (4, 4): - # select synthesizers that can lower to the target - if target is not None: - decomposers2q = self._decomposer_2q_from_target( - target, qubits, approximation_degree - ) - else: - decomposer2q = _decomposer_2q_from_basis_gates( - basis_gates, pulse_optimize, approximation_degree - ) - decomposers2q = [decomposer2q] if decomposer2q is not None else [] - # choose the cheapest output among synthesized circuits - synth_circuits = [] - # If we have a single TwoQubitBasisDecomposer skip dag creation as we don't need to - # store and can instead manually create the synthesized gates directly in the output dag - if len(decomposers2q) == 1 and isinstance(decomposers2q[0], TwoQubitBasisDecomposer): - preferred_direction = _preferred_direction( - decomposers2q[0], - qubits, - natural_direction, - coupling_map, - gate_lengths, - gate_errors, - ) - return self._synth_su4_no_dag( - unitary, decomposers2q[0], preferred_direction, approximation_degree - ) - for decomposer2q in decomposers2q: - preferred_direction = _preferred_direction( - decomposer2q, qubits, natural_direction, coupling_map, gate_lengths, gate_errors - ) - synth_circuit = self._synth_su4( - unitary, decomposer2q, preferred_direction, approximation_degree - ) - synth_circuits.append(synth_circuit) - synth_circuit = min( - synth_circuits, - key=partial(_error, target=target, qubits=tuple(qubits)), - default=None, - ) - else: - from qiskit.synthesis.unitary.qsd import ( # pylint: disable=cyclic-import - qs_decomposition, - ) - - # only decompose if needed. TODO: handle basis better - synth_circuit = qs_decomposition(unitary) if (basis_gates or target) else None - if synth_circuit is None: - return None - if isinstance(synth_circuit, DAGCircuit): - return synth_circuit - return circuit_to_dag(synth_circuit) - - def _synth_su4_no_dag(self, unitary, decomposer2q, preferred_direction, approximation_degree): - approximate = not approximation_degree == 1.0 - synth_circ = decomposer2q._inner_decomposer(unitary, approximate=approximate) - if not preferred_direction: - return (synth_circ, synth_circ.global_phase, decomposer2q.gate) - - synth_direction = None - # if the gates in synthesis are in the opposite direction of the preferred direction - # resynthesize a new operator which is the original conjugated by swaps. - # this new operator is doubly mirrored from the original and is locally equivalent. - for gate, _params, qubits in synth_circ: - if gate is None or gate == CXGate._standard_gate: - synth_direction = qubits - if synth_direction is not None and synth_direction != preferred_direction: - # TODO: Avoid using a dag to correct the synthesis direction - return self._reversed_synth_su4(unitary, decomposer2q, approximation_degree) - return (synth_circ, synth_circ.global_phase, decomposer2q.gate) - - def _synth_su4(self, su4_mat, decomposer2q, preferred_direction, approximation_degree): - approximate = not approximation_degree == 1.0 - synth_circ = decomposer2q(su4_mat, approximate=approximate, use_dag=True) - if not preferred_direction: - return synth_circ - synth_direction = None - # if the gates in synthesis are in the opposite direction of the preferred direction - # resynthesize a new operator which is the original conjugated by swaps. - # this new operator is doubly mirrored from the original and is locally equivalent. - for inst in synth_circ.topological_op_nodes(): - if inst.op.num_qubits == 2: - synth_direction = [synth_circ.find_bit(q).index for q in inst.qargs] - if synth_direction is not None and synth_direction != preferred_direction: - return self._reversed_synth_su4(su4_mat, decomposer2q, approximation_degree) - return synth_circ - - def _reversed_synth_su4(self, su4_mat, decomposer2q, approximation_degree): - approximate = not approximation_degree == 1.0 - su4_mat_mm = su4_mat.copy() - su4_mat_mm[[1, 2]] = su4_mat_mm[[2, 1]] - su4_mat_mm[:, [1, 2]] = su4_mat_mm[:, [2, 1]] - synth_circ = decomposer2q(su4_mat_mm, approximate=approximate, use_dag=True) - out_dag = DAGCircuit() - out_dag.global_phase = synth_circ.global_phase - out_dag.add_qubits(list(reversed(synth_circ.qubits))) - flip_bits = out_dag.qubits[::-1] - for node in synth_circ.topological_op_nodes(): - qubits = tuple(flip_bits[synth_circ.find_bit(x).index] for x in node.qargs) - node = DAGOpNode.from_instruction( - node._to_circuit_instruction().replace(qubits=qubits, params=node.params) - ) - out_dag._apply_op_node_back(node) - return out_dag diff --git a/test/python/transpiler/test_unitary_synthesis.py b/test/python/transpiler/test_unitary_synthesis.py index d93d6800985c..311816ab447c 100644 --- a/test/python/transpiler/test_unitary_synthesis.py +++ b/test/python/transpiler/test_unitary_synthesis.py @@ -32,7 +32,6 @@ from qiskit.quantum_info.operators import Operator from qiskit.quantum_info.random import random_unitary from qiskit.transpiler import PassManager, CouplingMap, Target, InstructionProperties -from qiskit.transpiler.exceptions import TranspilerError from qiskit.exceptions import QiskitError from qiskit.transpiler.passes import ( Collect2qBlocks, @@ -126,7 +125,6 @@ def test_empty_basis_gates(self): qc.unitary(op_1q.data, [0]) qc.unitary(op_2q.data, [0, 1]) qc.unitary(op_3q.data, [0, 1, 2]) - out = UnitarySynthesis(basis_gates=None, min_qubits=2)(qc) self.assertEqual(out.count_ops(), {"unitary": 3}) @@ -250,7 +248,7 @@ def test_two_qubit_natural_direction_true_gate_length_raises(self): natural_direction=True, ) pm = PassManager([triv_layout_pass, unisynth_pass]) - with self.assertRaises(TranspilerError): + with self.assertRaises(QiskitError): pm.run(qc) def test_two_qubit_pulse_optimal_none_optimal(self): diff --git a/test/python/transpiler/test_unitary_synthesis_plugin.py b/test/python/transpiler/test_unitary_synthesis_plugin.py index f6790e8ed14d..f4012bcff044 100644 --- a/test/python/transpiler/test_unitary_synthesis_plugin.py +++ b/test/python/transpiler/test_unitary_synthesis_plugin.py @@ -30,7 +30,7 @@ UnitarySynthesisPluginManager, unitary_synthesis_plugin_names, ) -from qiskit.transpiler.passes.synthesis.unitary_synthesis import DefaultUnitarySynthesis +from qiskit.transpiler.passes.synthesis.default_unitary_synth_plugin import DefaultUnitarySynthesis from test import QiskitTestCase # pylint: disable=wrong-import-order From 2d389e00e9d9b96b601f95c888a6e84e8338ca13 Mon Sep 17 00:00:00 2001 From: Luciano Bello Date: Tue, 18 Feb 2025 12:15:55 +0100 Subject: [PATCH 41/48] Change the register name for `measure_active` to `meas` (#13866) * change the register name for measure_active * Tweak cross-references --------- Co-authored-by: Jake Lishman --- qiskit/circuit/quantumcircuit.py | 2 +- releasenotes/notes/closes_12345-2356fd2d919e3f4a.yaml | 6 ++++++ test/python/circuit/test_circuit_operations.py | 4 ++-- 3 files changed, 9 insertions(+), 3 deletions(-) create mode 100644 releasenotes/notes/closes_12345-2356fd2d919e3f4a.yaml diff --git a/qiskit/circuit/quantumcircuit.py b/qiskit/circuit/quantumcircuit.py index da0749226434..012995e5021e 100644 --- a/qiskit/circuit/quantumcircuit.py +++ b/qiskit/circuit/quantumcircuit.py @@ -3953,7 +3953,7 @@ def measure_active(self, inplace: bool = True) -> Optional["QuantumCircuit"]: circ = self.copy() dag = circuit_to_dag(circ) qubits_to_measure = [qubit for qubit in circ.qubits if qubit not in dag.idle_wires()] - new_creg = circ._create_creg(len(qubits_to_measure), "measure") + new_creg = circ._create_creg(len(qubits_to_measure), "meas") circ.add_register(new_creg) circ.barrier() circ.measure(qubits_to_measure, new_creg) diff --git a/releasenotes/notes/closes_12345-2356fd2d919e3f4a.yaml b/releasenotes/notes/closes_12345-2356fd2d919e3f4a.yaml new file mode 100644 index 000000000000..ded47fb846ec --- /dev/null +++ b/releasenotes/notes/closes_12345-2356fd2d919e3f4a.yaml @@ -0,0 +1,6 @@ +--- +upgrade_circuits: + - | + The method :meth:`.QuantumCircuit.measure_active` has changed the name of the classical register it creates, + as the previous name conflicted with an OpenQASM reserved word. Instead of ``measure``, it is now called ``meas``, + aligning with the register name used by :meth:`~.QuantumCircuit.measure_all`. diff --git a/test/python/circuit/test_circuit_operations.py b/test/python/circuit/test_circuit_operations.py index bd35c8d920a5..72488973cf01 100644 --- a/test/python/circuit/test_circuit_operations.py +++ b/test/python/circuit/test_circuit_operations.py @@ -636,7 +636,7 @@ def test_measure_active(self): the amount of non-idle qubits to store the measured values. """ qr = QuantumRegister(4) - cr = ClassicalRegister(2, "measure") + cr = ClassicalRegister(2, "meas") circuit = QuantumCircuit(qr) circuit.h(qr[0]) @@ -658,7 +658,7 @@ def test_measure_active_copy(self): the amount of non-idle qubits to store the measured values. """ qr = QuantumRegister(4) - cr = ClassicalRegister(2, "measure") + cr = ClassicalRegister(2, "meas") circuit = QuantumCircuit(qr) circuit.h(qr[0]) From 97ed168c1d041b38877b45a69d086d787be78259 Mon Sep 17 00:00:00 2001 From: Alexander Ivrii Date: Tue, 18 Feb 2025 18:12:58 +0200 Subject: [PATCH 42/48] Ignoring unsupported gates for Solovay-Kitaev transpiler pass (#13690) * ignoring unsupported gates for SK * minor * making SK pass recurse over control-flow operations * Update releasenotes/notes/sk-ignore-unsupported-ops-8d7d5f6fca255ffb.yaml Co-authored-by: Julien Gacon --------- Co-authored-by: Julien Gacon --- .../synthesis/solovay_kitaev_synthesis.py | 18 ++--- ...nore-unsupported-ops-8d7d5f6fca255ffb.yaml | 7 ++ test/python/transpiler/test_solovay_kitaev.py | 72 ++++++++++++++----- 3 files changed, 70 insertions(+), 27 deletions(-) create mode 100644 releasenotes/notes/sk-ignore-unsupported-ops-8d7d5f6fca255ffb.yaml diff --git a/qiskit/transpiler/passes/synthesis/solovay_kitaev_synthesis.py b/qiskit/transpiler/passes/synthesis/solovay_kitaev_synthesis.py index bb31e5cc002c..5410d1302577 100644 --- a/qiskit/transpiler/passes/synthesis/solovay_kitaev_synthesis.py +++ b/qiskit/transpiler/passes/synthesis/solovay_kitaev_synthesis.py @@ -33,7 +33,7 @@ generate_basic_approximations, ) from qiskit.transpiler.basepasses import TransformationPass -from qiskit.transpiler.exceptions import TranspilerError +from qiskit.transpiler.passes.utils.control_flow import trivial_recurse from .plugin import UnitarySynthesisPlugin @@ -155,6 +155,7 @@ def __init__( self.recursion_degree = recursion_degree self._sk = SolovayKitaevDecomposition(basic_approximations) + @trivial_recurse def run(self, dag: DAGCircuit) -> DAGCircuit: """Run the ``SolovayKitaev`` pass on `dag`. @@ -168,18 +169,19 @@ def run(self, dag: DAGCircuit) -> DAGCircuit: TranspilerError: if a gates does not have to_matrix """ for node in dag.op_nodes(): - if not node.op.num_qubits == 1: - continue # ignore all non-single qubit gates + + # ignore operations on which the algorithm cannot run + if ( + (node.op.num_qubits != 1) + or node.is_parameterized() + or (not hasattr(node.op, "to_matrix")) + ): + continue # we do not check the input matrix as we know it comes from a Qiskit gate, as this # we know it will generate a valid SU(2) matrix check_input = not isinstance(node.op, Gate) - if not hasattr(node.op, "to_matrix"): - raise TranspilerError( - f"SolovayKitaev does not support gate without to_matrix method: {node.op.name}" - ) - matrix = node.op.to_matrix() # call solovay kitaev diff --git a/releasenotes/notes/sk-ignore-unsupported-ops-8d7d5f6fca255ffb.yaml b/releasenotes/notes/sk-ignore-unsupported-ops-8d7d5f6fca255ffb.yaml new file mode 100644 index 000000000000..28e0d85a3d77 --- /dev/null +++ b/releasenotes/notes/sk-ignore-unsupported-ops-8d7d5f6fca255ffb.yaml @@ -0,0 +1,7 @@ +--- +upgrade_transpiler: + - | + The :class:`.SolovayKitaev` transpiler pass no longer raises an exception on circuits + that contain single-qubit operations without a ``to_matrix`` method (such as measures, + barriers, control-flow operations) or parameterized single-qubit operations, + but will leave them unchanged. diff --git a/test/python/transpiler/test_solovay_kitaev.py b/test/python/transpiler/test_solovay_kitaev.py index 62b811c8e3bd..471c4791c114 100644 --- a/test/python/transpiler/test_solovay_kitaev.py +++ b/test/python/transpiler/test_solovay_kitaev.py @@ -22,8 +22,10 @@ from ddt import ddt, data from qiskit import transpile -from qiskit.circuit import QuantumCircuit +from qiskit.circuit import QuantumCircuit, Parameter +from qiskit.circuit.classicalregister import ClassicalRegister from qiskit.circuit.library import TGate, TdgGate, HGate, SGate, SdgGate, IGate, QFT +from qiskit.circuit.quantumregister import QuantumRegister from qiskit.converters import circuit_to_dag, dag_to_circuit from qiskit.quantum_info import Operator from qiskit.synthesis.discrete_basis.generate_basis_approximations import ( @@ -32,7 +34,6 @@ from qiskit.synthesis.discrete_basis.commutator_decompose import commutator_decompose from qiskit.synthesis.discrete_basis.gate_sequence import GateSequence from qiskit.transpiler import PassManager -from qiskit.transpiler.exceptions import TranspilerError from qiskit.transpiler.passes import UnitarySynthesis, Collect1qRuns, ConsolidateBlocks from qiskit.transpiler.passes.synthesis import SolovayKitaev, SolovayKitaevSynthesis from test import QiskitTestCase # pylint: disable=wrong-import-order @@ -152,23 +153,6 @@ def test_exact_decomposition_acts_trivially(self): decomposed_circuit = dag_to_circuit(decomposed_dag) self.assertEqual(circuit, decomposed_circuit) - def test_fails_with_no_to_matrix(self): - """Test failer if gate does not have to_matrix.""" - circuit = QuantumCircuit(1) - circuit.initialize("0") - - synth = SolovayKitaev(3, self.basic_approx) - - dag = circuit_to_dag(circuit) - - with self.assertRaises(TranspilerError) as cm: - _ = synth.run(dag) - - self.assertEqual( - "SolovayKitaev does not support gate without to_matrix method: initialize", - cm.exception.message, - ) - def test_str_basis_gates(self): """Test specifying the basis gates by string works.""" circuit = QuantumCircuit(1) @@ -261,6 +245,56 @@ def test_load_from_file(self): self.assertEqual(discretized, reference) + def test_measure(self): + """Test the Solovay-Kitaev transpiler pass on circuits with measure operators.""" + qc = QuantumCircuit(1, 1) + qc.x(0) + qc.measure(0, 0) + transpiled = SolovayKitaev()(qc) + self.assertEqual(set(transpiled.count_ops()), {"h", "t", "measure"}) + + def test_barrier(self): + """Test the Solovay-Kitaev transpiler pass on circuits with barriers.""" + qc = QuantumCircuit(1) + qc.x(0) + qc.barrier(0) + transpiled = SolovayKitaev()(qc) + self.assertEqual(set(transpiled.count_ops()), {"h", "t", "barrier"}) + + def test_parameterized_gates(self): + """Test the Solovay-Kitaev transpiler pass on circuits with parameterized gates.""" + qc = QuantumCircuit(1) + qc.x(0) + qc.rz(Parameter("t"), 0) + transpiled = SolovayKitaev()(qc) + self.assertEqual(set(transpiled.count_ops()), {"h", "t", "rz"}) + + def test_control_flow_if(self): + """Test the Solovay-Kitaev transpiler pass on circuits with control flow ops""" + qr = QuantumRegister(1) + cr = ClassicalRegister(1) + qc = QuantumCircuit(qr, cr) + + with qc.if_test((cr[0], 0)) as else_: + qc.y(0) + with else_: + qc.z(0) + transpiled = SolovayKitaev()(qc) + + # check that we still have an if-else block and all the operations within + # have been recursively synthesized + self.assertEqual(transpiled[0].name, "if_else") + for block in transpiled[0].operation.blocks: + self.assertLessEqual(set(block.count_ops()), {"h", "t", "tdg"}) + + def test_no_to_matrix(self): + """Test the Solovay-Kitaev transpiler pass ignores gates without to_matrix.""" + qc = QuantumCircuit(1) + qc.initialize("0") + + transpiled = SolovayKitaev()(qc) + self.assertEqual(set(transpiled.count_ops()), {"initialize"}) + @ddt class TestGateSequence(QiskitTestCase): From 3aa8cc12a1da332ceb7aa52115047cee12acc5de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guillermo=20Abad=20L=C3=B3pez?= <109400222+GuillermoAbadLopez@users.noreply.github.com> Date: Tue, 18 Feb 2025 18:20:40 +0100 Subject: [PATCH 43/48] Angle consitency in RZ docstring (#13873) --- qiskit/circuit/library/standard_gates/rz.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/qiskit/circuit/library/standard_gates/rz.py b/qiskit/circuit/library/standard_gates/rz.py index ed0207658441..7177dc3ae895 100644 --- a/qiskit/circuit/library/standard_gates/rz.py +++ b/qiskit/circuit/library/standard_gates/rz.py @@ -37,17 +37,17 @@ class RZGate(Gate): .. code-block:: text ┌───────┐ - q_0: ┤ Rz(λ) ├ + q_0: ┤ Rz(φ) ├ └───────┘ **Matrix Representation:** .. math:: - RZ(\lambda) = \exp\left(-i\frac{\lambda}{2}Z\right) = + RZ(\phi) = \exp\left(-i\frac{\phi}{2}Z\right) = \begin{pmatrix} - e^{-i\frac{\lambda}{2}} & 0 \\ - 0 & e^{i\frac{\lambda}{2}} + e^{-i\frac{\phi}{2}} & 0 \\ + 0 & e^{i\frac{\phi}{2}} \end{pmatrix} .. seealso:: @@ -57,7 +57,7 @@ class RZGate(Gate): .. math:: - U1(\lambda) = e^{i{\lambda}/2}RZ(\lambda) + U1(\theta=\phi) = e^{i{\phi}/2}RZ(\phi) Reference for virtual Z gate implementation: `1612.00858 `_ @@ -181,10 +181,10 @@ class CRZGate(ControlledGate): .. math:: CRZ(\theta)\ q_0, q_1 = - I \otimes |0\rangle\langle 0| + RZ(\theta) \otimes |1\rangle\langle 1| = + I \otimes |0\rangle\langle 0| + RZ(\phi=\theta) \otimes |1\rangle\langle 1| = \begin{pmatrix} 1 & 0 & 0 & 0 \\ - 0 & e^{-i\frac{\lambda}{2}} & 0 & 0 \\ + 0 & e^{-i\frac{\theta}{2}} & 0 & 0 \\ 0 & 0 & 1 & 0 \\ 0 & 0 & 0 & e^{i\frac{\theta}{2}} \end{pmatrix} From d9b8a186e683d08ec5ed3077a3922a7afa863d67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elena=20Pe=C3=B1a=20Tapia?= <57907331+ElePT@users.noreply.github.com> Date: Tue, 18 Feb 2025 18:51:48 +0100 Subject: [PATCH 44/48] Remove use of `BackendProperties` (BackendV1) in transpiler pipeline (#13722) * Remove `BackendProperties` from transpilation pipeline, remove unit tests that depend on BackendV1. * Remove coupling_map input in VF2PostLayout pass, as it would only be used if backend properties were present. After the removal of the latter, the coupling_map would always be overwritten by the target. --- qiskit/compiler/transpiler.py | 26 +- .../transpiler/passes/layout/dense_layout.py | 29 +- qiskit/transpiler/passes/layout/vf2_layout.py | 11 +- .../passes/layout/vf2_post_layout.py | 140 ++++----- qiskit/transpiler/passes/layout/vf2_utils.py | 26 +- qiskit/transpiler/passmanager_config.py | 14 - .../preset_passmanagers/builtin_plugins.py | 19 -- .../transpiler/preset_passmanagers/common.py | 7 +- .../generate_preset_pass_manager.py | 53 +--- ...end-props-transpiler-64aa771784084313.yaml | 25 ++ test/python/compiler/test_transpiler.py | 74 ----- .../transpiler/test_passmanager_config.py | 1 - test/python/transpiler/test_target.py | 32 -- test/python/transpiler/test_vf2_layout.py | 60 +--- .../python/transpiler/test_vf2_post_layout.py | 283 +----------------- 15 files changed, 120 insertions(+), 680 deletions(-) create mode 100644 releasenotes/notes/remove-backend-props-transpiler-64aa771784084313.yaml diff --git a/qiskit/compiler/transpiler.py b/qiskit/compiler/transpiler.py index 4d705481ce71..69eb12f7c5af 100644 --- a/qiskit/compiler/transpiler.py +++ b/qiskit/compiler/transpiler.py @@ -23,7 +23,6 @@ from qiskit.dagcircuit import DAGCircuit from qiskit.providers.backend import Backend from qiskit.providers.backend_compat import BackendV2Converter -from qiskit.providers.models.backendproperties import BackendProperties from qiskit.pulse import Schedule, InstructionScheduleMap from qiskit.transpiler import Layout, CouplingMap, PropertySet from qiskit.transpiler.basepasses import BasePass @@ -58,14 +57,6 @@ "with defined timing constraints with " "`Target.from_configuration(..., timing_constraints=...)`", ) -@deprecate_arg( - name="backend_properties", - since="1.3", - package_name="Qiskit", - removal_timeline="in Qiskit 2.0", - additional_msg="The `target` parameter should be used instead. You can build a `Target` instance " - "with defined properties with Target.from_configuration(..., backend_properties=...)", -) @deprecate_pulse_arg("inst_map", predicate=lambda inst_map: inst_map is not None) def transpile( # pylint: disable=too-many-return-statements circuits: _CircuitT, @@ -73,7 +64,6 @@ def transpile( # pylint: disable=too-many-return-statements basis_gates: Optional[List[str]] = None, inst_map: Optional[List[InstructionScheduleMap]] = None, coupling_map: Optional[Union[CouplingMap, List[List[int]]]] = None, - backend_properties: Optional[BackendProperties] = None, initial_layout: Optional[Union[Layout, Dict, List]] = None, layout_method: Optional[str] = None, routing_method: Optional[str] = None, @@ -105,7 +95,7 @@ def transpile( # pylint: disable=too-many-return-statements The prioritization of transpilation target constraints works as follows: if a ``target`` input is provided, it will take priority over any ``backend`` input or loose constraints - (``basis_gates``, ``inst_map``, ``coupling_map``, ``backend_properties``, ``instruction_durations``, + (``basis_gates``, ``inst_map``, ``coupling_map``, ``instruction_durations``, ``dt`` or ``timing_constraints``). If a ``backend`` is provided together with any loose constraint from the list above, the loose constraint will take priority over the corresponding backend constraint. This behavior is independent of whether the ``backend`` instance is of type @@ -123,7 +113,6 @@ def transpile( # pylint: disable=too-many-return-statements **inst_map** target inst_map inst_map **dt** target dt dt **timing_constraints** target timing_constraints timing_constraints - **backend_properties** target backend_properties backend_properties ============================ ========= ======================== ======================= Args: @@ -148,10 +137,6 @@ def transpile( # pylint: disable=too-many-return-statements #. List, must be given as an adjacency matrix, where each entry specifies all directed two-qubit interactions supported by backend, e.g: ``[[0, 1], [0, 3], [1, 2], [1, 5], [2, 5], [4, 1], [5, 3]]`` - - backend_properties: properties returned by a backend, including information on gate - errors, readout errors, qubit coherence times, etc. Find a backend - that provides this information with: ``backend.properties()`` initial_layout: Initial position of virtual qubits on physical qubits. If this layout makes the circuit compatible with the coupling_map constraints, it will be used. The final layout is not guaranteed to be the same, @@ -394,7 +379,7 @@ def callback_func(**kwargs): # Edge cases require using the old model (loose constraints) instead of building a target, # but we don't populate the passmanager config with loose constraints unless it's one of # the known edge cases to control the execution path. - # Filter instruction_durations, timing_constraints, backend_properties and inst_map deprecation + # Filter instruction_durations, timing_constraints and inst_map deprecation with warnings.catch_warnings(): warnings.filterwarnings( "ignore", @@ -414,12 +399,6 @@ def callback_func(**kwargs): message=".*``instruction_durations`` is deprecated as of Qiskit 1.3.*", module="qiskit", ) - warnings.filterwarnings( - "ignore", - category=DeprecationWarning, - message=".*``backend_properties`` is deprecated as of Qiskit 1.3.*", - module="qiskit", - ) pm = generate_preset_pass_manager( optimization_level, target=target, @@ -427,7 +406,6 @@ def callback_func(**kwargs): basis_gates=basis_gates, coupling_map=coupling_map, instruction_durations=instruction_durations, - backend_properties=backend_properties, timing_constraints=timing_constraints, inst_map=inst_map, initial_layout=initial_layout, diff --git a/qiskit/transpiler/passes/layout/dense_layout.py b/qiskit/transpiler/passes/layout/dense_layout.py index 71c69739990d..cbd9b548dcc8 100644 --- a/qiskit/transpiler/passes/layout/dense_layout.py +++ b/qiskit/transpiler/passes/layout/dense_layout.py @@ -36,17 +36,15 @@ class DenseLayout(AnalysisPass): by being set in ``property_set``. """ - def __init__(self, coupling_map=None, backend_prop=None, target=None): + def __init__(self, coupling_map=None, target=None): """DenseLayout initializer. Args: coupling_map (Coupling): directed graph representing a coupling map. - backend_prop (BackendProperties): backend properties object target (Target): A target representing the target backend. """ super().__init__() self.coupling_map = coupling_map - self.backend_prop = backend_prop self.target = target self.adjacency_matrix = None if target is not None: @@ -127,8 +125,6 @@ def _best_subset(self, num_qubits, num_meas, num_cx, coupling_map): error_mat, use_error = _build_error_matrix( coupling_map.size(), reverse_index_map, - backend_prop=self.backend_prop, - coupling_map=self.coupling_map, target=self.target, ) @@ -148,7 +144,7 @@ def _best_subset(self, num_qubits, num_meas, num_cx, coupling_map): return best_map -def _build_error_matrix(num_qubits, qubit_map, target=None, coupling_map=None, backend_prop=None): +def _build_error_matrix(num_qubits, qubit_map, target=None): error_mat = np.zeros((num_qubits, num_qubits)) use_error = False if target is not None and target.qargs is not None: @@ -178,25 +174,4 @@ def _build_error_matrix(num_qubits, qubit_map, target=None, coupling_map=None, b elif len(qargs) == 2: error_mat[qubit_map[qargs[0]]][qubit_map[qargs[1]]] = max_error use_error = True - elif backend_prop and coupling_map: - error_dict = { - tuple(gate.qubits): gate.parameters[0].value - for gate in backend_prop.gates - if len(gate.qubits) == 2 - } - for edge in coupling_map.get_edges(): - gate_error = error_dict.get(edge) - if gate_error is not None: - if edge[0] not in qubit_map or edge[1] not in qubit_map: - continue - error_mat[qubit_map[edge[0]]][qubit_map[edge[1]]] = gate_error - use_error = True - for index, qubit_data in enumerate(backend_prop.qubits): - if index not in qubit_map: - continue - for item in qubit_data: - if item.name == "readout_error": - mapped_index = qubit_map[index] - error_mat[mapped_index][mapped_index] = item.value - use_error = True return error_mat, use_error diff --git a/qiskit/transpiler/passes/layout/vf2_layout.py b/qiskit/transpiler/passes/layout/vf2_layout.py index 2e799ffa4d95..fc73cfd8c1ef 100644 --- a/qiskit/transpiler/passes/layout/vf2_layout.py +++ b/qiskit/transpiler/passes/layout/vf2_layout.py @@ -80,7 +80,6 @@ def __init__( seed=None, call_limit=None, time_limit=None, - properties=None, max_trials=None, target=None, ): @@ -94,16 +93,13 @@ def __init__( call_limit (int): The number of state visits to attempt in each execution of VF2. time_limit (float): The total time limit in seconds to run ``VF2Layout`` - properties (BackendProperties): The backend properties for the backend. If - :meth:`~qiskit.providers.models.BackendProperties.readout_error` is available - it is used to score the layout. max_trials (int): The maximum number of trials to run VF2 to find a layout. If this is not specified the number of trials will be limited based on the number of edges in the interaction graph or the coupling graph (whichever is larger) if no other limits are set. If set to a value <= 0 no limit on the number of trials will be set. target (Target): A target representing the backend device to run ``VF2Layout`` on. - If specified it will supersede a set value for ``properties`` and + If specified it will supersede a set value for ``coupling_map`` if the :class:`.Target` contains connectivity constraints. If the value of ``target`` models an ideal backend without any constraints then the value of ``coupling_map`` @@ -121,7 +117,6 @@ def __init__( self.coupling_map = target_coupling_map else: self.coupling_map = coupling_map - self.properties = properties self.strict_direction = strict_direction self.seed = seed self.call_limit = call_limit @@ -135,9 +130,7 @@ def run(self, dag): raise TranspilerError("coupling_map or target must be specified.") self.avg_error_map = self.property_set["vf2_avg_error_map"] if self.avg_error_map is None: - self.avg_error_map = vf2_utils.build_average_error_map( - self.target, self.properties, self.coupling_map - ) + self.avg_error_map = vf2_utils.build_average_error_map(self.target, self.coupling_map) result = vf2_utils.build_interaction_graph(dag, self.strict_direction) if result is None: diff --git a/qiskit/transpiler/passes/layout/vf2_post_layout.py b/qiskit/transpiler/passes/layout/vf2_post_layout.py index 1a29b0cb4506..f6d5f538e139 100644 --- a/qiskit/transpiler/passes/layout/vf2_post_layout.py +++ b/qiskit/transpiler/passes/layout/vf2_post_layout.py @@ -78,8 +78,7 @@ class VF2PostLayout(AnalysisPass): * ``">2q gates in basis"``: If VF2PostLayout can't work with the basis of the circuit. By default, this pass will construct a heuristic scoring map based on - the error rates in the provided ``target`` (or ``properties`` if ``target`` - is not provided). However, analysis passes can be run prior to this pass + the error rates in the provided ``target``. However, analysis passes can be run prior to this pass and set ``vf2_avg_error_map`` in the property set with a :class:`~.ErrorMap` instance. If a value is ``NaN`` that is treated as an ideal edge For example if an error map is created as:: @@ -102,8 +101,6 @@ class VF2PostLayout(AnalysisPass): def __init__( self, target=None, - coupling_map=None, - properties=None, seed=None, call_limit=None, time_limit=None, @@ -114,12 +111,6 @@ def __init__( Args: target (Target): A target representing the backend device to run ``VF2PostLayout`` on. - If specified it will supersede a set value for ``properties`` and - ``coupling_map``. - coupling_map (CouplingMap): Directed graph representing a coupling map. - properties (BackendProperties): The backend properties for the backend. If - :meth:`~qiskit.providers.models.BackendProperties.readout_error` is available - it is used to score the layout. seed (int): Sets the seed of the PRNG. -1 Means no node shuffling. call_limit (int): The number of state visits to attempt in each execution of VF2. @@ -138,12 +129,10 @@ def __init__( a layout. A value of ``0`` (the default) means 'unlimited'. Raises: - TypeError: At runtime, if neither ``coupling_map`` or ``target`` are provided. + TypeError: At runtime, if ``target`` isn't provided. """ super().__init__() self.target = target - self.coupling_map = coupling_map - self.properties = properties self.call_limit = call_limit self.time_limit = time_limit self.max_trials = max_trials @@ -153,16 +142,12 @@ def __init__( def run(self, dag): """run the layout method""" - if self.target is None and (self.coupling_map is None or self.properties is None): - raise TranspilerError( - "A target must be specified or a coupling map and properties must be provided" - ) + if self.target is None: + raise TranspilerError("A target must be specified or a coupling map must be provided") if not self.strict_direction: self.avg_error_map = self.property_set["vf2_avg_error_map"] if self.avg_error_map is None: - self.avg_error_map = vf2_utils.build_average_error_map( - self.target, self.properties, self.coupling_map - ) + self.avg_error_map = vf2_utils.build_average_error_map(self.target, None) result = vf2_utils.build_interaction_graph(dag, self.strict_direction) if result is None: @@ -172,67 +157,62 @@ def run(self, dag): scoring_bit_list = vf2_utils.build_bit_list(im_graph, im_graph_node_map) scoring_edge_list = vf2_utils.build_edge_list(im_graph) - if self.target is not None: - # If qargs is None then target is global and ideal so no - # scoring is needed - if self.target.qargs is None: - return - if self.strict_direction: - cm_graph = PyDiGraph(multigraph=False) - else: - cm_graph = PyGraph(multigraph=False) - # If None is present in qargs there are globally defined ideal operations - # we should add these to all entries based on the number of qubits, so we - # treat that as a valid operation even if there is no scoring for the - # strict direction case - global_ops = None - if None in self.target.qargs: - global_ops = {1: [], 2: []} - for op in self.target.operation_names_for_qargs(None): - operation = self.target.operation_for_name(op) - # If operation is a class this is a variable width ideal instruction - # so we treat it as available on both 1 and 2 qubits - if inspect.isclass(operation): - global_ops[1].append(op) - global_ops[2].append(op) - else: - num_qubits = operation.num_qubits - if num_qubits in global_ops: - global_ops[num_qubits].append(op) - op_names = [] - for i in range(self.target.num_qubits): - try: - entry = set(self.target.operation_names_for_qargs((i,))) - except KeyError: - entry = set() - if global_ops is not None: - entry.update(global_ops[1]) - op_names.append(entry) - cm_graph.add_nodes_from(op_names) - for qargs in self.target.qargs: - len_args = len(qargs) - # If qargs == 1 we already populated it and if qargs > 2 there are no instructions - # using those in the circuit because we'd have already returned by this point - if len_args == 2: - ops = set(self.target.operation_names_for_qargs(qargs)) - if global_ops is not None: - ops.update(global_ops[2]) - cm_graph.add_edge(qargs[0], qargs[1], ops) - cm_nodes = list(cm_graph.node_indexes()) - # Filter qubits without any supported operations. If they - # don't support any operations, they're not valid for layout selection. - # This is only needed in the undirected case because in strict direction - # mode the node matcher will not match since none of the circuit ops - # will match the cmap ops. - if not self.strict_direction: - has_operations = set(itertools.chain.from_iterable(self.target.qargs)) - to_remove = set(cm_graph.node_indices()).difference(has_operations) - if to_remove: - cm_graph.remove_nodes_from(list(to_remove)) + # If qargs is None then target is global and ideal so no + # scoring is needed + if self.target.qargs is None: + return + if self.strict_direction: + cm_graph = PyDiGraph(multigraph=False) else: - cm_graph, cm_nodes = vf2_utils.shuffle_coupling_graph( - self.coupling_map, self.seed, self.strict_direction - ) + cm_graph = PyGraph(multigraph=False) + # If None is present in qargs there are globally defined ideal operations + # we should add these to all entries based on the number of qubits, so we + # treat that as a valid operation even if there is no scoring for the + # strict direction case + global_ops = None + if None in self.target.qargs: + global_ops = {1: [], 2: []} + for op in self.target.operation_names_for_qargs(None): + operation = self.target.operation_for_name(op) + # If operation is a class this is a variable width ideal instruction + # so we treat it as available on both 1 and 2 qubits + if inspect.isclass(operation): + global_ops[1].append(op) + global_ops[2].append(op) + else: + num_qubits = operation.num_qubits + if num_qubits in global_ops: + global_ops[num_qubits].append(op) + op_names = [] + for i in range(self.target.num_qubits): + try: + entry = set(self.target.operation_names_for_qargs((i,))) + except KeyError: + entry = set() + if global_ops is not None: + entry.update(global_ops[1]) + op_names.append(entry) + cm_graph.add_nodes_from(op_names) + for qargs in self.target.qargs: + len_args = len(qargs) + # If qargs == 1 we already populated it and if qargs > 2 there are no instructions + # using those in the circuit because we'd have already returned by this point + if len_args == 2: + ops = set(self.target.operation_names_for_qargs(qargs)) + if global_ops is not None: + ops.update(global_ops[2]) + cm_graph.add_edge(qargs[0], qargs[1], ops) + cm_nodes = list(cm_graph.node_indexes()) + # Filter qubits without any supported operations. If they + # don't support any operations, they're not valid for layout selection. + # This is only needed in the undirected case because in strict direction + # mode the node matcher will not match since none of the circuit ops + # will match the cmap ops. + if not self.strict_direction: + has_operations = set(itertools.chain.from_iterable(self.target.qargs)) + to_remove = set(cm_graph.node_indices()).difference(has_operations) + if to_remove: + cm_graph.remove_nodes_from(list(to_remove)) logger.debug("Running VF2 to find post transpile mappings") if self.target and self.strict_direction: diff --git a/qiskit/transpiler/passes/layout/vf2_utils.py b/qiskit/transpiler/passes/layout/vf2_utils.py index 037ccc37155d..c2958d12610a 100644 --- a/qiskit/transpiler/passes/layout/vf2_utils.py +++ b/qiskit/transpiler/passes/layout/vf2_utils.py @@ -13,7 +13,6 @@ """This module contains common utils for vf2 layout passes.""" from collections import defaultdict -import statistics import random import numpy as np @@ -142,7 +141,7 @@ def score_layout( ) -def build_average_error_map(target, properties, coupling_map): +def build_average_error_map(target, coupling_map): """Build an average error map used for scoring layouts pre-basis translation.""" num_qubits = 0 if target is not None and target.qargs is not None: @@ -173,29 +172,6 @@ def build_average_error_map(target, properties, coupling_map): qargs = (qargs[0], qargs[0]) avg_map.add_error(qargs, qarg_error / count) built = True - elif properties is not None: - errors = defaultdict(list) - for qubit in range(len(properties.qubits)): - errors[(qubit,)].append(properties.readout_error(qubit)) - for gate in properties.gates: - qubits = tuple(gate.qubits) - for param in gate.parameters: - if param.name == "gate_error": - errors[qubits].append(param.value) - for k, v in errors.items(): - if len(k) == 1: - qargs = (k[0], k[0]) - else: - qargs = k - # If the properties payload contains an index outside the number of qubits - # the properties are invalid for the given input. This normally happens either - # with a malconstructed properties payload or if the faulty qubits feature of - # BackendV1/BackendPropeties is being used. In such cases we map noise characteristics - # so we should just treat the mapping as an ideal case. - if qargs[0] >= num_qubits or qargs[1] >= num_qubits: - continue - avg_map.add_error(qargs, statistics.mean(v)) - built = True # if there are no error rates in the target we should fallback to using the degree heuristic # used for a coupling map. To do this we can build the coupling map from the target before # running the fallback heuristic diff --git a/qiskit/transpiler/passmanager_config.py b/qiskit/transpiler/passmanager_config.py index 52f3d65449e5..1d473014b981 100644 --- a/qiskit/transpiler/passmanager_config.py +++ b/qiskit/transpiler/passmanager_config.py @@ -12,7 +12,6 @@ """Pass Manager Configuration class.""" -import pprint import warnings from qiskit.transpiler.coupling import CouplingMap @@ -35,7 +34,6 @@ def __init__( translation_method=None, scheduling_method=None, instruction_durations=None, - backend_properties=None, approximation_degree=None, seed_transpiler=None, timing_constraints=None, @@ -69,9 +67,6 @@ def __init__( be a plugin name if an external scheduling stage plugin is being used. instruction_durations (InstructionDurations): Dictionary of duration (in dt) for each instruction. - backend_properties (BackendProperties): Properties returned by a - backend, including information on gate errors, readout errors, - qubit coherence times, etc. approximation_degree (float): heuristic dial used for circuit approximation (1.0=no approximation, 0.0=maximal approximation) seed_transpiler (int): Sets random seed for the stochastic parts of @@ -102,7 +97,6 @@ def __init__( self.optimization_method = optimization_method self.scheduling_method = scheduling_method self.instruction_durations = instruction_durations - self.backend_properties = backend_properties self.approximation_degree = approximation_degree self.seed_transpiler = seed_transpiler self.timing_constraints = timing_constraints @@ -175,8 +169,6 @@ def from_backend(cls, backend, _skip_target=False, **pass_manager_options): res.instruction_durations = InstructionDurations.from_backend(backend) else: res.instruction_durations = backend.instruction_durations - if res.backend_properties is None and backend_version < 2: - res.backend_properties = backend.properties() if res.target is None and not _skip_target: if backend_version >= 2: res.target = backend.target @@ -189,11 +181,6 @@ def from_backend(cls, backend, _skip_target=False, **pass_manager_options): def __str__(self): newline = "\n" newline_tab = "\n\t" - if self.backend_properties is not None: - backend_props = pprint.pformat(self.backend_properties.to_dict()) - backend_props = backend_props.replace(newline, newline_tab) - else: - backend_props = str(None) return ( "Pass Manager Config:\n" f"\tinitial_layout: {self.initial_layout}\n" @@ -205,7 +192,6 @@ def __str__(self): f"\ttranslation_method: {self.translation_method}\n" f"\tscheduling_method: {self.scheduling_method}\n" f"\tinstruction_durations: {str(self.instruction_durations).replace(newline, newline_tab)}\n" - f"\tbackend_properties: {backend_props}\n" f"\tapproximation_degree: {self.approximation_degree}\n" f"\tseed_transpiler: {self.seed_transpiler}\n" f"\ttiming_constraints: {self.timing_constraints}\n" diff --git a/qiskit/transpiler/preset_passmanagers/builtin_plugins.py b/qiskit/transpiler/preset_passmanagers/builtin_plugins.py index a120a14c51a4..c2cd08eba90e 100644 --- a/qiskit/transpiler/preset_passmanagers/builtin_plugins.py +++ b/qiskit/transpiler/preset_passmanagers/builtin_plugins.py @@ -256,7 +256,6 @@ def pass_manager(self, pass_manager_config, optimization_level=None) -> PassMana """Build routing stage PassManager.""" target = pass_manager_config.target coupling_map = pass_manager_config.coupling_map - backend_properties = pass_manager_config.backend_properties if target is None: routing_pass = BasicSwap(coupling_map) else: @@ -282,7 +281,6 @@ def pass_manager(self, pass_manager_config, optimization_level=None) -> PassMana coupling_map, vf2_call_limit=vf2_call_limit, vf2_max_trials=vf2_max_trials, - backend_properties=backend_properties, seed_transpiler=-1, check_trivial=True, use_barrier_before_measurement=True, @@ -294,7 +292,6 @@ def pass_manager(self, pass_manager_config, optimization_level=None) -> PassMana coupling_map=coupling_map, vf2_call_limit=vf2_call_limit, vf2_max_trials=vf2_max_trials, - backend_properties=backend_properties, seed_transpiler=-1, use_barrier_before_measurement=True, ) @@ -305,7 +302,6 @@ def pass_manager(self, pass_manager_config, optimization_level=None) -> PassMana coupling_map=coupling_map, vf2_call_limit=vf2_call_limit, vf2_max_trials=vf2_max_trials, - backend_properties=backend_properties, seed_transpiler=-1, use_barrier_before_measurement=True, ) @@ -323,7 +319,6 @@ def pass_manager(self, pass_manager_config, optimization_level=None) -> PassMana coupling_map_routing = target if coupling_map_routing is None: coupling_map_routing = coupling_map - backend_properties = pass_manager_config.backend_properties vf2_call_limit, vf2_max_trials = common.get_vf2_limits( optimization_level, pass_manager_config.layout_method, @@ -349,7 +344,6 @@ def pass_manager(self, pass_manager_config, optimization_level=None) -> PassMana coupling_map, vf2_call_limit=vf2_call_limit, vf2_max_trials=vf2_max_trials, - backend_properties=backend_properties, seed_transpiler=-1, check_trivial=True, use_barrier_before_measurement=True, @@ -361,7 +355,6 @@ def pass_manager(self, pass_manager_config, optimization_level=None) -> PassMana coupling_map=coupling_map, vf2_call_limit=vf2_call_limit, vf2_max_trials=vf2_max_trials, - backend_properties=backend_properties, seed_transpiler=-1, use_barrier_before_measurement=True, ) @@ -378,7 +371,6 @@ def pass_manager(self, pass_manager_config, optimization_level=None) -> PassMana coupling_map_routing = target if coupling_map_routing is None: coupling_map_routing = coupling_map - backend_properties = pass_manager_config.backend_properties vf2_call_limit, vf2_max_trials = common.get_vf2_limits( optimization_level, pass_manager_config.layout_method, @@ -401,7 +393,6 @@ def pass_manager(self, pass_manager_config, optimization_level=None) -> PassMana coupling_map, vf2_call_limit=vf2_call_limit, vf2_max_trials=vf2_max_trials, - backend_properties=backend_properties, seed_transpiler=-1, check_trivial=True, use_barrier_before_measurement=True, @@ -414,7 +405,6 @@ def pass_manager(self, pass_manager_config, optimization_level=None) -> PassMana coupling_map=coupling_map, vf2_call_limit=vf2_call_limit, vf2_max_trials=vf2_max_trials, - backend_properties=backend_properties, seed_transpiler=-1, use_barrier_before_measurement=True, ) @@ -426,7 +416,6 @@ def pass_manager(self, pass_manager_config, optimization_level=None) -> PassMana coupling_map=coupling_map, vf2_call_limit=vf2_call_limit, vf2_max_trials=vf2_max_trials, - backend_properties=backend_properties, seed_transpiler=-1, use_barrier_before_measurement=True, ) @@ -444,7 +433,6 @@ def pass_manager(self, pass_manager_config, optimization_level=None) -> PassMana coupling_map_routing = target if coupling_map_routing is None: coupling_map_routing = coupling_map - backend_properties = pass_manager_config.backend_properties vf2_call_limit, vf2_max_trials = common.get_vf2_limits( optimization_level, pass_manager_config.layout_method, @@ -479,7 +467,6 @@ def pass_manager(self, pass_manager_config, optimization_level=None) -> PassMana coupling_map, vf2_call_limit=vf2_call_limit, vf2_max_trials=vf2_max_trials, - backend_properties=backend_properties, seed_transpiler=-1, check_trivial=True, use_barrier_before_measurement=True, @@ -499,7 +486,6 @@ def pass_manager(self, pass_manager_config, optimization_level=None) -> PassMana coupling_map=coupling_map, vf2_call_limit=vf2_call_limit, vf2_max_trials=vf2_max_trials, - backend_properties=backend_properties, seed_transpiler=-1, use_barrier_before_measurement=True, ) @@ -517,7 +503,6 @@ def pass_manager(self, pass_manager_config, optimization_level=None) -> PassMana coupling_map=coupling_map, vf2_call_limit=vf2_call_limit, vf2_max_trials=vf2_max_trials, - backend_properties=backend_properties, seed_transpiler=-1, use_barrier_before_measurement=True, ) @@ -806,7 +791,6 @@ def _swap_mapped(property_set): coupling_map=pass_manager_config.coupling_map, seed=-1, call_limit=int(5e4), # Set call limit to ~100ms with rustworkx 0.10.2 - properties=pass_manager_config.backend_properties, target=pass_manager_config.target, max_trials=2500, # Limits layout scoring to < 600ms on ~400 qubit devices ) @@ -839,7 +823,6 @@ def _swap_mapped(property_set): coupling_map=pass_manager_config.coupling_map, seed=-1, call_limit=int(5e6), # Set call limit to ~10s with rustworkx 0.10.2 - properties=pass_manager_config.backend_properties, target=pass_manager_config.target, max_trials=2500, # Limits layout scoring to < 600ms on ~400 qubit devices ) @@ -874,7 +857,6 @@ def _swap_mapped(property_set): coupling_map=pass_manager_config.coupling_map, seed=-1, call_limit=int(3e7), # Set call limit to ~60s with rustworkx 0.10.2 - properties=pass_manager_config.backend_properties, target=pass_manager_config.target, max_trials=250000, # Limits layout scoring to < 60s on ~400 qubit devices ) @@ -955,7 +937,6 @@ def _choose_layout_condition(property_set): ConditionalController( DenseLayout( coupling_map=pass_manager_config.coupling_map, - backend_prop=pass_manager_config.backend_properties, target=pass_manager_config.target, ), condition=_choose_layout_condition, diff --git a/qiskit/transpiler/preset_passmanagers/common.py b/qiskit/transpiler/preset_passmanagers/common.py index 0f0a6b7ea0a7..2c60a657e422 100644 --- a/qiskit/transpiler/preset_passmanagers/common.py +++ b/qiskit/transpiler/preset_passmanagers/common.py @@ -280,7 +280,6 @@ def generate_routing_passmanager( target, coupling_map=None, vf2_call_limit=None, - backend_properties=None, seed_transpiler=-1, check_trivial=False, use_barrier_before_measurement=True, @@ -297,8 +296,6 @@ def generate_routing_passmanager( vf2_call_limit (int): The internal call limit for the vf2 post layout pass. If this is ``None`` or ``0`` the vf2 post layout will not be run. - backend_properties (BackendProperties): Properties of a backend to - synthesize for (e.g. gate fidelities). seed_transpiler (int): Sets random seed for the stochastic parts of the transpiler. This is currently only used for :class:`.VF2PostLayout` and the default value of ``-1`` is strongly recommended (which is no randomization). @@ -354,13 +351,11 @@ def _swap_condition(property_set): routing.append(ConditionalController(routing_pass, condition=_swap_condition)) is_vf2_fully_bounded = vf2_call_limit and vf2_max_trials - if (target is not None or backend_properties is not None) and is_vf2_fully_bounded: + if target is not None and is_vf2_fully_bounded: routing.append( ConditionalController( VF2PostLayout( target, - coupling_map, - backend_properties, seed=seed_transpiler, call_limit=vf2_call_limit, max_trials=vf2_max_trials, diff --git a/qiskit/transpiler/preset_passmanagers/generate_preset_pass_manager.py b/qiskit/transpiler/preset_passmanagers/generate_preset_pass_manager.py index 62f991990d4e..0deb1879a07e 100644 --- a/qiskit/transpiler/preset_passmanagers/generate_preset_pass_manager.py +++ b/qiskit/transpiler/preset_passmanagers/generate_preset_pass_manager.py @@ -27,7 +27,7 @@ from qiskit.transpiler.instruction_durations import InstructionDurations from qiskit.transpiler.layout import Layout from qiskit.transpiler.passmanager_config import PassManagerConfig -from qiskit.transpiler.target import Target, target_to_backend_properties +from qiskit.transpiler.target import Target from qiskit.transpiler.timing_constraints import TimingConstraints from qiskit.utils import deprecate_arg from qiskit.utils.deprecate_pulse import deprecate_pulse_arg @@ -56,14 +56,6 @@ "with defined timing constraints with " "`Target.from_configuration(..., timing_constraints=...)`", ) -@deprecate_arg( - name="backend_properties", - since="1.3", - package_name="Qiskit", - removal_timeline="in Qiskit 2.0", - additional_msg="The `target` parameter should be used instead. You can build a `Target` instance " - "with defined properties with Target.from_configuration(..., backend_properties=...)", -) @deprecate_pulse_arg("inst_map", predicate=lambda inst_map: inst_map is not None) def generate_preset_pass_manager( optimization_level=2, @@ -73,7 +65,6 @@ def generate_preset_pass_manager( inst_map=None, coupling_map=None, instruction_durations=None, - backend_properties=None, timing_constraints=None, initial_layout=None, layout_method=None, @@ -102,7 +93,7 @@ def generate_preset_pass_manager( The target constraints for the pass manager construction can be specified through a :class:`.Target` instance, a :class:`.BackendV1` or :class:`.BackendV2` instance, or via loose constraints - (``basis_gates``, ``inst_map``, ``coupling_map``, ``backend_properties``, ``instruction_durations``, + (``basis_gates``, ``inst_map``, ``coupling_map``, ``instruction_durations``, ``dt`` or ``timing_constraints``). The order of priorities for target constraints works as follows: if a ``target`` input is provided, it will take priority over any ``backend`` input or loose constraints. @@ -123,7 +114,6 @@ def generate_preset_pass_manager( **inst_map** target inst_map inst_map **dt** target dt dt **timing_constraints** target timing_constraints timing_constraints - **backend_properties** target backend_properties backend_properties ============================ ========= ======================== ======================= Args: @@ -140,15 +130,14 @@ def generate_preset_pass_manager( backend (Backend): An optional backend object which can be used as the source of the default values for the ``basis_gates``, ``inst_map``, - ``coupling_map``, ``backend_properties``, ``instruction_durations``, + ``coupling_map``, ``instruction_durations``, ``timing_constraints``, and ``target``. If any of those other arguments are specified in addition to ``backend`` they will take precedence over the value contained in the backend. target (Target): The :class:`~.Target` representing a backend compilation target. The following attributes will be inferred from this argument if they are not set: ``coupling_map``, ``basis_gates``, - ``instruction_durations``, ``inst_map``, ``timing_constraints`` - and ``backend_properties``. + ``instruction_durations``, ``inst_map`` and ``timing_constraints``. basis_gates (list): List of basis gate names to unroll to (e.g: ``['u1', 'u2', 'u3', 'cx']``). inst_map (InstructionScheduleMap): DEPRECATED. Mapping object that maps gates to schedules. @@ -230,9 +219,6 @@ def generate_preset_pass_manager( for the ``scheduling`` stage of the output :class:`~.StagedPassManager`. You can see a list of installed plugins by using :func:`~.list_stage_plugins` with ``"scheduling"`` for the ``stage_name`` argument. - backend_properties (BackendProperties): Properties returned by a - backend, including information on gate errors, readout errors, - qubit coherence times, etc. approximation_degree (float): Heuristic dial used for circuit approximation (1.0=no approximation, 0.0=maximal approximation). seed_transpiler (int): Sets random seed for the stochastic parts of @@ -305,7 +291,6 @@ def generate_preset_pass_manager( and coupling_map is None and dt is None and instruction_durations is None - and backend_properties is None and timing_constraints is None ) # If it's an edge case => do not build target @@ -336,7 +321,6 @@ def generate_preset_pass_manager( elif not _skip_target: # Only parse backend properties when the target isn't skipped to # preserve the former behavior of transpile. - backend_properties = _parse_backend_properties(backend_properties, backend) with warnings.catch_warnings(): # TODO: inst_map will be removed in 2.0 warnings.filterwarnings( @@ -353,7 +337,6 @@ def generate_preset_pass_manager( # If the instruction map has custom gates, do not give as config, the information # will be added to the target with update_from_instruction_schedule_map inst_map=inst_map if inst_map and not inst_map.has_custom_gate() else None, - backend_properties=backend_properties, instruction_durations=instruction_durations, concurrent_measurements=( backend.target.concurrent_measurements if backend is not None else None @@ -380,18 +363,6 @@ def generate_preset_pass_manager( inst_map = target._get_instruction_schedule_map() if timing_constraints is None: timing_constraints = target.timing_constraints() - if backend_properties is None: - with warnings.catch_warnings(): - # TODO this approach (target-to-properties) is going to be removed soon (1.3) in favor - # of backend-to-target approach - # https://github.com/Qiskit/qiskit/pull/12850 - warnings.filterwarnings( - "ignore", - category=DeprecationWarning, - message=r".+qiskit\.transpiler\.target\.target_to_backend_properties.+", - module="qiskit", - ) - backend_properties = target_to_backend_properties(target) # Parse non-target dependent pm options initial_layout = _parse_initial_layout(initial_layout) @@ -404,7 +375,6 @@ def generate_preset_pass_manager( "inst_map": inst_map, "coupling_map": coupling_map, "instruction_durations": instruction_durations, - "backend_properties": backend_properties, "timing_constraints": timing_constraints, "layout_method": layout_method, "routing_method": routing_method, @@ -520,21 +490,6 @@ def _parse_inst_map(inst_map, backend): return inst_map -def _parse_backend_properties(backend_properties, backend): - # try getting backend_props from user, else backend - if backend_properties is None and backend is not None: - with warnings.catch_warnings(): - # filter target_to_backend_properties warning - warnings.filterwarnings( - "ignore", - category=DeprecationWarning, - message=".*``qiskit.transpiler.target.target_to_backend_properties\\(\\)``.*", - module="qiskit", - ) - backend_properties = target_to_backend_properties(backend.target) - return backend_properties - - def _parse_dt(dt, backend): # try getting dt from user, else backend if dt is None and backend is not None: diff --git a/releasenotes/notes/remove-backend-props-transpiler-64aa771784084313.yaml b/releasenotes/notes/remove-backend-props-transpiler-64aa771784084313.yaml new file mode 100644 index 000000000000..c1206d0a1e80 --- /dev/null +++ b/releasenotes/notes/remove-backend-props-transpiler-64aa771784084313.yaml @@ -0,0 +1,25 @@ +--- +upgrade_transpiler: + - | + The following deprecated uses of the ``BackendProperties`` object in the transpilation + pipeline have been removed in Qiskit 2.0: + + * ``backend_properties`` input argument in :func:`.transpile` + * ``backend_properties`` input argument in :class:`.PassManagerConfig` + * ``backend_properties`` input argument in :func:`.generate_preset_pass_manager` + * ``backend_properties`` input argument in :func:`.generate_routing_passmanager` + * ``backend_properties`` input argument in :func:`.generate_translation_passmanager` + * ``backend_properties`` input argument :meth:`.Target.from_configuration` + + The following passes have also been updated to only accept a ``target`` instead of: + + * ``backend_prop`` input argument in :class:`.DenseLayout` + * ``properties`` input argument in :class:`.VF2Layout` + * ``properties`` and ``coupling_map`` input arguments in :class:`.VF2PostLayout` + * ``backend_props`` input argument in :class:`.UnitarySynthesis` + + The ``BackendProperties`` class has been deprecated since Qiskit 1.2, as it was part + of the BackendV1 workflow. Specific instruction properties such as gate errors or + durations can be added to a :class:`.Target` upon construction through the + :meth:`.Target.add_instruction` method, and communicated to the relevant transpiler + passes through the `target` input argument. \ No newline at end of file diff --git a/test/python/compiler/test_transpiler.py b/test/python/compiler/test_transpiler.py index a0ea0dc118da..30a18e1fcf03 100644 --- a/test/python/compiler/test_transpiler.py +++ b/test/python/compiler/test_transpiler.py @@ -91,7 +91,6 @@ InstructionProperties, Target, InstructionDurations, - target_to_backend_properties, ) from test import QiskitTestCase, combine, slow_test # pylint: disable=wrong-import-order @@ -1606,78 +1605,6 @@ def test_scheduling_dt_constraints(self): scheduled = transpile(qc, backend=backend_v2, scheduling_method="asap", dt=original_dt / 2) self.assertEqual(scheduled.duration, original_duration * 2) - def test_backend_props_constraints(self): - """Test that loose transpile constraints work with both BackendV1 and BackendV2.""" - - with self.assertWarns(DeprecationWarning): - backend_v1 = Fake20QV1() - backend_v2 = BackendV2Converter(backend_v1) - qr1 = QuantumRegister(3, "qr1") - qr2 = QuantumRegister(2, "qr2") - qc = QuantumCircuit(qr1, qr2) - qc.cx(qr1[0], qr1[1]) - qc.cx(qr1[1], qr1[2]) - qc.cx(qr1[2], qr2[0]) - qc.cx(qr2[0], qr2[1]) - - # generate a fake backend with same number of qubits - # but different backend properties - fake_backend = GenericBackendV2(num_qubits=20, seed=42) - with self.assertWarns(DeprecationWarning): - custom_backend_properties = target_to_backend_properties(fake_backend.target) - - # expected layout for custom_backend_properties - # (different from expected layout for Fake20QV1) - vf2_layout = { - 18: Qubit(QuantumRegister(3, "qr1"), 1), - 13: Qubit(QuantumRegister(3, "qr1"), 2), - 19: Qubit(QuantumRegister(3, "qr1"), 0), - 14: Qubit(QuantumRegister(2, "qr2"), 0), - 9: Qubit(QuantumRegister(2, "qr2"), 1), - 0: Qubit(QuantumRegister(15, "ancilla"), 0), - 1: Qubit(QuantumRegister(15, "ancilla"), 1), - 2: Qubit(QuantumRegister(15, "ancilla"), 2), - 3: Qubit(QuantumRegister(15, "ancilla"), 3), - 4: Qubit(QuantumRegister(15, "ancilla"), 4), - 5: Qubit(QuantumRegister(15, "ancilla"), 5), - 6: Qubit(QuantumRegister(15, "ancilla"), 6), - 7: Qubit(QuantumRegister(15, "ancilla"), 7), - 8: Qubit(QuantumRegister(15, "ancilla"), 8), - 10: Qubit(QuantumRegister(15, "ancilla"), 9), - 11: Qubit(QuantumRegister(15, "ancilla"), 10), - 12: Qubit(QuantumRegister(15, "ancilla"), 11), - 15: Qubit(QuantumRegister(15, "ancilla"), 12), - 16: Qubit(QuantumRegister(15, "ancilla"), 13), - 17: Qubit(QuantumRegister(15, "ancilla"), 14), - } - - with self.assertWarnsRegex( - DeprecationWarning, - expected_regex="The `transpile` function will stop supporting inputs of type `BackendV1` ", - ): - result = transpile( - qc, - backend=backend_v1, - backend_properties=custom_backend_properties, - optimization_level=2, - seed_transpiler=42, - ) - - self.assertEqual(result._layout.initial_layout._p2v, vf2_layout) - with self.assertWarnsRegex( - DeprecationWarning, - expected_regex="The `target` parameter should be used instead", - ): - result = transpile( - qc, - backend=backend_v2, - backend_properties=custom_backend_properties, - optimization_level=2, - seed_transpiler=42, - ) - - self.assertEqual(result._layout.initial_layout._p2v, vf2_layout) - @data(1, 2, 3) def test_no_infinite_loop(self, optimization_level): """Verify circuit cost always descends and optimization does not flip flop indefinitely.""" @@ -2642,7 +2569,6 @@ def test_custom_multiple_circuits(self): initial_layout=None, basis_gates=["u1", "u2", "u3", "cx"], coupling_map=CouplingMap([[0, 1]]), - backend_properties=None, seed_transpiler=1, ) passmanager = level_0_pass_manager(pm_conf) diff --git a/test/python/transpiler/test_passmanager_config.py b/test/python/transpiler/test_passmanager_config.py index fccd68f0a261..aa93ab2af4fe 100644 --- a/test/python/transpiler/test_passmanager_config.py +++ b/test/python/transpiler/test_passmanager_config.py @@ -135,7 +135,6 @@ def test_str(self): \ttranslation_method: None \tscheduling_method: None \tinstruction_durations:\u0020 -\tbackend_properties: None \tapproximation_degree: None \tseed_transpiler: None \ttiming_constraints: None diff --git a/test/python/transpiler/test_target.py b/test/python/transpiler/test_target.py index 694ef38dc4fa..6092fcad664b 100644 --- a/test/python/transpiler/test_target.py +++ b/test/python/transpiler/test_target.py @@ -1723,41 +1723,10 @@ def test_basis_gates_coupling_map(self): self.assertEqual({(0,), (1,), (2,)}, target["u"].keys()) self.assertEqual({(0, 1), (1, 2), (2, 0)}, target["cx"].keys()) - def test_properties(self): - with self.assertWarns(DeprecationWarning): - fake_backend = Fake5QV1() - config = fake_backend.configuration() - properties = fake_backend.properties() - target = Target.from_configuration( - basis_gates=config.basis_gates, - num_qubits=config.num_qubits, - coupling_map=CouplingMap(config.coupling_map), - backend_properties=properties, - ) - self.assertEqual(0, target["rz"][(0,)].error) - self.assertEqual(0, target["rz"][(0,)].duration) - - def test_properties_with_durations(self): - with self.assertWarns(DeprecationWarning): - fake_backend = Fake5QV1() - config = fake_backend.configuration() - properties = fake_backend.properties() - durations = InstructionDurations([("rz", 0, 0.5)], dt=1.0) - target = Target.from_configuration( - basis_gates=config.basis_gates, - num_qubits=config.num_qubits, - coupling_map=CouplingMap(config.coupling_map), - backend_properties=properties, - instruction_durations=durations, - dt=config.dt, - ) - self.assertEqual(0.5, target["rz"][(0,)].duration) - def test_inst_map(self): with self.assertWarns(DeprecationWarning): fake_backend = Fake7QPulseV1() config = fake_backend.configuration() - properties = fake_backend.properties() defaults = fake_backend.defaults() constraints = TimingConstraints(**config.timing_constraints) with self.assertWarns(DeprecationWarning): @@ -1765,7 +1734,6 @@ def test_inst_map(self): basis_gates=config.basis_gates, num_qubits=config.num_qubits, coupling_map=CouplingMap(config.coupling_map), - backend_properties=properties, dt=config.dt, inst_map=defaults.instruction_schedule_map, timing_constraints=constraints, diff --git a/test/python/transpiler/test_vf2_layout.py b/test/python/transpiler/test_vf2_layout.py index 9fbc9ac45480..c03bb0498618 100644 --- a/test/python/transpiler/test_vf2_layout.py +++ b/test/python/transpiler/test_vf2_layout.py @@ -27,7 +27,7 @@ from qiskit.transpiler.passes.layout.vf2_layout import VF2Layout, VF2LayoutStopReason from qiskit._accelerate.error_map import ErrorMap from qiskit.converters import circuit_to_dag -from qiskit.providers.fake_provider import Fake5QV1, Fake127QPulseV1, GenericBackendV2 +from qiskit.providers.fake_provider import GenericBackendV2 from qiskit.circuit import Measure from qiskit.circuit.library import GraphStateGate, CXGate, XGate, HGate from qiskit.transpiler import PassManager, AnalysisPass @@ -35,7 +35,7 @@ from qiskit.transpiler.preset_passmanagers.common import generate_embed_passmanager from test import QiskitTestCase # pylint: disable=wrong-import-order -from ..legacy_cmaps import TENERIFE_CMAP, RUESCHLIKON_CMAP, MANHATTAN_CMAP +from ..legacy_cmaps import TENERIFE_CMAP, RUESCHLIKON_CMAP, MANHATTAN_CMAP, YORKTOWN_CMAP class LayoutTestCase(QiskitTestCase): @@ -631,31 +631,28 @@ def test_no_properties(self): def test_with_properties(self): """Test it finds the least noise perfect layout with no properties.""" - with self.assertWarns(DeprecationWarning): - backend = Fake5QV1() qr = QuantumRegister(2) qc = QuantumCircuit(qr) qc.x(qr) qc.measure_all() - cmap = CouplingMap(backend.configuration().coupling_map) - properties = backend.properties() - vf2_pass = VF2Layout(cmap, properties=properties) + cmap = CouplingMap(YORKTOWN_CMAP) + backend = GenericBackendV2(num_qubits=5, coupling_map=cmap, seed=15) + vf2_pass = VF2Layout(target=backend.target) property_set = {} vf2_pass(qc, property_set) self.assertEqual(set(property_set["layout"].get_physical_bits()), {1, 3}) def test_max_trials_exceeded(self): """Test it exits when max_trials is reached.""" - with self.assertWarns(DeprecationWarning): - backend = Fake5QV1() + qr = QuantumRegister(2) qc = QuantumCircuit(qr) qc.x(qr) qc.cx(0, 1) qc.measure_all() - cmap = CouplingMap(backend.configuration().coupling_map) - properties = backend.properties() - vf2_pass = VF2Layout(cmap, properties=properties, seed=-1, max_trials=1) + cmap = CouplingMap(YORKTOWN_CMAP) + backend = GenericBackendV2(num_qubits=5, coupling_map=cmap, seed=1) + vf2_pass = VF2Layout(target=backend.target, seed=-1, max_trials=1) property_set = {} with self.assertLogs("qiskit.transpiler.passes.layout.vf2_layout", level="DEBUG") as cm: vf2_pass(qc, property_set) @@ -667,16 +664,14 @@ def test_max_trials_exceeded(self): def test_time_limit_exceeded(self): """Test the pass stops after time_limit is reached.""" - with self.assertWarns(DeprecationWarning): - backend = Fake5QV1() qr = QuantumRegister(2) qc = QuantumCircuit(qr) qc.x(qr) qc.cx(0, 1) qc.measure_all() - cmap = CouplingMap(backend.configuration().coupling_map) - properties = backend.properties() - vf2_pass = VF2Layout(cmap, properties=properties, seed=-1, time_limit=0.0) + cmap = CouplingMap(YORKTOWN_CMAP) + backend = GenericBackendV2(num_qubits=5, coupling_map=cmap, seed=1) + vf2_pass = VF2Layout(target=backend.target, seed=-1, time_limit=0.0) property_set = {} with self.assertLogs("qiskit.transpiler.passes.layout.vf2_layout", level="DEBUG") as cm: vf2_pass(qc, property_set) @@ -690,27 +685,6 @@ def test_time_limit_exceeded(self): self.assertEqual(set(property_set["layout"].get_physical_bits()), {2, 0}) - def test_reasonable_limits_for_simple_layouts_v1(self): - """Test that the default trials is set to a reasonable number. - REMOVE ONCE Fake127QPulseV1 IS GONE""" - with self.assertWarns(DeprecationWarning): - backend = Fake127QPulseV1() - qc = QuantumCircuit(5) - qc.cx(2, 3) - qc.cx(0, 1) - cmap = CouplingMap(backend.configuration().coupling_map) - properties = backend.properties() - # Run without any limits set - vf2_pass = VF2Layout(cmap, properties=properties, seed=42) - property_set = {} - with self.assertLogs("qiskit.transpiler.passes.layout.vf2_layout", level="DEBUG") as cm: - vf2_pass(qc, property_set) - self.assertIn( - "DEBUG:qiskit.transpiler.passes.layout.vf2_layout:Trial 299 is >= configured max trials 299", - cm.output, - ) - self.assertEqual(set(property_set["layout"].get_physical_bits()), {57, 58, 61, 62, 0}) - def test_reasonable_limits_for_simple_layouts(self): """Test that the default trials is set to a reasonable number.""" backend = GenericBackendV2(27, seed=42) @@ -731,16 +705,14 @@ def test_reasonable_limits_for_simple_layouts(self): def test_no_limits_with_negative(self): """Test that we're not enforcing a trial limit if set to negative.""" - with self.assertWarns(DeprecationWarning): - backend = Fake5QV1() qc = QuantumCircuit(3) qc.h(0) - cmap = CouplingMap(backend.configuration().coupling_map) - properties = backend.properties() + cmap = CouplingMap(YORKTOWN_CMAP) + backend = GenericBackendV2(num_qubits=5, coupling_map=cmap, seed=4) + # Run without any limits set vf2_pass = VF2Layout( - cmap, - properties=properties, + target=backend.target, seed=42, max_trials=0, ) diff --git a/test/python/transpiler/test_vf2_post_layout.py b/test/python/transpiler/test_vf2_post_layout.py index 9aaab695197a..8b2548e38b6f 100644 --- a/test/python/transpiler/test_vf2_post_layout.py +++ b/test/python/transpiler/test_vf2_post_layout.py @@ -17,10 +17,10 @@ from qiskit import QuantumRegister, QuantumCircuit from qiskit.circuit import ControlFlowOp from qiskit.circuit.library import CXGate, XGate -from qiskit.transpiler import CouplingMap, Layout, TranspilerError +from qiskit.transpiler import Layout, TranspilerError from qiskit.transpiler.passes.layout.vf2_post_layout import VF2PostLayout, VF2PostLayoutStopReason from qiskit.converters import circuit_to_dag -from qiskit.providers.fake_provider import Fake5QV1, GenericBackendV2 +from qiskit.providers.fake_provider import GenericBackendV2 from qiskit.circuit import Qubit from qiskit.compiler.transpiler import transpile from qiskit.transpiler.target import Target, InstructionProperties @@ -34,36 +34,9 @@ class TestVF2PostLayout(QiskitTestCase): seed = 42 - def assertLayout(self, dag, coupling_map, property_set): - """Checks if the circuit in dag was a perfect layout in property_set for the given - coupling_map""" - self.assertEqual( - property_set["VF2PostLayout_stop_reason"], VF2PostLayoutStopReason.SOLUTION_FOUND - ) - - layout = property_set["post_layout"] - edges = coupling_map.graph.edge_list() - - def run(dag, wire_map): - for gate in dag.two_qubit_ops(): - with self.assertWarns(DeprecationWarning): - if dag.has_calibration_for(gate) or isinstance(gate.op, ControlFlowOp): - continue - physical_q0 = wire_map[gate.qargs[0]] - physical_q1 = wire_map[gate.qargs[1]] - self.assertTrue((physical_q0, physical_q1) in edges) - for node in dag.op_nodes(ControlFlowOp): - for block in node.op.blocks: - inner_wire_map = { - inner: wire_map[outer] for outer, inner in zip(node.qargs, block.qubits) - } - run(circuit_to_dag(block), inner_wire_map) - - run(dag, {bit: layout[bit] for bit in dag.qubits if bit in layout}) - def assertLayoutV2(self, dag, target, property_set): """Checks if the circuit in dag was a perfect layout in property_set for the given - coupling_map""" + target""" self.assertEqual( property_set["VF2PostLayout_stop_reason"], VF2PostLayoutStopReason.SOLUTION_FOUND ) @@ -95,27 +68,6 @@ def test_no_constraints(self): with self.assertRaises(TranspilerError): empty_pass.run(circuit_to_dag(qc)) - def test_no_backend_properties(self): - """Test we raise at runtime if no properties are provided with a coupling graph.""" - qc = QuantumCircuit(2) - empty_pass = VF2PostLayout(coupling_map=CouplingMap([(0, 1), (1, 2)])) - with self.assertRaises(TranspilerError): - empty_pass.run(circuit_to_dag(qc)) - - def test_empty_circuit(self): - """Test no solution found for empty circuit""" - with self.assertWarns(DeprecationWarning): - backend = Fake5QV1() - qc = QuantumCircuit(2, 2) - cmap = CouplingMap(backend.configuration().coupling_map) - props = backend.properties() - vf2_pass = VF2PostLayout(coupling_map=cmap, properties=props) - vf2_pass.run(circuit_to_dag(qc)) - self.assertEqual( - vf2_pass.property_set["VF2PostLayout_stop_reason"], - VF2PostLayoutStopReason.NO_BETTER_SOLUTION_FOUND, - ) - def test_empty_circuit_v2(self): """Test no solution found for empty circuit with v2 backend""" qc = QuantumCircuit(2, 2) @@ -129,35 +81,6 @@ def test_empty_circuit_v2(self): VF2PostLayoutStopReason.NO_BETTER_SOLUTION_FOUND, ) - def test_skip_3q_circuit(self): - """Test that the pass is a no-op on circuits with >2q gates.""" - with self.assertWarns(DeprecationWarning): - backend = Fake5QV1() - qc = QuantumCircuit(3) - qc.ccx(0, 1, 2) - cmap = CouplingMap(backend.configuration().coupling_map) - props = backend.properties() - vf2_pass = VF2PostLayout(coupling_map=cmap, properties=props) - vf2_pass.run(circuit_to_dag(qc)) - self.assertEqual( - vf2_pass.property_set["VF2PostLayout_stop_reason"], VF2PostLayoutStopReason.MORE_THAN_2Q - ) - - def test_skip_3q_circuit_control_flow(self): - """Test that the pass is a no-op on circuits with >2q gates.""" - with self.assertWarns(DeprecationWarning): - backend = Fake5QV1() - qc = QuantumCircuit(3) - with qc.for_loop((1,)): - qc.ccx(0, 1, 2) - cmap = CouplingMap(backend.configuration().coupling_map) - props = backend.properties() - vf2_pass = VF2PostLayout(coupling_map=cmap, properties=props) - vf2_pass.run(circuit_to_dag(qc)) - self.assertEqual( - vf2_pass.property_set["VF2PostLayout_stop_reason"], VF2PostLayoutStopReason.MORE_THAN_2Q - ) - def test_skip_3q_circuit_v2(self): """Test that the pass is a no-op on circuits with >2q gates with a target.""" qc = QuantumCircuit(3) @@ -185,58 +108,6 @@ def test_skip_3q_circuit_control_flow_v2(self): vf2_pass.property_set["VF2PostLayout_stop_reason"], VF2PostLayoutStopReason.MORE_THAN_2Q ) - def test_2q_circuit_5q_backend(self): - """A simple example, without considering the direction - 0 - 1 - qr1 - qr0 - """ - with self.assertWarns(DeprecationWarning): - backend = Fake5QV1() - - qr = QuantumRegister(2, "qr") - circuit = QuantumCircuit(qr) - circuit.cx(qr[1], qr[0]) # qr1 -> qr0 - with self.assertWarnsRegex( - DeprecationWarning, - expected_regex="The `transpile` function will " - "stop supporting inputs of type `BackendV1`", - ): - tqc = transpile(circuit, backend, layout_method="dense") - initial_layout = tqc._layout - dag = circuit_to_dag(tqc) - cmap = CouplingMap(backend.configuration().coupling_map) - props = backend.properties() - pass_ = VF2PostLayout(coupling_map=cmap, properties=props, seed=self.seed) - pass_.run(dag) - self.assertLayout(dag, cmap, pass_.property_set) - self.assertNotEqual(pass_.property_set["post_layout"], initial_layout) - - def test_2q_circuit_5q_backend_controlflow(self): - """A simple example, without considering the direction - 0 - 1 - qr1 - qr0 - """ - with self.assertWarns(DeprecationWarning): - backend = Fake5QV1() - - circuit = QuantumCircuit(2, 1) - with circuit.for_loop((1,)): - circuit.cx(1, 0) # qr1 -> qr0 - with circuit.if_test((circuit.clbits[0], True)) as else_: - pass - with else_: - with circuit.while_loop((circuit.clbits[0], True)): - circuit.cx(1, 0) # qr1 -> qr0 - initial_layout = Layout(dict(enumerate(circuit.qubits))) - circuit._layout = initial_layout - dag = circuit_to_dag(circuit) - cmap = CouplingMap(backend.configuration().coupling_map) - props = backend.properties() - pass_ = VF2PostLayout(coupling_map=cmap, properties=props, seed=self.seed) - pass_.run(dag) - self.assertLayout(dag, cmap, pass_.property_set) - self.assertNotEqual(pass_.property_set["post_layout"], initial_layout) - def test_2q_circuit_5q_backend_max_trials(self): """A simple example, without considering the direction 0 - 1 @@ -256,7 +127,6 @@ def test_2q_circuit_5q_backend_max_trials(self): tqc = transpile(circuit, backend, layout_method="dense") initial_layout = tqc._layout dag = circuit_to_dag(tqc) - cmap = CouplingMap(backend.coupling_map) pass_ = VF2PostLayout(target=backend.target, seed=self.seed, max_trials=max_trials) with self.assertLogs( "qiskit.transpiler.passes.layout.vf2_post_layout", level="DEBUG" @@ -270,46 +140,7 @@ def test_2q_circuit_5q_backend_max_trials(self): self.assertEqual( pass_.property_set["VF2PostLayout_stop_reason"], VF2PostLayoutStopReason.SOLUTION_FOUND ) - self.assertLayout(dag, cmap, pass_.property_set) - self.assertNotEqual(pass_.property_set["post_layout"], initial_layout) - - def test_2q_circuit_5q_backend_max_trials_v1(self): - """A simple example, without considering the direction - 0 - 1 - qr1 - qr0 - """ - max_trials = 11 - with self.assertWarns(DeprecationWarning): - backend = Fake5QV1() - - qr = QuantumRegister(2, "qr") - circuit = QuantumCircuit(qr) - circuit.cx(qr[1], qr[0]) # qr1 -> qr0 - with self.assertWarnsRegex( - DeprecationWarning, - expected_regex="The `transpile` function will " - "stop supporting inputs of type `BackendV1`", - ): - tqc = transpile(circuit, backend, layout_method="dense") - initial_layout = tqc._layout - dag = circuit_to_dag(tqc) - cmap = CouplingMap(backend.configuration().coupling_map) - props = backend.properties() - pass_ = VF2PostLayout( - coupling_map=cmap, properties=props, seed=self.seed, max_trials=max_trials - ) - - with self.assertLogs( - "qiskit.transpiler.passes.layout.vf2_post_layout", level="DEBUG" - ) as cm: - pass_.run(dag) - self.assertIn( - f"DEBUG:qiskit.transpiler.passes.layout.vf2_post_layout:Trial {max_trials} " - f"is >= configured max trials {max_trials}", - cm.output, - ) - - self.assertLayout(dag, cmap, pass_.property_set) + self.assertLayoutV2(dag, backend.target, pass_.property_set) self.assertNotEqual(pass_.property_set["post_layout"], initial_layout) def test_best_mapping_ghz_state_full_device_multiple_qregs(self): @@ -492,7 +323,7 @@ def test_last_qubits_best(self): ) dag = circuit_to_dag(circuit) vf2_pass.run(dag) - self.assertLayout(dag, target_last_qubits_best.build_coupling_map(), vf2_pass.property_set) + self.assertLayoutV2(dag, target_last_qubits_best, vf2_pass.property_set) class TestVF2PostLayoutScoring(QiskitTestCase): @@ -587,30 +418,6 @@ def test_no_constraints(self): with self.assertRaises(TranspilerError): empty_pass.run(circuit_to_dag(qc)) - def test_no_backend_properties(self): - """Test we raise at runtime if no properties are provided with a coupling graph.""" - qc = QuantumCircuit(2) - empty_pass = VF2PostLayout( - coupling_map=CouplingMap([(0, 1), (1, 2)]), strict_direction=False - ) - with self.assertRaises(TranspilerError): - empty_pass.run(circuit_to_dag(qc)) - - def test_empty_circuit(self): - """Test no solution found for empty circuit""" - with self.assertWarns(DeprecationWarning): - backend = Fake5QV1() - - qc = QuantumCircuit(2, 2) - cmap = CouplingMap(backend.configuration().coupling_map) - props = backend.properties() - vf2_pass = VF2PostLayout(coupling_map=cmap, properties=props, strict_direction=False) - vf2_pass.run(circuit_to_dag(qc)) - self.assertEqual( - vf2_pass.property_set["VF2PostLayout_stop_reason"], - VF2PostLayoutStopReason.NO_BETTER_SOLUTION_FOUND, - ) - def test_empty_circuit_v2(self): """Test no solution found for empty circuit with v2 backend""" qc = QuantumCircuit(2, 2) @@ -627,22 +434,6 @@ def test_empty_circuit_v2(self): VF2PostLayoutStopReason.NO_BETTER_SOLUTION_FOUND, ) - def test_skip_3q_circuit(self): - """Test that the pass is a no-op on circuits with >2q gates.""" - with self.assertWarns(DeprecationWarning): - backend = Fake5QV1() - - qc = QuantumCircuit(3) - qc.ccx(0, 1, 2) - cmap = CouplingMap(backend.configuration().coupling_map) - props = backend.properties() - vf2_pass = VF2PostLayout(coupling_map=cmap, properties=props, strict_direction=False) - vf2_pass.run(circuit_to_dag(qc)) - self.assertEqual( - vf2_pass.property_set["VF2PostLayout_stop_reason"], - VF2PostLayoutStopReason.MORE_THAN_2Q, - ) - def test_skip_3q_circuit_v2(self): """Test that the pass is a no-op on circuits with >2q gates with a target.""" qc = QuantumCircuit(3) @@ -680,40 +471,9 @@ def test_best_mapping_ghz_state_full_device_multiple_qregs(self): tqc = transpile(qc, seed_transpiler=self.seed, layout_method="trivial") initial_layout = tqc._layout dag = circuit_to_dag(tqc) - cmap = CouplingMap(backend.coupling_map) pass_ = VF2PostLayout(target=backend.target, seed=self.seed, strict_direction=False) pass_.run(dag) - self.assertLayout(dag, cmap, pass_.property_set) - self.assertNotEqual(pass_.property_set["post_layout"], initial_layout) - - def test_best_mapping_ghz_state_full_device_multiple_qregs_v1(self): - """Test best mappings with multiple registers""" - with self.assertWarns(DeprecationWarning): - backend = Fake5QV1() - qr_a = QuantumRegister(2) - qr_b = QuantumRegister(3) - qc = QuantumCircuit(qr_a, qr_b) - qc.h(qr_a[0]) - qc.cx(qr_a[0], qr_a[1]) - qc.cx(qr_a[0], qr_b[0]) - qc.cx(qr_a[0], qr_b[1]) - qc.cx(qr_a[0], qr_b[2]) - qc.measure_all() - with self.assertWarnsRegex( - DeprecationWarning, - expected_regex="The `transpile` function will " - "stop supporting inputs of type `BackendV1`", - ): - tqc = transpile(qc, backend, seed_transpiler=self.seed, layout_method="trivial") - initial_layout = tqc._layout - dag = circuit_to_dag(tqc) - cmap = CouplingMap(backend.configuration().coupling_map) - props = backend.properties() - pass_ = VF2PostLayout( - coupling_map=cmap, properties=props, seed=self.seed, strict_direction=False - ) - pass_.run(dag) - self.assertLayout(dag, cmap, pass_.property_set) + self.assertLayoutV2(dag, backend.target, pass_.property_set) self.assertNotEqual(pass_.property_set["post_layout"], initial_layout) def test_2q_circuit_5q_backend(self): @@ -733,38 +493,9 @@ def test_2q_circuit_5q_backend(self): tqc = transpile(circuit, backend, layout_method="dense") initial_layout = tqc._layout dag = circuit_to_dag(tqc) - cmap = CouplingMap(backend.coupling_map) pass_ = VF2PostLayout(target=backend.target, seed=self.seed, strict_direction=False) pass_.run(dag) - self.assertLayout(dag, cmap, pass_.property_set) - self.assertNotEqual(pass_.property_set["post_layout"], initial_layout) - - def test_2q_circuit_5q_backend_v1(self): - """A simple example, without considering the direction - 0 - 1 - qr1 - qr0 - """ - with self.assertWarns(DeprecationWarning): - backend = Fake5QV1() - - qr = QuantumRegister(2, "qr") - circuit = QuantumCircuit(qr) - circuit.cx(qr[1], qr[0]) # qr1 -> qr0 - with self.assertWarnsRegex( - DeprecationWarning, - expected_regex="The `transpile` function will " - "stop supporting inputs of type `BackendV1`", - ): - tqc = transpile(circuit, backend, layout_method="dense") - initial_layout = tqc._layout - dag = circuit_to_dag(tqc) - cmap = CouplingMap(backend.configuration().coupling_map) - props = backend.properties() - pass_ = VF2PostLayout( - coupling_map=cmap, properties=props, seed=self.seed, strict_direction=False - ) - pass_.run(dag) - self.assertLayout(dag, cmap, pass_.property_set) + self.assertLayoutV2(dag, backend.target, pass_.property_set) self.assertNotEqual(pass_.property_set["post_layout"], initial_layout) def test_best_mapping_ghz_state_full_device_multiple_qregs_v2(self): From 8c50ce46316a38898884449ace5411122bf5c7ba Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Wed, 19 Feb 2025 04:29:59 -0500 Subject: [PATCH 45/48] Add method to compute estimated duration of scheduled circuit (#13783) * Add method to compute estimated duration of scheduled circuit This commit adds a new QuantumCircuit method to compute the estimated duration of a scheduled circuit. This is to replace the deprecated duration attribute that the transpiler was potentially setting during the scheduling stage. This method computes the longest duration path in the dag view of the circuit internally. This method should have been included in the 1.2.0 release prior to the deprecation of the `QuantumCircuit.duration` attribute in 1.3.0. But, this was an oversight in the path to deprecation, as it was part of larger deprecation of numerous scheduling pieces in the 1.3.0. We should definitely backport this for the 1.4.0 release for inclusion in that release prior to the Qiskit 2.0.0 release which removes the deprecated attribute * Simplify dag node indexing * Expand docs * Fix handling for StandardInstruction in rust and add first test * Expand test coverage * Fix lint --- crates/accelerate/src/circuit_duration.rs | 109 ++++++++++++++++ crates/accelerate/src/lib.rs | 1 + .../accelerate/src/target_transpiler/mod.rs | 11 ++ crates/pyext/src/lib.rs | 1 + qiskit/__init__.py | 1 + qiskit/circuit/quantumcircuit.py | 65 +++++++++- ...mate_duration-method-a35bf8eef4b2f210.yaml | 7 + test/python/circuit/test_scheduled_circuit.py | 120 ++++++++++++++++++ 8 files changed, 314 insertions(+), 1 deletion(-) create mode 100644 crates/accelerate/src/circuit_duration.rs create mode 100644 releasenotes/notes/add-estimate_duration-method-a35bf8eef4b2f210.yaml diff --git a/crates/accelerate/src/circuit_duration.rs b/crates/accelerate/src/circuit_duration.rs new file mode 100644 index 000000000000..0e9738822f84 --- /dev/null +++ b/crates/accelerate/src/circuit_duration.rs @@ -0,0 +1,109 @@ +// This code is part of Qiskit. +// +// (C) Copyright IBM 2025 +// +// 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. + +use pyo3::prelude::*; +use pyo3::wrap_pyfunction; + +use qiskit_circuit::dag_circuit::{DAGCircuit, NodeType, Wire}; +use qiskit_circuit::operations::{DelayUnit, Operation, OperationRef, Param, StandardInstruction}; + +use crate::nlayout::PhysicalQubit; +use crate::target_transpiler::Target; +use crate::QiskitError; +use rustworkx_core::dag_algo::longest_path; +use rustworkx_core::petgraph::stable_graph::StableDiGraph; +use rustworkx_core::petgraph::visit::{EdgeRef, IntoEdgeReferences}; + +/// Estimate the duration of a scheduled circuit in seconds +#[pyfunction] +pub(crate) fn compute_estimated_duration(dag: &DAGCircuit, target: &Target) -> PyResult { + let dt = target.dt; + + let get_duration = + |edge: <&StableDiGraph as IntoEdgeReferences>::EdgeRef| -> PyResult { + let node_weight = &dag[edge.target()]; + match node_weight { + NodeType::Operation(inst) => { + let name = inst.op.name(); + let qubits = dag.get_qargs(inst.qubits); + let physical_qubits: Vec = + qubits.iter().map(|x| PhysicalQubit::new(x.0)).collect(); + + if let OperationRef::StandardInstruction(op) = inst.op.view() { + if let StandardInstruction::Delay(unit) = op { + let dur = &inst.params.as_ref().unwrap()[0]; + return if unit == DelayUnit::DT { + if let Some(dt) = dt { + match dur { + Param::Float(val) => + { + Ok(val / dt) + + }, + Param::Obj(val) => { + Python::with_gil(|py| { + let dur_float: f64 = val.extract(py)?; + Ok(dur_float * dt) + }) + }, + Param::ParameterExpression(_) => Err(QiskitError::new_err( + "Circuit contains parameterized delays, can't compute a duration estimate with this circuit" + )), + } + } else { + Err(QiskitError::new_err( + "Circuit contains delays in dt but the target doesn't specify dt" + )) + } + } else if unit == DelayUnit::S { + match dur { + Param::Float(val) => Ok(*val), + _ => Err(QiskitError::new_err( + "Invalid type for parameter value for delay in circuit", + )), + } + } else { + Err(QiskitError::new_err( + "Circuit contains delays in units other then seconds or dt, the circuit is not scheduled." + )) + }; + } else if let StandardInstruction::Barrier(_) = op { + return Ok(0.); + } + } + match target.get_duration(name, &physical_qubits) { + Some(dur) => Ok(dur), + None => Err(QiskitError::new_err(format!( + "Duration not found for {} on qubits: {:?}", + name, qubits + ))), + } + } + NodeType::QubitOut(_) | NodeType::ClbitOut(_) => Ok(0.), + NodeType::ClbitIn(_) | NodeType::QubitIn(_) => { + Err(QiskitError::new_err("Invalid circuit provided")) + } + _ => Err(QiskitError::new_err( + "Circuit contains Vars, duration can't be calculated with classical variables", + )), + } + }; + match longest_path(dag.dag(), get_duration)? { + Some((_, weight)) => Ok(weight), + None => Err(QiskitError::new_err("Invalid circuit provided")), + } +} + +pub fn compute_duration(m: &Bound) -> PyResult<()> { + m.add_wrapped(wrap_pyfunction!(compute_estimated_duration))?; + Ok(()) +} diff --git a/crates/accelerate/src/lib.rs b/crates/accelerate/src/lib.rs index e26689f4f0c3..76b26d22a771 100644 --- a/crates/accelerate/src/lib.rs +++ b/crates/accelerate/src/lib.rs @@ -17,6 +17,7 @@ use pyo3::import_exception; pub mod barrier_before_final_measurement; pub mod basis; pub mod check_map; +pub mod circuit_duration; pub mod circuit_library; pub mod commutation_analysis; pub mod commutation_cancellation; diff --git a/crates/accelerate/src/target_transpiler/mod.rs b/crates/accelerate/src/target_transpiler/mod.rs index 9df6047a494b..b03f2e95523f 100644 --- a/crates/accelerate/src/target_transpiler/mod.rs +++ b/crates/accelerate/src/target_transpiler/mod.rs @@ -977,6 +977,17 @@ impl Target { }) } + /// Get the duration of a given instruction in the target + pub fn get_duration(&self, name: &str, qargs: &[PhysicalQubit]) -> Option { + self.gate_map.get(name).and_then(|gate_props| { + let qargs_key: Qargs = qargs.iter().cloned().collect(); + match gate_props.get(Some(&qargs_key)) { + Some(props) => props.as_ref().and_then(|inst_props| inst_props.duration), + None => None, + } + }) + } + /// Get an iterator over the indices of all physical qubits of the target pub fn physical_qubits(&self) -> impl ExactSizeIterator { 0..self.num_qubits.unwrap_or_default() diff --git a/crates/pyext/src/lib.rs b/crates/pyext/src/lib.rs index d2a0db8d19fd..f93f2af32a81 100644 --- a/crates/pyext/src/lib.rs +++ b/crates/pyext/src/lib.rs @@ -31,6 +31,7 @@ fn _accelerate(m: &Bound) -> PyResult<()> { add_submodule(m, ::qiskit_accelerate::barrier_before_final_measurement::barrier_before_final_measurements_mod, "barrier_before_final_measurement")?; add_submodule(m, ::qiskit_accelerate::basis::basis, "basis")?; add_submodule(m, ::qiskit_accelerate::check_map::check_map_mod, "check_map")?; + add_submodule(m, ::qiskit_accelerate::circuit_duration::compute_duration, "circuit_duration")?; add_submodule(m, ::qiskit_accelerate::circuit_library::circuit_library, "circuit_library")?; add_submodule(m, ::qiskit_accelerate::commutation_analysis::commutation_analysis, "commutation_analysis")?; add_submodule(m, ::qiskit_accelerate::commutation_cancellation::commutation_cancellation, "commutation_cancellation")?; diff --git a/qiskit/__init__.py b/qiskit/__init__.py index 376881a56799..9ac2702f4cf2 100644 --- a/qiskit/__init__.py +++ b/qiskit/__init__.py @@ -110,6 +110,7 @@ sys.modules["qiskit._accelerate.twirling"] = _accelerate.twirling sys.modules["qiskit._accelerate.high_level_synthesis"] = _accelerate.high_level_synthesis sys.modules["qiskit._accelerate.remove_identity_equiv"] = _accelerate.remove_identity_equiv +sys.modules["qiskit._accelerate.circuit_duration"] = _accelerate.circuit_duration from qiskit.exceptions import QiskitError, MissingOptionalLibraryError diff --git a/qiskit/circuit/quantumcircuit.py b/qiskit/circuit/quantumcircuit.py index 012995e5021e..e73769a42bc0 100644 --- a/qiskit/circuit/quantumcircuit.py +++ b/qiskit/circuit/quantumcircuit.py @@ -41,6 +41,7 @@ import numpy as np from qiskit._accelerate.circuit import CircuitData from qiskit._accelerate.circuit import StandardGate +from qiskit._accelerate.circuit_duration import compute_estimated_duration from qiskit.exceptions import QiskitError from qiskit.utils.multiprocessing import is_main_process from qiskit.circuit.instruction import Instruction @@ -157,6 +158,8 @@ class QuantumCircuit: :attr:`data` List of individual :class:`CircuitInstruction`\\ s that make up the circuit. :attr:`duration` Total duration of the circuit, added by scheduling transpiler passes. + This attribute is deprecated and :meth:`.estimate_duration` should + be used instead. :attr:`layout` Hardware layout and routing information added by the transpiler. :attr:`num_ancillas` The number of ancilla qubits in the circuit. @@ -909,8 +912,9 @@ class QuantumCircuit: If a :class:`QuantumCircuit` has been scheduled as part of a transpilation pipeline, the timing information for individual qubits can be accessed. The whole-circuit timing information is - available through the :attr:`duration`, :attr:`unit` and :attr:`op_start_times` attributes. + available through the :meth:`estimate_duration` method and :attr:`op_start_times` attribute. + .. automethod:: estimate_duration .. automethod:: qubit_duration .. automethod:: qubit_start_time .. automethod:: qubit_stop_time @@ -6919,6 +6923,65 @@ def qubit_stop_time(self, *qubits: Union[Qubit, int]) -> float: else: return 0 # If there are no instructions over bits + def estimate_duration(self, target, unit: str = "s") -> int | float: + """Estimate the duration of a scheduled circuit + + This method computes the estimate of the circuit duration by finding + the longest duration path in the circuit based on the durations + provided by a given target. This method only works for simple circuits + that have no control flow or other classical feed-forward operations. + + Args: + target (Target): The :class:`.Target` instance that contains durations for + the instructions if the target is missing duration data for any of the + instructions in the circuit an :class:`.QiskitError` will be raised. This + should be the same target object used as the target for transpilation. + unit: The unit to return the duration in. This defaults to "s" for seconds + but this can be a supported SI prefix for seconds returns. For example + setting this to "n" will return in unit of nanoseconds. Supported values + of this type are "f", "p", "n", "u", "µ", "m", "k", "M", "G", "T", and + "P". Additionally, a value of "dt" is also accepted to output an integer + in units of "dt". For this to function "dt" must be specified in the + ``target``. + + Returns: + The estimated duration for the execution of a single shot of the circuit in + the specified unit. + + Raises: + QiskitError: If the circuit is not scheduled or contains other + details that prevent computing an estimated duration from + (such as parameterized delay). + """ + from qiskit.converters import circuit_to_dag + + dur = compute_estimated_duration(circuit_to_dag(self), target) + if unit == "s": + return dur + if unit == "dt": + from qiskit.circuit.duration import duration_in_dt # pylint: disable=cyclic-import + + return duration_in_dt(dur, target.dt) + + prefix_dict = { + "f": 1e-15, + "p": 1e-12, + "n": 1e-9, + "u": 1e-6, + "µ": 1e-6, + "m": 1e-3, + "k": 1e3, + "M": 1e6, + "G": 1e9, + "T": 1e12, + "P": 1e15, + } + if unit not in prefix_dict: + raise QiskitError( + f"Specified unit: {unit} is not a valid/supported SI prefix, 's', or 'dt'" + ) + return dur / prefix_dict[unit] + class _OuterCircuitScopeInterface(CircuitScopeInterface): # This is an explicit interface-fulfilling object friend of QuantumCircuit that acts as its diff --git a/releasenotes/notes/add-estimate_duration-method-a35bf8eef4b2f210.yaml b/releasenotes/notes/add-estimate_duration-method-a35bf8eef4b2f210.yaml new file mode 100644 index 000000000000..ae6bff75d89e --- /dev/null +++ b/releasenotes/notes/add-estimate_duration-method-a35bf8eef4b2f210.yaml @@ -0,0 +1,7 @@ +--- +features_circuits: + - | + Added a new method, :meth:`.QuantumCircuit.estimate_duration`, to compute + the estimated duration of a scheduled circuit output from the :mod:`.transpiler`. + This should be used if you need an estimate of the full circuit duration instead + of the deprecated :attr:`.QuantumCircuit.duration` attribute. diff --git a/test/python/circuit/test_scheduled_circuit.py b/test/python/circuit/test_scheduled_circuit.py index c09265d51b8b..0793139429ef 100644 --- a/test/python/circuit/test_scheduled_circuit.py +++ b/test/python/circuit/test_scheduled_circuit.py @@ -469,6 +469,126 @@ def test_convert_duration_to_dt(self): ref_unit, ) + @data("s", "dt", "f", "p", "n", "u", "µ", "m", "k", "M", "G", "T", "P") + def test_estimate_duration(self, unit): + """Test the circuit duration is computed correctly.""" + backend = GenericBackendV2(num_qubits=3, seed=42) + + circ = QuantumCircuit(2) + circ.cx(0, 1) + circ.measure_all() + + circuit_dt = transpile(circ, backend, scheduling_method="asap") + duration = circuit_dt.estimate_duration(backend.target, unit=unit) + expected_in_sec = 1.815516e-06 + expected_val = { + "s": expected_in_sec, + "dt": int(expected_in_sec / backend.target.dt), + "f": expected_in_sec / 1e-15, + "p": expected_in_sec / 1e-12, + "n": expected_in_sec / 1e-9, + "u": expected_in_sec / 1e-6, + "µ": expected_in_sec / 1e-6, + "m": expected_in_sec / 1e-3, + "k": expected_in_sec / 1e3, + "M": expected_in_sec / 1e6, + "G": expected_in_sec / 1e9, + "T": expected_in_sec / 1e12, + "P": expected_in_sec / 1e15, + } + self.assertEqual(duration, expected_val[unit]) + + @data("s", "dt", "f", "p", "n", "u", "µ", "m", "k", "M", "G", "T", "P") + def test_estimate_duration_with_long_delay(self, unit): + """Test the circuit duration is computed correctly.""" + backend = GenericBackendV2(num_qubits=3, seed=42) + + circ = QuantumCircuit(3) + circ.cx(0, 1) + circ.measure_all() + circ.delay(1e15, 2) + + circuit_dt = transpile(circ, backend, scheduling_method="asap") + duration = circuit_dt.estimate_duration(backend.target, unit=unit) + expected_in_sec = 222000.00000139928 + expected_val = { + "s": expected_in_sec, + "dt": int(expected_in_sec / backend.target.dt), + "f": expected_in_sec / 1e-15, + "p": expected_in_sec / 1e-12, + "n": expected_in_sec / 1e-9, + "u": expected_in_sec / 1e-6, + "µ": expected_in_sec / 1e-6, + "m": expected_in_sec / 1e-3, + "k": expected_in_sec / 1e3, + "M": expected_in_sec / 1e6, + "G": expected_in_sec / 1e9, + "T": expected_in_sec / 1e12, + "P": expected_in_sec / 1e15, + } + self.assertEqual(duration, expected_val[unit]) + + def test_estimate_duration_invalid_unit(self): + backend = GenericBackendV2(num_qubits=3, seed=42) + + circ = QuantumCircuit(2) + circ.cx(0, 1) + circ.measure_all() + + circuit_dt = transpile(circ, backend, scheduling_method="asap") + with self.assertRaises(QiskitError): + circuit_dt.estimate_duration(backend.target, unit="jiffy") + + def test_delay_circ(self): + backend = GenericBackendV2(num_qubits=3, seed=42) + + circ = QuantumCircuit(2) + circ.delay(100, 0, unit="dt") + + circuit_dt = transpile(circ, backend, scheduling_method="asap") + res = circuit_dt.estimate_duration(backend.target, unit="dt") + self.assertIsInstance(res, int) + self.assertEqual(res, 100) + + def test_estimate_duration_control_flow(self): + backend = GenericBackendV2(num_qubits=3, seed=42, control_flow=True) + + circ = QuantumCircuit(2) + circ.cx(0, 1) + circ.measure_all() + with circ.if_test((0, True)): + circ.x(0) + with self.assertRaises(QiskitError): + circ.estimate_duration(backend.target) + + def test_estimate_duration_with_var(self): + backend = GenericBackendV2(num_qubits=3, seed=42, control_flow=True) + + circ = QuantumCircuit(2) + circ.cx(0, 1) + circ.measure_all() + circ.add_var("a", False) + with self.assertRaises(QiskitError): + circ.estimate_duration(backend.target) + + def test_estimate_duration_parameterized_delay(self): + backend = GenericBackendV2(num_qubits=3, seed=42, control_flow=True) + + circ = QuantumCircuit(2) + circ.cx(0, 1) + circ.measure_all() + circ.delay(Parameter("t"), 0) + with self.assertRaises(QiskitError): + circ.estimate_duration(backend.target) + + def test_estimate_duration_dt_delay_no_dt(self): + backend = GenericBackendV2(num_qubits=3, seed=42) + circ = QuantumCircuit(1) + circ.delay(100, 0) + backend.target.dt = None + with self.assertRaises(QiskitError): + circ.estimate_duration(backend.target) + def test_change_dt_in_transpile(self): qc = QuantumCircuit(1, 1) qc.x(0) From bd30693331bf308c3a06aa7a18e326ceed0e6fa4 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Wed, 19 Feb 2025 05:43:52 -0500 Subject: [PATCH 46/48] Remove deprecated legacy scheduling passes (#13876) This commit removes the deprecated legacy scheduling passes. These passes were replaced by the new scheduling passes a long time ago and finally marked as deprecated in 1.1.0. This commit follows through on the promise of that deprecation and removes the passes marked as deprecated. --- qiskit/transpiler/passes/__init__.py | 8 - .../transpiler/passes/scheduling/__init__.py | 6 - qiskit/transpiler/passes/scheduling/alap.py | 153 ---- .../passes/scheduling/alignments/__init__.py | 1 - .../scheduling/alignments/align_measures.py | 255 ------ qiskit/transpiler/passes/scheduling/asap.py | 175 ---- .../passes/scheduling/base_scheduler.py | 310 ------- .../passes/scheduling/dynamical_decoupling.py | 313 ------- ...cy-scheduling-passes-ee1d593c41fe95c6.yaml | 9 + .../transpiler/legacy_scheduling/__init__.py | 13 - .../test_instruction_alignments.py | 327 ------- .../legacy_scheduling/test_scheduling_pass.py | 864 ------------------ 12 files changed, 9 insertions(+), 2425 deletions(-) delete mode 100644 qiskit/transpiler/passes/scheduling/alap.py delete mode 100644 qiskit/transpiler/passes/scheduling/alignments/align_measures.py delete mode 100644 qiskit/transpiler/passes/scheduling/asap.py delete mode 100644 qiskit/transpiler/passes/scheduling/base_scheduler.py delete mode 100644 qiskit/transpiler/passes/scheduling/dynamical_decoupling.py create mode 100644 releasenotes/notes/drop-legacy-scheduling-passes-ee1d593c41fe95c6.yaml delete mode 100644 test/python/transpiler/legacy_scheduling/__init__.py delete mode 100644 test/python/transpiler/legacy_scheduling/test_instruction_alignments.py delete mode 100644 test/python/transpiler/legacy_scheduling/test_scheduling_pass.py diff --git a/qiskit/transpiler/passes/__init__.py b/qiskit/transpiler/passes/__init__.py index 1fd8454159a3..ae4bb218ef4a 100644 --- a/qiskit/transpiler/passes/__init__.py +++ b/qiskit/transpiler/passes/__init__.py @@ -115,10 +115,6 @@ ConstrainedReschedule InstructionDurationCheck SetIOLatency - ALAPSchedule - ASAPSchedule - DynamicalDecoupling - AlignMeasures Circuit Analysis ================ @@ -273,10 +269,6 @@ from .scheduling import ConstrainedReschedule from .scheduling import InstructionDurationCheck from .scheduling import SetIOLatency -from .scheduling import ALAPSchedule -from .scheduling import ASAPSchedule -from .scheduling import DynamicalDecoupling -from .scheduling import AlignMeasures # additional utility passes from .utils import CheckMap diff --git a/qiskit/transpiler/passes/scheduling/__init__.py b/qiskit/transpiler/passes/scheduling/__init__.py index 2eeb29661d5e..4b02b471b07e 100644 --- a/qiskit/transpiler/passes/scheduling/__init__.py +++ b/qiskit/transpiler/passes/scheduling/__init__.py @@ -12,9 +12,6 @@ """Module containing circuit scheduling passes.""" -from .alap import ALAPSchedule -from .asap import ASAPSchedule -from .dynamical_decoupling import DynamicalDecoupling from .scheduling import ALAPScheduleAnalysis, ASAPScheduleAnalysis, SetIOLatency from .time_unit_conversion import TimeUnitConversion from .padding import PadDelay, PadDynamicalDecoupling @@ -22,6 +19,3 @@ # For backward compatibility from . import alignments as instruction_alignments - -# TODO Deprecated pass. Will be removed after deprecation period. -from .alignments import AlignMeasures diff --git a/qiskit/transpiler/passes/scheduling/alap.py b/qiskit/transpiler/passes/scheduling/alap.py deleted file mode 100644 index cdbdd4654f3c..000000000000 --- a/qiskit/transpiler/passes/scheduling/alap.py +++ /dev/null @@ -1,153 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2020. -# -# 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. - -"""ALAP Scheduling.""" - -from qiskit.circuit import Delay, Qubit, Measure -from qiskit.dagcircuit import DAGCircuit -from qiskit.transpiler.exceptions import TranspilerError -from qiskit.transpiler.passes.scheduling.base_scheduler import BaseSchedulerTransform -from qiskit.utils.deprecation import deprecate_func - - -class ALAPSchedule(BaseSchedulerTransform): - """ALAP Scheduling pass, which schedules the **stop** time of instructions as late as possible. - - See :class:`~qiskit.transpiler.passes.scheduling.base_scheduler.BaseSchedulerTransform` for the - detailed behavior of the control flow operation, i.e. ``c_if``. - """ - - @deprecate_func( - additional_msg=( - "Instead, use :class:`~.ALAPScheduleAnalysis`, which is an " - "analysis pass that requires a padding pass to later modify the circuit." - ), - since="1.1.0", - ) - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - - def run(self, dag): - """Run the ALAPSchedule 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("ALAP schedule runs on physical circuits only") - - time_unit = self.property_set["time_unit"] - 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) - - idle_before = {q: 0 for q in dag.qubits + dag.clbits} - for node in reversed(list(dag.topological_op_nodes())): - op_duration = self._get_node_duration(node, dag) - - # compute t0, t1: instruction interval, note that - # t0: start time of instruction - # t1: end time of instruction - - # since this is alap scheduling, node is scheduled in reversed topological ordering - # and nodes are packed from the very end of the circuit. - # the physical meaning of t0 and t1 is flipped here. - if isinstance(node.op, self.CONDITIONAL_SUPPORTED): - t0q = max(idle_before[q] for q in node.qargs) - if node.op.condition_bits: - # conditional is bit tricky due to conditional_latency - t0c = max(idle_before[c] for c in node.op.condition_bits) - # Assume following case (t0c > t0q): - # - # |t0q - # Q ░░░░░░░░░░░░░▒▒▒ - # C ░░░░░░░░▒▒▒▒▒▒▒▒ - # |t0c - # - # In this case, there is no actual clbit read before gate. - # - # |t0q' = t0c - conditional_latency - # Q ░░░░░░░░▒▒▒░░▒▒▒ - # C ░░░░░░▒▒▒▒▒▒▒▒▒▒ - # |t1c' = t0c + conditional_latency - # - # rather than naively doing - # - # |t1q' = t0c + duration - # Q ░░░░░▒▒▒░░░░░▒▒▒ - # C ░░▒▒░░░░▒▒▒▒▒▒▒▒ - # |t1c' = t0c + duration + conditional_latency - # - t0 = max(t0q, t0c - op_duration) - t1 = t0 + op_duration - for clbit in node.op.condition_bits: - idle_before[clbit] = t1 + self.conditional_latency - 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 ALAP scheduler." - ) - - if isinstance(node.op, Measure): - # clbit time is always right (alap) justified - t0 = max(idle_before[bit] for bit in node.qargs + node.cargs) - t1 = t0 + op_duration - # - # |t1 = t0 + duration - # Q ░░░░░▒▒▒▒▒▒▒▒▒▒▒ - # C ░░░░░░░░░▒▒▒▒▒▒▒ - # |t0 + (duration - clbit_write_latency) - # - for clbit in node.cargs: - idle_before[clbit] = t0 + (op_duration - self.clbit_write_latency) - else: - # It happens to be directives such as barrier - t0 = max(idle_before[bit] for bit in node.qargs + node.cargs) - t1 = t0 + op_duration - - for bit in node.qargs: - delta = t0 - idle_before[bit] - if delta > 0 and self._delay_supported(dag.find_bit(bit).index): - new_dag.apply_operation_front(Delay(delta, time_unit), [bit], [], check=False) - idle_before[bit] = t1 - - new_dag.apply_operation_front(node.op, node.qargs, node.cargs, check=False) - - circuit_duration = max(idle_before.values()) - for bit, before in idle_before.items(): - delta = circuit_duration - before - if not (delta > 0 and isinstance(bit, Qubit)): - continue - if self._delay_supported(dag.find_bit(bit).index): - new_dag.apply_operation_front(Delay(delta, time_unit), [bit], [], check=False) - - new_dag.name = dag.name - new_dag.metadata = dag.metadata - new_dag._calibrations_prop = dag._calibrations_prop - - # set circuit duration and unit to indicate it is scheduled - new_dag.duration = circuit_duration - new_dag.unit = time_unit - - return new_dag diff --git a/qiskit/transpiler/passes/scheduling/alignments/__init__.py b/qiskit/transpiler/passes/scheduling/alignments/__init__.py index 8ecd68eacdbb..507fb95d3486 100644 --- a/qiskit/transpiler/passes/scheduling/alignments/__init__.py +++ b/qiskit/transpiler/passes/scheduling/alignments/__init__.py @@ -77,4 +77,3 @@ from .check_durations import InstructionDurationCheck from .reschedule import ConstrainedReschedule -from .align_measures import AlignMeasures diff --git a/qiskit/transpiler/passes/scheduling/alignments/align_measures.py b/qiskit/transpiler/passes/scheduling/alignments/align_measures.py deleted file mode 100644 index 5327d8b2a5ec..000000000000 --- a/qiskit/transpiler/passes/scheduling/alignments/align_measures.py +++ /dev/null @@ -1,255 +0,0 @@ -# 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. - -"""Align measurement instructions.""" -from __future__ import annotations -import itertools -import warnings -from collections import defaultdict -from collections.abc import Iterable -from typing import Type - -from qiskit.circuit.quantumcircuit import ClbitSpecifier, QubitSpecifier - -from qiskit.circuit.delay import Delay -from qiskit.circuit.measure import Measure -from qiskit.circuit.parameterexpression import ParameterExpression -from qiskit.dagcircuit import DAGCircuit -from qiskit.transpiler.basepasses import TransformationPass -from qiskit.transpiler.exceptions import TranspilerError -from qiskit.utils.deprecation import deprecate_func - - -class AlignMeasures(TransformationPass): - """Measurement alignment. - - This is a control electronics aware optimization pass. - - In many quantum computing architectures gates (instructions) are implemented with - shaped analog stimulus signals. These signals are digitally stored in the - waveform memory of the control electronics and converted into analog voltage signals - by electronic components called digital to analog converters (DAC). - - In a typical hardware implementation of superconducting quantum processors, - a single qubit instruction is implemented by a - microwave signal with the duration of around several tens of ns with a per-sample - time resolution of ~0.1-10ns, as reported by ``backend.configuration().dt``. - In such systems requiring higher DAC bandwidth, control electronics often - defines a `pulse granularity`, in other words a data chunk, to allow the DAC to - perform the signal conversion in parallel to gain the bandwidth. - - Measurement alignment is required if a backend only allows triggering ``measure`` - instructions at a certain multiple value of this pulse granularity. - This value is usually provided by ``backend.configuration().timing_constraints``. - - In Qiskit SDK, the duration of delay can take arbitrary value in units of ``dt``, - thus circuits involving delays may violate the above alignment constraint (i.e. misalignment). - This pass shifts measurement instructions to a new time position to fix the misalignment, - by inserting extra delay right before the measure instructions. - The input of this pass should be scheduled :class:`~qiskit.dagcircuit.DAGCircuit`, - thus one should select one of the scheduling passes - (:class:`~qiskit.transpiler.passes.ALAPSchedule` or - :class:`~qiskit.trasnpiler.passes.ASAPSchedule`) before calling this. - - Examples: - We assume executing the following circuit on a backend with ``alignment=16``. - - .. code-block:: text - - ┌───┐┌────────────────┐┌─┐ - q_0: ┤ X ├┤ Delay(100[dt]) ├┤M├ - └───┘└────────────────┘└╥┘ - c: 1/════════════════════════╩═ - 0 - - Note that delay of 100 dt induces a misalignment of 4 dt at the measurement. - This pass appends an extra 12 dt time shift to the input circuit. - - .. code-block:: text - - ┌───┐┌────────────────┐┌─┐ - q_0: ┤ X ├┤ Delay(112[dt]) ├┤M├ - └───┘└────────────────┘└╥┘ - c: 1/════════════════════════╩═ - 0 - - This pass always inserts a positive delay before measurements - rather than reducing other delays. - - Notes: - The Backend may allow users to execute circuits violating the alignment constraint. - However, it may return meaningless measurement data mainly due to the phase error. - """ - - @deprecate_func( - additional_msg=( - "Instead, use :class:`~.ConstrainedReschedule`, which performs the same function " - "but also supports aligning to additional timing constraints." - ), - since="1.1.0", - ) - def __init__(self, alignment: int = 1): - """Create new pass. - - Args: - alignment: Integer number representing the minimum time resolution to - trigger measure instruction in units of ``dt``. This value depends on - the control electronics of your quantum processor. - """ - super().__init__() - self.alignment = alignment - - def run(self, dag: DAGCircuit): - """Run the measurement alignment pass on `dag`. - - Args: - dag (DAGCircuit): DAG to be checked. - - Returns: - DAGCircuit: DAG with consistent timing and op nodes annotated with duration. - - Raises: - TranspilerError: If circuit is not scheduled. - """ - time_unit = self.property_set["time_unit"] - - if not _check_alignment_required(dag, self.alignment, Measure): - # return input as-is to avoid unnecessary scheduling. - # because following procedure regenerate new DAGCircuit, - # we should avoid continuing if not necessary from performance viewpoint. - return dag - - # if circuit is not yet scheduled, schedule with ALAP method - if dag.duration is None: - raise TranspilerError( - f"This circuit {dag.name} may involve a delay instruction violating the " - "pulse controller alignment. To adjust instructions to " - "right timing, you should call one of scheduling passes first. " - "This is usually done by calling transpiler with scheduling_method='alap'." - ) - - # the following lines are basically copied from ASAPSchedule pass - # - # * some validations for non-scheduled nodes are dropped, since we assume scheduled input - # * pad_with_delay is called only with non-delay node to avoid consecutive delay - new_dag = dag.copy_empty_like() - - qubit_time_available: dict[QubitSpecifier, int] = defaultdict(int) # to track op start time - qubit_stop_times: dict[QubitSpecifier, int] = defaultdict( - int - ) # to track delay start time for padding - clbit_readable: dict[ClbitSpecifier, int] = defaultdict(int) - clbit_writeable: dict[ClbitSpecifier, int] = defaultdict(int) - - def pad_with_delays(qubits: Iterable[QubitSpecifier], until, unit) -> None: - """Pad idle time-slots in ``qubits`` with delays in ``unit`` until ``until``.""" - for q in qubits: - if qubit_stop_times[q] < until: - idle_duration = until - qubit_stop_times[q] - new_dag.apply_operation_back(Delay(idle_duration, unit), (q,), check=False) - - for node in dag.topological_op_nodes(): - # choose appropriate clbit available time depending on op - clbit_time_available = ( - clbit_writeable if isinstance(node.op, Measure) else clbit_readable - ) - # correction to change clbit start time to qubit start time - delta = node.op.duration if isinstance(node.op, Measure) else 0 - start_time = max( - itertools.chain( - (qubit_time_available[q] for q in node.qargs), - ( - clbit_time_available[c] - delta - for c in node.cargs + tuple(node.op.condition_bits) - ), - ) - ) - - if isinstance(node.op, Measure): - if start_time % self.alignment != 0: - start_time = ((start_time // self.alignment) + 1) * self.alignment - - if not isinstance(node.op, Delay): # exclude delays for combining consecutive delays - pad_with_delays(node.qargs, until=start_time, unit=time_unit) - new_dag.apply_operation_back(node.op, node.qargs, node.cargs, check=False) - - stop_time = start_time + node.op.duration - # update time table - for q in node.qargs: - qubit_time_available[q] = stop_time - if not isinstance(node.op, Delay): - qubit_stop_times[q] = stop_time - for c in node.cargs: # measure - clbit_writeable[c] = clbit_readable[c] = stop_time - for c in node.op.condition_bits: # conditional op - clbit_writeable[c] = max(start_time, clbit_writeable[c]) - - working_qubits = qubit_time_available.keys() - circuit_duration = max(qubit_time_available[q] for q in working_qubits) - pad_with_delays(new_dag.qubits, until=circuit_duration, unit=time_unit) - - new_dag.name = dag.name - new_dag.metadata = dag.metadata - - # set circuit duration and unit to indicate it is scheduled - new_dag.duration = circuit_duration - new_dag.unit = time_unit - - return new_dag - - -def _check_alignment_required( - dag: DAGCircuit, - alignment: int, - instructions: Type | list[Type], -) -> bool: - """Check DAG nodes and return a boolean representing if instruction scheduling is necessary. - - Args: - dag: DAG circuit to check. - alignment: Instruction alignment condition. - instructions: Target instructions. - - Returns: - If instruction scheduling is necessary. - """ - if not isinstance(instructions, list): - instructions = [instructions] - - if alignment == 1: - # disable alignment if arbitrary t0 value can be used - return False - - if all(len(dag.op_nodes(inst)) == 0 for inst in instructions): - # disable alignment if target instruction is not involved - return False - - # check delay durations - for delay_node in dag.op_nodes(Delay): - duration = delay_node.op.duration - if isinstance(duration, ParameterExpression): - # duration is parametrized: - # raise user warning if backend alignment is not 1. - warnings.warn( - f"Parametrized delay with {repr(duration)} is found in circuit {dag.name}. " - f"This backend requires alignment={alignment}. " - "Please make sure all assigned values are multiple values of the alignment.", - UserWarning, - ) - else: - # duration is bound: - # check duration and trigger alignment if it violates constraint - if duration % alignment != 0: - return True - - # disable alignment if all delays are multiple values of the alignment - return False diff --git a/qiskit/transpiler/passes/scheduling/asap.py b/qiskit/transpiler/passes/scheduling/asap.py deleted file mode 100644 index 13ff58b1b0f0..000000000000 --- a/qiskit/transpiler/passes/scheduling/asap.py +++ /dev/null @@ -1,175 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2020. -# -# 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. - -"""ASAP Scheduling.""" - -from qiskit.circuit import Delay, Qubit, Measure -from qiskit.dagcircuit import DAGCircuit -from qiskit.transpiler.exceptions import TranspilerError -from qiskit.transpiler.passes.scheduling.base_scheduler import BaseSchedulerTransform -from qiskit.utils.deprecation import deprecate_func - - -class ASAPSchedule(BaseSchedulerTransform): - """ASAP Scheduling pass, which schedules the start time of instructions as early as possible.. - - See :class:`~qiskit.transpiler.passes.scheduling.base_scheduler.BaseSchedulerTransform` for the - detailed behavior of the control flow operation, i.e. ``c_if``. - - .. note:: - - This base class has been superseded by :class:`~.ASAPScheduleAnalysis` and - the new scheduling workflow. It will be deprecated and subsequently - removed in a future release. - """ - - @deprecate_func( - additional_msg=( - "Instead, use :class:`~.ASAPScheduleAnalysis`, which is an " - "analysis pass that requires a padding pass to later modify the circuit." - ), - since="1.1.0", - ) - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - - 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") - - time_unit = self.property_set["time_unit"] - - 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) - - idle_after = {q: 0 for q in dag.qubits + dag.clbits} - for node in dag.topological_op_nodes(): - op_duration = self._get_node_duration(node, 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 - self.conditional_latency, t0c) - t1c = t0c + self.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 - self.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 - - # Add delay to qubit wire - for bit in node.qargs: - delta = t0 - idle_after[bit] - if ( - delta > 0 - and isinstance(bit, Qubit) - and self._delay_supported(dag.find_bit(bit).index) - ): - new_dag.apply_operation_back(Delay(delta, time_unit), [bit], []) - idle_after[bit] = t1 - - new_dag.apply_operation_back(node.op, node.qargs, node.cargs) - - circuit_duration = max(idle_after.values()) - for bit, after in idle_after.items(): - delta = circuit_duration - after - if not (delta > 0 and isinstance(bit, Qubit)): - continue - if self._delay_supported(dag.find_bit(bit).index): - new_dag.apply_operation_back(Delay(delta, time_unit), [bit], []) - - new_dag.name = dag.name - new_dag.metadata = dag.metadata - new_dag._calibrations_prop = dag._calibrations_prop - - # set circuit duration and unit to indicate it is scheduled - new_dag.duration = circuit_duration - new_dag.unit = time_unit - return new_dag diff --git a/qiskit/transpiler/passes/scheduling/base_scheduler.py b/qiskit/transpiler/passes/scheduling/base_scheduler.py deleted file mode 100644 index c380c9f8c199..000000000000 --- a/qiskit/transpiler/passes/scheduling/base_scheduler.py +++ /dev/null @@ -1,310 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2020. -# -# 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. - -"""Base circuit scheduling pass.""" -import warnings - -from qiskit.circuit import Delay, Gate, Measure, Reset -from qiskit.circuit.parameterexpression import ParameterExpression -from qiskit.dagcircuit import DAGOpNode, DAGCircuit, DAGOutNode -from qiskit.transpiler.basepasses import TransformationPass -from qiskit.transpiler.exceptions import TranspilerError -from qiskit.transpiler.instruction_durations import InstructionDurations -from qiskit.transpiler.passes.scheduling.time_unit_conversion import TimeUnitConversion -from qiskit.transpiler.target import Target - - -class BaseSchedulerTransform(TransformationPass): - """Base scheduler pass. - - .. warning:: - - This base class is not part of the public interface for this module - it should not be used to develop new scheduling passes as the passes - which are using this are pending a future deprecation and subsequent - removal. If you are developing new scheduling passes look at the - ``BaseScheduler`` class instead which is used in the new scheduling - pass workflow. - - Policy of topological node ordering in scheduling - - The DAG representation of ``QuantumCircuit`` respects the node ordering also in the - classical register wires, though theoretically two conditional instructions - conditioned on the same register are commute, i.e. read-access to the - classical register doesn't change its state. - - .. code-block:: text - - qc = QuantumCircuit(2, 1) - qc.delay(100, 0) - qc.x(0).c_if(0, True) - qc.x(1).c_if(0, True) - - The scheduler SHOULD comply with above topological ordering policy of the DAG circuit. - Accordingly, the `asap`-scheduled circuit will become - - .. code-block:: text - - ┌────────────────┐ ┌───┐ - q_0: ┤ Delay(100[dt]) ├───┤ X ├────────────── - ├────────────────┤ └─╥─┘ ┌───┐ - q_1: ┤ Delay(100[dt]) ├─────╫────────┤ X ├─── - └────────────────┘ ║ └─╥─┘ - ┌────╨────┐┌────╨────┐ - c: 1/══════════════════╡ c_0=0x1 ╞╡ c_0=0x1 ╞ - └─────────┘└─────────┘ - - Note that this scheduling might be inefficient in some cases, - because the second conditional operation can start without waiting the delay of 100 dt. - However, such optimization should be done by another pass, - otherwise scheduling may break topological ordering of the original circuit. - - Realistic control flow scheduling respecting for microarchitecture - - In the dispersive QND readout scheme, qubit is measured with microwave stimulus to qubit (Q) - followed by resonator ring-down (depopulation). This microwave signal is recorded - in the buffer memory (B) with hardware kernel, then a discriminated (D) binary value - is moved to the classical register (C). - The sequence from t0 to t1 of the measure instruction interval might be modeled as follows: - - .. code-block:: text - - Q ░▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒░ - B ░░▒▒▒▒▒▒▒▒░░░░░░░░░ - D ░░░░░░░░░░▒▒▒▒▒▒░░░ - C ░░░░░░░░░░░░░░░░▒▒░ - - However, ``QuantumCircuit`` representation is not enough accurate to represent - this model. In the circuit representation, thus ``Qubit`` is occupied by the - stimulus microwave signal during the first half of the interval, - and ``Clbit`` is only occupied at the very end of the interval. - - This precise model may induce weird edge case. - - .. code-block:: text - - ┌───┐ - q_0: ───┤ X ├────── - └─╥─┘ ┌─┐ - q_1: ─────╫─────┤M├ - ┌────╨────┐└╥┘ - c: 1/╡ c_0=0x1 ╞═╩═ - └─────────┘ 0 - - In this example, user may intend to measure the state of ``q_1``, after ``XGate`` is - applied to the ``q_0``. This is correct interpretation from viewpoint of - the topological node ordering, i.e. x gate node come in front of the measure node. - However, according to the measurement model above, the data in the register - is unchanged during the stimulus, thus two nodes are simultaneously operated. - If one `alap`-schedule this circuit, it may return following circuit. - - .. code-block:: text - - ┌────────────────┐ ┌───┐ - q_0: ┤ Delay(500[dt]) ├───┤ X ├────── - └────────────────┘ └─╥─┘ ┌─┐ - q_1: ───────────────────────╫─────┤M├ - ┌────╨────┐└╥┘ - c: 1/══════════════════╡ c_0=0x1 ╞═╩═ - └─────────┘ 0 - - Note that there is no delay on ``q_1`` wire, and the measure instruction immediately - start after t=0, while the conditional gate starts after the delay. - It looks like the topological ordering between the nodes are flipped in the scheduled view. - This behavior can be understood by considering the control flow model described above, - - .. code-block:: text - - : Quantum Circuit, first-measure - 0 ░░░░░░░░░░░░▒▒▒▒▒▒░ - 1 ░▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒░ - - : In wire q0 - Q ░░░░░░░░░░░░░░░▒▒▒░ - C ░░░░░░░░░░░░▒▒░░░░░ - - : In wire q1 - Q ░▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒░ - B ░░▒▒▒▒▒▒▒▒░░░░░░░░░ - D ░░░░░░░░░░▒▒▒▒▒▒░░░ - C ░░░░░░░░░░░░░░░░▒▒░ - - Since there is no qubit register (Q0, Q1) overlap, the node ordering is determined by the - shared classical register C. As you can see, the execution order is still - preserved on C, i.e. read C then apply ``XGate``, finally store the measured outcome in C. - Because ``DAGOpNode`` cannot define different durations for associated registers, - the time ordering of two nodes is inverted anyways. - - This behavior can be controlled by ``clbit_write_latency`` and ``conditional_latency``. - The former parameter determines the delay of the register write-access from - the beginning of the measure instruction t0, and another parameter determines - the delay of conditional gate operation from t0 which comes from the register read-access. - - Since we usually expect topological ordering and time ordering are identical - without the context of microarchitecture, both latencies are set to zero by default. - In this case, ``Measure`` instruction immediately locks the register C. - Under this configuration, the `alap`-scheduled circuit of above example may become - - .. code-block:: text - - ┌───┐ - q_0: ───┤ X ├────── - └─╥─┘ ┌─┐ - q_1: ─────╫─────┤M├ - ┌────╨────┐└╥┘ - c: 1/╡ c_0=0x1 ╞═╩═ - └─────────┘ 0 - - If the backend microarchitecture supports smart scheduling of the control flow, i.e. - it may separately schedule qubit and classical register, - insertion of the delay yields unnecessary longer total execution time. - - .. code-block:: text - - : Quantum Circuit, first-xgate - 0 ░▒▒▒░░░░░░░░░░░░░░░ - 1 ░▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒░ - - : In wire q0 - Q ░▒▒▒░░░░░░░░░░░░░░░ - C ░░░░░░░░░░░░░░░░░░░ (zero latency) - - : In wire q1 - Q ░▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒░ - C ░▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒░ (zero latency, scheduled after C0 read-access) - - However this result is much more intuitive in the topological ordering view. - If finite conditional latency is provided, for example, 30 dt, the circuit - is scheduled as follows. - - .. code-block:: text - - ┌───────────────┐ ┌───┐ - q_0: ┤ Delay(30[dt]) ├───┤ X ├────── - ├───────────────┤ └─╥─┘ ┌─┐ - q_1: ┤ Delay(30[dt]) ├─────╫─────┤M├ - └───────────────┘┌────╨────┐└╥┘ - c: 1/═════════════════╡ c_0=0x1 ╞═╩═ - └─────────┘ 0 - - with the timing model: - - .. code-block:: text - - : Quantum Circuit, first-xgate - 0 ░░▒▒▒░░░░░░░░░░░░░░░ - 1 ░░▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒░ - - : In wire q0 - Q ░░▒▒▒░░░░░░░░░░░░░░░ - C ░▒░░░░░░░░░░░░░░░░░░ (30dt latency) - - : In wire q1 - Q ░░▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒░ - C ░░▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒░ - - See https://arxiv.org/abs/2102.01682 for more details. - - """ - - CONDITIONAL_SUPPORTED = (Gate, Delay) - - def __init__( - self, - durations: InstructionDurations = None, - clbit_write_latency: int = 0, - conditional_latency: int = 0, - target: Target = None, - ): - """Scheduler initializer. - - Args: - durations: Durations of instructions to be used in scheduling - clbit_write_latency: A control flow constraints. Because standard superconducting - quantum processor implement dispersive QND readout, the actual data transfer - to the clbit happens after the round-trip stimulus signal is buffered - and discriminated into quantum state. - The interval ``[t0, t0 + clbit_write_latency]`` is regarded as idle time - for clbits associated with the measure instruction. - This defaults to 0 dt which is identical to Qiskit Pulse scheduler. - conditional_latency: A control flow constraints. This value represents - a latency of reading a classical register for the conditional operation. - The gate operation occurs after this latency. This appears as a delay - in front of the DAGOpNode of the gate. - This defaults to 0 dt. - target: The :class:`~.Target` representing the target backend, if both - ``durations`` and this are specified then this argument will take - precedence and ``durations`` will be ignored. - """ - super().__init__() - self.durations = durations - # Ensure op node durations are attached and in consistent unit - if target is not None: - self.durations = target.durations() - self.requires.append(TimeUnitConversion(self.durations)) - - # Control flow constraints. - self.clbit_write_latency = clbit_write_latency - self.conditional_latency = conditional_latency - - self.target = target - - @staticmethod - def _get_node_duration( - node: DAGOpNode, - dag: DAGCircuit, - ) -> int: - """A helper method to get duration from node or calibration.""" - indices = [dag.find_bit(qarg).index for qarg in node.qargs] - - if dag._has_calibration_for(node): - # If node has calibration, this value should be the highest priority - cal_key = tuple(indices), tuple(float(p) for p in node.op.params) - duration = dag._calibrations_prop[node.op.name][cal_key].duration - else: - duration = node.op.duration - - if isinstance(node.op, Reset): - warnings.warn( - "Qiskit scheduler assumes Reset works similarly to Measure instruction. " - "Actual behavior depends on the control system of your quantum backend. " - "Your backend may provide a plugin scheduler pass." - ) - elif isinstance(node.op, Measure): - is_mid_circuit = not any( - isinstance(x, DAGOutNode) for x in dag.quantum_successors(node) - ) - if is_mid_circuit: - warnings.warn( - "Qiskit scheduler assumes mid-circuit measurement works as a standard instruction. " - "Actual backend may apply custom scheduling. " - "Your backend may provide a plugin scheduler pass." - ) - - if isinstance(duration, ParameterExpression): - raise TranspilerError( - f"Parameterized duration ({duration}) " - f"of {node.op.name} on qubits {indices} is not bounded." - ) - if duration is None: - raise TranspilerError(f"Duration of {node.op.name} on qubits {indices} is not found.") - - return duration - - def _delay_supported(self, qarg: int) -> bool: - """Delay operation is supported on the qubit (qarg) or not.""" - if self.target is None or self.target.instruction_supported("delay", qargs=(qarg,)): - return True - return False - - def run(self, dag: DAGCircuit): - raise NotImplementedError diff --git a/qiskit/transpiler/passes/scheduling/dynamical_decoupling.py b/qiskit/transpiler/passes/scheduling/dynamical_decoupling.py deleted file mode 100644 index 08371c488422..000000000000 --- a/qiskit/transpiler/passes/scheduling/dynamical_decoupling.py +++ /dev/null @@ -1,313 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2021, 2024. -# -# 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.""" - -import itertools -import warnings - -import numpy as np -from qiskit.circuit import Gate, Delay, Reset -from qiskit.circuit.library.standard_gates import IGate, UGate, U3Gate -from qiskit.dagcircuit import DAGOpNode, DAGInNode -from qiskit.quantum_info.operators.predicates import matrix_equal -from qiskit.synthesis.one_qubit import OneQubitEulerDecomposer -from qiskit.transpiler.basepasses import TransformationPass -from qiskit.transpiler.exceptions import TranspilerError -from qiskit.transpiler.instruction_durations import InstructionDurations -from qiskit.transpiler.passes.optimization import Optimize1qGates -from qiskit.utils.deprecation import deprecate_func - - -class DynamicalDecoupling(TransformationPass): - """Dynamical decoupling insertion pass. - - 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). - - .. plot:: - :alt: Output from the previous code. - :include-source: - - import numpy as np - from qiskit.circuit import QuantumCircuit - from qiskit.circuit.library import XGate - from qiskit.transpiler import PassManager, InstructionDurations - from qiskit.transpiler.passes import ALAPSchedule, DynamicalDecoupling - from qiskit.visualization import timeline_drawer - - # Because the legacy passes do not propagate the scheduling information correctly, it is - # necessary to run a no-op "re-schedule" before the output circuits can be drawn. - def draw(circuit): - from qiskit import transpile - - scheduled = transpile( - circuit, - optimization_level=0, - instruction_durations=InstructionDurations(), - scheduling_method="alap", - ) - return timeline_drawer(scheduled) - - 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)] - ) - # balanced X-X sequence on all qubits - dd_sequence = [XGate(), XGate()] - pm = PassManager([ALAPSchedule(durations), - DynamicalDecoupling(durations, dd_sequence)]) - circ_dd = pm.run(circ) - draw(circ_dd) - - # 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( - [ - ALAPSchedule(durations), - DynamicalDecoupling(durations, dd_sequence, qubits=[0], spacing=spacing), - ] - ) - circ_dd = pm.run(circ) - draw(circ_dd) - """ - - @deprecate_func( - additional_msg=( - "Instead, use :class:`~.PadDynamicalDecoupling`, which performs the same " - "function but requires scheduling and alignment analysis passes to run prior to it." - ), - since="1.1.0", - ) - def __init__( - self, durations, dd_sequence, qubits=None, spacing=None, skip_reset_qubits=True, target=None - ): - """Dynamical decoupling initializer. - - Args: - durations (InstructionDurations): Durations of instructions to be - used in scheduling. - dd_sequence (list[Gate]): sequence of gates to apply in idle spots. - qubits (list[int]): physical qubits on which to apply DD. - If None, all qubits will undergo DD (when possible). - spacing (list[float]): 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 (bool): 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). - target (Target): The :class:`~.Target` representing the target backend, if both - ``durations`` and this are specified then this argument will take - precedence and ``durations`` will be ignored. - """ - super().__init__() - self._durations = durations - self._dd_sequence = dd_sequence - self._qubits = qubits - self._spacing = spacing - self._skip_reset_qubits = skip_reset_qubits - self._target = target - if target is not None: - self._durations = target.durations() - for gate in dd_sequence: - if gate.name not in target.operation_names: - raise TranspilerError( - f"{gate.name} in dd_sequence is not supported in the target" - ) - - def run(self, dag): - """Run the DynamicalDecoupling pass on dag. - - Args: - dag (DAGCircuit): a scheduled DAG. - - Returns: - DAGCircuit: equivalent circuit with delays interrupted by DD, - where possible. - - Raises: - TranspilerError: if the circuit is not mapped on physical qubits. - """ - if len(dag.qregs) != 1 or dag.qregs.get("q", None) is None: - raise TranspilerError("DD runs on physical circuits only.") - - if dag.duration is None: - raise TranspilerError("DD runs after circuit is scheduled.") - - durations = self._update_inst_durations(dag) - - num_pulses = len(self._dd_sequence) - sequence_gphase = 0 - 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.") - sequence_gphase = np.angle(noop[0][0]) - - if self._qubits is None: - self._qubits = set(range(dag.num_qubits())) - else: - self._qubits = set(self._qubits) - - if self._spacing: - 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." - ) - else: # default to balanced spacing - mid = 1 / num_pulses - end = mid / 2 - self._spacing = [end] + [mid] * (num_pulses - 1) + [end] - - for qarg in list(self._qubits): - for gate in self._dd_sequence: - if not self.__gate_supported(gate, qarg): - self._qubits.discard(qarg) - break - - index_sequence_duration_map = {} - for physical_qubit in self._qubits: - dd_sequence_duration = 0 - for index, gate in enumerate(self._dd_sequence): - gate = gate.to_mutable() - self._dd_sequence[index] = gate - gate.duration = durations.get(gate, physical_qubit) - - dd_sequence_duration += gate.duration - index_sequence_duration_map[physical_qubit] = dd_sequence_duration - - new_dag = dag.copy_empty_like() - - for nd in dag.topological_op_nodes(): - if not isinstance(nd.op, Delay): - new_dag.apply_operation_back(nd.op, nd.qargs, nd.cargs, check=False) - continue - - dag_qubit = nd.qargs[0] - physical_qubit = dag.find_bit(dag_qubit).index - if physical_qubit not in self._qubits: # skip unwanted qubits - new_dag.apply_operation_back(nd.op, nd.qargs, nd.cargs, check=False) - continue - - pred = next(dag.predecessors(nd)) - succ = next(dag.successors(nd)) - if self._skip_reset_qubits: # discount initial delays - if isinstance(pred, DAGInNode) or isinstance(pred.op, Reset): - new_dag.apply_operation_back(nd.op, nd.qargs, nd.cargs, check=False) - continue - - dd_sequence_duration = index_sequence_duration_map[physical_qubit] - slack = nd.op.duration - dd_sequence_duration - if slack <= 0: # dd doesn't fit - new_dag.apply_operation_back(nd.op, nd.qargs, nd.cargs, check=False) - continue - - if num_pulses == 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) - # absorb the inverse into the successor (from left in circuit) - if isinstance(succ, DAGOpNode) and isinstance(succ.op, (UGate, U3Gate)): - theta_r, phi_r, lam_r = succ.op.params - succ.op.params = Optimize1qGates.compose_u3( - theta_r, phi_r, lam_r, theta, phi, lam - ) - sequence_gphase += phase - # absorb the inverse into the predecessor (from right in circuit) - elif isinstance(pred, DAGOpNode) and isinstance(pred.op, (UGate, U3Gate)): - theta_l, phi_l, lam_l = pred.op.params - pred.op.params = Optimize1qGates.compose_u3( - theta, phi, lam, theta_l, phi_l, lam_l - ) - sequence_gphase += phase - # don't do anything if there's no single-qubit gate to absorb the inverse - else: - new_dag.apply_operation_back(nd.op, nd.qargs, nd.cargs, check=False) - continue - - # insert the actual DD sequence - taus = [int(slack * a) for a in self._spacing] - unused_slack = slack - sum(taus) # unused, due to rounding to int multiples of dt - middle_index = int((len(taus) - 1) / 2) # arbitrary: redistribute to middle - taus[middle_index] += unused_slack # now we add up to original delay duration - - for tau, gate in itertools.zip_longest(taus, self._dd_sequence): - if tau > 0: - new_dag.apply_operation_back(Delay(tau), [dag_qubit], check=False) - if gate is not None: - new_dag.apply_operation_back(gate, [dag_qubit], check=False) - - new_dag.global_phase = new_dag.global_phase + sequence_gphase - - return new_dag - - def _update_inst_durations(self, dag): - """Update instruction durations with circuit information. If the dag contains gate - calibrations and no instruction durations were provided through the target or as a - standalone input, the circuit calibration durations will be used. - The priority order for instruction durations is: target > standalone > circuit. - """ - circ_durations = InstructionDurations() - - if dag._calibrations_prop: - cal_durations = [] - with warnings.catch_warnings(): - warnings.simplefilter(action="ignore", category=DeprecationWarning) - # `schedule.duration` emits pulse deprecation warnings which we don't want - # to see here - for gate, gate_cals in dag._calibrations_prop.items(): - for (qubits, parameters), schedule in gate_cals.items(): - cal_durations.append((gate, qubits, parameters, schedule.duration)) - circ_durations.update(cal_durations, circ_durations.dt) - - if self._durations is not None: - circ_durations.update(self._durations, getattr(self._durations, "dt", None)) - - return circ_durations - - def __gate_supported(self, gate: Gate, qarg: int) -> bool: - """A gate is supported on the qubit (qarg) or not.""" - if self._target is None or self._target.instruction_supported(gate.name, qargs=(qarg,)): - return True - return False diff --git a/releasenotes/notes/drop-legacy-scheduling-passes-ee1d593c41fe95c6.yaml b/releasenotes/notes/drop-legacy-scheduling-passes-ee1d593c41fe95c6.yaml new file mode 100644 index 000000000000..b57a6dcc8920 --- /dev/null +++ b/releasenotes/notes/drop-legacy-scheduling-passes-ee1d593c41fe95c6.yaml @@ -0,0 +1,9 @@ +--- +upgrade_transpiler: + - | + The deprecated transpiler passes ``ASAPSchedule``, ``ALAPSchedule``, + ``DynamicalDecoupling``, and ``AlignMeasures`` have been removed. These + passes were marked as deprecated. They have been replaced by the + :class:`.ALAPScheduleAnalysis`, :class:`.ASAPScheduleAnalysis`, + :class:`.PadDynamicalDecoupling`, and :class:`.ConstrainedReschedule` + passes respectively which can be used instead. diff --git a/test/python/transpiler/legacy_scheduling/__init__.py b/test/python/transpiler/legacy_scheduling/__init__.py deleted file mode 100644 index 3ce633ebcc4d..000000000000 --- a/test/python/transpiler/legacy_scheduling/__init__.py +++ /dev/null @@ -1,13 +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. - -"""Qiskit legacy scheduling transpiler pass unit tests.""" diff --git a/test/python/transpiler/legacy_scheduling/test_instruction_alignments.py b/test/python/transpiler/legacy_scheduling/test_instruction_alignments.py deleted file mode 100644 index aee567444cff..000000000000 --- a/test/python/transpiler/legacy_scheduling/test_instruction_alignments.py +++ /dev/null @@ -1,327 +0,0 @@ -# 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. - -"""Testing legacy instruction alignment pass.""" - -from qiskit import QuantumCircuit -from qiskit.transpiler import InstructionDurations -from qiskit.transpiler.passes import ( - AlignMeasures, - ALAPSchedule, - TimeUnitConversion, -) -from test import QiskitTestCase # pylint: disable=wrong-import-order - - -class TestAlignMeasures(QiskitTestCase): - """A test for measurement alignment pass.""" - - def setUp(self): - super().setUp() - instruction_durations = InstructionDurations() - instruction_durations.update( - [ - ("rz", (0,), 0), - ("rz", (1,), 0), - ("x", (0,), 160), - ("x", (1,), 160), - ("sx", (0,), 160), - ("sx", (1,), 160), - ("cx", (0, 1), 800), - ("cx", (1, 0), 800), - ("measure", None, 1600), - ] - ) - self.time_conversion_pass = TimeUnitConversion(inst_durations=instruction_durations) - # reproduce old behavior of 0.20.0 before #7655 - # currently default write latency is 0 - with self.assertWarns(DeprecationWarning): - self.scheduling_pass = ALAPSchedule( - durations=instruction_durations, - clbit_write_latency=1600, - conditional_latency=0, - ) - self.align_measure_pass = AlignMeasures(alignment=16) - - def test_t1_experiment_type(self): - """Test T1 experiment type circuit. - - (input) - - ┌───┐┌────────────────┐┌─┐ - q_0: ┤ X ├┤ Delay(100[dt]) ├┤M├ - └───┘└────────────────┘└╥┘ - c: 1/════════════════════════╩═ - 0 - - (aligned) - - ┌───┐┌────────────────┐┌─┐ - q_0: ┤ X ├┤ Delay(112[dt]) ├┤M├ - └───┘└────────────────┘└╥┘ - c: 1/════════════════════════╩═ - 0 - - This type of experiment slightly changes delay duration of interest. - However the quantization error should be less than alignment * dt. - """ - circuit = QuantumCircuit(1, 1) - circuit.x(0) - circuit.delay(100, 0, unit="dt") - circuit.measure(0, 0) - - timed_circuit = self.time_conversion_pass(circuit) - scheduled_circuit = self.scheduling_pass(timed_circuit, property_set={"time_unit": "dt"}) - aligned_circuit = self.align_measure_pass( - scheduled_circuit, property_set={"time_unit": "dt"} - ) - - ref_circuit = QuantumCircuit(1, 1) - ref_circuit.x(0) - ref_circuit.delay(112, 0, unit="dt") - ref_circuit.measure(0, 0) - - self.assertEqual(aligned_circuit, ref_circuit) - - def test_hanh_echo_experiment_type(self): - """Test Hahn echo experiment type circuit. - - (input) - - ┌────┐┌────────────────┐┌───┐┌────────────────┐┌────┐┌─┐ - q_0: ┤ √X ├┤ Delay(100[dt]) ├┤ X ├┤ Delay(100[dt]) ├┤ √X ├┤M├ - └────┘└────────────────┘└───┘└────────────────┘└────┘└╥┘ - c: 1/══════════════════════════════════════════════════════╩═ - 0 - - (output) - - ┌────┐┌────────────────┐┌───┐┌────────────────┐┌────┐┌──────────────┐┌─┐ - q_0: ┤ √X ├┤ Delay(100[dt]) ├┤ X ├┤ Delay(100[dt]) ├┤ √X ├┤ Delay(8[dt]) ├┤M├ - └────┘└────────────────┘└───┘└────────────────┘└────┘└──────────────┘└╥┘ - c: 1/══════════════════════════════════════════════════════════════════════╩═ - 0 - - This type of experiment doesn't change duration of interest (two in the middle). - However induces slight delay less than alignment * dt before measurement. - This might induce extra amplitude damping error. - """ - circuit = QuantumCircuit(1, 1) - circuit.sx(0) - circuit.delay(100, 0, unit="dt") - circuit.x(0) - circuit.delay(100, 0, unit="dt") - circuit.sx(0) - circuit.measure(0, 0) - - timed_circuit = self.time_conversion_pass(circuit) - scheduled_circuit = self.scheduling_pass(timed_circuit, property_set={"time_unit": "dt"}) - aligned_circuit = self.align_measure_pass( - scheduled_circuit, property_set={"time_unit": "dt"} - ) - - ref_circuit = QuantumCircuit(1, 1) - ref_circuit.sx(0) - ref_circuit.delay(100, 0, unit="dt") - ref_circuit.x(0) - ref_circuit.delay(100, 0, unit="dt") - ref_circuit.sx(0) - ref_circuit.delay(8, 0, unit="dt") - ref_circuit.measure(0, 0) - - self.assertEqual(aligned_circuit, ref_circuit) - - def test_mid_circuit_measure(self): - """Test circuit with mid circuit measurement. - - (input) - - ┌───┐┌────────────────┐┌─┐┌───────────────┐┌───┐┌────────────────┐┌─┐ - q_0: ┤ X ├┤ Delay(100[dt]) ├┤M├┤ Delay(10[dt]) ├┤ X ├┤ Delay(120[dt]) ├┤M├ - └───┘└────────────────┘└╥┘└───────────────┘└───┘└────────────────┘└╥┘ - c: 2/════════════════════════╩══════════════════════════════════════════╩═ - 0 1 - - (output) - - ┌───┐┌────────────────┐┌─┐┌───────────────┐┌───┐┌────────────────┐┌─┐ - q_0: ┤ X ├┤ Delay(112[dt]) ├┤M├┤ Delay(10[dt]) ├┤ X ├┤ Delay(134[dt]) ├┤M├ - └───┘└────────────────┘└╥┘└───────────────┘└───┘└────────────────┘└╥┘ - c: 2/════════════════════════╩══════════════════════════════════════════╩═ - 0 1 - - Extra delay is always added to the existing delay right before the measurement. - Delay after measurement is unchanged. - """ - circuit = QuantumCircuit(1, 2) - circuit.x(0) - circuit.delay(100, 0, unit="dt") - circuit.measure(0, 0) - circuit.delay(10, 0, unit="dt") - circuit.x(0) - circuit.delay(120, 0, unit="dt") - circuit.measure(0, 1) - - timed_circuit = self.time_conversion_pass(circuit) - scheduled_circuit = self.scheduling_pass(timed_circuit, property_set={"time_unit": "dt"}) - aligned_circuit = self.align_measure_pass( - scheduled_circuit, property_set={"time_unit": "dt"} - ) - - ref_circuit = QuantumCircuit(1, 2) - ref_circuit.x(0) - ref_circuit.delay(112, 0, unit="dt") - ref_circuit.measure(0, 0) - ref_circuit.delay(10, 0, unit="dt") - ref_circuit.x(0) - ref_circuit.delay(134, 0, unit="dt") - ref_circuit.measure(0, 1) - - self.assertEqual(aligned_circuit, ref_circuit) - - def test_mid_circuit_multiq_gates(self): - """Test circuit with mid circuit measurement and multi qubit gates. - - (input) - - ┌───┐┌────────────────┐┌─┐ ┌─┐ - q_0: ┤ X ├┤ Delay(100[dt]) ├┤M├──■───────■──┤M├ - └───┘└────────────────┘└╥┘┌─┴─┐┌─┐┌─┴─┐└╥┘ - q_1: ────────────────────────╫─┤ X ├┤M├┤ X ├─╫─ - ║ └───┘└╥┘└───┘ ║ - c: 2/════════════════════════╩═══════╩═══════╩═ - 0 1 0 - - (output) - - ┌───┐ ┌────────────────┐┌─┐ ┌─────────────────┐ ┌─┐» - q_0: ───────┤ X ├───────┤ Delay(112[dt]) ├┤M├──■──┤ Delay(1600[dt]) ├──■──┤M├» - ┌──────┴───┴──────┐└────────────────┘└╥┘┌─┴─┐└───────┬─┬───────┘┌─┴─┐└╥┘» - q_1: ┤ Delay(1872[dt]) ├───────────────────╫─┤ X ├────────┤M├────────┤ X ├─╫─» - └─────────────────┘ ║ └───┘ └╥┘ └───┘ ║ » - c: 2/══════════════════════════════════════╩═══════════════╩═══════════════╩═» - 0 1 0 » - « - «q_0: ─────────────────── - « ┌─────────────────┐ - «q_1: ┤ Delay(1600[dt]) ├ - « └─────────────────┘ - «c: 2/═══════════════════ - « - - Delay for the other channel paired by multi-qubit instruction is also scheduled. - Delay (1872dt) = X (160dt) + Delay (100dt + extra 12dt) + Measure (1600dt). - """ - circuit = QuantumCircuit(2, 2) - circuit.x(0) - circuit.delay(100, 0, unit="dt") - circuit.measure(0, 0) - circuit.cx(0, 1) - circuit.measure(1, 1) - circuit.cx(0, 1) - circuit.measure(0, 0) - - timed_circuit = self.time_conversion_pass(circuit) - scheduled_circuit = self.scheduling_pass(timed_circuit, property_set={"time_unit": "dt"}) - aligned_circuit = self.align_measure_pass( - scheduled_circuit, property_set={"time_unit": "dt"} - ) - - ref_circuit = QuantumCircuit(2, 2) - ref_circuit.x(0) - ref_circuit.delay(112, 0, unit="dt") - ref_circuit.measure(0, 0) - ref_circuit.delay(160 + 112 + 1600, 1, unit="dt") - ref_circuit.cx(0, 1) - ref_circuit.delay(1600, 0, unit="dt") - ref_circuit.measure(1, 1) - ref_circuit.cx(0, 1) - ref_circuit.delay(1600, 1, unit="dt") - ref_circuit.measure(0, 0) - - self.assertEqual(aligned_circuit, ref_circuit) - - def test_alignment_is_not_processed(self): - """Test avoid pass processing if delay is aligned.""" - circuit = QuantumCircuit(2, 2) - circuit.x(0) - circuit.delay(160, 0, unit="dt") - circuit.measure(0, 0) - circuit.cx(0, 1) - circuit.measure(1, 1) - circuit.cx(0, 1) - circuit.measure(0, 0) - - # pre scheduling is not necessary because alignment is skipped - # this is to minimize breaking changes to existing code. - transpiled = self.align_measure_pass(circuit, property_set={"time_unit": "dt"}) - - self.assertEqual(transpiled, circuit) - - def test_circuit_using_clbit(self): - """Test a circuit with instructions using a common clbit. - - (input) - ┌───┐┌────────────────┐┌─┐ - q_0: ┤ X ├┤ Delay(100[dt]) ├┤M├────────────── - └───┘└────────────────┘└╥┘ ┌───┐ - q_1: ────────────────────────╫────┤ X ├────── - ║ └─╥─┘ ┌─┐ - q_2: ────────────────────────╫──────╫─────┤M├ - ║ ┌────╨────┐└╥┘ - c: 1/════════════════════════╩═╡ c_0 = T ╞═╩═ - 0 └─────────┘ 0 - - (aligned) - ┌───┐ ┌────────────────┐┌─┐┌────────────────┐ - q_0: ───────┤ X ├───────┤ Delay(112[dt]) ├┤M├┤ Delay(160[dt]) ├─── - ┌──────┴───┴──────┐└────────────────┘└╥┘└─────┬───┬──────┘ - q_1: ┤ Delay(1872[dt]) ├───────────────────╫───────┤ X ├────────── - └┬────────────────┤ ║ └─╥─┘ ┌─┐ - q_2: ─┤ Delay(432[dt]) ├───────────────────╫─────────╫─────────┤M├ - └────────────────┘ ║ ┌────╨────┐ └╥┘ - c: 1/══════════════════════════════════════╩════╡ c_0 = T ╞═════╩═ - 0 └─────────┘ 0 - - Looking at the q_0, the total schedule length T becomes - 160 (x) + 112 (aligned delay) + 1600 (measure) + 160 (delay) = 2032. - The last delay comes from ALAP scheduling called before the AlignMeasure pass, - which aligns stop times as late as possible, so the start time of x(1).c_if(0) - and the stop time of measure(0, 0) become T - 160. - """ - circuit = QuantumCircuit(3, 1) - circuit.x(0) - circuit.delay(100, 0, unit="dt") - circuit.measure(0, 0) - with self.assertWarns(DeprecationWarning): - circuit.x(1).c_if(0, 1) - circuit.measure(2, 0) - - timed_circuit = self.time_conversion_pass(circuit) - scheduled_circuit = self.scheduling_pass(timed_circuit, property_set={"time_unit": "dt"}) - aligned_circuit = self.align_measure_pass( - scheduled_circuit, property_set={"time_unit": "dt"} - ) - self.assertEqual(aligned_circuit.duration, 2032) - - ref_circuit = QuantumCircuit(3, 1) - ref_circuit.x(0) - ref_circuit.delay(112, 0, unit="dt") - ref_circuit.delay(1872, 1, unit="dt") # 2032 - 160 - ref_circuit.delay(432, 2, unit="dt") # 2032 - 1600 - ref_circuit.measure(0, 0) - with self.assertWarns(DeprecationWarning): - ref_circuit.x(1).c_if(0, 1) - ref_circuit.delay(160, 0, unit="dt") - ref_circuit.measure(2, 0) - - self.assertEqual(aligned_circuit, ref_circuit) diff --git a/test/python/transpiler/legacy_scheduling/test_scheduling_pass.py b/test/python/transpiler/legacy_scheduling/test_scheduling_pass.py deleted file mode 100644 index a13ca2e12de1..000000000000 --- a/test/python/transpiler/legacy_scheduling/test_scheduling_pass.py +++ /dev/null @@ -1,864 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2020. -# -# 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 legacy Scheduling passes""" - -import unittest - -from ddt import ddt, data, unpack - -from qiskit import QuantumCircuit -from qiskit.circuit import Delay, Parameter -from qiskit.circuit.library.standard_gates import XGate, YGate, CXGate -from qiskit.transpiler.exceptions import TranspilerError -from qiskit.transpiler.instruction_durations import InstructionDurations -from qiskit.transpiler.passes import ASAPSchedule, ALAPSchedule, DynamicalDecoupling -from qiskit.transpiler.passmanager import PassManager -from qiskit.transpiler.target import Target, InstructionProperties -from test import QiskitTestCase # pylint: disable=wrong-import-order - - -@ddt -class TestSchedulingPass(QiskitTestCase): - """Tests the Scheduling passes""" - - def test_alap_agree_with_reverse_asap_reverse(self): - """Test if ALAP schedule agrees with doubly-reversed ASAP schedule.""" - qc = QuantumCircuit(2) - qc.h(0) - qc.delay(500, 1) - qc.cx(0, 1) - qc.measure_all() - - durations = InstructionDurations( - [("h", 0, 200), ("cx", [0, 1], 700), ("measure", None, 1000)] - ) - - with self.assertWarns(DeprecationWarning): - pm = PassManager(ALAPSchedule(durations)) - alap_qc = pm.run(qc) - - with self.assertWarns(DeprecationWarning): - pm = PassManager(ASAPSchedule(durations)) - new_qc = pm.run(qc.reverse_ops()) - new_qc = new_qc.reverse_ops() - new_qc.name = new_qc.name - - self.assertEqual(alap_qc, new_qc) - - @data(ALAPSchedule, ASAPSchedule) - def test_classically_controlled_gate_after_measure(self, schedule_pass): - """Test if ALAP/ASAP 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) - with self.assertWarns(DeprecationWarning): - qc.x(1).c_if(0, True) - - durations = InstructionDurations([("x", None, 200), ("measure", None, 1000)]) - with self.assertWarns(DeprecationWarning): - pm = PassManager(schedule_pass(durations)) - scheduled = pm.run(qc) - - expected = QuantumCircuit(2, 1) - expected.measure(0, 0) - expected.delay(1000, 1) # x.c_if starts after measure - with self.assertWarns(DeprecationWarning): - expected.x(1).c_if(0, True) - expected.delay(200, 0) - - self.assertEqual(expected, scheduled) - - @data(ALAPSchedule, ASAPSchedule) - def test_measure_after_measure(self, schedule_pass): - """Test if ALAP/ASAP 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)]) - with self.assertWarns(DeprecationWarning): - pm = PassManager(schedule_pass(durations)) - 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) - - @data(ALAPSchedule, ASAPSchedule) - def test_c_if_on_different_qubits(self, schedule_pass): - """Test if ALAP/ASAP 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) - with self.assertWarns(DeprecationWarning): - qc.x(1).c_if(0, True) - with self.assertWarns(DeprecationWarning): - qc.x(2).c_if(0, True) - - durations = InstructionDurations([("x", None, 200), ("measure", None, 1000)]) - with self.assertWarns(DeprecationWarning): - pm = PassManager(schedule_pass(durations)) - scheduled = pm.run(qc) - - expected = QuantumCircuit(3, 1) - expected.measure(0, 0) - expected.delay(1000, 1) - expected.delay(1000, 2) - with self.assertWarns(DeprecationWarning): - expected.x(1).c_if(0, True) - with self.assertWarns(DeprecationWarning): - expected.x(2).c_if(0, True) - expected.delay(200, 0) - - self.assertEqual(expected, scheduled) - - @data(ALAPSchedule, ASAPSchedule) - def test_shorter_measure_after_measure(self, schedule_pass): - """Test if ALAP/ASAP 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)]) - with self.assertWarns(DeprecationWarning): - pm = PassManager(schedule_pass(durations)) - 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) - - @data(ALAPSchedule, ASAPSchedule) - def test_measure_after_c_if(self, schedule_pass): - """Test if ALAP/ASAP 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) - with self.assertWarns(DeprecationWarning): - qc.x(1).c_if(0, 1) - qc.measure(2, 0) - - durations = InstructionDurations([("x", None, 200), ("measure", None, 1000)]) - with self.assertWarns(DeprecationWarning): - pm = PassManager(schedule_pass(durations)) - scheduled = pm.run(qc) - - expected = QuantumCircuit(3, 1) - expected.delay(1000, 1) - expected.delay(1000, 2) - expected.measure(0, 0) - with self.assertWarns(DeprecationWarning): - 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, ALAP) - ┌────────────────┐┌───┐┌─┐ - q_0: ┤ Delay(200[dt]) ├┤ X ├┤M├ - └─────┬───┬──────┘└┬─┬┘└╥┘ - q_1: ──────┤ X ├────────┤M├──╫─ - └───┘ └╥┘ ║ - c: 2/════════════════════╩═══╩═ - 1 0 - - (expected, ASAP) - ┌───┐┌─┐┌────────────────┐ - 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)] - ) - with self.assertWarns(DeprecationWarning): - pm = PassManager(ALAPSchedule(durations)) - qc_alap = pm.run(qc) - - alap_expected = QuantumCircuit(2, 2) - alap_expected.delay(200, 0) - alap_expected.x(0) - alap_expected.x(1) - alap_expected.measure(0, 0) - alap_expected.measure(1, 1) - - self.assertEqual(qc_alap, alap_expected) - - with self.assertWarns(DeprecationWarning): - pm = PassManager(ASAPSchedule(durations)) - qc_asap = pm.run(qc) - - asap_expected = QuantumCircuit(2, 2) - asap_expected.x(0) - asap_expected.x(1) - asap_expected.measure(0, 0) # immediately start after X gate - asap_expected.measure(1, 1) - asap_expected.delay(200, 0) - - self.assertEqual(qc_asap, asap_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, ALAP) - ┌────────────────┐┌───┐ ░ ┌─┐ - q_0: ┤ Delay(200[dt]) ├┤ X ├─░─┤M├─── - └─────┬───┬──────┘└───┘ ░ └╥┘┌─┐ - q_1: ──────┤ X ├─────────────░──╫─┤M├ - └───┘ ░ ║ └╥┘ - c: 2/═══════════════════════════╩══╩═ - 0 1 - - (expected, ASAP) - ┌───┐┌────────────────┐ ░ ┌─┐ - 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)] - ) - with self.assertWarns(DeprecationWarning): - pm = PassManager(ALAPSchedule(durations)) - qc_alap = pm.run(qc) - - alap_expected = QuantumCircuit(2, 2) - alap_expected.delay(200, 0) - alap_expected.x(0) - alap_expected.x(1) - alap_expected.barrier() - alap_expected.measure(0, 0) - alap_expected.measure(1, 1) - - self.assertEqual(qc_alap, alap_expected) - - with self.assertWarns(DeprecationWarning): - pm = PassManager(ASAPSchedule(durations)) - qc_asap = pm.run(qc) - - asap_expected = QuantumCircuit(2, 2) - asap_expected.x(0) - asap_expected.delay(200, 0) - asap_expected.x(1) - asap_expected.barrier() - asap_expected.measure(0, 0) - asap_expected.measure(1, 1) - - self.assertEqual(qc_asap, asap_expected) - - def test_measure_after_c_if_on_edge_locking(self): - """Test if ALAP/ASAP 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 - - (ASAP 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 - - (ALAP scheduled) - ┌─┐┌────────────────┐ - q_0: ───────────────────┤M├┤ Delay(200[dt]) ├─── - ┌─────────────────┐└╥┘└─────┬───┬──────┘ - q_1: ┤ Delay(1000[dt]) ├─╫───────┤ X ├────────── - └┬────────────────┤ ║ └─╥─┘ ┌─┐ - q_2: ─┤ Delay(200[dt]) ├─╫─────────╫─────────┤M├ - └────────────────┘ ║ ┌────╨────┐ └╥┘ - c: 1/════════════════════╩════╡ c_0=0x1 ╞═════╩═ - 0 └─────────┘ 0 - - """ - qc = QuantumCircuit(3, 1) - qc.measure(0, 0) - with self.assertWarns(DeprecationWarning): - qc.x(1).c_if(0, 1) - qc.measure(2, 0) - - durations = InstructionDurations([("x", None, 200), ("measure", None, 1000)]) - - # lock at the end edge - with self.assertWarns(DeprecationWarning): - actual_asap = PassManager(ASAPSchedule(durations, clbit_write_latency=1000)).run(qc) - actual_alap = PassManager(ALAPSchedule(durations, clbit_write_latency=1000)).run(qc) - - # start times of 2nd measure depends on ASAP/ALAP - expected_asap = QuantumCircuit(3, 1) - expected_asap.measure(0, 0) - expected_asap.delay(1000, 1) - with self.assertWarns(DeprecationWarning): - expected_asap.x(1).c_if(0, 1) - expected_asap.measure(2, 0) - expected_asap.delay(200, 0) - expected_asap.delay(200, 2) - self.assertEqual(expected_asap, actual_asap) - - expected_alap = QuantumCircuit(3, 1) - expected_alap.measure(0, 0) - expected_alap.delay(1000, 1) - with self.assertWarns(DeprecationWarning): - expected_alap.x(1).c_if(0, 1) - expected_alap.delay(200, 2) - expected_alap.measure(2, 0) - expected_alap.delay(200, 0) - self.assertEqual(expected_alap, actual_alap) - - @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) - with self.assertWarns(DeprecationWarning): - qc.x(0).c_if(0, 1) - qc.measure(0, 0) - with self.assertWarns(DeprecationWarning): - qc.x(0).c_if(0, 1) - qc.measure(0, 0) - with self.assertWarns(DeprecationWarning): - qc.x(0).c_if(0, 1) - - durations = InstructionDurations([("x", None, 100), ("measure", None, 1000)]) - with self.assertWarns(DeprecationWarning): - actual_asap = PassManager( - ASAPSchedule(durations, clbit_write_latency=write_lat, conditional_latency=cond_lat) - ).run(qc) - actual_alap = PassManager( - ALAPSchedule(durations, clbit_write_latency=write_lat, conditional_latency=cond_lat) - ).run(qc) - - expected = QuantumCircuit(1, 1) - expected.measure(0, 0) - if cond_lat > 0: - expected.delay(cond_lat, 0) - with self.assertWarns(DeprecationWarning): - expected.x(0).c_if(0, 1) - expected.measure(0, 0) - if cond_lat > 0: - expected.delay(cond_lat, 0) - with self.assertWarns(DeprecationWarning): - expected.x(0).c_if(0, 1) - expected.measure(0, 0) - if cond_lat > 0: - expected.delay(cond_lat, 0) - with self.assertWarns(DeprecationWarning): - expected.x(0).c_if(0, 1) - - self.assertEqual(expected, actual_asap) - self.assertEqual(expected, actual_alap) - - 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 └─────────┘ - - (ASAP scheduled) duration = 2800 dt - ┌────────────────┐┌────────────────┐ ┌───┐ ░ ┌─────────────────┐» - q_0: ┤ Delay(100[dt]) ├┤ Delay(100[dt]) ├───┤ X ├────░─┤ Delay(1400[dt]) ├» - ├────────────────┤└────────────────┘ └─╥─┘ ░ ├─────────────────┤» - q_1: ┤ Delay(300[dt]) ├───────────────────────╫──────░─┤ Delay(1200[dt]) ├» - ├────────────────┤ ║ ░ └───────┬─┬───────┘» - q_2: ┤ Delay(300[dt]) ├───────────────────────╫──────░─────────┤M├────────» - └────────────────┘ ┌────╨────┐ ░ └╥┘ » - c: 1/════════════════════════════════════╡ c_0=0x1 ╞════════════╩═════════» - └─────────┘ 0 » - « ┌───┐ ┌────────────────┐» - «q_0: ────────────────────────────────┤ X ├───┤ Delay(300[dt]) ├» - « ┌───┐ └─╥─┘ └────────────────┘» - «q_1: ───┤ X ├──────────────────────────╫─────────────■─────────» - « └─╥─┘ ┌────────────────┐ ║ ┌─┴─┐ » - «q_2: ─────╫─────┤ Delay(300[dt]) ├─────╫───────────┤ X ├───────» - « ┌────╨────┐└────────────────┘┌────╨────┐ └───┘ » - «c: 1/╡ c_0=0x0 ╞══════════════════╡ c_0=0x0 ╞══════════════════» - « └─────────┘ └─────────┘ » - « ┌───┐ ┌────────────────┐ - «q_0: ──────┤ X ├────────────■─────┤ Delay(700[dt]) ├ - « ┌─────┴───┴──────┐ ┌─┴─┐ ├────────────────┤ - «q_1: ┤ Delay(400[dt]) ├───┤ X ├───┤ Delay(700[dt]) ├ - « ├────────────────┤ └─╥─┘ └──────┬─┬───────┘ - «q_2: ┤ Delay(300[dt]) ├─────╫────────────┤M├──────── - « └────────────────┘┌────╨────┐ └╥┘ - «c: 1/══════════════════╡ c_0=0x0 ╞════════╩═════════ - « └─────────┘ 0 - - (ALAP scheduled) duration = 3100 - ┌────────────────┐┌────────────────┐ ┌───┐ ░ ┌─────────────────┐» - q_0: ┤ Delay(100[dt]) ├┤ Delay(100[dt]) ├───┤ X ├────░─┤ Delay(1400[dt]) ├» - ├────────────────┤└────────────────┘ └─╥─┘ ░ ├─────────────────┤» - q_1: ┤ Delay(300[dt]) ├───────────────────────╫──────░─┤ Delay(1200[dt]) ├» - ├────────────────┤ ║ ░ └───────┬─┬───────┘» - q_2: ┤ Delay(300[dt]) ├───────────────────────╫──────░─────────┤M├────────» - └────────────────┘ ┌────╨────┐ ░ └╥┘ » - c: 1/════════════════════════════════════╡ c_0=0x1 ╞════════════╩═════════» - └─────────┘ 0 » - « ┌───┐ ┌────────────────┐» - «q_0: ────────────────────────────────┤ X ├───┤ Delay(300[dt]) ├» - « ┌───┐ ┌────────────────┐ └─╥─┘ └────────────────┘» - «q_1: ───┤ X ├───┤ Delay(300[dt]) ├─────╫─────────────■─────────» - « └─╥─┘ ├────────────────┤ ║ ┌─┴─┐ » - «q_2: ─────╫─────┤ Delay(600[dt]) ├─────╫───────────┤ X ├───────» - « ┌────╨────┐└────────────────┘┌────╨────┐ └───┘ » - «c: 1/╡ c_0=0x0 ╞══════════════════╡ c_0=0x0 ╞══════════════════» - « └─────────┘ └─────────┘ » - « ┌───┐ ┌────────────────┐ - «q_0: ──────┤ X ├────────────■─────┤ Delay(700[dt]) ├ - « ┌─────┴───┴──────┐ ┌─┴─┐ ├────────────────┤ - «q_1: ┤ Delay(100[dt]) ├───┤ X ├───┤ Delay(700[dt]) ├ - « └──────┬─┬───────┘ └─╥─┘ └────────────────┘ - «q_2: ───────┤M├─────────────╫─────────────────────── - « └╥┘ ┌────╨────┐ - «c: 1/════════╩═════════╡ c_0=0x0 ╞══════════════════ - « 0 └─────────┘ - - """ - qc = QuantumCircuit(3, 1) - qc.delay(100, 0) - with self.assertWarns(DeprecationWarning): - qc.x(0).c_if(0, 1) - qc.barrier() - qc.measure(2, 0) - with self.assertWarns(DeprecationWarning): - qc.x(1).c_if(0, 0) - with self.assertWarns(DeprecationWarning): - qc.x(0).c_if(0, 0) - qc.delay(300, 0) - qc.cx(1, 2) - qc.x(0) - with self.assertWarns(DeprecationWarning): - qc.cx(0, 1).c_if(0, 0) - qc.measure(2, 0) - - durations = InstructionDurations( - [("x", None, 100), ("measure", None, 1000), ("cx", None, 200)] - ) - - with self.assertWarns(DeprecationWarning): - actual_asap = PassManager( - ASAPSchedule(durations, clbit_write_latency=100, conditional_latency=200) - ).run(qc) - actual_alap = PassManager( - ALAPSchedule(durations, clbit_write_latency=100, conditional_latency=200) - ).run(qc) - - expected_asap = QuantumCircuit(3, 1) - expected_asap.delay(100, 0) - expected_asap.delay(100, 0) # due to conditional latency of 200dt - expected_asap.delay(300, 1) - expected_asap.delay(300, 2) - with self.assertWarns(DeprecationWarning): - expected_asap.x(0).c_if(0, 1) - expected_asap.barrier() - expected_asap.delay(1400, 0) - expected_asap.delay(1200, 1) - expected_asap.measure(2, 0) - with self.assertWarns(DeprecationWarning): - expected_asap.x(1).c_if(0, 0) - with self.assertWarns(DeprecationWarning): - expected_asap.x(0).c_if(0, 0) - expected_asap.delay(300, 0) - expected_asap.x(0) - expected_asap.delay(300, 2) - expected_asap.cx(1, 2) - expected_asap.delay(400, 1) - with self.assertWarns(DeprecationWarning): - expected_asap.cx(0, 1).c_if(0, 0) - expected_asap.delay(700, 0) # creg is released at t0 of cx(0,1).c_if(0,0) - expected_asap.delay( - 700, 1 - ) # no creg write until 100dt. thus measure can move left by 300dt. - expected_asap.delay(300, 2) - expected_asap.measure(2, 0) - self.assertEqual(expected_asap, actual_asap) - self.assertEqual(actual_asap.duration, 3100) - - expected_alap = QuantumCircuit(3, 1) - expected_alap.delay(100, 0) - expected_alap.delay(100, 0) # due to conditional latency of 200dt - expected_alap.delay(300, 1) - expected_alap.delay(300, 2) - with self.assertWarns(DeprecationWarning): - expected_alap.x(0).c_if(0, 1) - expected_alap.barrier() - expected_alap.delay(1400, 0) - expected_alap.delay(1200, 1) - expected_alap.measure(2, 0) - with self.assertWarns(DeprecationWarning): - expected_alap.x(1).c_if(0, 0) - with self.assertWarns(DeprecationWarning): - expected_alap.x(0).c_if(0, 0) - expected_alap.delay(300, 0) - expected_alap.x(0) - expected_alap.delay(300, 1) - expected_alap.delay(600, 2) - expected_alap.cx(1, 2) - expected_alap.delay(100, 1) - with self.assertWarns(DeprecationWarning): - expected_alap.cx(0, 1).c_if(0, 0) - expected_alap.measure(2, 0) - expected_alap.delay(700, 0) - expected_alap.delay(700, 1) - self.assertEqual(expected_alap, actual_alap) - self.assertEqual(actual_alap.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 ASAP 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 ╞ - └─────────┘ └─────────┘ - - (ASAP 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) - with self.assertWarns(DeprecationWarning): - qc.x(0).c_if(0, True) - with self.assertWarns(DeprecationWarning): - qc.x(1).c_if(0, True) - - durations = InstructionDurations([("x", None, 160)]) - with self.assertWarns(DeprecationWarning): - pm = PassManager(ASAPSchedule(durations)) - scheduled = pm.run(qc) - - expected = QuantumCircuit(2, 1) - expected.delay(100, 0) - expected.delay(100, 1) # due to extra dependency on clbits - with self.assertWarns(DeprecationWarning): - expected.x(0).c_if(0, True) - with self.assertWarns(DeprecationWarning): - expected.x(1).c_if(0, True) - - self.assertEqual(expected, scheduled) - - @data(ALAPSchedule, ASAPSchedule) - def test_respect_target_instruction_constraints(self, schedule_pass): - """Test if ALAP/ASAP does not pad delays for qubits that do not support delay instructions. - See: https://github.com/Qiskit/qiskit-terra/issues/9993 - """ - target = Target(dt=1) - target.add_instruction(XGate(), {(1,): InstructionProperties(duration=200)}) - # delays are not supported - - qc = QuantumCircuit(2) - qc.x(1) - - with self.assertWarns(DeprecationWarning): - pm = PassManager(schedule_pass(target=target)) - scheduled = pm.run(qc) - - expected = QuantumCircuit(2) - expected.x(1) - # no delay on qubit 0 - - self.assertEqual(expected, scheduled) - - def test_dd_respect_target_instruction_constraints(self): - """Test if DD pass does not pad delays for qubits that do not support delay instructions - and does not insert DD gates for qubits that do not support necessary gates. - See: https://github.com/Qiskit/qiskit-terra/issues/9993 - """ - qc = QuantumCircuit(3) - qc.cx(0, 1) - qc.cx(1, 2) - - target = Target(dt=1) - # Y is partially supported (not supported on qubit 2) - target.add_instruction( - XGate(), {(q,): InstructionProperties(duration=100) for q in range(2)} - ) - target.add_instruction( - CXGate(), - { - (0, 1): InstructionProperties(duration=1000), - (1, 2): InstructionProperties(duration=1000), - }, - ) - # delays are not supported - - # No DD instructions nor delays are padded due to no delay support in the target - with self.assertWarns(DeprecationWarning): - pm_scheduler = PassManager( - [ - ALAPSchedule(target=target), - DynamicalDecoupling( - durations=None, dd_sequence=[XGate(), XGate()], target=target - ), - ] - ) - scheduled = pm_scheduler.run(qc) - self.assertEqual(qc, scheduled) - - # Fails since Y is not supported in the target - with self.assertWarns(DeprecationWarning): - with self.assertRaises(TranspilerError): - PassManager( - [ - ALAPSchedule(target=target), - DynamicalDecoupling( - durations=None, - dd_sequence=[XGate(), YGate(), XGate(), YGate()], - target=target, - ), - ] - ) - - # Add delay support to the target - target.add_instruction(Delay(Parameter("t")), {(q,): None for q in range(3)}) - # No error but no DD on qubit 2 (just delay is padded) since X is not supported on it - scheduled = pm_scheduler.run(qc) - - expected = QuantumCircuit(3) - expected.delay(1000, [2]) - expected.cx(0, 1) - expected.cx(1, 2) - expected.delay(200, [0]) - expected.x([0]) - expected.delay(400, [0]) - expected.x([0]) - expected.delay(200, [0]) - self.assertEqual(expected, scheduled) - - -if __name__ == "__main__": - unittest.main() From 7f0bdad994f822893f24c5fae6c718f13830f8a3 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 19 Feb 2025 17:55:35 +0000 Subject: [PATCH 47/48] Pin symengine in backwards compatibility tests (#13885) (#13887) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add symengine constraint in qpy backwards compat tests * Add comments * Only pin upper limit * Update comment (cherry picked from commit 8c06fd20be537377a7b96be687df76050e2e82f1) Co-authored-by: Elena Peña Tapia <57907331+ElePT@users.noreply.github.com> --- requirements.txt | 5 +++-- test/qpy_compat/qpy_test_constraints.txt | 5 +++++ test/qpy_compat/run_tests.sh | 2 ++ 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index 6eb5902ae9fd..845aea9c8f29 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,6 +7,7 @@ python-dateutil>=2.8.0 stevedore>=3.0.0 typing-extensions -# If updating the version range here, consider updating 'test/qpy_compat/run_tests.sh' to update the -# list of symengine dependencies used in the cross-version tests. +# If updating the version range here, consider updating the +# list of symengine dependencies used in the cross-version tests +# in 'test/qpy_compat/run_tests.sh' and 'test/qpy_compat/qpy_test_constraints.txt' symengine>=0.11,<0.14 diff --git a/test/qpy_compat/qpy_test_constraints.txt b/test/qpy_compat/qpy_test_constraints.txt index 03f798b3c01a..9b407a59b6ba 100644 --- a/test/qpy_compat/qpy_test_constraints.txt +++ b/test/qpy_compat/qpy_test_constraints.txt @@ -1,2 +1,7 @@ numpy===1.24.4 scipy===1.10.1 + +# This is a loose constraint because we want to test different versions, +# as defined in 'test/qpy_compat/run_tests.sh', but any symengine version +# above (and including) 0.14 will be incompatible with qpy. +symengine<0.14 diff --git a/test/qpy_compat/run_tests.sh b/test/qpy_compat/run_tests.sh index f1c770809c77..fd9e17c58071 100755 --- a/test/qpy_compat/run_tests.sh +++ b/test/qpy_compat/run_tests.sh @@ -57,6 +57,8 @@ popd # This will likely duplicate the base dev-compatibility test, but the tests are fairly fast, and # it's better safe than sorry with the serialisation tests. +# Note that the constraint in the range of symengine versions is logically duplicated +# in `qpy_test_constraints.txt` symengine_versions=( '>=0.11,<0.12' '>=0.13,<0.14' From 55c7904879b3489ffe56384ae747c45bd0488695 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 19 Feb 2025 18:28:00 +0000 Subject: [PATCH 48/48] Applying a slight perturbation for ill-conditioned matrices (#13882) (#13889) * Applying a slight perturbation for ill-conditioned matrices * Further improving the fix based on discussion with Jake and Gadi, and adding bugfix release note * Jake's suggestion from code review (cherry picked from commit 69eb2aa4f7edc9b8d189859ca8d6f3d2791b96b0) Co-authored-by: Alexander Ivrii --- .../operators/channel/transformations.py | 15 +++++++++++++++ .../notes/choi-to-kraus-3ae7d49f0a27f639.yaml | 5 +++++ 2 files changed, 20 insertions(+) create mode 100644 releasenotes/notes/choi-to-kraus-3ae7d49f0a27f639.yaml diff --git a/qiskit/quantum_info/operators/channel/transformations.py b/qiskit/quantum_info/operators/channel/transformations.py index 657ee62703ec..79619f798832 100644 --- a/qiskit/quantum_info/operators/channel/transformations.py +++ b/qiskit/quantum_info/operators/channel/transformations.py @@ -234,8 +234,23 @@ def _choi_to_kraus(data, input_dim, output_dim, atol=ATOL_DEFAULT): # # So the eigenvalues are on the diagonal, therefore the basis-transformation matrix must be # a spanning set of the eigenspace. + # + # In addition, to prevent `numpy.linalg` errors when the matrix A is ill-conditioned, + # we apply a small perturbation, replacing A by A + eI. Since (A + eI)x = kx is + # equivalent to Ax = (k-e)x, it means that the eigenvectors of A + eI and A are the same, + # and we can perfectly recover the eigenvalues of A from the eigenvalues of A + eI by + # subtracting e. + apply_perturbation = np.linalg.cond(data) >= 1e10 + + if apply_perturbation: + data += 1e-10 * np.eye(data.shape[0]) + triangular, vecs = scipy.linalg.schur(data) values = triangular.diagonal().real + + if apply_perturbation: + values = values - 1e-10 + # If we're not a CP map, fall-through back to the generalization handling. Since we needed # to get the eigenvalues anyway, we can do the CP check manually rather than deferring to a # separate re-calculation. diff --git a/releasenotes/notes/choi-to-kraus-3ae7d49f0a27f639.yaml b/releasenotes/notes/choi-to-kraus-3ae7d49f0a27f639.yaml new file mode 100644 index 000000000000..bd055121aff3 --- /dev/null +++ b/releasenotes/notes/choi-to-kraus-3ae7d49f0a27f639.yaml @@ -0,0 +1,5 @@ +--- +fixes: + - | + Applied a small regularisation factor against ill-conditioned Hermitian matrices + in super-operator representations.