From 675533e9e8fdfb095a6aead9b3c0acf9a2008daa Mon Sep 17 00:00:00 2001 From: Arianne Meijer Date: Thu, 5 Sep 2024 15:02:25 +0300 Subject: [PATCH 01/47] Minimal changes to support qiskit v1.2 - layout preservation of transpile_to_IQM broke here. --- CHANGELOG.rst | 10 + docs/user_guide.rst | 46 +--- pyproject.toml | 4 +- src/iqm/qiskit_iqm/iqm_backend.py | 174 ++++++++----- src/iqm/qiskit_iqm/iqm_naive_move_pass.py | 249 ++++++------------- src/iqm/qiskit_iqm/iqm_provider.py | 76 +++--- src/iqm/qiskit_iqm/iqm_transpilation.py | 1 + tests/move_architecture/test_architecture.py | 9 +- tests/test_iqm_backend.py | 6 +- tests/test_iqm_naive_move_pass.py | 13 +- tests/test_iqm_provider.py | 6 +- tox.ini | 8 +- 12 files changed, 282 insertions(+), 320 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 7bce5a15e..c2fe23fba 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -5,6 +5,16 @@ Changelog Version 13.13 ============= +* Refactored :meth:`IQMBackend.create_run_request` to improve user experience when using IQM specific run options. +* Updated the documentation for using additional run options with IQM backends. +* :meth:`IQMBackendBase.qubit_name_to_index` and :meth:`IQMBackendBase.index_to_qubit_name` now raises an error when using an invalid qubit name or index, rather than returning None. +* Unified `IQMBackendBase.architecture` and `IQMBackendBase._quantum_architecture` to refer to the same object. +* Introduction of `IQMBackendBase.physical_target` and `IQMBackendBase.fake_target` to represent the physical quantum architectures and a Qiskit-compatible version, respectively. +* Added support for ``qiskit == 1.2`` and ``qiskit-aer == 1.5``. + +Version 13.13 +============= + * Adjustments needed to support Qiskit V1 that are backwards compatible with ``qiskit < 1.0``. `#114 `_ * Updated Qiskit dependencies and testing to support ``qiskit >= 0.45.3 < 1.2`` and ``qiskit-aer >= 0.13 < 0.15``. * Adjusted documentation to recommend the use of :meth:`qiskit.transpile()` or :meth:`transpile_to_IQM()` in combination with :meth:`backend.run()` instead of using :meth:`execute()`. diff --git a/docs/user_guide.rst b/docs/user_guide.rst index 4f6387ccd..727756fac 100644 --- a/docs/user_guide.rst +++ b/docs/user_guide.rst @@ -114,48 +114,10 @@ you want to use, you can provide it as follows: job = backend.run(qc, shots=1000, calibration_set_id="f7d9642e-b0ca-4f2d-af2a-30195bd7a76d") -Alternatively, you can update the values of the options directly on the backend instance using the :meth:`.IQMBackend.set_options` -and then call execution methods without specifying additional keyword arguments. You can view all available options and -their current values using `backend.options`. Below table summarizes currently available options: - -.. list-table:: - :widths: 25 100 - :header-rows: 1 - - * - Name - - Description - * - `shots` - - Type: ``int``, Example value: ``1207``. - - Number of shots. - * - `calibration_set_id` - - Type: ``str``, Example value ``"f7d9642e-b0ca-4f2d-af2a-30195bd7a76d"``. - - Indicates the calibration set to use. Defaults to `None`, which means the IQM server will use the best - available calibration set automatically. - * - `max_circuit_duration_over_t2` - - Type: ``float``, Example value: ``1.0``. - - Set server-side circuit disqualification threshold. If any circuit in a job is estimated to take longer than the - shortest T2 time of any qubit used in the circuit multiplied by this value, the server will reject the job. - Setting this value to ``0.0`` will disable circuit duration check. - The default value ``None`` means the server default value will be used. - * - `heralding_mode` - - Type: :class:`~iqm.iqm_client.models.HeraldingMode`, Example value: ``"zeros"``. - - Heralding mode to use during execution. The default value is "none". - * - `circuit_callback` - - Type: :class:`collections.abc.Callable`, Example value: ``None``. - - A function that accepts a list of :class:`qiskit.QuantumCircuit` instances and does not return anything. - When the backend receives circuits for execution, it will call this function (if provided) and pass those - circuits as argument. This may be useful in situations when you do not have explicit control over transpilation, - but need some information on how it was done. This can happen, for example, when you use pre-implemented - algorithms and experiments in Qiskit, where the implementation of the said algorithm or experiment takes care of - delivering correctly transpiled circuits to the backend. This callback method gives you a chance to look into - those transpiled circuits, extract any info you need. As a side effect, you can also use this callback to modify - the transpiled circuits in-place, just before execution, however we do not recommend to use it for this purpose. - +Alternatively, you can update the values of the options directly in the backend instance using the :meth:`.IQMBackend.set_options` +and then call execution methods without specifying additional keyword arguments. +As the `.backend.options` attribute is used to store additional keyword arguments for :meth:`.IQMBackend.run`, you can find the +an up-to-date list of available options and their current values in the documentation of the :meth:`.IQMBackend.run` method. If the IQM server you are connecting to requires authentication, you will also have to use `Cortex CLI `_ to retrieve and automatically refresh access tokens, diff --git a/pyproject.toml b/pyproject.toml index 0e45b8226..75d6e425d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,8 +20,8 @@ classifiers = [ requires-python = ">=3.9, <3.12" dependencies = [ "numpy", - "qiskit >= 0.45, < 1.2", - "qiskit-aer >= 0.13.1, < 0.15", + "qiskit >= 0.45, < 1.3", + "qiskit-aer >= 0.13.1, < 0.16", "iqm-client >= 18.0, < 19.0" ] diff --git a/src/iqm/qiskit_iqm/iqm_backend.py b/src/iqm/qiskit_iqm/iqm_backend.py index 78d84a454..653a004a3 100644 --- a/src/iqm/qiskit_iqm/iqm_backend.py +++ b/src/iqm/qiskit_iqm/iqm_backend.py @@ -17,7 +17,7 @@ from abc import ABC import re -from typing import Final, Optional +from typing import Final from qiskit.circuit import Parameter from qiskit.circuit.library import CZGate, IGate, Measure, RGate @@ -30,6 +30,81 @@ IQM_TO_QISKIT_GATE_NAME: Final[dict[str, str]] = {'prx': 'r', 'cz': 'cz'} +def _QuantumArchitectureSpecification_to_qiskit_target( + architecture: QuantumArchitectureSpecification, +) -> tuple[Target, Target, dict[str, int]]: + """Converts a QuantumArchitectureSpecification object to a Qiskit Target object. + + Args: + architecture: The quantum architecture specification to convert. + + Returns: + A Qiskit Target object representing the given quantum architecture specification. + """ + target = Target() + fake_target = Target() + + def get_num_or_zero(name: str) -> int: + match = re.search(r'(\d+)', name) + return int(match.group(1)) if match else 0 + + qb_to_idx = {qb: idx for idx, qb in enumerate(sorted(architecture.qubits, key=get_num_or_zero))} + operations = architecture.operations + + # There is no dedicated direct way of setting just the qubit connectivity and the native gates to the target. + # Such info is automatically deduced once all instruction properties are set. Currently, we do not retrieve + # any properties from the server, and we are interested only in letting the target know what is the native gate + # set and the connectivity of the device under use. Thus, we populate the target with None properties. + def _create_connections(name: str): + """Creates the connection map of allowed loci for this instruction, mapped to None.""" + if is_multi_qubit_instruction(name): + if is_directed_instruction(name): + return {(qb_to_idx[qb1], qb_to_idx[qb2]): None for [qb1, qb2] in operations[name]} + return { + (qb_to_idx[qb1], qb_to_idx[qb2]): None for pair in operations[name] for qb1, qb2 in (pair, pair[::-1]) + } + return {(qb_to_idx[qb],): None for [qb] in operations[name]} + + if 'prx' in operations or 'phased_rx' in operations: + target.add_instruction( + RGate(Parameter('theta'), Parameter('phi')), + _create_connections('prx' if 'prx' in operations else 'phased_rx'), + ) + fake_target.add_instruction( + RGate(Parameter('theta'), Parameter('phi')), + _create_connections('prx' if 'prx' in operations else 'phased_rx'), + ) + target.add_instruction(IGate(), {(qb_to_idx[qb],): None for qb in architecture.qubits}) + fake_target.add_instruction( + IGate(), {(qb_to_idx[qb],): None for qb in architecture.qubits if not qb.startswith('COMP_R')} + ) + # Even though CZ is a symmetric gate, we still need to add properties for both directions. This is because + # coupling maps in Qiskit are directed graphs and the gate symmetry is not implicitly planted there. It should + # be explicitly supplied. This allows Qiskit to have coupling maps with non-symmetric gates like cx. + if 'measure' in operations: + target.add_instruction(Measure(), _create_connections('measure')) + fake_target.add_instruction(Measure(), _create_connections('measure')) + if 'measurement' in operations: + target.add_instruction(Measure(), _create_connections('measurement')) + fake_target.add_instruction(Measure(), _create_connections('measurement')) + if 'move' in operations: + target.add_instruction(MoveGate(), _create_connections('move')) + if 'cz' in operations: + target.add_instruction(CZGate(), _create_connections('cz')) + fake_cz_connections: dict[tuple[int, int], None] = {} + for qb1, res in operations['move']: + for qb2 in [q for q in architecture.qubits if q not in [qb1, res]]: + if [qb2, res] in operations['cz'] or [res, qb2] in operations['cz']: + fake_cz_connections[(qb_to_idx[qb1], qb_to_idx[qb2])] = None + fake_cz_connections[(qb_to_idx[qb2], qb_to_idx[qb1])] = None + fake_target.add_instruction(CZGate(), fake_cz_connections) + else: + if 'cz' in operations: + target.add_instruction(CZGate(), _create_connections('cz')) + fake_target.add_instruction(CZGate(), _create_connections('cz')) + return target, fake_target, qb_to_idx + + class IQMBackendBase(BackendV2, ABC): """Abstract base class for various IQM-specific backends. @@ -37,75 +112,56 @@ class IQMBackendBase(BackendV2, ABC): architecture: Description of the quantum architecture associated with the backend instance. """ - architecture: QuantumArchitectureSpecification - def __init__(self, architecture: QuantumArchitectureSpecification, **kwargs): super().__init__(**kwargs) - self.architecture = architecture - - def get_num_or_zero(name: str) -> int: - match = re.search(r'(\d+)', name) - return int(match.group(1)) if match else 0 - - qb_to_idx = {qb: idx for idx, qb in enumerate(sorted(architecture.qubits, key=get_num_or_zero))} - operations = architecture.operations - - target = Target() - - # There is no dedicated direct way of setting just the qubit connectivity and the native gates to the target. - # Such info is automatically deduced once all instruction properties are set. Currently, we do not retrieve - # any properties from the server, and we are interested only in letting the target know what is the native gate - # set and the connectivity of the device under use. Thus, we populate the target with None properties. - def _create_connections(name: str): - """Creates the connection map of allowed loci for this instruction, mapped to None.""" - if is_multi_qubit_instruction(name): - if is_directed_instruction(name): - return {(qb_to_idx[qb1], qb_to_idx[qb2]): None for [qb1, qb2] in operations[name]} - return { - (qb_to_idx[qb1], qb_to_idx[qb2]): None - for pair in operations['cz'] - for qb1, qb2 in (pair, pair[::-1]) - } - return {(qb_to_idx[qb],): None for [qb] in operations[name]} - - if 'prx' in operations or 'phased_rx' in operations: - target.add_instruction( - RGate(Parameter('theta'), Parameter('phi')), - _create_connections('prx' if 'prx' in operations else 'phased_rx'), - ) - target.add_instruction(IGate(), {(qb_to_idx[qb],): None for qb in architecture.qubits}) - # Even though CZ is a symmetric gate, we still need to add properties for both directions. This is because - # coupling maps in Qiskit are directed graphs and the gate symmetry is not implicitly planted there. It should - # be explicitly supplied. This allows Qiskit to have coupling maps with non-symmetric gates like cx. - if 'cz' in operations: - target.add_instruction(CZGate(), _create_connections('cz')) - if 'measure' in operations: - target.add_instruction(Measure(), _create_connections('measure')) - if 'measurement' in operations: - target.add_instruction(Measure(), _create_connections('measurement')) - if 'move' in operations: - target.add_instruction(MoveGate(), _create_connections('move')) - - self._target = target - self._qb_to_idx = qb_to_idx - self._idx_to_qb = {v: k for k, v in qb_to_idx.items()} - # Copy of the original quantum architecture that was used to construct the target. Used for validation only. + + self._physical_target, self._fake_target, self._qb_to_idx = _QuantumArchitectureSpecification_to_qiskit_target( + architecture + ) + self._idx_to_qb = {v: k for k, v in self._qb_to_idx.items()} + self._quantum_architecture = architecture self.name = 'IQMBackend' @property def target(self) -> Target: - return self._target + return self._physical_target + + @property + def fake_target(self) -> Target: + """A target representing the backend where resonators are abstracted away. If the backend does not support + resonators, this target is the same as the `target` property, but different instances. + """ + return self._fake_target + + @property + def physical_target(self) -> Target: + """A target providing an accurate representation of the backend.""" + return self._physical_target + + @property + def physical_qubits(self) -> list[str]: + """Return the list of physical qubits in the backend.""" + return list(self._qb_to_idx) - def qubit_name_to_index(self, name: str) -> Optional[int]: + @property + def architecture(self) -> QuantumArchitectureSpecification: + """Description of the quantum architecture associated with the backend instance.""" + return self._quantum_architecture + + def qubit_name_to_index(self, name: str) -> int: """Given an IQM-style qubit name ('QB1', 'QB2', etc.) return the corresponding index in the register. Returns None is the given name does not belong to the backend.""" - return self._qb_to_idx.get(name) + if name not in self._qb_to_idx: + raise ValueError(f"Qubit name '{name}' is not part of the backend.") + return self._qb_to_idx[name] - def index_to_qubit_name(self, index: int) -> Optional[str]: + def index_to_qubit_name(self, index: int) -> str: """Given an index in the backend register return the corresponding IQM-style qubit name ('QB1', 'QB2', etc.). Returns None if the given index does not correspond to any qubit in the backend.""" - return self._idx_to_qb.get(index) + if index not in self._idx_to_qb: + raise ValueError(f"Qubit index '{index}' is not part of the backend.") + return self._idx_to_qb[index] def validate_compatible_architecture(self, architecture: QuantumArchitectureSpecification) -> bool: """Given a quantum architecture specification returns true if its number of qubits, names of qubits and qubit @@ -118,3 +174,7 @@ def validate_compatible_architecture(self, architecture: QuantumArchitectureSpec connectivity_match = self_connectivity == target_connectivity return qubits_match and ops_match and connectivity_match + + # def get_scheduling_stage_plugin(self): + # """Return the plugin that should be used for scheduling the circuits on this backend.""" + # raise NotImplementedError diff --git a/src/iqm/qiskit_iqm/iqm_naive_move_pass.py b/src/iqm/qiskit_iqm/iqm_naive_move_pass.py index 82c9fa38b..f864469ea 100644 --- a/src/iqm/qiskit_iqm/iqm_naive_move_pass.py +++ b/src/iqm/qiskit_iqm/iqm_naive_move_pass.py @@ -1,24 +1,16 @@ # Copyright 2024 Qiskit on IQM developers """Naive transpilation for N-star architecture""" -from datetime import datetime -from typing import Optional, Union - -from qiskit import QuantumCircuit, user_config +from qiskit import QuantumCircuit, transpile from qiskit.circuit import QuantumRegister, Qubit from qiskit.dagcircuit import DAGCircuit, DAGOpNode -from qiskit.providers.models import BackendProperties -from qiskit.transpiler import CouplingMap, Layout, TranspileLayout +from qiskit.transpiler import Layout, TranspileLayout from qiskit.transpiler.basepasses import TransformationPass from qiskit.transpiler.exceptions import TranspilerError from qiskit.transpiler.passmanager import PassManager -from qiskit.transpiler.passmanager_config import PassManagerConfig -from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager -from qiskit.transpiler.target import Target -from .fake_backends.iqm_fake_backend import IQMFakeBackend +from .iqm_backend import IQMBackendBase from .iqm_circuit import IQMCircuit -from .iqm_provider import IQMBackend from .iqm_transpilation import IQMOptimizeSingleQubitGates from .move_gate import MoveGate @@ -202,138 +194,23 @@ def _move_resonator(self, qubit: int, canonical_register: QuantumRegister, curre return swap_layer -def _to_qubit_indices(backend: Union[IQMBackend, IQMFakeBackend], qubit_names: list[str]) -> list[int]: - indices = [backend.qubit_name_to_index(res) for res in qubit_names] - return [i for i in indices if i is not None] - - -def _qubit_to_index_without_resonator( - backend: Union[IQMBackend, IQMFakeBackend], resonator_registers: list[str], qb: str -) -> Optional[int]: - resonator_indices = _to_qubit_indices(backend, resonator_registers) - idx = backend.qubit_name_to_index(qb) - return (idx - sum(1 for r in resonator_indices if r < idx)) if idx is not None else None - - -def _generate_coupling_map_without_resonator(backend: Union[IQMBackend, IQMFakeBackend]) -> CouplingMap: - # Grab qubits from backend operations - allowed_ops = backend.architecture.operations - allowed_czs = allowed_ops["cz"] - allowed_moves = allowed_ops["move"] - - iqm_registers = backend.architecture.qubits - resonator_registers = [r for r in iqm_registers if r.startswith("COMP_R")] - - move_qubits = {r: [q for pair in allowed_moves for q in pair if r in pair and q != r] for r in resonator_registers} - - edges = [] - for qb1, qb2 in allowed_czs: - if qb1 in resonator_registers: - vs1 = move_qubits[qb1] - else: - vs1 = [qb1] - if qb2 in resonator_registers: - vs2 = move_qubits[qb2] - else: - vs2 = [qb2] - for v1 in vs1: - for v2 in vs2: - qb1_idx = _qubit_to_index_without_resonator(backend, resonator_registers, v1) - qb2_idx = _qubit_to_index_without_resonator(backend, resonator_registers, v2) - if qb1_idx is not None and qb2_idx is not None: - edges.append((qb1_idx, qb2_idx)) - - return CouplingMap(edges) - - -def build_IQM_star_pass_manager_config( - backend: Union[IQMBackend, IQMFakeBackend], circuit: QuantumCircuit -) -> PassManagerConfig: - """Build configuration for IQM backend. - - We need to pass precomputed values to be used in transpiler passes via backend_properties. - This function performs precomputation for the backend and packages the values to the config object.""" - coupling_map = _generate_coupling_map_without_resonator(backend) - allowed_ops = backend.architecture.operations - allowed_moves = allowed_ops["move"] - - iqm_registers = backend.architecture.qubits - classical_registers = list(range(len(circuit.clbits))) - resonator_registers = [r for r in iqm_registers if r.startswith("COMP_R")] - move_qubits = {r: [q for pair in allowed_moves for q in pair if r in pair and q != r] for r in resonator_registers} - qubit_registers = [q for q in iqm_registers if q not in resonator_registers] - - qubit_indices = [backend.qubit_name_to_index(qb) for qb in qubit_registers] - bit_indices = [_qubit_to_index_without_resonator(backend, resonator_registers, qb) for qb in qubit_registers] - - resonator_indices = [backend.qubit_name_to_index(r) for r in resonator_registers] - - if len(resonator_indices) != 1: - raise NotImplementedError("Device must have exactly one resonator.") - if any(idx is None for idx in resonator_indices): - raise RuntimeError("Could not find index of a resonator.") - move_indices = _to_qubit_indices(backend, move_qubits[resonator_registers[0]]) - - extra_backend_properties = { - "resonator_indices": resonator_indices, - "move_indices": move_indices, - "qubit_indices": qubit_indices, - "bit_indices": bit_indices, - "classical_registers": classical_registers, - } - backend_properties = BackendProperties( - backend_name=backend.name, - backend_version="", - last_update_date=datetime.now(), - qubits=[], - gates=[], - general=[], - ) - backend_properties._data.update(**extra_backend_properties) - return PassManagerConfig( - basis_gates=backend.operation_names, - backend_properties=backend_properties, - target=Target(num_qubits=len(qubit_indices)), - coupling_map=coupling_map, - ) - - -def build_IQM_star_pass(pass_manager_config: PassManagerConfig) -> TransformationPass: - """Build translate pass for IQM star architecture""" - - backend_props = pass_manager_config.backend_properties.to_dict() - resonator_indices = backend_props.get("resonator_indices") - return IQMNaiveResonatorMoving( - resonator_indices[0], - backend_props.get("move_indices"), - pass_manager_config.basis_gates, - ) - - def transpile_to_IQM( # pylint: disable=too-many-arguments circuit: QuantumCircuit, - backend: Union[IQMBackend, IQMFakeBackend], + backend: IQMBackendBase, optimize_single_qubits: bool = True, ignore_barriers: bool = False, - remove_final_rzs: bool = False, - optimization_level: Optional[int] = None, + remove_final_rzs: bool = True, + **qiskit_transpiler_qwargs, ) -> QuantumCircuit: """Basic function for transpiling to IQM backends. Currently works with Deneb and Garnet Args: - circuit (QuantumCircuit): The circuit to be transpiled without MOVE gates. - backend (IQMBackend | IQMFakeBackend): The target backend to compile to containing a single resonator. - optimize_single_qubits (bool): Whether to optimize single qubit gates away (default = True). - ignore_barriers (bool): Whether to ignore barriers when optimizing single qubit gates away (default = False). - remove_final_rzs (bool): Whether to remove the final Rz rotations (default = False). - optimization_level: How much optimization to perform on the circuits as per Qiskit transpiler. - Higher levels generate more optimized circuits, - at the expense of longer transpilation time. - - * 0: no optimization - * 1: light optimization (default) - * 2: heavy optimization - * 3: even heavier optimization + circuit: The circuit to be transpiled without MOVE gates. + backend: The target backend to compile to. Does not require a resonator. + optimize_single_qubits: Whether to optimize single qubit gates away. + ignore_barriers: Whether to ignore barriers when optimizing single qubit gates away. + remove_final_rzs: Whether to remove the final Rz rotations. + qiskit_transpiler_qwargs: Arguments to be passed to the Qiskit transpiler. Raises: NotImplementedError: Thrown when the backend supports multiple resonators. @@ -341,69 +218,89 @@ def transpile_to_IQM( # pylint: disable=too-many-arguments Returns: QuantumCircuit: The transpiled circuit ready for running on the backend. """ + circuit_with_resonator = IQMCircuit(backend.fake_target.num_qubits) + circuit_with_resonator.add_bits(circuit.clbits) + qubit_indices = [backend.qubit_name_to_index(qb) for qb in backend.physical_qubits if not qb.startswith("COMP_R")] + circuit_with_resonator.append( + circuit, + [circuit_with_resonator.qubits[qubit_indices[i]] for i in range(circuit.num_qubits)], + circuit.clbits, + ) + + # Transpile the circuit using the fake target without resonators + simple_transpile = transpile( + circuit_with_resonator, + target=backend.fake_target, + basis_gates=backend.fake_target.operation_names, + **qiskit_transpiler_qwargs, + ) + # Construct the pass sequence for the additional passes passes = [] if optimize_single_qubits: optimize_pass = IQMOptimizeSingleQubitGates(remove_final_rzs, ignore_barriers) passes.append(optimize_pass) - if optimization_level is None: - config = user_config.get_config() - optimization_level = config.get("transpile_optimization_level", 1) - - if "move" not in backend.architecture.operations.keys(): - pass_manager = generate_preset_pass_manager(backend=backend, optimization_level=optimization_level) - simple_transpile = pass_manager.run(circuit) - if passes: - return PassManager(passes).run(simple_transpile) - return simple_transpile - pass_manager_config = build_IQM_star_pass_manager_config(backend, circuit) - move_pass = build_IQM_star_pass(pass_manager_config) - passes.append(move_pass) - - backend_props = pass_manager_config.backend_properties.to_dict() - qubit_indices = backend_props.get("qubit_indices") - resonator_indices = backend_props.get("resonator_indices") - classical_registers = backend_props.get("classical_registers") + if "move" in backend.architecture.operations.keys(): + move_pass = IQMNaiveResonatorMoving( + backend.architecture.qubits.index("COMP_R"), + [backend.qubit_name_to_index(q) for q, r in backend.architecture.operations["move"] if r == "COMP_R"], + backend._physical_target.operation_names, + ) + passes.append(move_pass) + + # circuit_with_resonator = add_resonators_to_circuit(simple_transpile, backend) + # else: + # circuit_with_resonator = simple_transpile + + # Transpiler passes strip the layout information, so we need to add it back + layout = simple_transpile._layout + # TODO Update the circuit so that following passes can use the layout information, + # old buggy logic in _add_resonators_to_circuit + # TODO Add actual tests for the updating the layout. Currrently not done because Deneb's fake_target is + # fully connected. + transpiled_circuit = PassManager(passes).run(simple_transpile) + transpiled_circuit._layout = layout + return transpiled_circuit + + +def _add_resonators_to_circuit(circuit: QuantumCircuit, backend: IQMBackendBase) -> QuantumCircuit: + """Add resonators to a circuit for a backend that supports multiple resonators. + + Args: + circuit: The circuit to add resonators to. + backend: The backend to add resonators for. + + Returns: + QuantumCircuit: The circuit with resonators added. + """ + qubit_indices = [backend.qubit_name_to_index(qb) for qb in backend.physical_qubits if not qb.startswith("COMP_R")] + resonator_indices = [backend.qubit_name_to_index(qb) for qb in backend.physical_qubits if qb.startswith("COMP_R")] + n_classical_regs = len(circuit.cregs) n_qubits = len(qubit_indices) n_resonators = len(resonator_indices) - pass_manager = generate_preset_pass_manager( - optimization_level, - basis_gates=pass_manager_config.basis_gates, - coupling_map=pass_manager_config.coupling_map, - ) - simple_transpile = pass_manager.run(circuit) - circuit_with_resonator = IQMCircuit( - n_qubits + n_resonators, - max(classical_registers) + 1 if len(classical_registers) > 0 else 0, - ) - + circuit_with_resonator = IQMCircuit(n_qubits + n_resonators, n_classical_regs) + # Update and copy the initial and final layout of the circuit found by the transpiler layout_dict = { qb: i + sum(1 for r_i in resonator_indices if r_i <= i + n_resonators) - for qb, i in simple_transpile._layout.initial_layout._v2p.items() + for qb, i in circuit._layout.initial_layout._v2p.items() } layout_dict.update({Qubit(QuantumRegister(n_resonators, "resonator"), r_i): r_i for r_i in resonator_indices}) initial_layout = Layout(input_dict=layout_dict) init_mapping = layout_dict final_layout = None - if simple_transpile.layout.final_layout: + if circuit.layout.final_layout: final_layout_dict = { qb: i + sum(1 for r_i in resonator_indices if r_i <= i + n_resonators) - for qb, i in simple_transpile.layout.final_layout._v2p.items() + for qb, i in circuit.layout.final_layout._v2p.items() } final_layout_dict.update( {Qubit(QuantumRegister(n_resonators, "resonator"), r_i): r_i for r_i in resonator_indices} ) final_layout = Layout(final_layout_dict) new_layout = TranspileLayout(initial_layout, init_mapping, final_layout=final_layout) - - circuit_with_resonator.append( - simple_transpile, qubit_indices, classical_registers if len(classical_registers) > 0 else None - ) - circuit_with_resonator._layout = new_layout + circuit_with_resonator.append(circuit, circuit_with_resonator.qregs, circuit_with_resonator.cregs) circuit_with_resonator = circuit_with_resonator.decompose() - - transpiled_circuit = PassManager(passes).run(circuit_with_resonator) - transpiled_circuit._layout = new_layout - return transpiled_circuit + circuit_with_resonator._layout = new_layout + return circuit_with_resonator diff --git a/src/iqm/qiskit_iqm/iqm_provider.py b/src/iqm/qiskit_iqm/iqm_provider.py index debb1ad13..4fe1e8561 100644 --- a/src/iqm/qiskit_iqm/iqm_provider.py +++ b/src/iqm/qiskit_iqm/iqm_provider.py @@ -13,7 +13,7 @@ # limitations under the License. """Qiskit Backend Provider for IQM backends. """ -from copy import copy +from collections.abc import Callable from importlib.metadata import PackageNotFoundError, version from functools import reduce from typing import Optional, Union @@ -61,6 +61,7 @@ def _default_options(cls) -> Options: calibration_set_id=None, circuit_compilation_options=CircuitCompilationOptions(), circuit_callback=None, + timeout_seconds=None, ) @property @@ -86,38 +87,47 @@ def run(self, run_input: Union[QuantumCircuit, list[QuantumCircuit]], **options) Args: run_input (Union[QuantumCircuit, list[QuantumCircuit]]): The circuits to run. options: A dictionary of options for the run. The following options are supported: - - shots (int): Number of repetitions of each circuit, for sampling. Default is 1024. - calibration_set_id (str or UUID): ID of the calibration set to use for the run. Default is None. - circuit_compilation_options (CircuitCompilationOptions): Compilation options for the circuits as - documented in ``iqm-client``. - circuit_callback (Callable): Any callback function that will be called for each circuit before sending - the circuits to the device. timeout_seconds Optional(float): Optional timeout passed to the :class:`IQMJob` in seconds. + other options: Additional options to be passed to :meth:`create_run_request`. Returns: IQMJob: The Job from which the results can be obtained once the circuits are executed. """ - timeout_seconds = options.pop('timeout_seconds', None) + timeout_seconds = options.pop('timeout_seconds', self.options.timeout_seconds) run_request = self.create_run_request(run_input, **options) job_id = self.client.submit_run_request(run_request) job = IQMJob(self, str(job_id), shots=run_request.shots, timeout_seconds=timeout_seconds) job.circuit_metadata = [c.metadata for c in run_request.circuits] return job - def create_run_request(self, run_input: Union[QuantumCircuit, list[QuantumCircuit]], **options) -> RunRequest: + def create_run_request( + self, + run_input: Union[QuantumCircuit, list[QuantumCircuit]], + shots: Optional[int] = None, + calibration_set_id: Optional[Union[str, UUID]] = None, + circuit_compilation_options: Optional[CircuitCompilationOptions] = None, + circuit_callback: Optional[Callable] = None, + **unknown_options, + ) -> RunRequest: """Creates a run request without submitting it for execution. This can be used to check what would be submitted for execution by an equivalent call to :meth:`run`. Args: run_input: same as ``run_input`` for :meth:`run` - options: same as ``options`` for :meth:`run` without ``timeout_seconds`` + shots: The number of shots for each circuit. If not set, it will use the default from `IQMBackend.options`. + calibration_set_id: ID of the calibration set to use for the run. If not set, it will use the default from + `IQMBackend.options`. + circuit_compilation_options: Compilation options for the circuits as documented in ``iqm-client``. + If not set, it will use the default from `IQMBackend.options`. + circuit_callback (Callable): Any callback function that will be called for each circuit before sending + the circuits to the device. If not set, it will use the default from `IQMBackend.options`. Returns: - the created run request object + The created run request object """ + # pylint: disable=too-many-arguments if self.client is None: raise RuntimeError('Session to IQM client has been closed.') @@ -126,31 +136,41 @@ def create_run_request(self, run_input: Union[QuantumCircuit, list[QuantumCircui if len(circuits) == 0: raise ValueError('Empty list of circuits submitted for execution.') - unknown_options = set(options.keys()) - set(self.options.keys()) + if shots is None: + shots = self.options.shots + # Catch old iqm-client options - if 'max_circuit_duration_over_t2' in unknown_options and 'circuit_compilation_options' not in options: - self.options['circuit_compilation_options'].max_circuit_duration_over_t2 = options.pop( - 'max_circuit_duration_over_t2' + if 'max_circuit_duration_over_t2' in unknown_options or 'heralding_mode' in unknown_options: + warnings.warn( + DeprecationWarning( + 'max_circuit_duration_over_t2 and heralding_mode are deprecated, please use ' + + 'circuit_compilation_options instead.' + ) + ) + if circuit_compilation_options is None: + circuit_compilation_options = CircuitCompilationOptions( + max_circuit_duration_over_t2=unknown_options.pop( + 'max_circuit_duration_over_t2', + self.options.circuit_compilation_options.max_circuit_duration_over_t2, + ), + heralding_mode=unknown_options.pop( + 'heralding_mode', self.options.circuit_compilation_options.heralding_mode + ), ) - unknown_options.remove('max_circuit_duration_over_t2') - if 'heralding_mode' in unknown_options and 'circuit_compilation_options' not in options: - self.options['circuit_compilation_options'].heralding_mode = options.pop('heralding_mode') - unknown_options.remove('heralding_mode') if unknown_options: warnings.warn(f'Unknown backend option(s): {unknown_options}') - # merge given options with default options and get resulting values - merged_options = copy(self.options) - merged_options.update_options(**dict(options)) - shots = merged_options['shots'] - calibration_set_id = merged_options['calibration_set_id'] + if calibration_set_id is None: + calibration_set_id = self.options.calibration_set_id + if calibration_set_id is not None and not isinstance(calibration_set_id, UUID): calibration_set_id = UUID(calibration_set_id) - circuit_callback = merged_options['circuit_callback'] if circuit_callback: circuit_callback(circuits) + elif self.options.circuit_callback: + self.options.circuit_callback(circuits) circuits_serialized: list[Circuit] = [self.serialize_circuit(circuit) for circuit in circuits] used_indices: set[int] = reduce( @@ -161,9 +181,9 @@ def create_run_request(self, run_input: Union[QuantumCircuit, list[QuantumCircui return self.client.create_run_request( circuits_serialized, qubit_mapping=qubit_mapping, - calibration_set_id=calibration_set_id if calibration_set_id else None, + calibration_set_id=calibration_set_id, shots=shots, - options=merged_options['circuit_compilation_options'], + options=circuit_compilation_options, ) def retrieve_job(self, job_id: str) -> IQMJob: diff --git a/src/iqm/qiskit_iqm/iqm_transpilation.py b/src/iqm/qiskit_iqm/iqm_transpilation.py index b30d4df54..456e13c41 100644 --- a/src/iqm/qiskit_iqm/iqm_transpilation.py +++ b/src/iqm/qiskit_iqm/iqm_transpilation.py @@ -21,6 +21,7 @@ from qiskit.transpiler.basepasses import TransformationPass from qiskit.transpiler.passes import BasisTranslator, Optimize1qGatesDecomposition, RemoveBarriers from qiskit.transpiler.passmanager import PassManager +from qiskit.transpiler.preset_passmanagers.plugin import PassManagerStagePlugin class IQMOptimizeSingleQubitGates(TransformationPass): diff --git a/tests/move_architecture/test_architecture.py b/tests/move_architecture/test_architecture.py index a71e5cd2c..3e5da9842 100644 --- a/tests/move_architecture/test_architecture.py +++ b/tests/move_architecture/test_architecture.py @@ -63,13 +63,14 @@ def test_backend_configuration_new(new_architecture): backend, _client = get_mocked_backend(new_architecture) assert backend.target.physical_qubits == [0, 1, 2, 3, 4, 5, 6] assert set(backend.target.operation_names) == {'r', 'id', 'cz', 'measure', 'move'} - assert [f'{o.name}:{o.num_qubits}' for o in backend.target.operations] == [ + assert {f'{o.name}:{o.num_qubits}' for o in backend.target.operations} == { 'r:1', 'id:1', 'cz:2', 'measure:1', 'move:2', - ] + } + check_instruction(backend.instructions, 'r', [(1,), (2,), (3,), (4,), (5,), (6,)]) check_instruction(backend.instructions, 'measure', [(1,), (2,), (3,), (4,), (5,), (6,)]) check_instruction(backend.instructions, 'id', [(0,), (1,), (2,), (3,), (4,), (5,), (6,)]) @@ -85,12 +86,12 @@ def test_backend_configuration_adonis(adonis_architecture): backend, _client = get_mocked_backend(adonis_architecture) assert backend.target.physical_qubits == [0, 1, 2, 3, 4] assert set(backend.target.operation_names) == {'r', 'id', 'cz', 'measure'} - assert [f'{o.name}:{o.num_qubits}' for o in backend.target.operations] == [ + assert {f'{o.name}:{o.num_qubits}' for o in backend.target.operations} == { 'r:1', 'id:1', 'cz:2', 'measure:1', - ] + } check_instruction(backend.instructions, 'r', [(0,), (1,), (2,), (3,), (4,)]) check_instruction(backend.instructions, 'measure', [(0,), (1,), (2,), (3,), (4,)]) check_instruction(backend.instructions, 'id', [(0,), (1,), (2,), (3,), (4,)]) diff --git a/tests/test_iqm_backend.py b/tests/test_iqm_backend.py index f7e3c8f9f..da63cfa43 100644 --- a/tests/test_iqm_backend.py +++ b/tests/test_iqm_backend.py @@ -52,8 +52,10 @@ def test_qubit_name_to_index_to_qubit_name(adonis_architecture_shuffled_names): assert all(backend.index_to_qubit_name(idx) == name for idx, name in correct_idx_name_associations) assert all(backend.qubit_name_to_index(name) == idx for idx, name in correct_idx_name_associations) - assert backend.index_to_qubit_name(7) is None - assert backend.qubit_name_to_index('Alice') is None + with pytest.raises(ValueError, match="Qubit index '7' is not part of the backend."): + backend.index_to_qubit_name(7) + with pytest.raises(ValueError, match="Qubit name 'Alice' is not part of the backend."): + backend.qubit_name_to_index('Alice') def test_transpile(backend): diff --git a/tests/test_iqm_naive_move_pass.py b/tests/test_iqm_naive_move_pass.py index 89ad573d9..3e75b26b7 100644 --- a/tests/test_iqm_naive_move_pass.py +++ b/tests/test_iqm_naive_move_pass.py @@ -1,6 +1,7 @@ """Testing IQM transpilation. """ +import pytest from qiskit.circuit import QuantumCircuit from qiskit.circuit.library import QuantumVolume from qiskit.circuit.quantumcircuitdata import CircuitInstruction @@ -12,14 +13,16 @@ from .utils import _get_allowed_ops, _is_valid_instruction, get_mocked_backend -def test_transpile_to_IQM_star_semantically_preserving(ndonis_architecture): # pylint: disable=too-many-locals +@pytest.mark.parametrize("n_qubits", list(range(2, 6))) +def test_transpile_to_IQM_star_semantically_preserving( + ndonis_architecture, n_qubits +): # pylint: disable=too-many-locals backend, _client = get_mocked_backend(ndonis_architecture) qubit_registers = _get_qubit_registers(backend) - n_qubits = len(qubit_registers) - for i in range(2, n_qubits + 1): - circuit = QuantumVolume(i, i) + if len(qubit_registers) >= n_qubits: + circuit = QuantumVolume(n_qubits, n_qubits) # Use optimization_level=0 to avoid that the qubits get remapped. - transpiled_circuit = transpile_to_IQM(circuit, backend, optimization_level=0) + transpiled_circuit = transpile_to_IQM(circuit, backend, optimization_level=0, remove_final_rzs=False) transpiled_operator = Operator(transpiled_circuit) # Update the original circuit to have the correct number of qubits and resonators. diff --git a/tests/test_iqm_provider.py b/tests/test_iqm_provider.py index 546769df0..2661d7219 100644 --- a/tests/test_iqm_provider.py +++ b/tests/test_iqm_provider.py @@ -121,8 +121,10 @@ def test_qubit_name_to_index_to_qubit_name(adonis_architecture_shuffled_names): assert all(backend.index_to_qubit_name(idx) == name for idx, name in correct_idx_name_associations) assert all(backend.qubit_name_to_index(name) == idx for idx, name in correct_idx_name_associations) - assert backend.index_to_qubit_name(7) is None - assert backend.qubit_name_to_index('Alice') is None + with pytest.raises(ValueError, match="Qubit index '7' is not part of the backend."): + backend.index_to_qubit_name(7) + with pytest.raises(ValueError, match="Qubit name 'Alice' is not part of the backend."): + backend.qubit_name_to_index('Alice') def test_serialize_circuit_raises_error_for_non_transpiled_circuit(circuit, linear_architecture_3q): diff --git a/tox.ini b/tox.ini index 06c5b4a0e..d7c9e6773 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,6 @@ [tox] minversion = 4.11 -envlist = py39, py310, py311, qiskit-{v0.45,v0.46,v1.0,v1.1} +envlist = py39, py310, py311, qiskit-{v0.45,v0.46,v1.0,v1.1,v1.2} skip_missing_interpreters = True [gh-actions] @@ -28,7 +28,7 @@ commands = python -m pytest --cov iqm.qiskit_iqm --cov-report=term-missing --junitxml=test_report.xml --doctest-modules --pylint --pylint-rcfile=tests/.pylintrc --verbose --strict-markers tests python -m mypy tests -[testenv:qiskit-{v0.45,v0.46,v1.0,v1.1}] +[testenv:qiskit-{v0.45,v0.46,v1.0,v1.1,v1.2}] description = Invoke pytest to run automated tests for all supported Qiskit versions. base_python = py310: python3.10 @@ -39,9 +39,13 @@ deps = v0.46: qiskit-aer >= 0.13.1, < 0.14 v1.0: qiskit >= 1.0, <1.1 v1.1: qiskit >= 1.1, <1.2 + v1.1: qiskit-aer >= 0.15, < 0.16 + v1.2: qiskit >= 1.2, <1.3 + v1.2: qiskit-aer >= 0.15, < 0.16 extras = testing commands = + pip list | grep qiskit python -m pytest --cov iqm.qiskit_iqm --cov-report=term-missing --junitxml=test_report.xml --doctest-modules --pylint --pylint-rcfile=tests/.pylintrc --verbose --strict-markers tests [testenv:format] From 9fb91eee12c41a90ec4159d4f97ce82c6635802c Mon Sep 17 00:00:00 2001 From: Arianne Meijer Date: Thu, 5 Sep 2024 15:15:05 +0300 Subject: [PATCH 02/47] Update version --- CHANGELOG.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index c2fe23fba..5f15d7ec2 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -2,7 +2,7 @@ Changelog ========= -Version 13.13 +Version 13.14 ============= * Refactored :meth:`IQMBackend.create_run_request` to improve user experience when using IQM specific run options. From 1dd4b29a99fa4106e389bfff183a21f0a42ef811 Mon Sep 17 00:00:00 2001 From: Arianne Meijer Date: Thu, 5 Sep 2024 15:36:59 +0300 Subject: [PATCH 03/47] removed unused import --- src/iqm/qiskit_iqm/iqm_transpilation.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/iqm/qiskit_iqm/iqm_transpilation.py b/src/iqm/qiskit_iqm/iqm_transpilation.py index 456e13c41..b30d4df54 100644 --- a/src/iqm/qiskit_iqm/iqm_transpilation.py +++ b/src/iqm/qiskit_iqm/iqm_transpilation.py @@ -21,7 +21,6 @@ from qiskit.transpiler.basepasses import TransformationPass from qiskit.transpiler.passes import BasisTranslator, Optimize1qGatesDecomposition, RemoveBarriers from qiskit.transpiler.passmanager import PassManager -from qiskit.transpiler.preset_passmanagers.plugin import PassManagerStagePlugin class IQMOptimizeSingleQubitGates(TransformationPass): From a22d8db06d06a2bdfb402ee6f8f52cdb15ebb2d9 Mon Sep 17 00:00:00 2001 From: Arianne Meijer Date: Fri, 1 Nov 2024 13:01:09 +0200 Subject: [PATCH 04/47] typo --- tests/move_architecture/test_architecture.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/move_architecture/test_architecture.py b/tests/move_architecture/test_architecture.py index 403cec85e..c0a076970 100644 --- a/tests/move_architecture/test_architecture.py +++ b/tests/move_architecture/test_architecture.py @@ -32,7 +32,7 @@ def test_backend_configuration_new(move_architecture): 'r:1', 'cz:2', 'move:2', - } + ] check_instruction(backend.instructions, 'r', [(1,), (2,), (3,), (4,), (5,), (6,)]) check_instruction(backend.instructions, 'measure', [(1,), (2,), (3,), (4,), (5,), (6,)]) From 5ece8fa99c40cddbb21606a3fb1ada5c78554007 Mon Sep 17 00:00:00 2001 From: Arianne Meijer Date: Fri, 1 Nov 2024 13:05:55 +0200 Subject: [PATCH 05/47] pylint --- src/iqm/qiskit_iqm/iqm_backend.py | 19 ++++++++++--------- src/iqm/qiskit_iqm/iqm_provider.py | 1 - 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/iqm/qiskit_iqm/iqm_backend.py b/src/iqm/qiskit_iqm/iqm_backend.py index e795d02ac..cc143bc04 100644 --- a/src/iqm/qiskit_iqm/iqm_backend.py +++ b/src/iqm/qiskit_iqm/iqm_backend.py @@ -18,7 +18,7 @@ from abc import ABC import itertools import re -from typing import Final, Optional, Union +from typing import Final, Union from uuid import UUID from qiskit.circuit import Parameter, Reset @@ -80,9 +80,10 @@ def _DQA_to_qiskit_target( Returns: A Qiskit Target object representing the given quantum architecture specification. """ + # pylint: disable=unreachable target = Target() fake_target = Target() - raise NotImplementedError("This function is not yet implemented.") + raise NotImplementedError('This function is not yet implemented.') def get_num_or_zero(name: str) -> int: match = re.search(r'(\d+)', name) @@ -97,12 +98,12 @@ def get_num_or_zero(name: str) -> int: # set and the connectivity of the device under use. Thus, we populate the target with None properties. def _create_connections(name: str): """Creates the connection map of allowed loci for this instruction, mapped to None.""" - if is_multi_qubit_instruction(name): - if is_directed_instruction(name): - return {(qb_to_idx[qb1], qb_to_idx[qb2]): None for [qb1, qb2] in operations[name]} - return { - (qb_to_idx[qb1], qb_to_idx[qb2]): None for pair in operations[name] for qb1, qb2 in (pair, pair[::-1]) - } + # if is_multi_qubit_instruction(name): + # if is_directed_instruction(name): + # return {(qb_to_idx[qb1], qb_to_idx[qb2]): None for [qb1, qb2] in operations[name]} + # return { + # (qb_to_idx[qb1], qb_to_idx[qb2]): None for pair in operations[name] for qb1, qb2 in (pair, pair[::-1]) + # } return {(qb_to_idx[qb],): None for [qb] in operations[name]} if 'prx' in operations or 'phased_rx' in operations: @@ -260,4 +261,4 @@ def index_to_qubit_name(self, index: int) -> str: def get_scheduling_stage_plugin(self) -> str: """Return the plugin that should be used for scheduling the circuits on this backend.""" - return "default" + return 'default' diff --git a/src/iqm/qiskit_iqm/iqm_provider.py b/src/iqm/qiskit_iqm/iqm_provider.py index d00ce005d..ee88a07a5 100644 --- a/src/iqm/qiskit_iqm/iqm_provider.py +++ b/src/iqm/qiskit_iqm/iqm_provider.py @@ -128,7 +128,6 @@ def create_run_request( self, run_input: Union[QuantumCircuit, list[QuantumCircuit]], shots: Optional[int] = None, - calibration_set_id: Optional[Union[str, UUID]] = None, circuit_compilation_options: Optional[CircuitCompilationOptions] = None, circuit_callback: Optional[Callable] = None, **unknown_options, From 5ab12d84b71ea512017ebe71f467cdd984e89c82 Mon Sep 17 00:00:00 2001 From: Arianne Meijer Date: Fri, 1 Nov 2024 15:36:40 +0200 Subject: [PATCH 06/47] Fixed DQA integration. Transpiler still broken --- src/iqm/qiskit_iqm/iqm_backend.py | 68 ++++++++++++-------- src/iqm/qiskit_iqm/iqm_naive_move_pass.py | 10 ++- src/iqm/qiskit_iqm/iqm_provider.py | 45 +++++-------- tests/conftest.py | 2 +- tests/move_architecture/test_architecture.py | 14 ++-- tests/test_iqm_backend.py | 10 ++- tests/test_iqm_backend_base.py | 15 +++-- tests/test_iqm_provider.py | 4 +- 8 files changed, 86 insertions(+), 82 deletions(-) diff --git a/src/iqm/qiskit_iqm/iqm_backend.py b/src/iqm/qiskit_iqm/iqm_backend.py index cc143bc04..011ffe80c 100644 --- a/src/iqm/qiskit_iqm/iqm_backend.py +++ b/src/iqm/qiskit_iqm/iqm_backend.py @@ -16,6 +16,7 @@ from __future__ import annotations from abc import ABC +from copy import copy import itertools import re from typing import Final, Union @@ -82,21 +83,24 @@ def _DQA_to_qiskit_target( """ # pylint: disable=unreachable target = Target() - fake_target = Target() - raise NotImplementedError('This function is not yet implemented.') def get_num_or_zero(name: str) -> int: match = re.search(r'(\d+)', name) return int(match.group(1)) if match else 0 - qb_to_idx = {qb: idx for idx, qb in enumerate(sorted(architecture.qubits, key=get_num_or_zero))} - operations = architecture.operations + component_to_idx = { + qb: idx + for idx, qb in enumerate( + sorted(architecture.computational_resonators + architecture.qubits, key=get_num_or_zero) + ) + } + operations = architecture.gates # There is no dedicated direct way of setting just the qubit connectivity and the native gates to the target. # Such info is automatically deduced once all instruction properties are set. Currently, we do not retrieve # any properties from the server, and we are interested only in letting the target know what is the native gate # set and the connectivity of the device under use. Thus, we populate the target with None properties. - def _create_connections(name: str): + def _create_connections(name: str, is_symmetric: bool = False) -> dict[tuple[int, ...], None]: """Creates the connection map of allowed loci for this instruction, mapped to None.""" # if is_multi_qubit_instruction(name): # if is_directed_instruction(name): @@ -104,46 +108,54 @@ def _create_connections(name: str): # return { # (qb_to_idx[qb1], qb_to_idx[qb2]): None for pair in operations[name] for qb1, qb2 in (pair, pair[::-1]) # } - return {(qb_to_idx[qb],): None for [qb] in operations[name]} + gate_info = operations[name] + all_loci = gate_info.implementations[gate_info.default_implementation].loci + connections = {tuple(component_to_idx[locus] for locus in loci): None for loci in all_loci} + if is_symmetric: + # If the gate is symmetric, we need to add the reverse connections as well. + connections.update({tuple(reversed(loci)): None for loci in connections}) + return connections if 'prx' in operations or 'phased_rx' in operations: target.add_instruction( RGate(Parameter('theta'), Parameter('phi')), - _create_connections('prx' if 'prx' in operations else 'phased_rx'), - ) - fake_target.add_instruction( - RGate(Parameter('theta'), Parameter('phi')), - _create_connections('prx' if 'prx' in operations else 'phased_rx'), + _create_connections('prx'), ) - target.add_instruction(IGate(), {(qb_to_idx[qb],): None for qb in architecture.qubits}) - fake_target.add_instruction( - IGate(), {(qb_to_idx[qb],): None for qb in architecture.qubits if not qb.startswith('COMP_R')} + if 'cc_prx' in operations: + # HACK reset gate shares cc_prx loci for now + target.add_instruction(Reset(), _create_connections('cc_prx')) + + target.add_instruction( + IGate(), {(component_to_idx[qb],): None for qb in architecture.computational_resonators + architecture.qubits} ) # Even though CZ is a symmetric gate, we still need to add properties for both directions. This is because # coupling maps in Qiskit are directed graphs and the gate symmetry is not implicitly planted there. It should # be explicitly supplied. This allows Qiskit to have coupling maps with non-symmetric gates like cx. if 'measure' in operations: target.add_instruction(Measure(), _create_connections('measure')) - fake_target.add_instruction(Measure(), _create_connections('measure')) - if 'measurement' in operations: - target.add_instruction(Measure(), _create_connections('measurement')) - fake_target.add_instruction(Measure(), _create_connections('measurement')) + + # Special work for devices with a MoveGate. + fake_target = copy(target) + if 'cz' in operations: + target.add_instruction(CZGate(), _create_connections('cz', True)) if 'move' in operations: target.add_instruction(MoveGate(), _create_connections('move')) if 'cz' in operations: - target.add_instruction(CZGate(), _create_connections('cz')) fake_cz_connections: dict[tuple[int, int], None] = {} + cz_loci = operations['cz'].implementations[operations['cz'].default_implementation].loci + for qb1, qb2 in cz_loci: + if ( + qb1 not in architecture.computational_resonators + and qb2 not in architecture.computational_resonators + ): + fake_cz_connections[(component_to_idx[qb1], component_to_idx[qb2])] = None + fake_cz_connections[(component_to_idx[qb2], component_to_idx[qb1])] = None for qb1, res in operations['move']: for qb2 in [q for q in architecture.qubits if q not in [qb1, res]]: - if [qb2, res] in operations['cz'] or [res, qb2] in operations['cz']: - fake_cz_connections[(qb_to_idx[qb1], qb_to_idx[qb2])] = None - fake_cz_connections[(qb_to_idx[qb2], qb_to_idx[qb1])] = None - fake_target.add_instruction(CZGate(), fake_cz_connections) - else: - if 'cz' in operations: - target.add_instruction(CZGate(), _create_connections('cz')) - fake_target.add_instruction(CZGate(), _create_connections('cz')) - return target, fake_target, qb_to_idx + if [qb2, res] in cz_loci or [res, qb2] in cz_loci: + fake_cz_connections[(component_to_idx[qb1], component_to_idx[qb2])] = None + fake_cz_connections[(component_to_idx[qb2], component_to_idx[qb1])] = None + return target, fake_target, component_to_idx class IQMBackendBase(BackendV2, ABC): diff --git a/src/iqm/qiskit_iqm/iqm_naive_move_pass.py b/src/iqm/qiskit_iqm/iqm_naive_move_pass.py index f7fe3fd55..b658bab66 100644 --- a/src/iqm/qiskit_iqm/iqm_naive_move_pass.py +++ b/src/iqm/qiskit_iqm/iqm_naive_move_pass.py @@ -253,10 +253,14 @@ def transpile_to_IQM( # pylint: disable=too-many-arguments optimize_pass = IQMOptimizeSingleQubitGates(remove_final_rzs, ignore_barriers) passes.append(optimize_pass) - if "move" in backend.architecture.operations.keys(): + if "move" in backend.architecture.gates.keys(): move_pass = IQMNaiveResonatorMoving( - backend.architecture.qubits.index("COMP_R"), - [backend.qubit_name_to_index(q) for q, r in backend.architecture.operations["move"] if r == "COMP_R"], + backend.qubit_name_to_index(backend.architecture.computational_resonators[0]), + [ + backend.qubit_name_to_index(q) + for q, r in backend.architecture.gates["move"] + if r == backend.architecture.computational_resonators[0] + ], backend._physical_target.operation_names, ) passes.append(move_pass) diff --git a/src/iqm/qiskit_iqm/iqm_provider.py b/src/iqm/qiskit_iqm/iqm_provider.py index ee88a07a5..ff7639ae0 100644 --- a/src/iqm/qiskit_iqm/iqm_provider.py +++ b/src/iqm/qiskit_iqm/iqm_provider.py @@ -16,7 +16,6 @@ from __future__ import annotations from collections.abc import Callable -from copy import copy from importlib.metadata import PackageNotFoundError, version from functools import reduce from typing import Optional, Union @@ -74,12 +73,10 @@ def __init__(self, client: IQMClient, *, calibration_set_id: Union[str, UUID, No @classmethod def _default_options(cls) -> Options: - return Options( - shots=1024, - circuit_compilation_options=CircuitCompilationOptions(), - circuit_callback=None, - timeout_seconds=None, - ) + """Qiskit method for defining the default options for running the backend. We don't use them since they would + not be documented here. Instead, we use the keyword arguments of the run method to pass options. + """ + return Options() @property def max_circuits(self) -> Optional[int]: @@ -117,7 +114,7 @@ def run( Job object from which the results can be obtained once the execution has finished. """ - timeout_seconds = options.pop('timeout_seconds', self.options.timeout_seconds) + timeout_seconds = options.pop('timeout_seconds', None) run_request = self.create_run_request(run_input, **options) job_id = self.client.submit_run_request(run_request) job = IQMJob(self, str(job_id), shots=run_request.shots, timeout_seconds=timeout_seconds) @@ -127,7 +124,7 @@ def run( def create_run_request( self, run_input: Union[QuantumCircuit, list[QuantumCircuit]], - shots: Optional[int] = None, + shots: int = 1024, circuit_compilation_options: Optional[CircuitCompilationOptions] = None, circuit_callback: Optional[Callable] = None, **unknown_options, @@ -142,7 +139,8 @@ def create_run_request( Keyword Args: shots (int): Number of repetitions of each circuit, for sampling. Default is 1024. circuit_compilation_options (iqm.iqm_client.models.CircuitCompilationOptions): - Compilation options for the circuits, passed on to :mod:`iqm-client`. + Compilation options for the circuits, passed on to :mod:`iqm-client`. If not provided, the default is + the ``CircuitCompilationOptions`` default. circuit_callback (collections.abc.Callable[[list[QuantumCircuit]], Any]): Callback function that, if provided, will be called for the circuits before sending them to the device. This may be useful in situations when you do not have explicit @@ -164,9 +162,6 @@ def create_run_request( if len(circuits) == 0: raise ValueError('Empty list of circuits submitted for execution.') - if shots is None: - shots = self.options.shots - # Catch old iqm-client options if 'max_circuit_duration_over_t2' in unknown_options or 'heralding_mode' in unknown_options: warnings.warn( @@ -176,28 +171,18 @@ def create_run_request( ) ) if circuit_compilation_options is None: - circuit_compilation_options = CircuitCompilationOptions( - max_circuit_duration_over_t2=unknown_options.pop( - 'max_circuit_duration_over_t2', - self.options.circuit_compilation_options.max_circuit_duration_over_t2, - ), - heralding_mode=unknown_options.pop( - 'heralding_mode', self.options.circuit_compilation_options.heralding_mode - ), - ) + cc_options_kwargs = {} + if 'max_circuit_duration_over_t2' in unknown_options: + cc_options_kwargs['max_circuit_duration_over_t2'] = unknown_options.pop('max_circuit_duration_over_t2') + if 'heralding_mode' in unknown_options: + cc_options_kwargs['heralding_mode'] = unknown_options.pop('heralding_mode') + circuit_compilation_options = CircuitCompilationOptions(**cc_options_kwargs) if unknown_options: warnings.warn(f'Unknown backend option(s): {unknown_options}') - # merge given options with default options and get resulting values - merged_options = copy(self.options) - merged_options.update_options(**dict(unknown_options)) - shots = merged_options['shots'] - if circuit_callback: circuit_callback(circuits) - elif self.options.circuit_callback: - self.options.circuit_callback(circuits) circuits_serialized: list[Circuit] = [self.serialize_circuit(circuit) for circuit in circuits] used_physical_qubit_indices: set[int] = reduce( @@ -219,7 +204,7 @@ def create_run_request( qubit_mapping=qubit_mapping, calibration_set_id=self._calibration_set_id, shots=shots, - options=merged_options['circuit_compilation_options'], + options=circuit_compilation_options, ) except CircuitValidationError as e: raise CircuitValidationError( diff --git a/tests/conftest.py b/tests/conftest.py index 5faa73c08..9fcdb6e9a 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -198,7 +198,7 @@ def move_architecture(): @pytest.fixture def adonis_coupling_map(): - return {(0, 2), (2, 0), (1, 2), (2, 1), (2, 3), (3, 2), (2, 4), (4, 2)} + return {(0, 2), (1, 2), (3, 2), (4, 2), (2, 0), (2, 1), (2, 3), (2, 4)} @pytest.fixture diff --git a/tests/move_architecture/test_architecture.py b/tests/move_architecture/test_architecture.py index c0a076970..f5662d2ce 100644 --- a/tests/move_architecture/test_architecture.py +++ b/tests/move_architecture/test_architecture.py @@ -24,15 +24,15 @@ def test_backend_configuration_new(move_architecture): """Check that the extended architecture is configured correctly to the Qiskit backend.""" assert move_architecture is not None backend, _client = get_mocked_backend(move_architecture) - assert backend.target.physical_qubits == [0, 1, 2, 3, 4, 5, 6] + assert set(backend.target.physical_qubits) == {0, 1, 2, 3, 4, 5, 6} assert set(backend.target.operation_names) == {'r', 'id', 'cz', 'measure', 'move'} - assert [f'{o.name}:{o.num_qubits}' for o in backend.target.operations] == [ + assert {f'{o.name}:{o.num_qubits}' for o in backend.target.operations} == { 'measure:1', 'id:1', 'r:1', 'cz:2', 'move:2', - ] + } check_instruction(backend.instructions, 'r', [(1,), (2,), (3,), (4,), (5,), (6,)]) check_instruction(backend.instructions, 'measure', [(1,), (2,), (3,), (4,), (5,), (6,)]) @@ -49,13 +49,13 @@ def test_backend_configuration_adonis(adonis_architecture): backend, _client = get_mocked_backend(adonis_architecture) assert backend.target.physical_qubits == [0, 1, 2, 3, 4] assert set(backend.target.operation_names) == {'r', 'id', 'cz', 'measure', 'reset'} - assert [f'{o.name}:{o.num_qubits}' for o in backend.target.operations] == [ + assert {f'{o.name}:{o.num_qubits}' for o in backend.target.operations} == { 'measure:1', 'id:1', 'r:1', 'cz:2', 'reset:1', - ] + } check_instruction(backend.instructions, 'r', [(0,), (1,), (2,), (3,), (4,)]) check_instruction(backend.instructions, 'measure', [(0,), (1,), (2,), (3,), (4,)]) check_instruction(backend.instructions, 'id', [(0,), (1,), (2,), (3,), (4,)]) @@ -68,5 +68,5 @@ def check_instruction( expected_connections: list[Union[tuple[int], tuple[int, int]]], ): """Checks that the given instruction is defined for the expected qubits (directed).""" - target_qubits = [k for (i, k) in instructions if i.name == name] - assert target_qubits == expected_connections + target_qubits = {k for (i, k) in instructions if i.name == name} + assert target_qubits == set(expected_connections) diff --git a/tests/test_iqm_backend.py b/tests/test_iqm_backend.py index bd7130d6e..d097ad1b2 100644 --- a/tests/test_iqm_backend.py +++ b/tests/test_iqm_backend.py @@ -81,11 +81,8 @@ def run_request(): def test_default_options(backend): - assert backend.options.shots == 1024 - for k, v in backend.options.circuit_compilation_options.__dict__.items(): - assert v == CircuitCompilationOptions().__dict__[k] - assert backend.options.circuit_compilation_options - assert backend.options.circuit_callback is None + """Test that there are no default options set. The user specifies defaults through the function calls.""" + assert len(backend.options) == 0 def test_backend_name(backend): @@ -460,8 +457,9 @@ def test_run_uses_heralding_mode_none_by_default( ): circuit.measure(0, 0) circuit_ser = backend.serialize_circuit(circuit) + default_compilation_options = CircuitCompilationOptions() kwargs = create_run_request_default_kwargs | { - 'options': backend.options.circuit_compilation_options, + 'options': default_compilation_options, 'qubit_mapping': {'0': 'QB1'}, } when(backend.client).create_run_request([circuit_ser], **kwargs).thenReturn(run_request) diff --git a/tests/test_iqm_backend_base.py b/tests/test_iqm_backend_base.py index daf7a84f3..b4e551edf 100644 --- a/tests/test_iqm_backend_base.py +++ b/tests/test_iqm_backend_base.py @@ -36,8 +36,7 @@ def _default_options(cls) -> Options: def max_circuits(self) -> Optional[int]: return None - def run(self, run_input, **options): - ... + def run(self, run_input, **options): ... @pytest.fixture @@ -52,8 +51,10 @@ def test_qubit_name_to_index_to_qubit_name(adonis_shuffled_names_architecture): assert all(backend.index_to_qubit_name(idx) == name for idx, name in correct_idx_name_associations) assert all(backend.qubit_name_to_index(name) == idx for idx, name in correct_idx_name_associations) - assert backend.index_to_qubit_name(7) is None - assert backend.qubit_name_to_index('Alice') is None + with pytest.raises(ValueError, match="Qubit index '7' is not part of the backend."): + backend.index_to_qubit_name(7) + with pytest.raises(ValueError, match="Qubit name 'Alice' is not part of the backend."): + backend.qubit_name_to_index('Alice') def test_transpile(backend): @@ -64,7 +65,11 @@ def test_transpile(backend): circuit.cx(2, 0) circuit_transpiled = transpile(circuit, backend=backend) - cmap = backend.coupling_map.get_edges() + print(backend.target) + assert backend.target is not None + cmap = backend.target.build_coupling_map() + assert cmap is not None + cmap = cmap.get_edges() for instr in circuit_transpiled.data: instruction = instr.operation qubits = instr.qubits diff --git a/tests/test_iqm_provider.py b/tests/test_iqm_provider.py index cb8c5fe55..b5491dfa3 100644 --- a/tests/test_iqm_provider.py +++ b/tests/test_iqm_provider.py @@ -52,7 +52,7 @@ def test_get_backend(linear_3q_architecture): assert isinstance(backend, IQMBackend) assert backend.client._api.iqm_server_url == url assert backend.num_qubits == 3 - assert set(backend.coupling_map.get_edges()) == {(0, 1), (1, 0), (1, 2), (2, 1)} + assert set(backend.coupling_map.get_edges()) == {(0, 1), (1, 2), (1, 0), (2, 1)} assert backend._calibration_set_id == linear_3q_architecture.calibration_set_id @@ -76,7 +76,7 @@ def test_get_facade_backend(adonis_architecture, adonis_coupling_map): assert isinstance(backend, IQMFacadeBackend) assert backend.client._api.iqm_server_url == url assert backend.num_qubits == 5 - assert set(backend.coupling_map.get_edges()) == adonis_coupling_map + assert set(backend.coupling_map.get_edges()) == set(adonis_coupling_map) def test_get_facade_backend_raises_error_non_matching_architecture(linear_3q_architecture): From 422eb93411725266a9bd8afb8637f14b241bddb6 Mon Sep 17 00:00:00 2001 From: Arianne Meijer Date: Fri, 1 Nov 2024 15:42:52 +0200 Subject: [PATCH 07/47] formatting --- tests/test_iqm_backend_base.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/test_iqm_backend_base.py b/tests/test_iqm_backend_base.py index b4e551edf..751beac5f 100644 --- a/tests/test_iqm_backend_base.py +++ b/tests/test_iqm_backend_base.py @@ -36,7 +36,8 @@ def _default_options(cls) -> Options: def max_circuits(self) -> Optional[int]: return None - def run(self, run_input, **options): ... + def run(self, run_input, **options): + ... @pytest.fixture From 1460a2039e396c93204b4d144bf46a8d7d87a4a1 Mon Sep 17 00:00:00 2001 From: Arianne Meijer Date: Fri, 1 Nov 2024 18:06:06 +0200 Subject: [PATCH 08/47] Buggy Implementation of the foundation of the transpiler integration. --- pyproject.toml | 3 + src/iqm/qiskit_iqm/__init__.py | 1 + src/iqm/qiskit_iqm/iqm_backend.py | 231 ++++++++----------- src/iqm/qiskit_iqm/iqm_naive_move_pass.py | 186 +++------------ src/iqm/qiskit_iqm/iqm_provider.py | 7 + src/iqm/qiskit_iqm/iqm_transpilation.py | 4 +- src/iqm/qiskit_iqm/transpiler_plugins.py | 37 +++ tests/fake_backends/test_fake_deneb.py | 1 + tests/fake_backends/test_iqm_fake_backend.py | 3 + 9 files changed, 174 insertions(+), 299 deletions(-) create mode 100644 src/iqm/qiskit_iqm/transpiler_plugins.py diff --git a/pyproject.toml b/pyproject.toml index f5811660a..83e64c5dc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -158,3 +158,6 @@ ignore-imports = true [tool.pylint.string] check-quote-consistency = true + +[project.entry-points."qiskit.transpiler.scheduling"] +move_routing = "iqm.qiskit_iqm:MoveGateRoutingPlugin" diff --git a/src/iqm/qiskit_iqm/__init__.py b/src/iqm/qiskit_iqm/__init__.py index 3cccbe0a1..0d715dfa2 100644 --- a/src/iqm/qiskit_iqm/__init__.py +++ b/src/iqm/qiskit_iqm/__init__.py @@ -26,6 +26,7 @@ from iqm.qiskit_iqm.iqm_provider import IQMBackend, IQMProvider, __version__ from iqm.qiskit_iqm.iqm_transpilation import IQMOptimizeSingleQubitGates, optimize_single_qubit_gates from iqm.qiskit_iqm.move_gate import MoveGate +from iqm.qiskit_iqm.transpiler_plugins import MoveGateRoutingPlugin if qiskit_version < "1.0.0": warn( diff --git a/src/iqm/qiskit_iqm/iqm_backend.py b/src/iqm/qiskit_iqm/iqm_backend.py index 011ffe80c..5978b2188 100644 --- a/src/iqm/qiskit_iqm/iqm_backend.py +++ b/src/iqm/qiskit_iqm/iqm_backend.py @@ -16,16 +16,14 @@ from __future__ import annotations from abc import ABC -from copy import copy -import itertools -import re +from copy import deepcopy from typing import Final, Union from uuid import UUID from qiskit.circuit import Parameter, Reset from qiskit.circuit.library import CZGate, IGate, Measure, RGate from qiskit.providers import BackendV2 -from qiskit.transpiler import InstructionProperties, Target +from qiskit.transpiler import Target from iqm.iqm_client import ( DynamicQuantumArchitecture, @@ -70,94 +68,6 @@ def _dqa_from_static_architecture(sqa: QuantumArchitectureSpecification) -> Dyna ) -def _DQA_to_qiskit_target( - architecture: DynamicQuantumArchitecture, -) -> tuple[Target, Target, dict[str, int]]: - """Converts a QuantumArchitectureSpecification object to a Qiskit Target object. - - Args: - architecture: The quantum architecture specification to convert. - - Returns: - A Qiskit Target object representing the given quantum architecture specification. - """ - # pylint: disable=unreachable - target = Target() - - def get_num_or_zero(name: str) -> int: - match = re.search(r'(\d+)', name) - return int(match.group(1)) if match else 0 - - component_to_idx = { - qb: idx - for idx, qb in enumerate( - sorted(architecture.computational_resonators + architecture.qubits, key=get_num_or_zero) - ) - } - operations = architecture.gates - - # There is no dedicated direct way of setting just the qubit connectivity and the native gates to the target. - # Such info is automatically deduced once all instruction properties are set. Currently, we do not retrieve - # any properties from the server, and we are interested only in letting the target know what is the native gate - # set and the connectivity of the device under use. Thus, we populate the target with None properties. - def _create_connections(name: str, is_symmetric: bool = False) -> dict[tuple[int, ...], None]: - """Creates the connection map of allowed loci for this instruction, mapped to None.""" - # if is_multi_qubit_instruction(name): - # if is_directed_instruction(name): - # return {(qb_to_idx[qb1], qb_to_idx[qb2]): None for [qb1, qb2] in operations[name]} - # return { - # (qb_to_idx[qb1], qb_to_idx[qb2]): None for pair in operations[name] for qb1, qb2 in (pair, pair[::-1]) - # } - gate_info = operations[name] - all_loci = gate_info.implementations[gate_info.default_implementation].loci - connections = {tuple(component_to_idx[locus] for locus in loci): None for loci in all_loci} - if is_symmetric: - # If the gate is symmetric, we need to add the reverse connections as well. - connections.update({tuple(reversed(loci)): None for loci in connections}) - return connections - - if 'prx' in operations or 'phased_rx' in operations: - target.add_instruction( - RGate(Parameter('theta'), Parameter('phi')), - _create_connections('prx'), - ) - if 'cc_prx' in operations: - # HACK reset gate shares cc_prx loci for now - target.add_instruction(Reset(), _create_connections('cc_prx')) - - target.add_instruction( - IGate(), {(component_to_idx[qb],): None for qb in architecture.computational_resonators + architecture.qubits} - ) - # Even though CZ is a symmetric gate, we still need to add properties for both directions. This is because - # coupling maps in Qiskit are directed graphs and the gate symmetry is not implicitly planted there. It should - # be explicitly supplied. This allows Qiskit to have coupling maps with non-symmetric gates like cx. - if 'measure' in operations: - target.add_instruction(Measure(), _create_connections('measure')) - - # Special work for devices with a MoveGate. - fake_target = copy(target) - if 'cz' in operations: - target.add_instruction(CZGate(), _create_connections('cz', True)) - if 'move' in operations: - target.add_instruction(MoveGate(), _create_connections('move')) - if 'cz' in operations: - fake_cz_connections: dict[tuple[int, int], None] = {} - cz_loci = operations['cz'].implementations[operations['cz'].default_implementation].loci - for qb1, qb2 in cz_loci: - if ( - qb1 not in architecture.computational_resonators - and qb2 not in architecture.computational_resonators - ): - fake_cz_connections[(component_to_idx[qb1], component_to_idx[qb2])] = None - fake_cz_connections[(component_to_idx[qb2], component_to_idx[qb1])] = None - for qb1, res in operations['move']: - for qb2 in [q for q in architecture.qubits if q not in [qb1, res]]: - if [qb2, res] in cz_loci or [res, qb2] in cz_loci: - fake_cz_connections[(component_to_idx[qb1], component_to_idx[qb2])] = None - fake_cz_connections[(component_to_idx[qb2], component_to_idx[qb1])] = None - return target, fake_target, component_to_idx - - class IQMBackendBase(BackendV2, ABC): """Abstract base class for various IQM-specific backends. @@ -181,60 +91,16 @@ def __init__( # Qiskit uses integer indices to refer to qubits, so we need to map component names to indices. qb_to_idx = {qb: idx for idx, qb in enumerate(arch.components)} - operations = {gate_name: gate_info.loci for gate_name, gate_info in arch.gates.items()} - target = Target() - - def _create_properties( - op_name: str, symmetric: bool = False - ) -> dict[tuple[int, ...], InstructionProperties | None]: - """Creates the Qiskit instruction properties dictionary for the given IQM native operation. - - Currently we do not provide any actual properties for the operation other than the - allowed loci. - """ - loci = operations[op_name] - if symmetric: - # For symmetric gates, construct all the valid loci for Qiskit. - # Coupling maps in Qiskit are directed graphs, and gate symmetry is provided explicitly. - loci = tuple(permuted_locus for locus in loci for permuted_locus in itertools.permutations(locus)) - return {tuple(qb_to_idx[qb] for qb in locus): None for locus in loci} - if 'measure' in operations: - target.add_instruction(Measure(), _create_properties('measure')) - target.add_instruction( - IGate(), - {(qb_to_idx[qb],): None for qb in arch.components}, - ) - if 'prx' in operations: - target.add_instruction(RGate(Parameter('theta'), Parameter('phi')), _create_properties('prx')) - if 'cz' in operations: - target.add_instruction(CZGate(), _create_properties('cz', symmetric=True)) - if 'move' in operations: - target.add_instruction(MoveGate(), _create_properties('move')) - if 'cc_prx' in operations: - # HACK reset gate shares cc_prx loci for now - target.add_instruction(Reset(), _create_properties('cc_prx')) - - self._physical_target, self._fake_target, self._qb_to_idx = _DQA_to_qiskit_target(arch) + self._target = IQMStarTarget(arch, qb_to_idx) self._qb_to_idx = qb_to_idx self._idx_to_qb = {v: k for k, v in qb_to_idx.items()} self.name = 'IQMBackend' + self._coupling_map = self.target.build_coupling_map() @property def target(self) -> Target: - return self._physical_target - - @property - def fake_target(self) -> Target: - """A target representing the backend where resonators are abstracted away. If the backend does not support - resonators, this target is the same as the `target` property, but different instances. - """ - return self._fake_target - - @property - def physical_target(self) -> Target: - """A target providing an accurate representation of the backend.""" - return self._physical_target + return self._target @property def physical_qubits(self) -> list[str]: @@ -273,4 +139,89 @@ def index_to_qubit_name(self, index: int) -> str: def get_scheduling_stage_plugin(self) -> str: """Return the plugin that should be used for scheduling the circuits on this backend.""" - return 'default' + return 'move_routing' + + +class IQMStarTarget(Target): + """A target representing an IQM backend with resonators. + + This target is used to represent the physical layout of the backend, including the resonators as well as a fake + coupling map to . + """ + + def __init__(self, architecture: DynamicQuantumArchitecture, component_to_idx: dict[str, int]): + super().__init__() + self.iqm_dynamic_architecture = architecture + self.iqm_component_to_idx = component_to_idx + self.iqm_idx_to_component = {v: k for k, v in component_to_idx.items()} + self._iqm_create_instructions(architecture, component_to_idx) + + def _iqm_create_instructions(self, architecture: DynamicQuantumArchitecture, component_to_idx: dict[str, int]): + """Converts a QuantumArchitectureSpecification object to a Qiskit Target object. + + Args: + architecture: The quantum architecture specification to convert. + + Returns: + A Qiskit Target object representing the given quantum architecture specification. + """ + operations = architecture.gates + + # There is no dedicated direct way of setting just the qubit connectivity and the native gates to the target. + # Such info is automatically deduced once all instruction properties are set. Currently, we do not retrieve + # any properties from the server, and we are interested only in letting the target know what is the native gate + # set and the connectivity of the device under use. Thus, we populate the target with None properties. + def _create_connections(name: str, is_symmetric: bool = False) -> dict[tuple[int, ...], None]: + """Creates the connection map of allowed loci for this instruction, mapped to None.""" + gate_info = operations[name] + all_loci = gate_info.implementations[gate_info.default_implementation].loci + connections = {tuple(component_to_idx[locus] for locus in loci): None for loci in all_loci} + if is_symmetric: + # If the gate is symmetric, we need to add the reverse connections as well. + connections.update({tuple(reversed(loci)): None for loci in connections}) + return connections + + if 'prx' in operations or 'phased_rx' in operations: + self.add_instruction( + RGate(Parameter('theta'), Parameter('phi')), + _create_connections('prx'), + ) + if 'cc_prx' in operations: + # HACK reset gate shares cc_prx loci for now + self.add_instruction(Reset(), _create_connections('cc_prx')) + + self.add_instruction( + IGate(), + {(component_to_idx[qb],): None for qb in architecture.computational_resonators + architecture.qubits}, + ) + # Even though CZ is a symmetric gate, we still need to add properties for both directions. This is because + # coupling maps in Qiskit are directed graphs and the gate symmetry is not implicitly planted there. It should + # be explicitly supplied. This allows Qiskit to have coupling maps with non-symmetric gates like cx. + if 'measure' in operations: + self.add_instruction(Measure(), _create_connections('measure')) + + # Special work for devices with a MoveGate. + real_target = deepcopy(self) + if 'cz' in operations: + real_target.add_instruction(CZGate(), _create_connections('cz', True)) + if 'move' in operations: + real_target.add_instruction(MoveGate(), _create_connections('move')) + if 'cz' in operations: + fake_cz_connections: dict[tuple[int, int], None] = {} + cz_loci = operations['cz'].implementations[operations['cz'].default_implementation].loci + for qb1, qb2 in cz_loci: + if ( + qb1 not in architecture.computational_resonators + and qb2 not in architecture.computational_resonators + ): + fake_cz_connections[(component_to_idx[qb1], component_to_idx[qb2])] = None + fake_cz_connections[(component_to_idx[qb2], component_to_idx[qb1])] = None + for qb1, res in operations['move']: + for qb2 in [q for q in architecture.qubits if q not in [qb1, res]]: + if [qb2, res] in cz_loci or [res, qb2] in cz_loci: + fake_cz_connections[(component_to_idx[qb1], component_to_idx[qb2])] = None + fake_cz_connections[(component_to_idx[qb2], component_to_idx[qb1])] = None + self.add_instruction(CZGate(), fake_cz_connections) + else: + self.add_instruction(CZGate(), _create_connections('cz', True)) + self.real_target = real_target diff --git a/src/iqm/qiskit_iqm/iqm_naive_move_pass.py b/src/iqm/qiskit_iqm/iqm_naive_move_pass.py index b658bab66..ef1b9ff59 100644 --- a/src/iqm/qiskit_iqm/iqm_naive_move_pass.py +++ b/src/iqm/qiskit_iqm/iqm_naive_move_pass.py @@ -12,19 +12,23 @@ # See the License for the specific language governing permissions and # limitations under the License. """Naive transpilation for the IQM Star architecture.""" +from typing import Optional from qiskit import QuantumCircuit, transpile from qiskit.circuit import QuantumRegister, Qubit -from qiskit.dagcircuit import DAGCircuit, DAGOpNode +from qiskit.converters import circuit_to_dag, dag_to_circuit +from qiskit.dagcircuit import DAGCircuit from qiskit.transpiler import Layout, TranspileLayout from qiskit.transpiler.basepasses import TransformationPass -from qiskit.transpiler.exceptions import TranspilerError from qiskit.transpiler.passmanager import PassManager -from .iqm_backend import IQMBackendBase +from iqm.iqm_client import Circuit as IQMClientCircuit +from iqm.iqm_client.transpile import ExistingMoveHandlingOptions, transpile_insert_moves + +from .iqm_backend import IQMBackendBase, IQMStarTarget from .iqm_circuit import IQMCircuit +from .iqm_provider import _deserialize_instructions, _serialize_instructions from .iqm_transpilation import IQMOptimizeSingleQubitGates -from .move_gate import MoveGate class IQMNaiveResonatorMoving(TransformationPass): @@ -39,7 +43,12 @@ class IQMNaiveResonatorMoving(TransformationPass): Additionally, it assumes that no single qubit gates are allowed on the resonator. """ - def __init__(self, resonator_register: int, move_qubits: list[int], gate_set: list[str]): + def __init__( + self, + target: IQMStarTarget, + gate_set: list[str], + existing_moves_handling: Optional[ExistingMoveHandlingOptions] = None, + ): """WIP Naive transpilation pass for resonator moving Args: @@ -48,10 +57,9 @@ def __init__(self, resonator_register: int, move_qubits: list[int], gate_set: li gate_set (list[str]): Which gates are allowed by the target backend. """ super().__init__() - self.resonator_register = resonator_register - self.current_resonator_state_location = resonator_register - self.move_qubits = move_qubits + self.target = target self.gate_set = gate_set + self.existing_moves_handling = existing_moves_handling def run(self, dag: DAGCircuit): # pylint: disable=too-many-branches """Run the IQMNaiveResonatorMoving pass on `dag`. @@ -65,146 +73,18 @@ def run(self, dag: DAGCircuit): # pylint: disable=too-many-branches Raises: TranspilerError: if the layout are not compatible with the DAG, or if the input gate set is incorrect. """ - new_dag = dag.copy_empty_like() - # Check for sensible inputs - if len(dag.qregs) != 1 or dag.qregs.get("q", None) is None: - raise TranspilerError("IQMNaiveResonatorMoving runs on physical circuits only") - - # Create a trivial layout - canonical_register = dag.qregs["q"] - trivial_layout = Layout.generate_trivial_layout(canonical_register) - current_layout = trivial_layout.copy() - - for layer in dag.serial_layers(): - subdag = layer["graph"] - if len(layer["partition"]) > 0: - qubits = layer["partition"][0] - else: - new_dag.compose(subdag) - continue # No qubit gate (e.g. Barrier) - - if sum(subdag.count_ops().values()) > 1: - raise TranspilerError( - """The DAGCircuit is not flattened enough for this transpiler pass. - It needs to be processed by another pass first.""" - ) - if list(subdag.count_ops().keys())[0] not in self.gate_set: - raise TranspilerError( - """Encountered an incompatible gate in the DAGCircuit. - Please transpile to the correct gate set first.""" - ) - - if len(qubits) == 1: # Single qubit gate - # Check if the qubit is not in the resonator - if self.current_resonator_state_location == dag.qubits.index(qubits[0]): - # Unload the current qubit from the resonator - new_dag.compose( - self._move_resonator(dag.qubits.index(qubits[0]), canonical_register, current_layout) - ) - new_dag.compose(subdag) - elif len(qubits) == 2: # Two qubit gate - physical_q0 = current_layout[qubits[0]] - physical_q1 = current_layout[qubits[1]] - if self.current_resonator_state_location in (physical_q0, physical_q1): - # The resonator is already loaded with the correct qubit data - pass - else: - swap_layer = DAGCircuit() - swap_layer.add_qreg(canonical_register) - if self.current_resonator_state_location != self.resonator_register: - # Unload the current qubit from the resonator - new_dag.compose( - self._move_resonator( - self.current_resonator_state_location, canonical_register, current_layout - ) - ) - # Load the new qubit to the resonator - if physical_q0 in self.move_qubits and physical_q1 in self.move_qubits: - # We can choose, let's select the better one by seeing which one is used most. - chosen_qubit = self._lookahead_first_qubit_used(dag, subdag) - new_qubit_to_load = current_layout[chosen_qubit] - elif physical_q0 in self.move_qubits: - new_qubit_to_load = physical_q0 - elif physical_q1 in self.move_qubits: - new_qubit_to_load = physical_q1 - else: - raise TranspilerError( - """Two qubit gate between qubits that are not allowed to move. - Please route the circuit first.""" - ) - new_dag.compose(self._move_resonator(new_qubit_to_load, canonical_register, current_layout)) - # Add the gate to the circuit - order = list(range(len(canonical_register))) - order[self.resonator_register] = self.current_resonator_state_location - order[self.current_resonator_state_location] = self.resonator_register - new_dag.compose(subdag, qubits=order) - else: - raise TranspilerError( - """Three qubit gates are not allowed as input for this pass. - Please use a different transpiler pass to decompose first.""" - ) - - new_dag.compose( - self._move_resonator( - self.current_resonator_state_location, - canonical_register, - current_layout, - ) + circuit = dag_to_circuit(dag) + iqm_json = IQMClientCircuit( + name="Transpiling Circuit", + instructions=tuple(_serialize_instructions(circuit, self.target.iqm_idx_to_component)), + ) + routed_json = transpile_insert_moves( + iqm_json, self.target.iqm_dynamic_architecture, self.existing_moves_handling ) + routed_circuit = _deserialize_instructions(list(routed_json.instructions), self.target.iqm_component_to_idx) + new_dag = circuit_to_dag(routed_circuit) return new_dag - def _lookahead_first_qubit_used(self, full_dag: DAGCircuit, current_layer: DAGCircuit) -> Qubit: - """Lookahead function to see which qubit will be used first again for a CZ gate. - - Args: - full_dag (DAGCircuit): The DAG representing the circuit - current_layer (DAGCircuit): The DAG representing the current operator - - Returns: - Qubit: Which qubit is recommended to move because it will be used first. - """ - nodes = [n for n in current_layer.nodes() if isinstance(n, DAGOpNode)] - current_opnode = nodes[0] - qb1, qb2 = current_opnode.qargs - next_ops = [ - n for n, _ in full_dag.bfs_successors(current_opnode) if isinstance(n, DAGOpNode) and n.name == "cz" - ] - # Check which qubit will be used next first - for qb1_used, qb2_used in zip([qb1 in n.qargs for n in next_ops], [qb2 in n.qargs for n in next_ops]): - if qb1_used and not qb2_used: - return qb1 - if qb2_used and not qb1_used: - return qb2 - return qb1 - - def _move_resonator(self, qubit: int, canonical_register: QuantumRegister, current_layout: Layout): - """Logic for creating the DAG for swapping a qubit in and out of the resonator. - - Args: - qubit (int): The qubit to swap in or out. The returning DAG is empty if the qubit is the resonator. - canonical_register (QuantumRegister): The qubit register to initialize the DAG - current_layout (Layout): The current qubit layout to map the qubit index to a Qiskit Qubit object. - - Returns: - DAGCircuit: A DAG storing the MoveGate logic to be added into the circuit by this TranspilerPass. - """ - swap_layer = DAGCircuit() - swap_layer.add_qreg(canonical_register) - if qubit != self.resonator_register: - swap_layer.apply_operation_back( - MoveGate(), - qargs=(current_layout[qubit], current_layout[self.resonator_register]), - cargs=(), - check=False, - ) - if self.current_resonator_state_location == self.resonator_register: - # We just loaded the qubit into the register - self.current_resonator_state_location = qubit - else: - # We just unloaded the qubit from the register - self.current_resonator_state_location = self.resonator_register - return swap_layer - def transpile_to_IQM( # pylint: disable=too-many-arguments circuit: QuantumCircuit, @@ -230,7 +110,7 @@ def transpile_to_IQM( # pylint: disable=too-many-arguments Returns: QuantumCircuit: The transpiled circuit ready for running on the backend. """ - circuit_with_resonator = IQMCircuit(backend.fake_target.num_qubits) + circuit_with_resonator = IQMCircuit(backend.target.num_qubits) circuit_with_resonator.add_bits(circuit.clbits) qubit_indices = [backend.qubit_name_to_index(qb) for qb in backend.physical_qubits if not qb.startswith("COMP_R")] circuit_with_resonator.append( @@ -242,8 +122,8 @@ def transpile_to_IQM( # pylint: disable=too-many-arguments # Transpile the circuit using the fake target without resonators simple_transpile = transpile( circuit_with_resonator, - target=backend.fake_target, - basis_gates=backend.fake_target.operation_names, + target=backend.target, + basis_gates=backend.target.operation_names, **qiskit_transpiler_qwargs, ) @@ -254,15 +134,7 @@ def transpile_to_IQM( # pylint: disable=too-many-arguments passes.append(optimize_pass) if "move" in backend.architecture.gates.keys(): - move_pass = IQMNaiveResonatorMoving( - backend.qubit_name_to_index(backend.architecture.computational_resonators[0]), - [ - backend.qubit_name_to_index(q) - for q, r in backend.architecture.gates["move"] - if r == backend.architecture.computational_resonators[0] - ], - backend._physical_target.operation_names, - ) + move_pass = IQMNaiveResonatorMoving(backend.target, backend.target.operation_names) passes.append(move_pass) # circuit_with_resonator = add_resonators_to_circuit(simple_transpile, backend) diff --git a/src/iqm/qiskit_iqm/iqm_provider.py b/src/iqm/qiskit_iqm/iqm_provider.py index 7abdbb3d9..b7e3d2134 100644 --- a/src/iqm/qiskit_iqm/iqm_provider.py +++ b/src/iqm/qiskit_iqm/iqm_provider.py @@ -389,6 +389,13 @@ def _serialize_instructions( return instructions +def _deserialize_instructions(instructions: list[Instruction], qubit_name_to_index: dict[str, int]) -> QuantumCircuit: + circuit = QuantumCircuit(max(qubit_name_to_index.values()) + 1) + for _ in instructions: + pass # TODO implement deserialization + return circuit + + class IQMFacadeBackend(IQMBackend): """Facade backend for mimicking the execution of quantum circuits on IQM quantum computers. Allows to submit a circuit to the IQM server, and if the execution was successful, performs a simulation with a respective IQM noise diff --git a/src/iqm/qiskit_iqm/iqm_transpilation.py b/src/iqm/qiskit_iqm/iqm_transpilation.py index 52b678bb3..85161491f 100644 --- a/src/iqm/qiskit_iqm/iqm_transpilation.py +++ b/src/iqm/qiskit_iqm/iqm_transpilation.py @@ -46,8 +46,8 @@ class IQMOptimizeSingleQubitGates(TransformationPass): def __init__(self, drop_final_rz: bool = False, ignore_barriers: bool = False): super().__init__() - self._basis = ['r', 'cz'] - self._intermediate_basis = ['u', 'cz'] + self._basis = ['r', 'cz', 'move'] + self._intermediate_basis = ['u', 'cz', 'move'] self._drop_final_rz = drop_final_rz self._ignore_barriers = ignore_barriers diff --git a/src/iqm/qiskit_iqm/transpiler_plugins.py b/src/iqm/qiskit_iqm/transpiler_plugins.py new file mode 100644 index 000000000..a603c9f79 --- /dev/null +++ b/src/iqm/qiskit_iqm/transpiler_plugins.py @@ -0,0 +1,37 @@ +# Copyright 2024 Qiskit on IQM developers +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Collection of Qiskit transpiler plugins for native use of specialized transpiler passes by our devices.""" + +from qiskit.transpiler.passmanager import PassManager +from qiskit.transpiler.preset_passmanagers.builtin_plugins import PassManagerStagePlugin + +from iqm.qiskit_iqm.iqm_backend import IQMStarTarget +from iqm.qiskit_iqm.iqm_naive_move_pass import IQMNaiveResonatorMoving +from iqm.qiskit_iqm.iqm_transpilation import IQMOptimizeSingleQubitGates + + +class MoveGateRoutingPlugin(PassManagerStagePlugin): + """Plugin class for IQM single qubit gate optimization and MoveGate routing as a scheduling stage.""" + + def pass_manager(self, pass_manager_config, optimization_level=None) -> PassManager: + """Build scheduling stage PassManager""" + + scheduling = PassManager() + scheduling.append(IQMOptimizeSingleQubitGates(drop_final_rz=True, ignore_barriers=False)) + # TODO Update the IQMNaiveResonatorMoving to use the IQMStarTarget and the transpiler_insert_moves function + if isinstance(pass_manager_config.target, IQMStarTarget): + scheduling.append( + IQMNaiveResonatorMoving(target=pass_manager_config.target, gate_set=pass_manager_config.basis_gates) + ) + return scheduling diff --git a/tests/fake_backends/test_fake_deneb.py b/tests/fake_backends/test_fake_deneb.py index 5b80f2fff..e33ddcb33 100644 --- a/tests/fake_backends/test_fake_deneb.py +++ b/tests/fake_backends/test_fake_deneb.py @@ -32,6 +32,7 @@ def test_iqm_fake_deneb(): def test_iqm_fake_deneb_connectivity(deneb_coupling_map): backend = IQMFakeDeneb() + print(backend.coupling_map) assert set(backend.coupling_map.get_edges()) == deneb_coupling_map diff --git a/tests/fake_backends/test_iqm_fake_backend.py b/tests/fake_backends/test_iqm_fake_backend.py index 3385bb4c4..fbda80138 100644 --- a/tests/fake_backends/test_iqm_fake_backend.py +++ b/tests/fake_backends/test_iqm_fake_backend.py @@ -159,6 +159,9 @@ def test_iqm_fake_backend_noise_model_instantiated(backend): def test_iqm_fake_backend_noise_model_basis_gates(backend): """Test that all operations named as part of the backend are utilizes in the noise_model""" + print(backend.noise_model.basis_gates) + print(backend.target.operation_names) + print(backend.operation_names) assert all(gates in backend.operation_names for gates in backend.noise_model.basis_gates) From 5bb86c02868910bde8c3823d22a379578daec396 Mon Sep 17 00:00:00 2001 From: Arianne Meijer Date: Thu, 28 Nov 2024 17:53:52 +0200 Subject: [PATCH 09/47] Native qiskit tranpsiler integration. --- pyproject.toml | 14 ++ src/iqm/qiskit_iqm/__init__.py | 2 +- .../fake_backends/iqm_fake_backend.py | 61 +----- src/iqm/qiskit_iqm/iqm_backend.py | 75 +++++-- src/iqm/qiskit_iqm/iqm_circuit_validation.py | 40 ++++ src/iqm/qiskit_iqm/iqm_move_layout.py | 49 +++-- src/iqm/qiskit_iqm/iqm_naive_move_pass.py | 172 +++++++--------- src/iqm/qiskit_iqm/iqm_provider.py | 159 +-------------- src/iqm/qiskit_iqm/iqm_transpilation.py | 22 +- src/iqm/qiskit_iqm/move_gate.py | 22 +- src/iqm/qiskit_iqm/qiskit_to_iqm.py | 183 ++++++++++++++++- src/iqm/qiskit_iqm/transpiler_plugins.py | 190 +++++++++++++++++- tests/conftest.py | 2 +- tests/fake_backends/test_fake_deneb.py | 84 ++++++-- tests/move_architecture/test_architecture.py | 12 +- tests/move_architecture/test_move_circuit.py | 88 ++++---- tests/test_iqm_backend.py | 32 ++- tests/test_iqm_backend_base.py | 9 +- tests/test_iqm_naive_move_pass.py | 82 +++----- tests/test_iqm_provider.py | 9 +- tests/test_iqm_transpilation.py | 17 +- tests/test_utils.py | 69 ------- tests/utils.py | 111 ++-------- 23 files changed, 827 insertions(+), 677 deletions(-) create mode 100644 src/iqm/qiskit_iqm/iqm_circuit_validation.py delete mode 100644 tests/test_utils.py diff --git a/pyproject.toml b/pyproject.toml index 83e64c5dc..4e9edfa48 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -159,5 +159,19 @@ ignore-imports = true [tool.pylint.string] check-quote-consistency = true +# Expose the different custom transpiler passes to the Qiskit transpiler using their plugin system. [project.entry-points."qiskit.transpiler.scheduling"] move_routing = "iqm.qiskit_iqm:MoveGateRoutingPlugin" +move_routing_keep = "iqm.qiskit_iqm:MoveGateRoutingKeepExistingMovesPlugin" +move_routing_remove = "iqm.qiskit_iqm:MoveGateRoutingRemoveExistingMovesPlugin" +move_routing_trust = "iqm.qiskit_iqm:MoveGateRoutingTrustExistingMovesPlugin" +only_move_routing = "iqm.qiskit_iqm:MoveGateRoutingOnlyPlugin" +only_move_routing_keep = "iqm.qiskit_iqm:MoveGateRoutingOnlyKeepExistingMovesPlugin" +only_move_routing_remove = "iqm.qiskit_iqm:MoveGateRoutingOnlyRemoveExistingMovesPlugin" +only_move_routing_trust = "iqm.qiskit_iqm:MoveGateRoutingOnlyTrustExistingMovesPlugin" +move_routing_exact_global_phase = "iqm.qiskit_iqm:MoveGateRoutingWithExactRzPlugin" +move_routing_Rz_optimization_ignores_barriers = "iqm.qiskit_iqm:MoveGateRoutingWithRzOptimizationIgnoreBarriersPlugin" +only_Rz_optimization = "iqm.qiskit_iqm:OnlyRzOptimizationPlugin" +only_Rz_optimization_exact_global_phase = "iqm.qiskit_iqm:OnlyRzOptimizationExactPlugin" +only_Rz_optimization_ignore_barriers = "iqm.qiskit_iqm:OnlyRzOptimizationIgnoreBarriersPlugin" + diff --git a/src/iqm/qiskit_iqm/__init__.py b/src/iqm/qiskit_iqm/__init__.py index 0d715dfa2..ede1bb588 100644 --- a/src/iqm/qiskit_iqm/__init__.py +++ b/src/iqm/qiskit_iqm/__init__.py @@ -26,7 +26,7 @@ from iqm.qiskit_iqm.iqm_provider import IQMBackend, IQMProvider, __version__ from iqm.qiskit_iqm.iqm_transpilation import IQMOptimizeSingleQubitGates, optimize_single_qubit_gates from iqm.qiskit_iqm.move_gate import MoveGate -from iqm.qiskit_iqm.transpiler_plugins import MoveGateRoutingPlugin +from iqm.qiskit_iqm.transpiler_plugins import * if qiskit_version < "1.0.0": warn( diff --git a/src/iqm/qiskit_iqm/fake_backends/iqm_fake_backend.py b/src/iqm/qiskit_iqm/fake_backends/iqm_fake_backend.py index 5d5727c8c..63fef9866 100644 --- a/src/iqm/qiskit_iqm/fake_backends/iqm_fake_backend.py +++ b/src/iqm/qiskit_iqm/fake_backends/iqm_fake_backend.py @@ -23,14 +23,19 @@ from qiskit import QuantumCircuit from qiskit.providers import JobV1, Options -from qiskit.transpiler import TransformationPass from qiskit_aer import AerSimulator from qiskit_aer.noise import NoiseModel from qiskit_aer.noise.errors import depolarizing_error, thermal_relaxation_error from iqm.iqm_client import DynamicQuantumArchitecture, QuantumArchitectureSpecification from iqm.qiskit_iqm.iqm_backend import IQM_TO_QISKIT_GATE_NAME, IQMBackendBase -from iqm.qiskit_iqm.iqm_circuit import IQMCircuit +from iqm.qiskit_iqm.iqm_circuit_validation import validate_circuit +from iqm.qiskit_iqm.iqm_transpilation import IQMReplaceGateWithUnitaryPass +from iqm.qiskit_iqm.move_gate import MOVE_GATE_UNITARY + +GATE_TO_UNITARY = { + "move": MOVE_GATE_UNITARY, +} # pylint: disable=too-many-instance-attributes @@ -281,9 +286,7 @@ def _default_options(cls) -> Options: def max_circuits(self) -> Optional[int]: return None - def run( - self, run_input: Union[QuantumCircuit, list[QuantumCircuit], IQMCircuit, list[IQMCircuit]], **options - ) -> JobV1: + def run(self, run_input: Union[QuantumCircuit, list[QuantumCircuit]], **options) -> JobV1: """ Run `run_input` on the fake backend using a simulator. @@ -302,64 +305,22 @@ def run( Raises: ValueError: If empty list of circuits is provided. """ - circuits_aux = [run_input] if isinstance(run_input, (QuantumCircuit, IQMCircuit)) else run_input + circuits_aux = [run_input] if isinstance(run_input, (QuantumCircuit)) else run_input if len(circuits_aux) == 0: raise ValueError("Empty list of circuits submitted for execution.") - this = self - - class check_move_validity(TransformationPass): - """Checks that the placement of move gates is valid in the circuit.""" - - def run(self, dag): - qubits_involved_in_last_move = None # Store which qubit was last used for MOVE IN - for node in dag.op_nodes(): - if node.op.name not in this.noise_model.basis_gates + ["id", "barrier", "measure"]: - raise ValueError("Operation '" + node.op.name + "' is not supported by the backend.") - if qubits_involved_in_last_move is not None: - # Verify that no single qubit gate is performed on the qubit between MOVE IN and MOVE OUT - if ( - node.op.name not in ["move", "barrier", "measure"] - and len(node.qargs) == 1 - and node.qargs[0] == qubits_involved_in_last_move[0] - ): - raise ValueError( - f"Operations to qubits {node.qargs[0]} while their states are moved to a resonator." - ) - if node.op.name == "move": - if qubits_involved_in_last_move is None: - # MOVE IN was performed - qubits_involved_in_last_move = node.qargs - elif qubits_involved_in_last_move != node.qargs: - raise ValueError( - f"Cannot apply MOVE on {node.qargs[0]} because COMP_R already holds the state of " - + f"{qubits_involved_in_last_move[0]}." - ) - else: - # MOVE OUT was performed - qubits_involved_in_last_move = None - - if qubits_involved_in_last_move is not None: - raise ValueError( - "The following resonators are still holding qubit states at the end of the circuit: " - + f"{qubits_involved_in_last_move[0]}." - ) - - return dag - circuits = [] for circ in circuits_aux: circ_to_add = circ - if "move" in self.noise_model.basis_gates: - check_move_validity()(circ) + validate_circuit(circ_to_add, self) for iqm_gate in [ g for g in self.noise_model.basis_gates if g not in list(IQM_TO_QISKIT_GATE_NAME.values()) ]: - circ_to_add = circ.decompose(gates_to_decompose=iqm_gate) + circ_to_add = IQMReplaceGateWithUnitaryPass(iqm_gate, GATE_TO_UNITARY[iqm_gate])(circ_to_add) circuits.append(circ_to_add) shots = options.get("shots", self.options.shots) diff --git a/src/iqm/qiskit_iqm/iqm_backend.py b/src/iqm/qiskit_iqm/iqm_backend.py index 5978b2188..3b2e04e43 100644 --- a/src/iqm/qiskit_iqm/iqm_backend.py +++ b/src/iqm/qiskit_iqm/iqm_backend.py @@ -17,6 +17,7 @@ from abc import ABC from copy import deepcopy +import re from typing import Final, Union from uuid import UUID @@ -51,7 +52,7 @@ def _dqa_from_static_architecture(sqa: QuantumArchitectureSpecification) -> Dyna """ # NOTE this prefix-based heuristic for identifying the qubits and resonators is not always guaranteed to work qubits = [qb for qb in sqa.qubits if qb.startswith('QB')] - computational_resonators = [qb for qb in sqa.qubits if qb.startswith('COMP')] + computational_resonators = [qb for qb in sqa.qubits if qb.lower().startswith('comp')] gates = { gate_name: GateInfo( implementations={'__fake': GateImplementationInfo(loci=tuple(tuple(locus) for locus in gate_loci))}, @@ -60,6 +61,11 @@ def _dqa_from_static_architecture(sqa: QuantumArchitectureSpecification) -> Dyna ) for gate_name, gate_loci in sqa.operations.items() } + gates['measure'] = GateInfo( + implementations={'__fake': GateImplementationInfo(loci=tuple(tuple([locus]) for locus in qubits))}, + default_implementation='__fake', + override_default_implementation={}, + ) return DynamicQuantumArchitecture( calibration_set_id=UUID('00000000-0000-0000-0000-000000000000'), qubits=qubits, @@ -68,6 +74,14 @@ def _dqa_from_static_architecture(sqa: QuantumArchitectureSpecification) -> Dyna ) +def _component_sort_key(component_name: str) -> tuple[str, int, str]: + def get_numeric_id(name: str) -> int: + match = re.search(r'(\d+)', name) + return int(match.group(1)) if match else 0 + + return re.sub(r'[^a-zA-Z]', '', component_name), get_numeric_id(component_name), component_name + + class IQMBackendBase(BackendV2, ABC): """Abstract base class for various IQM-specific backends. @@ -90,9 +104,17 @@ def __init__( self.architecture = arch # Qiskit uses integer indices to refer to qubits, so we need to map component names to indices. - qb_to_idx = {qb: idx for idx, qb in enumerate(arch.components)} + # Because of the way the Target and the transpiler interact, the resonators need to have higher indices than + # qubits, or else transpiling with optimization_level=0 will fail because of lacking resonator indices. + qb_to_idx = { + qb: idx + for idx, qb in enumerate( + sorted(arch.qubits, key=_component_sort_key) + + sorted(arch.computational_resonators, key=_component_sort_key) + ) + } - self._target = IQMStarTarget(arch, qb_to_idx) + self._target = IQMTarget(arch, qb_to_idx) self._qb_to_idx = qb_to_idx self._idx_to_qb = {v: k for k, v in qb_to_idx.items()} self.name = 'IQMBackend' @@ -107,6 +129,10 @@ def physical_qubits(self) -> list[str]: """Return the list of physical qubits in the backend.""" return list(self._qb_to_idx) + def has_resonators(self) -> bool: + """Return whether the backend has resonators.""" + return bool(self.architecture.computational_resonators) + def qubit_name_to_index(self, name: str) -> int: """Given an IQM-style qubit name, return the corresponding index in the register. @@ -142,8 +168,8 @@ def get_scheduling_stage_plugin(self) -> str: return 'move_routing' -class IQMStarTarget(Target): - """A target representing an IQM backend with resonators. +class IQMTarget(Target): + """A target representing an IQM backends that could have resonators. This target is used to represent the physical layout of the backend, including the resonators as well as a fake coupling map to . @@ -154,7 +180,7 @@ def __init__(self, architecture: DynamicQuantumArchitecture, component_to_idx: d self.iqm_dynamic_architecture = architecture self.iqm_component_to_idx = component_to_idx self.iqm_idx_to_component = {v: k for k, v in component_to_idx.items()} - self._iqm_create_instructions(architecture, component_to_idx) + self.real_target = self._iqm_create_instructions(architecture, component_to_idx) def _iqm_create_instructions(self, architecture: DynamicQuantumArchitecture, component_to_idx: dict[str, int]): """Converts a QuantumArchitectureSpecification object to a Qiskit Target object. @@ -165,6 +191,7 @@ def _iqm_create_instructions(self, architecture: DynamicQuantumArchitecture, com Returns: A Qiskit Target object representing the given quantum architecture specification. """ + # pylint: disable=too-many-branches operations = architecture.gates # There is no dedicated direct way of setting just the qubit connectivity and the native gates to the target. @@ -201,14 +228,18 @@ def _create_connections(name: str, is_symmetric: bool = False) -> dict[tuple[int self.add_instruction(Measure(), _create_connections('measure')) # Special work for devices with a MoveGate. - real_target = deepcopy(self) - if 'cz' in operations: - real_target.add_instruction(CZGate(), _create_connections('cz', True)) + real_target: IQMTarget = deepcopy(self) + if 'move' in operations: real_target.add_instruction(MoveGate(), _create_connections('move')) - if 'cz' in operations: + + fake_target_with_moves = deepcopy(real_target) + if 'cz' in operations: + real_target.add_instruction(CZGate(), _create_connections('cz', True)) + if 'move' in operations: fake_cz_connections: dict[tuple[int, int], None] = {} cz_loci = operations['cz'].implementations[operations['cz'].default_implementation].loci + move_cz_connections: dict[tuple[int, int], None] = {} for qb1, qb2 in cz_loci: if ( qb1 not in architecture.computational_resonators @@ -216,12 +247,28 @@ def _create_connections(name: str, is_symmetric: bool = False) -> dict[tuple[int ): fake_cz_connections[(component_to_idx[qb1], component_to_idx[qb2])] = None fake_cz_connections[(component_to_idx[qb2], component_to_idx[qb1])] = None - for qb1, res in operations['move']: + else: + move_cz_connections[(component_to_idx[qb1], component_to_idx[qb2])] = None + move_cz_connections[(component_to_idx[qb2], component_to_idx[qb1])] = None + for qb1, res in operations['move'].implementations[operations['move'].default_implementation].loci: for qb2 in [q for q in architecture.qubits if q not in [qb1, res]]: - if [qb2, res] in cz_loci or [res, qb2] in cz_loci: + if (qb2, res) in cz_loci or (res, qb2) in cz_loci: fake_cz_connections[(component_to_idx[qb1], component_to_idx[qb2])] = None fake_cz_connections[(component_to_idx[qb2], component_to_idx[qb1])] = None self.add_instruction(CZGate(), fake_cz_connections) - else: - self.add_instruction(CZGate(), _create_connections('cz', True)) + fake_cz_connections.update(move_cz_connections) + fake_target_with_moves.add_instruction(CZGate(), fake_cz_connections) + else: + self.add_instruction(CZGate(), _create_connections('cz', True)) + fake_target_with_moves.add_instruction(CZGate(), _create_connections('cz', True)) + fake_target_with_moves.set_real_target(real_target) + self.fake_target_with_moves: IQMTarget = fake_target_with_moves + return real_target + + def set_real_target(self, real_target: IQMTarget) -> None: + """Set the real target for this target. + + Args: + real_target: The real target to set. + """ self.real_target = real_target diff --git a/src/iqm/qiskit_iqm/iqm_circuit_validation.py b/src/iqm/qiskit_iqm/iqm_circuit_validation.py new file mode 100644 index 000000000..507682022 --- /dev/null +++ b/src/iqm/qiskit_iqm/iqm_circuit_validation.py @@ -0,0 +1,40 @@ +# Copyright 2024 Qiskit on IQM developers +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Helper functions for circuit validation.""" + +from typing import Optional + +from qiskit import QuantumCircuit + +from iqm.iqm_client import Circuit as IQMClientCircuit +from iqm.iqm_client import IQMClient, MoveGateValidationMode +from iqm.qiskit_iqm.iqm_backend import IQMBackendBase +from iqm.qiskit_iqm.qiskit_to_iqm import serialize_instructions + + +def validate_circuit( + circuit: QuantumCircuit, backend: IQMBackendBase, validate_moves: Optional[MoveGateValidationMode] = None +): + """Validate a circuit against the backend.""" + new_circuit = IQMClientCircuit( + name="Validation circuit", + instructions=serialize_instructions(circuit=circuit, qubit_index_to_name=backend._idx_to_qb), + ) + if validate_moves is None: + validate_moves = MoveGateValidationMode.STRICT + IQMClient._validate_circuit_instructions( + architecture=backend.architecture, + circuits=[new_circuit], + validate_moves=validate_moves, + ) diff --git a/src/iqm/qiskit_iqm/iqm_move_layout.py b/src/iqm/qiskit_iqm/iqm_move_layout.py index 1a56bef22..71b93d472 100644 --- a/src/iqm/qiskit_iqm/iqm_move_layout.py +++ b/src/iqm/qiskit_iqm/iqm_move_layout.py @@ -16,6 +16,7 @@ from qiskit import QuantumCircuit from qiskit.dagcircuit import DAGCircuit from qiskit.transpiler import PassManager, TranspilerError +from qiskit.transpiler.layout import Layout from qiskit.transpiler.passes import TrivialLayout from iqm.qiskit_iqm.iqm_provider import IQMBackend @@ -38,7 +39,7 @@ def __init__(self, backend: IQMBackend): super().__init__(backend.target) self._backend = backend - def run(self, dag): + def run(self, dag: DAGCircuit): """Creates the qubit layout for the given quantum circuit. Args: @@ -55,11 +56,23 @@ def run(self, dag): # No need to shuffle any qubits return - layout = self.get_initial_layout() - for src, dst in changes: - layout.swap(src, dst) + layout = self.get_initial_layout().get_physical_bits() + new_dict = {} + # Strict assignments + for src, dst in changes.items(): + new_dict[dst] = layout[src] - self.property_set['layout'] = layout + for physical_qubit, logical_qubit in layout.items(): + if logical_qubit not in new_dict.values(): + # Non-clashing assignments + if physical_qubit not in new_dict: + new_dict[physical_qubit] = logical_qubit + else: + # Clashing assignment + alt_physical_qubit = [q for q in layout if q not in new_dict][0] + new_dict[alt_physical_qubit] = logical_qubit + + self.property_set['layout'] = Layout(new_dict) def get_initial_layout(self): """Returns the initial layout generated by the algorithm. @@ -69,7 +82,7 @@ def get_initial_layout(self): """ return self.property_set['layout'] - def _determine_required_changes(self, dag: DAGCircuit) -> list[tuple[int, int]]: + def _determine_required_changes(self, dag: DAGCircuit) -> dict[int, int]: """Scans the operations in the given circuit and determines what qubits need to be switched so that the operations are valid for the specified quantum architecture. @@ -83,14 +96,14 @@ def _determine_required_changes(self, dag: DAGCircuit) -> list[tuple[int, int]]: reqs = self._calculate_requirements(dag) types = self._get_qubit_types() - changes: list[tuple[int, int]] = [] + changes = {} for index, qubit_type in reqs.items(): if index not in types or qubit_type != types[index]: # Need to change qubit at index to qubit_type - matching_qubit = next((i for i, t in types.items() if t == qubit_type), None) + matching_qubit = next((i for i, t in types.items() if qubit_type in t), None) if matching_qubit is None: raise TranspilerError(f"Cannot find a '{qubit_type}' from the quantum architecture.") - changes.append((index, matching_qubit)) + changes[index] = matching_qubit return changes def _get_qubit_types(self) -> dict[int, str]: @@ -110,6 +123,9 @@ def _get_qubit_types(self) -> dict[int, str]: qubit_types[qubit] = 'move_qubit' if resonator is not None: qubit_types[resonator] = 'resonator' + for i in range(backend.num_qubits): + if i not in qubit_types: + qubit_types[i] = 'qubit' return qubit_types @@ -127,11 +143,12 @@ def _calculate_requirements(dag: DAGCircuit) -> dict[int, str]: def _require_type(qubit_index: int, required_type: str, instruction_name: str): if qubit_index in required_types and required_types[qubit_index] != required_type: - raise TranspilerError( - f"""Invalid target '{qubit_index}' for the '{instruction_name}' operation, - qubit {qubit_index} would need to be {required_type} but it is already required to be - '{required_types[qubit_index]}'.""" - ) + if instruction_name != 'measure': + raise TranspilerError( + f"""Invalid target '{qubit_index}' for the '{instruction_name}' operation, + qubit {qubit_index} would need to be {required_type} but it is already required to be + '{required_types[qubit_index]}'.""" + ) required_types[qubit_index] = required_type for node in dag.topological_op_nodes(): @@ -141,6 +158,10 @@ def _require_type(qubit_index: int, required_type: str, instruction_name: str): (qubit, resonator) = node.qargs _require_type(dag.qubits.index(qubit), 'move_qubit', 'move') _require_type(dag.qubits.index(resonator), 'resonator', 'move') + for node in dag.topological_op_nodes(): + if node.name == 'measure': + for qubit in node.qargs: + _require_type(dag.qubits.index(qubit), 'qubit', 'measure') return required_types diff --git a/src/iqm/qiskit_iqm/iqm_naive_move_pass.py b/src/iqm/qiskit_iqm/iqm_naive_move_pass.py index ef1b9ff59..8a7b5914c 100644 --- a/src/iqm/qiskit_iqm/iqm_naive_move_pass.py +++ b/src/iqm/qiskit_iqm/iqm_naive_move_pass.py @@ -12,23 +12,22 @@ # See the License for the specific language governing permissions and # limitations under the License. """Naive transpilation for the IQM Star architecture.""" -from typing import Optional +from typing import Dict, List, Optional, Union +import warnings +from pydantic_core import ValidationError from qiskit import QuantumCircuit, transpile -from qiskit.circuit import QuantumRegister, Qubit from qiskit.converters import circuit_to_dag, dag_to_circuit from qiskit.dagcircuit import DAGCircuit -from qiskit.transpiler import Layout, TranspileLayout from qiskit.transpiler.basepasses import TransformationPass -from qiskit.transpiler.passmanager import PassManager +from qiskit.transpiler.layout import Layout from iqm.iqm_client import Circuit as IQMClientCircuit from iqm.iqm_client.transpile import ExistingMoveHandlingOptions, transpile_insert_moves -from .iqm_backend import IQMBackendBase, IQMStarTarget -from .iqm_circuit import IQMCircuit -from .iqm_provider import _deserialize_instructions, _serialize_instructions -from .iqm_transpilation import IQMOptimizeSingleQubitGates +from .iqm_backend import IQMBackendBase, IQMTarget +from .iqm_move_layout import generate_initial_layout +from .qiskit_to_iqm import deserialize_instructions, serialize_instructions class IQMNaiveResonatorMoving(TransformationPass): @@ -45,7 +44,7 @@ class IQMNaiveResonatorMoving(TransformationPass): def __init__( self, - target: IQMStarTarget, + target: IQMTarget, gate_set: list[str], existing_moves_handling: Optional[ExistingMoveHandlingOptions] = None, ): @@ -76,12 +75,16 @@ def run(self, dag: DAGCircuit): # pylint: disable=too-many-branches circuit = dag_to_circuit(dag) iqm_json = IQMClientCircuit( name="Transpiling Circuit", - instructions=tuple(_serialize_instructions(circuit, self.target.iqm_idx_to_component)), + instructions=tuple(serialize_instructions(circuit, self.target.iqm_idx_to_component)), ) - routed_json = transpile_insert_moves( - iqm_json, self.target.iqm_dynamic_architecture, self.existing_moves_handling - ) - routed_circuit = _deserialize_instructions(list(routed_json.instructions), self.target.iqm_component_to_idx) + try: + routed_json = transpile_insert_moves( + iqm_json, self.target.iqm_dynamic_architecture, self.existing_moves_handling + ) + routed_circuit = deserialize_instructions(list(routed_json.instructions), self.target.iqm_component_to_idx) + except ValidationError as _: # The Circuit without move gates is empty. + circ_args = [circuit.num_qubits, circuit.num_ancillas, circuit.num_clbits] + routed_circuit = QuantumCircuit(*(arg for arg in circ_args if arg > 0)) new_dag = circuit_to_dag(routed_circuit) return new_dag @@ -89,9 +92,13 @@ def run(self, dag: DAGCircuit): # pylint: disable=too-many-branches def transpile_to_IQM( # pylint: disable=too-many-arguments circuit: QuantumCircuit, backend: IQMBackendBase, + target: Optional[IQMTarget] = None, + initial_layout: Optional[Union[Layout, Dict, List]] = None, + perform_move_routing: bool = True, optimize_single_qubits: bool = True, ignore_barriers: bool = False, remove_final_rzs: bool = True, + existing_moves_handling: Optional[ExistingMoveHandlingOptions] = None, **qiskit_transpiler_qwargs, ) -> QuantumCircuit: """Basic function for transpiling to IQM backends. Currently works with Deneb and Garnet @@ -99,96 +106,65 @@ def transpile_to_IQM( # pylint: disable=too-many-arguments Args: circuit: The circuit to be transpiled without MOVE gates. backend: The target backend to compile to. Does not require a resonator. + target: An alternative target to compile to than the backend, using this option requires intimate knowledge + of the transpiler and thus it is not recommended to use. + initial_layout: The initial layout to use for the transpilation, same as `qiskit.transpile`. optimize_single_qubits: Whether to optimize single qubit gates away. ignore_barriers: Whether to ignore barriers when optimizing single qubit gates away. remove_final_rzs: Whether to remove the final Rz rotations. + existing_moves_handling: How to handle existing MOVE gates in the circuit, required if the circuit contains + MOVE gates. qiskit_transpiler_qwargs: Arguments to be passed to the Qiskit transpiler. - Raises: - NotImplementedError: Thrown when the backend supports multiple resonators. - Returns: QuantumCircuit: The transpiled circuit ready for running on the backend. """ - circuit_with_resonator = IQMCircuit(backend.target.num_qubits) - circuit_with_resonator.add_bits(circuit.clbits) - qubit_indices = [backend.qubit_name_to_index(qb) for qb in backend.physical_qubits if not qb.startswith("COMP_R")] - circuit_with_resonator.append( - circuit, - [circuit_with_resonator.qubits[qubit_indices[i]] for i in range(circuit.num_qubits)], - circuit.clbits, - ) - - # Transpile the circuit using the fake target without resonators - simple_transpile = transpile( - circuit_with_resonator, - target=backend.target, - basis_gates=backend.target.operation_names, - **qiskit_transpiler_qwargs, - ) - - # Construct the pass sequence for the additional passes - passes = [] - if optimize_single_qubits: - optimize_pass = IQMOptimizeSingleQubitGates(remove_final_rzs, ignore_barriers) - passes.append(optimize_pass) - - if "move" in backend.architecture.gates.keys(): - move_pass = IQMNaiveResonatorMoving(backend.target, backend.target.operation_names) - passes.append(move_pass) - - # circuit_with_resonator = add_resonators_to_circuit(simple_transpile, backend) - # else: - # circuit_with_resonator = simple_transpile - - # Transpiler passes strip the layout information, so we need to add it back - layout = simple_transpile._layout - # TODO Update the circuit so that following passes can use the layout information, - # old buggy logic in _add_resonators_to_circuit - # TODO Add actual tests for the updating the layout. Currrently not done because Deneb's fake_target is - # fully connected. - transpiled_circuit = PassManager(passes).run(simple_transpile) - transpiled_circuit._layout = layout - return transpiled_circuit - - -def _add_resonators_to_circuit(circuit: QuantumCircuit, backend: IQMBackendBase) -> QuantumCircuit: - """Add resonators to a circuit for a backend that supports multiple resonators. - - Args: - circuit: The circuit to add resonators to. - backend: The backend to add resonators for. - - Returns: - QuantumCircuit: The circuit with resonators added. - """ - qubit_indices = [backend.qubit_name_to_index(qb) for qb in backend.physical_qubits if not qb.startswith("COMP_R")] - resonator_indices = [backend.qubit_name_to_index(qb) for qb in backend.physical_qubits if qb.startswith("COMP_R")] - n_classical_regs = len(circuit.cregs) - n_qubits = len(qubit_indices) - n_resonators = len(resonator_indices) - - circuit_with_resonator = IQMCircuit(n_qubits + n_resonators, n_classical_regs) - # Update and copy the initial and final layout of the circuit found by the transpiler - layout_dict = { - qb: i + sum(1 for r_i in resonator_indices if r_i <= i + n_resonators) - for qb, i in circuit._layout.initial_layout._v2p.items() - } - layout_dict.update({Qubit(QuantumRegister(n_resonators, "resonator"), r_i): r_i for r_i in resonator_indices}) - initial_layout = Layout(input_dict=layout_dict) - init_mapping = layout_dict - final_layout = None - if circuit.layout.final_layout: - final_layout_dict = { - qb: i + sum(1 for r_i in resonator_indices if r_i <= i + n_resonators) - for qb, i in circuit.layout.final_layout._v2p.items() - } - final_layout_dict.update( - {Qubit(QuantumRegister(n_resonators, "resonator"), r_i): r_i for r_i in resonator_indices} + # pylint: disable=too-many-branches + + if target is None: + if circuit.count_ops().get("move", 0) > 0: + target = backend.target.fake_target_with_moves + # Create a sensible initial layout if none is provided + if initial_layout is None: + initial_layout = generate_initial_layout(backend, circuit) + if perform_move_routing and existing_moves_handling is None: + raise ValueError("The circuit contains MOVE gates but existing_moves_handling is not set.") + else: + target = backend.target + + # Determine which scheduling method to use + scheduling_method = qiskit_transpiler_qwargs.get("scheduling_method", None) + if scheduling_method is None: + if perform_move_routing: + if optimize_single_qubits: + if not remove_final_rzs: + scheduling_method = "move_routing_exact_global_phase" + elif ignore_barriers: + scheduling_method = "move_routing_Rz_optimization_ignores_barriers" + else: + scheduling_method = "move_routing" + else: + scheduling_method = "only_move_routing" + if existing_moves_handling is not None: + if not scheduling_method.endswith("routing"): + raise ValueError( + "Existing Move handling options are not compatible with `remove_final_rzs` and \ + `ignore_barriers` options." + ) + scheduling_method += "_" + existing_moves_handling.value + else: + if optimize_single_qubits: + scheduling_method = "only_Rz_optimization" + if not remove_final_rzs: + scheduling_method += "_exact_global_phase" + elif ignore_barriers: + scheduling_method += "_ignores_barriers" + else: + scheduling_method = "default" + else: + warnings.warn( + f"Scheduling method is set to {scheduling_method}, but it is normally used to pass other transpiler " + + "options, ignoring the other arguments." ) - final_layout = Layout(final_layout_dict) - new_layout = TranspileLayout(initial_layout, init_mapping, final_layout=final_layout) - circuit_with_resonator.append(circuit, circuit_with_resonator.qregs, circuit_with_resonator.cregs) - circuit_with_resonator = circuit_with_resonator.decompose() - circuit_with_resonator._layout = new_layout - return circuit_with_resonator + qiskit_transpiler_qwargs["scheduling_method"] = scheduling_method + return transpile(circuit, target=target, initial_layout=initial_layout, **qiskit_transpiler_qwargs) diff --git a/src/iqm/qiskit_iqm/iqm_provider.py b/src/iqm/qiskit_iqm/iqm_provider.py index b7e3d2134..250c1bab5 100644 --- a/src/iqm/qiskit_iqm/iqm_provider.py +++ b/src/iqm/qiskit_iqm/iqm_provider.py @@ -17,29 +17,19 @@ from collections.abc import Callable from importlib.metadata import PackageNotFoundError, version -from functools import reduce -from typing import Collection, Optional, Union +from typing import Optional, Union from uuid import UUID import warnings -import numpy as np from qiskit import QuantumCircuit -from qiskit.circuit import Clbit from qiskit.providers import JobStatus, JobV1, Options -from iqm.iqm_client import ( - Circuit, - CircuitCompilationOptions, - CircuitValidationError, - Instruction, - IQMClient, - RunRequest, -) +from iqm.iqm_client import Circuit, CircuitCompilationOptions, CircuitValidationError, IQMClient, RunRequest from iqm.iqm_client.util import to_json_dict from iqm.qiskit_iqm.fake_backends import IQMFakeAdonis from iqm.qiskit_iqm.iqm_backend import IQMBackendBase from iqm.qiskit_iqm.iqm_job import IQMJob -from iqm.qiskit_iqm.qiskit_to_iqm import MeasurementKey +from iqm.qiskit_iqm.qiskit_to_iqm import serialize_instructions try: __version__ = version('qiskit-iqm') @@ -185,10 +175,6 @@ def create_run_request( circuit_callback(circuits) circuits_serialized: list[Circuit] = [self.serialize_circuit(circuit) for circuit in circuits] - used_physical_qubit_indices: set[int] = reduce( - lambda qubits, circuit: qubits.union(set(int(q) for q in circuit.all_qubits())), circuits_serialized, set() - ) - qubit_mapping = {str(idx): qb for idx, qb in self._idx_to_qb.items() if idx in used_physical_qubit_indices} if self._use_default_calibration_set: default_calset_id = self.client.get_dynamic_quantum_architecture(None).calibration_set_id @@ -201,7 +187,6 @@ def create_run_request( try: run_request = self.client.create_run_request( circuits_serialized, - qubit_mapping=qubit_mapping, calibration_set_id=self._calibration_set_id, shots=shots, options=circuit_compilation_options, @@ -247,7 +232,7 @@ def serialize_circuit(self, circuit: QuantumCircuit) -> Circuit: Raises: ValueError: circuit contains an unsupported instruction or is not transpiled in general """ - instructions = _serialize_instructions(circuit, self._idx_to_qb) + instructions = serialize_instructions(circuit, self._idx_to_qb) try: metadata = to_json_dict(circuit.metadata) @@ -260,142 +245,6 @@ def serialize_circuit(self, circuit: QuantumCircuit) -> Circuit: return Circuit(name=circuit.name, instructions=instructions, metadata=metadata) -def _serialize_instructions( - circuit: QuantumCircuit, qubit_index_to_name: dict[int, str], allowed_nonnative_gates: Collection[str] = () -) -> list[Instruction]: - """Serialize a quantum circuit into the IQM data transfer format. - - This is IQM's internal helper for :meth:`.IQMBackend.serialize_circuit` that gives slightly more control. - See :meth:`.IQMBackend.serialize_circuit` for details. - - Args: - circuit: quantum circuit to serialize - qubit_index_to_name: Mapping from qubit indices to the corresponding qubit names. - allowed_nonnative_gates: Names of gates that are converted as-is without validation. - By default, any gate that can't be converted will raise an error. - If such gates are present in the circuit, the caller must edit the result to be valid and executable. - Notably, since IQM transfer format requires named parameters and qiskit parameters don't have names, the - `i` th parameter of an unrecognized instruction is given the name ``"p"``. - - Returns: - list of instructions representing the circuit - - Raises: - ValueError: circuit contains an unsupported instruction or is not transpiled in general - """ - # pylint: disable=too-many-branches,too-many-statements - instructions: list[Instruction] = [] - # maps clbits to the latest "measure" instruction to store its result there - clbit_to_measure: dict[Clbit, Instruction] = {} - for circuit_instruction in circuit.data: - instruction = circuit_instruction.operation - qubit_names = [str(circuit.find_bit(qubit).index) for qubit in circuit_instruction.qubits] - if instruction.name == 'r': - angle_t = float(instruction.params[0] / (2 * np.pi)) - phase_t = float(instruction.params[1] / (2 * np.pi)) - native_inst = Instruction(name='prx', qubits=qubit_names, args={'angle_t': angle_t, 'phase_t': phase_t}) - elif instruction.name == 'x': - native_inst = Instruction(name='prx', qubits=qubit_names, args={'angle_t': 0.5, 'phase_t': 0.0}) - elif instruction.name == 'rx': - angle_t = float(instruction.params[0] / (2 * np.pi)) - native_inst = Instruction(name='prx', qubits=qubit_names, args={'angle_t': angle_t, 'phase_t': 0.0}) - elif instruction.name == 'y': - native_inst = Instruction(name='prx', qubits=qubit_names, args={'angle_t': 0.5, 'phase_t': 0.25}) - elif instruction.name == 'ry': - angle_t = float(instruction.params[0] / (2 * np.pi)) - native_inst = Instruction(name='prx', qubits=qubit_names, args={'angle_t': angle_t, 'phase_t': 0.25}) - elif instruction.name == 'cz': - native_inst = Instruction(name='cz', qubits=qubit_names, args={}) - elif instruction.name == 'move': - native_inst = Instruction(name='move', qubits=qubit_names, args={}) - elif instruction.name == 'barrier': - native_inst = Instruction(name='barrier', qubits=qubit_names, args={}) - elif instruction.name == 'measure': - if len(circuit_instruction.clbits) != 1: - raise ValueError( - f'Unexpected: measurement instruction {circuit_instruction} uses multiple classical bits.' - ) - clbit = circuit_instruction.clbits[0] # always a single-qubit measurement - mk = str(MeasurementKey.from_clbit(clbit, circuit)) - native_inst = Instruction(name='measure', qubits=qubit_names, args={'key': mk}) - clbit_to_measure[clbit] = native_inst - elif instruction.name == 'reset': - # implemented using a measure instruction to measure the qubits, and - # one cc_prx per qubit to conditionally flip it to |0> - feedback_key = '_reset' - instructions.append( - Instruction( - name='measure', - qubits=qubit_names, - args={ - # HACK to get something unique, remove when key can be omitted - 'key': f'_reset_{len(instructions)}', - 'feedback_key': feedback_key, - }, - ) - ) - for q in qubit_names: - physical_qubit_name = qubit_index_to_name[int(q)] - instructions.append( - Instruction( - name='cc_prx', - qubits=[q], - args={ - 'angle_t': 0.5, - 'phase_t': 0.0, - 'feedback_key': feedback_key, - 'feedback_qubit': physical_qubit_name, - }, - ) - ) - continue - elif instruction.name == 'id': - continue - elif instruction.name in allowed_nonnative_gates: - args = {f'p{i}': param for i, param in enumerate(instruction.params)} - native_inst = Instruction.model_construct(name=instruction.name, qubits=tuple(qubit_names), args=args) - else: - raise ValueError( - f"Instruction '{instruction.name}' in the circuit '{circuit.name}' is not natively supported. " - f'You need to transpile the circuit before execution.' - ) - - # classically controlled gates (using the c_if method) - condition = instruction.condition - if condition is not None: - if native_inst.name != 'prx': - raise ValueError( - 'This backend only supports conditionals on r, x, y, rx and ry gates,' f' not on {instruction.name}' - ) - native_inst.name = 'cc_prx' - creg, value = condition - if len(creg) != 1: - raise ValueError(f'{instruction} is conditioned on multiple bits, this is not supported.') - if value != 1: - raise ValueError(f'{instruction} is conditioned on integer value {value}, only value 1 is supported.') - # Set up feedback routing. - # The latest "measure" instruction to write to that classical bit is modified, it is - # given an explicit feedback_key equal to its measurement key. - # The same feedback_key is given to the controlled instruction, along with the feedback qubit. - measure_inst = clbit_to_measure[creg[0]] - feedback_key = measure_inst.args['key'] - measure_inst.args['feedback_key'] = feedback_key # this measure is used to provide feedback - # TODO we should use physical qubit names in native circuits, not integer strings - physical_qubit_name = qubit_index_to_name[int(measure_inst.qubits[0])] # single-qubit measurement - native_inst.args['feedback_key'] = feedback_key - native_inst.args['feedback_qubit'] = physical_qubit_name - - instructions.append(native_inst) - return instructions - - -def _deserialize_instructions(instructions: list[Instruction], qubit_name_to_index: dict[str, int]) -> QuantumCircuit: - circuit = QuantumCircuit(max(qubit_name_to_index.values()) + 1) - for _ in instructions: - pass # TODO implement deserialization - return circuit - - class IQMFacadeBackend(IQMBackend): """Facade backend for mimicking the execution of quantum circuits on IQM quantum computers. Allows to submit a circuit to the IQM server, and if the execution was successful, performs a simulation with a respective IQM noise diff --git a/src/iqm/qiskit_iqm/iqm_transpilation.py b/src/iqm/qiskit_iqm/iqm_transpilation.py index 85161491f..de205a9cb 100644 --- a/src/iqm/qiskit_iqm/iqm_transpilation.py +++ b/src/iqm/qiskit_iqm/iqm_transpilation.py @@ -16,7 +16,7 @@ import numpy as np from qiskit import QuantumCircuit from qiskit.circuit.equivalence_library import SessionEquivalenceLibrary -from qiskit.circuit.library import RGate +from qiskit.circuit.library import RGate, UnitaryGate from qiskit.dagcircuit import DAGCircuit from qiskit.transpiler.basepasses import TransformationPass from qiskit.transpiler.passes import BasisTranslator, Optimize1qGatesDecomposition, RemoveBarriers @@ -107,3 +107,23 @@ def optimize_single_qubit_gates( optimised circuit """ return PassManager(IQMOptimizeSingleQubitGates(drop_final_rz, ignore_barriers)).run(circuit) + + +class IQMReplaceGateWithUnitaryPass(TransformationPass): + """Transpiler pass that replaces all gates with given name in a circuit with a UnitaryGate. + + Args: + gate: The name of the gate to replace. + unitary: The unitary matrix to replace the gate with. + """ + + def __init__(self, gate: str, unitary: list[list[float]]): + super().__init__() + self.gate = gate + self.unitary = unitary + + def run(self, dag): + for node in dag.op_nodes(): + if node.name == self.gate: + dag.substitute_node(node, UnitaryGate(self.unitary)) + return dag diff --git a/src/iqm/qiskit_iqm/move_gate.py b/src/iqm/qiskit_iqm/move_gate.py index c27e0fa22..1d36f19f5 100644 --- a/src/iqm/qiskit_iqm/move_gate.py +++ b/src/iqm/qiskit_iqm/move_gate.py @@ -14,9 +14,10 @@ """MOVE gate to be used on the IQM Star architecture.""" from qiskit.circuit import Gate -from qiskit.circuit.quantumcircuit import QuantumCircuit, QuantumRegister import qiskit.quantum_info as qi +MOVE_GATE_UNITARY = [[1.0, 0.0, 0.0, 0.0], [0.0, 0.0, 1.0, 0.0], [0.0, 1.0, 0.0, 0.0], [0.0, 0.0, 0.0, 1.0]] + class MoveGate(Gate): r"""The MOVE operation is a unitary population exchange operation between a qubit and a resonator. @@ -40,21 +41,10 @@ class MoveGate(Gate): def __init__(self, label=None): """Initializes the move gate""" super().__init__("move", 2, [], label=label) - self.unitary = qi.Operator( - [[1.0, 0.0, 0.0, 0.0], [0.0, 0.0, 1.0, 0.0], [0.0, 1.0, 0.0, 0.0], [0.0, 0.0, 0.0, 1.0]] - ) + self.unitary = qi.Operator(MOVE_GATE_UNITARY) def _define(self): - """Pretend that this gate is a SWAP for the purpose of matrix checking. - - The |0> needs to be traced out for the resonator 'qubits'. - - gate swap a,b + """This function is purposefully not defined so that that the Qiskit transpiler cannot accidentally + decompose the MOVE gate into a sequence of other gates, instead it will throw an error. """ - - q = QuantumRegister(2, "q") - qc = QuantumCircuit(q, name=self.label if self.label else self.name) - - qc.unitary(self.unitary, [q[0], q[1]], label=self.name) - - self.definition = qc + return diff --git a/src/iqm/qiskit_iqm/qiskit_to_iqm.py b/src/iqm/qiskit_iqm/qiskit_to_iqm.py index 003b2e12c..773586993 100644 --- a/src/iqm/qiskit_iqm/qiskit_to_iqm.py +++ b/src/iqm/qiskit_iqm/qiskit_to_iqm.py @@ -17,9 +17,14 @@ from dataclasses import dataclass import re +from typing import Collection +import numpy as np from qiskit import QuantumCircuit as QiskitQuantumCircuit -from qiskit.circuit import Clbit +from qiskit.circuit import ClassicalRegister, Clbit, QuantumRegister + +from iqm.iqm_client import Instruction +from iqm.qiskit_iqm.move_gate import MoveGate class InstructionNotSupportedError(RuntimeError): @@ -84,3 +89,179 @@ def from_clbit(cls, clbit: Clbit, circuit: QiskitQuantumCircuit) -> MeasurementK creg_idx = circuit.cregs.index(creg) clbit_idx = bitloc.registers[0][1] return cls(creg.name, len(creg), creg_idx, clbit_idx) + + +def serialize_instructions( + circuit: QiskitQuantumCircuit, qubit_index_to_name: dict[int, str], allowed_nonnative_gates: Collection[str] = () +) -> list[Instruction]: + """Serialize a quantum circuit into the IQM data transfer format. + + This is IQM's internal helper for :meth:`.IQMBackend.serialize_circuit` that gives slightly more control. + See :meth:`.IQMBackend.serialize_circuit` for details. + + Args: + circuit: quantum circuit to serialize + qubit_index_to_name: Mapping from qubit indices to the corresponding qubit names. + allowed_nonnative_gates: Names of gates that are converted as-is without validation. + By default, any gate that can't be converted will raise an error. + If such gates are present in the circuit, the caller must edit the result to be valid and executable. + Notably, since IQM transfer format requires named parameters and qiskit parameters don't have names, the + `i` th parameter of an unrecognized instruction is given the name ``"p"``. + + Returns: + list of instructions representing the circuit + + Raises: + ValueError: circuit contains an unsupported instruction or is not transpiled in general + """ + # pylint: disable=too-many-branches,too-many-statements + instructions: list[Instruction] = [] + # maps clbits to the latest "measure" instruction to store its result there + clbit_to_measure: dict[Clbit, Instruction] = {} + for circuit_instruction in circuit.data: + instruction = circuit_instruction.operation + qubit_names = [qubit_index_to_name[circuit.find_bit(qubit).index] for qubit in circuit_instruction.qubits] + if instruction.name == 'r': + angle_t = float(instruction.params[0] / (2 * np.pi)) + phase_t = float(instruction.params[1] / (2 * np.pi)) + native_inst = Instruction(name='prx', qubits=qubit_names, args={'angle_t': angle_t, 'phase_t': phase_t}) + elif instruction.name == 'x': + native_inst = Instruction(name='prx', qubits=qubit_names, args={'angle_t': 0.5, 'phase_t': 0.0}) + elif instruction.name == 'rx': + angle_t = float(instruction.params[0] / (2 * np.pi)) + native_inst = Instruction(name='prx', qubits=qubit_names, args={'angle_t': angle_t, 'phase_t': 0.0}) + elif instruction.name == 'y': + native_inst = Instruction(name='prx', qubits=qubit_names, args={'angle_t': 0.5, 'phase_t': 0.25}) + elif instruction.name == 'ry': + angle_t = float(instruction.params[0] / (2 * np.pi)) + native_inst = Instruction(name='prx', qubits=qubit_names, args={'angle_t': angle_t, 'phase_t': 0.25}) + elif instruction.name == 'cz': + native_inst = Instruction(name='cz', qubits=qubit_names, args={}) + elif instruction.name == 'move': + native_inst = Instruction(name='move', qubits=qubit_names, args={}) + elif instruction.name == 'barrier': + native_inst = Instruction(name='barrier', qubits=qubit_names, args={}) + elif instruction.name == 'measure': + if len(circuit_instruction.clbits) != 1: + raise ValueError( + f'Unexpected: measurement instruction {circuit_instruction} uses multiple classical bits.' + ) + clbit = circuit_instruction.clbits[0] # always a single-qubit measurement + mk = str(MeasurementKey.from_clbit(clbit, circuit)) + native_inst = Instruction(name='measure', qubits=qubit_names, args={'key': mk}) + clbit_to_measure[clbit] = native_inst + elif instruction.name == 'reset': + # implemented using a measure instruction to measure the qubits, and + # one cc_prx per qubit to conditionally flip it to |0> + feedback_key = '_reset' + instructions.append( + Instruction( + name='measure', + qubits=qubit_names, + args={ + # HACK to get something unique, remove when key can be omitted + 'key': f'_reset_{len(instructions)}', + 'feedback_key': feedback_key, + }, + ) + ) + for q in qubit_names: + instructions.append( + Instruction( + name='cc_prx', + qubits=[q], + args={ + 'angle_t': 0.5, + 'phase_t': 0.0, + 'feedback_key': feedback_key, + 'feedback_qubit': q, + }, + ) + ) + continue + elif instruction.name == 'id': + continue + elif instruction.name in allowed_nonnative_gates: + args = {f'p{i}': param for i, param in enumerate(instruction.params)} + native_inst = Instruction.model_construct(name=instruction.name, qubits=tuple(qubit_names), args=args) + else: + raise ValueError( + f"Instruction '{instruction.name}' in the circuit '{circuit.name}' is not natively supported. " + f'You need to transpile the circuit before execution.' + ) + + # classically controlled gates (using the c_if method) + condition = instruction.condition + if condition is not None: + if native_inst.name != 'prx': + raise ValueError( + 'This backend only supports conditionals on r, x, y, rx and ry gates,' f' not on {instruction.name}' + ) + native_inst.name = 'cc_prx' + creg, value = condition + if len(creg) != 1: + raise ValueError(f'{instruction} is conditioned on multiple bits, this is not supported.') + if value != 1: + raise ValueError(f'{instruction} is conditioned on integer value {value}, only value 1 is supported.') + # Set up feedback routing. + # The latest "measure" instruction to write to that classical bit is modified, it is + # given an explicit feedback_key equal to its measurement key. + # The same feedback_key is given to the controlled instruction, along with the feedback qubit. + measure_inst = clbit_to_measure[creg[0]] + feedback_key = measure_inst.args['key'] + measure_inst.args['feedback_key'] = feedback_key # this measure is used to provide feedback + physical_qubit_name = measure_inst.qubits[0] # single-qubit measurement + native_inst.args['feedback_key'] = feedback_key + native_inst.args['feedback_qubit'] = physical_qubit_name + + instructions.append(native_inst) + return instructions + + +def deserialize_instructions( + instructions: list[Instruction], qubit_name_to_index: dict[str, int] +) -> QiskitQuantumCircuit: + """Helper function to turn a list of IQM Instructions into a Qiskit QuantumCircuit. + + Args: + instructions (list[Instruction]): The gates in the circuit. + qubit_name_to_index (dict[str, int]): Mapping from qubit names to their indices, as specified in a backend. + + Raises: + ValueError: Thrown when a given instruction is not supported. + + Returns: + QiskitQuantumCircuit: The circuit represented by the given instructions. + """ + cl_bits: dict[str, int] = {} + cl_regs: dict[int, ClassicalRegister] = {} + for instr in instructions: + if instr.name == 'measure': + mk = MeasurementKey.from_string(instr.args['key']) + cl_regs[mk.creg_idx] = cl_regs.get(mk.creg_idx, ClassicalRegister(size=mk.creg_len, name=mk.creg_name)) + cl_bits[str(mk)] = cl_regs[mk.creg_idx][mk.clbit_idx] + qreg = QuantumRegister(max(qubit_name_to_index.values()) + 1, 'q') + circuit = QiskitQuantumCircuit(qreg, *(cl_regs[i] for i in range(len(cl_regs)))) + for instr in instructions: + loci = [qubit_name_to_index[q] for q in instr.qubits] + if instr.name == 'prx': + angle_t = instr.args['angle_t'] * 2 * np.pi + phase_t = instr.args['phase_t'] * 2 * np.pi + circuit.r(angle_t, phase_t, loci[0]) + elif instr.name == 'cz': + circuit.cz(loci[0], loci[1]) + elif instr.name == 'move': + circuit.append(MoveGate(), loci) + elif instr.name == 'measure': + mk = MeasurementKey.from_string(instr.args['key']) + circuit.measure(loci[0], cl_bits[str(mk)]) + elif instr.name == 'barrier': + circuit.barrier(*loci) + elif instr.name == 'cc_prx': + angle_t = instr.args['angle_t'] * 2 * np.pi + phase_t = instr.args['phase_t'] * 2 * np.pi + feedback_key = instr.args['feedback_key'] + circuit.r(angle_t, phase_t, loci[0]).c_if(cl_bits[feedback_key], 1) + else: + raise ValueError(f'Unsupported instruction {instr.name} in the circuit.') + return circuit diff --git a/src/iqm/qiskit_iqm/transpiler_plugins.py b/src/iqm/qiskit_iqm/transpiler_plugins.py index a603c9f79..e2d136f7d 100644 --- a/src/iqm/qiskit_iqm/transpiler_plugins.py +++ b/src/iqm/qiskit_iqm/transpiler_plugins.py @@ -12,26 +12,200 @@ # See the License for the specific language governing permissions and # limitations under the License. """Collection of Qiskit transpiler plugins for native use of specialized transpiler passes by our devices.""" +from typing import Optional from qiskit.transpiler.passmanager import PassManager +from qiskit.transpiler.passmanager_config import PassManagerConfig from qiskit.transpiler.preset_passmanagers.builtin_plugins import PassManagerStagePlugin -from iqm.qiskit_iqm.iqm_backend import IQMStarTarget +from iqm.iqm_client.transpile import ExistingMoveHandlingOptions +from iqm.qiskit_iqm.iqm_backend import IQMTarget from iqm.qiskit_iqm.iqm_naive_move_pass import IQMNaiveResonatorMoving from iqm.qiskit_iqm.iqm_transpilation import IQMOptimizeSingleQubitGates -class MoveGateRoutingPlugin(PassManagerStagePlugin): - """Plugin class for IQM single qubit gate optimization and MoveGate routing as a scheduling stage.""" +class IQMSchedulingPlugin(PassManagerStagePlugin): + """Basic plugin for scheduling stage of IQM devices. + + Args: + move_gate_routing: whether to include MoveGate routing in the scheduling stage. + optimize_sqg: Whether to include single qubit gate optimization in the scheduling stage. + drop_final_rz: Whether to drop trailing Rz gates in the circuit during single qubit gate optimization. + ignore_barriers: Whether to ignore barriers during single qubit gate optimization. + existing_move_handling: How to handle existing MoveGates in the circuit during MoveGate routing. + Raises: + ValueError: When incompatible options are set. + """ + + def __init__( + self, + move_gate_routing: bool, + optimize_sqg: bool, + drop_final_rz: bool, + ignore_barriers: bool, + existing_move_handling: Optional[ExistingMoveHandlingOptions], + ) -> None: + # pylint: disable=too-many-arguments + super().__init__() + self.move_gate_routing = move_gate_routing + self.optimize_sqg = optimize_sqg + self.drop_final_rz = drop_final_rz + self.ignore_barriers = ignore_barriers + self.existing_move_handling = existing_move_handling - def pass_manager(self, pass_manager_config, optimization_level=None) -> PassManager: + def pass_manager( + self, pass_manager_config: PassManagerConfig, optimization_level: Optional[int] = None + ) -> PassManager: """Build scheduling stage PassManager""" scheduling = PassManager() - scheduling.append(IQMOptimizeSingleQubitGates(drop_final_rz=True, ignore_barriers=False)) - # TODO Update the IQMNaiveResonatorMoving to use the IQMStarTarget and the transpiler_insert_moves function - if isinstance(pass_manager_config.target, IQMStarTarget): + if self.optimize_sqg: scheduling.append( - IQMNaiveResonatorMoving(target=pass_manager_config.target, gate_set=pass_manager_config.basis_gates) + IQMOptimizeSingleQubitGates(drop_final_rz=self.drop_final_rz, ignore_barriers=self.ignore_barriers) + ) + if pass_manager_config.target is None: + raise ValueError("PassManagerConfig must have a target backend set, unable to schedule MoveGate routing.") + if ( + self.move_gate_routing + and isinstance(pass_manager_config.target, IQMTarget) + and "move" in pass_manager_config.target.real_target.operation_names + ): + scheduling.append( + IQMNaiveResonatorMoving( + target=pass_manager_config.target, + gate_set=pass_manager_config.basis_gates, + existing_moves_handling=self.existing_move_handling, + ) ) return scheduling + + +class MoveGateRoutingPlugin(IQMSchedulingPlugin): + """Plugin class for IQM single qubit gate optimization and MoveGate routing as a scheduling stage.""" + + def __init__( + self, + optimize_sqg: bool = True, + drop_final_rz: bool = True, + ignore_barriers: bool = False, + existing_move_handling: Optional[ExistingMoveHandlingOptions] = None, + ) -> None: + super().__init__(True, optimize_sqg, drop_final_rz, ignore_barriers, existing_move_handling) + + +class MoveGateRoutingOnlyPlugin(MoveGateRoutingPlugin): + """Plugin class for MoveGate routing without single qubit gate optimization as a scheduling stage.""" + + def __init__(self): + super().__init__(optimize_sqg=False) + + +class MoveGateRoutingKeepExistingMovesPlugin(MoveGateRoutingPlugin): + """Plugin class for single qubit gate optimization and MoveGate routing where existing moves are kept.""" + + def __init__(self): + super().__init__( + optimize_sqg=True, + existing_move_handling=ExistingMoveHandlingOptions.KEEP, + ) + + +class MoveGateRoutingRemoveExistingMovesPlugin(MoveGateRoutingPlugin): + """Plugin class for single qubit gate optimization and MoveGate routing where existing moves are removed.""" + + def __init__(self): + super().__init__( + optimize_sqg=True, + existing_move_handling=ExistingMoveHandlingOptions.REMOVE, + ) + + +class MoveGateRoutingTrustExistingMovesPlugin(MoveGateRoutingPlugin): + """Plugin class for single qubit gate optimization and MoveGate routing where existing moves are not checked.""" + + def __init__(self): + super().__init__( + optimize_sqg=True, + existing_move_handling=ExistingMoveHandlingOptions.TRUST, + ) + + +class MoveGateRoutingWithExactRzPlugin(MoveGateRoutingPlugin): + """Plugin class for single qubit gate optimization and MoveGate routing where + trailing Rz Gates are kept in the circuit. + """ + + def __init__(self): + super().__init__(optimize_sqg=True, drop_final_rz=False) + + +class MoveGateRoutingWithRzOptimizationIgnoreBarriersPlugin(MoveGateRoutingPlugin): + """Plugin class for single qubit gate optimization and MoveGate routing where barriers are ignored during + optimization. + """ + + def __init__(self): + super().__init__( + optimize_sqg=True, + ignore_barriers=True, + ) + + +class MoveGateRoutingOnlyKeepExistingMovesPlugin(MoveGateRoutingPlugin): + """Plugin class for MoveGate routing without single qubit gate optimization + where existing moves are kept.""" + + def __init__(self): + super().__init__( + optimize_sqg=False, + existing_move_handling=ExistingMoveHandlingOptions.KEEP, + ) + + +class MoveGateRoutingOnlyRemoveExistingMovesPlugin(MoveGateRoutingPlugin): + """Plugin class for MoveGate routing without single qubit gate optimization + where existing moves are removed.""" + + def __init__(self): + super().__init__( + optimize_sqg=False, + existing_move_handling=ExistingMoveHandlingOptions.REMOVE, + ) + + +class MoveGateRoutingOnlyTrustExistingMovesPlugin(MoveGateRoutingPlugin): + """Plugin class for MoveGate routing without single qubit gate optimization + where existing moves are not checked.""" + + def __init__(self): + super().__init__( + optimize_sqg=False, + existing_move_handling=ExistingMoveHandlingOptions.TRUST, + ) + + +class OnlyRzOptimizationPlugin(IQMSchedulingPlugin): + """Plugin class for single qubit gate optimization without MOVE gate routing.""" + + def __init__( + self, + drop_final_rz=True, + ignore_barriers=False, + ): + super().__init__(False, True, drop_final_rz, ignore_barriers, None) + + +class OnlyRzOptimizationExactPlugin(OnlyRzOptimizationPlugin): + """Plugin class for single qubit gate optimization without MOVE gate routing and + the final Rz gates are not dropped. + """ + + def __init__(self): + super().__init__(drop_final_rz=False) + + +class OnlyRzOptimizationIgnoreBarriersPlugin(OnlyRzOptimizationPlugin): + """Plugin class for single qubit gate optimization without MOVE gate routing where barriers are ignored.""" + + def __init__(self): + super().__init__(ignore_barriers=True) diff --git a/tests/conftest.py b/tests/conftest.py index 9fcdb6e9a..05bf9ec26 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -203,7 +203,7 @@ def adonis_coupling_map(): @pytest.fixture def deneb_coupling_map(): - return {(1, 0), (0, 1), (2, 0), (0, 2), (3, 0), (0, 3), (4, 0), (0, 4), (5, 0), (0, 5), (6, 0), (0, 6)} + return {(1, 6), (6, 1), (2, 6), (6, 2), (3, 6), (6, 3), (4, 6), (6, 4), (5, 6), (6, 5), (6, 0), (0, 6)} @pytest.fixture diff --git a/tests/fake_backends/test_fake_deneb.py b/tests/fake_backends/test_fake_deneb.py index e33ddcb33..2608405aa 100644 --- a/tests/fake_backends/test_fake_deneb.py +++ b/tests/fake_backends/test_fake_deneb.py @@ -20,8 +20,10 @@ from qiskit import ClassicalRegister, QuantumCircuit, QuantumRegister, transpile from qiskit_aer.noise.noise_model import NoiseModel +from iqm.iqm_client import CircuitTranspilationError, CircuitValidationError, ExistingMoveHandlingOptions from iqm.qiskit_iqm import IQMCircuit, transpile_to_IQM from iqm.qiskit_iqm.fake_backends.fake_deneb import IQMFakeDeneb +from iqm.qiskit_iqm.iqm_backend import IQMTarget def test_iqm_fake_deneb(): @@ -32,8 +34,9 @@ def test_iqm_fake_deneb(): def test_iqm_fake_deneb_connectivity(deneb_coupling_map): backend = IQMFakeDeneb() - print(backend.coupling_map) - assert set(backend.coupling_map.get_edges()) == deneb_coupling_map + assert isinstance(backend.target, IQMTarget) + assert set(backend.target.real_target.build_coupling_map()) == set(deneb_coupling_map) + assert set(backend.coupling_map.get_edges()) == {(qb1, qb2) for qb1 in range(6) for qb2 in range(6) if qb1 != qb2} def test_iqm_fake_deneb_noise_model_instantiated(): @@ -43,7 +46,7 @@ def test_iqm_fake_deneb_noise_model_instantiated(): def test_move_gate_sandwich_interrupted_with_single_qubit_gate(): backend = IQMFakeDeneb() - no_qubits = 6 + no_qubits = 1 comp_r = QuantumRegister(1, "comp_r") # Computational resonator q = QuantumRegister(no_qubits, "q") # Qubits c = ClassicalRegister(no_qubits, "c") # Classical register, used for readout @@ -55,12 +58,13 @@ def test_move_gate_sandwich_interrupted_with_single_qubit_gate(): qc.measure(q, c) with pytest.raises( - ValueError, + CircuitValidationError, match=re.escape( - "Operations to qubits Qubit(QuantumRegister(7, 'q'), 1) while their states are moved to a resonator." + "Instruction prx acts on ('QB1',) while the state(s) of {'QB1'} are in a resonator. " + + "Current resonator occupation: {'COMP_R': 'QB1'}" ), ): - backend.run(transpile(qc, backend=backend), shots=1000) + backend.run(transpile_to_IQM(qc, backend=backend, perform_move_routing=False), shots=1000) def test_move_gate_sandwich_interrupted_with_second_move_gate(): @@ -77,13 +81,21 @@ def test_move_gate_sandwich_interrupted_with_second_move_gate(): qc.measure(q, c) with pytest.raises( - ValueError, + CircuitTranspilationError, match=re.escape( - "Cannot apply MOVE on Qubit(QuantumRegister(6, 'q'), 1) because COMP_R already holds the state of " - + "Qubit(QuantumRegister(6, 'q'), 0)." + "Unable to transpile the circuit after validation error: MOVE instruction ('QB3', 'COMP_R') " + + "to an already occupied resonator: {'COMP_R': 'QB2'}." ), ): - backend.run(qc, shots=1000) + transpile_to_IQM( + qc, backend=backend, perform_move_routing=True, existing_moves_handling=ExistingMoveHandlingOptions.KEEP + ) + + with pytest.raises( + CircuitValidationError, + match=re.escape("MOVE instruction ('QB3', 'COMP_R') to an already occupied resonator: {'COMP_R': 'QB2'}."), + ): + backend.run(transpile_to_IQM(qc, backend=backend, perform_move_routing=False), shots=1000) def test_move_gate_not_closed(): @@ -98,13 +110,16 @@ def test_move_gate_not_closed(): qc.measure(q, c) with pytest.raises( - ValueError, + CircuitValidationError, match=re.escape( - "The following resonators are still holding qubit states at the end of the circuit: " - + "Qubit(QuantumRegister(6, 'q'), 0)." + "Instruction measure acts on ('QB2',) while the state(s) of {'QB2'} are in a resonator. " + + "Current resonator occupation: {'COMP_R': 'QB2'}." ), ): - backend.run(qc, shots=1000) + backend.run( + transpile_to_IQM(qc, backend=backend, perform_move_routing=False), + shots=1000, + ) def test_simulate_ghz_circuit_with_iqm_fake_deneb_noise_model_(): @@ -125,7 +140,10 @@ def test_simulate_ghz_circuit_with_iqm_fake_deneb_noise_model_(): qc.barrier() qc.measure(q, c) - job = backend.run(transpile(qc, backend=backend), shots=1000) + job = backend.run( + transpile_to_IQM(qc, backend=backend, perform_move_routing=False), + shots=1000, + ) res = job.result() counts = res.get_counts() @@ -159,6 +177,28 @@ def test_transpile_to_IQM_for_ghz_with_fake_deneb_noise_model(): assert count[0] in ["000000", "111111"] +def test_qiskit_transpile_for_ghz_with_fake_deneb_noise_model(): + backend = IQMFakeDeneb() + num_qb = 6 + qc = QuantumCircuit(6) + qc.h(0) + for qb in range(1, num_qb): + qc.cx(0, qb) + qc.measure_all() + + transpiled_qc = transpile(qc, backend=backend) + + job = backend.run(transpiled_qc, shots=1000) + res = job.result() + counts = res.get_counts() + + # see that 000000 and 111111 states have most counts + largest_two = sorted(counts.items(), key=lambda x: x[1])[-2:] + + for count in largest_two: + assert count[0] in ["000000", "111111"] + + def test_transpiling_works_but_backend_run_doesnt_with_unsupported_gates(): backend = IQMFakeDeneb() num_qb = 6 @@ -167,12 +207,16 @@ def test_transpiling_works_but_backend_run_doesnt_with_unsupported_gates(): qc_list.append(QuantumCircuit(num_qb)) qc_list[0].h(1) - qc_list[1].x(2) - qc_list[2].y(3) - qc_list[3].z(4) + qc_list[1].sdg(2) + qc_list[2].t(3) + qc_list[3].s(4) for qc in qc_list: - backend.run(transpile_to_IQM(qc, backend=backend), shots=1000) + backend.run(transpile(qc, backend=backend), shots=1000) - with pytest.raises(ValueError, match=r"^Operation '[A-Za-z]' is not supported by the backend."): + with pytest.raises( + ValueError, + match=r"^Instruction '[A-Za-z]+' in the circuit 'circuit-\d+' is not natively supported. " + + "You need to transpile the circuit before execution.", + ): backend.run(qc, shots=1000) diff --git a/tests/move_architecture/test_architecture.py b/tests/move_architecture/test_architecture.py index f5662d2ce..92aebc15c 100644 --- a/tests/move_architecture/test_architecture.py +++ b/tests/move_architecture/test_architecture.py @@ -25,22 +25,18 @@ def test_backend_configuration_new(move_architecture): assert move_architecture is not None backend, _client = get_mocked_backend(move_architecture) assert set(backend.target.physical_qubits) == {0, 1, 2, 3, 4, 5, 6} - assert set(backend.target.operation_names) == {'r', 'id', 'cz', 'measure', 'move'} + assert set(backend.target.operation_names) == {'r', 'id', 'cz', 'measure'} assert {f'{o.name}:{o.num_qubits}' for o in backend.target.operations} == { 'measure:1', 'id:1', 'r:1', 'cz:2', - 'move:2', } - check_instruction(backend.instructions, 'r', [(1,), (2,), (3,), (4,), (5,), (6,)]) - check_instruction(backend.instructions, 'measure', [(1,), (2,), (3,), (4,), (5,), (6,)]) + check_instruction(backend.instructions, 'r', [(0,), (1,), (2,), (3,), (4,), (5,)]) + check_instruction(backend.instructions, 'measure', [(0,), (1,), (2,), (3,), (4,), (5,)]) check_instruction(backend.instructions, 'id', [(0,), (1,), (2,), (3,), (4,), (5,), (6,)]) - check_instruction( - backend.instructions, 'cz', [(1, 0), (0, 1), (2, 0), (0, 2), (3, 0), (0, 3), (4, 0), (0, 4), (5, 0), (0, 5)] - ) - check_instruction(backend.instructions, 'move', [(6, 0)]) + check_instruction(backend.instructions, 'cz', [(i, 5) for i in range(5)] + [(5, i) for i in range(5)]) def test_backend_configuration_adonis(adonis_architecture): diff --git a/tests/move_architecture/test_move_circuit.py b/tests/move_architecture/test_move_circuit.py index 9a94f8a53..7fad286e6 100644 --- a/tests/move_architecture/test_move_circuit.py +++ b/tests/move_architecture/test_move_circuit.py @@ -15,11 +15,13 @@ """ import pytest from qiskit import QuantumCircuit +from qiskit.compiler import transpile from qiskit.transpiler import TranspilerError from iqm.qiskit_iqm.iqm_circuit import IQMCircuit +from iqm.qiskit_iqm.iqm_circuit_validation import validate_circuit from iqm.qiskit_iqm.move_gate import MoveGate -from tests.utils import describe_instruction, get_transpiled_circuit_json +from tests.utils import describe_instruction, get_mocked_backend, get_transpiled_circuit_json def test_move_gate_trivial_layout(move_architecture): @@ -31,22 +33,25 @@ def test_move_gate_trivial_layout(move_architecture): qc.append(MoveGate(), [6, 0]) submitted_circuit = get_transpiled_circuit_json(qc, move_architecture) assert [describe_instruction(i) for i in submitted_circuit.instructions] == [ - 'move:6,0', - 'cz:0,3', - 'cz:2,0', - 'move:6,0', + 'move:QB6,COMP_R', + 'cz:COMP_R,QB4', + 'cz:QB3,COMP_R', + 'move:QB6,COMP_R', ] def test_move_gate_nontrivial_layout(move_architecture): """ - For now only trivial layouts (1-to-1 mapping between virtual and physical qubits) are supported - if there are qubit connections that don't have all operations specified. + Test whether the transpiler can find a layout for a nontrivial circuit. """ qc = QuantumCircuit(7) - qc.append(MoveGate(), [3, 0]) - with pytest.raises(TranspilerError): - get_transpiled_circuit_json(qc, move_architecture) + qc.append(MoveGate(), [3, 4]) + qc.append(MoveGate(), [3, 4]) + submitted_circuit = get_transpiled_circuit_json(qc, move_architecture) + assert [describe_instruction(i) for i in submitted_circuit.instructions] == [ + 'move:QB6,COMP_R', + 'move:QB6,COMP_R', + ] def test_mapped_move_qubit(move_architecture): @@ -58,8 +63,12 @@ def test_mapped_move_qubit(move_architecture): qc.append(MoveGate(), [3, 0]) qc.cz(0, 2) qc.append(MoveGate(), [3, 0]) - submitted_circuit = get_transpiled_circuit_json(qc, move_architecture, create_move_layout=True) - assert [describe_instruction(i) for i in submitted_circuit.instructions] == ['move:6,0', 'cz:0,2', 'move:6,0'] + submitted_circuit = get_transpiled_circuit_json(qc, move_architecture) + assert [describe_instruction(i) for i in submitted_circuit.instructions] == [ + 'move:QB6,COMP_R', + 'cz:COMP_R,QB3', + 'move:QB6,COMP_R', + ] def test_mapped_move_qubit_and_resonator(move_architecture): @@ -71,15 +80,14 @@ def test_mapped_move_qubit_and_resonator(move_architecture): qc.cz(2, 0) qc.move(5, 2) qc.h(5) - submitted_circuit = get_transpiled_circuit_json(qc, move_architecture, create_move_layout=True) + submitted_circuit = get_transpiled_circuit_json(qc, move_architecture) assert [describe_instruction(i) for i in submitted_circuit.instructions] == [ - 'cz:0,4', - 'move:6,0', - 'cz:0,1', - 'cz:0,2', - 'move:6,0', - 'prx:6', - 'prx:6', + 'cz:COMP_R,QB5', + 'move:QB6,COMP_R', + 'cz:COMP_R,QB2', + 'cz:COMP_R,QB1', + 'move:QB6,COMP_R', + 'prx:QB6', ] @@ -88,7 +96,7 @@ def test_cant_layout_two_resonators(move_architecture): qc.append(MoveGate(), [0, 6]) qc.append(MoveGate(), [3, 6]) with pytest.raises(TranspilerError): - get_transpiled_circuit_json(qc, move_architecture, create_move_layout=True) + get_transpiled_circuit_json(qc, move_architecture) def test_cant_layout_two_move_qubits(move_architecture): @@ -96,7 +104,7 @@ def test_cant_layout_two_move_qubits(move_architecture): qc.append(MoveGate(), [0, 6]) qc.append(MoveGate(), [0, 4]) with pytest.raises(TranspilerError): - get_transpiled_circuit_json(qc, move_architecture, create_move_layout=True) + get_transpiled_circuit_json(qc, move_architecture) def test_transpiled_circuit(move_architecture): @@ -114,21 +122,23 @@ def test_transpiled_circuit(move_architecture): qc.measure(3, 1) submitted_circuit = get_transpiled_circuit_json(qc, move_architecture, seed_transpiler=1, optimization_level=0) assert [describe_instruction(i) for i in submitted_circuit.instructions] == [ - # h(4) is moved before the move gate - 'prx:4', - 'prx:4', - # move(6, 0) - 'move:6,0', - # cz(0, 3) - 'cz:0,3', - # cz(4, 0) is optimized before h(6) - 'cz:4,0', - # h(6) - # barrier() - 'barrier:0,1,2,3,4,5,6', - # move (6, 0) - 'move:6,0', - # measurements - 'measure:6', - 'measure:3', + 'prx:QB5', + 'move:QB6,COMP_R', + 'cz:COMP_R,QB4', + 'cz:QB5,COMP_R', + 'barrier:COMP_R,QB2,QB3,QB4,QB5,QB1,QB6', + 'move:QB6,COMP_R', + 'measure:QB6', + 'measure:QB4', ] + + +@pytest.mark.parametrize('optimization_level', list(range(4))) +def test_qiskit_native_transpiler(move_architecture, optimization_level): + backend, _ = get_mocked_backend(move_architecture) + qc = QuantumCircuit(2) + qc.h(0) + qc.cx(0, 1) + qc.measure_all() + transpiled_circuit = transpile(qc, backend=backend, optimization_level=optimization_level) + validate_circuit(transpiled_circuit, backend) diff --git a/tests/test_iqm_backend.py b/tests/test_iqm_backend.py index d097ad1b2..ffa519689 100644 --- a/tests/test_iqm_backend.py +++ b/tests/test_iqm_backend.py @@ -60,7 +60,6 @@ def circuit_2() -> QuantumCircuit: @pytest.fixture def create_run_request_default_kwargs(linear_3q_architecture) -> dict: return { - 'qubit_mapping': None, 'calibration_set_id': linear_3q_architecture.calibration_set_id, 'shots': 1024, 'options': ANY, @@ -119,9 +118,7 @@ def test_serialize_circuit_raises_error_for_non_transpiled_circuit(circuit, line backend = IQMBackend(client) circuit = QuantumCircuit(3) circuit.cz(0, 2) - with pytest.raises( - CircuitValidationError, match=re.escape("'0', '2') = ('QB1', 'QB3') is not allowed as locus for 'cz'") - ): + with pytest.raises(CircuitValidationError, match=re.escape("('QB1', 'QB3') is not allowed as locus for 'cz'")): backend.run(circuit) @@ -162,7 +159,7 @@ def test_serialize_circuit_maps_r_gate(circuit, gate, expected_angle, expected_p assert len(circuit_ser.instructions) == 1 instr = circuit_ser.instructions[0] assert instr.name == 'prx' - assert instr.qubits == ('0',) + assert instr.qubits == ('QB1',) # Serialized angles should be in full turns assert instr.args['angle_t'] == expected_angle assert instr.args['phase_t'] == expected_phase @@ -185,7 +182,7 @@ def test_serialize_circuit_maps_x_rx_y_ry_gates(backend, circuit, gate, expected assert len(circuit_ser.instructions) == 1 instr = circuit_ser.instructions[0] assert instr.name == 'prx' - assert instr.qubits == ('0',) + assert instr.qubits == ('QB1',) assert instr.args['angle_t'] == expected_angle assert instr.args['phase_t'] == expected_phase @@ -195,7 +192,7 @@ def test_serialize_circuit_maps_cz_gate(circuit, backend): circuit_ser = backend.serialize_circuit(circuit) assert len(circuit_ser.instructions) == 1 assert circuit_ser.instructions[0].name == 'cz' - assert circuit_ser.instructions[0].qubits == ('0', '2') + assert circuit_ser.instructions[0].qubits == ('QB1', 'QB3') assert circuit_ser.instructions[0].args == {} @@ -207,7 +204,7 @@ def test_serialize_circuit_maps_individual_measurements(circuit, backend): assert len(circuit_ser.instructions) == 3 for i, instruction in enumerate(circuit_ser.instructions): assert instruction.name == 'measure' - assert instruction.qubits == (f'{i}',) + assert instruction.qubits == (f'QB{i+1}',) key = f'c_3_0_{i}' assert instruction.args == {'key': key} @@ -218,7 +215,7 @@ def test_serialize_circuit_batch_measurement(circuit, backend): assert len(circuit_ser.instructions) == 3 for i, instruction in enumerate(circuit_ser.instructions): assert instruction.name == 'measure' - assert instruction.qubits == (f'{i}',) + assert instruction.qubits == (f'QB{i+1}',) key = f'c_3_0_{i}' assert instruction.args == {'key': key} @@ -229,7 +226,7 @@ def test_serialize_circuit_barrier(circuit, backend): circuit_ser = backend.serialize_circuit(circuit) assert len(circuit_ser.instructions) == 2 assert circuit_ser.instructions[1].name == 'barrier' - assert circuit_ser.instructions[1].qubits == ('0', '1') + assert circuit_ser.instructions[1].qubits == ('QB1', 'QB2') assert circuit_ser.instructions[1].args == {} @@ -367,7 +364,7 @@ def test_run_non_native_circuit(backend, circuit, job_id, run_request): def test_run_single_circuit(backend, circuit, create_run_request_default_kwargs, job_id, run_request): circuit.measure(0, 0) circuit_ser = backend.serialize_circuit(circuit) - kwargs = create_run_request_default_kwargs | {'qubit_mapping': {'0': 'QB1'}} + kwargs = create_run_request_default_kwargs when(backend.client).create_run_request([circuit_ser], **kwargs).thenReturn(run_request) when(backend.client).submit_run_request(run_request).thenReturn(job_id) job = backend.run(circuit) @@ -402,7 +399,7 @@ def test_run_with_custom_number_of_shots( ): # pylint: disable=too-many-arguments circuit.measure(0, 0) - kwargs = create_run_request_default_kwargs | {'shots': shots, 'qubit_mapping': {'0': 'QB1'}} + kwargs = create_run_request_default_kwargs | {'shots': shots} when(backend.client).create_run_request(ANY, **kwargs).thenReturn(run_request) when(backend.client).submit_run_request(run_request).thenReturn(job_id) backend.run(circuit, shots=shots) @@ -433,7 +430,6 @@ def test_backend_run_with_custom_calibration_set_id( circuit_ser = backend.serialize_circuit(circuit) kwargs = create_run_request_default_kwargs | { 'calibration_set_id': expected_id, - 'qubit_mapping': {'0': 'QB1'}, } when(backend.client).create_run_request([circuit_ser], **kwargs).thenReturn(run_request) when(backend.client).submit_run_request(run_request).thenReturn(job_id) @@ -445,7 +441,7 @@ def test_run_with_duration_check_disabled(backend, circuit, create_run_request_d circuit.measure(0, 0) circuit_ser = backend.serialize_circuit(circuit) options = CircuitCompilationOptions(max_circuit_duration_over_t2=0.0) - kwargs = create_run_request_default_kwargs | {'qubit_mapping': {'0': 'QB1'}, 'options': options} + kwargs = create_run_request_default_kwargs | {'options': options} when(backend.client).create_run_request([circuit_ser], **kwargs).thenReturn(run_request) when(backend.client).submit_run_request(run_request).thenReturn(job_id) @@ -460,7 +456,6 @@ def test_run_uses_heralding_mode_none_by_default( default_compilation_options = CircuitCompilationOptions() kwargs = create_run_request_default_kwargs | { 'options': default_compilation_options, - 'qubit_mapping': {'0': 'QB1'}, } when(backend.client).create_run_request([circuit_ser], **kwargs).thenReturn(run_request) when(backend.client).submit_run_request(run_request).thenReturn(job_id) @@ -473,7 +468,6 @@ def test_run_with_heralding_mode_zeros(backend, circuit, create_run_request_defa options = CircuitCompilationOptions(heralding_mode=HeraldingMode.ZEROS) kwargs = create_run_request_default_kwargs | { 'options': options, - 'qubit_mapping': {'0': 'QB1'}, } when(backend.client).create_run_request([circuit_ser], **kwargs).thenReturn(run_request) when(backend.client).submit_run_request(run_request).thenReturn(job_id) @@ -498,7 +492,7 @@ def sample_callback(circuits) -> None: sample_callback.called = False - kwargs = create_run_request_default_kwargs | {'qubit_mapping': {'0': 'QB1', '1': 'QB2', '2': 'QB3'}} + kwargs = create_run_request_default_kwargs when(backend.client).create_run_request(ANY, **kwargs).thenReturn(run_request) when(backend.client).submit_run_request(run_request).thenReturn(job_id) backend.run([qc1, qc2], circuit_callback=sample_callback) @@ -521,7 +515,7 @@ def test_run_batch_of_circuits(backend, circuit, create_run_request_default_kwar circuit.cz(0, 1) circuits = [circuit.assign_parameters({theta: t}) for t in theta_range] circuits_serialized = [backend.serialize_circuit(circuit) for circuit in circuits] - kwargs = create_run_request_default_kwargs | {'qubit_mapping': {'0': 'QB1', '1': 'QB2'}} + kwargs = create_run_request_default_kwargs when(backend.client).create_run_request(circuits_serialized, **kwargs).thenReturn(run_request) when(backend.client).submit_run_request(run_request).thenReturn(job_id) @@ -569,7 +563,7 @@ def test_create_run_request(backend, circuit, create_run_request_default_kwargs, circuit_transpiled = transpile(circuit, backend, **options) circuit_serialized = backend.serialize_circuit(circuit_transpiled) - kwargs = create_run_request_default_kwargs | {'qubit_mapping': {'0': 'QB1', '1': 'QB2', '2': 'QB3'}} + kwargs = create_run_request_default_kwargs # verifies that backend.create_run_request() and backend.run() call client.create_run_request() with same arguments expect(backend.client, times=2).create_run_request( diff --git a/tests/test_iqm_backend_base.py b/tests/test_iqm_backend_base.py index 751beac5f..f4c6f11a7 100644 --- a/tests/test_iqm_backend_base.py +++ b/tests/test_iqm_backend_base.py @@ -49,8 +49,13 @@ def test_qubit_name_to_index_to_qubit_name(adonis_shuffled_names_architecture): backend = DummyIQMBackend(adonis_shuffled_names_architecture) correct_idx_name_associations = set(enumerate(['QB1', 'QB2', 'QB3', 'QB4', 'QB5'])) - assert all(backend.index_to_qubit_name(idx) == name for idx, name in correct_idx_name_associations) - assert all(backend.qubit_name_to_index(name) == idx for idx, name in correct_idx_name_associations) + + print(backend._idx_to_qb) + print(backend._qb_to_idx) + # Unrolled for debugging purposes + for idx, name in correct_idx_name_associations: + assert backend.index_to_qubit_name(idx) == name + assert backend.qubit_name_to_index(name) == idx with pytest.raises(ValueError, match="Qubit index '7' is not part of the backend."): backend.index_to_qubit_name(7) diff --git a/tests/test_iqm_naive_move_pass.py b/tests/test_iqm_naive_move_pass.py index ab92343e9..adc7bfbe6 100644 --- a/tests/test_iqm_naive_move_pass.py +++ b/tests/test_iqm_naive_move_pass.py @@ -4,13 +4,15 @@ import pytest from qiskit.circuit import QuantumCircuit from qiskit.circuit.library import QuantumVolume -from qiskit.circuit.quantumcircuitdata import CircuitInstruction from qiskit.quantum_info import Operator +from iqm.iqm_client import Circuit as IQMCircuit from iqm.qiskit_iqm.iqm_naive_move_pass import transpile_to_IQM -from iqm.qiskit_iqm.iqm_provider import IQMBackend +from iqm.qiskit_iqm.iqm_transpilation import IQMReplaceGateWithUnitaryPass +from iqm.qiskit_iqm.move_gate import MOVE_GATE_UNITARY +from iqm.qiskit_iqm.qiskit_to_iqm import serialize_instructions -from .utils import _get_allowed_ops, _is_valid_instruction, get_mocked_backend +from .utils import get_mocked_backend @pytest.mark.parametrize("n_qubits", list(range(2, 6))) @@ -18,16 +20,17 @@ def test_transpile_to_IQM_star_semantically_preserving( ndonis_architecture, n_qubits ): # pylint: disable=too-many-locals backend, _client = get_mocked_backend(ndonis_architecture) - qubit_registers = _get_qubit_registers(backend) - if len(qubit_registers) >= n_qubits: + n_backend_qubits = backend.target.num_qubits + if n_backend_qubits >= n_qubits: circuit = QuantumVolume(n_qubits, n_qubits) # Use optimization_level=0 to avoid that the qubits get remapped. transpiled_circuit = transpile_to_IQM(circuit, backend, optimization_level=0, remove_final_rzs=False) - transpiled_operator = Operator(transpiled_circuit) - + transpiled_circuit_without_moves = IQMReplaceGateWithUnitaryPass("move", MOVE_GATE_UNITARY)(transpiled_circuit) + print(transpiled_circuit_without_moves) + transpiled_operator = Operator(transpiled_circuit_without_moves) # Update the original circuit to have the correct number of qubits and resonators. - original_with_resonator = QuantumCircuit(backend.num_qubits, 0) - original_with_resonator.append(circuit, [qubit_registers[circuit.find_bit(i)[0]] for i in circuit.qubits]) + original_with_resonator = QuantumCircuit(transpiled_circuit.num_qubits) + original_with_resonator.append(circuit, range(circuit.num_qubits)) # Make it into an Operator and cheeck equivalence. circuit_operator = Operator(original_with_resonator) @@ -37,52 +40,33 @@ def test_transpile_to_IQM_star_semantically_preserving( def test_allowed_gates_only(ndonis_architecture): """Test that transpiled circuit has gates that are allowed by the backend""" backend, _client = get_mocked_backend(ndonis_architecture) - qubit_registers = _get_qubit_registers(backend) - n_qubits = len(qubit_registers) - allowed_ops = _get_allowed_ops(backend) - for i in range(2, n_qubits + 1): + n_qubits = backend.num_qubits + print("test allowed ops backend size", n_qubits) + for i in range(2, n_qubits): circuit = QuantumVolume(i) transpiled_circuit = transpile_to_IQM(circuit, backend) - for instruction in transpiled_circuit.data: - assert _is_valid_instruction(transpiled_circuit, allowed_ops, instruction) + iqm_circuit = IQMCircuit( + name="Transpiling Circuit", + instructions=serialize_instructions(transpiled_circuit, backend._idx_to_qb), + ) + _client._validate_circuit_instructions( + backend.architecture, + [iqm_circuit], + ) def test_moves_with_zero_state(ndonis_architecture): """Test that move gate is applied only when one qubit is in zero state.""" backend, _client = get_mocked_backend(ndonis_architecture) - qubit_registers = _get_qubit_registers(backend) - n_qubits = len(qubit_registers) - for i in range(2, n_qubits + 1): + n_qubits = backend.num_qubits + for i in range(2, n_qubits): circuit = QuantumVolume(i) - resonator_index = next( - j - for j, c in enumerate(backend.architecture.components) - if c in backend.architecture.computational_resonators - ) transpiled_circuit = transpile_to_IQM(circuit, backend) - moves = [instruction for instruction in transpiled_circuit.data if instruction.operation.name == "move"] - assert _is_valid_move_sequence(resonator_index, moves, transpiled_circuit) - - -def _get_qubit_registers(backend: IQMBackend) -> list[int]: - return [ - q - for r in backend.architecture.components - for q in [backend.qubit_name_to_index(r)] - if not r in backend.architecture.computational_resonators - if q is not None - ] - - -def _is_valid_move_sequence(resonator_index: int, moves: list[CircuitInstruction], circuit: QuantumCircuit) -> bool: - if len(moves) == 0: - return True - try: - qubit_to_resonator, qubit_from_resonator, *rest = moves - source_qubit, target_resonator = (circuit.find_bit(q)[0] for q in qubit_to_resonator.qubits) - target_qubit, source_resonator = (circuit.find_bit(q)[0] for q in qubit_from_resonator.qubits) - if source_qubit != target_qubit or target_resonator != resonator_index or source_resonator != resonator_index: - return False - return _is_valid_move_sequence(resonator_index, rest, circuit) - except ValueError: # mismatched number of moves - return False + iqm_json = IQMCircuit( + name="Transpiling Circuit", + instructions=serialize_instructions( + transpiled_circuit, + {backend.qubit_name_to_index(qubit_name): qubit_name for qubit_name in backend.physical_qubits}, + ), + ) + _client._validate_circuit_moves(backend.architecture, iqm_json) diff --git a/tests/test_iqm_provider.py b/tests/test_iqm_provider.py index 3f04e381b..78622dcf7 100644 --- a/tests/test_iqm_provider.py +++ b/tests/test_iqm_provider.py @@ -23,7 +23,8 @@ import requests from iqm.iqm_client import Instruction, IQMClient, RunRequest, RunResult, RunStatus -from iqm.qiskit_iqm.iqm_provider import IQMBackend, IQMFacadeBackend, IQMProvider, _serialize_instructions +from iqm.qiskit_iqm.iqm_provider import IQMBackend, IQMFacadeBackend, IQMProvider +from iqm.qiskit_iqm.qiskit_to_iqm import serialize_instructions from tests.utils import get_mock_ok_response @@ -130,7 +131,7 @@ def test_serialize_instructions_can_allow_nonnative_gates(): mapping = {i: f'QB{i + 1}' for i in range(5)} with pytest.raises(ValueError, match='is not natively supported. You need to transpile'): - _serialize_instructions(circuit, mapping) + serialize_instructions(circuit, mapping) - instructions = _serialize_instructions(circuit, mapping, allowed_nonnative_gates={'nonnative'}) - assert instructions[0] == Instruction.model_construct(name='nonnative', qubits=('1', '2', '4'), args={}) + instructions = serialize_instructions(circuit, mapping, allowed_nonnative_gates={'nonnative'}) + assert instructions[0] == Instruction.model_construct(name='nonnative', qubits=('QB2', 'QB3', 'QB5'), args={}) diff --git a/tests/test_iqm_transpilation.py b/tests/test_iqm_transpilation.py index 9d4e6f740..db46e5dae 100644 --- a/tests/test_iqm_transpilation.py +++ b/tests/test_iqm_transpilation.py @@ -114,19 +114,16 @@ def test_submitted_circuit(adonis_architecture): instr_names = [f"{instr.name}:{','.join(instr.qubits)}" for instr in submitted_circuit.instructions] assert instr_names == [ # Hadamard on 0 (= physical 0) - 'prx:2', - 'prx:2', + 'prx:QB3', # CX phase 1: Hadamard on target qubit 1 (= physical 4) - 'prx:4', - 'prx:4', + 'prx:QB5', # CX phase 2: CZ on 0,1 (= physical 2,4) - 'cz:2,4', + 'cz:QB3,QB5', # Hadamard again on target qubit 1 (= physical 4) - 'prx:4', - 'prx:4', + 'prx:QB5', # Barrier before measurements - 'barrier:2,4', + 'barrier:QB3,QB5', # Measurement on both qubits - 'measure:2', - 'measure:4', + 'measure:QB3', + 'measure:QB5', ] diff --git a/tests/test_utils.py b/tests/test_utils.py deleted file mode 100644 index 2600afcb2..000000000 --- a/tests/test_utils.py +++ /dev/null @@ -1,69 +0,0 @@ -"""Testing utility functions for investigating transpiled circuits. -""" - -from .utils import AllowedOps, _coerce_to_allowed_ops, _get_allowed_ops, _map_operations_to_indices, get_mocked_backend - -ALLOWED_OPS = AllowedOps( - cz=[ - (1, 0), - (0, 1), - (2, 0), - (0, 2), - (3, 0), - (0, 3), - (4, 0), - (0, 4), - (5, 0), - (0, 5), - (6, 0), - (0, 6), - ], - prx=[1, 2, 3, 4, 5, 6], - move=[ - (1, 0), - (2, 0), - (3, 0), - (4, 0), - (5, 0), - (6, 0), - ], - measure=[1, 2, 3, 4, 5, 6], -) - -ALLOWED_OPS_AS_NESTED_LISTS = { - "cz": [ - [1, 0], - [2, 0], - [3, 0], - [4, 0], - [5, 0], - [6, 0], - ], - "prx": [[1], [2], [3], [4], [5], [6]], - "move": [ - [1, 0], - [2, 0], - [3, 0], - [4, 0], - [5, 0], - [6, 0], - ], - "measure": [[1], [2], [3], [4], [5], [6]], -} - - -def test_get_allowed_ops(ndonis_architecture): - backend, _client = get_mocked_backend(ndonis_architecture) - allowed_ops = _get_allowed_ops(backend) - assert ALLOWED_OPS == allowed_ops - - -def test_coerce_to_allowed_ops(): - actual = _coerce_to_allowed_ops(ALLOWED_OPS_AS_NESTED_LISTS) - assert ALLOWED_OPS == actual - - -def test_map_operators_to_indices(ndonis_architecture): - backend, _client = get_mocked_backend(ndonis_architecture) - as_indices = _map_operations_to_indices(backend.architecture.gates, backend.architecture.components) - assert ALLOWED_OPS_AS_NESTED_LISTS == as_indices diff --git a/tests/utils.py b/tests/utils.py index ecfe53b2b..1373f96bc 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -13,32 +13,19 @@ # limitations under the License. """Testing and mocking utility functions. """ -from functools import partial -from typing import Any, Callable, Literal, TypedDict, cast, get_type_hints from unittest.mock import Mock from uuid import UUID from mockito import matchers, when -from qiskit import transpile -from qiskit.circuit import QuantumCircuit, Qubit -from qiskit.circuit.quantumcircuitdata import CircuitInstruction -from qiskit.transpiler.exceptions import TranspilerError +from qiskit.circuit import QuantumCircuit import requests from requests import Response -from iqm.iqm_client import Circuit, DynamicQuantumArchitecture, GateInfo, Instruction, IQMClient -from iqm.qiskit_iqm.iqm_move_layout import generate_initial_layout -from iqm.qiskit_iqm.iqm_provider import IQMBackend - - -class AllowedOps(TypedDict): - cz: list[tuple[int, int]] - prx: list[int] - move: list[tuple[int, int]] - measure: list[int] +from iqm.iqm_client import Circuit, DynamicQuantumArchitecture, Instruction, IQMClient +from iqm.qiskit_iqm import transpile_to_IQM - -ALLOWED_OP_NAMES = get_type_hints(AllowedOps).keys() +# from iqm.qiskit_iqm.iqm_circuit_validation import validate_circuit +from iqm.qiskit_iqm.iqm_provider import IQMBackend def get_mocked_backend(architecture: DynamicQuantumArchitecture) -> tuple[IQMBackend, IQMClient]: @@ -88,8 +75,6 @@ def get_transpiled_circuit_json( architecture: DynamicQuantumArchitecture, seed_transpiler=None, optimization_level=None, - create_move_layout: bool = False, - initial_layout=None, ) -> Circuit: """Configures an IQM backend running against the given architecture, submits the given circuit to it, captures the transpiled circuit and returns it. @@ -97,95 +82,25 @@ def get_transpiled_circuit_json( Returns: the circuit that was transpiled by the IQM backend """ - backend, _client = get_mocked_backend(architecture) + backend, client = get_mocked_backend(architecture) submitted_circuits_batch = capture_submitted_circuits() - - if create_move_layout: - initial_layout = generate_initial_layout(backend, circuit) - transpiled_circuit = transpile( + transpiled_circuit = transpile_to_IQM( circuit, backend, seed_transpiler=seed_transpiler, optimization_level=optimization_level, - initial_layout=initial_layout, + perform_move_routing=False, ) job = backend.run(transpiled_circuit, shots=1000) assert job.job_id() == '00000001-0002-0003-0004-000000000005' assert len(submitted_circuits_batch.all_values) == 1 - assert len(submitted_circuits_batch.value['circuits']) == 1 - return Circuit.model_validate(submitted_circuits_batch.value['circuits'][0]) + return_circuits = submitted_circuits_batch.value['circuits'] + assert len(return_circuits) == 1 + return_circuit = Circuit.model_validate(return_circuits[0]) + client._validate_circuit_instructions(architecture, [return_circuit]) + return return_circuit def describe_instruction(instruction: Instruction) -> str: """Returns a string describing the instruction (includes name and locus).""" return f"{instruction.name}:{','.join(instruction.qubits)}" - - -def _get_allowed_ops(backend: IQMBackend) -> AllowedOps: - ops_with_indices = _map_operations_to_indices(backend.architecture.gates, backend.architecture.components) - return _coerce_to_allowed_ops(ops_with_indices) - - -def _map_operations_to_indices(ops: dict[str, GateInfo], components: tuple[str, ...]) -> dict[str, list[list[int]]]: - return { - op_name: [[components.index(q) for q in valid_operands] for valid_operands in ops[op_name].loci] - for op_name in ALLOWED_OP_NAMES - if op_name in ops - } - - -def _coerce_to_allowed_ops(operations: dict[str, list[list[int]]]) -> AllowedOps: - op_mapping: dict[str, Callable] = { - 'cz': partial(_tuplify, symmetric=True), # Order of operations does not matter for CZ - 'prx': _flatten, - 'move': _tuplify, - 'measure': _flatten, - } - ops = {op_name: fn(operations[op_name]) if op_name in operations else [] for op_name, fn in op_mapping.items()} - return cast(AllowedOps, ops) - - -def _tuplify(valid_operands: list[list[int]], symmetric: bool = False) -> list[tuple[int, int]]: - result: list[tuple[int, int]] = [] - for operands in valid_operands: - if len(operands) != 2: - raise TranspilerError('Binary operation must have two operands') - result.append((operands[0], operands[1])) - if symmetric: - result.append((operands[1], operands[0])) - return result - - -def _flatten(operands: list[list[Any]]) -> list[Any]: - return sum(operands, []) - - -def _is_valid_instruction(circuit: QuantumCircuit, allowed_ops: AllowedOps, instruction: CircuitInstruction) -> bool: - operation_name = instruction.operation.name - if operation_name == 'move': - return _verify_move(circuit, allowed_ops, instruction.qubits) - if operation_name == 'r': - return _verify_r(circuit, allowed_ops, instruction.qubits) - if operation_name == 'cz': - return _verify_cz(circuit, allowed_ops, instruction.qubits) - raise TranspilerError('Unknown operation.') - - -def _make_verify_instruction( - instruction_name: Literal['cz', 'prx', 'move', 'measure'], n_operands: int -) -> Callable[[QuantumCircuit, AllowedOps, tuple[Qubit, ...]], bool]: - def __verify(circuit: QuantumCircuit, allowed_ops: AllowedOps, qubits: tuple[Qubit, ...]) -> bool: - if instruction_name not in allowed_ops: - raise TranspilerError('Operation not supported.') - allowed_operands = allowed_ops[instruction_name] - idx = tuple(circuit.find_bit(q).index for q in qubits) - if len(idx) != n_operands: - raise TranspilerError('Operation got wrong number of operands.') - return (idx[0] if len(idx) == 1 else idx) in allowed_operands - - return __verify - - -_verify_move = _make_verify_instruction('move', 2) -_verify_r = _make_verify_instruction('prx', 1) -_verify_cz = _make_verify_instruction('cz', 2) From a0ad923b6e1d9c25e91d2527bf648b876901899a Mon Sep 17 00:00:00 2001 From: Arianne Meijer Date: Tue, 10 Dec 2024 15:26:32 +0200 Subject: [PATCH 10/47] Qiskit transpiler refactor --- pyproject.toml | 1 + src/iqm/qiskit_iqm/iqm_backend.py | 34 +++++++++- src/iqm/qiskit_iqm/iqm_circuit_validation.py | 9 ++- src/iqm/qiskit_iqm/iqm_move_layout.py | 29 +++++--- src/iqm/qiskit_iqm/iqm_naive_move_pass.py | 23 +++++-- src/iqm/qiskit_iqm/iqm_provider.py | 10 ++- src/iqm/qiskit_iqm/iqm_transpilation.py | 19 +++++- src/iqm/qiskit_iqm/transpiler_plugins.py | 17 +++++ tests/fake_backends/test_fake_deneb.py | 2 +- tests/test_iqm_naive_move_pass.py | 70 ++++++++++++-------- tests/test_iqm_target.py | 32 +++++++++ tests/test_iqm_transpilation.py | 36 ++++++++++ tests/utils.py | 7 +- 13 files changed, 231 insertions(+), 58 deletions(-) create mode 100644 tests/test_iqm_target.py diff --git a/pyproject.toml b/pyproject.toml index 4e9edfa48..4425da073 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -174,4 +174,5 @@ move_routing_Rz_optimization_ignores_barriers = "iqm.qiskit_iqm:MoveGateRoutingW only_Rz_optimization = "iqm.qiskit_iqm:OnlyRzOptimizationPlugin" only_Rz_optimization_exact_global_phase = "iqm.qiskit_iqm:OnlyRzOptimizationExactPlugin" only_Rz_optimization_ignore_barriers = "iqm.qiskit_iqm:OnlyRzOptimizationIgnoreBarriersPlugin" +iqm_default_scheduling = "iqm.qiskit_iqm:IQMDefaultSchedulingPlugin" diff --git a/src/iqm/qiskit_iqm/iqm_backend.py b/src/iqm/qiskit_iqm/iqm_backend.py index 3b2e04e43..a982ef9e9 100644 --- a/src/iqm/qiskit_iqm/iqm_backend.py +++ b/src/iqm/qiskit_iqm/iqm_backend.py @@ -18,7 +18,7 @@ from abc import ABC from copy import deepcopy import re -from typing import Final, Union +from typing import Final, List, Union from uuid import UUID from qiskit.circuit import Parameter, Reset @@ -165,7 +165,7 @@ def index_to_qubit_name(self, index: int) -> str: def get_scheduling_stage_plugin(self) -> str: """Return the plugin that should be used for scheduling the circuits on this backend.""" - return 'move_routing' + return 'iqm_default_scheduling' class IQMTarget(Target): @@ -272,3 +272,33 @@ def set_real_target(self, real_target: IQMTarget) -> None: real_target: The real target to set. """ self.real_target = real_target + + def restrict_to_qubits(self, qubits: Union[List[int], List[str]]) -> IQMTarget: + """Restrict the target to only the given qubits. + + Args: + qubits: The qubits to restrict the target to. Can be either a list of qubit indices or qubit names. + """ + qubits_str = [self.iqm_idx_to_component[q] if isinstance(q, int) else str(q) for q in qubits] + new_gates = {} + for gate_name, gate_info in self.iqm_dynamic_architecture.gates.items(): + new_implementations = {} + for implementation_name, implementation_info in gate_info.implementations.items(): + new_loci = [loci for loci in implementation_info.loci if all(q in qubits_str for q in loci)] + if new_loci: + new_implementations[implementation_name] = GateImplementationInfo(loci=new_loci) + if new_implementations: + new_gates[gate_name] = GateInfo( + implementations=new_implementations, + default_implementation=gate_info.default_implementation, + override_default_implementation=gate_info.override_default_implementation, + ) + new_arch = DynamicQuantumArchitecture( + calibration_set_id=self.iqm_dynamic_architecture.calibration_set_id, + qubits=[q for q in qubits_str if q in self.iqm_dynamic_architecture.qubits], + computational_resonators=[ + q for q in qubits_str if q in self.iqm_dynamic_architecture.computational_resonators + ], + gates=new_gates, + ) + return IQMTarget(new_arch, {name: idx for idx, name in enumerate(qubits_str)}) diff --git a/src/iqm/qiskit_iqm/iqm_circuit_validation.py b/src/iqm/qiskit_iqm/iqm_circuit_validation.py index 507682022..8d6bbb73b 100644 --- a/src/iqm/qiskit_iqm/iqm_circuit_validation.py +++ b/src/iqm/qiskit_iqm/iqm_circuit_validation.py @@ -24,12 +24,17 @@ def validate_circuit( - circuit: QuantumCircuit, backend: IQMBackendBase, validate_moves: Optional[MoveGateValidationMode] = None + circuit: QuantumCircuit, + backend: IQMBackendBase, + validate_moves: Optional[MoveGateValidationMode] = None, + qubit_mapping: Optional[dict[int, str]] = None, ): """Validate a circuit against the backend.""" + if qubit_mapping is None: + qubit_mapping = backend._idx_to_qb new_circuit = IQMClientCircuit( name="Validation circuit", - instructions=serialize_instructions(circuit=circuit, qubit_index_to_name=backend._idx_to_qb), + instructions=serialize_instructions(circuit=circuit, qubit_index_to_name=qubit_mapping), ) if validate_moves is None: validate_moves = MoveGateValidationMode.STRICT diff --git a/src/iqm/qiskit_iqm/iqm_move_layout.py b/src/iqm/qiskit_iqm/iqm_move_layout.py index 71b93d472..986d19743 100644 --- a/src/iqm/qiskit_iqm/iqm_move_layout.py +++ b/src/iqm/qiskit_iqm/iqm_move_layout.py @@ -13,18 +13,21 @@ # limitations under the License. """A layout algorithm that generates an initial layout for a quantum circuit that is valid on the quantum architecture specification of the given IQM backend.""" +from typing import List, Optional, Union + from qiskit import QuantumCircuit from qiskit.dagcircuit import DAGCircuit from qiskit.transpiler import PassManager, TranspilerError from qiskit.transpiler.layout import Layout from qiskit.transpiler.passes import TrivialLayout +from iqm.qiskit_iqm.iqm_backend import IQMTarget from iqm.qiskit_iqm.iqm_provider import IQMBackend class IQMMoveLayout(TrivialLayout): r"""Creates a qubit layout that is valid on the quantum architecture specification of the - given IQM backend with regard to the move gate. In more detail, assumes that the move + given IQM target with regard to the move gate. In more detail, assumes that the move operations in the quantum architecture define which physical qubit is the resonator and which is a move qubit, and shuffles the logical indices of the circuit so that they match the requirements. @@ -35,10 +38,6 @@ class IQMMoveLayout(TrivialLayout): Note: This simple version of the mapper only works reliably with a single move qubit and resonator, and only if the circuit contains at least one move gate.""" - def __init__(self, backend: IQMBackend): - super().__init__(backend.target) - self._backend = backend - def run(self, dag: DAGCircuit): """Creates the qubit layout for the given quantum circuit. @@ -113,17 +112,17 @@ def _get_qubit_types(self) -> dict[int, str]: a dictionary mapping logical indices to qubit types for those qubits where the type is relevant. """ - backend = self._backend + target: IQMTarget = self.target qubit_types: dict[int, str] = {} - for gate_name, gate_info in backend.architecture.gates.items(): + for gate_name, gate_info in target.iqm_dynamic_architecture.gates.items(): if gate_name == 'move': for locus in gate_info.loci: - [qubit, resonator] = [backend.qubit_name_to_index(q) for q in locus] + [qubit, resonator] = [target.iqm_component_to_idx[q] for q in locus] if qubit is not None: qubit_types[qubit] = 'move_qubit' if resonator is not None: qubit_types[resonator] = 'resonator' - for i in range(backend.num_qubits): + for i in range(target.num_qubits): if i not in qubit_types: qubit_types[i] = 'qubit' @@ -166,7 +165,11 @@ def _require_type(qubit_index: int, required_type: str, instruction_name: str): return required_types -def generate_initial_layout(backend: IQMBackend, circuit: QuantumCircuit): +def generate_initial_layout( + backend: IQMBackend, + circuit: QuantumCircuit, + restrict_to_qubits: Optional[Union[List[int], List[str]]] = None, +) -> Layout: """Generates the initial layout for the given circuit, when run against the given backend. Args: @@ -177,7 +180,11 @@ def generate_initial_layout(backend: IQMBackend, circuit: QuantumCircuit): a layout that remaps the qubits so that the move qubit and the resonator are using the correct indices. """ - layout_gen = IQMMoveLayout(backend) + if restrict_to_qubits is not None: + target = backend.target.restrict_to_qubits(restrict_to_qubits) + else: + target = backend.target + layout_gen = IQMMoveLayout(target) pm = PassManager(layout_gen) pm.run(circuit) return layout_gen.get_initial_layout() diff --git a/src/iqm/qiskit_iqm/iqm_naive_move_pass.py b/src/iqm/qiskit_iqm/iqm_naive_move_pass.py index 8a7b5914c..00d323a6b 100644 --- a/src/iqm/qiskit_iqm/iqm_naive_move_pass.py +++ b/src/iqm/qiskit_iqm/iqm_naive_move_pass.py @@ -99,6 +99,7 @@ def transpile_to_IQM( # pylint: disable=too-many-arguments ignore_barriers: bool = False, remove_final_rzs: bool = True, existing_moves_handling: Optional[ExistingMoveHandlingOptions] = None, + restrict_to_qubits: Optional[Union[List[int], List[str]]] = None, **qiskit_transpiler_qwargs, ) -> QuantumCircuit: """Basic function for transpiling to IQM backends. Currently works with Deneb and Garnet @@ -107,13 +108,16 @@ def transpile_to_IQM( # pylint: disable=too-many-arguments circuit: The circuit to be transpiled without MOVE gates. backend: The target backend to compile to. Does not require a resonator. target: An alternative target to compile to than the backend, using this option requires intimate knowledge - of the transpiler and thus it is not recommended to use. + of the transpiler and thus it is not recommended to use. initial_layout: The initial layout to use for the transpilation, same as `qiskit.transpile`. optimize_single_qubits: Whether to optimize single qubit gates away. ignore_barriers: Whether to ignore barriers when optimizing single qubit gates away. - remove_final_rzs: Whether to remove the final Rz rotations. + remove_final_rzs: Whether to remove the final Rz rotations. It is recommended always to set this to true as + the final RZ gates do no change the measurement outcomes of the circuit. existing_moves_handling: How to handle existing MOVE gates in the circuit, required if the circuit contains - MOVE gates. + MOVE gates. + restrict_to_qubits: Restrict the transpilation to only use these specific qubits. Note that you will have to + pass this information qiskit_transpiler_qwargs: Arguments to be passed to the Qiskit transpiler. Returns: @@ -121,17 +125,25 @@ def transpile_to_IQM( # pylint: disable=too-many-arguments """ # pylint: disable=too-many-branches + if restrict_to_qubits is not None: + restrict_to_qubits = [ + backend.qubit_name_to_index(q) if isinstance(q, str) else int(q) for q in restrict_to_qubits + ] + if target is None: if circuit.count_ops().get("move", 0) > 0: target = backend.target.fake_target_with_moves # Create a sensible initial layout if none is provided if initial_layout is None: - initial_layout = generate_initial_layout(backend, circuit) + initial_layout = generate_initial_layout(backend, circuit, restrict_to_qubits) if perform_move_routing and existing_moves_handling is None: raise ValueError("The circuit contains MOVE gates but existing_moves_handling is not set.") else: target = backend.target + if restrict_to_qubits is not None: + target = target.restrict_to_qubits(restrict_to_qubits) + # Determine which scheduling method to use scheduling_method = qiskit_transpiler_qwargs.get("scheduling_method", None) if scheduling_method is None: @@ -167,4 +179,5 @@ def transpile_to_IQM( # pylint: disable=too-many-arguments + "options, ignoring the other arguments." ) qiskit_transpiler_qwargs["scheduling_method"] = scheduling_method - return transpile(circuit, target=target, initial_layout=initial_layout, **qiskit_transpiler_qwargs) + new_circuit = transpile(circuit, target=target, initial_layout=initial_layout, **qiskit_transpiler_qwargs) + return new_circuit diff --git a/src/iqm/qiskit_iqm/iqm_provider.py b/src/iqm/qiskit_iqm/iqm_provider.py index 250c1bab5..c4261e109 100644 --- a/src/iqm/qiskit_iqm/iqm_provider.py +++ b/src/iqm/qiskit_iqm/iqm_provider.py @@ -111,12 +111,14 @@ def run( job.circuit_metadata = [c.metadata for c in run_request.circuits] return job + # pylint: disable=too-many-arguments def create_run_request( self, run_input: Union[QuantumCircuit, list[QuantumCircuit]], shots: int = 1024, circuit_compilation_options: Optional[CircuitCompilationOptions] = None, circuit_callback: Optional[Callable] = None, + qubit_mapping: Optional[dict[int, str]] = None, **unknown_options, ) -> RunRequest: """Creates a run request without submitting it for execution. @@ -174,7 +176,7 @@ def create_run_request( if circuit_callback: circuit_callback(circuits) - circuits_serialized: list[Circuit] = [self.serialize_circuit(circuit) for circuit in circuits] + circuits_serialized: list[Circuit] = [self.serialize_circuit(circuit, qubit_mapping) for circuit in circuits] if self._use_default_calibration_set: default_calset_id = self.client.get_dynamic_quantum_architecture(None).calibration_set_id @@ -207,7 +209,7 @@ def close_client(self) -> None: """Close IQMClient's session with the authentication server.""" self.client.close_auth_session() - def serialize_circuit(self, circuit: QuantumCircuit) -> Circuit: + def serialize_circuit(self, circuit: QuantumCircuit, qubit_mapping: Optional[dict[int, str]] = None) -> Circuit: """Serialize a quantum circuit into the IQM data transfer format. Serializing is not strictly bound to the native gateset, i.e. some gates that are not explicitly mentioned in @@ -232,7 +234,9 @@ def serialize_circuit(self, circuit: QuantumCircuit) -> Circuit: Raises: ValueError: circuit contains an unsupported instruction or is not transpiled in general """ - instructions = serialize_instructions(circuit, self._idx_to_qb) + if qubit_mapping is None: + qubit_mapping = self._idx_to_qb + instructions = serialize_instructions(circuit, qubit_index_to_name=qubit_mapping) try: metadata = to_json_dict(circuit.metadata) diff --git a/src/iqm/qiskit_iqm/iqm_transpilation.py b/src/iqm/qiskit_iqm/iqm_transpilation.py index de205a9cb..386caa263 100644 --- a/src/iqm/qiskit_iqm/iqm_transpilation.py +++ b/src/iqm/qiskit_iqm/iqm_transpilation.py @@ -12,6 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. """Transpilation tool to optimize the decomposition of single-qubit gates tailored to IQM hardware.""" +import warnings import numpy as np from qiskit import QuantumCircuit @@ -41,10 +42,12 @@ class IQMOptimizeSingleQubitGates(TransformationPass): Args: drop_final_rz: Drop terminal RZ gates even if there are no measurements following them (since they do not affect the measurement results). Note that this will change the unitary propagator of the circuit. + It is recommended always to set this to true as the final RZ gates do no change the measurement outcomes of + the circuit. ignore_barriers (bool): Removes the barriers from the circuit before optimization (default = False). """ - def __init__(self, drop_final_rz: bool = False, ignore_barriers: bool = False): + def __init__(self, drop_final_rz: bool = True, ignore_barriers: bool = False): super().__init__() self._basis = ['r', 'cz', 'move'] self._intermediate_basis = ['u', 'cz', 'move'] @@ -101,12 +104,24 @@ def optimize_single_qubit_gates( circuit: quantum circuit to optimise drop_final_rz: Drop terminal RZ gates even if there are no measurements following them (since they do not affect the measurement results). Note that this will change the unitary propagator of the circuit. + It is recommended always to set this to true as the final RZ gates do no change the measurement outcomes of + the circuit. ignore_barriers (bool): Removes barriers from the circuit if they exist (default = False) before optimization. Returns: optimised circuit """ - return PassManager(IQMOptimizeSingleQubitGates(drop_final_rz, ignore_barriers)).run(circuit) + warnings.warn( + DeprecationWarning( + 'This function is deprecated and will be removed in a later version of `iqm.qiskit_iqm`. ' + + 'Single qubit gate optimization is now automatically applied when running `qiskit.transpile()` on any ' + + 'IQM device. If you want to have more fine grained control over the optimization, please use the ' + + '`iqm.qiskit_iqm.transpile_to_IQM` function.' + ) + ) + new_circuit = PassManager(IQMOptimizeSingleQubitGates(drop_final_rz, ignore_barriers)).run(circuit) + new_circuit._layout = circuit.layout + return new_circuit class IQMReplaceGateWithUnitaryPass(TransformationPass): diff --git a/src/iqm/qiskit_iqm/transpiler_plugins.py b/src/iqm/qiskit_iqm/transpiler_plugins.py index e2d136f7d..b05756c76 100644 --- a/src/iqm/qiskit_iqm/transpiler_plugins.py +++ b/src/iqm/qiskit_iqm/transpiler_plugins.py @@ -209,3 +209,20 @@ class OnlyRzOptimizationIgnoreBarriersPlugin(OnlyRzOptimizationPlugin): def __init__(self): super().__init__(ignore_barriers=True) + + +class IQMDefaultSchedulingPlugin(IQMSchedulingPlugin): + """Plugin class for IQM single qubit gate optimization and MoveGate routing as a scheduling stage.""" + + def __init__(self) -> None: + super().__init__( + True, optimize_sqg=True, drop_final_rz=True, ignore_barriers=False, existing_move_handling=None + ) + + def pass_manager( + self, pass_manager_config: PassManagerConfig, optimization_level: Optional[int] = None + ) -> PassManager: + """Build scheduling stage PassManager""" + if optimization_level == 0: + self.optimize_sqg = False + return super().pass_manager(pass_manager_config, optimization_level) diff --git a/tests/fake_backends/test_fake_deneb.py b/tests/fake_backends/test_fake_deneb.py index 2608405aa..924acc91e 100644 --- a/tests/fake_backends/test_fake_deneb.py +++ b/tests/fake_backends/test_fake_deneb.py @@ -164,7 +164,7 @@ def test_transpile_to_IQM_for_ghz_with_fake_deneb_noise_model(): qc.cx(0, qb) qc.measure_all() - transpiled_qc = transpile_to_IQM(qc, backend=backend) + transpiled_qc = transpile_to_IQM(qc, backend=backend, optimize_single_qubits=False) job = backend.run(transpiled_qc, shots=1000) res = job.result() diff --git a/tests/test_iqm_naive_move_pass.py b/tests/test_iqm_naive_move_pass.py index adc7bfbe6..2c38a4eb2 100644 --- a/tests/test_iqm_naive_move_pass.py +++ b/tests/test_iqm_naive_move_pass.py @@ -4,15 +4,18 @@ import pytest from qiskit.circuit import QuantumCircuit from qiskit.circuit.library import QuantumVolume +from qiskit.compiler import transpile from qiskit.quantum_info import Operator -from iqm.iqm_client import Circuit as IQMCircuit +from iqm.qiskit_iqm.fake_backends.fake_adonis import IQMFakeAdonis +from iqm.qiskit_iqm.fake_backends.fake_aphrodite import IQMFakeAphrodite +from iqm.qiskit_iqm.fake_backends.fake_deneb import IQMFakeDeneb +from iqm.qiskit_iqm.iqm_circuit_validation import validate_circuit from iqm.qiskit_iqm.iqm_naive_move_pass import transpile_to_IQM from iqm.qiskit_iqm.iqm_transpilation import IQMReplaceGateWithUnitaryPass from iqm.qiskit_iqm.move_gate import MOVE_GATE_UNITARY -from iqm.qiskit_iqm.qiskit_to_iqm import serialize_instructions -from .utils import get_mocked_backend +from .utils import capture_submitted_circuits, get_mocked_backend @pytest.mark.parametrize("n_qubits", list(range(2, 6))) @@ -37,36 +40,47 @@ def test_transpile_to_IQM_star_semantically_preserving( assert circuit_operator.equiv(transpiled_operator) -def test_allowed_gates_only(ndonis_architecture): +@pytest.mark.parametrize("n_qubits", list(range(2, 6))) +def test_transpile_to_IQM_valid_result(ndonis_architecture, n_qubits): """Test that transpiled circuit has gates that are allowed by the backend""" - backend, _client = get_mocked_backend(ndonis_architecture) - n_qubits = backend.num_qubits - print("test allowed ops backend size", n_qubits) + backend, _ = get_mocked_backend(ndonis_architecture) for i in range(2, n_qubits): circuit = QuantumVolume(i) - transpiled_circuit = transpile_to_IQM(circuit, backend) - iqm_circuit = IQMCircuit( - name="Transpiling Circuit", - instructions=serialize_instructions(transpiled_circuit, backend._idx_to_qb), - ) - _client._validate_circuit_instructions( - backend.architecture, - [iqm_circuit], - ) + transpiled_circuit = transpile_to_IQM(circuit, backend, optimize_single_qubits=False) + validate_circuit(transpiled_circuit, backend) -def test_moves_with_zero_state(ndonis_architecture): +@pytest.mark.parametrize("n_qubits", list(range(2, 6))) +def test_qiskit_transpile_valid_result(ndonis_architecture, n_qubits): """Test that move gate is applied only when one qubit is in zero state.""" - backend, _client = get_mocked_backend(ndonis_architecture) - n_qubits = backend.num_qubits + backend, _ = get_mocked_backend(ndonis_architecture) for i in range(2, n_qubits): circuit = QuantumVolume(i) - transpiled_circuit = transpile_to_IQM(circuit, backend) - iqm_json = IQMCircuit( - name="Transpiling Circuit", - instructions=serialize_instructions( - transpiled_circuit, - {backend.qubit_name_to_index(qubit_name): qubit_name for qubit_name in backend.physical_qubits}, - ), - ) - _client._validate_circuit_moves(backend.architecture, iqm_json) + transpiled_circuit = transpile(circuit, backend) + validate_circuit(transpiled_circuit, backend) + + +@pytest.mark.parametrize( + "fake_backend,restriction", + [ + (IQMFakeAdonis(), ["QB4", "QB3", "QB1"]), + (IQMFakeAphrodite(), ["QB18", "QB17", "QB25"]), + (IQMFakeDeneb(), ["QB5", "QB3", "QB1", "COMP_R"]), + ], +) +def test_transpiling_with_restricted_qubits(fake_backend, restriction): + """Test that the transpiled circuit only uses the qubits specified in the restriction.""" + n_qubits = 3 + circuit = QuantumVolume(n_qubits, seed=42) + for backend in [fake_backend, get_mocked_backend(fake_backend.architecture)[0]]: + restriction_idxs = [backend.qubit_name_to_index(qubit) for qubit in restriction] + for restricted in [restriction, restriction_idxs]: + print("Restriction:", restricted) + transpiled_circuit = transpile_to_IQM(circuit, backend=backend, restrict_to_qubits=restricted) + validate_circuit(transpiled_circuit, backend, qubit_mapping=dict(enumerate(restriction))) + assert transpiled_circuit.num_qubits == len(restricted) + print(transpiled_circuit) + if hasattr(backend, "client"): + # Check that the run doesn't fail. + capture_submitted_circuits() + backend.run(transpiled_circuit, shots=1, qubit_mapping=dict(enumerate(restriction))) diff --git a/tests/test_iqm_target.py b/tests/test_iqm_target.py new file mode 100644 index 000000000..2b040ded2 --- /dev/null +++ b/tests/test_iqm_target.py @@ -0,0 +1,32 @@ +"""Testing IQM transpilation. +""" + +import pytest + +from iqm.qiskit_iqm.fake_backends.fake_adonis import IQMFakeAdonis +from iqm.qiskit_iqm.fake_backends.fake_aphrodite import IQMFakeAphrodite +from iqm.qiskit_iqm.fake_backends.fake_deneb import IQMFakeDeneb + + +@pytest.mark.parametrize( + "backend,restriction", + [ + (IQMFakeAdonis(), ["QB4", "QB3", "QB1"]), + (IQMFakeAphrodite(), ["QB18", "QB17", "QB25"]), + (IQMFakeDeneb(), ["QB5", "QB3", "QB1", "COMP_R"]), + ], +) +def test_transpiling_with_restricted_qubits(backend, restriction): + """Test that the transpiled circuit only uses the qubits specified in the restriction.""" + restriction_idxs = [backend.qubit_name_to_index(qubit) for qubit in restriction] + for restricted in [restriction, restriction_idxs]: + print("Restriction:", restricted) + restricted_target = backend.target.restrict_to_qubits(restricted) + assert restricted_target.num_qubits == len(restricted) + + for edge in restricted_target.build_coupling_map().get_edges(): + translated_edge = ( + backend.qubit_name_to_index(restriction[edge[0]]), + backend.qubit_name_to_index(restriction[edge[1]]), + ) + assert translated_edge in backend.coupling_map.get_edges() diff --git a/tests/test_iqm_transpilation.py b/tests/test_iqm_transpilation.py index db46e5dae..b0f26ea52 100644 --- a/tests/test_iqm_transpilation.py +++ b/tests/test_iqm_transpilation.py @@ -19,6 +19,10 @@ from qiskit import QuantumCircuit, transpile from qiskit_aer import AerSimulator +from iqm.qiskit_iqm.fake_backends.fake_adonis import IQMFakeAdonis +from iqm.qiskit_iqm.fake_backends.fake_aphrodite import IQMFakeAphrodite +from iqm.qiskit_iqm.fake_backends.fake_deneb import IQMFakeDeneb +from iqm.qiskit_iqm.iqm_move_layout import generate_initial_layout from iqm.qiskit_iqm.iqm_transpilation import optimize_single_qubit_gates from tests.utils import get_transpiled_circuit_json @@ -127,3 +131,35 @@ def test_submitted_circuit(adonis_architecture): 'measure:QB3', 'measure:QB5', ] + + +@pytest.mark.parametrize('backend', [IQMFakeAdonis(), IQMFakeDeneb(), IQMFakeAphrodite()]) +def test_optimize_single_qubit_gates_preserves_layout(backend): + """Test that a circuit submitted via IQM backend gets transpiled into proper JSON.""" + + qc = QuantumCircuit(3) + qc.h(0) + qc.cx(0, 1) + qc.cx(0, 2) + qc.measure_all() + + # In case the layout is not set + qc_optimized = optimize_single_qubit_gates(transpile(qc, basis_gates=['r', 'cz'])) + assert qc_optimized.layout is None + + # In case the layout is set by the user + initial_layout = generate_initial_layout(backend, qc) + transpiled_circuit_alt = transpile(qc, backend=backend, initial_layout=initial_layout) + for physical_qubit, logical_qubit in initial_layout.get_physical_bits().items(): + assert transpiled_circuit_alt.layout.initial_layout[logical_qubit] == physical_qubit + + # In case the layout is set by the transpiler + transpiled_circuit = transpile(qc, backend=backend) + layout = transpiled_circuit.layout + qc_optimized = optimize_single_qubit_gates(transpiled_circuit) + assert layout == qc_optimized.layout + # Transpile automatically runs the optimization pass followed by move gate transpilation, + # so the two circuits should be exactly the same if there are no moves. + # Otherwise, some MoveGate and RGate might be swapped when drawing the circuit + if 'move' not in transpiled_circuit.count_ops(): + assert str(transpiled_circuit.draw(output='text')) == str(qc_optimized.draw(output='text')) diff --git a/tests/utils.py b/tests/utils.py index 1373f96bc..6c3f977fc 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -23,8 +23,7 @@ from iqm.iqm_client import Circuit, DynamicQuantumArchitecture, Instruction, IQMClient from iqm.qiskit_iqm import transpile_to_IQM - -# from iqm.qiskit_iqm.iqm_circuit_validation import validate_circuit +from iqm.qiskit_iqm.iqm_circuit_validation import validate_circuit from iqm.qiskit_iqm.iqm_provider import IQMBackend @@ -82,7 +81,7 @@ def get_transpiled_circuit_json( Returns: the circuit that was transpiled by the IQM backend """ - backend, client = get_mocked_backend(architecture) + backend, _ = get_mocked_backend(architecture) submitted_circuits_batch = capture_submitted_circuits() transpiled_circuit = transpile_to_IQM( circuit, @@ -91,13 +90,13 @@ def get_transpiled_circuit_json( optimization_level=optimization_level, perform_move_routing=False, ) + validate_circuit(transpiled_circuit, backend) job = backend.run(transpiled_circuit, shots=1000) assert job.job_id() == '00000001-0002-0003-0004-000000000005' assert len(submitted_circuits_batch.all_values) == 1 return_circuits = submitted_circuits_batch.value['circuits'] assert len(return_circuits) == 1 return_circuit = Circuit.model_validate(return_circuits[0]) - client._validate_circuit_instructions(architecture, [return_circuit]) return return_circuit From eb275f2e9535cad5db4032cba5e25b3ea8f337da Mon Sep 17 00:00:00 2001 From: "sourav.majumder" Date: Wed, 30 Oct 2024 13:32:36 +0100 Subject: [PATCH 11/47] change the fidelity number to error --- src/iqm/qiskit_iqm/fake_backends/fake_deneb.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/iqm/qiskit_iqm/fake_backends/fake_deneb.py b/src/iqm/qiskit_iqm/fake_backends/fake_deneb.py index 53786d881..830447de3 100644 --- a/src/iqm/qiskit_iqm/fake_backends/fake_deneb.py +++ b/src/iqm/qiskit_iqm/fake_backends/fake_deneb.py @@ -105,12 +105,12 @@ def IQMFakeDeneb() -> IQMFakeBackend: two_qubit_gate_durations={"cz": 120.0, "move": 96.0}, readout_errors={ "COMP_R": {"0": 0.0, "1": 0.0}, - "QB1": {"0": 0.977, "1": 0.977}, - "QB2": {"0": 0.977, "1": 0.977}, - "QB3": {"0": 0.977, "1": 0.977}, - "QB4": {"0": 0.977, "1": 0.977}, - "QB5": {"0": 0.977, "1": 0.977}, - "QB6": {"0": 0.977, "1": 0.977}, + "QB1": {"0": 0.02, "1": 0.02}, + "QB2": {"0": 0.02, "1": 0.02}, + "QB3": {"0": 0.02, "1": 0.02}, + "QB4": {"0": 0.02, "1": 0.02}, + "QB5": {"0": 0.02, "1": 0.02}, + "QB6": {"0": 0.02, "1": 0.02}, }, name="sample-chip", ) From 92a5641712f33990cfa9eeb01fba453641686238 Mon Sep 17 00:00:00 2001 From: Arianne Meijer Date: Tue, 10 Dec 2024 17:11:58 +0200 Subject: [PATCH 12/47] Deneb fix changelog update --- CHANGELOG.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index ca64da119..13e06eaf1 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -2,10 +2,10 @@ Changelog ========= - Version 16.0 ============= +* Fixed Deneb Readout errors to closer resemble reality. Fidelities were reported as errors. `#125 `_ * Refactored :meth:`IQMBackend.create_run_request` to improve user experience when using IQM specific run options. * Updated the documentation for using additional run options with IQM backends. * :meth:`IQMBackendBase.qubit_name_to_index` and :meth:`IQMBackendBase.index_to_qubit_name` now raises an error when using an invalid qubit name or index, rather than returning None. From 99b8ed7a91a9b6451ebe8993b960ca283e840e23 Mon Sep 17 00:00:00 2001 From: Arianne Meijer Date: Tue, 10 Dec 2024 18:42:40 +0200 Subject: [PATCH 13/47] Updated user guide with new transpiler information --- docs/user_guide.rst | 192 +++++++++++++++++++++++++------------------- 1 file changed, 110 insertions(+), 82 deletions(-) diff --git a/docs/user_guide.rst b/docs/user_guide.rst index c081bd5cc..ada3c18ec 100644 --- a/docs/user_guide.rst +++ b/docs/user_guide.rst @@ -407,110 +407,138 @@ Starting from the :ref:`GHZ circuit ` we created above: print(transpiled_circuit.draw(output='text', idle_wires=False)) :: + global phase: 3π/2 + ┌─────────────┐ ┌─────────────┐ ░ ┌─┐ + q_2 -> 5 ┤ R(π/2,3π/2) ├──────────■───────┤ R(π/2,5π/2) ├─░───────┤M├ + ├─────────────┤ │ └─────────────┘ ░ ┌─┐ └╥┘ + q_0 -> 10 ┤ R(π/2,3π/2) ├─■────────■───────────────────────░─┤M├────╫─ + ├─────────────┤ │ ┌─────────────┐ ░ └╥┘┌─┐ ║ + q_1 -> 15 ┤ R(π/2,3π/2) ├─■─┤ R(π/2,5π/2) ├────────────────░──╫─┤M├─╫─ + └─────────────┘ └─────────────┘ ░ ║ └╥┘ ║ + meas: 3/════════════════════════════════════════════════════╩══╩══╩═ + 0 1 2 + +Under the hood the Qiskit transpiler uses the :class:`.IQMDefaultSchedulingPlugin` plugin that automatically adapts the +transpiled circuit from Qiskit to the IQM backend. In particular, if the `optimization_level >= 0`, the plugin will use +the :class:`.IQMOptimizeSingleQubitGates` pass to optimize single-qubit gates, and the :class:`.IQMNaiveResonatorMoving` +to insert :class:`.MoveGate` instructions for devices that have a support resonators. +Alternatively, you can use the :meth:`transpile_to_IQM` function for more precise control over the transpilation process +as documented :ref:`below <_transpile_to_IQM>`. +It is also possible to use one of our other pre-defined transpiler plugins as an argument to :meth:`qiskit.transpile`. +For example, `transpile(cirucit, backend=backend, scheduling_method="only_move_routing_keep")`. Additionally, you can +use any of our transpiler passes to define your own :class:`qiskit.transpiler.PassManager` if you want to assemble +custom transpilation procedures manually. - global phase: π/2 - ┌────────────┐┌────────┐ ┌────────────┐┌────────┐ ░ ┌─┐ - q_2 -> 0 ┤ R(π/2,π/2) ├┤ R(π,0) ├─────────■───────┤ R(π/2,π/2) ├┤ R(π,0) ├─░───────┤M├ - ├────────────┤├────────┤ │ └────────────┘└────────┘ ░ ┌─┐ └╥┘ - q_0 -> 2 ┤ R(π/2,π/2) ├┤ R(π,0) ├─■───────■────────────────────────────────░─┤M├────╫─ - ├────────────┤├────────┤ │ ┌────────────┐ ┌────────┐ ░ └╥┘┌─┐ ║ - q_1 -> 3 ┤ R(π/2,π/2) ├┤ R(π,0) ├─■─┤ R(π/2,π/2) ├──┤ R(π,0) ├─────────────░──╫─┤M├─╫─ - └────────────┘└────────┘ └────────────┘ └────────┘ ░ ║ └╥┘ ║ - meas: 3/═════════════════════════════════════════════════════════════════════╩══╩══╩═ - 0 1 2 +Computational resonators +~~~~~~~~~~~~~~~~~~~~~~~~ -We also provide an optimization pass specific to the native IQM gate set which aims to reduce the number -of single-qubit gates. This optimization expects an already transpiled circuit. As an example, let's apply it to the above circuit: +The IQM Star architecture includes computational resonators as additional QPU components. +Because the resonator is not a real qubit, the standard Qiskit transpiler does not know how to compile for it. +Thus, we have a custom scheduling plugin that adds the necessary :class:`.MoveGate` instructions where necessary. +This plugin is executed automatically when you use the Qiskit transpiler. + +Starting from the :ref:`GHZ circuit ` we created above: .. code-block:: python - from iqm.qiskit_iqm.iqm_transpilation import optimize_single_qubit_gates + from qiskit.compiler import transpile + from iqm.qiskit_iqm import IQMProvider - optimized_circuit = optimize_single_qubit_gates(transpiled_circuit) - print(optimized_circuit.draw(output='text', idle_wires=False)) + resonator_backend = IQMProvider("https://cocos.resonance.meetiqm.com/deneb").get_backend() + transpiled_circuit2 = transpile(circuit, resonator_backend) + + print(transpiled_circuit2.draw(output='text', idle_wires=False)) :: + ┌─────────────┐┌───────┐ ┌───────┐ ░ ┌─┐ + q_0 -> 0 ┤ R(π/2,3π/2) ├┤0 ├──────────────────┤0 ├────────────────░─┤M├────── + ├─────────────┤│ │ ┌─────────────┐│ │ ░ └╥┘┌─┐ + q_1 -> 1 ┤ R(π/2,3π/2) ├┤ ├─■─┤ R(π/2,5π/2) ├┤ ├────────────────░──╫─┤M├─── + ├─────────────┤│ Move │ │ └─────────────┘│ Move │┌─────────────┐ ░ ║ └╥┘┌─┐ + q_2 -> 2 ┤ R(π/2,3π/2) ├┤ ├─┼────────■───────┤ ├┤ R(π/2,5π/2) ├─░──╫──╫─┤M├ + └─────────────┘│ │ │ │ │ │└─────────────┘ ░ ║ ║ └╥┘ + ancilla_3 -> 6 ───────────────┤1 ├─■────────■───────┤1 ├───────────────────╫──╫──╫─ + └───────┘ └───────┘ ║ ║ ║ + meas: 3/══════════════════════════════════════════════════════════════════════╩══╩══╩═ + 0 1 2 + + +Under the hood, the IQM Backend pretends that the resonators do not exist for the Qiskit +transpiler, and then uses an additional transpiler stage defined by the :class:`.IQMDefaultSchedulingPlugin` plugin +introduce the resonators and add :class:`MOVE gates <.MoveGate>` between qubits and resonators as +necessary. For more control over the transpilation process, you can use the :meth:`transpile_to_IQM` function documented +:ref:`here <_transpile_to_IQM>`. - global phase: 3π/2 - ┌─────────────┐ ┌─────────────┐ ░ ┌─┐ - q_0: ┤ R(π/2,3π/2) ├─■─┤ R(π/2,5π/2) ├────────────────░────┤M├─── - ├─────────────┤ │ └─────────────┘ ░ ┌─┐└╥┘ - q_2: ┤ R(π/2,3π/2) ├─■────────■───────────────────────░─┤M├─╫──── - ├─────────────┤ │ ┌─────────────┐ ░ └╥┘ ║ ┌─┐ - q_3: ┤ R(π/2,3π/2) ├──────────■───────┤ R(π/2,5π/2) ├─░──╫──╫─┤M├ - └─────────────┘ └─────────────┘ ░ ║ ║ └╥┘ - meas: 3/════════════════════════════════════════════════════╩══╩══╩═ - 0 1 2 -Under the hood :func:`.optimize_single_qubit_gates` uses :class:`.IQMOptimizeSingleQubitGates` which inherits from -the Qiskit provided class :class:`.TransformationPass` and can also be used directly if you want to assemble -custom transpilation procedures manually. +.. _transpile_to_IQM: +The :meth:`transpile_to_IQM` method +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Computational resonators -~~~~~~~~~~~~~~~~~~~~~~~~ +As an alternative to the native Qiskit transpiler integration, you can use the :meth:`transpile_to_IQM`. +This method is meant for users who want at least one of the following things: + - more fine grained control over the transpiler process without having to figure out which IQM transpiler plugin + to use, + - transpile circuits that already contain a computational resonator, or + - forcefully restrict the transpiler to use a strict subset of qubits on the device. -The IQM Star architecture includes computational resonators as additional QPU components. -Because the resonator is not a real qubit, the standard Qiskit transpiler does not know how to compile for it. -Thus, we have a custom transpile method :func:`.transpile_to_IQM` that can handle QPUs with resonators. +For example, if you want to transpile the circuit with `optimization_level=0` but also apply the single qubit gate +optimization pass, you can do the following, equivalent things: .. code-block:: python - import os - from qiskit import QuantumCircuit - from iqm.qiskit_iqm import IQMProvider, transpile_to_IQM + transpile(circuit, backend=backend, optimization_level=0, scheduling_method='only_Rz_optimization') + transpile_to_IQM(circuit, backend=backend, optimization_level=0, perform_move_routing=False, optimize_single_qubits=True) - circuit = QuantumCircuit(5) - circuit.h(0) - for i in range(1, 5): - circuit.cx(0, i) - circuit.measure_all() +Similarly, if you want to transpile a circuit that already contains a computational resonator, you can do the following: - iqm_server_url = "https://cocos.resonance.meetiqm.com/deneb" - provider = IQMProvider(iqm_server_url) - backend = provider.get_backend() - transpiled_circuit = transpile_to_IQM(circuit, backend) +.. code-block:: python - print(transpiled_circuit) + from iqm.iqm_client.transpile import ExistingMoveHandlingOptions + from iqm.qiskit_iqm import MoveGate + move_circuit = QuantumCircuit(3) + move_circuit.h(0) + move_circuit.append(MoveGate(), [0, 1]) + move_circuit.cx(1, 2) + move_circuit.append(MoveGate(), [0, 1]) + # Using transpile() does not work here, as the circuit contains a MoveGate + transpile_to_IQM(move_circuit, backend=resonator_backend, existing_moves_handling=ExistingMoveHandlingOptions.KEEP) :: + ┌─────────────┐┌───────┐ ┌───────┐ + q_0 -> 0 ┤ R(π/2,3π/2) ├┤0 ├───┤0 ├─────────────── + └─────────────┘│ │ │ │ + ancilla_0 -> 1 ───────────────┤ ├───┤ ├─────────────── + ┌─────────────┐│ │ │ │┌─────────────┐ + q_2 -> 2 ┤ R(π/2,3π/2) ├┤ ├─■─┤ ├┤ R(π/2,5π/2) ├ + └─────────────┘│ │ │ │ │└─────────────┘ + ancilla_1 -> 3 ───────────────┤ Move ├─┼─┤ Move ├─────────────── + │ │ │ │ │ + ancilla_2 -> 4 ───────────────┤ ├─┼─┤ ├─────────────── + │ │ │ │ │ + ancilla_3 -> 5 ───────────────┤ ├─┼─┤ ├─────────────── + │ │ │ │ │ + q_1 -> 6 ───────────────┤1 ├─■─┤1 ├─────────────── + └───────┘ └───────┘ + +And if you want force the compiler to use a strict subset of qubits on the device, you can do the following: +.. code-block:: python - ┌───────┐ ┌───────┐ - Qubit(QuantumRegister(1, 'resonator'), 0) -> 0 ───────────────┤1 ├─■─────────────────■─────────────────■─────────────────■───────────────────┤1 ├──────────── - ┌─────────────┐│ Move │ │ │ │ │ ░ │ Move │ ┌─┐ - Qubit(QuantumRegister(5, 'q'), 0) -> 1 ┤ R(π/2,3π/2) ├┤0 ├─┼─────────────────┼─────────────────┼─────────────────┼─────────────────░─┤0 ├─────────┤M├ - ├─────────────┤└───────┘ │ ┌─────────────┐ │ │ │ ░ └──┬─┬──┘ └╥┘ - Qubit(QuantumRegister(5, 'q'), 1) -> 2 ┤ R(π/2,3π/2) ├──────────■─┤ R(π/2,5π/2) ├─┼─────────────────┼─────────────────┼─────────────────░────┤M├─────────────╫─ - ├─────────────┤ └─────────────┘ │ ┌─────────────┐ │ │ ░ └╥┘ ┌─┐ ║ - Qubit(QuantumRegister(5, 'q'), 2) -> 3 ┤ R(π/2,3π/2) ├────────────────────────────■─┤ R(π/2,5π/2) ├─┼─────────────────┼─────────────────░─────╫────┤M├───────╫─ - ├─────────────┤ └─────────────┘ │ ┌─────────────┐ │ ░ ║ └╥┘┌─┐ ║ - Qubit(QuantumRegister(5, 'q'), 3) -> 4 ┤ R(π/2,3π/2) ├──────────────────────────────────────────────■─┤ R(π/2,5π/2) ├─┼─────────────────░─────╫─────╫─┤M├────╫─ - ├─────────────┤ └─────────────┘ │ ┌─────────────┐ ░ ║ ║ └╥┘┌─┐ ║ - Qubit(QuantumRegister(5, 'q'), 4) -> 5 ┤ R(π/2,3π/2) ├────────────────────────────────────────────────────────────────■─┤ R(π/2,5π/2) ├─░─────╫─────╫──╫─┤M├─╫─ - └─────────────┘ └─────────────┘ ░ ║ ║ ║ └╥┘ ║ - Qubit(QuantumRegister(1, 'ancilla'), 0) -> 6 ───────────────────────────────────────────────────────────────────────────────────────────────────────╫─────╫──╫──╫──╫─ - ║ ║ ║ ║ ║ - c: 5/═══════════════════════════════════════════════════════════════════════════════════════════════════════╩═════╩══╩══╩══╩═ - 1 2 3 4 0 - - -Under the hood, the IQM transpiler pretends that the resonators do not exist for the Qiskit -transpiler, and then uses an additional transpiler pass :class:`.IQMNaiveResonatorMoving` to -introduce the resonators and add :class:`MOVE gates <.MoveGate>` between qubits and resonators as -necessary. If ``optimize_single_qubits=True``, the :class:`.IQMOptimizeSingleQubitGates` pass is -also used. The resulting layout shows a resonator register, a qubit register, a register of unused -qubits, and how they are mapped to the QPU components of the target device. As you can see in the -example, qubit 0 in the original circuit is mapped to qubit 0 of the register ``q``, and its state -is moved into the resonator so that the CZ gates can be performed. Lastly, the state is moved out of -the resonator and back to the qubit so that it can be measured. - -Additionally, if the IQM transpiler is used to transpile for a device that does not have a -resonator, it will simply skip the :class:`.IQMNaiveResonatorMoving` step and transpile with the -Qiskit transpiler and the optional :class:`.IQMOptimizeSingleQubitGates` step. It is also possible -for the user to provide :func:`.transpile_to_IQM` with an ``optimization_level`` in the same manner -as the Qiskit :func:`transpile` function. - + transpile_to_IQM(circuit, backend=backend, restrict_to_qubits=[4,3,8]) + c = transpile_to_IQM(circuit, backend=backend, restrict_to_qubits=['QB5', 'QB4', 'QB9']) + print(c) +:: + global phase: 3π/2 + ┌─────────────┐ ┌─────────────┐ ░ ┌─┐ + q_1 -> 0 ┤ R(π/2,3π/2) ├─■─┤ R(π/2,5π/2) ├────────────────░────┤M├─── + ├─────────────┤ │ └─────────────┘ ░ ┌─┐└╥┘ + q_0 -> 1 ┤ R(π/2,3π/2) ├─■────────■───────────────────────░─┤M├─╫──── + ├─────────────┤ │ ┌─────────────┐ ░ └╥┘ ║ ┌─┐ + q_2 -> 2 ┤ R(π/2,3π/2) ├──────────■───────┤ R(π/2,5π/2) ├─░──╫──╫─┤M├ + └─────────────┘ └─────────────┘ ░ ║ ║ └╥┘ + meas: 3/════════════════════════════════════════════════════╩══╩══╩═ + 0 1 2 Batch execution of circuits --------------------------- @@ -594,7 +622,7 @@ not indicate multiplexing. q_2: ─░──╫──╫─┤M├─░─ ░ ║ ║ └╥┘ ░ meas: 3/════╩══╩══╩═══ - 0 1 2 + 0 1 2 From 33296c14b14cd4cf7a44e58c9dbc2cd1fa8bdbb0 Mon Sep 17 00:00:00 2001 From: Arianne Meijer Date: Tue, 10 Dec 2024 18:47:35 +0200 Subject: [PATCH 14/47] Updated documentation --- CHANGELOG.rst | 1 + src/iqm/qiskit_iqm/iqm_provider.py | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 13e06eaf1..39e87bdb6 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -11,6 +11,7 @@ Version 16.0 * :meth:`IQMBackendBase.qubit_name_to_index` and :meth:`IQMBackendBase.index_to_qubit_name` now raises an error when using an invalid qubit name or index, rather than returning None. * Introduction of `IQMBackendBase.physical_target` and `IQMBackendBase.fake_target` to represent the physical quantum architectures and a Qiskit-compatible version, respectively. * Added support for ``qiskit == 1.2`` and ``qiskit-aer == 1.5``. +* Moved the circuit serialization logic from :class:`IQMProvider` to :mod:`iqm.qiskit_iqm.qiskit_to_iqm`. * Refactoring of the Qiskit transpiler: * The Qiskit transpiler now automatically uses the :class:`IQMOptimizeSingleQubitGates` pass to optimize single-qubit gates if the `optimization_level >= 0`. * You can now use the native Qiskit :meth:`transpile` method to transpile a circuit to the IQM Deneb backend as long as your circuit does not contain any resonators. diff --git a/src/iqm/qiskit_iqm/iqm_provider.py b/src/iqm/qiskit_iqm/iqm_provider.py index c4261e109..3348252e1 100644 --- a/src/iqm/qiskit_iqm/iqm_provider.py +++ b/src/iqm/qiskit_iqm/iqm_provider.py @@ -144,6 +144,8 @@ def create_run_request( As a side effect, you can also use this callback to modify the transpiled circuits in-place, just before execution; however, we do not recommend to use it for this purpose. + qubit_mapping: Mapping from qubit indices in the circuit to qubit names on the device. If not provided, + `self.index_to_qubit` will be used. Returns: The created run request object @@ -227,6 +229,8 @@ def serialize_circuit(self, circuit: QuantumCircuit, qubit_mapping: Optional[dic Args: circuit: quantum circuit to serialize + qubit_mapping: Mapping from qubit indices in the circuit to qubit names on the device. If not provided, + `self.index_to_qubit` will be used. Returns: data transfer object representing the circuit From 547f0eaddfecfdb31940c413443cb79a1bbdaa22 Mon Sep 17 00:00:00 2001 From: Arianne Meijer Date: Tue, 10 Dec 2024 18:53:15 +0200 Subject: [PATCH 15/47] Removed Brittish spelling --- src/iqm/qiskit_iqm/iqm_transpilation.py | 10 +++++----- tests/test_iqm_transpilation.py | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/iqm/qiskit_iqm/iqm_transpilation.py b/src/iqm/qiskit_iqm/iqm_transpilation.py index 18322d053..4d54865e1 100644 --- a/src/iqm/qiskit_iqm/iqm_transpilation.py +++ b/src/iqm/qiskit_iqm/iqm_transpilation.py @@ -27,11 +27,11 @@ class IQMOptimizeSingleQubitGates(TransformationPass): r"""Optimize the decomposition of single-qubit gates for the IQM gate set. - This optimisation pass expects the circuit to be correctly layouted and translated to the IQM architecture + This optimization pass expects the circuit to be correctly layouted and translated to the IQM architecture and raises an error otherwise. - The optimisation logic follows the steps: + The optimization logic follows the steps: - 1. Convert single-qubit gates to :math:`U` gates and combine all neighbouring :math:`U` gates. + 1. Convert single-qubit gates to :math:`U` gates and combine all neighboring :math:`U` gates. 2. Convert :math:`U` gates according to :math:`U(\theta , \phi , \lambda) = ~ RZ(\phi + \lambda) R(\theta, \pi / 2 - \lambda)`. 3. Commute `RZ` gates to the end of the circuit using the fact that `RZ` and `CZ` gates commute, and @@ -115,7 +115,7 @@ def optimize_single_qubit_gates( """Optimize number of single-qubit gates in a transpiled circuit exploiting the IQM specific gate set. Args: - circuit: quantum circuit to optimise + circuit: quantum circuit to optimize drop_final_rz: Drop terminal RZ gates even if there are no measurements following them (since they do not affect the measurement results). Note that this will change the unitary propagator of the circuit. It is recommended always to set this to true as the final RZ gates do no change the measurement outcomes of @@ -123,7 +123,7 @@ def optimize_single_qubit_gates( ignore_barriers (bool): Removes barriers from the circuit if they exist (default = False) before optimization. Returns: - optimised circuit + optimized circuit """ warnings.warn( DeprecationWarning( diff --git a/tests/test_iqm_transpilation.py b/tests/test_iqm_transpilation.py index b0f26ea52..9a9907c79 100644 --- a/tests/test_iqm_transpilation.py +++ b/tests/test_iqm_transpilation.py @@ -95,7 +95,7 @@ def test_optimize_single_qubit_gates_reduces_gate_count(): def test_optimize_single_qubit_gates_raises_on_invalid_basis(): - """Test that optimisation pass raises error if gates other than ``RZ`` and ``CZ`` are provided.""" + """Test that optimization pass raises error if gates other than ``RZ`` and ``CZ`` are provided.""" circuit = QuantumCircuit(1, 1) circuit.h(0) From b5e9e0453815a591736a755f5a0884d99d4b0d4b Mon Sep 17 00:00:00 2001 From: Arianne Meijer Date: Tue, 10 Dec 2024 19:16:45 +0200 Subject: [PATCH 16/47] fixed docs --- docs/user_guide.rst | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/docs/user_guide.rst b/docs/user_guide.rst index ada3c18ec..7f53811bb 100644 --- a/docs/user_guide.rst +++ b/docs/user_guide.rst @@ -407,6 +407,7 @@ Starting from the :ref:`GHZ circuit ` we created above: print(transpiled_circuit.draw(output='text', idle_wires=False)) :: + global phase: 3π/2 ┌─────────────┐ ┌─────────────┐ ░ ┌─┐ q_2 -> 5 ┤ R(π/2,3π/2) ├──────────■───────┤ R(π/2,5π/2) ├─░───────┤M├ @@ -418,14 +419,15 @@ Starting from the :ref:`GHZ circuit ` we created above: meas: 3/════════════════════════════════════════════════════╩══╩══╩═ 0 1 2 + Under the hood the Qiskit transpiler uses the :class:`.IQMDefaultSchedulingPlugin` plugin that automatically adapts the transpiled circuit from Qiskit to the IQM backend. In particular, if the `optimization_level >= 0`, the plugin will use the :class:`.IQMOptimizeSingleQubitGates` pass to optimize single-qubit gates, and the :class:`.IQMNaiveResonatorMoving` to insert :class:`.MoveGate` instructions for devices that have a support resonators. Alternatively, you can use the :meth:`transpile_to_IQM` function for more precise control over the transpilation process -as documented :ref:`below <_transpile_to_IQM>`. +as documented below. It is also possible to use one of our other pre-defined transpiler plugins as an argument to :meth:`qiskit.transpile`. -For example, `transpile(cirucit, backend=backend, scheduling_method="only_move_routing_keep")`. Additionally, you can +For example, `transpile(circuit, backend=backend, scheduling_method="only_move_routing_keep")`. Additionally, you can use any of our transpiler passes to define your own :class:`qiskit.transpiler.PassManager` if you want to assemble custom transpilation procedures manually. @@ -451,6 +453,7 @@ Starting from the :ref:`GHZ circuit ` we created above: print(transpiled_circuit2.draw(output='text', idle_wires=False)) :: + ┌─────────────┐┌───────┐ ┌───────┐ ░ ┌─┐ q_0 -> 0 ┤ R(π/2,3π/2) ├┤0 ├──────────────────┤0 ├────────────────░─┤M├────── ├─────────────┤│ │ ┌─────────────┐│ │ ░ └╥┘┌─┐ @@ -468,20 +471,18 @@ Under the hood, the IQM Backend pretends that the resonators do not exist for th transpiler, and then uses an additional transpiler stage defined by the :class:`.IQMDefaultSchedulingPlugin` plugin introduce the resonators and add :class:`MOVE gates <.MoveGate>` between qubits and resonators as necessary. For more control over the transpilation process, you can use the :meth:`transpile_to_IQM` function documented -:ref:`here <_transpile_to_IQM>`. - +below. -.. _transpile_to_IQM: The :meth:`transpile_to_IQM` method ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ As an alternative to the native Qiskit transpiler integration, you can use the :meth:`transpile_to_IQM`. This method is meant for users who want at least one of the following things: - - more fine grained control over the transpiler process without having to figure out which IQM transpiler plugin - to use, - - transpile circuits that already contain a computational resonator, or - - forcefully restrict the transpiler to use a strict subset of qubits on the device. + +* more fine grained control over the transpiler process without having to figure out which IQM transpiler plugin to use, +* transpile circuits that already contain a computational resonator, or +* forcefully restrict the transpiler to use a strict subset of qubits on the device. For example, if you want to transpile the circuit with `optimization_level=0` but also apply the single qubit gate optimization pass, you can do the following, equivalent things: @@ -505,7 +506,9 @@ Similarly, if you want to transpile a circuit that already contains a computatio move_circuit.append(MoveGate(), [0, 1]) # Using transpile() does not work here, as the circuit contains a MoveGate transpile_to_IQM(move_circuit, backend=resonator_backend, existing_moves_handling=ExistingMoveHandlingOptions.KEEP) + :: + ┌─────────────┐┌───────┐ ┌───────┐ q_0 -> 0 ┤ R(π/2,3π/2) ├┤0 ├───┤0 ├─────────────── └─────────────┘│ │ │ │ @@ -528,7 +531,9 @@ And if you want force the compiler to use a strict subset of qubits on the devic transpile_to_IQM(circuit, backend=backend, restrict_to_qubits=[4,3,8]) c = transpile_to_IQM(circuit, backend=backend, restrict_to_qubits=['QB5', 'QB4', 'QB9']) print(c) + :: + global phase: 3π/2 ┌─────────────┐ ┌─────────────┐ ░ ┌─┐ q_1 -> 0 ┤ R(π/2,3π/2) ├─■─┤ R(π/2,5π/2) ├────────────────░────┤M├─── From ea4d1bc13db1ab898a448f8754fccf726e47f423 Mon Sep 17 00:00:00 2001 From: Ville Bergholm Date: Wed, 11 Dec 2024 20:01:41 +0200 Subject: [PATCH 17/47] Fixes. --- CHANGELOG.rst | 48 +++++++++++++------ docs/user_guide.rst | 32 +++++++------ src/iqm/qiskit_iqm/__init__.py | 2 +- .../fake_backends/iqm_fake_backend.py | 21 ++++---- src/iqm/qiskit_iqm/iqm_backend.py | 12 +++-- src/iqm/qiskit_iqm/iqm_naive_move_pass.py | 2 +- 6 files changed, 69 insertions(+), 48 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 39e87bdb6..3c1a2b009 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -5,24 +5,42 @@ Changelog Version 16.0 ============= -* Fixed Deneb Readout errors to closer resemble reality. Fidelities were reported as errors. `#125 `_ -* Refactored :meth:`IQMBackend.create_run_request` to improve user experience when using IQM specific run options. +* Added support for ``qiskit == 1.2`` and ``qiskit-aer == 0.15``. +* :meth:`IQMBackendBase.qubit_name_to_index` and :meth:`IQMBackendBase.index_to_qubit_name` now + raises an error when using an invalid qubit name or index, rather than returning None. +* Refactored :meth:`IQMBackend.create_run_request` to improve user experience when using IQM + specific run options. * Updated the documentation for using additional run options with IQM backends. -* :meth:`IQMBackendBase.qubit_name_to_index` and :meth:`IQMBackendBase.index_to_qubit_name` now raises an error when using an invalid qubit name or index, rather than returning None. -* Introduction of `IQMBackendBase.physical_target` and `IQMBackendBase.fake_target` to represent the physical quantum architectures and a Qiskit-compatible version, respectively. -* Added support for ``qiskit == 1.2`` and ``qiskit-aer == 1.5``. +* Introduced :attr:`IQMBackendBase.physical_target` and :attr:`IQMBackendBase.fake_target` to + represent the physical quantum architectures and a Qiskit-compatible version, respectively. * Moved the circuit serialization logic from :class:`IQMProvider` to :mod:`iqm.qiskit_iqm.qiskit_to_iqm`. -* Refactoring of the Qiskit transpiler: - * The Qiskit transpiler now automatically uses the :class:`IQMOptimizeSingleQubitGates` pass to optimize single-qubit gates if the `optimization_level >= 0`. - * You can now use the native Qiskit :meth:`transpile` method to transpile a circuit to the IQM Deneb backend as long as your circuit does not contain any resonators. - * There are many new transpiler plugins available that you can use as the `scheduling_method` argument in Qiskit's :meth:`transpile` method. You can find them in following the `Qiskit documentation `_. - * If your circuit contains resonators, and optionally :class:`MoveGate` operations, you can use the :meth:`transpile_to_IQM` method to transpile your circuit to the IQM Deneb backend. - * :meth:`transpile_to_IQM` can now restrict itself to use a restricted set of qubits by specifying the `restrict_to_qubits` argument. You will need to additionally provide a qubit mapping to the :meth:`backend.run` method to ensure that the correct qubits are used. - * Bugfix where the :meth:`transpile_to_IQM` did not retain the circuit layout after transpiling. +* Using the Qiskit transpiler with :class:`IQMBackend`: + + * You can now use the native Qiskit :func:`transpile` function to transpile a circuit to the IQM + Star architecture as long as your initial circuit does not use any resonators. + * The Qiskit transpiler now automatically uses the :class:`IQMOptimizeSingleQubitGates` pass to + optimize single-qubit gates if ``optimization_level >= 0``. + * There are many new transpiler plugins available that you can use as the ``scheduling_method`` + argument in Qiskit's :func:`transpile` function. You can find them in the + `Qiskit documentation `_. + * If your circuit contains resonators, and optionally :class:`MoveGate` operations, you can use + the :func:`transpile_to_IQM` function to transpile your circuit for the IQM Star architecture. + * :func:`transpile_to_IQM` can now restrict itself to use a subset of the qubits by specifying + the ``restrict_to_qubits`` argument. You will need to additionally provide a qubit mapping to the + :meth:`backend.run` method to ensure that the correct qubits are used. + * Bugfix where the :func:`transpile_to_IQM` did not retain the circuit layout after transpiling. + +* Fixed :func:`IQMFakeDeneb` readout errors. Fidelities were reported as errors. * Deprecated features: - * :meth:`optimize_single_qubit_gates` has been deprecated in favor of using the new transpiler plugins or :meth:`transpile_to_IQM`. Additionally, this is now incorporated into the Qiskit transpiler as documented above. - * In :meth:`IQMBackend.create_run_request`, and as a result in :meth:`IQMBackend.run`, the `max_circuit_duration_over_t2` and `heralding_mode` options have been deprecated in favor of using the `CircuitCompilationOptions` class from :mod:`iqm-client`. - * The :class:`IQMBackend` no longer uses Qiskit's `options` attribute to give run options in favor of using the arguments of the :meth:`IQMBackend.run` method directly. + + * :func:`optimize_single_qubit_gates` has been deprecated in favor of using the new transpiler + plugins or :func:`transpile_to_IQM`. Additionally, this is now incorporated into the Qiskit + transpiler as documented above. + * In :meth:`IQMBackend.create_run_request`, and as a result in :meth:`IQMBackend.run`, the + ``max_circuit_duration_over_t2`` and ``heralding_mode`` options have been deprecated in favor of + using the :class:`CircuitCompilationOptions` class from :mod:`iqm.iqm_client`. + * The :class:`IQMBackend` no longer uses Qiskit's ``options`` attribute to give run options in + favor of using the arguments of the :meth:`IQMBackend.run` method directly. Version 15.5 diff --git a/docs/user_guide.rst b/docs/user_guide.rst index 7f53811bb..d028e1b27 100644 --- a/docs/user_guide.rst +++ b/docs/user_guide.rst @@ -420,16 +420,20 @@ Starting from the :ref:`GHZ circuit ` we created above: 0 1 2 -Under the hood the Qiskit transpiler uses the :class:`.IQMDefaultSchedulingPlugin` plugin that automatically adapts the -transpiled circuit from Qiskit to the IQM backend. In particular, if the `optimization_level >= 0`, the plugin will use -the :class:`.IQMOptimizeSingleQubitGates` pass to optimize single-qubit gates, and the :class:`.IQMNaiveResonatorMoving` -to insert :class:`.MoveGate` instructions for devices that have a support resonators. -Alternatively, you can use the :meth:`transpile_to_IQM` function for more precise control over the transpilation process -as documented below. -It is also possible to use one of our other pre-defined transpiler plugins as an argument to :meth:`qiskit.transpile`. -For example, `transpile(circuit, backend=backend, scheduling_method="only_move_routing_keep")`. Additionally, you can -use any of our transpiler passes to define your own :class:`qiskit.transpiler.PassManager` if you want to assemble -custom transpilation procedures manually. +Under the hood the Qiskit transpiler uses the :class:`.IQMDefaultSchedulingPlugin` plugin that +automatically adapts the transpiled circuit from Qiskit to the IQM backend. In particular, if the +``optimization_level >= 0``, the plugin will use the :class:`.IQMOptimizeSingleQubitGates` pass to +optimize single-qubit gates, and the :class:`.IQMNaiveResonatorMoving` to insert :class:`.MoveGate` +instructions for devices that have the IQM Star architecture. Alternatively, you can +use the :func:`transpile_to_IQM` function for more precise control over the transpilation process as +documented below. + +It is also possible to use one of our other pre-defined transpiler plugins as an argument to +:func:`qiskit.transpile`. For example, +``transpile(circuit, backend=backend, scheduling_method="only_move_routing_keep")``. +Additionally, you can use any of our transpiler passes +to define your own :class:`qiskit.transpiler.PassManager` if you want to assemble custom +transpilation procedures manually. Computational resonators @@ -474,11 +478,11 @@ necessary. For more control over the transpilation process, you can use the :met below. -The :meth:`transpile_to_IQM` method -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +The :func:`transpile_to_IQM` function +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -As an alternative to the native Qiskit transpiler integration, you can use the :meth:`transpile_to_IQM`. -This method is meant for users who want at least one of the following things: +As an alternative to the native Qiskit transpiler integration, you can use the :func:`transpile_to_IQM` function. +It is meant for users who want at least one of the following things: * more fine grained control over the transpiler process without having to figure out which IQM transpiler plugin to use, * transpile circuits that already contain a computational resonator, or diff --git a/src/iqm/qiskit_iqm/__init__.py b/src/iqm/qiskit_iqm/__init__.py index ede1bb588..97f925d42 100644 --- a/src/iqm/qiskit_iqm/__init__.py +++ b/src/iqm/qiskit_iqm/__init__.py @@ -1,4 +1,4 @@ -# Copyright 2022 Qiskit on IQM developers +# Copyright 2022-2024 Qiskit on IQM developers # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/src/iqm/qiskit_iqm/fake_backends/iqm_fake_backend.py b/src/iqm/qiskit_iqm/fake_backends/iqm_fake_backend.py index 63fef9866..8a8c4f94f 100644 --- a/src/iqm/qiskit_iqm/fake_backends/iqm_fake_backend.py +++ b/src/iqm/qiskit_iqm/fake_backends/iqm_fake_backend.py @@ -288,14 +288,14 @@ def max_circuits(self) -> Optional[int]: def run(self, run_input: Union[QuantumCircuit, list[QuantumCircuit]], **options) -> JobV1: """ - Run `run_input` on the fake backend using a simulator. + Run ``run_input`` on the fake backend using a simulator. This method runs circuit jobs (an individual or a list of QuantumCircuit or IQMCircuit ) and returns a :class:`~qiskit.providers.JobV1` object. It will run the simulation with a noise model of the fake backend (e.g. Adonis, Deneb). - Validity of move gates is also checked. The method also transpiles circuit - to the native gates so that moves are implemented as unitaries. + Validity of MOVE gates is also checked. The method also transpiles circuit + to the native gates so that MOVEs are implemented as unitaries. Args: run_input: One or more quantum circuits to simulate on the backend. @@ -305,7 +305,7 @@ def run(self, run_input: Union[QuantumCircuit, list[QuantumCircuit]], **options) Raises: ValueError: If empty list of circuits is provided. """ - circuits_aux = [run_input] if isinstance(run_input, (QuantumCircuit)) else run_input + circuits_aux = [run_input] if isinstance(run_input, QuantumCircuit) else run_input if len(circuits_aux) == 0: raise ValueError("Empty list of circuits submitted for execution.") @@ -313,15 +313,12 @@ def run(self, run_input: Union[QuantumCircuit, list[QuantumCircuit]], **options) circuits = [] for circ in circuits_aux: - circ_to_add = circ + validate_circuit(circ, self) - validate_circuit(circ_to_add, self) - - for iqm_gate in [ - g for g in self.noise_model.basis_gates if g not in list(IQM_TO_QISKIT_GATE_NAME.values()) - ]: - circ_to_add = IQMReplaceGateWithUnitaryPass(iqm_gate, GATE_TO_UNITARY[iqm_gate])(circ_to_add) - circuits.append(circ_to_add) + for g in self.noise_model.basis_gates: + if g not in IQM_TO_QISKIT_GATE_NAME.values(): + circ = IQMReplaceGateWithUnitaryPass(g, GATE_TO_UNITARY[g])(circ) + circuits.append(circ) shots = options.get("shots", self.options.shots) diff --git a/src/iqm/qiskit_iqm/iqm_backend.py b/src/iqm/qiskit_iqm/iqm_backend.py index a982ef9e9..681147464 100644 --- a/src/iqm/qiskit_iqm/iqm_backend.py +++ b/src/iqm/qiskit_iqm/iqm_backend.py @@ -143,10 +143,10 @@ def qubit_name_to_index(self, name: str) -> int: Index of the given qubit in the quantum register. Raises: - ValueError if qubit name cannot be found. + ValueError: Qubit name cannot be found on the backend. """ if name not in self._qb_to_idx: - raise ValueError(f"Qubit name '{name}' is not part of the backend.") + raise ValueError(f"Qubit '{name}' is not found on the backend.") return self._qb_to_idx[name] def index_to_qubit_name(self, index: int) -> str: @@ -156,11 +156,13 @@ def index_to_qubit_name(self, index: int) -> str: index: Qubit index in the quantum register. Returns: - Corresponding IQM-style qubit name ('QB1', 'QB2', etc.), or ``None`` if - the given index does not correspond to any qubit on the backend. + Corresponding IQM-style qubit name ('QB1', 'QB2', etc.). + + Raises: + ValueError: Qubit index cannot be found on the backend. """ if index not in self._idx_to_qb: - raise ValueError(f"Qubit index '{index}' is not part of the backend.") + raise ValueError(f"Qubit index {index} is not found on the backend.") return self._idx_to_qb[index] def get_scheduling_stage_plugin(self) -> str: diff --git a/src/iqm/qiskit_iqm/iqm_naive_move_pass.py b/src/iqm/qiskit_iqm/iqm_naive_move_pass.py index be37fa22f..6664d7a71 100644 --- a/src/iqm/qiskit_iqm/iqm_naive_move_pass.py +++ b/src/iqm/qiskit_iqm/iqm_naive_move_pass.py @@ -121,7 +121,7 @@ def transpile_to_IQM( # pylint: disable=too-many-arguments qiskit_transpiler_qwargs: Arguments to be passed to the Qiskit transpiler. Returns: - QuantumCircuit: The transpiled circuit ready for running on the backend. + The transpiled circuit ready for running on the backend. """ # pylint: disable=too-many-branches From 0f5cbe56efc0a90c54e71f8b2393d78aa61a0e27 Mon Sep 17 00:00:00 2001 From: Arianne Meijer Date: Thu, 12 Dec 2024 13:59:09 +0200 Subject: [PATCH 18/47] Changes to reflect supported CZ directions as calibrated in the hardware --- src/iqm/qiskit_iqm/iqm_backend.py | 2 +- tests/conftest.py | 124 +------------------ tests/move_architecture/test_architecture.py | 6 +- tests/move_architecture/test_move_circuit.py | 14 +-- tests/test_iqm_provider.py | 2 +- tests/test_iqm_transpilation.py | 18 +-- 6 files changed, 22 insertions(+), 144 deletions(-) diff --git a/src/iqm/qiskit_iqm/iqm_backend.py b/src/iqm/qiskit_iqm/iqm_backend.py index 165b14ac2..2bb5ee2f3 100644 --- a/src/iqm/qiskit_iqm/iqm_backend.py +++ b/src/iqm/qiskit_iqm/iqm_backend.py @@ -118,7 +118,7 @@ def _create_properties( if 'prx' in operations: target.add_instruction(RGate(Parameter('theta'), Parameter('phi')), _create_properties('prx')) if 'cz' in operations: - target.add_instruction(CZGate(), _create_properties('cz', symmetric=True)) + target.add_instruction(CZGate(), _create_properties('cz')) if 'move' in operations: target.add_instruction(MoveGate(), _create_properties('move')) if 'cc_prx' in operations: diff --git a/tests/conftest.py b/tests/conftest.py index 5faa73c08..a4f975763 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -198,12 +198,12 @@ def move_architecture(): @pytest.fixture def adonis_coupling_map(): - return {(0, 2), (2, 0), (1, 2), (2, 1), (2, 3), (3, 2), (2, 4), (4, 2)} + return {(0, 2), (1, 2), (3, 2), (4, 2)} @pytest.fixture def deneb_coupling_map(): - return {(1, 0), (0, 1), (2, 0), (0, 2), (3, 0), (0, 3), (4, 0), (0, 4), (5, 0), (0, 5), (6, 0), (0, 6)} + return {(1, 0), (2, 0), (3, 0), (4, 0), (5, 0), (6, 0)} @pytest.fixture @@ -271,65 +271,35 @@ def ndonis_architecture(): def apollo_coupling_map(): return { (0, 1), - (1, 0), (0, 3), - (3, 0), (1, 4), - (4, 1), (2, 3), - (3, 2), (7, 2), - (2, 7), (3, 4), - (4, 3), (8, 3), - (3, 8), (4, 5), - (5, 4), (9, 4), - (4, 9), (5, 6), - (6, 5), (10, 5), - (5, 10), (11, 6), - (6, 11), (7, 8), - (8, 7), (7, 12), - (12, 7), (8, 9), - (9, 8), (8, 13), - (13, 8), (9, 10), - (10, 9), (9, 14), - (14, 9), (10, 11), - (11, 10), (15, 10), - (10, 15), (16, 11), - (11, 16), (12, 13), - (13, 12), (13, 14), - (14, 13), (17, 13), - (13, 17), (15, 14), - (14, 15), (18, 14), - (14, 18), (15, 16), - (16, 15), (15, 19), - (19, 15), (17, 18), - (18, 17), (18, 19), - (19, 18), } @@ -337,183 +307,93 @@ def apollo_coupling_map(): def aphrodite_coupling_map(): return { (0, 1), - (1, 0), (0, 4), - (4, 0), (1, 5), - (5, 1), (2, 3), - (3, 2), (2, 8), - (8, 2), (3, 4), - (4, 3), (3, 9), - (9, 3), (4, 5), - (5, 4), (4, 10), - (10, 4), (5, 6), - (6, 5), (5, 11), - (11, 5), (6, 12), - (12, 6), (7, 8), - (8, 7), (7, 15), - (15, 7), (8, 9), - (9, 8), (8, 16), - (16, 8), (9, 10), - (10, 9), (9, 17), - (17, 9), (10, 11), - (11, 10), (10, 18), - (18, 10), (11, 12), - (12, 11), (11, 19), - (19, 11), (12, 13), - (13, 12), (12, 20), - (20, 12), (13, 21), - (21, 13), (14, 15), - (15, 14), (14, 22), - (22, 14), (15, 16), - (16, 15), (15, 23), - (23, 15), (16, 17), - (17, 16), (16, 24), - (24, 16), (17, 18), - (18, 17), (17, 25), - (25, 17), (18, 19), - (19, 18), (18, 26), - (26, 18), (19, 20), - (20, 19), (19, 27), - (27, 19), (20, 21), - (21, 20), (20, 28), - (28, 20), (21, 29), - (29, 21), (22, 23), - (23, 22), (23, 24), - (24, 23), (23, 31), - (31, 23), (24, 25), - (25, 24), (24, 32), - (32, 24), (25, 26), - (26, 25), (25, 33), - (33, 25), (26, 27), - (27, 26), (26, 34), - (34, 26), (27, 28), - (28, 27), (27, 35), - (35, 27), (28, 29), - (29, 28), (28, 36), - (36, 28), (29, 30), - (30, 29), (29, 37), - (37, 29), (30, 38), - (38, 30), (31, 32), - (32, 31), (31, 39), - (39, 31), (32, 33), - (33, 32), (32, 40), - (40, 32), (33, 34), - (34, 33), (33, 41), - (41, 33), (34, 35), - (35, 34), (34, 42), - (42, 34), (35, 36), - (36, 35), (35, 43), - (43, 35), (36, 37), - (37, 36), (36, 44), - (44, 36), (37, 38), - (38, 37), (37, 45), - (45, 37), (39, 40), - (40, 39), (40, 41), - (41, 40), (40, 46), - (46, 40), (41, 42), - (42, 41), (41, 47), - (47, 41), (42, 43), - (43, 42), (42, 48), - (48, 42), (43, 44), - (44, 43), (43, 49), - (49, 43), (44, 45), - (45, 44), (44, 50), - (50, 44), (46, 47), - (47, 46), (47, 48), - (48, 47), (47, 51), - (51, 47), (48, 49), - (49, 48), (48, 52), - (52, 48), (49, 50), - (50, 49), (49, 53), - (53, 49), (51, 52), - (52, 51), (52, 53), - (53, 52), } diff --git a/tests/move_architecture/test_architecture.py b/tests/move_architecture/test_architecture.py index 23539eda2..a234a692a 100644 --- a/tests/move_architecture/test_architecture.py +++ b/tests/move_architecture/test_architecture.py @@ -36,9 +36,7 @@ def test_backend_configuration_new(move_architecture): check_instruction(backend.instructions, 'r', [(1,), (2,), (3,), (4,), (5,), (6,)]) check_instruction(backend.instructions, 'measure', [(1,), (2,), (3,), (4,), (5,), (6,)]) check_instruction(backend.instructions, 'id', [(0,), (1,), (2,), (3,), (4,), (5,), (6,)]) - check_instruction( - backend.instructions, 'cz', [(1, 0), (0, 1), (2, 0), (0, 2), (3, 0), (0, 3), (4, 0), (0, 4), (5, 0), (0, 5)] - ) + check_instruction(backend.instructions, 'cz', [(i, 0) for i in range(1, 6)]) check_instruction(backend.instructions, 'move', [(6, 0)]) @@ -58,7 +56,7 @@ def test_backend_configuration_adonis(adonis_architecture): check_instruction(backend.instructions, 'r', [(0,), (1,), (2,), (3,), (4,)]) check_instruction(backend.instructions, 'measure', [(0,), (1,), (2,), (3,), (4,)]) check_instruction(backend.instructions, 'id', [(0,), (1,), (2,), (3,), (4,)]) - check_instruction(backend.instructions, 'cz', [(0, 2), (2, 0), (1, 2), (2, 1), (3, 2), (2, 3), (4, 2), (2, 4)]) + check_instruction(backend.instructions, 'cz', [(0, 2), (1, 2), (3, 2), (4, 2)]) def check_instruction( diff --git a/tests/move_architecture/test_move_circuit.py b/tests/move_architecture/test_move_circuit.py index 9a94f8a53..c5dcd80c9 100644 --- a/tests/move_architecture/test_move_circuit.py +++ b/tests/move_architecture/test_move_circuit.py @@ -32,7 +32,7 @@ def test_move_gate_trivial_layout(move_architecture): submitted_circuit = get_transpiled_circuit_json(qc, move_architecture) assert [describe_instruction(i) for i in submitted_circuit.instructions] == [ 'move:6,0', - 'cz:0,3', + 'cz:3,0', 'cz:2,0', 'move:6,0', ] @@ -56,10 +56,10 @@ def test_mapped_move_qubit(move_architecture): """ qc = QuantumCircuit(7) qc.append(MoveGate(), [3, 0]) - qc.cz(0, 2) + qc.cz(2, 0) qc.append(MoveGate(), [3, 0]) submitted_circuit = get_transpiled_circuit_json(qc, move_architecture, create_move_layout=True) - assert [describe_instruction(i) for i in submitted_circuit.instructions] == ['move:6,0', 'cz:0,2', 'move:6,0'] + assert [describe_instruction(i) for i in submitted_circuit.instructions] == ['move:6,0', 'cz:2,0', 'move:6,0'] def test_mapped_move_qubit_and_resonator(move_architecture): @@ -73,10 +73,10 @@ def test_mapped_move_qubit_and_resonator(move_architecture): qc.h(5) submitted_circuit = get_transpiled_circuit_json(qc, move_architecture, create_move_layout=True) assert [describe_instruction(i) for i in submitted_circuit.instructions] == [ - 'cz:0,4', + 'cz:4,0', 'move:6,0', - 'cz:0,1', - 'cz:0,2', + 'cz:1,0', + 'cz:2,0', 'move:6,0', 'prx:6', 'prx:6', @@ -120,7 +120,7 @@ def test_transpiled_circuit(move_architecture): # move(6, 0) 'move:6,0', # cz(0, 3) - 'cz:0,3', + 'cz:3,0', # cz(4, 0) is optimized before h(6) 'cz:4,0', # h(6) diff --git a/tests/test_iqm_provider.py b/tests/test_iqm_provider.py index 69149e6ef..5b9d6af59 100644 --- a/tests/test_iqm_provider.py +++ b/tests/test_iqm_provider.py @@ -55,7 +55,7 @@ def test_get_backend(linear_3q_architecture): assert isinstance(backend, IQMBackend) assert backend.client._api.iqm_server_url == url assert backend.num_qubits == 3 - assert set(backend.coupling_map.get_edges()) == {(0, 1), (1, 0), (1, 2), (2, 1)} + assert set(backend.coupling_map.get_edges()) == {(0, 1), (1, 2)} assert backend._calibration_set_id == linear_3q_architecture.calibration_set_id diff --git a/tests/test_iqm_transpilation.py b/tests/test_iqm_transpilation.py index 9d4e6f740..c5f56f80b 100644 --- a/tests/test_iqm_transpilation.py +++ b/tests/test_iqm_transpilation.py @@ -113,20 +113,20 @@ def test_submitted_circuit(adonis_architecture): instr_names = [f"{instr.name}:{','.join(instr.qubits)}" for instr in submitted_circuit.instructions] assert instr_names == [ - # Hadamard on 0 (= physical 0) + # CX phase 1: Hadamard on target qubit 1 (= physical 2) 'prx:2', 'prx:2', - # CX phase 1: Hadamard on target qubit 1 (= physical 4) - 'prx:4', - 'prx:4', - # CX phase 2: CZ on 0,1 (= physical 2,4) - 'cz:2,4', - # Hadamard again on target qubit 1 (= physical 4) + # Hadamard on target qubit 0 (= physical 4) 'prx:4', 'prx:4', + # CX phase 2: CZ on 0,1 (= physical 4,2) + 'cz:4,2', + # CX phase 3: Hadamard again on target qubit 1 (= physical 2) + 'prx:2', + 'prx:2', # Barrier before measurements - 'barrier:2,4', + 'barrier:4,2', # Measurement on both qubits - 'measure:2', 'measure:4', + 'measure:2', ] From 82fc6a3f39f69beaae69e797b84a8d002d36deb3 Mon Sep 17 00:00:00 2001 From: Arianne Meijer Date: Thu, 12 Dec 2024 14:02:54 +0200 Subject: [PATCH 19/47] Update changelog --- CHANGELOG.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index abab2da0b..29755ad9c 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -5,6 +5,11 @@ Changelog Version 15.5 ============ +* Qiskit Target now contains CZ with the directions are calibrated on the hardwared. `#140 `_ + +Version 15.5 +============ + * Fix compatibility with ``iqm-client`` V2 APIVariant. `#132 `_ Version 15.4 From 5fd22e3ef4d0c2a678cdd7d9958cb9f8b1f3f086 Mon Sep 17 00:00:00 2001 From: Arianne Meijer Date: Thu, 12 Dec 2024 14:04:36 +0200 Subject: [PATCH 20/47] fix typo --- CHANGELOG.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 29755ad9c..603c62f45 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -2,7 +2,7 @@ Changelog ========= -Version 15.5 +Version 15.6 ============ * Qiskit Target now contains CZ with the directions are calibrated on the hardwared. `#140 `_ From c56d86c35c88bd1c83169240bb93fc472880c7d7 Mon Sep 17 00:00:00 2001 From: Arianne Meijer Date: Thu, 12 Dec 2024 14:12:54 +0200 Subject: [PATCH 21/47] fix pylint --- .coverage.rev130-154.vpn.iqm.fi.71168.XRtLzOmx | Bin 0 -> 53248 bytes .coverage.rev130-154.vpn.iqm.fi.71168.XiWlaWLx | Bin 0 -> 69632 bytes src/iqm/qiskit_iqm/iqm_backend.py | 4 ++-- 3 files changed, 2 insertions(+), 2 deletions(-) create mode 100644 .coverage.rev130-154.vpn.iqm.fi.71168.XRtLzOmx create mode 100644 .coverage.rev130-154.vpn.iqm.fi.71168.XiWlaWLx diff --git a/.coverage.rev130-154.vpn.iqm.fi.71168.XRtLzOmx b/.coverage.rev130-154.vpn.iqm.fi.71168.XRtLzOmx new file mode 100644 index 0000000000000000000000000000000000000000..9a6ebaa026a317d73cc71b65b1906dcc7641d6b7 GIT binary patch literal 53248 zcmeI)Z*SW~90zba+2STmJb(gyFfnNiDxM&bU7Sm6i0#yN z`sV>c*^nwB@eU9#gm=M9;Mtz>h(|o(_p^O*(l*ncO$%SENgVs`eD}HE-6eImub)2l z5+#OVHtcVVdgWKc{%+%m z-LC((@$UMcb-(({`tO#kx^w~?1Rwwb2>ibVrcdjZv%PIT|1pvMkxC-zswjKj`0T5L z{lf!sxc}+n1CbpQdo@8@yDj!b6rPE(ip0C}^04S1oWW3<$9 z!cLl>_j=xdawY1BiWoO9E~P$9}Y3OI$>%dHRW1lMYmt#JO_|d=2NSl=oKtVl9M_?#UabQGLo+ zGM+?wB6-y$sdE&wq#h&r8EQ`3syO>Q%bJs?>vi*;wW)Q>a<;b2X*MEwUgF2?g+}m^ zVhCJW2wuPXW0@R_>viYDRPn~`iu2LdvWjyRdR>05y!f`1R{3%})%R)A=SO*=%6z1# z@)zo&eJV#Zn~bFw(U7G%I9mY(CmcvhEc@XkN#mE_rs2tJPf_2jfa1Njr&P@}ISubG z!#MV6S>oR9~WiB7hJ~CX(O$Z zUR-#ZKcqpY#~1DRz6=KWdd16dRw~W?JUIC}*(5?4MS})qOhfcanYz&YEYDcF{LYw6 zxKv*bvf<91GFRf+vAR}Vwi+&;y zOJ3uTa?@SYa_-+Zr;~gzX7fxhrbXSCy4v~M(n642Ia$QZ6Pj@8)6RUHu-zO7zs;_| zlUvq<{Y*ip%0KJWn*0UdwEs2e2O9(+009U<00Izz00bZa0SG_<0;?xbHcO_(-~UVY zUxxiRJz;|Y1Rwwb2tWV=5P$##AOHafK;Z2ZsF%w3Z2c!5UE3`0IQbs|e9(En^RPy( zD%md$`=$NQ+u1i02P2muH{00Izz00bZa0SG_<0ucBQ=I0%m literal 0 HcmV?d00001 diff --git a/.coverage.rev130-154.vpn.iqm.fi.71168.XiWlaWLx b/.coverage.rev130-154.vpn.iqm.fi.71168.XiWlaWLx new file mode 100644 index 0000000000000000000000000000000000000000..43af6bf3020792847a5050bf4527754f3b29a845 GIT binary patch literal 69632 zcmeI433wIN-T%+IcjnHSGjnHdCi{K~AR#0K+yFN~K}4a7Bw-5)H^~h|vXMnWP@EZD zTgA1n)}^*u#Uj?Vwr+K)ORE;QqPB{-P?uU;^QvvFZT`P=&;4DXt$p6-ANst{YbTGw z=giDGbLKl|zH@$aeqrH)c^w1EczaJ@S7IQ35Qz~=$&7fM5MsdpS@1vpXaEWp;BUGY ze;FENV)uPfyMdUI3yIwnJ<+Z)uZbRDJa2aC7Z}%R3B46|1V4cSfdYX7fdc>83b+eQ zEx)jkdS?tIS~`;heTmj&pZqg=%*nH7*3FLB%{+SE?6}+(ujFy~si}$2jQ91N7Vk~= z#oIeNlktx3wvN`sKu7n=_`s?p9zEEfY!fd!*aojUap1&MeL>9~ZBT0f_^m$@XMlvb#0eFMCnh(Keos_cwAa zKRcVAn@n9(qOTSHGxoU}*s$kP=C(satv!8hqW3L*iSE`_$^PniB6EeUJIXT7%skYX*~&DV{At;gz^F{F z?O)Rg^R4E@;6P8R+zdCrc{2QAZpva>et9`PcTH-b2zi=YItKbPpzkwiGMm3-=!pGa zK5oQQvhN@fPf=%Qyr(_&$cWl;d2?Vi@BPfwq8N!j@u0_REzcNXzvBxFx8AK`7?{Z>~hk_HBU|hb2UZF&&{Q-d?JJ|iT~AoE<$YC^9cOii{O9W{ocgDs`x+l zU2KMqk5eQ0hv$A-$3+*KCyPHL{<2W2%jXuR)b9e*7ypO%>9W|er^_cK*VUYw=macc<2m8S1$qko-fW96yi zQhmagi?I1Rc7+TLteI52rYfG!dv69|sR*KRlv;*CsZa-YXmn z+n?AAnuu1r5^eHv7dK_E7EfE1{DFiM|k?uVCpR|318E{66QB!9Pa&v-cz z3>RiQdyf;L-ClBtqCA5zH+ctJqzZUcdalFAN1>P0{tJQsf}cQvK!HGkK!HGkK!HGk zK!HGkK!HGkK!HGkK!LBG0%01Wnuz~H_M621J^T^;1PTNS1PTNS1PTNS1PTNS1PTNS z1PTNS1PTNS{NGf-424T=O!1g(({OQqItgIP#6u@e<8W0W`#oa6XMg&C^8$lj2MPoV z1PTNS1PTNS1PTNS1PTNS1PTNS1PTNSpaN#Nl;T%_5DypY>5l+n{XdVLMC?24I_nc_ zvvp+jchQTZN0{%Jt~tth)aW%T_%nPRFV~;YlX{fh0S5;^fdYX7fdYX7fdYX7fdYFe z(440b_n6xHe)!$Z+C*PRqPsgeu`AheO0utZPRGEi!Is)J9sR3225Ne`YdY3+)%N$b z){4L7)@FgpwTZ6Y&SZaWU$Vca8~z1^WOI5G{DSAwTsXnB{d9tsWM^k{`j>>E?S>p^ zd-8s2yScfeyJMia8QN{m-jAZdzedsB-z$FQc#kNH95}&2BkOpk-M;4D&cT%(-Tlz> z#aYnsAtP%TZfSFOqT|$La~J%ceRFT3U$nd+1}%>pdCR?hJ*RfGCHtVsIvbj-8F`bb zZgwWt!S7r{tMe>qwRGgIp3>6-t(_Ew*2a#!wbqWl*1-#t3f+LGN#(cvZonxDDfn@{xibawVY`w0&1&-pjBzpAgN4gRSOI7NdFrx@X5 zEe0_@A@DED^mIeJCo^bw{K&eVw!MKK;e62MG7Z{1d}M8kzNW`VGyKB<9q?=Qvcq4C zK;xrE)_CgSSP8!n51lai{IC6Z~ELpSwtLC-1Me_nKto>`zfJ3fVjU&t9y!2aT}fnRfS%|D7d@d&meI z7BldFjQ_JvR@`y>q2;~e|5&}^*6fES_l^JU`HEY*A6k|1zqLSd$L@#L_Qe0uCdHjT z!WQ?9Hc};w|IKE_ow@&;&&2;mi{j4tceKAJ{^$GQu@-|EpAZbE^@WN%euQ2B`}m(V zD(>MUZ1YRwf9*uY9W}zn_m2M~`)M%miT~9$#XVt!J=}LN|C9J%`8N$|jQ_*?;U4aL z5qrH$LSRSb`p90edo>sQ`+vctEl?m(AW$GsAW$GsAW$GsAW$GsAW$GsAW+~dtw5ND zNmQ)=Q~L`7{{=sR0)Ya70)Ya70)Ya70)Ya70)Ya70)Ya70)YZwK?TC$s4mw3N$e|l zals`83IqxS3IqxS3IqxS3IqxS3IqxS3IqxS3Iqyd6cFqGLHwVQBY;4GK!HGkK!HGk zK!HGkK!HGkK!HGkK!HGkucQJhTotuH7uS|0|N2T^W^hq~0)Ya70)Ya70)Ya70)Ya7 z0)Ya70)Ya70{^)R#JnRyaP`#wgV4a1fBomm5nOqoK%hXNK%hXNK%hXNK%hXNK%hXN zK%hXNz*k5C%ZN~&(vbC1HTm!0_y4~_cQ&|`K!HGkK!HGkK!HGkK!HGkK!HGkK!HGk zK!Gn&K>Ysyo9bi4{-gan`)&JGd%OL(eZPH&eY1V7eVKi}z0n@DSKBM>di!{Lx_z)c z-Y&DVZD#$|8nWIsKZ>4dZLzMk&bCgsdaO2UiS;$>7;CCkV^vr@Ivo8Z`hIk0^oi*G z(etDI(dE%q(PN{BM<+$cMvJ0$R5AZ-K5M>hzGB{Ee&4*#`jz#p^{{o9b+P%dl^cCN zdRz26(aWO?jB}02W~Z4j8_c83gUm{^)O3u`jo%yZ7_SDtNt-xpzqdS)t}WL(eGjJ>rwp{{aXDJ{cL@m-lZq>dVQ`=*{AxE`ec2q zUdUc&&$GwbkJ&c%9d-pfpPj+huq0c;=CSE)G8@YZS(H)jQ|*22b?tfWaqY+2Htjpw z725gQ8QK~xsV&jwYcsTiwMwm6i$;bcA4lGfyc~Ht@{`D2k((n|M>a?N$Z3()k><$4 z$ehUGk=n@UNPfgnht>a9-&KFDZdV^ucB}WQTh(u?m#OEf>(w5$Rh_FIsZLh&RYUn$ zc|>_tc~-ecxkcHmcxt^mTKTJTwKAZr3_qaEP!3iql@cYUDB(YaKMd~*zZ8Bdd_(y5 z@cH4X;p%X?vP78|o)x|#ygu9&ZVoR9XN8^(JrcSnbW7;k&?TX>L+e7#;c)2P(5InS zL#w}Js0Et?1p)>3zXIvcxzWN8$eYOdh2(YQynOOThKI;5&cbu`IF!*@}^0B11bbrP+YGFCs(T7m%Uu^T<&5Ib^82 zJpFxJYPa{L!pGVwAPSB5l8eaCa7XVN@&t1GZt^(tw080^@`2OH zPmoIvBo87N_mcZFTte@_N#K`MITuueJr^VdDd)l1M-+zyXReBG)39RgkMQTu#1)Tu?;*3mNXyH<95! zU4fjFM=nQ(dvh5w+?z|0V-C3l8AkNQ$Z$V4r&%j2A{QZ-mXQmQOG?Q%kc&&m*OB2G zFF?*OBIhGR-_ApZF?236jG=Roog8vDGW2Z|GW5+yhQ4{oMMK0*b0lwwoW)a%PBfC2 zzY+0+yfYE=$De_iowosT;s@&yv$9V|jAgAuv|?)!hpf{O$4@*pb>=ct_DhB&se6>;>~7R1ug32cO1 zNw%!O?Zu_dh(*Osh=oPV5eo_%u_DbIUbqam=M^kP%*|VZn3KC0F*|1wqLbZ_X~Uv< z^|(Fe)FE22g@{Ae$#^t5k6gJRQ`kh#J1J8*i)=bEeLIv5VT0IwWdDC%-H!PonxOB&{h)b5vMqIpP7UH7C#~?N=IvTOQVJ2c- z{S3r~b<@)~PoeW}I0`pUSbij8^}-_%tEvx2+);HHV%3CccujN^oi{a8D5ImMWD2FU z>`<&RcFZA&W2z3$98yF}55nygV-G|eRWTVcK57zTdAt^}tbAgojY3*fgWF5Wsu7Dz zCLk6SS0NS_jYljftVGN!7>AgfcK~8e?pVa^oH2-5*`x8)@N#*x0=HXPqY#Izc;=7` z$wlRv!g=JvvP{7x=aps(XA!p~T|JyLRE(IFQ-m1HDnztm1&Bjdei}o$LwSffxw(kh zIXQ?`u#v>@<~@TWKmOji#x%#7t9Bv5}_2BA%v#LOo6S z1uRW@`C6KC^CD@=$yL+j&FL(=tQGXT}G{PU8jR3FAKFHsia-6%dWDGgceRjT4QdjYEwpqs(xO2>%?S?zi|3 z{&W5izmtEDU&SxvE+6Ep_)ePa8)Cm<&#;Ht-Rv6Xvy0gbR>?|OOnXmzO}kS2hIW`XQOkvE z|3Yigj@3r#?fPQM@{B6|hm?v)UIhqQ;aJGEbEXKKA#UF3+!A0p30-if>tc{p-+ zWJ~0l$iIOrM#p( zr97bAu6#qeQaKAE?o~>oa*{GjnW`|QT8S$;;djDcgkK3i9DXKzcX&(qn()QpP2tY) zap4u=y6~j186FcZ2z?TIF7#gLwa`yP_lCY5x-4{BXj!NuG~a&5-eEsw-)CQEd-lcl zfW6p00>0Etv>od+Te04=p0U1bJ!IW(onxJ1t+g7h8CITkpfv`*u6!Q-5WdY^6@4sv zPxNLQa?U)P;%v~_a3;kWpmX+m`UCMW!8F%dPtK+|9dy=jpxgEdXVP;+hg>kcp8haX zI-6WX@5z+TAs5iQGo`c1IrJ_p4U6V+cKZL3r=3pg^%SS0 zPNJ3K6x3Ofpg09}u3tfMdg(MaP@G;m%Ni+8L7k<`C{8tMUGAaSG~8-A!={>Krze;uO@Gb{NGes55mM z#VM#$TS;*W>P)PqI0bcTCQ_V&I@L84r=ZS+YKl`(r)mPlDX3F9mEsiCDPBl%3hI

8k(7xMHsXeaXI8oC;J(qx*Nl0yDhR_!F( zf!k}Q(^VO+r7Mv~HPY04R2*MXLz9{96|@ccjZw4}xoi||K`t$$3FMMex&pbVgf=4= z7SSf;{6e}M8SY&pGTggm$T_)mDROoWU4jhvaWOL7yG6(`hc+PFFo{YS>pUTN84aLZg9rPsJQQt`A+!S`y)zhz~4J<}pAUo)MEYTDH_7I(i zCGrdM0zCmsbUrngdnxt;6d|-AU9!ooWDCvCl(v#vsgo&vpKPUBnbIxf`!tp* zZ6UW%D}B#xvSoDQ$=HpEISK$aeB^rgRm#iF}kPT}rMZAEd9>Y9yDEzogG) zEo&s7BhOey{)9Yb2Kgg$%@pz}a&-;)B*R1GGvvx@GK73UCHVt#$pPf|$VDaOcgTfB zkbGVAWl_~8aJL#L5(i>zK-IXcrByZ3+(!~US2Lv-h9}ckGNtF>efDyuw4FRhcVKCFGTBali6!#% z@L}|&Oz8>O_hP2>Bzc;?kSRSuo}|xXY4|Ym1bq%m5}qm9v%qsK*$iXI%TipHaPQ644cXXbCsH_R8! zC(Q@VyUbh6Yt75d^UaOspt;&?HtWq3%$epCbAma_%r~QE*!Yw2p|Q(&$@rP^pmB$B zvvI9)nQ^{xrZHfgVyrM4j1!ER#uTH*7;O|nzJSXA!awHk^4IwD{0V+Pza73@e2Z`9 zoA^53&0G0m_;ztLpTeslGa!!}oamqGALwtuSBszN59xR5TOcdoQvF9zV8y->IGF#99>E!)L@$$rj$0^c!iVb`(C*m;m0&E84T#quRaNR_%Jo54b?vs10ZxT9dX=J5D=NJ5U>^m1tR7B=Q&dnz1|b ztH}1qPa{8$+!nbZa%JSghzD5$osmSOA#y_GsK`N)%1CJ>7E#sD)!(UasV}R~sE?>W zge-yUAxl7f(-2t#4eIggboF3$yjrGas|@l2hLm@e*Ocd#CzSh@A1L2d{!6()IYa4H zT9rl0JY}YG2xJQsE0#jTpM>8J?+m{P*#ZxS?+o7z-!3-8w~KY*?r>{(ad>|C=)F}2jaL!bZ?QkYpX?_j|+HsKKF9VD@80`m@( zxTA`BlO>J=oFsAdIOf$#tQgI`iGbpyFr{Q(jocouU|zMv@;LJ*NGvO7UX{esGUkn! zSW?QoN{PiK%o``Mu$XxVNGvGi-dI3!5}08!Z;afYSHQf{5_9vIS0OPcmwBTkX6G<3 zF44(mUb)1W!@M$yR*ZS265*SlR{|(b0uxv66{ia@Q{`S!x&V_{?iFVCQ0^7Tf|xw& zUOp6PNX+J#mnWFQWR7{c66e+FUXD1Nig_XTvIU1yGfnO}=>kkNxtEnLz&w+Cv2+2Z znas1H0IiOn$2}{(9VVQ*7Zuw>HcSPXXUbzr$1~56_zvjGB?`55iG{G0Nh~O2o+eSK z8j&beRV4~l6^TOCutcG1NTN`cN))ORi9?p|ZUhvUyy+e8o+%hE879BFdq!%9m?3g^ zLuyBAf~dRep#Z;#1@k=So-UZeG>^OM(#OEGk-2N-_8H5Vdz!>4Gnjj-#OfjD4oa+< z!rX!M)*9yaOMI!CxqT8VtGT-dFch-j9krdgy>j~jmCWstSaJY&yVHljyp*|Ja(ht; zb2}v#7BP3VL>R8_De0qO-pbq#xgCUcS4jk6-IWqSShrmw2d%x#kR0i12Q#QZ|$HcHHft;-~4!P%Bdw6d7HM4~vw z;uJz&19KNi%z{ofNQ}X;^%6zT>LiM43sVRw4a_}RqNuh&qNsL~L{aTTiK5!qQV7`$ z%$+Y$RGTMJR69YUsCK+WF~H^uOywFd_qfz%$VXuA9EofCn0u_m-ZjjfEwOz!cV_{L z1PDlS(A{H1fr_jK?jD`q0TTo6&Xff)FJSHrC{U_TYiI6si3d*O?osLEA(Mi+N6PIb z2QpWBx4yfWxzfA!V(6FjZoRmKyD9I69zfCsbA@?B+XZm6ux>zD&2WWr1Lnc)OxZTo z#o&>yt&71UU0WA}N4mBy29I=YJ>X0G&XHoA$q(zDr^SpPnQHLTbEP?JV8gC7XANF@t~6&& z^h}zwmJijWIcuUj(wsGM@1;3w;@(Sh)*R@WG-pk8N1C%Hx+Be56Wx*KtQEnHmF5ij z2TXV~71;+&I5QAo+iI!)nudCR`X&0ven2lm&-WNH<{8g4KpbCL9=c z*EcX}zv|X{Cf!$^U(bd4hC-=K11{ZHC6H*K3-c8j2-}9ZFkf*9q#1DGzJejkfD8Kt z7D)!W@LzHGmaRgcgdny*?qnhWn0O+k7Aljf@ymon+SD%??N zy(+vRr1Prqf)K_Fbwp+Xlg_Kc>p>c?3O8B$t_t^9+O7(NPr9z^WOHGxhhO)gyo99K|TQ&o+}vA3Aiv_!H`X$r(8EAk_nh}UC1S1(sY&8>$&jU zP$-p2z@+Ia%ZFIXbE(p_nn}l1mNzkJxXKO7nen&tVG$E93)|r4N{dy%YNW#|3+wch!BUYtz@)t@6P7dSu1fVnCe2l;s%Fw#6_~dQ zYlQ}@CUD`aZ~-E1fD2<43|RwQIICbt8eq~|m9b-(bXH|d6&J=T_Cv-1lg_GCjAhbT zl~EN;`l=Ei#iXq&Fw>Q;s+5&;VXC4w0L;9%# zlQC(h3XBQqrV5M+X{HK{3F)N@j0tI_N>(0@(pv zIH_Pr4&cH{1w(EC7hWnDQUjPYQ^*Wp(o4heDwb9nh9_P+X&A@M@A48iIaG2MrBbnyrd-fbt>DN0!>Q+WC27{kejEeDagsyloU8QS~vwUM+>FEifJ?jLzejc z|C?AFu|KsxuwS>IgY5tN>}~e-ko$j*z25G%TkQsWF03in+GFfOJ8FllKU%-Fc3Ho) zes2B5y4$+Nx(;Ig^Q<$hK5HecFE6l;wGOu?S!1mti1-zV_& zvkP()7Mb%P+CLPc{kWNHa%0%|#CRXF6J9i)f@uFvi1x2BE-}tA)*HP>(pU-^3bTx9 z#zctq3yi2i`DgsMd>4NaBK;rpTlu&7rTlEZmapbbd;y;YQGOLKhi zkLve8gnzYuk?ulX!b*LqK3|`qAEX}u(Y?urAwyv|+DR(`)|XT zHuyVGAW$GsAW$Gs;6GUb?zg1oTdB1y?u&o|QpsRRbSL*^OyLk%$>P2UD#RXG$kKfg zSA?io$Kt+-EW{32#^SyRE<^!VvA8ee3pk3r%zY7Hz#g%N#eETDh{IqBi~AzX5CvGl z;=YJ9L;)7CxG#bYdDwH@m+^)pman)k0uHeURVBL!QBK!~qShnK6h(JUER;{=%gAhk7T5(@QAz}}# zS#e(kBBB6GR@@h{h$z5{757CrBKO?EeG!pBL9ADCUj!v$4=h)4U&JM%0IOBp7omwL zz+x5mMRX$f+{Aqmpg=(^RdHX$C}Iz+RB>N~DWU)iRoqWSDo}uRD(;I|Meezp`yyNs z4ZtcD_eI1a_P`<)_eIbm3b01SeG#{a0xVH+UxqHJ6)Nt_*u@baqPQ=EmsCn2_eJy~ z>Oe*z_eB6B3Xo99eHp`uJ?C&=1TnBjq!Th<#xamh$b1>dKyoAVWh4W+jk=!-Wm2h) z%oo87rI6Xkd>P9?A|dl-FyjntX1DhbHR6B-^F>qxc>N0I zi=YOuv4Qy_rU6{m$b1>nK;9tpWlRHUgUpvf4P*^6Uq&^MG{}4z)6uH;^pId|P&B>NMtC5^F1&AC)+< zmieZ{nu*LeBv#ijpG%xj&3s)VtkU>QVkMkHlUNMhiAXG|V!kR7K0x|P`hXJVhb6*? zMn5DGzJ2*rB76$+iA0Dwy^RvH;q(8Q5+Q=~&X5QpoVP(Dd|mX`ON6hB-sut{D)iQ+ z5V8W9w^m~ND(0OgG1<<%Qzf<~nKvl0wT*cL5?flC*Do>A!n{6-aJ#)V5}Q{ruUBGI zGxK^R!q*6|TVmsK=5?iUhyAP8-hAc>>xQTyrnc8o%PAM=E5L-m<+m?umdaK=pTNz0Cj int: ValueError: Qubit name cannot be found on the backend. """ if name not in self._qb_to_idx: - raise ValueError(f"Qubit '{name}' is not found on the backend.") + raise ValueError(f'Qubit \'{name}\' is not found on the backend.') return self._qb_to_idx[name] def index_to_qubit_name(self, index: int) -> str: @@ -162,7 +162,7 @@ def index_to_qubit_name(self, index: int) -> str: ValueError: Qubit index cannot be found on the backend. """ if index not in self._idx_to_qb: - raise ValueError(f"Qubit index {index} is not found on the backend.") + raise ValueError(f'Qubit index {index} is not found on the backend.') return self._idx_to_qb[index] def get_scheduling_stage_plugin(self) -> str: From 4206c1a97d4268993cca866b98ac78f51296ccaf Mon Sep 17 00:00:00 2001 From: Arianne Meijer Date: Thu, 12 Dec 2024 14:14:21 +0200 Subject: [PATCH 22/47] Delete accidental files --- .coverage.rev130-154.vpn.iqm.fi.71168.XRtLzOmx | Bin 53248 -> 0 bytes .coverage.rev130-154.vpn.iqm.fi.71168.XiWlaWLx | Bin 69632 -> 0 bytes 2 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 .coverage.rev130-154.vpn.iqm.fi.71168.XRtLzOmx delete mode 100644 .coverage.rev130-154.vpn.iqm.fi.71168.XiWlaWLx diff --git a/.coverage.rev130-154.vpn.iqm.fi.71168.XRtLzOmx b/.coverage.rev130-154.vpn.iqm.fi.71168.XRtLzOmx deleted file mode 100644 index 9a6ebaa026a317d73cc71b65b1906dcc7641d6b7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 53248 zcmeI)Z*SW~90zba+2STmJb(gyFfnNiDxM&bU7Sm6i0#yN z`sV>c*^nwB@eU9#gm=M9;Mtz>h(|o(_p^O*(l*ncO$%SENgVs`eD}HE-6eImub)2l z5+#OVHtcVVdgWKc{%+%m z-LC((@$UMcb-(({`tO#kx^w~?1Rwwb2>ibVrcdjZv%PIT|1pvMkxC-zswjKj`0T5L z{lf!sxc}+n1CbpQdo@8@yDj!b6rPE(ip0C}^04S1oWW3<$9 z!cLl>_j=xdawY1BiWoO9E~P$9}Y3OI$>%dHRW1lMYmt#JO_|d=2NSl=oKtVl9M_?#UabQGLo+ zGM+?wB6-y$sdE&wq#h&r8EQ`3syO>Q%bJs?>vi*;wW)Q>a<;b2X*MEwUgF2?g+}m^ zVhCJW2wuPXW0@R_>viYDRPn~`iu2LdvWjyRdR>05y!f`1R{3%})%R)A=SO*=%6z1# z@)zo&eJV#Zn~bFw(U7G%I9mY(CmcvhEc@XkN#mE_rs2tJPf_2jfa1Njr&P@}ISubG z!#MV6S>oR9~WiB7hJ~CX(O$Z zUR-#ZKcqpY#~1DRz6=KWdd16dRw~W?JUIC}*(5?4MS})qOhfcanYz&YEYDcF{LYw6 zxKv*bvf<91GFRf+vAR}Vwi+&;y zOJ3uTa?@SYa_-+Zr;~gzX7fxhrbXSCy4v~M(n642Ia$QZ6Pj@8)6RUHu-zO7zs;_| zlUvq<{Y*ip%0KJWn*0UdwEs2e2O9(+009U<00Izz00bZa0SG_<0;?xbHcO_(-~UVY zUxxiRJz;|Y1Rwwb2tWV=5P$##AOHafK;Z2ZsF%w3Z2c!5UE3`0IQbs|e9(En^RPy( zD%md$`=$NQ+u1i02P2muH{00Izz00bZa0SG_<0ucBQ=I0%m diff --git a/.coverage.rev130-154.vpn.iqm.fi.71168.XiWlaWLx b/.coverage.rev130-154.vpn.iqm.fi.71168.XiWlaWLx deleted file mode 100644 index 43af6bf3020792847a5050bf4527754f3b29a845..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 69632 zcmeI433wIN-T%+IcjnHSGjnHdCi{K~AR#0K+yFN~K}4a7Bw-5)H^~h|vXMnWP@EZD zTgA1n)}^*u#Uj?Vwr+K)ORE;QqPB{-P?uU;^QvvFZT`P=&;4DXt$p6-ANst{YbTGw z=giDGbLKl|zH@$aeqrH)c^w1EczaJ@S7IQ35Qz~=$&7fM5MsdpS@1vpXaEWp;BUGY ze;FENV)uPfyMdUI3yIwnJ<+Z)uZbRDJa2aC7Z}%R3B46|1V4cSfdYX7fdc>83b+eQ zEx)jkdS?tIS~`;heTmj&pZqg=%*nH7*3FLB%{+SE?6}+(ujFy~si}$2jQ91N7Vk~= z#oIeNlktx3wvN`sKu7n=_`s?p9zEEfY!fd!*aojUap1&MeL>9~ZBT0f_^m$@XMlvb#0eFMCnh(Keos_cwAa zKRcVAn@n9(qOTSHGxoU}*s$kP=C(satv!8hqW3L*iSE`_$^PniB6EeUJIXT7%skYX*~&DV{At;gz^F{F z?O)Rg^R4E@;6P8R+zdCrc{2QAZpva>et9`PcTH-b2zi=YItKbPpzkwiGMm3-=!pGa zK5oQQvhN@fPf=%Qyr(_&$cWl;d2?Vi@BPfwq8N!j@u0_REzcNXzvBxFx8AK`7?{Z>~hk_HBU|hb2UZF&&{Q-d?JJ|iT~AoE<$YC^9cOii{O9W{ocgDs`x+l zU2KMqk5eQ0hv$A-$3+*KCyPHL{<2W2%jXuR)b9e*7ypO%>9W|er^_cK*VUYw=macc<2m8S1$qko-fW96yi zQhmagi?I1Rc7+TLteI52rYfG!dv69|sR*KRlv;*CsZa-YXmn z+n?AAnuu1r5^eHv7dK_E7EfE1{DFiM|k?uVCpR|318E{66QB!9Pa&v-cz z3>RiQdyf;L-ClBtqCA5zH+ctJqzZUcdalFAN1>P0{tJQsf}cQvK!HGkK!HGkK!HGk zK!HGkK!HGkK!HGkK!LBG0%01Wnuz~H_M621J^T^;1PTNS1PTNS1PTNS1PTNS1PTNS z1PTNS1PTNS{NGf-424T=O!1g(({OQqItgIP#6u@e<8W0W`#oa6XMg&C^8$lj2MPoV z1PTNS1PTNS1PTNS1PTNS1PTNS1PTNSpaN#Nl;T%_5DypY>5l+n{XdVLMC?24I_nc_ zvvp+jchQTZN0{%Jt~tth)aW%T_%nPRFV~;YlX{fh0S5;^fdYX7fdYX7fdYX7fdYFe z(440b_n6xHe)!$Z+C*PRqPsgeu`AheO0utZPRGEi!Is)J9sR3225Ne`YdY3+)%N$b z){4L7)@FgpwTZ6Y&SZaWU$Vca8~z1^WOI5G{DSAwTsXnB{d9tsWM^k{`j>>E?S>p^ zd-8s2yScfeyJMia8QN{m-jAZdzedsB-z$FQc#kNH95}&2BkOpk-M;4D&cT%(-Tlz> z#aYnsAtP%TZfSFOqT|$La~J%ceRFT3U$nd+1}%>pdCR?hJ*RfGCHtVsIvbj-8F`bb zZgwWt!S7r{tMe>qwRGgIp3>6-t(_Ew*2a#!wbqWl*1-#t3f+LGN#(cvZonxDDfn@{xibawVY`w0&1&-pjBzpAgN4gRSOI7NdFrx@X5 zEe0_@A@DED^mIeJCo^bw{K&eVw!MKK;e62MG7Z{1d}M8kzNW`VGyKB<9q?=Qvcq4C zK;xrE)_CgSSP8!n51lai{IC6Z~ELpSwtLC-1Me_nKto>`zfJ3fVjU&t9y!2aT}fnRfS%|D7d@d&meI z7BldFjQ_JvR@`y>q2;~e|5&}^*6fES_l^JU`HEY*A6k|1zqLSd$L@#L_Qe0uCdHjT z!WQ?9Hc};w|IKE_ow@&;&&2;mi{j4tceKAJ{^$GQu@-|EpAZbE^@WN%euQ2B`}m(V zD(>MUZ1YRwf9*uY9W}zn_m2M~`)M%miT~9$#XVt!J=}LN|C9J%`8N$|jQ_*?;U4aL z5qrH$LSRSb`p90edo>sQ`+vctEl?m(AW$GsAW$GsAW$GsAW$GsAW$GsAW+~dtw5ND zNmQ)=Q~L`7{{=sR0)Ya70)Ya70)Ya70)Ya70)Ya70)Ya70)YZwK?TC$s4mw3N$e|l zals`83IqxS3IqxS3IqxS3IqxS3IqxS3IqxS3Iqyd6cFqGLHwVQBY;4GK!HGkK!HGk zK!HGkK!HGkK!HGkK!HGkucQJhTotuH7uS|0|N2T^W^hq~0)Ya70)Ya70)Ya70)Ya7 z0)Ya70)Ya70{^)R#JnRyaP`#wgV4a1fBomm5nOqoK%hXNK%hXNK%hXNK%hXNK%hXN zK%hXNz*k5C%ZN~&(vbC1HTm!0_y4~_cQ&|`K!HGkK!HGkK!HGkK!HGkK!HGkK!HGk zK!Gn&K>Ysyo9bi4{-gan`)&JGd%OL(eZPH&eY1V7eVKi}z0n@DSKBM>di!{Lx_z)c z-Y&DVZD#$|8nWIsKZ>4dZLzMk&bCgsdaO2UiS;$>7;CCkV^vr@Ivo8Z`hIk0^oi*G z(etDI(dE%q(PN{BM<+$cMvJ0$R5AZ-K5M>hzGB{Ee&4*#`jz#p^{{o9b+P%dl^cCN zdRz26(aWO?jB}02W~Z4j8_c83gUm{^)O3u`jo%yZ7_SDtNt-xpzqdS)t}WL(eGjJ>rwp{{aXDJ{cL@m-lZq>dVQ`=*{AxE`ec2q zUdUc&&$GwbkJ&c%9d-pfpPj+huq0c;=CSE)G8@YZS(H)jQ|*22b?tfWaqY+2Htjpw z725gQ8QK~xsV&jwYcsTiwMwm6i$;bcA4lGfyc~Ht@{`D2k((n|M>a?N$Z3()k><$4 z$ehUGk=n@UNPfgnht>a9-&KFDZdV^ucB}WQTh(u?m#OEf>(w5$Rh_FIsZLh&RYUn$ zc|>_tc~-ecxkcHmcxt^mTKTJTwKAZr3_qaEP!3iql@cYUDB(YaKMd~*zZ8Bdd_(y5 z@cH4X;p%X?vP78|o)x|#ygu9&ZVoR9XN8^(JrcSnbW7;k&?TX>L+e7#;c)2P(5InS zL#w}Js0Et?1p)>3zXIvcxzWN8$eYOdh2(YQynOOThKI;5&cbu`IF!*@}^0B11bbrP+YGFCs(T7m%Uu^T<&5Ib^82 zJpFxJYPa{L!pGVwAPSB5l8eaCa7XVN@&t1GZt^(tw080^@`2OH zPmoIvBo87N_mcZFTte@_N#K`MITuueJr^VdDd)l1M-+zyXReBG)39RgkMQTu#1)Tu?;*3mNXyH<95! zU4fjFM=nQ(dvh5w+?z|0V-C3l8AkNQ$Z$V4r&%j2A{QZ-mXQmQOG?Q%kc&&m*OB2G zFF?*OBIhGR-_ApZF?236jG=Roog8vDGW2Z|GW5+yhQ4{oMMK0*b0lwwoW)a%PBfC2 zzY+0+yfYE=$De_iowosT;s@&yv$9V|jAgAuv|?)!hpf{O$4@*pb>=ct_DhB&se6>;>~7R1ug32cO1 zNw%!O?Zu_dh(*Osh=oPV5eo_%u_DbIUbqam=M^kP%*|VZn3KC0F*|1wqLbZ_X~Uv< z^|(Fe)FE22g@{Ae$#^t5k6gJRQ`kh#J1J8*i)=bEeLIv5VT0IwWdDC%-H!PonxOB&{h)b5vMqIpP7UH7C#~?N=IvTOQVJ2c- z{S3r~b<@)~PoeW}I0`pUSbij8^}-_%tEvx2+);HHV%3CccujN^oi{a8D5ImMWD2FU z>`<&RcFZA&W2z3$98yF}55nygV-G|eRWTVcK57zTdAt^}tbAgojY3*fgWF5Wsu7Dz zCLk6SS0NS_jYljftVGN!7>AgfcK~8e?pVa^oH2-5*`x8)@N#*x0=HXPqY#Izc;=7` z$wlRv!g=JvvP{7x=aps(XA!p~T|JyLRE(IFQ-m1HDnztm1&Bjdei}o$LwSffxw(kh zIXQ?`u#v>@<~@TWKmOji#x%#7t9Bv5}_2BA%v#LOo6S z1uRW@`C6KC^CD@=$yL+j&FL(=tQGXT}G{PU8jR3FAKFHsia-6%dWDGgceRjT4QdjYEwpqs(xO2>%?S?zi|3 z{&W5izmtEDU&SxvE+6Ep_)ePa8)Cm<&#;Ht-Rv6Xvy0gbR>?|OOnXmzO}kS2hIW`XQOkvE z|3Yigj@3r#?fPQM@{B6|hm?v)UIhqQ;aJGEbEXKKA#UF3+!A0p30-if>tc{p-+ zWJ~0l$iIOrM#p( zr97bAu6#qeQaKAE?o~>oa*{GjnW`|QT8S$;;djDcgkK3i9DXKzcX&(qn()QpP2tY) zap4u=y6~j186FcZ2z?TIF7#gLwa`yP_lCY5x-4{BXj!NuG~a&5-eEsw-)CQEd-lcl zfW6p00>0Etv>od+Te04=p0U1bJ!IW(onxJ1t+g7h8CITkpfv`*u6!Q-5WdY^6@4sv zPxNLQa?U)P;%v~_a3;kWpmX+m`UCMW!8F%dPtK+|9dy=jpxgEdXVP;+hg>kcp8haX zI-6WX@5z+TAs5iQGo`c1IrJ_p4U6V+cKZL3r=3pg^%SS0 zPNJ3K6x3Ofpg09}u3tfMdg(MaP@G;m%Ni+8L7k<`C{8tMUGAaSG~8-A!={>Krze;uO@Gb{NGes55mM z#VM#$TS;*W>P)PqI0bcTCQ_V&I@L84r=ZS+YKl`(r)mPlDX3F9mEsiCDPBl%3hI

8k(7xMHsXeaXI8oC;J(qx*Nl0yDhR_!F( zf!k}Q(^VO+r7Mv~HPY04R2*MXLz9{96|@ccjZw4}xoi||K`t$$3FMMex&pbVgf=4= z7SSf;{6e}M8SY&pGTggm$T_)mDROoWU4jhvaWOL7yG6(`hc+PFFo{YS>pUTN84aLZg9rPsJQQt`A+!S`y)zhz~4J<}pAUo)MEYTDH_7I(i zCGrdM0zCmsbUrngdnxt;6d|-AU9!ooWDCvCl(v#vsgo&vpKPUBnbIxf`!tp* zZ6UW%D}B#xvSoDQ$=HpEISK$aeB^rgRm#iF}kPT}rMZAEd9>Y9yDEzogG) zEo&s7BhOey{)9Yb2Kgg$%@pz}a&-;)B*R1GGvvx@GK73UCHVt#$pPf|$VDaOcgTfB zkbGVAWl_~8aJL#L5(i>zK-IXcrByZ3+(!~US2Lv-h9}ckGNtF>efDyuw4FRhcVKCFGTBali6!#% z@L}|&Oz8>O_hP2>Bzc;?kSRSuo}|xXY4|Ym1bq%m5}qm9v%qsK*$iXI%TipHaPQ644cXXbCsH_R8! zC(Q@VyUbh6Yt75d^UaOspt;&?HtWq3%$epCbAma_%r~QE*!Yw2p|Q(&$@rP^pmB$B zvvI9)nQ^{xrZHfgVyrM4j1!ER#uTH*7;O|nzJSXA!awHk^4IwD{0V+Pza73@e2Z`9 zoA^53&0G0m_;ztLpTeslGa!!}oamqGALwtuSBszN59xR5TOcdoQvF9zV8y->IGF#99>E!)L@$$rj$0^c!iVb`(C*m;m0&E84T#quRaNR_%Jo54b?vs10ZxT9dX=J5D=NJ5U>^m1tR7B=Q&dnz1|b ztH}1qPa{8$+!nbZa%JSghzD5$osmSOA#y_GsK`N)%1CJ>7E#sD)!(UasV}R~sE?>W zge-yUAxl7f(-2t#4eIggboF3$yjrGas|@l2hLm@e*Ocd#CzSh@A1L2d{!6()IYa4H zT9rl0JY}YG2xJQsE0#jTpM>8J?+m{P*#ZxS?+o7z-!3-8w~KY*?r>{(ad>|C=)F}2jaL!bZ?QkYpX?_j|+HsKKF9VD@80`m@( zxTA`BlO>J=oFsAdIOf$#tQgI`iGbpyFr{Q(jocouU|zMv@;LJ*NGvO7UX{esGUkn! zSW?QoN{PiK%o``Mu$XxVNGvGi-dI3!5}08!Z;afYSHQf{5_9vIS0OPcmwBTkX6G<3 zF44(mUb)1W!@M$yR*ZS265*SlR{|(b0uxv66{ia@Q{`S!x&V_{?iFVCQ0^7Tf|xw& zUOp6PNX+J#mnWFQWR7{c66e+FUXD1Nig_XTvIU1yGfnO}=>kkNxtEnLz&w+Cv2+2Z znas1H0IiOn$2}{(9VVQ*7Zuw>HcSPXXUbzr$1~56_zvjGB?`55iG{G0Nh~O2o+eSK z8j&beRV4~l6^TOCutcG1NTN`cN))ORi9?p|ZUhvUyy+e8o+%hE879BFdq!%9m?3g^ zLuyBAf~dRep#Z;#1@k=So-UZeG>^OM(#OEGk-2N-_8H5Vdz!>4Gnjj-#OfjD4oa+< z!rX!M)*9yaOMI!CxqT8VtGT-dFch-j9krdgy>j~jmCWstSaJY&yVHljyp*|Ja(ht; zb2}v#7BP3VL>R8_De0qO-pbq#xgCUcS4jk6-IWqSShrmw2d%x#kR0i12Q#QZ|$HcHHft;-~4!P%Bdw6d7HM4~vw z;uJz&19KNi%z{ofNQ}X;^%6zT>LiM43sVRw4a_}RqNuh&qNsL~L{aTTiK5!qQV7`$ z%$+Y$RGTMJR69YUsCK+WF~H^uOywFd_qfz%$VXuA9EofCn0u_m-ZjjfEwOz!cV_{L z1PDlS(A{H1fr_jK?jD`q0TTo6&Xff)FJSHrC{U_TYiI6si3d*O?osLEA(Mi+N6PIb z2QpWBx4yfWxzfA!V(6FjZoRmKyD9I69zfCsbA@?B+XZm6ux>zD&2WWr1Lnc)OxZTo z#o&>yt&71UU0WA}N4mBy29I=YJ>X0G&XHoA$q(zDr^SpPnQHLTbEP?JV8gC7XANF@t~6&& z^h}zwmJijWIcuUj(wsGM@1;3w;@(Sh)*R@WG-pk8N1C%Hx+Be56Wx*KtQEnHmF5ij z2TXV~71;+&I5QAo+iI!)nudCR`X&0ven2lm&-WNH<{8g4KpbCL9=c z*EcX}zv|X{Cf!$^U(bd4hC-=K11{ZHC6H*K3-c8j2-}9ZFkf*9q#1DGzJejkfD8Kt z7D)!W@LzHGmaRgcgdny*?qnhWn0O+k7Aljf@ymon+SD%??N zy(+vRr1Prqf)K_Fbwp+Xlg_Kc>p>c?3O8B$t_t^9+O7(NPr9z^WOHGxhhO)gyo99K|TQ&o+}vA3Aiv_!H`X$r(8EAk_nh}UC1S1(sY&8>$&jU zP$-p2z@+Ia%ZFIXbE(p_nn}l1mNzkJxXKO7nen&tVG$E93)|r4N{dy%YNW#|3+wch!BUYtz@)t@6P7dSu1fVnCe2l;s%Fw#6_~dQ zYlQ}@CUD`aZ~-E1fD2<43|RwQIICbt8eq~|m9b-(bXH|d6&J=T_Cv-1lg_GCjAhbT zl~EN;`l=Ei#iXq&Fw>Q;s+5&;VXC4w0L;9%# zlQC(h3XBQqrV5M+X{HK{3F)N@j0tI_N>(0@(pv zIH_Pr4&cH{1w(EC7hWnDQUjPYQ^*Wp(o4heDwb9nh9_P+X&A@M@A48iIaG2MrBbnyrd-fbt>DN0!>Q+WC27{kejEeDagsyloU8QS~vwUM+>FEifJ?jLzejc z|C?AFu|KsxuwS>IgY5tN>}~e-ko$j*z25G%TkQsWF03in+GFfOJ8FllKU%-Fc3Ho) zes2B5y4$+Nx(;Ig^Q<$hK5HecFE6l;wGOu?S!1mti1-zV_& zvkP()7Mb%P+CLPc{kWNHa%0%|#CRXF6J9i)f@uFvi1x2BE-}tA)*HP>(pU-^3bTx9 z#zctq3yi2i`DgsMd>4NaBK;rpTlu&7rTlEZmapbbd;y;YQGOLKhi zkLve8gnzYuk?ulX!b*LqK3|`qAEX}u(Y?urAwyv|+DR(`)|XT zHuyVGAW$GsAW$Gs;6GUb?zg1oTdB1y?u&o|QpsRRbSL*^OyLk%$>P2UD#RXG$kKfg zSA?io$Kt+-EW{32#^SyRE<^!VvA8ee3pk3r%zY7Hz#g%N#eETDh{IqBi~AzX5CvGl z;=YJ9L;)7CxG#bYdDwH@m+^)pman)k0uHeURVBL!QBK!~qShnK6h(JUER;{=%gAhk7T5(@QAz}}# zS#e(kBBB6GR@@h{h$z5{757CrBKO?EeG!pBL9ADCUj!v$4=h)4U&JM%0IOBp7omwL zz+x5mMRX$f+{Aqmpg=(^RdHX$C}Iz+RB>N~DWU)iRoqWSDo}uRD(;I|Meezp`yyNs z4ZtcD_eI1a_P`<)_eIbm3b01SeG#{a0xVH+UxqHJ6)Nt_*u@baqPQ=EmsCn2_eJy~ z>Oe*z_eB6B3Xo99eHp`uJ?C&=1TnBjq!Th<#xamh$b1>dKyoAVWh4W+jk=!-Wm2h) z%oo87rI6Xkd>P9?A|dl-FyjntX1DhbHR6B-^F>qxc>N0I zi=YOuv4Qy_rU6{m$b1>nK;9tpWlRHUgUpvf4P*^6Uq&^MG{}4z)6uH;^pId|P&B>NMtC5^F1&AC)+< zmieZ{nu*LeBv#ijpG%xj&3s)VtkU>QVkMkHlUNMhiAXG|V!kR7K0x|P`hXJVhb6*? zMn5DGzJ2*rB76$+iA0Dwy^RvH;q(8Q5+Q=~&X5QpoVP(Dd|mX`ON6hB-sut{D)iQ+ z5V8W9w^m~ND(0OgG1<<%Qzf<~nKvl0wT*cL5?flC*Do>A!n{6-aJ#)V5}Q{ruUBGI zGxK^R!q*6|TVmsK=5?iUhyAP8-hAc>>xQTyrnc8o%PAM=E5L-m<+m?umdaK=pTNz0Cj Date: Thu, 12 Dec 2024 14:23:27 +0200 Subject: [PATCH 23/47] fix new error message test --- tests/test_iqm_backend_base.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_iqm_backend_base.py b/tests/test_iqm_backend_base.py index f4c6f11a7..bebc7020a 100644 --- a/tests/test_iqm_backend_base.py +++ b/tests/test_iqm_backend_base.py @@ -57,9 +57,9 @@ def test_qubit_name_to_index_to_qubit_name(adonis_shuffled_names_architecture): assert backend.index_to_qubit_name(idx) == name assert backend.qubit_name_to_index(name) == idx - with pytest.raises(ValueError, match="Qubit index '7' is not part of the backend."): + with pytest.raises(ValueError, match='Qubit index 7 is not found on the backend.'): backend.index_to_qubit_name(7) - with pytest.raises(ValueError, match="Qubit name 'Alice' is not part of the backend."): + with pytest.raises(ValueError, match='Qubit \'Alice\' is not found on the backend.'): backend.qubit_name_to_index('Alice') From f3334545159843db9fadb1c541f66a7742f7b3f0 Mon Sep 17 00:00:00 2001 From: Arianne Meijer Date: Thu, 12 Dec 2024 14:28:09 +0200 Subject: [PATCH 24/47] Update doc string --- src/iqm/qiskit_iqm/fake_backends/iqm_fake_backend.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/iqm/qiskit_iqm/fake_backends/iqm_fake_backend.py b/src/iqm/qiskit_iqm/fake_backends/iqm_fake_backend.py index 8a8c4f94f..554e39062 100644 --- a/src/iqm/qiskit_iqm/fake_backends/iqm_fake_backend.py +++ b/src/iqm/qiskit_iqm/fake_backends/iqm_fake_backend.py @@ -294,8 +294,7 @@ def run(self, run_input: Union[QuantumCircuit, list[QuantumCircuit]], **options) and returns a :class:`~qiskit.providers.JobV1` object. It will run the simulation with a noise model of the fake backend (e.g. Adonis, Deneb). - Validity of MOVE gates is also checked. The method also transpiles circuit - to the native gates so that MOVEs are implemented as unitaries. + Validity of MOVE gates is also checked. Args: run_input: One or more quantum circuits to simulate on the backend. From 836c26f38d244b4cf5888430f44948a281fba513 Mon Sep 17 00:00:00 2001 From: Arianne Meijer Date: Thu, 12 Dec 2024 14:35:25 +0200 Subject: [PATCH 25/47] Drop qiskit 0.45 support --- CHANGELOG.rst | 3 ++- pyproject.toml | 2 +- tox.ini | 6 ++---- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 3c1a2b009..ea8e6667c 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -6,6 +6,7 @@ Version 16.0 ============= * Added support for ``qiskit == 1.2`` and ``qiskit-aer == 0.15``. +* Drop support for ``qiskit < 0.45``. * :meth:`IQMBackendBase.qubit_name_to_index` and :meth:`IQMBackendBase.index_to_qubit_name` now raises an error when using an invalid qubit name or index, rather than returning None. * Refactored :meth:`IQMBackend.create_run_request` to improve user experience when using IQM @@ -30,7 +31,7 @@ Version 16.0 :meth:`backend.run` method to ensure that the correct qubits are used. * Bugfix where the :func:`transpile_to_IQM` did not retain the circuit layout after transpiling. -* Fixed :func:`IQMFakeDeneb` readout errors. Fidelities were reported as errors. +* Fixed :func:`IQMFakeDeneb` readout errors. Fidelities were reported as errors. `#125 `_ * Deprecated features: * :func:`optimize_single_qubit_gates` has been deprecated in favor of using the new transpiler diff --git a/pyproject.toml b/pyproject.toml index 4425da073..625fdb40f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,7 +20,7 @@ classifiers = [ requires-python = ">=3.9, <3.12" dependencies = [ "numpy", - "qiskit >= 0.45, < 1.3", + "qiskit >= 0.46, < 1.3", "qiskit-aer >= 0.13.1, < 0.16", "iqm-client >= 20.0, < 21.0" ] diff --git a/tox.ini b/tox.ini index 2ebe03d9c..3d0acb752 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,6 @@ [tox] minversion = 4.11 -envlist = py39, py310, py311, qiskit-{v0.45,v0.46,v1.0,v1.1,v1.2} +envlist = py39, py310, py311, qiskit-{v0.46,v1.0,v1.1,v1.2} skip_missing_interpreters = True [gh-actions] @@ -28,13 +28,11 @@ commands = python -m pytest --cov iqm.qiskit_iqm --cov-report=term-missing --junitxml=test_report.xml --doctest-modules --pylint --pylint-rcfile=tests/.pylintrc --verbose --strict-markers tests python -m mypy tests -[testenv:qiskit-{v0.45,v0.46,v1.0,v1.1,v1.2}] +[testenv:qiskit-{v0.46,v1.0,v1.1,v1.2}] description = Invoke pytest to run automated tests for all supported Qiskit versions. base_python = py310: python3.10 deps = - v0.45: qiskit >= 0.45, <0.46 - v0.45: qiskit-aer >= 0.13.1, <0.14 v0.46: qiskit >= 0.46, <0.47 v0.46: qiskit-aer >= 0.13.1, < 0.14 v1.0: qiskit >= 1.0, <1.1 From 8fadfca637676264dd97ad98113521a1a0500f27 Mon Sep 17 00:00:00 2001 From: Arianne Meijer Date: Thu, 12 Dec 2024 14:35:41 +0200 Subject: [PATCH 26/47] Update iqmtarget docstring --- src/iqm/qiskit_iqm/iqm_backend.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/iqm/qiskit_iqm/iqm_backend.py b/src/iqm/qiskit_iqm/iqm_backend.py index d840d1e8c..809f7b055 100644 --- a/src/iqm/qiskit_iqm/iqm_backend.py +++ b/src/iqm/qiskit_iqm/iqm_backend.py @@ -174,7 +174,11 @@ class IQMTarget(Target): """A target representing an IQM backends that could have resonators. This target is used to represent the physical layout of the backend, including the resonators as well as a fake - coupling map to . + coupling map to present to the Qiskit transpiler. + + Args: + architecture: The quantum architecture specification representing the backend. + component_to_idx: A mapping from component names to integer indices. """ def __init__(self, architecture: DynamicQuantumArchitecture, component_to_idx: dict[str, int]): From bf46609ffe64983dfb79a9e32ac789185f65aef5 Mon Sep 17 00:00:00 2001 From: Arianne Meijer Date: Thu, 12 Dec 2024 14:41:28 +0200 Subject: [PATCH 27/47] Removed the measure gate as a special case gate when converting a sqa to a dqa --- src/iqm/qiskit_iqm/iqm_backend.py | 5 ----- tests/conftest.py | 11 ++++++++++- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/iqm/qiskit_iqm/iqm_backend.py b/src/iqm/qiskit_iqm/iqm_backend.py index 809f7b055..015a4b2b5 100644 --- a/src/iqm/qiskit_iqm/iqm_backend.py +++ b/src/iqm/qiskit_iqm/iqm_backend.py @@ -61,11 +61,6 @@ def _dqa_from_static_architecture(sqa: QuantumArchitectureSpecification) -> Dyna ) for gate_name, gate_loci in sqa.operations.items() } - gates['measure'] = GateInfo( - implementations={'__fake': GateImplementationInfo(loci=tuple(tuple([locus]) for locus in qubits))}, - default_implementation='__fake', - override_default_implementation={}, - ) return DynamicQuantumArchitecture( calibration_set_id=UUID('00000000-0000-0000-0000-000000000000'), qubits=qubits, diff --git a/tests/conftest.py b/tests/conftest.py index 05bf9ec26..5f7df1e93 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -35,7 +35,11 @@ def reset_mocks_after_tests(): def linear_3q_architecture_static(): return QuantumArchitectureSpecification( name='3q_line', - operations={'prx': [['QB1'], ['QB2'], ['QB3']], 'cz': [['QB1', 'QB2'], ['QB2', 'QB3']]}, + operations={ + 'prx': [['QB1'], ['QB2'], ['QB3']], + 'cz': [['QB1', 'QB2'], ['QB2', 'QB3']], + 'measure': [['QB1'], ['QB2'], ['QB3']], + }, qubits=['QB1', 'QB2', 'QB3'], qubit_connectivity=[['QB1', 'QB2'], ['QB2', 'QB3']], ) @@ -58,6 +62,11 @@ def linear_3q_architecture(): default_implementation='tgss', override_default_implementation={}, ), + 'measure': GateInfo( + implementations={'constant': GateImplementationInfo(loci=(('QB1',), ('QB2',), ('QB3',)))}, + default_implementation='constant', + override_default_implementation={}, + ), }, ) From be65521eb5c1d5bc1481b0297e0ecfc2f8a8497d Mon Sep 17 00:00:00 2001 From: Arianne Meijer Date: Thu, 12 Dec 2024 15:19:40 +0200 Subject: [PATCH 28/47] Added a section of transpiler plugins --- docs/user_guide.rst | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/docs/user_guide.rst b/docs/user_guide.rst index d028e1b27..736f511a8 100644 --- a/docs/user_guide.rst +++ b/docs/user_guide.rst @@ -530,6 +530,7 @@ Similarly, if you want to transpile a circuit that already contains a computatio └───────┘ └───────┘ And if you want force the compiler to use a strict subset of qubits on the device, you can do the following: + .. code-block:: python transpile_to_IQM(circuit, backend=backend, restrict_to_qubits=[4,3,8]) @@ -549,6 +550,27 @@ And if you want force the compiler to use a strict subset of qubits on the devic meas: 3/════════════════════════════════════════════════════╩══╩══╩═ 0 1 2 +Note that if you do this, you do need to provide the :meth:`.IBMBackend.run` method with the qubit restriction: + +.. code-block:: python + + restricted_qubits = [4, 3, 8] + restricted_circuit = transpile_to_IQM(circuit, backend=backend, restrict_to_qubits=restricted_qubits) + job = backend.run(restricted_circuit, qubit_mapping={i: backend.index_to_qubit_name(q) for i, q in enumerate(restricted_qubits)}) + +Using custom IQM transpiler plugins +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +For convenient native integration of the custom IQM transpiler passes with the Qiskit transpiler, we have implemented +several scheduling plugins for the Qiskit transpiler. These plugins can be used as the ``scheduling_method`` argument +which is provided as a string, not a python object. The string is defined in the `pyproject.toml` file of this package +and it points to class that would be used by the Qiskit transpiler. For maintainability, the documentation of these +plugins in found in their respective plugin classes. + +If you are unsure which plugin to use, you can use :meth:`transpile_to_IQM` with the appropriate arguments. This function +determines which plugin to use based on the backend and the provided arguments. +Note that the Qiskit transpiler automatically uses the IQMDefaultSchedulingPlugin when the backend is an IQM backend. + Batch execution of circuits --------------------------- From 177fb0584de9a6140d15cfb37fa23dfd9d7acf87 Mon Sep 17 00:00:00 2001 From: Arianne Meijer Date: Thu, 12 Dec 2024 15:38:45 +0200 Subject: [PATCH 29/47] Added directional CZs --- src/iqm/qiskit_iqm/iqm_backend.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/iqm/qiskit_iqm/iqm_backend.py b/src/iqm/qiskit_iqm/iqm_backend.py index 015a4b2b5..6b9330d48 100644 --- a/src/iqm/qiskit_iqm/iqm_backend.py +++ b/src/iqm/qiskit_iqm/iqm_backend.py @@ -247,13 +247,12 @@ def _create_connections(name: str, is_symmetric: bool = False) -> dict[tuple[int and qb2 not in architecture.computational_resonators ): fake_cz_connections[(component_to_idx[qb1], component_to_idx[qb2])] = None - fake_cz_connections[(component_to_idx[qb2], component_to_idx[qb1])] = None else: move_cz_connections[(component_to_idx[qb1], component_to_idx[qb2])] = None - move_cz_connections[(component_to_idx[qb2], component_to_idx[qb1])] = None for qb1, res in operations['move'].implementations[operations['move'].default_implementation].loci: for qb2 in [q for q in architecture.qubits if q not in [qb1, res]]: - if (qb2, res) in cz_loci or (res, qb2) in cz_loci: + if (qb2, res) in cz_loci: (res, qb2) in cz_loci: + # This is a fake CZ and can be bidirectional. fake_cz_connections[(component_to_idx[qb1], component_to_idx[qb2])] = None fake_cz_connections[(component_to_idx[qb2], component_to_idx[qb1])] = None self.add_instruction(CZGate(), fake_cz_connections) From 0d4add2bfc0617458a2a06247f1c57359e457364 Mon Sep 17 00:00:00 2001 From: Arianne Meijer Date: Thu, 12 Dec 2024 18:52:18 +0200 Subject: [PATCH 30/47] Removed flaky duplicated assert --- tests/test_iqm_transpilation.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/tests/test_iqm_transpilation.py b/tests/test_iqm_transpilation.py index 66b898a12..862c569e1 100644 --- a/tests/test_iqm_transpilation.py +++ b/tests/test_iqm_transpilation.py @@ -158,8 +158,3 @@ def test_optimize_single_qubit_gates_preserves_layout(backend): layout = transpiled_circuit.layout qc_optimized = optimize_single_qubit_gates(transpiled_circuit) assert layout == qc_optimized.layout - # Transpile automatically runs the optimization pass followed by move gate transpilation, - # so the two circuits should be exactly the same if there are no moves. - # Otherwise, some MoveGate and RGate might be swapped when drawing the circuit - if 'move' not in transpiled_circuit.count_ops(): - assert str(transpiled_circuit.draw(output='text')) == str(qc_optimized.draw(output='text')) From c388dd02f5d7d89557557a96a8f428e5d6a9c19f Mon Sep 17 00:00:00 2001 From: Ville Bergholm Date: Mon, 16 Dec 2024 21:17:49 +0200 Subject: [PATCH 31/47] Lots of doc fixes. --- CHANGELOG.rst | 51 +++-- docs/user_guide.rst | 215 +++++++++++----------- pyproject.toml | 10 +- src/iqm/qiskit_iqm/iqm_backend.py | 5 +- src/iqm/qiskit_iqm/iqm_naive_move_pass.py | 22 ++- src/iqm/qiskit_iqm/iqm_provider.py | 32 ++-- src/iqm/qiskit_iqm/move_gate.py | 4 +- src/iqm/qiskit_iqm/transpiler_plugins.py | 16 +- 8 files changed, 185 insertions(+), 170 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index c3831d1e8..9ce74fbb8 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -6,49 +6,46 @@ Version 16.0 ============= * Added support for ``qiskit == 1.2`` and ``qiskit-aer == 0.15``. -* Drop support for ``qiskit < 0.45``. -* :meth:`IQMBackendBase.qubit_name_to_index` and :meth:`IQMBackendBase.index_to_qubit_name` now - raises an error when using an invalid qubit name or index, rather than returning None. -* Refactored :meth:`IQMBackend.create_run_request` to improve user experience when using IQM +* Dropped support for ``qiskit < 0.45``. +* Updated the documentation. +* :meth:`.IQMBackendBase.qubit_name_to_index` and :meth:`.IQMBackendBase.index_to_qubit_name` now + raise an error when using an invalid qubit name or index, rather than returning None. +* Refactored :meth:`.IQMBackend.create_run_request` to improve user experience when using IQM specific run options. -* Updated the documentation for using additional run options with IQM backends. -* Introduced :attr:`IQMBackendBase.physical_target` and :attr:`IQMBackendBase.fake_target` to +* Introduced :attr:`.IQMBackendBase.physical_target` and :attr:`.IQMBackendBase.fake_target` to represent the physical quantum architectures and a Qiskit-compatible version, respectively. -* Moved the circuit serialization logic from :class:`IQMProvider` to :mod:`iqm.qiskit_iqm.qiskit_to_iqm`. -* Using the Qiskit transpiler with :class:`IQMBackend`: +* Moved the circuit serialization logic from :class:`.IQMProvider` to :mod:`iqm.qiskit_iqm.qiskit_to_iqm`. +* Using the Qiskit transpiler with :class:`.IQMBackend`: * You can now use the native Qiskit :func:`transpile` function to transpile a circuit to the IQM Star architecture as long as your initial circuit does not use any resonators. - * The Qiskit transpiler now automatically uses the :class:`IQMOptimizeSingleQubitGates` pass to - optimize single-qubit gates if ``optimization_level >= 0``. + * The Qiskit transpiler now automatically uses the :class:`.IQMOptimizeSingleQubitGates` pass to + optimize single-qubit gates if ``optimization_level > 0``. * There are many new transpiler plugins available that you can use as the ``scheduling_method`` argument in Qiskit's :func:`transpile` function. You can find them in the `Qiskit documentation `_. - * If your circuit contains resonators, and optionally :class:`MoveGate` operations, you can use - the :func:`transpile_to_IQM` function to transpile your circuit for the IQM Star architecture. - * :func:`transpile_to_IQM` can now restrict itself to use a subset of the qubits by specifying + * If your circuit contains resonators, and optionally :class:`.MoveGate` operations, you can use + the :func:`.transpile_to_IQM` function to transpile your circuit for the IQM Star architecture. + * :func:`.transpile_to_IQM` can now restrict itself to use a subset of the qubits by specifying the ``restrict_to_qubits`` argument. You will need to additionally provide a qubit mapping to the - :meth:`backend.run` method to ensure that the correct qubits are used. - * Bugfix where the :func:`transpile_to_IQM` did not retain the circuit layout after transpiling. + :meth:`.IQMBackend.run` method to ensure that the correct qubits are used. + * Bugfix where the :func:`.transpile_to_IQM` did not retain the circuit layout after transpiling. -* Fixed :func:`IQMFakeDeneb` readout errors. Fidelities were reported as errors. `#125 `_ +* Fixed :func:`.IQMFakeDeneb` readout errors. Fidelities were reported as errors. `#125 `_ +* :attr:`.IQMBackend.target` now contains CZ gates only in direction they appear in the calibration + set. `#140 `_ * Deprecated features: - * :func:`optimize_single_qubit_gates` has been deprecated in favor of using the new transpiler - plugins or :func:`transpile_to_IQM`. Additionally, this is now incorporated into the Qiskit + * :func:`.optimize_single_qubit_gates` has been deprecated in favor of using the new transpiler + plugins or :func:`.transpile_to_IQM`. Additionally, this is now incorporated into the Qiskit transpiler as documented above. - * In :meth:`IQMBackend.create_run_request`, and as a result in :meth:`IQMBackend.run`, the + * In :meth:`.IQMBackend.create_run_request`, and as a result in :meth:`.IQMBackend.run`, the ``max_circuit_duration_over_t2`` and ``heralding_mode`` options have been deprecated in favor of - using the :class:`CircuitCompilationOptions` class from :mod:`iqm.iqm_client`. - * The :class:`IQMBackend` no longer uses Qiskit's ``options`` attribute to give run options in - favor of using the arguments of the :meth:`IQMBackend.run` method directly. + using the :class:`.CircuitCompilationOptions` class from :mod:`iqm.iqm_client`. + * The :class:`.IQMBackend` no longer uses Qiskit's ``options`` attribute to give run options in + favor of using the arguments of the :meth:`.IQMBackend.run` method directly. -Version 15.6 -============ - -* Qiskit Target now contains CZ with the directions are calibrated on the hardwared. `#140 `_ - Version 15.5 ============ diff --git a/docs/user_guide.rst b/docs/user_guide.rst index 736f511a8..c3779344d 100644 --- a/docs/user_guide.rst +++ b/docs/user_guide.rst @@ -22,13 +22,13 @@ through the IQM cloud service Resonance, or using an on-premises quantum compute IQM Resonance ~~~~~~~~~~~~~ -1. Login to `IQM Resonance ` with your credentials. +1. Login to `IQM Resonance `_ with your credentials. 2. Upon your first visit to IQM Resonance, you can generate your unique, non-recoverable API token directly from the Dashboard page by selecting ``Generate token``. It's important to copy the token immediately from the window, as you won't be able to do so once the window is closed. If you lose your token, you have the option to regenerate it at any time. However, be aware that regenerating your API token will invalidate any previously generated token. -3. Download one of the demo notebooks from `IQM Academy ` or the +3. Download one of the demo notebooks from `IQM Academy `_ or the `resonance_example.py example file `_ (Save Page As...) 4. Install Qiskit on IQM as instructed below. @@ -39,8 +39,8 @@ IQM Resonance measurements resulting in '00000' and almost half in '11111' - if this is the case, things are set up correctly! -You can find a video guide on how to set things up `here `. -More ready-to-run examples can also be found at `IQM Academy `. +You can find a video guide on how to set things up `here `_. +More ready-to-run examples can also be found at `IQM Academy `_. On-premises device @@ -132,7 +132,7 @@ Let's consider the following quantum circuit which prepares and measures a GHZ s To run this circuit on an IQM quantum computer you need to initialize an :class:`.IQMProvider` instance with the IQM server URL, use it to retrieve an :class:`.IQMBackend` instance representing -the quantum computer, and use Qiskit's :func:`~qiskit.compiler.transpiler.transpile` function +the quantum computer, and use Qiskit's :func:`~qiskit.compiler.transpile` function followed by :meth:`.IQMBackend.run` as usual. ``shots`` denotes the number of times the quantum circuit(s) are sampled: @@ -155,7 +155,7 @@ circuit(s) are sampled: to first transpile the circuit and then run as shown in the code above. Alternatively, the function :func:`.transpile_to_IQM` can also be used to transpile circuits. In particular, when running circuits on devices with computational resonators (the IQM Star architecture), - it is recommended to use :func:`.transpile_to_IQM` instead of :func:`transpile`. + it is recommended to use :func:`.transpile_to_IQM` instead of :func:`~qiskit.compiler.transpile`. FIXME .. note:: @@ -164,18 +164,9 @@ circuit(s) are sampled: `Inspecting circuits before submitting them for execution`_ for inspecting the actual run request sent for execution. -The calibration data for an IQM quantum computer is stored in a calibration set. An :class:`.IQMBackend` instance -always corresponds to a specific calibration set, so that its transpilation target uses only those QPU components -(qubits and computational resonators) and gates which are available in that calibration set. The server default -calibration set will be used by default, but you can also use a different calibration set by specifying the -``calibration_set_id`` parameter for :meth:`.IQMProvider.get_backend` or :class:`.IQMBackend`. If the server default -calibration set has changed after you have created the backend, the backend will still use the original default calibration -set when submitting circuits for execution. When this happens you will get a warning. -You will need to create a new backend if you want to use the new default calibration set instead. - -You can optionally set IQM backend specific options as additional keyword arguments to +You can optionally provide IQM backend specific options as additional keyword arguments to :meth:`.IQMBackend.run`, documented at :meth:`.IQMBackend.create_run_request`. -For example, you can enable heralding measurements by passing the appropriate circuit compilation option as follows: +For example, you can enable heralding measurements using ``circuit_compilation_options`` as follows: .. code-block:: python @@ -183,11 +174,18 @@ For example, you can enable heralding measurements by passing the appropriate ci job = backend.run(transpiled_circuit, shots=1000, circuit_compilation_options=CircuitCompilationOptions(heralding_mode=HeraldingMode.ZEROS)) -Alternatively, you can update the values of the options directly on the backend instance using the :meth:`.IQMBackend.set_options` -and then call :meth:`.IQMBackend.run` without specifying additional keyword arguments. -As the `.backend.options` attribute is used to store additional keyword arguments for :meth:`.IQMBackend.run`, you can find the -an up-to-date list of available options and their current values in the documentation of the :meth:`.IQMBackend.run` method. +Calibration +~~~~~~~~~~~ + +The calibration data for an IQM quantum computer is stored in a *calibration set*. An :class:`.IQMBackend` instance +always corresponds to a specific calibration set, so that its transpilation target uses only those QPU components +(qubits and computational resonators) and gates which are available in that calibration set. The server default +calibration set will be used by default, but you can also use a different calibration set by specifying the +``calibration_set_id`` parameter to :meth:`.IQMProvider.get_backend` or :class:`.IQMBackend`. If the server default +calibration set has changed after you have created the backend, the backend will still use the original default calibration +set when submitting circuits for execution. When this happens you will get a warning. +You will need to create a new backend if you want to use the new default calibration set instead. Inspecting the results ~~~~~~~~~~~~~~~~~~~~~~ @@ -201,13 +199,14 @@ The results of a job that was executed on the IQM quantum computer, represented print(result.get_counts()) print(result.get_memory()) -The result also contains the original request with e.g. the qubit mapping that was used in execution. You -can check this mapping as follows: - +The result comes with some metadata, such as the :class:`~iqm.iqm_client.models.RunRequest` that +produced it in ``result.request``. The request contains e.g. the qubit mapping and the ID of the +calibration set that were used in the execution: .. code-block:: python print(result.request.qubit_mapping) + print(result.request.calibration_set_id) :: @@ -216,9 +215,10 @@ can check this mapping as follows: SingleQubitMapping(logical_name='1', physical_name='QB2'), SingleQubitMapping(logical_name='2', physical_name='QB3') ] + 1320eae6-f4e2-424d-b299-ef82d556d2c3 -The job result also contains metadata on the execution, including timestamps of the various steps of processing the -job. The timestamps are stored in the dict ``result.timestamps``. The job processing has three steps, +Another piece of useful metadata are the timestamps of the various steps of processing the job. The +timestamps are stored in the dict ``result.timestamps``. The job processing has three steps, * ``compile`` where the circuits are converted to instruction schedules, * ``submit`` where the instruction schedules are submitted for execution, and @@ -274,7 +274,7 @@ Note that for IQM backends the identity gate ``id`` is not actually a gate that At IQM we identify qubits by their names, e.g. 'QB1', 'QB2', etc. as demonstrated above. In Qiskit, qubits are identified by their indices in the quantum register, as you can see from the printed coupling map above. Most of the time you do not need to deal with IQM-style qubit names when using Qiskit, however when you need, the methods -:meth:`~.IQMBackendBase.qubit_name_to_index` and :meth:`~.IQMBackendBase.index_to_qubit_name` can become handy. +:meth:`.IQMBackendBase.qubit_name_to_index` and :meth:`.IQMBackendBase.index_to_qubit_name` can become handy. Classically controlled gates @@ -338,7 +338,7 @@ Resetting qubits The :class:`qiskit.circuit.Reset` operation can be used to reset qubits to the :math:`|0\rangle` state. It is currently implemented as a (projective) measurement followed by a classically controlled X gate conditioned -on the result. +on the result, and is only available if the quantum computer supports classically controlled gates. .. code-block:: python @@ -413,23 +413,27 @@ Starting from the :ref:`GHZ circuit ` we created above: q_2 -> 5 ┤ R(π/2,3π/2) ├──────────■───────┤ R(π/2,5π/2) ├─░───────┤M├ ├─────────────┤ │ └─────────────┘ ░ ┌─┐ └╥┘ q_0 -> 10 ┤ R(π/2,3π/2) ├─■────────■───────────────────────░─┤M├────╫─ - ├─────────────┤ │ ┌─────────────┐ ░ └╥┘┌─┐ ║ + ├─────────────┤ │ ┌─────────────┐ ░ └╥┘┌─┐ ║ q_1 -> 15 ┤ R(π/2,3π/2) ├─■─┤ R(π/2,5π/2) ├────────────────░──╫─┤M├─╫─ - └─────────────┘ └─────────────┘ ░ ║ └╥┘ ║ + └─────────────┘ └─────────────┘ ░ ║ └╥┘ ║ meas: 3/════════════════════════════════════════════════════╩══╩══╩═ - 0 1 2 + 0 1 2 Under the hood the Qiskit transpiler uses the :class:`.IQMDefaultSchedulingPlugin` plugin that -automatically adapts the transpiled circuit from Qiskit to the IQM backend. In particular, if the -``optimization_level >= 0``, the plugin will use the :class:`.IQMOptimizeSingleQubitGates` pass to -optimize single-qubit gates, and the :class:`.IQMNaiveResonatorMoving` to insert :class:`.MoveGate` -instructions for devices that have the IQM Star architecture. Alternatively, you can -use the :func:`transpile_to_IQM` function for more precise control over the transpilation process as -documented below. +automatically adapts the transpiled circuit to the IQM backend. In particular, + +* if ``optimization_level > 0``, the plugin will use the :class:`.IQMOptimizeSingleQubitGates` + pass to optimize single-qubit gates, and +* for devices that have the IQM Star architecture, the plugin will use the + :class:`.IQMNaiveResonatorMoving` pass to automatically insert :class:`.MoveGate` instructions + as needed. + +Alternatively, you can use the :func:`transpile_to_IQM` function for more precise control over the +transpilation process as documented below. It is also possible to use one of our other pre-defined transpiler plugins as an argument to -:func:`qiskit.transpile`. For example, +:func:`~qiskit.compiler.transpile`, for example ``transpile(circuit, backend=backend, scheduling_method="only_move_routing_keep")``. Additionally, you can use any of our transpiler passes to define your own :class:`qiskit.transpiler.PassManager` if you want to assemble custom @@ -441,8 +445,8 @@ Computational resonators The IQM Star architecture includes computational resonators as additional QPU components. Because the resonator is not a real qubit, the standard Qiskit transpiler does not know how to compile for it. -Thus, we have a custom scheduling plugin that adds the necessary :class:`.MoveGate` instructions where necessary. -This plugin is executed automatically when you use the Qiskit transpiler. +Thus, we have a custom scheduling plugin that adds the necessary :class:`.MoveGate` instructions where necessary. +This plugin is executed automatically when you use the Qiskit transpiler. Starting from the :ref:`GHZ circuit ` we created above: @@ -452,51 +456,56 @@ Starting from the :ref:`GHZ circuit ` we created above: from iqm.qiskit_iqm import IQMProvider resonator_backend = IQMProvider("https://cocos.resonance.meetiqm.com/deneb").get_backend() - transpiled_circuit2 = transpile(circuit, resonator_backend) + transpiled_circuit = transpile(circuit, resonator_backend) - print(transpiled_circuit2.draw(output='text', idle_wires=False)) + print(transpiled_circuit.draw(output='text', idle_wires=False)) :: - ┌─────────────┐┌───────┐ ┌───────┐ ░ ┌─┐ + ┌─────────────┐┌───────┐ ┌───────┐ ░ ┌─┐ q_0 -> 0 ┤ R(π/2,3π/2) ├┤0 ├──────────────────┤0 ├────────────────░─┤M├────── - ├─────────────┤│ │ ┌─────────────┐│ │ ░ └╥┘┌─┐ + ├─────────────┤│ │ ┌─────────────┐│ │ ░ └╥┘┌─┐ q_1 -> 1 ┤ R(π/2,3π/2) ├┤ ├─■─┤ R(π/2,5π/2) ├┤ ├────────────────░──╫─┤M├─── ├─────────────┤│ Move │ │ └─────────────┘│ Move │┌─────────────┐ ░ ║ └╥┘┌─┐ q_2 -> 2 ┤ R(π/2,3π/2) ├┤ ├─┼────────■───────┤ ├┤ R(π/2,5π/2) ├─░──╫──╫─┤M├ └─────────────┘│ │ │ │ │ │└─────────────┘ ░ ║ ║ └╥┘ ancilla_3 -> 6 ───────────────┤1 ├─■────────■───────┤1 ├───────────────────╫──╫──╫─ - └───────┘ └───────┘ ║ ║ ║ + └───────┘ └───────┘ ║ ║ ║ meas: 3/══════════════════════════════════════════════════════════════════════╩══╩══╩═ - 0 1 2 + 0 1 2 -Under the hood, the IQM Backend pretends that the resonators do not exist for the Qiskit -transpiler, and then uses an additional transpiler stage defined by the :class:`.IQMDefaultSchedulingPlugin` plugin -introduce the resonators and add :class:`MOVE gates <.MoveGate>` between qubits and resonators as -necessary. For more control over the transpilation process, you can use the :meth:`transpile_to_IQM` function documented -below. +Under the hood, the IQM Backend pretends that the resonators do not exist for the Qiskit transpiler, +and then uses an additional transpiler stage defined by :class:`.IQMDefaultSchedulingPlugin` to +introduce resonators and add :class:`MOVE gates <.MoveGate>` between qubits and resonators as +necessary. For more control over the transpilation process, you can use the :func:`.transpile_to_IQM` +function documented below. -The :func:`transpile_to_IQM` function -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Custom transpilation +~~~~~~~~~~~~~~~~~~~~ -As an alternative to the native Qiskit transpiler integration, you can use the :func:`transpile_to_IQM` function. -It is meant for users who want at least one of the following things: +As an alternative to the native Qiskit transpiler integration, you can use the +:func:`.transpile_to_IQM` function. It is meant for users who want at least one of the following: -* more fine grained control over the transpiler process without having to figure out which IQM transpiler plugin to use, +* more fine grained control over the transpilation process without having to figure out which IQM + transpiler plugin to use, * transpile circuits that already contain a computational resonator, or -* forcefully restrict the transpiler to use a strict subset of qubits on the device. +* force the transpiler to use a strict subset of qubits on the device. -For example, if you want to transpile the circuit with `optimization_level=0` but also apply the single qubit gate -optimization pass, you can do the following, equivalent things: +For example, if you want to transpile the circuit with ``optimization_level=0`` but also apply the +single qubit gate optimization pass, you can do one of the following, equivalent things: .. code-block:: python - transpile(circuit, backend=backend, optimization_level=0, scheduling_method='only_Rz_optimization') transpile_to_IQM(circuit, backend=backend, optimization_level=0, perform_move_routing=False, optimize_single_qubits=True) -Similarly, if you want to transpile a circuit that already contains a computational resonator, you can do the following: +.. code-block:: python + + transpile(circuit, backend=backend, optimization_level=0, scheduling_method='only_rz_optimization') + +Similarly, if you want to transpile a circuit that already contains :class:`.MoveGate` instances +(that act on a qubit and a computational resonator), you can do the following: .. code-block:: python @@ -508,68 +517,66 @@ Similarly, if you want to transpile a circuit that already contains a computatio move_circuit.append(MoveGate(), [0, 1]) move_circuit.cx(1, 2) move_circuit.append(MoveGate(), [0, 1]) - # Using transpile() does not work here, as the circuit contains a MoveGate - transpile_to_IQM(move_circuit, backend=resonator_backend, existing_moves_handling=ExistingMoveHandlingOptions.KEEP) + + # Using transpile() does not work here, as the circuit already contains a MoveGate + transpiled_circuit = transpile_to_IQM(move_circuit, backend=resonator_backend, existing_moves_handling=ExistingMoveHandlingOptions.KEEP) + print(transpiled_circuit.draw(output='text', idle_wires=False)) :: - ┌─────────────┐┌───────┐ ┌───────┐ - q_0 -> 0 ┤ R(π/2,3π/2) ├┤0 ├───┤0 ├─────────────── - └─────────────┘│ │ │ │ - ancilla_0 -> 1 ───────────────┤ ├───┤ ├─────────────── - ┌─────────────┐│ │ │ │┌─────────────┐ - q_2 -> 2 ┤ R(π/2,3π/2) ├┤ ├─■─┤ ├┤ R(π/2,5π/2) ├ - └─────────────┘│ │ │ │ │└─────────────┘ - ancilla_1 -> 3 ───────────────┤ Move ├─┼─┤ Move ├─────────────── - │ │ │ │ │ - ancilla_2 -> 4 ───────────────┤ ├─┼─┤ ├─────────────── - │ │ │ │ │ - ancilla_3 -> 5 ───────────────┤ ├─┼─┤ ├─────────────── - │ │ │ │ │ - q_1 -> 6 ───────────────┤1 ├─■─┤1 ├─────────────── - └───────┘ └───────┘ + ┌─────────────┐┌───────┐ ┌───────┐ + q_0 -> 0 ┤ R(π/2,3π/2) ├┤0 ├───┤0 ├─────────────── + ├─────────────┤│ │ │ │┌─────────────┐ + q_2 -> 1 ┤ R(π/2,3π/2) ├┤ Move ├─■─┤ Move ├┤ R(π/2,5π/2) ├ + └─────────────┘│ │ │ │ │└─────────────┘ + q_1 -> 6 ───────────────┤1 ├─■─┤1 ├─────────────── + └───────┘ └───────┘ And if you want force the compiler to use a strict subset of qubits on the device, you can do the following: .. code-block:: python - transpile_to_IQM(circuit, backend=backend, restrict_to_qubits=[4,3,8]) - c = transpile_to_IQM(circuit, backend=backend, restrict_to_qubits=['QB5', 'QB4', 'QB9']) - print(c) + qubits = [4, 3, 8] + # or qubits = ['QB5', 'QB4', 'QB9'] + transpiled_circuit = transpile_to_IQM(circuit, backend=backend, restrict_to_qubits=qubits) + print(transpiled_circuit.draw(output='text', idle_wires=False)) :: - + global phase: 3π/2 - ┌─────────────┐ ┌─────────────┐ ░ ┌─┐ + ┌─────────────┐ ┌─────────────┐ ░ ┌─┐ q_1 -> 0 ┤ R(π/2,3π/2) ├─■─┤ R(π/2,5π/2) ├────────────────░────┤M├─── - ├─────────────┤ │ └─────────────┘ ░ ┌─┐└╥┘ + ├─────────────┤ │ └─────────────┘ ░ ┌─┐└╥┘ q_0 -> 1 ┤ R(π/2,3π/2) ├─■────────■───────────────────────░─┤M├─╫──── ├─────────────┤ │ ┌─────────────┐ ░ └╥┘ ║ ┌─┐ q_2 -> 2 ┤ R(π/2,3π/2) ├──────────■───────┤ R(π/2,5π/2) ├─░──╫──╫─┤M├ └─────────────┘ └─────────────┘ ░ ║ ║ └╥┘ meas: 3/════════════════════════════════════════════════════╩══╩══╩═ - 0 1 2 + 0 1 2 -Note that if you do this, you do need to provide the :meth:`.IBMBackend.run` method with the qubit restriction: +Note that if you do this, you do need to provide the :meth:`.IQMBackend.run` method a qubit +mapping that matches the restriction: .. code-block:: python - restricted_qubits = [4, 3, 8] - restricted_circuit = transpile_to_IQM(circuit, backend=backend, restrict_to_qubits=restricted_qubits) - job = backend.run(restricted_circuit, qubit_mapping={i: backend.index_to_qubit_name(q) for i, q in enumerate(restricted_qubits)}) + qubit_mapping = {i: backend.index_to_qubit_name(q) for i, q in enumerate(qubits)} + job = backend.run(transpiled_circuit, qubit_mapping=qubit_mapping) + Using custom IQM transpiler plugins ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -For convenient native integration of the custom IQM transpiler passes with the Qiskit transpiler, we have implemented -several scheduling plugins for the Qiskit transpiler. These plugins can be used as the ``scheduling_method`` argument -which is provided as a string, not a python object. The string is defined in the `pyproject.toml` file of this package -and it points to class that would be used by the Qiskit transpiler. For maintainability, the documentation of these -plugins in found in their respective plugin classes. +For the native integration of the custom IQM transpiler passes with the Qiskit transpiler, we +have implemented several scheduling plugins for the Qiskit transpiler. These plugins can be used as +the ``scheduling_method`` string argument for :func:`~qiskit.compiler.transpile`. +The mapping between these strings and the classes that implement the plugins is defined in the +:file:`pyproject.toml` file of this package. +The documentation of these plugins in found in the respective plugin classes. -If you are unsure which plugin to use, you can use :meth:`transpile_to_IQM` with the appropriate arguments. This function -determines which plugin to use based on the backend and the provided arguments. -Note that the Qiskit transpiler automatically uses the IQMDefaultSchedulingPlugin when the backend is an IQM backend. +If you are unsure which plugin to use, you can use :func:`.transpile_to_IQM` with the appropriate +arguments. This function determines which plugin to use based on the backend and the provided +arguments. Note that the Qiskit transpiler automatically uses the +:class:`.IQMDefaultSchedulingPlugin` when the backend is an IQM backend. Batch execution of circuits --------------------------- @@ -633,15 +640,15 @@ Multiplexed measurements When multiple measurement instructions are present in a circuit, the measurements may be multiplexed, meaning the measurement pulses would be simultaneously executed on the quantum hardware, if possible. Multiplexing requires the -measurement instructions to be grouped continuously, i.e. not have other instructions between them acting on the same +measurement instructions to form a convex subgraph, i.e. not have other instructions between them acting on the same qubits. -You don't have to do anything special to enable multiplexing, it is automatically attempted by the circuit-to-pulse -compiler on the server side. However, if you want to ensure multiplexing is applied (whenever possible on the hardware -level), you have to put a ``barrier`` instruction in front of and after a group of measurements instructions. -This prevents the transpiler to put other instructions between the measurements. -There is no concept of multiplexed or simultaneous measurements in Qiskit, so the drawings of the circuits still would -not indicate multiplexing. +You don't have to do anything special to enable multiplexing, it is automatically attempted by the +circuit-to-pulse compiler on the server side. However, you can ensure multiplexing (whenever +possible on the hardware level) by putting a ``barrier`` instruction before and after a group of +measurements. This prevents the transpiler from inserting any other instructions between the +measurements. There is no concept of multiplexed or simultaneous measurements in Qiskit, so the +circuit diagram will not indicate any multiplexing: :: diff --git a/pyproject.toml b/pyproject.toml index 625fdb40f..57d05130f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -169,10 +169,10 @@ only_move_routing = "iqm.qiskit_iqm:MoveGateRoutingOnlyPlugin" only_move_routing_keep = "iqm.qiskit_iqm:MoveGateRoutingOnlyKeepExistingMovesPlugin" only_move_routing_remove = "iqm.qiskit_iqm:MoveGateRoutingOnlyRemoveExistingMovesPlugin" only_move_routing_trust = "iqm.qiskit_iqm:MoveGateRoutingOnlyTrustExistingMovesPlugin" -move_routing_exact_global_phase = "iqm.qiskit_iqm:MoveGateRoutingWithExactRzPlugin" -move_routing_Rz_optimization_ignores_barriers = "iqm.qiskit_iqm:MoveGateRoutingWithRzOptimizationIgnoreBarriersPlugin" -only_Rz_optimization = "iqm.qiskit_iqm:OnlyRzOptimizationPlugin" -only_Rz_optimization_exact_global_phase = "iqm.qiskit_iqm:OnlyRzOptimizationExactPlugin" -only_Rz_optimization_ignore_barriers = "iqm.qiskit_iqm:OnlyRzOptimizationIgnoreBarriersPlugin" +move_routing_exact_global_phase = "iqm.qiskit_iqm:MoveGateRoutingWithExactRZPlugin" +move_routing_rz_optimization_ignores_barriers = "iqm.qiskit_iqm:MoveGateRoutingWithRZOptimizationIgnoreBarriersPlugin" +only_rz_optimization = "iqm.qiskit_iqm:OnlyRZOptimizationPlugin" +only_rz_optimization_exact_global_phase = "iqm.qiskit_iqm:OnlyRZOptimizationExactPlugin" +only_rz_optimization_ignore_barriers = "iqm.qiskit_iqm:OnlyRZOptimizationIgnoreBarriersPlugin" iqm_default_scheduling = "iqm.qiskit_iqm:IQMDefaultSchedulingPlugin" diff --git a/src/iqm/qiskit_iqm/iqm_backend.py b/src/iqm/qiskit_iqm/iqm_backend.py index 52e3c8104..18efabfc9 100644 --- a/src/iqm/qiskit_iqm/iqm_backend.py +++ b/src/iqm/qiskit_iqm/iqm_backend.py @@ -84,8 +84,6 @@ class IQMBackendBase(BackendV2, ABC): architecture: Description of the quantum architecture associated with the backend instance. """ - architecture: DynamicQuantumArchitecture - def __init__( self, architecture: Union[QuantumArchitectureSpecification, DynamicQuantumArchitecture], @@ -96,7 +94,8 @@ def __init__( arch = _dqa_from_static_architecture(architecture) else: arch = architecture - self.architecture = arch + self.architecture: DynamicQuantumArchitecture = arch + """Dynamic quantum architecture of the backend instance.""" # Qiskit uses integer indices to refer to qubits, so we need to map component names to indices. # Because of the way the Target and the transpiler interact, the resonators need to have higher indices than diff --git a/src/iqm/qiskit_iqm/iqm_naive_move_pass.py b/src/iqm/qiskit_iqm/iqm_naive_move_pass.py index 6664d7a71..c21c26ee5 100644 --- a/src/iqm/qiskit_iqm/iqm_naive_move_pass.py +++ b/src/iqm/qiskit_iqm/iqm_naive_move_pass.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. """Naive transpilation for the IQM Star architecture.""" -from typing import Dict, List, Optional, Union +from typing import Optional, Union import warnings from pydantic_core import ValidationError @@ -93,16 +93,18 @@ def transpile_to_IQM( # pylint: disable=too-many-arguments circuit: QuantumCircuit, backend: IQMBackendBase, target: Optional[IQMTarget] = None, - initial_layout: Optional[Union[Layout, Dict, List]] = None, + initial_layout: Optional[Union[Layout, dict, list]] = None, perform_move_routing: bool = True, optimize_single_qubits: bool = True, ignore_barriers: bool = False, remove_final_rzs: bool = True, existing_moves_handling: Optional[ExistingMoveHandlingOptions] = None, - restrict_to_qubits: Optional[Union[List[int], List[str]]] = None, + restrict_to_qubits: Optional[Union[list[int], list[str]]] = None, **qiskit_transpiler_qwargs, ) -> QuantumCircuit: - """Basic function for transpiling to IQM backends. Currently works with Deneb and Garnet + """Customized transpilation to IQM backends. + + Works with both the Crystal and Star architectures. Args: circuit: The circuit to be transpiled without MOVE gates. @@ -112,22 +114,22 @@ def transpile_to_IQM( # pylint: disable=too-many-arguments initial_layout: The initial layout to use for the transpilation, same as `qiskit.transpile`. optimize_single_qubits: Whether to optimize single qubit gates away. ignore_barriers: Whether to ignore barriers when optimizing single qubit gates away. - remove_final_rzs: Whether to remove the final Rz rotations. It is recommended always to set this to true as + remove_final_rzs: Whether to remove the final z rotations. It is recommended always to set this to true as the final RZ gates do no change the measurement outcomes of the circuit. existing_moves_handling: How to handle existing MOVE gates in the circuit, required if the circuit contains MOVE gates. restrict_to_qubits: Restrict the transpilation to only use these specific physical qubits. Note that you will - have to pass this information to the `backend.run` method as well as a dictionary. + have to pass this information to the ``backend.run`` method as well as a dictionary. qiskit_transpiler_qwargs: Arguments to be passed to the Qiskit transpiler. Returns: - The transpiled circuit ready for running on the backend. + Transpiled circuit ready for running on the backend. """ # pylint: disable=too-many-branches if restrict_to_qubits is not None: restrict_to_qubits = [ - backend.qubit_name_to_index(q) if isinstance(q, str) else int(q) for q in restrict_to_qubits + backend.qubit_name_to_index(q) if isinstance(q, str) else q for q in restrict_to_qubits ] if target is None: @@ -152,7 +154,7 @@ def transpile_to_IQM( # pylint: disable=too-many-arguments if not remove_final_rzs: scheduling_method = "move_routing_exact_global_phase" elif ignore_barriers: - scheduling_method = "move_routing_Rz_optimization_ignores_barriers" + scheduling_method = "move_routing_rz_optimization_ignores_barriers" else: scheduling_method = "move_routing" else: @@ -166,7 +168,7 @@ def transpile_to_IQM( # pylint: disable=too-many-arguments scheduling_method += "_" + existing_moves_handling.value else: if optimize_single_qubits: - scheduling_method = "only_Rz_optimization" + scheduling_method = "only_rz_optimization" if not remove_final_rzs: scheduling_method += "_exact_global_phase" elif ignore_barriers: diff --git a/src/iqm/qiskit_iqm/iqm_provider.py b/src/iqm/qiskit_iqm/iqm_provider.py index 3348252e1..28e5bd85f 100644 --- a/src/iqm/qiskit_iqm/iqm_provider.py +++ b/src/iqm/qiskit_iqm/iqm_provider.py @@ -17,7 +17,7 @@ from collections.abc import Callable from importlib.metadata import PackageNotFoundError, version -from typing import Optional, Union +from typing import Any, Optional, Union from uuid import UUID import warnings @@ -117,7 +117,7 @@ def create_run_request( run_input: Union[QuantumCircuit, list[QuantumCircuit]], shots: int = 1024, circuit_compilation_options: Optional[CircuitCompilationOptions] = None, - circuit_callback: Optional[Callable] = None, + circuit_callback: Optional[Callable[[list[QuantumCircuit]], Any]] = None, qubit_mapping: Optional[dict[int, str]] = None, **unknown_options, ) -> RunRequest: @@ -128,12 +128,13 @@ def create_run_request( Args: run_input: Same as in :meth:`run`. - Keyword Args: - shots (int): Number of repetitions of each circuit, for sampling. Default is 1024. - circuit_compilation_options (iqm.iqm_client.models.CircuitCompilationOptions): - Compilation options for the circuits, passed on to :mod:`iqm-client`. If not provided, the default is - the ``CircuitCompilationOptions`` default. - circuit_callback (collections.abc.Callable[[list[QuantumCircuit]], Any]): + Args: + shots: Number of repetitions of each circuit, for sampling. + circuit_compilation_options: + Compilation options for the circuits, passed on to :class:`~iqm.iqm_client.iqm_client.IQMClient`. + If ``None``, the defaults of the :class:`~iqm.iqm_client.models.CircuitCompilationOptions` + class are used. + circuit_callback: Callback function that, if provided, will be called for the circuits before sending them to the device. This may be useful in situations when you do not have explicit control over transpilation, but need some information on how it was done. This can @@ -144,8 +145,8 @@ def create_run_request( As a side effect, you can also use this callback to modify the transpiled circuits in-place, just before execution; however, we do not recommend to use it for this purpose. - qubit_mapping: Mapping from qubit indices in the circuit to qubit names on the device. If not provided, - `self.index_to_qubit` will be used. + qubit_mapping: Mapping from qubit indices in the circuit to qubit names on the device. If ``None``, + :attr:`.IQMBackendBase.index_to_qubit_name` will be used. Returns: The created run request object @@ -204,7 +205,14 @@ def create_run_request( return run_request def retrieve_job(self, job_id: str) -> IQMJob: - """Create and return an IQMJob instance associated with this backend with given job id.""" + """Create and return an IQMJob instance associated with this backend with given job id. + + Args: + job_id: ID of the job to retrieve. + + Returns: + corresponding job + """ return IQMJob(self, job_id) def close_client(self) -> None: @@ -230,7 +238,7 @@ def serialize_circuit(self, circuit: QuantumCircuit, qubit_mapping: Optional[dic Args: circuit: quantum circuit to serialize qubit_mapping: Mapping from qubit indices in the circuit to qubit names on the device. If not provided, - `self.index_to_qubit` will be used. + :attr:`.IQMBackendBase.index_to_qubit_name` will be used. Returns: data transfer object representing the circuit diff --git a/src/iqm/qiskit_iqm/move_gate.py b/src/iqm/qiskit_iqm/move_gate.py index 1d36f19f5..dadc31ad1 100644 --- a/src/iqm/qiskit_iqm/move_gate.py +++ b/src/iqm/qiskit_iqm/move_gate.py @@ -35,7 +35,9 @@ class MoveGate(Gate): recommended that no single qubit gates are applied to the qubit in between a pair of MOVE operations. - Note: At this point the locus for the move gate must be defined in the order: ``[qubit, resonator]``. + .. note:: + The MOVE gate must always be be applied on the qubit and the resonator in the + order ``[qubit, resonator]``, regardless of which component is currently holding the state. """ def __init__(self, label=None): diff --git a/src/iqm/qiskit_iqm/transpiler_plugins.py b/src/iqm/qiskit_iqm/transpiler_plugins.py index b05756c76..ef686e93a 100644 --- a/src/iqm/qiskit_iqm/transpiler_plugins.py +++ b/src/iqm/qiskit_iqm/transpiler_plugins.py @@ -30,7 +30,7 @@ class IQMSchedulingPlugin(PassManagerStagePlugin): Args: move_gate_routing: whether to include MoveGate routing in the scheduling stage. optimize_sqg: Whether to include single qubit gate optimization in the scheduling stage. - drop_final_rz: Whether to drop trailing Rz gates in the circuit during single qubit gate optimization. + drop_final_rz: Whether to drop trailing RZ gates in the circuit during single qubit gate optimization. ignore_barriers: Whether to ignore barriers during single qubit gate optimization. existing_move_handling: How to handle existing MoveGates in the circuit during MoveGate routing. Raises: @@ -130,16 +130,16 @@ def __init__(self): ) -class MoveGateRoutingWithExactRzPlugin(MoveGateRoutingPlugin): +class MoveGateRoutingWithExactRZPlugin(MoveGateRoutingPlugin): """Plugin class for single qubit gate optimization and MoveGate routing where - trailing Rz Gates are kept in the circuit. + trailing RZ gates are kept in the circuit. """ def __init__(self): super().__init__(optimize_sqg=True, drop_final_rz=False) -class MoveGateRoutingWithRzOptimizationIgnoreBarriersPlugin(MoveGateRoutingPlugin): +class MoveGateRoutingWithRZOptimizationIgnoreBarriersPlugin(MoveGateRoutingPlugin): """Plugin class for single qubit gate optimization and MoveGate routing where barriers are ignored during optimization. """ @@ -184,7 +184,7 @@ def __init__(self): ) -class OnlyRzOptimizationPlugin(IQMSchedulingPlugin): +class OnlyRZOptimizationPlugin(IQMSchedulingPlugin): """Plugin class for single qubit gate optimization without MOVE gate routing.""" def __init__( @@ -195,16 +195,16 @@ def __init__( super().__init__(False, True, drop_final_rz, ignore_barriers, None) -class OnlyRzOptimizationExactPlugin(OnlyRzOptimizationPlugin): +class OnlyRZOptimizationExactPlugin(OnlyRZOptimizationPlugin): """Plugin class for single qubit gate optimization without MOVE gate routing and - the final Rz gates are not dropped. + the final RZ gates are not dropped. """ def __init__(self): super().__init__(drop_final_rz=False) -class OnlyRzOptimizationIgnoreBarriersPlugin(OnlyRzOptimizationPlugin): +class OnlyRZOptimizationIgnoreBarriersPlugin(OnlyRZOptimizationPlugin): """Plugin class for single qubit gate optimization without MOVE gate routing where barriers are ignored.""" def __init__(self): From 322dc56243dc28a368f20e852799b4974bd74665 Mon Sep 17 00:00:00 2001 From: Ville Bergholm Date: Mon, 16 Dec 2024 22:06:32 +0200 Subject: [PATCH 32/47] Cleanup. --- .../fake_backends/iqm_fake_backend.py | 19 +++++++------------ src/iqm/qiskit_iqm/iqm_backend.py | 4 ++-- src/iqm/qiskit_iqm/move_gate.py | 2 ++ 3 files changed, 11 insertions(+), 14 deletions(-) diff --git a/src/iqm/qiskit_iqm/fake_backends/iqm_fake_backend.py b/src/iqm/qiskit_iqm/fake_backends/iqm_fake_backend.py index 554e39062..b8ce5e492 100644 --- a/src/iqm/qiskit_iqm/fake_backends/iqm_fake_backend.py +++ b/src/iqm/qiskit_iqm/fake_backends/iqm_fake_backend.py @@ -33,10 +33,6 @@ from iqm.qiskit_iqm.iqm_transpilation import IQMReplaceGateWithUnitaryPass from iqm.qiskit_iqm.move_gate import MOVE_GATE_UNITARY -GATE_TO_UNITARY = { - "move": MOVE_GATE_UNITARY, -} - # pylint: disable=too-many-instance-attributes @dataclass @@ -288,13 +284,10 @@ def max_circuits(self) -> Optional[int]: def run(self, run_input: Union[QuantumCircuit, list[QuantumCircuit]], **options) -> JobV1: """ - Run ``run_input`` on the fake backend using a simulator. - - This method runs circuit jobs (an individual or a list of QuantumCircuit or IQMCircuit ) - and returns a :class:`~qiskit.providers.JobV1` object. + Run quantum circuits on the fake backend (by simulating them). - It will run the simulation with a noise model of the fake backend (e.g. Adonis, Deneb). - Validity of MOVE gates is also checked. + This method will run the simulation with the noise model of the fake backend. + Validity of the circuits is also checked. Args: run_input: One or more quantum circuits to simulate on the backend. @@ -302,7 +295,7 @@ def run(self, run_input: Union[QuantumCircuit, list[QuantumCircuit]], **options) Returns: The job object representing the run. Raises: - ValueError: If empty list of circuits is provided. + ValueError: Empty list of circuits was provided. """ circuits_aux = [run_input] if isinstance(run_input, QuantumCircuit) else run_input @@ -310,6 +303,9 @@ def run(self, run_input: Union[QuantumCircuit, list[QuantumCircuit]], **options) raise ValueError("Empty list of circuits submitted for execution.") circuits = [] + GATE_TO_UNITARY = { + "move": MOVE_GATE_UNITARY, + } for circ in circuits_aux: validate_circuit(circ, self) @@ -325,7 +321,6 @@ def run(self, run_input: Union[QuantumCircuit, list[QuantumCircuit]], **options) sim_noise = AerSimulator(noise_model=self.noise_model) job = sim_noise.run(circuits, shots=shots) - return job def validate_compatible_architecture(self, architecture: DynamicQuantumArchitecture) -> bool: diff --git a/src/iqm/qiskit_iqm/iqm_backend.py b/src/iqm/qiskit_iqm/iqm_backend.py index 18efabfc9..d7eeec048 100644 --- a/src/iqm/qiskit_iqm/iqm_backend.py +++ b/src/iqm/qiskit_iqm/iqm_backend.py @@ -18,7 +18,7 @@ from abc import ABC from copy import deepcopy import re -from typing import Final, List, Union +from typing import Final, Union from uuid import UUID from qiskit.circuit import Parameter, Reset @@ -272,7 +272,7 @@ def set_real_target(self, real_target: IQMTarget) -> None: """ self.real_target = real_target - def restrict_to_qubits(self, qubits: Union[List[int], List[str]]) -> IQMTarget: + def restrict_to_qubits(self, qubits: Union[list[int], list[str]]) -> IQMTarget: """Restrict the target to only the given qubits. Args: diff --git a/src/iqm/qiskit_iqm/move_gate.py b/src/iqm/qiskit_iqm/move_gate.py index dadc31ad1..43f29113a 100644 --- a/src/iqm/qiskit_iqm/move_gate.py +++ b/src/iqm/qiskit_iqm/move_gate.py @@ -17,6 +17,8 @@ import qiskit.quantum_info as qi MOVE_GATE_UNITARY = [[1.0, 0.0, 0.0, 0.0], [0.0, 0.0, 1.0, 0.0], [0.0, 1.0, 0.0, 0.0], [0.0, 0.0, 0.0, 1.0]] +"""Unitary for simulating the ideal MOVE gate. It is not fully realistic since it applies a zero phase on the +moved state and does nothing in the :math:`|11\rangle` subspace, thus being equal to the SWAP gate.""" class MoveGate(Gate): From b4a096cfb0c893d251aca60c28a4fde2f8d638ef Mon Sep 17 00:00:00 2001 From: Ville Bergholm Date: Mon, 16 Dec 2024 22:07:15 +0200 Subject: [PATCH 33/47] Remove DQA sorting, it should be sorted already. --- src/iqm/qiskit_iqm/iqm_backend.py | 37 +++++------------ src/iqm/qiskit_iqm/iqm_naive_move_pass.py | 4 +- tests/conftest.py | 42 -------------------- tests/fake_backends/test_iqm_fake_backend.py | 3 +- tests/test_iqm_backend_base.py | 11 ++--- 5 files changed, 16 insertions(+), 81 deletions(-) diff --git a/src/iqm/qiskit_iqm/iqm_backend.py b/src/iqm/qiskit_iqm/iqm_backend.py index d7eeec048..f8f83343d 100644 --- a/src/iqm/qiskit_iqm/iqm_backend.py +++ b/src/iqm/qiskit_iqm/iqm_backend.py @@ -17,7 +17,6 @@ from abc import ABC from copy import deepcopy -import re from typing import Final, Union from uuid import UUID @@ -69,14 +68,6 @@ def _dqa_from_static_architecture(sqa: QuantumArchitectureSpecification) -> Dyna ) -def _component_sort_key(component_name: str) -> tuple[str, int, str]: - def get_numeric_id(name: str) -> int: - match = re.search(r'(\d+)', name) - return int(match.group(1)) if match else 0 - - return re.sub(r'[^a-zA-Z]', '', component_name), get_numeric_id(component_name), component_name - - class IQMBackendBase(BackendV2, ABC): """Abstract base class for various IQM-specific backends. @@ -100,13 +91,7 @@ def __init__( # Qiskit uses integer indices to refer to qubits, so we need to map component names to indices. # Because of the way the Target and the transpiler interact, the resonators need to have higher indices than # qubits, or else transpiling with optimization_level=0 will fail because of lacking resonator indices. - qb_to_idx = { - qb: idx - for idx, qb in enumerate( - sorted(arch.qubits, key=_component_sort_key) - + sorted(arch.computational_resonators, key=_component_sort_key) - ) - } + qb_to_idx = {qb: idx for idx, qb in enumerate(arch.qubits + arch.computational_resonators)} self._target = IQMTarget(arch, qb_to_idx) self._qb_to_idx = qb_to_idx @@ -165,14 +150,14 @@ def get_scheduling_stage_plugin(self) -> str: class IQMTarget(Target): - """A target representing an IQM backends that could have resonators. + """Transpiler target representing an IQM backend that can have computational resonators. - This target is used to represent the physical layout of the backend, including the resonators as well as a fake - coupling map to present to the Qiskit transpiler. + This target represents the physical layout of the backend including the resonators, as + well as a fake coupling map without them to present to the Qiskit transpiler. Args: - architecture: The quantum architecture specification representing the backend. - component_to_idx: A mapping from component names to integer indices. + architecture: Represents the gates (and their loci) available for the transpilation. + component_to_idx: Mapping from QPU component names to integer indices used by Qiskit to refer to them. """ def __init__(self, architecture: DynamicQuantumArchitecture, component_to_idx: dict[str, int]): @@ -194,12 +179,12 @@ def _iqm_create_instructions(self, architecture: DynamicQuantumArchitecture, com # pylint: disable=too-many-branches operations = architecture.gates - # There is no dedicated direct way of setting just the qubit connectivity and the native gates to the target. - # Such info is automatically deduced once all instruction properties are set. Currently, we do not retrieve - # any properties from the server, and we are interested only in letting the target know what is the native gate - # set and the connectivity of the device under use. Thus, we populate the target with None properties. def _create_connections(name: str, is_symmetric: bool = False) -> dict[tuple[int, ...], None]: - """Creates the connection map of allowed loci for this instruction, mapped to None.""" + """Creates the Qiskit instruction properties dictionary for the given IQM native operation. + + Currently we do not provide any actual properties for the operation, hence the all the + allowed loci map to None. + """ gate_info = operations[name] all_loci = gate_info.implementations[gate_info.default_implementation].loci connections = {tuple(component_to_idx[locus] for locus in loci): None for loci in all_loci} diff --git a/src/iqm/qiskit_iqm/iqm_naive_move_pass.py b/src/iqm/qiskit_iqm/iqm_naive_move_pass.py index c21c26ee5..3bc4e6bd9 100644 --- a/src/iqm/qiskit_iqm/iqm_naive_move_pass.py +++ b/src/iqm/qiskit_iqm/iqm_naive_move_pass.py @@ -128,9 +128,7 @@ def transpile_to_IQM( # pylint: disable=too-many-arguments # pylint: disable=too-many-branches if restrict_to_qubits is not None: - restrict_to_qubits = [ - backend.qubit_name_to_index(q) if isinstance(q, str) else q for q in restrict_to_qubits - ] + restrict_to_qubits = [backend.qubit_name_to_index(q) if isinstance(q, str) else q for q in restrict_to_qubits] if target is None: if circuit.count_ops().get("move", 0) > 0: diff --git a/tests/conftest.py b/tests/conftest.py index 6878aa445..e662f0e88 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -112,48 +112,6 @@ def adonis_architecture(): ) -@pytest.fixture -def adonis_shuffled_names_architecture(): - """Like adonis_architecture, but relative order of loci has been shuffled.""" - return DynamicQuantumArchitecture( - calibration_set_id=UUID('59478539-dcef-4b2e-80c8-122d7ec3fc89'), - qubits=['QB2', 'QB3', 'QB1', 'QB5', 'QB4'], - computational_resonators=[], - gates={ - 'prx': GateInfo( - implementations={ - 'drag_gaussian': GateImplementationInfo(loci=(('QB2',), ('QB3',), ('QB1',), ('QB5',), ('QB4',))) - }, - default_implementation='drag_gaussian', - override_default_implementation={}, - ), - 'cc_prx': GateInfo( - implementations={ - 'prx_composite': GateImplementationInfo(loci=(('QB5',), ('QB3',), ('QB2',), ('QB4',), ('QB1',))) - }, - default_implementation='prx_composite', - override_default_implementation={}, - ), - 'cz': GateInfo( - implementations={ - 'tgss': GateImplementationInfo( - loci=(('QB1', 'QB3'), ('QB2', 'QB3'), ('QB4', 'QB3'), ('QB5', 'QB3')) - ) - }, - default_implementation='tgss', - override_default_implementation={}, - ), - 'measure': GateInfo( - implementations={ - 'constant': GateImplementationInfo(loci=(('QB2',), ('QB3',), ('QB1',), ('QB5',), ('QB4',))) - }, - default_implementation='constant', - override_default_implementation={}, - ), - }, - ) - - @pytest.fixture def move_architecture(): return DynamicQuantumArchitecture( diff --git a/tests/fake_backends/test_iqm_fake_backend.py b/tests/fake_backends/test_iqm_fake_backend.py index fbda80138..ec97a8b05 100644 --- a/tests/fake_backends/test_iqm_fake_backend.py +++ b/tests/fake_backends/test_iqm_fake_backend.py @@ -237,9 +237,8 @@ def test_noise_model_contains_all_errors(backend): def test_validate_compatible_architecture( - adonis_architecture, adonis_shuffled_names_architecture, linear_3q_architecture + adonis_architecture, linear_3q_architecture ): backend = IQMFakeAdonis() assert backend.validate_compatible_architecture(adonis_architecture) is True - assert backend.validate_compatible_architecture(adonis_shuffled_names_architecture) is True assert backend.validate_compatible_architecture(linear_3q_architecture) is False diff --git a/tests/test_iqm_backend_base.py b/tests/test_iqm_backend_base.py index bebc7020a..5e46467f8 100644 --- a/tests/test_iqm_backend_base.py +++ b/tests/test_iqm_backend_base.py @@ -45,15 +45,10 @@ def backend(linear_3q_architecture): return DummyIQMBackend(linear_3q_architecture) -def test_qubit_name_to_index_to_qubit_name(adonis_shuffled_names_architecture): - backend = DummyIQMBackend(adonis_shuffled_names_architecture) +def test_qubit_name_to_index_to_qubit_name(adonis_architecture): + backend = DummyIQMBackend(adonis_architecture) - correct_idx_name_associations = set(enumerate(['QB1', 'QB2', 'QB3', 'QB4', 'QB5'])) - - print(backend._idx_to_qb) - print(backend._qb_to_idx) - # Unrolled for debugging purposes - for idx, name in correct_idx_name_associations: + for idx, name in backend._idx_to_qb.items(): assert backend.index_to_qubit_name(idx) == name assert backend.qubit_name_to_index(name) == idx From 33c6d9bb4b823d6a95b283cc50af56fb06cfa3ca Mon Sep 17 00:00:00 2001 From: Ville Bergholm Date: Wed, 18 Dec 2024 17:11:55 +0200 Subject: [PATCH 34/47] Cleanup --- docs/user_guide.rst | 23 ++-- pyproject.toml | 2 +- src/iqm/qiskit_iqm/iqm_backend.py | 111 ++++++++++--------- src/iqm/qiskit_iqm/iqm_move_layout.py | 2 +- src/iqm/qiskit_iqm/iqm_naive_move_pass.py | 32 +++--- src/iqm/qiskit_iqm/move_gate.py | 6 +- src/iqm/qiskit_iqm/qiskit_to_iqm.py | 6 +- tests/fake_backends/test_iqm_fake_backend.py | 4 +- 8 files changed, 93 insertions(+), 93 deletions(-) diff --git a/docs/user_guide.rst b/docs/user_guide.rst index c3779344d..15f0aed7a 100644 --- a/docs/user_guide.rst +++ b/docs/user_guide.rst @@ -648,20 +648,17 @@ circuit-to-pulse compiler on the server side. However, you can ensure multiplexi possible on the hardware level) by putting a ``barrier`` instruction before and after a group of measurements. This prevents the transpiler from inserting any other instructions between the measurements. There is no concept of multiplexed or simultaneous measurements in Qiskit, so the -circuit diagram will not indicate any multiplexing: - -:: - - ░ ┌─┐ ░ - q_0: ─░─┤M├───────░─ - ░ └╥┘┌─┐ ░ - q_1: ─░──╫─┤M├────░─ - ░ ║ └╥┘┌─┐ ░ - q_2: ─░──╫──╫─┤M├─░─ - ░ ║ ║ └╥┘ ░ +circuit diagram will not indicate any multiplexing:: + + ░ ┌─┐ ░ + q_0: ─░─┤M├───────░─ + ░ └╥┘┌─┐ ░ + q_1: ─░──╫─┤M├────░─ + ░ ║ └╥┘┌─┐ ░ + q_2: ─░──╫──╫─┤M├─░─ + ░ ║ ║ └╥┘ ░ meas: 3/════╩══╩══╩═══ - 0 1 2 - + 0 1 2 Simulation diff --git a/pyproject.toml b/pyproject.toml index 57d05130f..7e8b22de1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,7 +22,7 @@ dependencies = [ "numpy", "qiskit >= 0.46, < 1.3", "qiskit-aer >= 0.13.1, < 0.16", - "iqm-client >= 20.0, < 21.0" + "iqm-client >= 21.0, < 22.0" ] [project.urls] diff --git a/src/iqm/qiskit_iqm/iqm_backend.py b/src/iqm/qiskit_iqm/iqm_backend.py index f8f83343d..120b9455e 100644 --- a/src/iqm/qiskit_iqm/iqm_backend.py +++ b/src/iqm/qiskit_iqm/iqm_backend.py @@ -17,6 +17,7 @@ from abc import ABC from copy import deepcopy +import itertools from typing import Final, Union from uuid import UUID @@ -35,6 +36,8 @@ IQM_TO_QISKIT_GATE_NAME: Final[dict[str, str]] = {'prx': 'r', 'cz': 'cz'} +Locus = tuple[str, ...] + def _dqa_from_static_architecture(sqa: QuantumArchitectureSpecification) -> DynamicQuantumArchitecture: """Create a dynamic quantum architecture from the given static quantum architecture. @@ -162,101 +165,105 @@ class IQMTarget(Target): def __init__(self, architecture: DynamicQuantumArchitecture, component_to_idx: dict[str, int]): super().__init__() - self.iqm_dynamic_architecture = architecture + self.iqm_dqa = architecture self.iqm_component_to_idx = component_to_idx self.iqm_idx_to_component = {v: k for k, v in component_to_idx.items()} - self.real_target = self._iqm_create_instructions(architecture, component_to_idx) + self.real_target: IQMTarget = self._iqm_create_instructions(architecture, component_to_idx) - def _iqm_create_instructions(self, architecture: DynamicQuantumArchitecture, component_to_idx: dict[str, int]): + def _iqm_create_instructions( + self, architecture: DynamicQuantumArchitecture, component_to_idx: dict[str, int] + ) -> Target: """Converts a QuantumArchitectureSpecification object to a Qiskit Target object. Args: architecture: The quantum architecture specification to convert. + component_to_idx: Mapping from QPU component names to integer indices used by Qiskit to refer to them. Returns: A Qiskit Target object representing the given quantum architecture specification. """ # pylint: disable=too-many-branches - operations = architecture.gates - - def _create_connections(name: str, is_symmetric: bool = False) -> dict[tuple[int, ...], None]: + # mapping from op name to all its allowed loci + op_loci = {gate_name: gate_info.loci for gate_name, gate_info in architecture.gates.items()} + + def idx_locus(locus: Locus) -> tuple[int, ...]: + """Map the given locus to use component indices instead of component names.""" + return tuple(component_to_idx[component] for component in locus) + + def create_properties(name: str, *, symmetrize: bool = False) -> dict[tuple[int, ...], None]: """Creates the Qiskit instruction properties dictionary for the given IQM native operation. Currently we do not provide any actual properties for the operation, hence the all the allowed loci map to None. """ - gate_info = operations[name] - all_loci = gate_info.implementations[gate_info.default_implementation].loci - connections = {tuple(component_to_idx[locus] for locus in loci): None for loci in all_loci} - if is_symmetric: - # If the gate is symmetric, we need to add the reverse connections as well. - connections.update({tuple(reversed(loci)): None for loci in connections}) - return connections - - if 'prx' in operations or 'phased_rx' in operations: - self.add_instruction( - RGate(Parameter('theta'), Parameter('phi')), - _create_connections('prx'), - ) - if 'cc_prx' in operations: - # HACK reset gate shares cc_prx loci for now - self.add_instruction(Reset(), _create_connections('cc_prx')) + loci = op_loci[name] + if symmetrize: + # symmetrize the loci + loci = tuple(permuted_locus for locus in loci for permuted_locus in itertools.permutations(locus)) + return {idx_locus(locus): None for locus in loci} + + if 'measure' in op_loci: + self.add_instruction(Measure(), create_properties('measure')) + # identity gate does nothing and is removed in serialization, so we may as well allow it everywhere self.add_instruction( IGate(), - {(component_to_idx[qb],): None for qb in architecture.computational_resonators + architecture.qubits}, + {idx_locus((component,)): None for component in architecture.components}, ) - # Even though CZ is a symmetric gate, we still need to add properties for both directions. This is because - # coupling maps in Qiskit are directed graphs and the gate symmetry is not implicitly planted there. It should - # be explicitly supplied. This allows Qiskit to have coupling maps with non-symmetric gates like cx. - if 'measure' in operations: - self.add_instruction(Measure(), _create_connections('measure')) + + if 'prx' in op_loci: + self.add_instruction( + RGate(Parameter('theta'), Parameter('phi')), + create_properties('prx'), + ) + + # HACK reset gate shares cc_prx loci for now + if 'cc_prx' in op_loci: + self.add_instruction(Reset(), create_properties('cc_prx')) # Special work for devices with a MoveGate. real_target: IQMTarget = deepcopy(self) - if 'move' in operations: - real_target.add_instruction(MoveGate(), _create_connections('move')) + if 'move' in op_loci: + real_target.add_instruction(MoveGate(), create_properties('move')) fake_target_with_moves = deepcopy(real_target) - if 'cz' in operations: - real_target.add_instruction(CZGate(), _create_connections('cz')) - if 'move' in operations: + # self has just single-q stuff, fake and real also have MOVE + + if 'cz' in op_loci: + real_target.add_instruction(CZGate(), create_properties('cz')) + if 'move' in op_loci: fake_cz_connections: dict[tuple[int, int], None] = {} - cz_loci = operations['cz'].implementations[operations['cz'].default_implementation].loci move_cz_connections: dict[tuple[int, int], None] = {} + cz_loci = op_loci['cz'] for qb1, qb2 in cz_loci: if ( qb1 not in architecture.computational_resonators and qb2 not in architecture.computational_resonators ): + # every cz locus that only uses qubits goes to fake_cz_conn fake_cz_connections[(component_to_idx[qb1], component_to_idx[qb2])] = None else: + # otherwise it goes to move_cz_conn move_cz_connections[(component_to_idx[qb1], component_to_idx[qb2])] = None - for qb1, res in operations['move'].implementations[operations['move'].default_implementation].loci: + for qb1, res in op_loci['move']: for qb2 in [q for q in architecture.qubits if q not in [qb1, res]]: + # loop over qb2 that is not qb1 if (qb2, res) in cz_loci or (res, qb2) in cz_loci: # This is a fake CZ and can be bidirectional. + # cz routable via res between qubits, put into fake_cz_conn both ways fake_cz_connections[(component_to_idx[qb1], component_to_idx[qb2])] = None fake_cz_connections[(component_to_idx[qb2], component_to_idx[qb1])] = None - self.add_instruction(CZGate(), fake_cz_connections) + self.add_instruction(CZGate(), fake_cz_connections) # self has fake cz conn fake_cz_connections.update(move_cz_connections) fake_target_with_moves.add_instruction(CZGate(), fake_cz_connections) else: - self.add_instruction(CZGate(), _create_connections('cz')) - fake_target_with_moves.add_instruction(CZGate(), _create_connections('cz')) - fake_target_with_moves.set_real_target(real_target) + self.add_instruction(CZGate(), create_properties('cz')) + fake_target_with_moves.add_instruction(CZGate(), create_properties('cz')) + fake_target_with_moves.real_target = real_target self.fake_target_with_moves: IQMTarget = fake_target_with_moves return real_target - def set_real_target(self, real_target: IQMTarget) -> None: - """Set the real target for this target. - - Args: - real_target: The real target to set. - """ - self.real_target = real_target - def restrict_to_qubits(self, qubits: Union[list[int], list[str]]) -> IQMTarget: """Restrict the target to only the given qubits. @@ -265,7 +272,7 @@ def restrict_to_qubits(self, qubits: Union[list[int], list[str]]) -> IQMTarget: """ qubits_str = [self.iqm_idx_to_component[q] if isinstance(q, int) else str(q) for q in qubits] new_gates = {} - for gate_name, gate_info in self.iqm_dynamic_architecture.gates.items(): + for gate_name, gate_info in self.iqm_dqa.gates.items(): new_implementations = {} for implementation_name, implementation_info in gate_info.implementations.items(): new_loci = [loci for loci in implementation_info.loci if all(q in qubits_str for q in loci)] @@ -278,11 +285,9 @@ def restrict_to_qubits(self, qubits: Union[list[int], list[str]]) -> IQMTarget: override_default_implementation=gate_info.override_default_implementation, ) new_arch = DynamicQuantumArchitecture( - calibration_set_id=self.iqm_dynamic_architecture.calibration_set_id, - qubits=[q for q in qubits_str if q in self.iqm_dynamic_architecture.qubits], - computational_resonators=[ - q for q in qubits_str if q in self.iqm_dynamic_architecture.computational_resonators - ], + calibration_set_id=self.iqm_dqa.calibration_set_id, + qubits=[q for q in qubits_str if q in self.iqm_dqa.qubits], + computational_resonators=[q for q in qubits_str if q in self.iqm_dqa.computational_resonators], gates=new_gates, ) return IQMTarget(new_arch, {name: idx for idx, name in enumerate(qubits_str)}) diff --git a/src/iqm/qiskit_iqm/iqm_move_layout.py b/src/iqm/qiskit_iqm/iqm_move_layout.py index 986d19743..6741a7bcf 100644 --- a/src/iqm/qiskit_iqm/iqm_move_layout.py +++ b/src/iqm/qiskit_iqm/iqm_move_layout.py @@ -114,7 +114,7 @@ def _get_qubit_types(self) -> dict[int, str]: """ target: IQMTarget = self.target qubit_types: dict[int, str] = {} - for gate_name, gate_info in target.iqm_dynamic_architecture.gates.items(): + for gate_name, gate_info in target.iqm_dqa.gates.items(): if gate_name == 'move': for locus in gate_info.loci: [qubit, resonator] = [target.iqm_component_to_idx[q] for q in locus] diff --git a/src/iqm/qiskit_iqm/iqm_naive_move_pass.py b/src/iqm/qiskit_iqm/iqm_naive_move_pass.py index 3bc4e6bd9..3b8d7ed87 100644 --- a/src/iqm/qiskit_iqm/iqm_naive_move_pass.py +++ b/src/iqm/qiskit_iqm/iqm_naive_move_pass.py @@ -40,6 +40,11 @@ class IQMNaiveResonatorMoving(TransformationPass): The pass assumes that all single qubit and two-qubit gates are allowed. The resonator is used to swap the qubit states for the two-qubit gates. Additionally, it assumes that no single qubit gates are allowed on the resonator. + + Args: + target: Transpilation target. + gate_set: Basis gates of the target backend. + existing_moves_handling: ffff """ def __init__( @@ -48,41 +53,34 @@ def __init__( gate_set: list[str], existing_moves_handling: Optional[ExistingMoveHandlingOptions] = None, ): - """WIP Naive transpilation pass for resonator moving - - Args: - resonator_register (int): Which qubit/vertex index represents the resonator. - move_qubits (int): Which qubits (indices) can be moved into the resonator. - gate_set (list[str]): Which gates are allowed by the target backend. - """ super().__init__() self.target = target self.gate_set = gate_set self.existing_moves_handling = existing_moves_handling - def run(self, dag: DAGCircuit): # pylint: disable=too-many-branches - """Run the IQMNaiveResonatorMoving pass on `dag`. + def run(self, dag: DAGCircuit) -> DAGCircuit: + """Run the pass on a circuit. Args: - dag (DAGCircuit): DAG to map. + dag: DAG to map. Returns: - DAGCircuit: A mapped DAG. + Mapped ``dag``. Raises: - TranspilerError: if the layout are not compatible with the DAG, or if the input gate set is incorrect. + TranspilerError: The layout is not compatible with the DAG, or if the input gate set is incorrect. """ circuit = dag_to_circuit(dag) - iqm_json = IQMClientCircuit( + iqm_circuit = IQMClientCircuit( name="Transpiling Circuit", instructions=tuple(serialize_instructions(circuit, self.target.iqm_idx_to_component)), ) try: - routed_json = transpile_insert_moves( - iqm_json, self.target.iqm_dynamic_architecture, self.existing_moves_handling + routed_iqm_circuit = transpile_insert_moves(iqm_circuit, self.target.iqm_dqa, existing_moves=self.existing_moves_handling) + routed_circuit = deserialize_instructions( + list(routed_iqm_circuit.instructions), self.target.iqm_component_to_idx ) - routed_circuit = deserialize_instructions(list(routed_json.instructions), self.target.iqm_component_to_idx) - except ValidationError as _: # The Circuit without move gates is empty. + except ValidationError: # The Circuit without move gates is empty. circ_args = [circuit.num_qubits, circuit.num_ancillas, circuit.num_clbits] routed_circuit = QuantumCircuit(*(arg for arg in circ_args if arg > 0)) new_dag = circuit_to_dag(routed_circuit) diff --git a/src/iqm/qiskit_iqm/move_gate.py b/src/iqm/qiskit_iqm/move_gate.py index 43f29113a..8badd05f5 100644 --- a/src/iqm/qiskit_iqm/move_gate.py +++ b/src/iqm/qiskit_iqm/move_gate.py @@ -17,8 +17,10 @@ import qiskit.quantum_info as qi MOVE_GATE_UNITARY = [[1.0, 0.0, 0.0, 0.0], [0.0, 0.0, 1.0, 0.0], [0.0, 1.0, 0.0, 0.0], [0.0, 0.0, 0.0, 1.0]] -"""Unitary for simulating the ideal MOVE gate. It is not fully realistic since it applies a zero phase on the -moved state and does nothing in the :math:`|11\rangle` subspace, thus being equal to the SWAP gate.""" +"""Unitary matrix for simulating the ideal MOVE gate. + +This matrix is not a realistic description of MOVE, since it applies a zero phase on the moved +state, and acts as identity in the :math:`|11\rangle` subspace, thus being equal to the SWAP gate.""" class MoveGate(Gate): diff --git a/src/iqm/qiskit_iqm/qiskit_to_iqm.py b/src/iqm/qiskit_iqm/qiskit_to_iqm.py index 773586993..ed83d4c50 100644 --- a/src/iqm/qiskit_iqm/qiskit_to_iqm.py +++ b/src/iqm/qiskit_iqm/qiskit_to_iqm.py @@ -224,14 +224,14 @@ def deserialize_instructions( """Helper function to turn a list of IQM Instructions into a Qiskit QuantumCircuit. Args: - instructions (list[Instruction]): The gates in the circuit. - qubit_name_to_index (dict[str, int]): Mapping from qubit names to their indices, as specified in a backend. + instructions: The gates in the circuit. + qubit_name_to_index: Mapping from qubit names to their indices, as specified in a backend. Raises: ValueError: Thrown when a given instruction is not supported. Returns: - QiskitQuantumCircuit: The circuit represented by the given instructions. + Qiskit circuit represented by the given instructions. """ cl_bits: dict[str, int] = {} cl_regs: dict[int, ClassicalRegister] = {} diff --git a/tests/fake_backends/test_iqm_fake_backend.py b/tests/fake_backends/test_iqm_fake_backend.py index ec97a8b05..20a5a56ef 100644 --- a/tests/fake_backends/test_iqm_fake_backend.py +++ b/tests/fake_backends/test_iqm_fake_backend.py @@ -236,9 +236,7 @@ def test_noise_model_contains_all_errors(backend): assert set(backend.noise_model._local_quantum_errors["cz"].keys()) == set([(0, 1), (1, 0), (1, 2), (2, 1)]) -def test_validate_compatible_architecture( - adonis_architecture, linear_3q_architecture -): +def test_validate_compatible_architecture(adonis_architecture, linear_3q_architecture): backend = IQMFakeAdonis() assert backend.validate_compatible_architecture(adonis_architecture) is True assert backend.validate_compatible_architecture(linear_3q_architecture) is False From 69d74a14526339564d395d0cf17f53dcffaa8c3c Mon Sep 17 00:00:00 2001 From: Ville Bergholm Date: Mon, 30 Dec 2024 16:36:11 +0200 Subject: [PATCH 35/47] Cleanup. --- src/iqm/qiskit_iqm/iqm_backend.py | 53 ++++++++++++++--------- src/iqm/qiskit_iqm/iqm_naive_move_pass.py | 16 ++++--- 2 files changed, 42 insertions(+), 27 deletions(-) diff --git a/src/iqm/qiskit_iqm/iqm_backend.py b/src/iqm/qiskit_iqm/iqm_backend.py index 120b9455e..d73e9094c 100644 --- a/src/iqm/qiskit_iqm/iqm_backend.py +++ b/src/iqm/qiskit_iqm/iqm_backend.py @@ -112,7 +112,7 @@ def physical_qubits(self) -> list[str]: return list(self._qb_to_idx) def has_resonators(self) -> bool: - """Return whether the backend has resonators.""" + """True iff the backend QPU has computational resonators.""" return bool(self.architecture.computational_resonators) def qubit_name_to_index(self, name: str) -> int: @@ -170,6 +170,7 @@ def __init__(self, architecture: DynamicQuantumArchitecture, component_to_idx: d self.iqm_idx_to_component = {v: k for k, v in component_to_idx.items()} self.real_target: IQMTarget = self._iqm_create_instructions(architecture, component_to_idx) + def _iqm_create_instructions( self, architecture: DynamicQuantumArchitecture, component_to_idx: dict[str, int] ) -> Target: @@ -186,10 +187,10 @@ def _iqm_create_instructions( # mapping from op name to all its allowed loci op_loci = {gate_name: gate_info.loci for gate_name, gate_info in architecture.gates.items()} - def idx_locus(locus: Locus) -> tuple[int, ...]: + def locus_to_idx(locus: Locus) -> tuple[int, ...]: """Map the given locus to use component indices instead of component names.""" return tuple(component_to_idx[component] for component in locus) - + def create_properties(name: str, *, symmetrize: bool = False) -> dict[tuple[int, ...], None]: """Creates the Qiskit instruction properties dictionary for the given IQM native operation. @@ -200,7 +201,7 @@ def create_properties(name: str, *, symmetrize: bool = False) -> dict[tuple[int, if symmetrize: # symmetrize the loci loci = tuple(permuted_locus for locus in loci for permuted_locus in itertools.permutations(locus)) - return {idx_locus(locus): None for locus in loci} + return {locus_to_idx(locus): None for locus in loci} if 'measure' in op_loci: self.add_instruction(Measure(), create_properties('measure')) @@ -208,7 +209,7 @@ def create_properties(name: str, *, symmetrize: bool = False) -> dict[tuple[int, # identity gate does nothing and is removed in serialization, so we may as well allow it everywhere self.add_instruction( IGate(), - {idx_locus((component,)): None for component in architecture.components}, + {locus_to_idx((component,)): None for component in architecture.components}, ) if 'prx' in op_loci: @@ -232,43 +233,55 @@ def create_properties(name: str, *, symmetrize: bool = False) -> dict[tuple[int, if 'cz' in op_loci: real_target.add_instruction(CZGate(), create_properties('cz')) + if 'move' in op_loci: + # CZ and MOVE: star fake_cz_connections: dict[tuple[int, int], None] = {} move_cz_connections: dict[tuple[int, int], None] = {} cz_loci = op_loci['cz'] - for qb1, qb2 in cz_loci: + for c1, c2 in cz_loci: + idx_locus = locus_to_idx((c1, c2)) if ( - qb1 not in architecture.computational_resonators - and qb2 not in architecture.computational_resonators + c1 not in architecture.computational_resonators + and c2 not in architecture.computational_resonators ): + # cz between two qubits TODO not possible in Star # every cz locus that only uses qubits goes to fake_cz_conn - fake_cz_connections[(component_to_idx[qb1], component_to_idx[qb2])] = None + fake_cz_connections[idx_locus] = None else: # otherwise it goes to move_cz_conn - move_cz_connections[(component_to_idx[qb1], component_to_idx[qb2])] = None - for qb1, res in op_loci['move']: - for qb2 in [q for q in architecture.qubits if q not in [qb1, res]]: - # loop over qb2 that is not qb1 - if (qb2, res) in cz_loci or (res, qb2) in cz_loci: - # This is a fake CZ and can be bidirectional. - # cz routable via res between qubits, put into fake_cz_conn both ways - fake_cz_connections[(component_to_idx[qb1], component_to_idx[qb2])] = None - fake_cz_connections[(component_to_idx[qb2], component_to_idx[qb1])] = None + move_cz_connections[idx_locus] = None + + for c1, res in op_loci['move']: + for c2 in architecture.qubits: + if c2 not in [c1, res]: + # loop over c2 that is not c1 + if (c2, res) in cz_loci or (res, c2) in cz_loci: + # This is a fake CZ and can be bidirectional. + # cz routable via res between qubits, put into fake_cz_conn both ways + idx_locus = locus_to_idx((c1, c2)) + fake_cz_connections[idx_locus] = None + fake_cz_connections[idx_locus[::-1]] = None self.add_instruction(CZGate(), fake_cz_connections) # self has fake cz conn fake_cz_connections.update(move_cz_connections) fake_target_with_moves.add_instruction(CZGate(), fake_cz_connections) else: + # CZ but no MOVE: crystal self.add_instruction(CZGate(), create_properties('cz')) fake_target_with_moves.add_instruction(CZGate(), create_properties('cz')) fake_target_with_moves.real_target = real_target self.fake_target_with_moves: IQMTarget = fake_target_with_moves return real_target + def restrict_to_qubits(self, qubits: Union[list[int], list[str]]) -> IQMTarget: - """Restrict the target to only the given qubits. + """Restrict the transpilation target to only the given qubits. Args: - qubits: The qubits to restrict the target to. Can be either a list of qubit indices or qubit names. + qubits: Qubits to restrict the target to. Can be either a list of qubit indices or qubit names. + + Returns: + restricted target """ qubits_str = [self.iqm_idx_to_component[q] if isinstance(q, int) else str(q) for q in qubits] new_gates = {} diff --git a/src/iqm/qiskit_iqm/iqm_naive_move_pass.py b/src/iqm/qiskit_iqm/iqm_naive_move_pass.py index 3b8d7ed87..7be9a08cd 100644 --- a/src/iqm/qiskit_iqm/iqm_naive_move_pass.py +++ b/src/iqm/qiskit_iqm/iqm_naive_move_pass.py @@ -34,7 +34,7 @@ class IQMNaiveResonatorMoving(TransformationPass): """WIP Naive transpilation pass for resonator moving A naive transpiler pass for use with the Qiskit PassManager. - Although it requires a CouplingMap, Target, or Backend, it does not take this into account when adding MoveGates. + Although it requires a CouplingMap, Target, or Backend, it does not take this into account when adding MOVE gates. It assumes target connectivity graph is star shaped with a single resonator in the middle. Which qubit is the resonator is represented with the resonator_register attribute. The pass assumes that all single qubit and two-qubit gates are allowed. @@ -76,7 +76,9 @@ def run(self, dag: DAGCircuit) -> DAGCircuit: instructions=tuple(serialize_instructions(circuit, self.target.iqm_idx_to_component)), ) try: - routed_iqm_circuit = transpile_insert_moves(iqm_circuit, self.target.iqm_dqa, existing_moves=self.existing_moves_handling) + routed_iqm_circuit = transpile_insert_moves( + iqm_circuit, self.target.iqm_dqa, existing_moves=self.existing_moves_handling + ) routed_circuit = deserialize_instructions( list(routed_iqm_circuit.instructions), self.target.iqm_component_to_idx ) @@ -98,7 +100,7 @@ def transpile_to_IQM( # pylint: disable=too-many-arguments remove_final_rzs: bool = True, existing_moves_handling: Optional[ExistingMoveHandlingOptions] = None, restrict_to_qubits: Optional[Union[list[int], list[str]]] = None, - **qiskit_transpiler_qwargs, + **qiskit_transpiler_kwargs, ) -> QuantumCircuit: """Customized transpilation to IQM backends. @@ -118,7 +120,7 @@ def transpile_to_IQM( # pylint: disable=too-many-arguments MOVE gates. restrict_to_qubits: Restrict the transpilation to only use these specific physical qubits. Note that you will have to pass this information to the ``backend.run`` method as well as a dictionary. - qiskit_transpiler_qwargs: Arguments to be passed to the Qiskit transpiler. + qiskit_transpiler_kwargs: Arguments to be passed to the Qiskit transpiler. Returns: Transpiled circuit ready for running on the backend. @@ -143,7 +145,7 @@ def transpile_to_IQM( # pylint: disable=too-many-arguments target = target.restrict_to_qubits(restrict_to_qubits) # Determine which scheduling method to use - scheduling_method = qiskit_transpiler_qwargs.pop("scheduling_method", None) + scheduling_method = qiskit_transpiler_kwargs.pop("scheduling_method", None) if scheduling_method is None: if perform_move_routing: if optimize_single_qubits: @@ -176,6 +178,6 @@ def transpile_to_IQM( # pylint: disable=too-many-arguments f"Scheduling method is set to {scheduling_method}, but it is normally used to pass other transpiler " + "options, ignoring the other arguments." ) - qiskit_transpiler_qwargs["scheduling_method"] = scheduling_method - new_circuit = transpile(circuit, target=target, initial_layout=initial_layout, **qiskit_transpiler_qwargs) + qiskit_transpiler_kwargs["scheduling_method"] = scheduling_method + new_circuit = transpile(circuit, target=target, initial_layout=initial_layout, **qiskit_transpiler_kwargs) return new_circuit From aa5658b71f9a1670e8c9182bbbe8534b37000814 Mon Sep 17 00:00:00 2001 From: Ville Bergholm Date: Mon, 30 Dec 2024 17:03:48 +0200 Subject: [PATCH 36/47] Fixes. --- src/iqm/qiskit_iqm/iqm_backend.py | 2 +- src/iqm/qiskit_iqm/iqm_naive_move_pass.py | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/iqm/qiskit_iqm/iqm_backend.py b/src/iqm/qiskit_iqm/iqm_backend.py index d73e9094c..db9558c8a 100644 --- a/src/iqm/qiskit_iqm/iqm_backend.py +++ b/src/iqm/qiskit_iqm/iqm_backend.py @@ -288,7 +288,7 @@ def restrict_to_qubits(self, qubits: Union[list[int], list[str]]) -> IQMTarget: for gate_name, gate_info in self.iqm_dqa.gates.items(): new_implementations = {} for implementation_name, implementation_info in gate_info.implementations.items(): - new_loci = [loci for loci in implementation_info.loci if all(q in qubits_str for q in loci)] + new_loci = [locus for locus in implementation_info.loci if all(q in qubits_str for q in locus)] if new_loci: new_implementations[implementation_name] = GateImplementationInfo(loci=new_loci) if new_implementations: diff --git a/src/iqm/qiskit_iqm/iqm_naive_move_pass.py b/src/iqm/qiskit_iqm/iqm_naive_move_pass.py index 7be9a08cd..41160749b 100644 --- a/src/iqm/qiskit_iqm/iqm_naive_move_pass.py +++ b/src/iqm/qiskit_iqm/iqm_naive_move_pass.py @@ -36,7 +36,6 @@ class IQMNaiveResonatorMoving(TransformationPass): A naive transpiler pass for use with the Qiskit PassManager. Although it requires a CouplingMap, Target, or Backend, it does not take this into account when adding MOVE gates. It assumes target connectivity graph is star shaped with a single resonator in the middle. - Which qubit is the resonator is represented with the resonator_register attribute. The pass assumes that all single qubit and two-qubit gates are allowed. The resonator is used to swap the qubit states for the two-qubit gates. Additionally, it assumes that no single qubit gates are allowed on the resonator. @@ -111,7 +110,7 @@ def transpile_to_IQM( # pylint: disable=too-many-arguments backend: The target backend to compile to. Does not require a resonator. target: An alternative target to compile to than the backend, using this option requires intimate knowledge of the transpiler and thus it is not recommended to use. - initial_layout: The initial layout to use for the transpilation, same as `qiskit.transpile`. + initial_layout: The initial layout to use for the transpilation, same as :func:`~qiskit.compiler.transpile`. optimize_single_qubits: Whether to optimize single qubit gates away. ignore_barriers: Whether to ignore barriers when optimizing single qubit gates away. remove_final_rzs: Whether to remove the final z rotations. It is recommended always to set this to true as From cc43862c21e2b22b3dd2544086243725e3e309c4 Mon Sep 17 00:00:00 2001 From: Ville Bergholm Date: Wed, 8 Jan 2025 16:30:02 +0200 Subject: [PATCH 37/47] Cleanup, FIXMEs. --- docs/user_guide.rst | 6 ++++ pyproject.toml | 2 +- src/iqm/qiskit_iqm/__init__.py | 2 +- src/iqm/qiskit_iqm/iqm_backend.py | 11 +++---- src/iqm/qiskit_iqm/iqm_move_layout.py | 39 +++++++++++------------ src/iqm/qiskit_iqm/iqm_naive_move_pass.py | 1 + 6 files changed, 33 insertions(+), 28 deletions(-) diff --git a/docs/user_guide.rst b/docs/user_guide.rst index 15f0aed7a..a4bff5cd6 100644 --- a/docs/user_guide.rst +++ b/docs/user_guide.rst @@ -445,6 +445,12 @@ Computational resonators The IQM Star architecture includes computational resonators as additional QPU components. Because the resonator is not a real qubit, the standard Qiskit transpiler does not know how to compile for it. + +FIXME better explanation needed + +One needs to use :class:`.MoveGate` instructions to move qubit states to and from the resonators. +The standard Qiskit transpiler does not know to use the MOVE gate. + Thus, we have a custom scheduling plugin that adds the necessary :class:`.MoveGate` instructions where necessary. This plugin is executed automatically when you use the Qiskit transpiler. diff --git a/pyproject.toml b/pyproject.toml index 7e8b22de1..b65d0072c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,7 +22,7 @@ dependencies = [ "numpy", "qiskit >= 0.46, < 1.3", "qiskit-aer >= 0.13.1, < 0.16", - "iqm-client >= 21.0, < 22.0" + "iqm-client >= 20.0, < 22.0" ] [project.urls] diff --git a/src/iqm/qiskit_iqm/__init__.py b/src/iqm/qiskit_iqm/__init__.py index 97f925d42..7c15833e5 100644 --- a/src/iqm/qiskit_iqm/__init__.py +++ b/src/iqm/qiskit_iqm/__init__.py @@ -21,7 +21,7 @@ from iqm.qiskit_iqm.fake_backends.iqm_fake_backend import IQMFakeBackend from iqm.qiskit_iqm.iqm_circuit import IQMCircuit from iqm.qiskit_iqm.iqm_job import IQMJob -from iqm.qiskit_iqm.iqm_move_layout import IQMMoveLayout, generate_initial_layout +from iqm.qiskit_iqm.iqm_move_layout import generate_initial_layout from iqm.qiskit_iqm.iqm_naive_move_pass import IQMNaiveResonatorMoving, transpile_to_IQM from iqm.qiskit_iqm.iqm_provider import IQMBackend, IQMProvider, __version__ from iqm.qiskit_iqm.iqm_transpilation import IQMOptimizeSingleQubitGates, optimize_single_qubit_gates diff --git a/src/iqm/qiskit_iqm/iqm_backend.py b/src/iqm/qiskit_iqm/iqm_backend.py index db9558c8a..fc1984774 100644 --- a/src/iqm/qiskit_iqm/iqm_backend.py +++ b/src/iqm/qiskit_iqm/iqm_backend.py @@ -37,6 +37,7 @@ IQM_TO_QISKIT_GATE_NAME: Final[dict[str, str]] = {'prx': 'r', 'cz': 'cz'} Locus = tuple[str, ...] +LocusIdx = tuple[int, ...] def _dqa_from_static_architecture(sqa: QuantumArchitectureSpecification) -> DynamicQuantumArchitecture: @@ -170,7 +171,6 @@ def __init__(self, architecture: DynamicQuantumArchitecture, component_to_idx: d self.iqm_idx_to_component = {v: k for k, v in component_to_idx.items()} self.real_target: IQMTarget = self._iqm_create_instructions(architecture, component_to_idx) - def _iqm_create_instructions( self, architecture: DynamicQuantumArchitecture, component_to_idx: dict[str, int] ) -> Target: @@ -183,11 +183,11 @@ def _iqm_create_instructions( Returns: A Qiskit Target object representing the given quantum architecture specification. """ - # pylint: disable=too-many-branches + # pylint: disable=too-many-branches,too-many-nested-blocks # mapping from op name to all its allowed loci op_loci = {gate_name: gate_info.loci for gate_name, gate_info in architecture.gates.items()} - def locus_to_idx(locus: Locus) -> tuple[int, ...]: + def locus_to_idx(locus: Locus) -> LocusIdx: """Map the given locus to use component indices instead of component names.""" return tuple(component_to_idx[component] for component in locus) @@ -236,8 +236,8 @@ def create_properties(name: str, *, symmetrize: bool = False) -> dict[tuple[int, if 'move' in op_loci: # CZ and MOVE: star - fake_cz_connections: dict[tuple[int, int], None] = {} - move_cz_connections: dict[tuple[int, int], None] = {} + fake_cz_connections: dict[LocusIdx, None] = {} + move_cz_connections: dict[LocusIdx, None] = {} cz_loci = op_loci['cz'] for c1, c2 in cz_loci: idx_locus = locus_to_idx((c1, c2)) @@ -273,7 +273,6 @@ def create_properties(name: str, *, symmetrize: bool = False) -> dict[tuple[int, self.fake_target_with_moves: IQMTarget = fake_target_with_moves return real_target - def restrict_to_qubits(self, qubits: Union[list[int], list[str]]) -> IQMTarget: """Restrict the transpilation target to only the given qubits. diff --git a/src/iqm/qiskit_iqm/iqm_move_layout.py b/src/iqm/qiskit_iqm/iqm_move_layout.py index 6741a7bcf..897cefb52 100644 --- a/src/iqm/qiskit_iqm/iqm_move_layout.py +++ b/src/iqm/qiskit_iqm/iqm_move_layout.py @@ -11,9 +11,9 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -"""A layout algorithm that generates an initial layout for a quantum circuit that is -valid on the quantum architecture specification of the given IQM backend.""" -from typing import List, Optional, Union +"""Generate an initial layout for a quantum circuit that is +valid on the quantum architecture specification of the given backend.""" +from typing import Optional, Union from qiskit import QuantumCircuit from qiskit.dagcircuit import DAGCircuit @@ -26,17 +26,17 @@ class IQMMoveLayout(TrivialLayout): - r"""Creates a qubit layout that is valid on the quantum architecture specification of the - given IQM target with regard to the move gate. In more detail, assumes that the move + """Creates a qubit layout that is valid on the quantum architecture specification of the + given IQM target with regard to the MOVE gate. In more detail, assumes that the MOVE operations in the quantum architecture define which physical qubit is the resonator and - which is a move qubit, and shuffles the logical indices of the circuit so that they match + which is a MOVE qubit, and shuffles the logical indices of the circuit so that they match the requirements. This is required because Qiskit's basic layout algorithm assumes each connection between two qubits has the same gates defined. - Note: This simple version of the mapper only works reliably with a single move qubit - and resonator, and only if the circuit contains at least one move gate.""" + Note: This simple version of the mapper only works reliably with a single MOVE qubit + and resonator, and only if the circuit contains at least one MOVE gate.""" def run(self, dag: DAGCircuit): """Creates the qubit layout for the given quantum circuit. @@ -73,11 +73,11 @@ def run(self, dag: DAGCircuit): self.property_set['layout'] = Layout(new_dict) - def get_initial_layout(self): + def get_initial_layout(self) -> Layout: """Returns the initial layout generated by the algorithm. Returns: - the initial layout + The initial layout. """ return self.property_set['layout'] @@ -86,11 +86,10 @@ def _determine_required_changes(self, dag: DAGCircuit) -> dict[int, int]: need to be switched so that the operations are valid for the specified quantum architecture. Args: - dag - the circuit to check + dag: circuit to check Returns: - the list of required changes as tuples of logical indices that should be switched; - empty list if no changes are required. + Required changes as a mapping of logical indices that should be switched. """ reqs = self._calculate_requirements(dag) types = self._get_qubit_types() @@ -109,7 +108,7 @@ def _get_qubit_types(self) -> dict[int, str]: """Determines the types of qubits in the quantum architecture. Returns: - a dictionary mapping logical indices to qubit types for those + Mapping of logical qubit indices to qubit types for those qubits where the type is relevant. """ target: IQMTarget = self.target @@ -133,10 +132,10 @@ def _calculate_requirements(dag: DAGCircuit) -> dict[int, str]: """Calculates the requirements for each logical qubit in the circuit. Args: - dag - the circuit to check + dag: circuit to check Returns: - A mapping of the logical qubit indices to the required type for that qubit. + Mapping of the logical qubit indices to the required type for that qubit. """ required_types: dict[int, str] = {} @@ -168,16 +167,16 @@ def _require_type(qubit_index: int, required_type: str, instruction_name: str): def generate_initial_layout( backend: IQMBackend, circuit: QuantumCircuit, - restrict_to_qubits: Optional[Union[List[int], List[str]]] = None, + restrict_to_qubits: Optional[Union[list[int], list[str]]] = None, ) -> Layout: """Generates the initial layout for the given circuit, when run against the given backend. Args: - backend - the IQM backend to run against - circuit - the circuit for which a layout is to be generated + backend: IQM backend to run against + circuit: circuit for which a layout is to be generated Returns: - a layout that remaps the qubits so that the move qubit and the resonator are using the correct + Layout that remaps the qubits so that the MOVE qubit and the resonator are using the correct indices. """ if restrict_to_qubits is not None: diff --git a/src/iqm/qiskit_iqm/iqm_naive_move_pass.py b/src/iqm/qiskit_iqm/iqm_naive_move_pass.py index 41160749b..71a89fcc4 100644 --- a/src/iqm/qiskit_iqm/iqm_naive_move_pass.py +++ b/src/iqm/qiskit_iqm/iqm_naive_move_pass.py @@ -82,6 +82,7 @@ def run(self, dag: DAGCircuit) -> DAGCircuit: list(routed_iqm_circuit.instructions), self.target.iqm_component_to_idx ) except ValidationError: # The Circuit without move gates is empty. + # FIXME seems unsafe, assuming this given a generic Pydantic exception circ_args = [circuit.num_qubits, circuit.num_ancillas, circuit.num_clbits] routed_circuit = QuantumCircuit(*(arg for arg in circ_args if arg > 0)) new_dag = circuit_to_dag(routed_circuit) From 15b0b52690fe170ab423558b89ebb7da0552d857 Mon Sep 17 00:00:00 2001 From: Arianne Meijer Date: Thu, 9 Jan 2025 14:18:19 +0200 Subject: [PATCH 38/47] Refactor IQMTarget --- src/iqm/qiskit_iqm/iqm_backend.py | 178 ++++++++++++++-------- src/iqm/qiskit_iqm/iqm_naive_move_pass.py | 37 ++--- src/iqm/qiskit_iqm/qiskit_to_iqm.py | 5 +- src/iqm/qiskit_iqm/transpiler_plugins.py | 11 +- 4 files changed, 139 insertions(+), 92 deletions(-) diff --git a/src/iqm/qiskit_iqm/iqm_backend.py b/src/iqm/qiskit_iqm/iqm_backend.py index fc1984774..672882ac0 100644 --- a/src/iqm/qiskit_iqm/iqm_backend.py +++ b/src/iqm/qiskit_iqm/iqm_backend.py @@ -16,7 +16,6 @@ from __future__ import annotations from abc import ABC -from copy import deepcopy import itertools from typing import Final, Union from uuid import UUID @@ -97,7 +96,8 @@ def __init__( # qubits, or else transpiling with optimization_level=0 will fail because of lacking resonator indices. qb_to_idx = {qb: idx for idx, qb in enumerate(arch.qubits + arch.computational_resonators)} - self._target = IQMTarget(arch, qb_to_idx) + self._target = IQMTarget(arch, qb_to_idx, include_moves=False) + self._fake_target_with_moves = IQMTarget(arch, qb_to_idx, include_moves=True) self._qb_to_idx = qb_to_idx self._idx_to_qb = {v: k for k, v in qb_to_idx.items()} self.name = 'IQMBackend' @@ -107,6 +107,11 @@ def __init__( def target(self) -> Target: return self._target + @property + def target_with_resonators(self) -> Target: + """Return the target with MOVE gates and resonators included.""" + return self._fake_target_with_moves + @property def physical_qubits(self) -> list[str]: """Return the list of physical qubits in the backend.""" @@ -116,6 +121,12 @@ def has_resonators(self) -> bool: """True iff the backend QPU has computational resonators.""" return bool(self.architecture.computational_resonators) + def get_real_target(self) -> Target: + """Return the real physical target of the backend without virtual CZ gates.""" + return IQMTarget( + architecture=self.architecture, component_to_idx=self._qb_to_idx, include_moves=True, include_fake_czs=False + ) + def qubit_name_to_index(self, name: str) -> int: """Given an IQM-style qubit name, return the corresponding index in the register. @@ -152,28 +163,88 @@ def get_scheduling_stage_plugin(self) -> str: """Return the plugin that should be used for scheduling the circuits on this backend.""" return 'iqm_default_scheduling' + def restrict_to_qubits( + self, qubits: Union[list[int], list[str]], include_resonators: bool = False, include_fake_czs: bool = True + ) -> IQMTarget: + """Generated a restricted transpilation target from this backend that only contains the given qubits. -class IQMTarget(Target): - """Transpiler target representing an IQM backend that can have computational resonators. + Args: + qubits: Qubits to restrict the target to. Can be either a list of qubit indices or qubit names. + include_resonators: Whether to restrict `self.target` or `self.target_with_resonators`. + include_fake_czs: Whether to include virtual CZs that are unsupported, but could be routed via MOVE. + + Returns: + restricted target + """ + qubits_str = [self._idx_to_qb[q] if isinstance(q, int) else str(q) for q in qubits] + return _restrict_dqa_to_qubits(self.architecture, qubits_str, include_resonators, include_fake_czs) + + +def _restrict_dqa_to_qubits( + architecture: DynamicQuantumArchitecture, qubits: list[str], include_moves: bool, include_fake_czs: bool = True +) -> IQMTarget: + """Generated a restricted transpilation target from this backend that only contains the given qubits. + + Args: + architecture: The dynamic quantum architecture to restrict. + qubits: Qubits to restrict the target to. Can be either a list of qubit indices or qubit names. + include_moves: Whether to include MOVE gates in the target. + include_fake_czs: Whether to include virtual CZs that are not natively supported, but could be routed via MOVE. + + Returns: + restricted target + """ + new_gates = {} + for gate_name, gate_info in architecture.gates.items(): + new_implementations = {} + for implementation_name, implementation_info in gate_info.implementations.items(): + new_loci = [locus for locus in implementation_info.loci if all(q in qubits for q in locus)] + if new_loci: + new_implementations[implementation_name] = GateImplementationInfo(loci=new_loci) + if new_implementations: + new_gates[gate_name] = GateInfo( + implementations=new_implementations, + default_implementation=gate_info.default_implementation, + override_default_implementation=gate_info.override_default_implementation, + ) + new_arch = DynamicQuantumArchitecture( + calibration_set_id=architecture.calibration_set_id, + qubits=[q for q in qubits if q in architecture.qubits], + computational_resonators=[q for q in qubits if q in architecture.computational_resonators], + gates=new_gates, + ) + return IQMTarget(new_arch, {name: idx for idx, name in enumerate(qubits)}, include_moves, include_fake_czs) - This target represents the physical layout of the backend including the resonators, as - well as a fake coupling map without them to present to the Qiskit transpiler. + +class IQMTarget(Target): + """ + Represents the IQM target for transpilation containing the mapping of physical qubit name on the device to qubit + index in the Target as well as the DQA architecture. Args: - architecture: Represents the gates (and their loci) available for the transpilation. + architecture: The quantum architecture specification to convert. component_to_idx: Mapping from QPU component names to integer indices used by Qiskit to refer to them. + include_moves: Whether to include MOVE gates in the target. + include_fake_czs: Whether to include virtual CZs that are not natively supported, but could be routed via MOVE. """ - def __init__(self, architecture: DynamicQuantumArchitecture, component_to_idx: dict[str, int]): + def __init__( + self, + architecture: DynamicQuantumArchitecture, + component_to_idx: dict[str, int], + include_moves: bool, + include_fake_czs: bool = True, + ): super().__init__() + # Using iqm as a prefix to avoid name clashes with other Qiskit targets. self.iqm_dqa = architecture self.iqm_component_to_idx = component_to_idx self.iqm_idx_to_component = {v: k for k, v in component_to_idx.items()} - self.real_target: IQMTarget = self._iqm_create_instructions(architecture, component_to_idx) + self.iqm_includes_moves = include_moves + self.iqm_includes_fake_czs = include_fake_czs + self._add_connections_from_DQA() - def _iqm_create_instructions( - self, architecture: DynamicQuantumArchitecture, component_to_idx: dict[str, int] - ) -> Target: + def _add_connections_from_DQA(self): """Converts a QuantumArchitectureSpecification object to a Qiskit Target object. Args: @@ -185,6 +256,8 @@ def _iqm_create_instructions( """ # pylint: disable=too-many-branches,too-many-nested-blocks # mapping from op name to all its allowed loci + architecture = self.iqm_dqa + component_to_idx = self.iqm_component_to_idx op_loci = {gate_name: gate_info.loci for gate_name, gate_info in architecture.gates.items()} def locus_to_idx(locus: Locus) -> LocusIdx: @@ -197,7 +270,13 @@ def create_properties(name: str, *, symmetrize: bool = False) -> dict[tuple[int, Currently we do not provide any actual properties for the operation, hence the all the allowed loci map to None. """ - loci = op_loci[name] + if self.iqm_includes_moves: + loci = op_loci[name] + else: + # Remove the loci that correspond to resonators. + loci = [ + locus for locus in op_loci[name] if all(component in self.iqm_dqa.qubits for component in locus) + ] if symmetrize: # symmetrize the loci loci = tuple(permuted_locus for locus in loci for permuted_locus in itertools.permutations(locus)) @@ -222,35 +301,18 @@ def create_properties(name: str, *, symmetrize: bool = False) -> dict[tuple[int, if 'cc_prx' in op_loci: self.add_instruction(Reset(), create_properties('cc_prx')) - # Special work for devices with a MoveGate. - real_target: IQMTarget = deepcopy(self) - - if 'move' in op_loci: - real_target.add_instruction(MoveGate(), create_properties('move')) - - fake_target_with_moves = deepcopy(real_target) - # self has just single-q stuff, fake and real also have MOVE + if self.iqm_includes_moves and 'move' in op_loci: + self.add_instruction(MoveGate(), create_properties('move')) if 'cz' in op_loci: - real_target.add_instruction(CZGate(), create_properties('cz')) - - if 'move' in op_loci: + if self.iqm_includes_fake_czs and 'move' in op_loci: # CZ and MOVE: star - fake_cz_connections: dict[LocusIdx, None] = {} - move_cz_connections: dict[LocusIdx, None] = {} + cz_connections: dict[LocusIdx, None] = {} cz_loci = op_loci['cz'] for c1, c2 in cz_loci: - idx_locus = locus_to_idx((c1, c2)) - if ( - c1 not in architecture.computational_resonators - and c2 not in architecture.computational_resonators - ): - # cz between two qubits TODO not possible in Star - # every cz locus that only uses qubits goes to fake_cz_conn - fake_cz_connections[idx_locus] = None - else: - # otherwise it goes to move_cz_conn - move_cz_connections[idx_locus] = None + if self.iqm_includes_moves or all(component in self.iqm_dqa.qubits for component in (c1, c2)): + idx_locus = locus_to_idx((c1, c2)) + cz_connections[idx_locus] = None for c1, res in op_loci['move']: for c2 in architecture.qubits: @@ -260,46 +322,28 @@ def create_properties(name: str, *, symmetrize: bool = False) -> dict[tuple[int, # This is a fake CZ and can be bidirectional. # cz routable via res between qubits, put into fake_cz_conn both ways idx_locus = locus_to_idx((c1, c2)) - fake_cz_connections[idx_locus] = None - fake_cz_connections[idx_locus[::-1]] = None - self.add_instruction(CZGate(), fake_cz_connections) # self has fake cz conn - fake_cz_connections.update(move_cz_connections) - fake_target_with_moves.add_instruction(CZGate(), fake_cz_connections) + cz_connections[idx_locus] = None + cz_connections[idx_locus[::-1]] = None + self.add_instruction(CZGate(), cz_connections) else: # CZ but no MOVE: crystal self.add_instruction(CZGate(), create_properties('cz')) - fake_target_with_moves.add_instruction(CZGate(), create_properties('cz')) - fake_target_with_moves.real_target = real_target - self.fake_target_with_moves: IQMTarget = fake_target_with_moves - return real_target + + @property + def physical_qubits(self) -> list[str]: + """Return the ordered list of physical qubits in the backend.""" + # Overwriting the property from the super class to contain the correct information. + return [self.iqm_idx_to_component[i] for i in range(self.num_qubits)] def restrict_to_qubits(self, qubits: Union[list[int], list[str]]) -> IQMTarget: - """Restrict the transpilation target to only the given qubits. + """Generated a restricted transpilation target from this Target that only contains the given qubits. Args: qubits: Qubits to restrict the target to. Can be either a list of qubit indices or qubit names. + include_fake_czs: Whether to include virtual CZs that are unsupported, but could be routed via MOVE. Returns: restricted target """ qubits_str = [self.iqm_idx_to_component[q] if isinstance(q, int) else str(q) for q in qubits] - new_gates = {} - for gate_name, gate_info in self.iqm_dqa.gates.items(): - new_implementations = {} - for implementation_name, implementation_info in gate_info.implementations.items(): - new_loci = [locus for locus in implementation_info.loci if all(q in qubits_str for q in locus)] - if new_loci: - new_implementations[implementation_name] = GateImplementationInfo(loci=new_loci) - if new_implementations: - new_gates[gate_name] = GateInfo( - implementations=new_implementations, - default_implementation=gate_info.default_implementation, - override_default_implementation=gate_info.override_default_implementation, - ) - new_arch = DynamicQuantumArchitecture( - calibration_set_id=self.iqm_dqa.calibration_set_id, - qubits=[q for q in qubits_str if q in self.iqm_dqa.qubits], - computational_resonators=[q for q in qubits_str if q in self.iqm_dqa.computational_resonators], - gates=new_gates, - ) - return IQMTarget(new_arch, {name: idx for idx, name in enumerate(qubits_str)}) + return _restrict_dqa_to_qubits(self.iqm_dqa, qubits_str, self.iqm_includes_moves, self.iqm_includes_fake_czs) diff --git a/src/iqm/qiskit_iqm/iqm_naive_move_pass.py b/src/iqm/qiskit_iqm/iqm_naive_move_pass.py index 71a89fcc4..ebfd21073 100644 --- a/src/iqm/qiskit_iqm/iqm_naive_move_pass.py +++ b/src/iqm/qiskit_iqm/iqm_naive_move_pass.py @@ -42,19 +42,18 @@ class IQMNaiveResonatorMoving(TransformationPass): Args: target: Transpilation target. - gate_set: Basis gates of the target backend. - existing_moves_handling: ffff + existing_moves_handling: How to handle existing MOVE gates in the circuit. """ def __init__( self, target: IQMTarget, - gate_set: list[str], - existing_moves_handling: Optional[ExistingMoveHandlingOptions] = None, + existing_moves_handling: ExistingMoveHandlingOptions = ExistingMoveHandlingOptions.KEEP, ): super().__init__() - self.target = target - self.gate_set = gate_set + self.architecture = target.iqm_dqa + self.idx_to_component = target.iqm_idx_to_component + self.component_to_idx = target.iqm_component_to_idx self.existing_moves_handling = existing_moves_handling def run(self, dag: DAGCircuit) -> DAGCircuit: @@ -69,22 +68,24 @@ def run(self, dag: DAGCircuit) -> DAGCircuit: Raises: TranspilerError: The layout is not compatible with the DAG, or if the input gate set is incorrect. """ + print("Transpiling circuit") circuit = dag_to_circuit(dag) iqm_circuit = IQMClientCircuit( name="Transpiling Circuit", - instructions=tuple(serialize_instructions(circuit, self.target.iqm_idx_to_component)), + instructions=tuple(serialize_instructions(circuit, self.idx_to_component)), ) try: routed_iqm_circuit = transpile_insert_moves( - iqm_circuit, self.target.iqm_dqa, existing_moves=self.existing_moves_handling + iqm_circuit, self.architecture, existing_moves=self.existing_moves_handling ) - routed_circuit = deserialize_instructions( - list(routed_iqm_circuit.instructions), self.target.iqm_component_to_idx - ) - except ValidationError: # The Circuit without move gates is empty. + routed_circuit = deserialize_instructions(list(routed_iqm_circuit.instructions), self.component_to_idx) + except ValidationError as e: # The Circuit without move gates is empty. # FIXME seems unsafe, assuming this given a generic Pydantic exception - circ_args = [circuit.num_qubits, circuit.num_ancillas, circuit.num_clbits] - routed_circuit = QuantumCircuit(*(arg for arg in circ_args if arg > 0)) + if e.title == "Empty Circuit": + circ_args = [circuit.num_qubits, circuit.num_ancillas, circuit.num_clbits] + routed_circuit = QuantumCircuit(*(arg for arg in circ_args if arg > 0)) + else: + raise e new_dag = circuit_to_dag(routed_circuit) return new_dag @@ -112,6 +113,7 @@ def transpile_to_IQM( # pylint: disable=too-many-arguments target: An alternative target to compile to than the backend, using this option requires intimate knowledge of the transpiler and thus it is not recommended to use. initial_layout: The initial layout to use for the transpilation, same as :func:`~qiskit.compiler.transpile`. + perform_move_routing: Whether to perform MOVE gate routing. optimize_single_qubits: Whether to optimize single qubit gates away. ignore_barriers: Whether to ignore barriers when optimizing single qubit gates away. remove_final_rzs: Whether to remove the final z rotations. It is recommended always to set this to true as @@ -132,7 +134,7 @@ def transpile_to_IQM( # pylint: disable=too-many-arguments if target is None: if circuit.count_ops().get("move", 0) > 0: - target = backend.target.fake_target_with_moves + target = backend.target_with_resonators # Create a sensible initial layout if none is provided if initial_layout is None: initial_layout = generate_initial_layout(backend, circuit, restrict_to_qubits) @@ -162,7 +164,7 @@ def transpile_to_IQM( # pylint: disable=too-many-arguments raise ValueError( "Existing Move handling options are not compatible with `remove_final_rzs` and \ `ignore_barriers` options." - ) + ) # No technical reason for this, just hard to maintain all combinations. scheduling_method += "_" + existing_moves_handling.value else: if optimize_single_qubits: @@ -176,7 +178,8 @@ def transpile_to_IQM( # pylint: disable=too-many-arguments else: warnings.warn( f"Scheduling method is set to {scheduling_method}, but it is normally used to pass other transpiler " - + "options, ignoring the other arguments." + + "options, ignoring the `perform_move_routing`, `optimize_single_qubits`, `remove_final_rzs`, " + + "`ignore_barriers`, and `existing_moves_handling` arguments." ) qiskit_transpiler_kwargs["scheduling_method"] = scheduling_method new_circuit = transpile(circuit, target=target, initial_layout=initial_layout, **qiskit_transpiler_kwargs) diff --git a/src/iqm/qiskit_iqm/qiskit_to_iqm.py b/src/iqm/qiskit_iqm/qiskit_to_iqm.py index ed83d4c50..806bb06e3 100644 --- a/src/iqm/qiskit_iqm/qiskit_to_iqm.py +++ b/src/iqm/qiskit_iqm/qiskit_to_iqm.py @@ -241,7 +241,10 @@ def deserialize_instructions( cl_regs[mk.creg_idx] = cl_regs.get(mk.creg_idx, ClassicalRegister(size=mk.creg_len, name=mk.creg_name)) cl_bits[str(mk)] = cl_regs[mk.creg_idx][mk.clbit_idx] qreg = QuantumRegister(max(qubit_name_to_index.values()) + 1, 'q') - circuit = QiskitQuantumCircuit(qreg, *(cl_regs[i] for i in range(len(cl_regs)))) + # Add an empty Classical register when the original circuit had unused classical registers + circuit = QiskitQuantumCircuit( + qreg, *(cl_regs.get(i, ClassicalRegister(0)) for i in range(max(cl_regs) + 1 if cl_regs else 0)) + ) for instr in instructions: loci = [qubit_name_to_index[q] for q in instr.qubits] if instr.name == 'prx': diff --git a/src/iqm/qiskit_iqm/transpiler_plugins.py b/src/iqm/qiskit_iqm/transpiler_plugins.py index ef686e93a..2f45895be 100644 --- a/src/iqm/qiskit_iqm/transpiler_plugins.py +++ b/src/iqm/qiskit_iqm/transpiler_plugins.py @@ -51,7 +51,9 @@ def __init__( self.optimize_sqg = optimize_sqg self.drop_final_rz = drop_final_rz self.ignore_barriers = ignore_barriers - self.existing_move_handling = existing_move_handling + self.existing_move_handling = ( + existing_move_handling if existing_move_handling is not None else ExistingMoveHandlingOptions.KEEP + ) def pass_manager( self, pass_manager_config: PassManagerConfig, optimization_level: Optional[int] = None @@ -65,15 +67,10 @@ def pass_manager( ) if pass_manager_config.target is None: raise ValueError("PassManagerConfig must have a target backend set, unable to schedule MoveGate routing.") - if ( - self.move_gate_routing - and isinstance(pass_manager_config.target, IQMTarget) - and "move" in pass_manager_config.target.real_target.operation_names - ): + if self.move_gate_routing and isinstance(pass_manager_config.target, IQMTarget): scheduling.append( IQMNaiveResonatorMoving( target=pass_manager_config.target, - gate_set=pass_manager_config.basis_gates, existing_moves_handling=self.existing_move_handling, ) ) From 786f658e89e5d4704e6888d350ed6a22d70483b7 Mon Sep 17 00:00:00 2001 From: Arianne Meijer Date: Thu, 9 Jan 2025 14:35:03 +0200 Subject: [PATCH 39/47] Fix broken fake backends --- src/iqm/qiskit_iqm/iqm_backend.py | 37 +++++++++++++++---------- tests/conftest.py | 10 ------- tests/fake_backends/test_fake_adonis.py | 9 ++++-- tests/fake_backends/test_fake_deneb.py | 10 +++++-- 4 files changed, 36 insertions(+), 30 deletions(-) diff --git a/src/iqm/qiskit_iqm/iqm_backend.py b/src/iqm/qiskit_iqm/iqm_backend.py index 672882ac0..f5ebac638 100644 --- a/src/iqm/qiskit_iqm/iqm_backend.py +++ b/src/iqm/qiskit_iqm/iqm_backend.py @@ -17,7 +17,7 @@ from abc import ABC import itertools -from typing import Final, Union +from typing import Final, Optional, Union from uuid import UUID from qiskit.circuit import Parameter, Reset @@ -96,8 +96,10 @@ def __init__( # qubits, or else transpiling with optimization_level=0 will fail because of lacking resonator indices. qb_to_idx = {qb: idx for idx, qb in enumerate(arch.qubits + arch.computational_resonators)} - self._target = IQMTarget(arch, qb_to_idx, include_moves=False) - self._fake_target_with_moves = IQMTarget(arch, qb_to_idx, include_moves=True) + self._target = IQMTarget(arch, qb_to_idx, include_resonators=False) + self._fake_target_with_moves = ( + IQMTarget(arch, qb_to_idx, include_resonators=True) if 'move' in arch.gates else None + ) self._qb_to_idx = qb_to_idx self._idx_to_qb = {v: k for k, v in qb_to_idx.items()} self.name = 'IQMBackend' @@ -108,7 +110,7 @@ def target(self) -> Target: return self._target @property - def target_with_resonators(self) -> Target: + def target_with_resonators(self) -> Optional[Target]: """Return the target with MOVE gates and resonators included.""" return self._fake_target_with_moves @@ -124,7 +126,10 @@ def has_resonators(self) -> bool: def get_real_target(self) -> Target: """Return the real physical target of the backend without virtual CZ gates.""" return IQMTarget( - architecture=self.architecture, component_to_idx=self._qb_to_idx, include_moves=True, include_fake_czs=False + architecture=self.architecture, + component_to_idx=self._qb_to_idx, + include_resonators=True, + include_fake_czs=False, ) def qubit_name_to_index(self, name: str) -> int: @@ -181,14 +186,14 @@ def restrict_to_qubits( def _restrict_dqa_to_qubits( - architecture: DynamicQuantumArchitecture, qubits: list[str], include_moves: bool, include_fake_czs: bool = True + architecture: DynamicQuantumArchitecture, qubits: list[str], include_resonators: bool, include_fake_czs: bool = True ) -> IQMTarget: """Generated a restricted transpilation target from this backend that only contains the given qubits. Args: architecture: The dynamic quantum architecture to restrict. qubits: Qubits to restrict the target to. Can be either a list of qubit indices or qubit names. - include_moves: Whether to include MOVE gates in the target. + include_resonators: Whether to include MOVE gates in the target. include_fake_czs: Whether to include virtual CZs that are not natively supported, but could be routed via MOVE. Returns: @@ -213,7 +218,7 @@ def _restrict_dqa_to_qubits( computational_resonators=[q for q in qubits if q in architecture.computational_resonators], gates=new_gates, ) - return IQMTarget(new_arch, {name: idx for idx, name in enumerate(qubits)}, include_moves, include_fake_czs) + return IQMTarget(new_arch, {name: idx for idx, name in enumerate(qubits)}, include_resonators, include_fake_czs) class IQMTarget(Target): @@ -224,7 +229,7 @@ class IQMTarget(Target): Args: architecture: The quantum architecture specification to convert. component_to_idx: Mapping from QPU component names to integer indices used by Qiskit to refer to them. - include_moves: Whether to include MOVE gates in the target. + include_resonators: Whether to include MOVE gates in the target. include_fake_czs: Whether to include virtual CZs that are not natively supported, but could be routed via MOVE. """ @@ -232,7 +237,7 @@ def __init__( self, architecture: DynamicQuantumArchitecture, component_to_idx: dict[str, int], - include_moves: bool, + include_resonators: bool, include_fake_czs: bool = True, ): super().__init__() @@ -240,7 +245,7 @@ def __init__( self.iqm_dqa = architecture self.iqm_component_to_idx = component_to_idx self.iqm_idx_to_component = {v: k for k, v in component_to_idx.items()} - self.iqm_includes_moves = include_moves + self.iqm_includes_resonators = include_resonators self.iqm_includes_fake_czs = include_fake_czs self._add_connections_from_DQA() @@ -270,7 +275,7 @@ def create_properties(name: str, *, symmetrize: bool = False) -> dict[tuple[int, Currently we do not provide any actual properties for the operation, hence the all the allowed loci map to None. """ - if self.iqm_includes_moves: + if self.iqm_includes_resonators: loci = op_loci[name] else: # Remove the loci that correspond to resonators. @@ -301,7 +306,7 @@ def create_properties(name: str, *, symmetrize: bool = False) -> dict[tuple[int, if 'cc_prx' in op_loci: self.add_instruction(Reset(), create_properties('cc_prx')) - if self.iqm_includes_moves and 'move' in op_loci: + if self.iqm_includes_resonators and 'move' in op_loci: self.add_instruction(MoveGate(), create_properties('move')) if 'cz' in op_loci: @@ -310,7 +315,7 @@ def create_properties(name: str, *, symmetrize: bool = False) -> dict[tuple[int, cz_connections: dict[LocusIdx, None] = {} cz_loci = op_loci['cz'] for c1, c2 in cz_loci: - if self.iqm_includes_moves or all(component in self.iqm_dqa.qubits for component in (c1, c2)): + if self.iqm_includes_resonators or all(component in self.iqm_dqa.qubits for component in (c1, c2)): idx_locus = locus_to_idx((c1, c2)) cz_connections[idx_locus] = None @@ -346,4 +351,6 @@ def restrict_to_qubits(self, qubits: Union[list[int], list[str]]) -> IQMTarget: restricted target """ qubits_str = [self.iqm_idx_to_component[q] if isinstance(q, int) else str(q) for q in qubits] - return _restrict_dqa_to_qubits(self.iqm_dqa, qubits_str, self.iqm_includes_moves, self.iqm_includes_fake_czs) + return _restrict_dqa_to_qubits( + self.iqm_dqa, qubits_str, self.iqm_includes_resonators, self.iqm_includes_fake_czs + ) diff --git a/tests/conftest.py b/tests/conftest.py index e662f0e88..01ec7d8b2 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -163,16 +163,6 @@ def move_architecture(): ) -@pytest.fixture -def adonis_coupling_map(): - return {(0, 2), (1, 2), (3, 2), (4, 2)} - - -@pytest.fixture -def deneb_coupling_map(): - return {(i, 6) for i in range(6)} - - @pytest.fixture def ndonis_architecture(): return DynamicQuantumArchitecture( diff --git a/tests/fake_backends/test_fake_adonis.py b/tests/fake_backends/test_fake_adonis.py index 781978f30..4080a48ef 100644 --- a/tests/fake_backends/test_fake_adonis.py +++ b/tests/fake_backends/test_fake_adonis.py @@ -17,6 +17,7 @@ from qiskit_aer.noise.noise_model import NoiseModel from iqm.qiskit_iqm.fake_backends.fake_adonis import IQMFakeAdonis +from iqm.qiskit_iqm.iqm_backend import IQMTarget def test_iqm_fake_adonis(): @@ -25,9 +26,13 @@ def test_iqm_fake_adonis(): assert backend.name == 'IQMFakeAdonisBackend' -def test_iqm_fake_adonis_connectivity(adonis_coupling_map): +def test_iqm_fake_adonis_connectivity(): backend = IQMFakeAdonis() - assert set(backend.coupling_map.get_edges()) == adonis_coupling_map + assert isinstance(backend.target, IQMTarget) + coupling_map = {(0, 2), (1, 2), (3, 2), (4, 2)} + assert set(backend.target.build_coupling_map()) == coupling_map + assert backend.target_with_resonators is None + assert set(backend.coupling_map.get_edges()) == coupling_map def test_iqm_fake_adonis_noise_model_instantiated(): diff --git a/tests/fake_backends/test_fake_deneb.py b/tests/fake_backends/test_fake_deneb.py index 924acc91e..1e38aade4 100644 --- a/tests/fake_backends/test_fake_deneb.py +++ b/tests/fake_backends/test_fake_deneb.py @@ -32,11 +32,15 @@ def test_iqm_fake_deneb(): assert backend.name == "IQMFakeDenebBackend" -def test_iqm_fake_deneb_connectivity(deneb_coupling_map): +def test_iqm_fake_deneb_connectivity(): backend = IQMFakeDeneb() assert isinstance(backend.target, IQMTarget) - assert set(backend.target.real_target.build_coupling_map()) == set(deneb_coupling_map) - assert set(backend.coupling_map.get_edges()) == {(qb1, qb2) for qb1 in range(6) for qb2 in range(6) if qb1 != qb2} + partial_coupling_map = {(qb1, qb2) for qb1 in range(6) for qb2 in range(6) if qb1 != qb2} + assert set(backend.target.build_coupling_map()) == partial_coupling_map + assert set(backend.target_with_resonators.build_coupling_map()) == partial_coupling_map.union( + ((i, 6) for i in range(6)) + ) + assert set(backend.coupling_map.get_edges()) == partial_coupling_map def test_iqm_fake_deneb_noise_model_instantiated(): From 33ad089f5ddc4055c0fe1f50093091d89f6645b9 Mon Sep 17 00:00:00 2001 From: Arianne Meijer Date: Thu, 9 Jan 2025 20:46:44 +0200 Subject: [PATCH 40/47] Refactor tests --- tests/move_architecture/test_architecture.py | 1 + tests/move_architecture/test_move_circuit.py | 2 + tests/test_iqm_naive_move_pass.py | 234 ++++++++++++++---- ...est_iqm_single_qubit_gate_optimization.py} | 31 --- tests/test_iqm_target.py | 17 +- tests/test_qiskit_to_iqm.py | 31 +++ 6 files changed, 236 insertions(+), 80 deletions(-) rename tests/{test_iqm_transpilation.py => test_iqm_single_qubit_gate_optimization.py} (81%) diff --git a/tests/move_architecture/test_architecture.py b/tests/move_architecture/test_architecture.py index 0008fe957..5c3570f70 100644 --- a/tests/move_architecture/test_architecture.py +++ b/tests/move_architecture/test_architecture.py @@ -20,6 +20,7 @@ from tests.utils import get_mocked_backend +# TODO Refactor to check more different architectures def test_backend_configuration_new(move_architecture): """Check that the extended architecture is configured correctly to the Qiskit backend.""" assert move_architecture is not None diff --git a/tests/move_architecture/test_move_circuit.py b/tests/move_architecture/test_move_circuit.py index 11fa4e955..c8cf8b4df 100644 --- a/tests/move_architecture/test_move_circuit.py +++ b/tests/move_architecture/test_move_circuit.py @@ -23,6 +23,8 @@ from iqm.qiskit_iqm.move_gate import MoveGate from tests.utils import describe_instruction, get_mocked_backend, get_transpiled_circuit_json +# TODO Most of these tests are not testing what is stated in the documentation. They should be refactored, if not removed. + def test_move_gate_trivial_layout(move_architecture): """Tests that a trivial 1-to-1 layout is translated correctly.""" diff --git a/tests/test_iqm_naive_move_pass.py b/tests/test_iqm_naive_move_pass.py index 2c38a4eb2..0e808915d 100644 --- a/tests/test_iqm_naive_move_pass.py +++ b/tests/test_iqm_naive_move_pass.py @@ -2,63 +2,195 @@ """ import pytest +from itertools import product +from typing import Iterable from qiskit.circuit import QuantumCircuit -from qiskit.circuit.library import QuantumVolume +from qiskit.circuit.library import QuantumVolume, PermutationGate from qiskit.compiler import transpile -from qiskit.quantum_info import Operator +from qiskit.quantum_info import Operator, Statevector -from iqm.qiskit_iqm.fake_backends.fake_adonis import IQMFakeAdonis -from iqm.qiskit_iqm.fake_backends.fake_aphrodite import IQMFakeAphrodite -from iqm.qiskit_iqm.fake_backends.fake_deneb import IQMFakeDeneb +from iqm.iqm_client import QuantumArchitectureSpecification, ExistingMoveHandlingOptions + +from iqm.qiskit_iqm.fake_backends import IQMFakeBackend, IQMFakeDeneb, IQMFakeAdonis, IQMErrorProfile, IQMFakeAphrodite from iqm.qiskit_iqm.iqm_circuit_validation import validate_circuit from iqm.qiskit_iqm.iqm_naive_move_pass import transpile_to_IQM from iqm.qiskit_iqm.iqm_transpilation import IQMReplaceGateWithUnitaryPass -from iqm.qiskit_iqm.move_gate import MOVE_GATE_UNITARY +from iqm.qiskit_iqm.move_gate import MOVE_GATE_UNITARY, MoveGate from .utils import capture_submitted_circuits, get_mocked_backend -@pytest.mark.parametrize("n_qubits", list(range(2, 6))) -def test_transpile_to_IQM_star_semantically_preserving( - ndonis_architecture, n_qubits -): # pylint: disable=too-many-locals - backend, _client = get_mocked_backend(ndonis_architecture) - n_backend_qubits = backend.target.num_qubits - if n_backend_qubits >= n_qubits: - circuit = QuantumVolume(n_qubits, n_qubits) - # Use optimization_level=0 to avoid that the qubits get remapped. - transpiled_circuit = transpile_to_IQM(circuit, backend, optimization_level=0, remove_final_rzs=False) - transpiled_circuit_without_moves = IQMReplaceGateWithUnitaryPass("move", MOVE_GATE_UNITARY)(transpiled_circuit) - print(transpiled_circuit_without_moves) - transpiled_operator = Operator(transpiled_circuit_without_moves) - # Update the original circuit to have the correct number of qubits and resonators. - original_with_resonator = QuantumCircuit(transpiled_circuit.num_qubits) - original_with_resonator.append(circuit, range(circuit.num_qubits)) - - # Make it into an Operator and cheeck equivalence. - circuit_operator = Operator(original_with_resonator) - assert circuit_operator.equiv(transpiled_operator) - - -@pytest.mark.parametrize("n_qubits", list(range(2, 6))) -def test_transpile_to_IQM_valid_result(ndonis_architecture, n_qubits): - """Test that transpiled circuit has gates that are allowed by the backend""" - backend, _ = get_mocked_backend(ndonis_architecture) - for i in range(2, n_qubits): - circuit = QuantumVolume(i) - transpiled_circuit = transpile_to_IQM(circuit, backend, optimize_single_qubits=False) - validate_circuit(transpiled_circuit, backend) +def quantum_volume_circuits(sizes: Iterable[int] = range(2, 6)): + """Generate random quantum volume circuits for testing.""" + return [QuantumVolume(n) for n in sizes] + + +def ghz_circuits(sizes: Iterable[int] = range(2, 6)): + """Generate GHZ circuits for testing.""" + circuits = [] + for n in sizes: + qc = QuantumCircuit(n) + qc.h(0) + # for i in range(n - 1): + # qc.cx(i, i + 1) + for i in range(1, n): + qc.cx(0, i) + circuits.append(qc) + return circuits + + +def hypothetical_fake_device(): + """Generate a hypothetical fake device for testing. + + QB1 QB2 + | | + COMP_R1 + | | + QB3 - QB4 QB7 - QB8 + | | + COMP_R2 + | | + QB5 QB6 + + + """ + architecture = QuantumArchitectureSpecification( + name="Hypothetical", + qubits=["COMP_R1", "COMP_R2", "QB1", "QB2", "QB3", "QB4", "QB5", "QB6", "QB7", "QB8"], + operations={ + "prx": [["QB1"], ["QB2"], ["QB3"], ["QB4"], ["QB5"], ["QB6"], ["QB7"], ["QB8"]], + "cz": [ + ["QB1", "COMP_R1"], + ["QB2", "COMP_R1"], + ["QB3", "QB4"], + ["QB4", "COMP_R1"], + ["QB4", "COMP_R2"], + ["QB5", "COMP_R2"], + ["QB6", "COMP_R2"], + ["QB7", "COMP_R1"], + ["QB7", "COMP_R2"], + ["QB7", "QB8"], + ], + "move": [ + ["QB4", "COMP_R1"], + ["QB4", "COMP_R2"], + ["QB7", "COMP_R1"], + ["QB7", "COMP_R2"], + ], + "measure": [["QB1"], ["QB2"], ["QB3"], ["QB4"], ["QB5"], ["QB6"], ["QB7"], ["QB8"]], + "barrier": [], + }, + qubit_connectivity=[ + ["QB1", "COMP_R1"], + ["QB2", "COMP_R1"], + ["QB3", "QB4"], + ["QB4", "COMP_R1"], + ["QB4", "COMP_R2"], + ["QB5", "COMP_R2"], + ["QB6", "COMP_R2"], + ["QB7", "COMP_R1"], + ["QB7", "COMP_R2"], + ["QB7", "QB8"], + ], + ) + error_profile = IQMErrorProfile( + t1s={q: 35000.0 for q in architecture.qubits}, + t2s={q: 33000.0 for q in architecture.qubits}, + single_qubit_gate_depolarizing_error_parameters={ + "prx": {q: 0.0002 for q in architecture.qubits}, + }, + two_qubit_gate_depolarizing_error_parameters={ + gate: {(qb1, qb2): 0.0128 for qb1, qb2 in architecture.qubit_connectivity} for gate in ["cz", "move"] + }, + single_qubit_gate_durations={"prx": 40.0}, + two_qubit_gate_durations={"cz": 120.0, "move": 96.0}, + readout_errors={q: {"0": 0.0, "1": 0.0} for q in architecture.qubits}, + name="sample-chip", + ) + + return IQMFakeBackend(architecture, error_profile, name="IQMHypotheticalTestingDevice") + +def devices_to_test_on(): + fake_devices = [IQMFakeDeneb(), IQMFakeAdonis(), hypothetical_fake_device()] + return fake_devices + [get_mocked_backend(device.architecture)[0] for device in fake_devices] -@pytest.mark.parametrize("n_qubits", list(range(2, 6))) -def test_qiskit_transpile_valid_result(ndonis_architecture, n_qubits): - """Test that move gate is applied only when one qubit is in zero state.""" - backend, _ = get_mocked_backend(ndonis_architecture) - for i in range(2, n_qubits): - circuit = QuantumVolume(i) - transpiled_circuit = transpile(circuit, backend) + +@pytest.mark.parametrize( + ("circuit", "backend", "method"), + product( + quantum_volume_circuits(range(2, 9, 3)) + ghz_circuits(range(2, 7, 2)), + devices_to_test_on(), + ["iqm", "native_integration"], + ), +) +class TestTranspilation: + + def test_semantically_preserving(self, circuit, backend, method): + """Test that the transpiled circuit is semantically equivalent to the original one.""" + n_backend_qubits = backend.target.num_qubits + print(backend.name) + # Only run the test if the circuit fits the device + if n_backend_qubits >= circuit.num_qubits: + # Use layout_method="trivial" to avoid initial qubit remapping. + # Use fixed seed so that the test is deterministic. + if method == "native_integration": + # Use optimization_level=0 to enforce equivalence up to global phase. + transpiled_circuit = transpile(circuit, backend, optimization_level=0, seed_transpiler=123) + elif method == "qiskit": # Debug case to check if the issues lies with the Qiskit transpiler. + transpiled_circuit = transpile( + circuit, target=backend.target, optimization_level=0, seed_transpiler=123 + ) + + else: + # Use remove_final_rzs=False to avoid equivalence up to global phase. + transpiled_circuit = transpile_to_IQM( + circuit, + backend, + optimization_level=0, + remove_final_rzs=False, + seed_transpiler=123, + ) + # Fix the dimension of the original circuit to agree with the transpiled_circuit + padded_circuit = QuantumCircuit(transpiled_circuit.num_qubits) + padded_circuit.compose(circuit, range(circuit.num_qubits), inplace=True) + circuit_operator = Operator.from_circuit(padded_circuit) + print() + print("After transpile", transpiled_circuit.layout) + if "move" in transpiled_circuit.count_ops(): + # Replace the move gate with the iSWAP unitary. + transpiled_circuit_without_moves = IQMReplaceGateWithUnitaryPass("move", MOVE_GATE_UNITARY)( + transpiled_circuit + ) + initial_layout = transpiled_circuit.layout.initial_layout if transpiled_circuit.layout else None + final_layout = transpiled_circuit.layout.final_layout if transpiled_circuit.layout else None + transpiled_operator = Operator.from_circuit( + transpiled_circuit_without_moves, + ignore_set_layout=True, + layout=initial_layout, + final_layout=final_layout, + ) + else: + transpiled_operator = Operator.from_circuit(transpiled_circuit, ignore_set_layout=False) + print(circuit) + print(transpiled_circuit) + # TODO figure out why the transpiled circuit is not always semantically equivalent to the original one when the GHZ is a ladder rather than a star. + assert circuit_operator.equiv(transpiled_operator) + + @pytest.mark.parametrize('optimization_level', list(range(4))) + def test_valid_result(self, circuit, backend, method, optimization_level): + """Test that transpiled circuit has gates that are allowed by the backend""" + if method == "native": + transpiled_circuit = transpile(circuit, backend, optimization_level=optimization_level) + else: + transpiled_circuit = transpile_to_IQM(circuit, backend, optimization_level=optimization_level) validate_circuit(transpiled_circuit, backend) + def test_transpiled_circuit_keeps_layout(self, circuit, backend, method): + """Test that the layout of the transpiled circuit is preserved.""" + # TODO implement + pytest.xfail("Not implemented yet") + @pytest.mark.parametrize( "fake_backend,restriction", @@ -72,6 +204,9 @@ def test_transpiling_with_restricted_qubits(fake_backend, restriction): """Test that the transpiled circuit only uses the qubits specified in the restriction.""" n_qubits = 3 circuit = QuantumVolume(n_qubits, seed=42) + # Test both a FakeBackend and a mocked IQM backend. + # NOTE the mocked client in the backend does not work nicely with pytest.mark.parametrize which causes a + # requests.exceptions.ConnectionError: HTTPConnectionPool error when getting the DQA rather than returning the mock. for backend in [fake_backend, get_mocked_backend(fake_backend.architecture)[0]]: restriction_idxs = [backend.qubit_name_to_index(qubit) for qubit in restriction] for restricted in [restriction, restriction_idxs]: @@ -84,3 +219,16 @@ def test_transpiling_with_restricted_qubits(fake_backend, restriction): # Check that the run doesn't fail. capture_submitted_circuits() backend.run(transpiled_circuit, shots=1, qubit_mapping=dict(enumerate(restriction))) + + +def test_transpile_empty_optimized_circuit(): + """In case the circuit is optimized to an empty circuit by the transpiler, it should not raise an error.""" + backend = IQMFakeDeneb() + qc = QuantumCircuit(2) + qc.append(MoveGate(), [0, 1]) + qc.append(MoveGate(), [0, 1]) + transpiled_circuit = transpile_to_IQM(qc, backend, existing_moves_handling=ExistingMoveHandlingOptions.REMOVE) + assert len(transpiled_circuit) == 0 + + transpiled_circuit = transpile(QuantumCircuit(), backend) + assert len(transpiled_circuit) == 0 diff --git a/tests/test_iqm_transpilation.py b/tests/test_iqm_single_qubit_gate_optimization.py similarity index 81% rename from tests/test_iqm_transpilation.py rename to tests/test_iqm_single_qubit_gate_optimization.py index 862c569e1..55e91136f 100644 --- a/tests/test_iqm_transpilation.py +++ b/tests/test_iqm_single_qubit_gate_optimization.py @@ -24,7 +24,6 @@ from iqm.qiskit_iqm.fake_backends.fake_deneb import IQMFakeDeneb from iqm.qiskit_iqm.iqm_move_layout import generate_initial_layout from iqm.qiskit_iqm.iqm_transpilation import optimize_single_qubit_gates -from tests.utils import get_transpiled_circuit_json def test_optimize_single_qubit_gates_preserves_unitary(): @@ -103,36 +102,6 @@ def test_optimize_single_qubit_gates_raises_on_invalid_basis(): optimize_single_qubit_gates(circuit) -def test_submitted_circuit(adonis_architecture): - """Test that a circuit submitted via IQM backend gets transpiled into proper JSON.""" - circuit = QuantumCircuit(2, 2) - circuit.h(0) - circuit.cx(0, 1) - - circuit.measure_all() - - # This transpilation seed maps virtual qubit 0 to physical qubit 2, and virtual qubit 1 to physical qubit 4 - # Other seeds will switch the mapping, and may also reorder the first prx instructions - submitted_circuit = get_transpiled_circuit_json(circuit, adonis_architecture, seed_transpiler=123) - - instr_names = [f"{instr.name}:{','.join(instr.qubits)}" for instr in submitted_circuit.instructions] - assert instr_names == [ - # CX phase 1: Hadamard on target qubit 1 (= QB3) - 'prx:QB3', - # Hadamard on 0 (= QB5) - 'prx:QB5', - # CX phase 2: CZ on 0,1 (= physical QB5, QB3) - 'cz:QB5,QB3', - # CX phase 3: Hadamard again on target qubit 1 (= physical QB3) - 'prx:QB3', - # Barrier before measurements - 'barrier:QB5,QB3', - # Measurement on both qubits - 'measure:QB5', - 'measure:QB3', - ] - - @pytest.mark.parametrize('backend', [IQMFakeAdonis(), IQMFakeDeneb(), IQMFakeAphrodite()]) def test_optimize_single_qubit_gates_preserves_layout(backend): """Test that a circuit submitted via IQM backend gets transpiled into proper JSON.""" diff --git a/tests/test_iqm_target.py b/tests/test_iqm_target.py index 2b040ded2..808ef8da3 100644 --- a/tests/test_iqm_target.py +++ b/tests/test_iqm_target.py @@ -16,15 +16,20 @@ (IQMFakeDeneb(), ["QB5", "QB3", "QB1", "COMP_R"]), ], ) -def test_transpiling_with_restricted_qubits(backend, restriction): - """Test that the transpiled circuit only uses the qubits specified in the restriction.""" +def test_target_from_restricted_qubits(backend, restriction): + """Test that the restricted target is properly created.""" restriction_idxs = [backend.qubit_name_to_index(qubit) for qubit in restriction] - for restricted in [restriction, restriction_idxs]: - print("Restriction:", restricted) - restricted_target = backend.target.restrict_to_qubits(restricted) + for restricted in [restriction, restriction_idxs]: # Check both string and integer restrictions + restricted_target = backend.target.restrict_to_qubits(restricted) # Restrict from IQMTarget + restricted_edges = restricted_target.build_coupling_map().get_edges() assert restricted_target.num_qubits == len(restricted) - for edge in restricted_target.build_coupling_map().get_edges(): + assert set(restricted_edges) == set( + backend.restrict_to_qubits(restricted).build_coupling_map().get_edges() + ) # Restrict from backend + + # Check if the edges are allowed in both the backend and the restricted target + for edge in restricted_edges: translated_edge = ( backend.qubit_name_to_index(restriction[edge[0]]), backend.qubit_name_to_index(restriction[edge[1]]), diff --git a/tests/test_qiskit_to_iqm.py b/tests/test_qiskit_to_iqm.py index 327d52c72..054b8ab25 100644 --- a/tests/test_qiskit_to_iqm.py +++ b/tests/test_qiskit_to_iqm.py @@ -18,6 +18,7 @@ from qiskit import ClassicalRegister, QuantumCircuit, QuantumRegister from iqm.qiskit_iqm.qiskit_to_iqm import MeasurementKey +from .utils import get_transpiled_circuit_json @pytest.fixture() @@ -47,3 +48,33 @@ def test_measurement_key_from_clbit(): def test_measurement_key_from_string(key_str): mk = MeasurementKey.from_string(key_str) assert str(mk) == key_str + + +def test_circuit_to_iqm_json(adonis_architecture): + """Test that a circuit submitted via IQM backend gets transpiled into proper JSON.""" + circuit = QuantumCircuit(2, 2) + circuit.h(0) + circuit.cx(0, 1) + + circuit.measure_all() + + # This transpilation seed maps virtual qubit 0 to physical qubit 2, and virtual qubit 1 to physical qubit 4 + # Other seeds will switch the mapping, and may also reorder the first prx instructions + submitted_circuit = get_transpiled_circuit_json(circuit, adonis_architecture, seed_transpiler=123) + + instr_names = [f"{instr.name}:{','.join(instr.qubits)}" for instr in submitted_circuit.instructions] + assert instr_names == [ + # CX phase 1: Hadamard on target qubit 1 (= QB3) + 'prx:QB3', + # Hadamard on 0 (= QB5) + 'prx:QB5', + # CX phase 2: CZ on 0,1 (= physical QB5, QB3) + 'cz:QB5,QB3', + # CX phase 3: Hadamard again on target qubit 1 (= physical QB3) + 'prx:QB3', + # Barrier before measurements + 'barrier:QB5,QB3', + # Measurement on both qubits + 'measure:QB5', + 'measure:QB3', + ] From 3b048dc7004877335a0f60a58ebb32782048762d Mon Sep 17 00:00:00 2001 From: Arianne Meijer Date: Thu, 9 Jan 2025 20:47:51 +0200 Subject: [PATCH 41/47] deserialize with the same quantum registers --- src/iqm/qiskit_iqm/qiskit_to_iqm.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/iqm/qiskit_iqm/qiskit_to_iqm.py b/src/iqm/qiskit_iqm/qiskit_to_iqm.py index 806bb06e3..f7e5c440f 100644 --- a/src/iqm/qiskit_iqm/qiskit_to_iqm.py +++ b/src/iqm/qiskit_iqm/qiskit_to_iqm.py @@ -21,7 +21,8 @@ import numpy as np from qiskit import QuantumCircuit as QiskitQuantumCircuit -from qiskit.circuit import ClassicalRegister, Clbit, QuantumRegister +from qiskit.circuit import ClassicalRegister, Clbit +from qiskit.transpiler.layout import Layout from iqm.iqm_client import Instruction from iqm.qiskit_iqm.move_gate import MoveGate @@ -219,7 +220,7 @@ def serialize_instructions( def deserialize_instructions( - instructions: list[Instruction], qubit_name_to_index: dict[str, int] + instructions: list[Instruction], qubit_name_to_index: dict[str, int], layout: Layout ) -> QiskitQuantumCircuit: """Helper function to turn a list of IQM Instructions into a Qiskit QuantumCircuit. @@ -240,13 +241,16 @@ def deserialize_instructions( mk = MeasurementKey.from_string(instr.args['key']) cl_regs[mk.creg_idx] = cl_regs.get(mk.creg_idx, ClassicalRegister(size=mk.creg_len, name=mk.creg_name)) cl_bits[str(mk)] = cl_regs[mk.creg_idx][mk.clbit_idx] - qreg = QuantumRegister(max(qubit_name_to_index.values()) + 1, 'q') + # qreg = QuantumRegister(max(qubit_name_to_index.values()) + 1, 'q') # Add an empty Classical register when the original circuit had unused classical registers circuit = QiskitQuantumCircuit( - qreg, *(cl_regs.get(i, ClassicalRegister(0)) for i in range(max(cl_regs) + 1 if cl_regs else 0)) + *layout.get_registers(), + *(cl_regs.get(i, ClassicalRegister(0)) for i in range(max(cl_regs) + 1 if cl_regs else 0)), ) + index_to_qiskit_qubit = layout.get_physical_bits() + print(index_to_qiskit_qubit) for instr in instructions: - loci = [qubit_name_to_index[q] for q in instr.qubits] + loci = [index_to_qiskit_qubit[qubit_name_to_index[q]] for q in instr.qubits] if instr.name == 'prx': angle_t = instr.args['angle_t'] * 2 * np.pi phase_t = instr.args['phase_t'] * 2 * np.pi From 82e84df8501aa80ce2c4c5f12f0a6adf6d7fb788 Mon Sep 17 00:00:00 2001 From: Arianne Meijer Date: Thu, 9 Jan 2025 20:48:17 +0200 Subject: [PATCH 42/47] transpiler keeps layout --- src/iqm/qiskit_iqm/iqm_naive_move_pass.py | 40 +++++++++++++++++++---- 1 file changed, 34 insertions(+), 6 deletions(-) diff --git a/src/iqm/qiskit_iqm/iqm_naive_move_pass.py b/src/iqm/qiskit_iqm/iqm_naive_move_pass.py index ebfd21073..edfd21349 100644 --- a/src/iqm/qiskit_iqm/iqm_naive_move_pass.py +++ b/src/iqm/qiskit_iqm/iqm_naive_move_pass.py @@ -69,7 +69,16 @@ def run(self, dag: DAGCircuit) -> DAGCircuit: TranspilerError: The layout is not compatible with the DAG, or if the input gate set is incorrect. """ print("Transpiling circuit") + print(self.property_set) circuit = dag_to_circuit(dag) + if len(circuit) == 0: + return dag # Empty circuit, no need to transpile. + print(circuit) + # For some reason, the dag does not contain the layout, so we need to do a bunch of fixing. + if self.property_set.get("layout"): + layout = self.property_set['layout'] + else: + layout = Layout.generate_trivial_layout(circuit) iqm_circuit = IQMClientCircuit( name="Transpiling Circuit", instructions=tuple(serialize_instructions(circuit, self.idx_to_component)), @@ -78,15 +87,34 @@ def run(self, dag: DAGCircuit) -> DAGCircuit: routed_iqm_circuit = transpile_insert_moves( iqm_circuit, self.architecture, existing_moves=self.existing_moves_handling ) - routed_circuit = deserialize_instructions(list(routed_iqm_circuit.instructions), self.component_to_idx) + routed_circuit = deserialize_instructions( + list(routed_iqm_circuit.instructions), self.component_to_idx, layout + ) except ValidationError as e: # The Circuit without move gates is empty. - # FIXME seems unsafe, assuming this given a generic Pydantic exception - if e.title == "Empty Circuit": - circ_args = [circuit.num_qubits, circuit.num_ancillas, circuit.num_clbits] - routed_circuit = QuantumCircuit(*(arg for arg in circ_args if arg > 0)) + errors = e.errors() + if ( + len(errors) == 1 + and errors[0]["msg"] == "Value error, Each circuit should have at least one instruction." + ): + circ_args = [circuit.num_ancillas, circuit.num_clbits] + routed_circuit = QuantumCircuit(*layout.get_registers(), *(arg for arg in circ_args if arg > 0)) else: raise e - new_dag = circuit_to_dag(routed_circuit) + # Create the new DAG and make sure that the qubits are properly ordered. + ordered_qubits = [layout.get_physical_bits()[i] for i in range(len(layout.get_physical_bits()))] + new_dag = circuit_to_dag(routed_circuit, qubit_order=ordered_qubits, clbit_order=routed_circuit.clbits) + # Update the final_layout with the correct bits. + if "final_layout" in self.property_set: + inv_layout = layout.get_physical_bits() + self.property_set['final_layout'] = Layout( + { + physical: inv_layout[dag.find_bit(virtual).index] + for physical, virtual in self.property_set['final_layout'].get_physical_bits().items() + } + ) + else: + self.property_set['final_layout'] = layout + print(self.property_set['final_layout']) return new_dag From ca287fe1f7d4f3baf93a9b1ed35260278ef16a6a Mon Sep 17 00:00:00 2001 From: Arianne Meijer Date: Thu, 9 Jan 2025 20:48:45 +0200 Subject: [PATCH 43/47] pylint --- tests/test_iqm_naive_move_pass.py | 11 +++++------ tests/test_qiskit_to_iqm.py | 1 + 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/test_iqm_naive_move_pass.py b/tests/test_iqm_naive_move_pass.py index 0e808915d..f47092dad 100644 --- a/tests/test_iqm_naive_move_pass.py +++ b/tests/test_iqm_naive_move_pass.py @@ -1,17 +1,17 @@ """Testing IQM transpilation. """ -import pytest from itertools import product from typing import Iterable + +import pytest from qiskit.circuit import QuantumCircuit -from qiskit.circuit.library import QuantumVolume, PermutationGate +from qiskit.circuit.library import PermutationGate, QuantumVolume from qiskit.compiler import transpile from qiskit.quantum_info import Operator, Statevector -from iqm.iqm_client import QuantumArchitectureSpecification, ExistingMoveHandlingOptions - -from iqm.qiskit_iqm.fake_backends import IQMFakeBackend, IQMFakeDeneb, IQMFakeAdonis, IQMErrorProfile, IQMFakeAphrodite +from iqm.iqm_client import ExistingMoveHandlingOptions, QuantumArchitectureSpecification +from iqm.qiskit_iqm.fake_backends import IQMErrorProfile, IQMFakeAdonis, IQMFakeAphrodite, IQMFakeBackend, IQMFakeDeneb from iqm.qiskit_iqm.iqm_circuit_validation import validate_circuit from iqm.qiskit_iqm.iqm_naive_move_pass import transpile_to_IQM from iqm.qiskit_iqm.iqm_transpilation import IQMReplaceGateWithUnitaryPass @@ -125,7 +125,6 @@ def devices_to_test_on(): ), ) class TestTranspilation: - def test_semantically_preserving(self, circuit, backend, method): """Test that the transpiled circuit is semantically equivalent to the original one.""" n_backend_qubits = backend.target.num_qubits diff --git a/tests/test_qiskit_to_iqm.py b/tests/test_qiskit_to_iqm.py index 054b8ab25..8fd6b8c7a 100644 --- a/tests/test_qiskit_to_iqm.py +++ b/tests/test_qiskit_to_iqm.py @@ -18,6 +18,7 @@ from qiskit import ClassicalRegister, QuantumCircuit, QuantumRegister from iqm.qiskit_iqm.qiskit_to_iqm import MeasurementKey + from .utils import get_transpiled_circuit_json From 42d2bd9d0c2c94fd5758f81f9bb91ca10c27a2fa Mon Sep 17 00:00:00 2001 From: Arianne Meijer Date: Thu, 9 Jan 2025 20:49:35 +0200 Subject: [PATCH 44/47] pylint --- src/iqm/qiskit_iqm/iqm_naive_move_pass.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/iqm/qiskit_iqm/iqm_naive_move_pass.py b/src/iqm/qiskit_iqm/iqm_naive_move_pass.py index edfd21349..222672a81 100644 --- a/src/iqm/qiskit_iqm/iqm_naive_move_pass.py +++ b/src/iqm/qiskit_iqm/iqm_naive_move_pass.py @@ -76,7 +76,7 @@ def run(self, dag: DAGCircuit) -> DAGCircuit: print(circuit) # For some reason, the dag does not contain the layout, so we need to do a bunch of fixing. if self.property_set.get("layout"): - layout = self.property_set['layout'] + layout = self.property_set["layout";] else: layout = Layout.generate_trivial_layout(circuit) iqm_circuit = IQMClientCircuit( @@ -106,15 +106,15 @@ def run(self, dag: DAGCircuit) -> DAGCircuit: # Update the final_layout with the correct bits. if "final_layout" in self.property_set: inv_layout = layout.get_physical_bits() - self.property_set['final_layout'] = Layout( + self.property_set["final_layout"] = Layout( { physical: inv_layout[dag.find_bit(virtual).index] - for physical, virtual in self.property_set['final_layout'].get_physical_bits().items() + for physical, virtual in self.property_set["final_layout"].get_physical_bits().items() } ) else: - self.property_set['final_layout'] = layout - print(self.property_set['final_layout']) + self.property_set["final_layout"] = layout + print(self.property_set["final_layout"]) return new_dag From 749e3e2f2bb2115e3f4b650da4cb15f015bf8e15 Mon Sep 17 00:00:00 2001 From: Arianne Meijer Date: Thu, 9 Jan 2025 20:50:03 +0200 Subject: [PATCH 45/47] typo --- src/iqm/qiskit_iqm/iqm_naive_move_pass.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/iqm/qiskit_iqm/iqm_naive_move_pass.py b/src/iqm/qiskit_iqm/iqm_naive_move_pass.py index 222672a81..53e5d41f5 100644 --- a/src/iqm/qiskit_iqm/iqm_naive_move_pass.py +++ b/src/iqm/qiskit_iqm/iqm_naive_move_pass.py @@ -76,7 +76,7 @@ def run(self, dag: DAGCircuit) -> DAGCircuit: print(circuit) # For some reason, the dag does not contain the layout, so we need to do a bunch of fixing. if self.property_set.get("layout"): - layout = self.property_set["layout";] + layout = self.property_set["layout"] else: layout = Layout.generate_trivial_layout(circuit) iqm_circuit = IQMClientCircuit( From d8ef600bb028322433cc5ba32ebc8a5852989890 Mon Sep 17 00:00:00 2001 From: Arianne Meijer Date: Thu, 9 Jan 2025 20:50:56 +0200 Subject: [PATCH 46/47] mypy --- src/iqm/qiskit_iqm/iqm_naive_move_pass.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/iqm/qiskit_iqm/iqm_naive_move_pass.py b/src/iqm/qiskit_iqm/iqm_naive_move_pass.py index 53e5d41f5..91a669882 100644 --- a/src/iqm/qiskit_iqm/iqm_naive_move_pass.py +++ b/src/iqm/qiskit_iqm/iqm_naive_move_pass.py @@ -171,7 +171,7 @@ def transpile_to_IQM( # pylint: disable=too-many-arguments else: target = backend.target - if restrict_to_qubits is not None: + if restrict_to_qubits is not None and target is not None: target = target.restrict_to_qubits(restrict_to_qubits) # Determine which scheduling method to use From 55174103b377fb290105e84ba615afe17ccbbef4 Mon Sep 17 00:00:00 2001 From: Arianne Meijer Date: Thu, 9 Jan 2025 20:58:33 +0200 Subject: [PATCH 47/47] Skipping too large circuits --- tests/test_iqm_naive_move_pass.py | 105 ++++++++++++++++-------------- 1 file changed, 56 insertions(+), 49 deletions(-) diff --git a/tests/test_iqm_naive_move_pass.py b/tests/test_iqm_naive_move_pass.py index f47092dad..c6010dee7 100644 --- a/tests/test_iqm_naive_move_pass.py +++ b/tests/test_iqm_naive_move_pass.py @@ -118,10 +118,12 @@ def devices_to_test_on(): @pytest.mark.parametrize( ("circuit", "backend", "method"), - product( - quantum_volume_circuits(range(2, 9, 3)) + ghz_circuits(range(2, 7, 2)), - devices_to_test_on(), - ["iqm", "native_integration"], + list( + product( + quantum_volume_circuits(range(2, 9, 3)) + ghz_circuits(range(2, 7, 2)), + devices_to_test_on(), + ["iqm", "native_integration"], + ) ), ) class TestTranspilation: @@ -130,55 +132,57 @@ def test_semantically_preserving(self, circuit, backend, method): n_backend_qubits = backend.target.num_qubits print(backend.name) # Only run the test if the circuit fits the device - if n_backend_qubits >= circuit.num_qubits: - # Use layout_method="trivial" to avoid initial qubit remapping. - # Use fixed seed so that the test is deterministic. - if method == "native_integration": - # Use optimization_level=0 to enforce equivalence up to global phase. - transpiled_circuit = transpile(circuit, backend, optimization_level=0, seed_transpiler=123) - elif method == "qiskit": # Debug case to check if the issues lies with the Qiskit transpiler. - transpiled_circuit = transpile( - circuit, target=backend.target, optimization_level=0, seed_transpiler=123 - ) - - else: - # Use remove_final_rzs=False to avoid equivalence up to global phase. - transpiled_circuit = transpile_to_IQM( - circuit, - backend, - optimization_level=0, - remove_final_rzs=False, - seed_transpiler=123, - ) - # Fix the dimension of the original circuit to agree with the transpiled_circuit - padded_circuit = QuantumCircuit(transpiled_circuit.num_qubits) - padded_circuit.compose(circuit, range(circuit.num_qubits), inplace=True) - circuit_operator = Operator.from_circuit(padded_circuit) - print() - print("After transpile", transpiled_circuit.layout) - if "move" in transpiled_circuit.count_ops(): - # Replace the move gate with the iSWAP unitary. - transpiled_circuit_without_moves = IQMReplaceGateWithUnitaryPass("move", MOVE_GATE_UNITARY)( - transpiled_circuit - ) - initial_layout = transpiled_circuit.layout.initial_layout if transpiled_circuit.layout else None - final_layout = transpiled_circuit.layout.final_layout if transpiled_circuit.layout else None - transpiled_operator = Operator.from_circuit( - transpiled_circuit_without_moves, - ignore_set_layout=True, - layout=initial_layout, - final_layout=final_layout, - ) - else: - transpiled_operator = Operator.from_circuit(transpiled_circuit, ignore_set_layout=False) - print(circuit) - print(transpiled_circuit) - # TODO figure out why the transpiled circuit is not always semantically equivalent to the original one when the GHZ is a ladder rather than a star. - assert circuit_operator.equiv(transpiled_operator) + if n_backend_qubits < circuit.num_qubits: + pytest.skip("Circuit does not fit the device") + # Use layout_method="trivial" to avoid initial qubit remapping. + # Use fixed seed so that the test is deterministic. + if method == "native_integration": + # Use optimization_level=0 to enforce equivalence up to global phase. + transpiled_circuit = transpile(circuit, backend, optimization_level=0, seed_transpiler=123) + elif method == "qiskit": # Debug case to check if the issues lies with the Qiskit transpiler. + transpiled_circuit = transpile(circuit, target=backend.target, optimization_level=0, seed_transpiler=123) + + else: + # Use remove_final_rzs=False to avoid equivalence up to global phase. + transpiled_circuit = transpile_to_IQM( + circuit, + backend, + optimization_level=0, + remove_final_rzs=False, + seed_transpiler=123, + ) + # Fix the dimension of the original circuit to agree with the transpiled_circuit + padded_circuit = QuantumCircuit(transpiled_circuit.num_qubits) + padded_circuit.compose(circuit, range(circuit.num_qubits), inplace=True) + circuit_operator = Operator.from_circuit(padded_circuit) + print() + print("After transpile", transpiled_circuit.layout) + if "move" in transpiled_circuit.count_ops(): + # Replace the move gate with the iSWAP unitary. + transpiled_circuit_without_moves = IQMReplaceGateWithUnitaryPass("move", MOVE_GATE_UNITARY)( + transpiled_circuit + ) + initial_layout = transpiled_circuit.layout.initial_layout if transpiled_circuit.layout else None + final_layout = transpiled_circuit.layout.final_layout if transpiled_circuit.layout else None + transpiled_operator = Operator.from_circuit( + transpiled_circuit_without_moves, + ignore_set_layout=True, + layout=initial_layout, + final_layout=final_layout, + ) + else: + transpiled_operator = Operator.from_circuit(transpiled_circuit, ignore_set_layout=False) + print(circuit) + print(transpiled_circuit) + # TODO figure out why the transpiled circuit is not always semantically equivalent to the original one when the GHZ is a ladder rather than a star. + assert circuit_operator.equiv(transpiled_operator) @pytest.mark.parametrize('optimization_level', list(range(4))) def test_valid_result(self, circuit, backend, method, optimization_level): """Test that transpiled circuit has gates that are allowed by the backend""" + # Only run the test if the circuit fits the device + if n_backend_qubits < circuit.num_qubits: + pytest.skip("Circuit does not fit the device") if method == "native": transpiled_circuit = transpile(circuit, backend, optimization_level=optimization_level) else: @@ -187,6 +191,9 @@ def test_valid_result(self, circuit, backend, method, optimization_level): def test_transpiled_circuit_keeps_layout(self, circuit, backend, method): """Test that the layout of the transpiled circuit is preserved.""" + # Only run the test if the circuit fits the device + if n_backend_qubits < circuit.num_qubits: + pytest.skip("Circuit does not fit the device") # TODO implement pytest.xfail("Not implemented yet")